0%

Unity

"아이가 색칠한 그림이 화면에서 살아 움직인다" — 스캐너 한 대로 만든 인터랙티브 아트의 비하인드

김*겸··수정됨 2026.07.19

"아이가 색칠한 그림이 화면에서 살아 움직인다" — 스캐너 한 대로 만든 인터랙티브 아트의 비하인드

프로젝트가 뭔데?

경기도 생태 교육 프로그램용 인터랙티브 컬러링 시스템이다.

동작은 이렇다:

  1. 아이가 활동지(수리부엉이, 금개구리, 맹꽁이 등 경기도 멸종위기종 11종)를 색칠한다

  2. 스캐너에 넣는다

  3. 화면에 자기가 칠한 색 그대로 입혀진 캐릭터가 하고 나타나서 움직인다

아이 입장에서는 마법이다. 개발자 입장에서는 지옥이다.


전체 파이프라인 — 단순해 보이지만 단순하지 않은

스캐너 폴더 감시 (ScanFolderWatcher)
    ↓
QR코드 읽기 → 어떤 동물인지 판별
    ↓
이미지 방향 자동 보정 (ImageOrientationCorrector)
    ↓
코너 마커 4개 검출 (CornerMarkerDetector) ← 여기가 지옥
    ↓
스케치 영역 크롭 + 색상 추출 (ColorExtractor)
    ↓
베이스 캐릭터에 색상 합성 (ScanProcessor)
    ↓
스폰 이펙트와 함께 등장! (SpawnEffect + AnimalModelManager)

"스캔해서 색 입히면 되는 거 아냐?"라고 생각했다. 아니었다.


1단계: 스캐너는 생각보다 멍청하다

ScanFolderWatcher — 파일 하나 읽는 게 이렇게 어려울 줄이야

스캐너가 파일을 저장하는 과정은 원자적이지 않다. JPEG 파일이 생성됐다고 이벤트가 오는데, 열어보면 아직 다 안 써졌다. 0바이트거나, 반쪽짜리 이미지다.

결국 이렇게 됐다:

  • 파일 읽기 10회 재시도, 50ms 간격

  • 최소 800×600 이상인지 사이즈 검증

  • 가로세로비 0.6~2.0 범위 검증 (썸네일이나 깨진 이미지 필터링)

  • 10초 타임아웃으로 유효한 이미지 대기

  • 잘못된 스캔이면 경고음 재생 → 다시 넣으라는 신호

파일 하나 안정적으로 읽는 데 264줄이 필요했다.


2단계: QR코드의 배신

QRReader — "왜 못 읽어?"의 반복

활동지 상단에 QR코드가 있다. 이걸 읽어야 어떤 동물인지 안다. ZXing 라이브러리를 붙였는데, 잘 안 읽힌다.

왜?

  • 스캔 이미지가 3000px급이라 ZXing이 타임아웃

  • 아이가 QR 위에 색칠을 해버리면 콘트라스트 저하

  • 활동지를 뒤집어 넣으면 QR이 엉뚱한 위치

해결:

  • 2000px 이상이면 다운샘플링 후 인식

  • 실패하면 히스토그램 스트레칭(밝기 595 퍼센타일을 0255로 확장)으로 콘트라스트 강제 보정 후 재시도

  • 이것만으로 인식률 50% 향상


3단계: "활동지가 거꾸로 들어갔잖아" 문제

ImageOrientationCorrector — 4방향 자동 감지

아이들은 활동지를 아무 방향으로 스캐너에 넣는다. 위아래 거꾸로, 좌우 반전, 180도 회전... 총 4가지 경우의 수.

QR코드 위치로 방향을 판별한다:

  • 우상단 → 정상

  • 좌상단 → 좌우 반전

  • 우하단 → 상하 반전

  • 좌하단 → 180도 회전

3단계 탐색 전략을 썼다:

  1. 이미지 4개 코너 영역(45%)에서 각각 QR 탐색

  2. 실패하면 더 넓은 영역(55%)으로 재탐색

  3. 그래도 실패하면 전체 이미지에서 탐색

