Skip to content
Published on

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

공유하기
Authors

사진처럼 보이는 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모듈에 대응한다는 서술은 원 자료의 숫자에서 제가 유도한 것입니다.