wiltodelta/remove-ai-watermarks) 딥다이브src/ 53개 파일 20,913줄 · PyPI remove-ai-watermarks · 호스팅판 raiw.cc)
저장소는 docs/legal-and-safety.md에서 범위를 명시한다 — 본인이 생성·편집한 콘텐츠의 AI 표식만 대상이고, 스톡 사진 미리보기·마켓플레이스 워터마크·타일 오버레이·Nightshade/Glaze 같은 작가 보호 장치는 범위 밖이다. watermark_registry.py에 스톡·에이전시 마크를 추가하는 것 자체가 기여 규칙으로 금지돼 있다.
또한 표식을 지운다고 해서 "사람이 만들었다"가 증명되지는 않는다. 제공자 서버에 남은 생성 이력·계정 연결은 그대로이고, 통계적 AI 탐지기도 따로 돈다. 나라·플랫폼에 따라 AI 표기 의무나 출처정보 제거 금지 규정이 있으니, 기술 학습 자료로 읽되 실제 사용은 각자의 법적 책임이다. 이 문서도 그 전제로 쓰였다.
이미지와 동영상을 모두 다루며, GPU 없이 되는 작업(메타데이터·보이는 로고·판별)과 CUDA가 반드시 필요한 작업(안 보이는 워터마크)이 설치 단계부터 갈라져 있다.
| 표식 종류 | 어디에 있나 | 제거 방식 | GPU |
|---|---|---|---|
| 보이는 마크 | 화면 구석 로고·글자 (Gemini 반짝이, 즉몽, 千问 등) | 찾아서 지우고 메워 넣기 (localize → inpaint) | 불필요 |
| 메타데이터 | 파일 헤더의 텍스트 (C2PA·EXIF·XMP·IPTC·TC260) | 포맷별 정밀 삭제 (픽셀 재압축 없음) | 불필요 |
| 안 보이는 워터마크 | 픽셀·주파수 영역 전체 (SynthID 등) | 이미지 전체 재생성 (diffusion / VAE 통과) | CUDA 필수 |
사진에 남은 세 가지 흔적을 지우는 일과 같다. 스티커는 떼고 그 자리를 메우면 된다(보이는 마크). 사진 뒤에 적힌 연필 메모는 지우개로 지우면 된다 — 사진 자체는 손대지 않는다(메타데이터). 그런데 종이 섬유에 눌러 새긴 자국은 지울 방법이 없어서, 사진을 통째로 다시 인화하는 수밖에 없다(안 보이는 워터마크). 세 번째만 원본이 미묘하게 달라지는 이유가 이것이다.
2026-03 생성 후 5개월 만에 별 5.2k. 배경은 Nano Banana(Gemini 이미지) 대중화다. 생성물에 반짝이 로고가 자동으로 박히기 시작하면서 "내가 만든 건데 로고가 남는다"는 수요가 폭발했다. 저장소 키워드에 nano-banana·gemini-watermark가 들어 있는 게 그 증거다.
| 흔한 워터마크 제거 도구 | 이 저장소 |
|---|---|
| "AI 워터마크 제거!" 한 버튼 | 표식 3종을 명령어부터 분리 (visible / metadata / invisible) |
| 신호가 없으면 "깨끗함"으로 표시 | 신호 없음 = Unknown. "깨끗하다"고 절대 말하지 않는다 |
| 제거 성공률을 광고 | docs/known-limitations.md에 실패 조건을 나열. SynthID는 "공개 디코더가 없어 런타임 검증 불가"라고 명시 |
| 탐지 실패 시 그냥 지움 | 탐지 못 하면 아무것도 쓰지 않는다(no output) |
이 저장소를 읽을 가치가 있게 만드는 지점. 폐기된 접근이 왜 폐기됐는지가 주석에 숫자로 남아 있다.
원래는 로고의 투명도 지도(alpha map)를 미리 캡처해 두고 원본 = (합성본 − a×로고) / (1−a)로 역산해 복원했다. 폐기 이유 둘: ⓐ 벤더가 로고를 다시 그리거나 위치를 옮기면 캡처가 통째로 무효, ⓑ 맞는 alpha map을 써도 색이 안 맞는다 — 나눗셈이 JPEG 색차 압축 오차를 1/(1−a)배로 증폭하기 때문. 사용자 제보 "색만 변하고 안 지워짐"의 정체가 이것이었다.
assume_ai 모드 — 사용자의 "이거 AI야"는 지시가 아니라 도박이었다"이 이미지는 AI다"라고 사용자가 주장하면 모든 마크의 탐지 문턱을 낮춰 주던 옵션. 문제는 그 주장이 어느 벤더의 로고가 어디에 있는지는 하나도 말해 주지 않는다는 것. 문턱만 내려간 결과, 신뢰도 하한을 두기 전 측정에서 진짜 카메라 사진의 59.8%에 있지도 않은 반짝이를 그려 메웠다. 대안은 명확했다 — 로고가 보이는 사용자는 erase --region으로 위치를 찍거나 --mark X로 이름을 대면 된다. 추측 대신 지시를 실행하게 바꾼 것이다.
| 계층 | 구성 | 비고 |
|---|---|---|
| 기본(필수) | pillow · piexif · click · c2pa 공식 리더 | numpy·torch·OpenCV 없음. 메타데이터 검사/제거만으로 쓰는 사람이 다수라 기본을 일부러 가볍게 유지 |
| 픽셀 계층 | numpy + opencv-python-headless | [pixels]. 보이는 마크 탐지·제거의 최소 단위 |
| 영상 | PyAV + 시스템 ffmpeg | [video]. PyAV는 가변 프레임 타임스탬프 유지용 |
| 학습형 인페인팅 | onnxruntime (MI-GAN / big-LaMa) | [migan]·[lama]. torch 없이 CPU에서 돈다 |
| 확산 재생성 | torch(CUDA) · diffusers · transformers 5.x · DiffSynth | [diffusion]·[qwen-zimage]. 여기서만 GPU 필요 |
| 선택 디코더 | PyWavelets(DWT-DCT) · trustmark · pillow-heif · paddleocr | 각각 [detect]·[trustmark]·[heif]·[text-draft] |
13개 extras가 서로를 참조하는 의존성 그래프로 짜여 있다(visible → pixels, video → visible, qwen-zimage → diffusion → pixels). ML 모듈 import는 전부 is_available() 가드 뒤에 있어서, GPU 패키지가 없어도 identify·metadata는 그대로 돈다. 파이썬 CLI를 만들 때 그대로 베낄 만한 패턴이다.
# pyproject.toml — diffusion extra 주석 요약
transformers>=5,<6 # diffusers 0.38이 transformers 5.x 전용 클래스를 import
tokenizers>=0.22,<0.23 # 0.23.0rc0의 CLIP 토크나이저가 SDXL 로딩을 깨뜨림
numpy>=2 (py3.13+) # numpy 1.x는 CPython 3.13 휠이 없어 소스 빌드 시도됨
"왜 이 버전에 묶였나"가 각 줄 위에 적혀 있다. 의존성 핀을 나중에 푸는 사람이 같은 함정을 다시 밟지 않게 만드는 문서화 방식.
레지스트리(watermark_registry.py)의 마크 하나하나는 (어디에 있나 · 어떻게 알아보나 · 어떻게 지우나) 세 정보를 들고 있는데, 셋째는 모든 마크가 같은 것을 공유한다. 마크별 탐지기는 이진 마스크만 만들고, 그 마스크를 하나뿐인 채우기 엔진에 넘긴다.
얼룩 제거를 "얼룩 찾는 사람"과 "천을 기워 넣는 사람"으로 나눈 것이다. 얼룩 종류가 11가지든 30가지든 기워 넣는 기술은 하나만 잘 만들면 되고, 반대로 더 좋은 기움 기술(MI-GAN·LaMa)이 나오면 11가지 얼룩에 동시에 적용된다. 앞서 폐기된 reverse-alpha는 이 분리가 없어서 마크마다 별도 복원 로직을 들고 있었고, 그래서 벤더가 로고를 바꿀 때마다 전부 깨졌다.
| 단계 | 의미 | 언제 도달하나 |
|---|---|---|
strict | 고정밀 시각 검사만. 깨끗한 이미지는 절대 안 건드림 | 기본. 근거가 없으면 여기 머무름 |
confirmed | 오탐 방지 게이트를 우회 | 같은 벤더의 증거가 있을 때만 — ① 해당 벤더의 메타데이터가 남아 있거나 ② 같은 제품의 형제 마크가 확실히 검출됨 |
핵심은 confirmed로 올라가는 조건이 "이 벤더"를 지목하는 증거뿐이라는 점. 형제 마크로 근거를 삼을 때도, 자기 탐지가 부실한 마크는 다른 마크에 면허를 넘겨줄 수 없게 별도 목록으로 막아 뒀다. 오탐이 잦은 탐지기가 게이트 우회권을 배포하는 사고를 구조로 차단한 것.
| 지키는 것 | 어떻게 |
|---|---|
| 오탐 방지 | 시간적 중재자(temporal arbiter) — 후보가 인접 프레임에서 같은 위치에 반복돼야 제거 허가. 깨끗한 영상 속 우연한 닮은꼴이 마스크가 되는 걸 막는다 |
| 깜빡임 방지 | 광학 흐름(optical flow)으로 직전 채움을 이동 정렬해 블렌드. 단, 주변 원본이 동의할 때만 적용(--no-temporal-consistency로 끔) |
| 타이밍 보존 | 가변 프레임 간격을 평균 fps로 뭉개지 않고 PyAV NUT 브리지로 PTS를 그대로 통과. 시작 타임스탬프·오디오 오프셋도 유지 |
| 색 보존 | ffmpeg 기본값(raw BGR)에 맡기지 않고 원본의 크로마 서브샘플링·컬러 태그·트랙 타임스케일을 그대로 씀. HDR/PQ/HLG·10bit 입력은 인코딩 전에 거부(조용히 8bit로 뭉개는 대신) |
| 부분 파일 방지 | 인코딩 완료본만 원자적으로 게시. 마크를 못 찾으면 출력 자체를 안 만든다 |
| 오디오 | 재인코딩 없이 stream copy, 자연 종료까지 허용 |
위 표 6줄 중 픽셀 품질에 관한 건 하나뿐이다. 나머지는 타임스탬프·색 태그·오디오 오프셋·원자성 — 결과물을 재생기에 넣었을 때 "뭔가 이상한데"를 만드는 것들이다. 영상 처리 코드를 짤 때 무엇을 체크리스트로 삼아야 하는지의 실물 예시.
| 포맷 | AI 표식 위치 | 제거 방식 |
|---|---|---|
| JPEG | EXIF·XMP·C2PA | 인코딩된 스캔 데이터를 그대로 두고 헤더만 수술 (재압축 = 화질 손실이므로) |
| MP4 / MOV | moov.udta.meta의 중국 TC260 AIGC 태그 | 박스 크기·미디어 오프셋을 바꾸지 않고 값만 공백 처리. moov가 뒤에 있는 파일, Doubao iOS 익스포트의 변종 형태까지 대응 |
| MKV / WebM | Segment.Tags.Tag.SimpleTag | 스트림 복사로 리먹스 |
| AVI | LIST/INFO/AIGC | 스트림 복사로 리먹스 |
| FLV | script.onMetaData.AIGC | 스트림 복사로 리먹스 |
이걸 위해 저장소가 컨테이너 파서를 직접 갖고 있다 — _internal/isobmff.py(MP4), ebml.py(MKV), riff.py(AVI), flv.py. 바이너리 포맷 파싱 학습 자료로 이 네 파일이 통째로 쓸 만하다.
| 모듈 | 하는 일 | 왜 흥미로운가 |
|---|---|---|
pixel_evidence.py | 이미지 1장에서 6계열 통계 추출 — DCT 계수 + 벤포드 법칙 이탈, FFT 방사 대역 + CFA 주기성(카메라 디모자이킹 흔적), 노이즈 첨도, ELA, 그래디언트, 색 히스토그램 | 제거가 아니라 포렌식 쪽. "이 사진 카메라로 찍힌 게 맞나"를 통계로 재는 교과서 |
humanizer.py | 결과물에 필름 그레인·색수차를 넣고 언샤프 마스크로 선명도 회복 | 확산 재생성이 남기는 "지나치게 매끈한 표면" 자체가 AI 티라는 관찰에서 나온 후처리 |
pixel_evidence의 artifacts는 통계값이 아니다DCT 히스토그램 같은 스칼라·고정길이 통계로는 원본을 복원할 수 없다. 그러나 같은 모듈이 함께 뱉는 artifacts(64비트 지각 해시 · 128px 썸네일 · 거친 ELA/노이즈/FFT-위상 맵)는 종류가 다르다 — 썸네일은 그림이고, 지각 해시는 원본을 식별한다. 이걸 수집·저장하는 쪽은 "통계 로그"가 아니라 원본을 특정할 수 있는 기록을 쌓고 있는 것이다.
| 순서 | 파일 | 얻는 것 |
|---|---|---|
| 1 | docs/legal-and-safety.md | 이 도구가 무엇을 하지 않기로 했는지 |
| 2 | src/.../api.py | 전체 그림 — 고수준 호출 4개가 뭘 감싸는지 |
| 3 | watermark_registry.py 모듈 독스트링 | 이 문서의 4장이 통째로 여기서 나왔다. 설계 결정과 폐기 사유가 코드 맨 위에 있음 |
| 4 | identify.py | "신호 없음"을 어떻게 정직하게 보고하는가 |
| 5 | video_visible.py 독스트링 | 영상 파이프라인이 지키는 불변식 목록 |
| 6 | docs/module-internals.md | 임계값이 왜 그 숫자인지 (측정 이력 포함) |
| 배울 것 | 어디서 | 왜 일반적으로 유용한가 |
|---|---|---|
| 탐지와 조치의 분리 | watermark_registry.py | N개 입력 유형 × 1개 처리 엔진. 유형이 늘어도 처리 코드는 안 늘고, 처리를 개선하면 N개가 동시에 좋아진다 |
| 확신 등급으로 임계값 관리 | resolve_trust | "민감도 슬라이더" 대신 증거가 있을 때만 완화. 오탐과 미탐 사이에서 정책을 설명 가능하게 만드는 패턴 |
| 선택 의존성 격리 | optional_deps.py + extras 그래프 | 무거운 ML 패키지를 안 깐 사용자도 기본 기능은 그대로 쓰게. 파이썬 CLI/라이브러리 배포의 정석 |
| 바이너리 컨테이너 파싱 | _internal/{isobmff,ebml,riff,flv}.py | MP4·MKV·AVI·FLV 구조를 코드로 배우는 가장 빠른 길. 각각 200줄 안팎 |
| 미디어 파이프라인 불변식 | video_encoding.py, video_temporal.py | VFR PTS·색 태그·오디오 오프셋·원자적 게시 — 영상 도구가 조용히 망가지는 지점들의 목록 |
| 이미지 포렌식 기초 | pixel_evidence.py | DCT/벤포드·CFA 주기성·ELA·노이즈 첨도. AI 탐지·위변조 탐지 양쪽의 공통 토대 |
| 실패를 남기는 문서화 | 모듈 독스트링 전반 | "이건 안 된다, 왜냐면 측정해 보니 59.8%" — 팀 문서를 이렇게 쓰면 같은 실수가 반복되지 않는다 |
identify는 신호를 못 찾으면 Unknown을 낸다. 로컬에서 못 읽는 워터마크(SynthID 픽셀)가 존재하기 때문이다. 탐지기를 만드는 사람이 가장 흔히 저지르는 실수가 "안 걸림 = 무해"로 보고하는 것이고, 보안 스캐너·헬스체크·품질 게이트 어디든 같은 함정이 있다. 반환 타입에 3번째 상태를 넣는 것만으로 하류 코드의 오독을 막는다.
| 하려는 것 | 필요 사양 | 설치 |
|---|---|---|
identify · metadata | 아무 데서나. numpy도 필요 없음 | remove-ai-watermarks |
visible (cv2 채움) | CPU만. 수백 MB RAM | [visible] |
visible (MI-GAN 채움) | CPU. 약 700~950MB 피크 RAM · 호출당 ~0.19초. 모델 28MB 최초 1회 다운로드 | [migan] |
visible (big-LaMa 채움) | CPU. 약 4.7GB 피크 RAM. 모델 200MB 최초 1회 다운로드 | [lama] |
| 영상 visible | CPU + 시스템 ffmpeg 필수. 전 프레임 디코드라 길이에 비례 | [video] |
invisible (이미지) | NVIDIA CUDA 필수. CPU·MPS·XPU 폴백 없음 — 생성 시점에 거부한다. VRAM 부족 시 --cpu-offload(느려짐) | [qwen-zimage] |
video invisible | GPU 권장. 512px 긴 변 · 12fps로 재생성되므로 해상도·프레임레이트·디테일이 바뀐다 | [video,diffusion] |
invisible은 이미지를 통째로 다시 생성한다. 얼굴·글자·미세 디테일이 바뀔 수 있고(그래서 SAM 마스크 얼굴 복구 단계가 따로 있다), 영상은 해상도까지 내려간다. 게다가 구글이 공개 SynthID 디코더를 내놓지 않아 로컬에서 "지워졌는지" 검증할 방법이 없다 — 저장소가 noise_std=0.15를 기본으로 삼은 근거도 2026-07-29·07-31에 제공자 검증기로 직접 돌려 본 두 건의 오라클 기록(data/evaluations/video-synthid-oracle.csv)이 전부다. 그 판정은 그 파일들에 대한 결과지 모든 이미지에 대한 보장이 아니다.
identify를 폴더 전체에 돌려 결과를 CSV로 모은 뒤, C2PA가 있는 파일 / EXIF 생성 파라미터가 있는 파일 / 아무 신호도 없는 파일 비율을 세어 본다.
핵심 관찰: 카톡·인스타를 한 번 거친 파일은 메타데이터가 통째로 날아간다. "AI로 만든 게 확실한데 신호가 0개"인 파일이 나오면, 그게 바로 Unknown ≠ clean이 필요한 이유다.
같은 이미지에 --backend cv2 / migan / lama를 각각 돌려 결과를 나란히 놓는다. 배경이 단색일 때는 셋이 거의 같고, 벽돌·격자·글자 위에서는 확 갈린다. RAM 사용량(/usr/bin/time -v)도 함께 재서 "품질 대 메모리" 표를 직접 만들어 본다.
_internal/isobmff.py를 읽고, 스크립트 30줄로 MP4 박스 트리를 출력해 moov → udta → meta → keys/ilst를 직접 걸어 들어간다. 그다음 video metadata --check와 결과를 대조.
얻는 것: MP4 구조를 평생 안 잊는다. 자막·챕터·회전 플래그 문제를 다룰 때 그대로 쓰인다.
기존 *_engine.py 하나를 골라 구조를 흉내 내, 임의로 만든 로고를 스스로 합성한 테스트 이미지에서 탐지·제거하도록 등록한다. 기여 규칙상 스톡·마켓플레이스 워터마크는 추가 금지이므로 반드시 자작 라벨로 연습할 것.
진짜 배우는 지점은 탐지가 아니라 오탐 게이트 튜닝이다 — 깨끗한 사진 50장에 돌려서 한 장도 안 건드리는지 확인해 보면, 폐기된 assume_ai가 왜 59.8%를 망쳤는지 몸으로 안다.
scripts/video_fidelity_probe.py의 발상을 이미지에 옮겨, invisible 전후의 PSNR·SSIM·LPIPS를 잰다. 저장소 설계에서 중요한 건 엔진 자체가 보고하는 PSNR은 리사이즈·인코딩 전 값이라 실제 전달된 그림보다 후하다는 점 — 프로브만이 최종 결과를 본다.
일반 교훈: 파이프라인 내부 지표와 최종 산출물 지표를 섞어 보고하면 자기 기만이 된다.
| 주차 | 주제 | 구체적으로 |
|---|---|---|
| 1주차 | 이미지 메타데이터와 출처 표준 | EXIF·XMP·IPTC 구조 → C2PA / Content Credentials 매니페스트 읽기 → identify.py와 _internal/c2pa.py 대조. "공식 리더 + 자체 폴백 파서" 이중 구성의 이유를 확인 |
| 2주차 | 인페인팅 계보 | cv2 Telea/Navier-Stokes(고전) → MI-GAN(경량 GAN) → big-LaMa(푸리에 합성곱). 셋의 원리 차이가 실습 과제 2의 결과 차이로 그대로 보인다. ONNX Runtime으로 torch 없이 추론하는 법도 함께 |
| 3주차 | 워터마킹 이론 | docs/watermarking-landscape.md → DWT-DCT(주파수 영역 매립, dwt_dct.py가 구현체) → TrustMark(Adobe, 학습 기반) → SynthID(비공개). "왜 어떤 건 로컬 디코딩이 되고 어떤 건 안 되나"의 답 |
| 4주차 | 확산 모델 + 영상 파이프라인 | latent diffusion / VAE 인코딩–디코딩이 왜 워터마크를 흐리는지 → ControlNet(Canny로 구도 고정) → SAM 세그멘테이션 → PyAV·ffmpeg로 VFR·색공간 다루기 |
4주차부터 시작하고 싶겠지만(가장 화려하니까), 1~2주차가 실제로 매일 쓰이는 기술이다. 메타데이터 파싱과 인페인팅은 워터마크와 무관한 프로젝트에서도 계속 나오는 반면, 확산 재생성은 GPU가 있어야 하고 결과도 확률적이다.
| 용어 | 코드 위치 | 뜻 |
|---|---|---|
| C2PA | _internal/c2pa.py | 콘텐츠 출처를 서명해 파일에 넣는 업계 표준(Content Credentials). 누가·무엇으로 만들었는지가 검증 가능한 형태로 들어감 |
| SynthID | video_synthid.py | 구글의 픽셀 워터마크. 공개 디코더가 없어 로컬 검증 불가 — 이 저장소의 여러 제약이 여기서 나온다 |
| TC260 AIGC | isobmff.py 등 | 중국 표준의 AI 생성 라벨. 이미지뿐 아니라 MP4·MKV·AVI·FLV 메타데이터에도 들어감 |
| localize → fill | watermark_registry.py | 이 저장소의 핵심 패턴. 마크를 찾아 마스크만 만들고, 채우기는 공용 엔진 하나에 맡김 |
| inpainting | region_eraser.py | 지워진 영역을 주변 맥락으로 자연스럽게 메우는 기술 |
| MI-GAN / big-LaMa | [migan]·[lama] | 학습형 인페인팅 모델 둘. 28MB/1GB RAM 대 200MB/4.7GB RAM — 가벼움 대 품질 |
| trust ladder | resolve_trust | strict→confirmed 2단계. 같은 벤더 증거가 있을 때만 오탐 게이트를 우회 |
| temporal arbiter | video_temporal.py | 인접 프레임에 같은 위치로 반복돼야 제거를 허가하는 영상 판정자 |
| VFR / PTS | video_encoding.py | 가변 프레임레이트와 각 프레임의 표시 시각. 평균 fps로 뭉개면 재생 타이밍이 어긋난다 |
| ControlNet (Canny) | qwen_zimage_pipeline.py | 확산 재생성 시 원본의 윤곽선을 조건으로 줘서 구도가 딴 그림이 되지 않게 잡아 주는 장치 |
| ELA | pixel_evidence.py | Error Level Analysis. 품질 90으로 재저장해 오차를 보는 위변조 탐지 기초 기법 |
| oracle-certified | README · data/evaluations/ | "제공자의 검증기로 실제로 돌려 봤다"는 뜻. 표본이 특정 파일들이라 일반 보장이 아니다 |