여기서도 콘트라스트 보정 폴백이 붙어있다. 집요함의 결정체.


4단계: 마커 검출 지옥 — 이 프로젝트의 보스몹

CornerMarkerDetector — 712줄의 처절한 사투

활동지에는 그림 영역을 표시하는 코너 마커(■) 4개가 있다. 이걸 찾아야 스케치 영역만 정확히 잘라낼 수 있다.

"까만 사각형 4개 찾으면 되는 거 아냐?"

아니다. 여기가 이 프로젝트에서 가장 많이 머리를 박은 곳이다.

함정 1: 마커가 코너에 없다

활동지 레이아웃을 보면, 상단 마커는 이미지 모서리가 아니라 y=65~75% 높이에 있다. 상단에 제목과 QR이 있기 때문이다. "코너 마커"라는 이름에 속으면 안 된다.

함정 2: QR코드가 마커인 척 한다

QR코드도 검은 점이 밀집된 영역이다. 밀도가 ~50% 정도로, 마커(~90%)보다 낮지만 단순 임계값으로는 구분이 어렵다. QR이 있는 우상단 y>82% 영역을 탐색에서 제외하는 하드코딩이 필요했다.

함정 3: 배경이 매번 다르다

스캐너 뚜껑 색, 책상 위 종이, 검은 천 배경... 매번 다르다.

해결: Otsu 이진화로 종이 영역 사전 분리.
HSV 밝기 채널에서 종이(밝음)와 배경(어두움)을 분리한다. 1/4 해상도로 다운샘플링해서 속도도 챙겼다.

함정 4: 인쇄 품질이 들쭉날쭉

어떤 활동지는 마커가 진하고, 어떤 건 흐리다.

해결: 3단계 적응형 임계값.

  1. Strict — 까다로운 기준으로 먼저 시도

  2. Relaxed — 못 찾은 마커만 느슨한 기준으로 재시도

  3. Permissive — 그래도 못 찾으면 최대한 관대하게

함정 5: 마커를 3개밖에 못 찾았을 때

아이가 마커 위에 색칠을 해버리거나, 종이가 찢어지면 4개 다 못 찾는다.

해결: 기하학적 복원.

  • 3개 → 평행사변형으로 4번째 추정

  • 2개 → 활동지 종횡비(0.7)로 사각형 추정

  • 대각선 쌍이면 → 직사각형 가정

최종 무기: 적분 이미지(Integral Image) + 2단계 슬라이딩 윈도우

모든 픽셀을 일일이 확인하면 너무 느리다. 적분 이미지를 미리 계산해두면, 임의 영역의 픽셀 합을 O(1)에 구할 수 있다.

  1. Coarse Search — 윈도우 크기의 1/3 간격으로 대략적 위치 파악

  2. Fine Search — 찾은 위치 주변에서 1픽셀 간격으로 정밀 탐색

712줄짜리 스크립트 하나에 적분 이미지, Otsu 이진화, 다중 패스 임계값, 밀도 기반 윈도우 탐색, 부분 복원 로직이 전부 들어있다. 이 프로젝트의 핵심이자 악몽이다.


5단계: 색을 입히는 기술

ScanProcessor — 두 이미지의 결혼

베이스 캐릭터(깨끗한 외곽선)와 스캔 이미지(아이가 색칠한 것)를 합성한다.

원리는 의외로 단순하다:

  • 어두운 픽셀(V < 0.5) = 외곽선 → 베이스 유지

  • 채도 낮은 픽셀 = 종이 흰색/회색 → 스킵

  • 밝고 채도 높은 픽셀 = 아이가 칠한 색 → 적용!

하지만 정렬이 문제다. 스캔 영역과 베이스 캐릭터의 크기·위치가 다르다. 바운딩 박스 좌표 매핑으로 정규화한다.

그리고 동물 11종마다 전부 다른 파라미터가 필요했다:

  • 최소 채도, 외곽선 임계값, 최소 밝기

  • X/Y 오프셋, X/Y 스케일

  • Y축 플립 여부

  • 머리/몸통 분리 높이

