Skip to content

Split View: 사진이 들어간 QR 코드는 무엇을 대가로 지불하는가 — 오류 정정은 예산입니다

✨ Learn with Quiz
|

사진이 들어간 QR 코드는 무엇을 대가로 지불하는가 — 오류 정정은 예산입니다

사진처럼 보이는 QR 코드를 보고 드는 의심

흑백 사진처럼 생겼는데 스캔하면 링크가 열리는 QR 코드를 본 적이 있을 겁니다. 가운데 로고가 박힌 정도가 아니라 코드 전체가 그림입니다. 처음 보면 대단해 보이고, 두 번째로 드는 생각은 대개 이겁니다. 저게 어떻게 읽히지.

답은 예상보다 단순하고, 단순해서 오히려 중요합니다. 읽히는 이유가 아니라 무엇을 대가로 지불했는지가 실무에서 알아야 할 부분입니다. 이 글은 Andrew T.가 공개한 오차 확산 QR 코드 생성기의 설명 문서를 따라가면서, 그 대가가 무엇인지 짚습니다.

QR 코드는 두 종류의 칸으로 되어 있습니다

원 자료의 설명은 QR 코드를 두 부분으로 나눕니다. 하나는 기능 패턴입니다. 모서리의 굵은 사각형 같은 도형들이고, 스캐너가 코드를 찾고 방향과 크기를 잡는 데 씁니다. 다른 하나는 데이터 모듈입니다. 나머지 전부이고, 실제 데이터와 헤더가 들어갑니다.

핵심은 읽는 순서입니다. 스캐너는 먼저 기능 패턴으로 코드의 좌표계를 확정하고, 그다음에 데이터 모듈을 읽습니다. 그래서 기능 패턴은 아주 또렷해야 하고, 데이터 모듈은 상대적으로 손댈 여지가 있습니다. 브랜드들이 만드는 개성 있는 QR 코드가 이 여지를 쓰는 것이고, 자료의 표현대로 스캔 강인성이 좀 떨어지긴 해도 꽤 많이 손대도 대체로 읽힙니다.

사진을 넣는 자리 — 모듈 하나를 아홉 칸으로 쪼개기

여기서 나오는 기법이 재미있습니다. 원 자료가 소개하는 방식은 모듈 하나를 3×3 격자로 쪼개고, 가운데 한 칸만 원래 데이터 색으로 두고 나머지 여덟 칸을 사진에 내주는 것입니다.

이게 통하는 이유가 앞의 순서에 있습니다. 스캐너는 기능 패턴이 알려 준 위치를 기준으로 각 모듈의 중심을 봅니다. 중심만 맞으면 주변은 자유입니다. 그래서 격자 한 칸 안에서 중심 한 칸만 지키고 여덟 칸을 그림으로 채우면, 코드도 읽히고 그림도 보입니다.

결과물은 저해상도의 1비트 흑백 사진입니다. 원 자료가 예로 든 이미지는 147×147 화소입니다. 이 숫자를 3으로 나누면 49가 나오는데, QR 코드의 한 변이 49모듈인 경우와 맞아떨어집니다. 즉 모듈 49개를 각각 3칸으로 쪼갠 결과라는 뜻입니다.

그래서 아홉 칸 중 하나가 무작위입니다

이 방식에는 피할 수 없는 잡음이 따라옵니다. 각 모듈의 중심 한 칸은 사진이 무엇이든 상관없이 데이터가 정한 색이어야 합니다. 데이터는 사진과 무관하니, 그 한 칸의 색은 그림 입장에서 사실상 무작위입니다.

원 자료는 이것을 정확히 이렇게 표현합니다. 화소 아홉 개 중 하나가 사실상 무작위 색이라는 것입니다. 그래서 결과 이미지에 소금과 후추를 뿌린 듯한 잡티가 생깁니다. 147×147짜리 1비트 이미지라 원래도 거칠지만, 거칠기의 상당 부분이 이 아홉 분의 일에서 옵니다.

여기서 보통 떠올리는 해법이 하나 있습니다. 그 중심 화소들 중 몇 개를 그림에 맞게 바꿔 버리는 것입니다. QR 코드에는 오류 정정이 있으니 몇 개쯤은 틀려도 읽히기 때문입니다. 가운데 로고가 박힌 QR 코드가 정확히 그 원리로 동작합니다. 그런데 원 자료의 결론은 다릅니다. 뒤에서 보겠습니다.

디더링 — 이진화 대신 오차를 흘려보내기

