검증 원리
무엇을 재는가
여섯 가지를 잽니다. 어느 것도 단독으로는 결론이 되지 못하고, 서로 어긋나는지를 볼 때만 쓸모가 있습니다.
출발점: 우리는 무엇을 증명할 수 없는가
사진이 진짜인지 파일만 보고 알아내는 방법은 없습니다. 이건 기술이 부족해서가 아니라 정보가 없기 때문입니다. 파일은 촬영 현장을 담고 있지 않습니다. 파일이 담고 있는 것은 어떤 과정을 거쳐 이 바이트 배열이 되었는가에 관한 부수적인 흔적뿐입니다.
그래서 질문을 바꿉니다. “이 사진은 진짜인가”가 아니라 “이 파일에 카메라를 지난 흔적이 남아 있는가, 그리고 그 흔적들이 서로 같은 이야기를 하는가”로. 이 질문에는 답할 수 있습니다. 측정 가능한 양들이기 때문입니다.
1. 촬영 정보
JPEG 파일 앞부분에는 EXIF라는 영역이 있습니다. 카메라가 촬영 직후에 적어 넣는 메모입니다. 제조사, 모델, 렌즈, 조리개, 셔터 속도, ISO, 초점 거리, 촬영 시각이 들어갑니다.
EXIF는 그냥 텍스트입니다. 무료 프로그램으로 3초면 고칩니다. 그러니 EXIF만 보고 무언가를 판단할 수는 없습니다. 여기서 EXIF를 읽는 이유는 믿기 위해서가 아니라, 대조할 주장을 확보하기 위해서입니다. “ISO 6400으로 찍었다”는 주장이 있어야 픽셀에게 되물을 수 있습니다.
GPS는 존재 여부만 확인하고 좌표는 읽지 않습니다. 좌표값은 화면에 표시되지 않고 어떤 변수에도 남기지 않습니다. 사진의 진위를 확인하는 데 촬영 장소는 필요하지 않은데, 유출되면 사람이 다칠 수 있는 종류의 정보이기 때문입니다.
2. EXIF와 픽셀의 정합성
이 서비스의 핵심입니다.
EXIF를 고치는 일은 쉽습니다. 그런데 EXIF에 적은 대로 픽셀을 고치는 일은 어렵습니다. “ISO 6400”이라고 적어 놓으려면 ISO 6400짜리 노이즈가 화면 전체에 밝기에 따라 정확한 비율로 앉아 있어야 합니다. 텍스트 한 줄 고치는 것과는 난이도가 다릅니다. 그래서 EXIF와 픽셀을 서로의 증인으로 씁니다.
ISO 대 실측 노이즈
화면에서 휘도 40 이하의 어두운 패치들을 골라 노이즈의 크기를 잽니다. 어두운 곳을 고르는 이유는 그곳이 노이즈가 가장 잘 보이는 자리이기 때문입니다. 밝은 곳에서는 신호가 커서 같은 양의 노이즈도 눈에 덜 띕니다.
측정에는 3×3 라플라시안 마스크의 절대 응답 평균을 쓰는 방식(Immerkær 추정)을 씁니다. 단순히 표준편차를 재면 피사체의 질감까지 노이즈로 세어 버리는데, 이 방식은 완만한 밝기 변화에 반응하지 않아 질감의 영향이 덜합니다. 그래도 완전히 분리되지는 않아서, 구간별 대표값은 평균이 아니라 낮은 분위수를 씁니다. 질감은 노이즈 추정치를 키우는 방향으로만 오염시키기 때문입니다.
ISO가 높게 적혀 있는데 어두운 부분이 매끈하면 모순 플래그를 세웁니다. 다만 이 조건은 아주 좁게 잡아 두었습니다. 강한 노이즈 제거를 건 실제 사진도 똑같이 매끈하기 때문입니다.
노이즈와 신호의 관계
이쪽이 더 중요합니다. 휘도를 여덟 구간으로 나누고, 각 구간에서 노이즈의 크기를 따로 잽니다. 그러면 밝기에 따른 노이즈 곡선이 나옵니다.
실제 센서에서 이 곡선은 반드시 올라갑니다. 빛이 알갱이로 도착하기 때문입니다. 밝은 곳에는 광자가 많이 오고, 광자의 개수는 포아송 분포를 따르므로 표준편차가 평균의 제곱근에 비례합니다. 즉 밝을수록 노이즈의 절대량이 커집니다. 이건 카메라의 설계와 무관한 물리입니다.
그래서 이 곡선이 평평하거나 뒤집혀 있으면 센서 출력으로 설명하기 어렵습니다. 다만 여기서도 조심해야 합니다. 휴대폰의 촬영 시 보정은 어두운 곳의 노이즈를 집중적으로 지우기 때문에 곡선을 평평하게 만듭니다. 그래서 평평한 곡선 하나만으로는 아무 결론도 내지 않습니다.
3. 광학 흔적
렌즈는 유리 덩어리입니다. 완벽할 수 없고, 그 불완전함이 화면에 남습니다.
비네팅
화면 중앙 영역의 평균 휘도와 네 모서리의 평균 휘도를 비교합니다. 실제 렌즈에서는 모서리가 어둡습니다. 빛이 비스듬히 들어오면서 유효 개구가 줄고(코사인 네제곱 법칙), 경통이 주변부 광선을 일부 가리기 때문입니다.
다만 이 값은 피사체에 크게 흔들립니다. 위쪽에 하늘이 있으면 위 두 모서리가 밝아집니다. 요즘 카메라는 비네팅을 촬영 순간 보정해서 아예 지워 버리고, 크롭한 사진에는 원래의 주변부가 남아 있지 않습니다. 그래서 비네팅은 “있으면 렌즈를 지났다”는 쪽으로만 읽고, 없다고 해서 감점하지 않습니다.
색수차
유리의 굴절률은 파장마다 조금씩 다릅니다. 빨강과 파랑이 정확히 같은 자리에 맺히지 않는다는 뜻입니다. 그래서 고대비 경계선을 확대해 보면 R 채널의 경계와 B 채널의 경계가 미세하게 어긋나 있습니다.
이 값을 재려면 원본 해상도가 필요합니다. 사진을 줄이면 인접 화소가 평균되면서 서브픽셀 단위의 어긋남이 사라지기 때문입니다. 그래서 축소본이 아니라 원본 배율의 타일에서 잽니다. 고립된 강한 경계선만 골라, 그 주변에서 R과 B의 기울기 무게중심 위치를 각각 구하고 차이를 냅니다. 보통 0.1픽셀에서 1픽셀 사이의 값이 나옵니다.
초점의 연속성
화면을 8×8로 나누고 칸마다 라플라시안 분산으로 선명도를 잽니다. 그러면 선명도 지도가 나옵니다.
실제 사진에서 이 지도는 부드럽게 변합니다. 초점면에서 멀어질수록 흐려지고, 거리는 연속적으로 변하기 때문입니다. 붙어 있는 두 칸이 완전히 다른 선명도를 갖는 일은 그 경계에 피사체의 가장자리가 놓여 있을 때뿐입니다. 지도 전체가 칸마다 튀면 하나의 광학계에서 한 번에 나온 화면으로 보기 어렵습니다.
이 측정은 배경이 크게 흐려진 사진에서 오판하기 쉽습니다. 그래서 단독으로는 판정을 바꾸지 않습니다.
4. 압축 이력
JPEG는 화면을 8×8 블록으로 잘라 블록마다 따로 압축합니다. 블록끼리 계산이 독립적이라 경계선에 아주 미세한 단차가 남습니다. 이 단차의 세기를 재면 파일이 몇 번 저장됐는지 짐작할 수 있습니다.
가로 방향으로 인접 화소의 밝기 차이를 전부 구한 다음, 그 위치를 8로 나눈 나머지별로 모아 평균을 냅니다. 압축 격자가 살아 있으면 특정 나머지에서만 값이 튑니다. 자르거나 크기를 바꾼 뒤 다시 저장한 파일에서는 첫 번째 격자와 어긋난 위치에 두 번째 봉우리가 생깁니다. 격자가 아예 보이지 않으면 무손실로 저장됐거나 리사이즈로 지워진 것입니다.
영역마다 격자 세기가 크게 다르면 일부만 다른 이력을 가졌다는 뜻이 될 수 있습니다. 다만 매끈한 하늘과 빽빽한 나뭇잎도 격자 세기를 크게 벌려 놓기 때문에, 이 신호 역시 단독으로는 판정을 바꾸지 않습니다.
5. 화면 재촬영 신호
여기부터는 기계가 할 수 있는 일이 없습니다.
생성된 이미지를 모니터에 띄우고 진짜 카메라로 촬영하면, EXIF도 진짜고 노이즈도 진짜고 렌즈 흔적도 진짜입니다. 앞의 검사를 전부 통과합니다. 이건 결함이 아니라 자동 검증이라는 방법 자체의 한계입니다.
그래서 확정하지 않고 플래그만 세웁니다. 두 가지를 봅니다. 하나는 모아레입니다. 모니터의 화소 격자와 센서의 화소 격자가 겹치면 간섭 무늬가 생기고, 주파수 영역에서 규칙적인 봉우리로 나타납니다. 원본 배율 타일에 해닝 창을 씌우고 2차원 FFT를 걸어 배경 대비 봉우리의 높이를 셉니다. 다른 하나는 평면성입니다. 평면을 찍었으므로 화면 전체의 선명도가 이상할 만큼 균일합니다.
둘 다 실제 피사체에서도 나옵니다. 직물, 벽돌, 방충망, 인쇄물 접사. 그래서 이 신호는 등급을 깎지 않습니다. 판단은 2등급에서 사람이 합니다.
6. 파일 지문
파일 전체의 SHA-256 해시를 Web Crypto로 계산합니다. 나중에 “이 파일이 그때 그 파일인지”를 대조할 때 씁니다. 한 바이트만 달라도 값이 완전히 바뀝니다.
지문은 바이트 배열을 가리킬 뿐, 사진의 내용이나 출처를 증명하지 않습니다. 함께 기록되는 시각은 사용자 기기의 시계를 읽은 값이고, 기기 시계는 바꿀 수 있습니다. 믿을 수 있는 시각 증명은 서버 없이는 불가능하며, 이 서비스에는 서버가 없습니다.
측정값을 숨기지 않는 이유
결과 화면에는 무엇을 쟀고 얼마가 나왔는지 전부 적습니다. 등급만 툭 던지고 근거를 감추면 그건 인증이 아니라 점집입니다. 이용자가 측정값을 보고 “아, 이건 내가 크롭을 심하게 해서 그렇구나” 하고 스스로 판단할 수 있어야 합니다.
다만 합격선 숫자와 가중치는 코드에만 두고 화면에 내보내지 않습니다. 이건 감추는 것이 아니라, 공개하면 그 선에 딱 맞춘 파일을 만드는 일이 쉬워지기 때문입니다. 어차피 자동 검증은 우회 가능하고 그 사실도 적어 두었습니다. 다만 우회를 한 단계 더 번거롭게 만드는 정도의 값어치는 있습니다.
계산은 어디서 이루어지나
전부 브라우저 안에서 이루어집니다. 이미지는 createImageBitmap으로 메모리에 올라가고, Canvas에서 픽셀을 읽고, 계산이 끝나면 사라집니다. 업로드 엔드포인트가 없고 서버가 없습니다. 검증 중에 네트워크 탭을 열어 두시면 아무 요청도 나가지 않는 것을 보실 수 있습니다.
외부 컴퓨터 비전 라이브러리는 쓰지 않습니다. 노이즈 추정, 색수차, 블록 격자, FFT까지 전부 직접 구현돼 있습니다. 코드 분량을 줄이기 위해서가 아니라, 무엇을 재고 있는지 한 줄씩 설명할 수 있어야 하기 때문입니다.
여기 적힌 방법으로 걸러지지 않는 경우들은 한계 페이지에 따로 정리해 두었습니다. 읽어 보시길 권합니다.