이걸 인스펙터에서 실시간 조정 가능하게 만들어서, 매 프레임 값이 바뀌면 즉시 재합성한다. 스캔 안 하고도 튜닝할 수 있다.


대반전: 3D에서 2D로의 급선회

초기 설계는 3D였다. 캡슐 메시에 Triplanar 쉐이더로 색상을 투영하는 방식. UV 언래핑, 메시 스키닝, 3D 모델링이 필요했다.

만들다 보니 깨달았다:

  • 활동지 자체가 2D 컬러링북인데, 굳이 3D?

  • 3D 투영은 뒤에서 보면 색이 늘어지거나 깨진다

  • 아이들은 정면만 본다

과감하게 2D로 전환했다. SpriteRenderer 기반으로 바꾸니 코드가 절반으로 줄고, 결과물은 더 깔끔했다. 삭제한 파일: AnimalWander.cs, SpawnParticleController.cs, 3D 쉐이더 전부.

그런데 또 반전이 있다. 2D로 갔다가, AnimatedColorProjection 쉐이더를 추가했다. GPU에서 실시간으로 스키닝된 메시 좌표를 rest-pose 기준으로 정규화해서, 매 프레임 UV를 재계산하는 방식. CPU 부하 없이 애니메이션 중에도 색이 정확히 따라붙는다. 3D의 장점은 취하고 단점은 버린 하이브리드가 최종 형태.


스폰 이펙트 — 마법의 순간

아이가 스캔하면 캐릭터가 바로 나타나는 게 아니라, 파티클이 응축되면서 형태를 갖춰가는 이펙트가 재생된다 (SpawnEffect.cs, 914줄). 퇴장할 때는 반대로 흩어지는 DespawnEffect(759줄).

"등장하는 순간"이 이 프로젝트의 감동 포인트다. 아이가 자기가 칠한 캐릭터가 화면에서 탄생하는 걸 보는 순간의 표정 — 그게 이 모든 삽질의 이유다.


배운 것들

  1. "단순한 문제"는 없다 — "마커 4개 찾기"가 712줄짜리 괴물이 된다

  2. 현실 세계는 더럽다 — 아이들은 마커 위에 색칠하고, 종이를 거꾸로 넣고, 스캐너 뚜껑은 매번 다른 색이다

  3. 폴백의 폴백의 폴백 — QR 인식 실패 → 히스토그램 보정 → 재시도 → 전체 탐색. 마커 검출 실패 → 3단계 임계값 → 부분 복원 → 수동 크롭. 한 겹이 아니라 세 겹의 안전망

  4. 과감한 피봇이 답이다 — 3D에 집착했으면 완성 못 했다. 2D로 바꾼 결정이 프로젝트를 살렸다

  5. 도메인 지식이 코드를 결정한다 — "마커가 y=65~75%에 있다"는 사실은 코드만 봐서는 절대 모른다. 실제 활동지를 수십 장 스캔해봐야 안다


기술 스택 한눈에

구분기술엔진Unity 2D, URP언어C#QR 인식ZXing.NET이미지 처리자체 구현 (Otsu, 적분 이미지, HSV 분석)쉐이더HLSL (AnimatedColorProjection)파일 감시FileSystemWatcher + 재시도 로직


마치며

총 코드량 약 4,000줄 이상, 커밋 9회, 3D→2D 피봇 1회, 쉐이더 6종, 동물 11종.

겉으로 보면 "스캔해서 색칠 입히는 앱"이지만, 안에는 컴퓨터 비전의 현실과 싸운 흔적이 가득하다.

완벽하지 않다. 마커를 하나도 못 찾으면 수동 크롭으로 넘어가고, QR이 완전히 가려지면 동물 판별이 안 된다. 하지만 실패하더라도 조용히 차선책으로 넘어가는 시스템을 만들었다. 그게 현실 세계 프로젝트의 진짜 기술이라고 생각한다.

아이들은 이 뒤에 712줄짜리 마커 검출기가 있는지 모른다. 그냥 "내가 칠한 수리부엉이가 움직여!"라고 소리지를 뿐이다.

그 한마디면 됐다.


읽기 도구

9분 읽기

이 글이 도움이 되었나요?

다음으로 읽기