먼저 그림 쪽 이야기를 정리해야 합니다. 밝기를 흑백 두 값으로 줄일 때 단순 이진화를 쓰면 중간 톤이 전부 사라집니다. 그래서 보통은 격자무늬로 중간 톤을 흉내 내는데, 규칙적인 격자는 그 나름의 무늬가 눈에 띕니다.

플로이드-스타인버그 오차 확산은 다르게 접근합니다. 원 자료의 설명을 그대로 옮기면 이렇습니다. 왼쪽 위에서 시작해 화소를 평소대로 이진화합니다. 밝기가 50퍼센트를 넘으면 흰색, 아니면 검은색입니다. 어떤 화소가 70퍼센트였다면 흰색으로 만들면서 30퍼센트만큼 밝기를 더 준 셈이 됩니다. 이 30퍼센트를 아직 처리하지 않은 주변 화소들에 나눠서 그만큼 어둡게 만듭니다. 그 화소들을 이진화할 때 그 값이 반영됩니다.

전체를 다 돌고 나면 이미지의 각 부분이 평균적으로는 원래 밝기에 더 가까워집니다. 그리고 격자무늬 대신 불규칙한 패턴이 나와서 덜 거슬립니다.

핵심 아이디어 — 데이터 모듈의 오차를 먼저 흘린다

이제 두 이야기가 만납니다. 원 자료의 진짜 아이디어는 오차 확산을 두 번 돌리는 것입니다.

두 번째 통과는 평범한 플로이드-스타인버그입니다. 첫 번째 통과가 다릅니다. 데이터 모듈의 중심 화소들만 대상으로 합니다. 그 화소들의 색은 이미 정해져 있으니, 정해진 색으로 칠하고 그때 생긴 오차를 주변 여덟 칸에 흘려보냅니다.

여기서 오차가 아주 커질 수 있습니다. 보통의 디더링에서는 우리가 색을 고르므로 오차가 50퍼센트를 넘지 않습니다. 그런데 이 경우에는 아주 어두운 영역의 화소를 순백으로 만들어야 할 수도 있고, 그러면 95퍼센트짜리 오차가 생깁니다. 원 자료는 이것이 나쁜 일처럼 들리지만 바로 그래서 흘려보내는 것이 중요하다고 말합니다. 그냥 받아들이면 그 자리에 잡티로 남고, 흘려보내면 주변이 그만큼 어두워지면서 평균이 맞습니다.

그래서 앞에서 미뤄 둔 질문의 답이 나옵니다. 원 자료의 결론은 데이터 모듈 몇 개를 그림에 맞게 바꾸는 방식이 화질에는 별로 도움이 안 되면서 스캔 성능은 심하게 깎는다는 것입니다. 오차를 제대로 흘려보내면 색을 바꿀 필요 자체가 거의 없어지기 때문입니다.

여유분은 예산입니다

이 지점이 이 글에서 가장 하고 싶은 이야기입니다. 화질을 올리는 축과 오류 정정 여유분을 쓰는 축은 다른 축입니다. 두 번째 오차 확산은 여유분을 한 톨도 쓰지 않고 그림을 개선합니다. 반면 데이터 모듈을 바꾸는 것은 여유분을 직접 태웁니다.

여유분이 무엇을 위해 있는지 생각하면 이 구분이 왜 중요한지 분명해집니다. QR 코드의 오류 정정 수준은 네 단계이고, 규격을 만든 덴소웨이브는 그중 가장 많이 쓰이는 수준이 15퍼센트를 복원한다고 안내합니다. 수준을 올리면 복원력이 오르지만 같은 데이터에 더 큰 코드가 필요해집니다. 그리고 그 복원력은 원래 인쇄 불량과 구김과 얼룩과 비스듬한 각도와 어두운 조명을 위해 남겨 둔 것입니다.

즉 예쁘게 만드느라 여유분을 쓴다는 것은, 현장의 나쁜 조건에 대비한 예산을 디자인으로 옮겨 쓰는 일입니다. 원 자료도 같은 결론에 이릅니다. 큰 화면이나 포스터에 붙일 코드라면 괜찮지만, 구겨질 수 있는 종이 전단이라면 그 여유분이 필요해진다는 것입니다. 그리고 한 문장을 덧붙입니다. 내 폰으로 노트북 화면에서 잘 읽힌다고 해서, 낯선 사람의 낡은 폰이 어두운 곳에서 인쇄물을 읽어낸다는 뜻은 아닙니다.

실제로 쓸 때 걸리는 것 — 여백과 확대

원 자료가 마지막에 붙여 둔 실무 주의사항 두 개도 그대로 옮길 값이 있습니다.

첫째는 여백입니다. 생성기는 여백 없는 작은 이미지를 만들어 주는데, QR 코드가 안정적으로 읽히려면 바깥에 여백이 필요합니다. 그리고 그 여백의 색은 모서리 큰 사각형 안쪽 색의 반대여야 합니다. 보통은 흰색이지만 반전된 코드를 만들었다면 검은 배경이 필요합니다.

둘째는 확대입니다. 브라우저는 작은 이미지를 크게 표시할 때 기본적으로 흐리게 보간합니다. 흐려진 경계는 스캐너가 모듈 중심을 판정하기 어렵게 만듭니다. CSS에서 보간을 끄지 않으면 이 문제가 조용히 생깁니다.

두 항목의 공통점이 있습니다. 만든 사람의 화면에서는 멀쩡하고, 쓰는 사람의 환경에서만 실패합니다. 예쁜 QR 코드의 실패는 대부분 이런 모양으로 옵니다.

정리와 출처

QR 코드에 그림을 넣는 것은 자유입니다. 다만 그것이 무엇에서 빌린 자유인지는 알고 쓰는 편이 낫습니다. 스캔 실패는 우리 화면이 아니라 사용자의 손에서 일어나고, 그때는 이미 인쇄가 끝난 뒤입니다.

  • How to make error-diffused QR codes — 기능 패턴과 데이터 모듈의 구분, 3×3 분할, 아홉 분의 일 잡음, 두 번의 오차 확산, 95퍼센트 오차, 여백과 확대 주의사항이 모두 이 문서의 설명입니다.
  • 생성기소스 코드 — 직접 만들어 볼 수 있습니다.
  • 오류 정정 수준에 관한 덴소웨이브 문서 — 네 단계가 있다는 점, 가장 많이 쓰이는 수준이 15퍼센트라는 점, 수준을 올리면 코드가 커진다는 점을 확인했습니다. 나머지 세 수준의 정확한 수치는 해당 페이지에서 이미지로만 제공되어 본문에 적지 않았습니다.
  • 147을 3으로 나누면 49라는 계산과 그것이 한 변 49모듈에 대응한다는 서술은 원 자료의 숫자에서 제가 유도한 것입니다.

What Does a QR Code with a Photo Inside Pay For It — Error Correction Is a Budget

The Suspicion You Get Looking at a QR Code That Looks Like a Photo

You have probably seen a QR code that looks like a black-and-white photograph but opens a link when you scan it. Not a logo dropped in the middle — the whole code is a picture. The first time you see one it looks impressive, and the second thought is usually this: how does that even scan.

The answer is simpler than expected, and being simple is exactly why it matters. The part worth knowing in practice is not why it scans but what was paid for it. This post follows the explanation document for the error-diffused QR code generator published by Andrew T. and pins down what that price is.

A QR Code Is Made of Two Kinds of Cells

The original write-up divides a QR code into two parts. One is the function patterns. These are the shapes like the thick squares in the corners, and the scanner uses them to find the code and establish its orientation and size. The other is the data modules. That is everything else, and it holds the actual data and headers.

The key is the reading order. The scanner first fixes the coordinate system of the code from the function patterns, and only then reads the data modules. So the function patterns have to be very crisp, while the data modules leave relatively more room to be messed with. The distinctive QR codes brands make are using that room, and as the source puts it, scanning robustness does drop somewhat, but you can mess with them quite a lot and they generally still read.

Where the Photo Goes — Splitting One Module into Nine Cells

The technique that comes out of this is interesting. The approach the original introduces is to split one module into a 3×3 grid, leave only the single center cell in the color the data requires, and give the other eight cells to the photo.

The reason this works is in the ordering above. Using the position the function patterns provided, the scanner looks at the center of each module. As long as the center is right, the surroundings are free. So if within one grid cell you protect only the one center cell and fill the eight with picture, the code still reads and the picture still shows.

The result is a low-resolution 1-bit black-and-white photograph. The image the original gives as an example is 147×147 pixels. Divide that number by 3 and you get 49, which lines up with a QR code that is 49 modules on a side. In other words, it is the result of splitting 49 modules into 3 cells each.

So One Cell in Nine Is Random

This approach comes with unavoidable noise. The one center cell of each module has to be the color the data dictates, regardless of what the photo wants. The data has nothing to do with the photo, so from the picture's point of view the color of that one cell is effectively random.

The original states this exactly this way: one pixel in nine is effectively a random color. That is why the resulting image gets speckles as if salt and pepper had been sprinkled on it. A 147×147 1-bit image is coarse to begin with, but a good share of the coarseness comes from this one-in-nine.

There is a fix that usually comes to mind here: change some of those center pixels to match the picture. QR codes have error correction, so a few of them can be wrong and it still reads. A QR code with a logo in the middle works on exactly that principle. But the original comes to a different conclusion. We will get to it below.

Dithering — Diffusing the Error Instead of Thresholding

First the picture side of the story needs sorting out. If you reduce brightness to two black-and-white values with plain thresholding, all the midtones vanish. So the usual approach is to imitate midtones with a lattice pattern, but a regular lattice has a visible pattern of its own.

Floyd-Steinberg error diffusion takes a different approach. To carry the original explanation over directly: you start at the top left and threshold pixels as usual. Over 50 percent brightness becomes white, otherwise black. If some pixel was at 70 percent, making it white means you gave it 30 percent more brightness than it deserved. You divide that 30 percent among the neighboring pixels you have not processed yet and darken them by that much. When those pixels get thresholded, that value is taken into account.

Once the whole pass is done, each part of the image is on average closer to the original brightness. And instead of a lattice you get an irregular pattern, which is less objectionable.

The Key Idea — Diffuse the Error of the Data Modules First

Now the two stories meet. The real idea in the original is to run error diffusion twice.

The second pass is ordinary Floyd-Steinberg. It is the first pass that differs. It targets only the center pixels of the data modules. Their colors are already fixed, so you paint them the fixed color and diffuse the error that arises at that moment into the eight surrounding cells.

The error can get very large here. In ordinary dithering we choose the color, so the error never exceeds 50 percent. In this case, though, you might have to make a pixel in a very dark region pure white, and then you get a 95 percent error. The original says this sounds like a bad thing but that is precisely why diffusing it matters. Just absorbing it leaves a speckle right there; diffusing it darkens the surroundings by that much so the average comes out right.

And that gives the answer to the question deferred earlier. The original concludes that changing a few data modules to match the picture helps image quality very little while badly degrading scanning performance. Diffuse the error properly and the need to change colors nearly disappears.

The Slack Is a Budget

This is the point this post most wants to make. The axis of raising image quality and the axis of spending error-correction slack are different axes. The second error diffusion improves the picture without spending a single grain of slack. Changing data modules, by contrast, burns slack directly.

Think about what the slack is there for and it becomes clear why the distinction matters. QR codes have four error correction levels, and Denso Wave, which created the specification, states that the most widely used of them restores 15 percent. Raising the level raises the resilience but requires a larger code for the same data. And that resilience was set aside in the first place for print defects, creases, smudges, oblique angles, and dim lighting.

In other words, spending slack to make it pretty is moving a budget reserved for bad field conditions over into design. The original reaches the same conclusion: it is fine for a code going on a big screen or a poster, but if it is a paper flyer that can get crumpled, you are going to need that slack. And it adds one sentence. The fact that it reads fine off your laptop screen with your own phone does not mean a stranger with an old phone will get it off a printout in a dark place.

What Trips You Up in Practice — Quiet Zone and Scaling

The two practical warnings the original appends at the end are also worth carrying over as they are.

The first is the quiet zone. The generator hands you a small image with no margin, but a QR code needs a margin around it to scan reliably. And the color of that margin has to be the opposite of the color inside the big corner squares. Usually white, but if you made an inverted code you need a black background.

The second is scaling. When a browser displays a small image large, it interpolates it blurry by default. Blurred edges make it hard for a scanner to decide where the module centers are. Unless you turn interpolation off in CSS, this problem happens quietly.

The two items have something in common. They are fine on the screen of the person who made it and fail only in the environment of the person using it. Failures of pretty QR codes mostly arrive in this shape.

Wrap-Up and Sources

Putting a picture in a QR code is your prerogative. It is just better to use that freedom knowing what it was borrowed from. Scan failures happen in the user's hand, not on our screen, and by then the printing is already done.

  • How to make error-diffused QR codes — the distinction between function patterns and data modules, the 3×3 split, the one-in-nine noise, the two error diffusion passes, the 95 percent error, and the quiet zone and scaling warnings are all explanations from this document.
  • The generator and the source code — you can make one yourself.
  • The Denso Wave page on error correction levels — I confirmed there that there are four levels, that the most widely used one is 15 percent, and that raising the level makes the code bigger. The exact figures for the other three levels are provided on that page only as images, so I did not write them into this post.
  • The calculation that 147 divided by 3 is 49, and the statement that this corresponds to 49 modules on a side, are things I derived from the numbers in the original.