본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 05. 22. 09:14

200KB AI 모델로 얼굴 자동 모자이크 — face-api.js + Canvas로 브라우저 내 완결

요약

face-api.js와 Canvas를 활용하여 이미지를 서버로 전송하지 않고 브라우저 내에서 얼굴을 자동 검출하고 모자이크 처리하는 구현 방법을 소개합니다. 프라이버시 보호를 위해 클라우드 API 대신 경량화된 모델을 로컬에서 실행하는 최적의 워크플로우를 다룹니다.

핵심 포인트

  • TinyFaceDetector(약 200KB)를 사용해 브라우저 내 완결형 구현 가능
  • 클라우드 API 대신 로컬 모델을 사용하여 사용자 프라이버시 완벽 보호
  • 유지보수가 활발한 @vladmandic/face-api 라이브러리 사용 권장
  • 모델 파일을 자사 서버에 직접 배포하여 외부 CDN 의존성 제거
  • Next.js 환경에서는 SSR 오류 방지를 위해 dynamic import 필수

단체 사진을 SNS에 올리기 전, 행인이나 동의하지 않은 타인의 얼굴을 일괄적으로 모자이크 처리하고 싶다. 이를 이미지를 클라우드로 보내지 않고, 브라우저 내의 AI만으로 완결시키려 하면 라이브러리 선정과 모델 배포에 있어 공력이 필요하다.

Panda Tools에 '얼굴 자동 모자이크'를 만들었으므로, @vladmandic/face-api의 TinyFaceDetector(약 200KB)로 브라우저 내에서 얼굴 검출을 실행하고, Canvas로 모자이크·블러(Blur)·검은색 칠하기를 적용하기까지의 구현과 겪었던 문제점들을 정리한다.

얼굴 검출 계열의 클라우드 API(Google Cloud Vision, AWS Rekognition 등)는 정밀도가 높다. 반면 **이미지를 업로드하는 시점에서 '제3자에게 사람의 얼굴 사진을 보내고 있는 것'**이 된다. 프라이버시 보호 목적의 도구가 그를 위해 타사의 서버로 이미지를 보내게 만드는 설계는 애초에 본말전도다.

브라우저 내 AI라면 다음과 같은 사항이 성립한다:

  • 이미지가 단말기 밖으로 나가지 않는다
  • 모델 파일조차 자사 사이트에서 배포하면 Google/AWS로의 요청도 발생하지 않는다
  • 사용자가 '서버에 업로드하지 않았다'는 것을 네트워크(Network) 탭에서 직접 확인할 수 있다

정밀도는 클라우드형에 뒤처진다. 하지만 '절대 보내지 않는다는 보장'이 선다면, 그 차이를 메우고도 남을 가치가 있다.

face-api.js 계열에는 두 가지 선택지가 있다:

패키지상태
face-api.js (justadudewhohacks 버전)원조. 유지보수 중단 상태 (최종 업데이트 2020년)
@vladmandic/face-api (vladmandic 버전)포크(fork)하여 활발히 유지보수 중. TF.js v4 대응 완료, TypeScript 타입 완비

face-api.js는 지금도 샘플이 많지만, TensorFlow.js 신버전과의 호환성 문제가 발생하므로, 새로 구축한다면 @vladmandic/face-api가 유일한 선택지다.

face-api.js는 내부적으로 TensorFlow.js를 사용하며, 훈련된 모델 파일(가중치 파일 + manifest)을 네트워크를 통해 로드한다. 기본 동작으로는 어딘가의 CDN에서 가져오기 때문에, 이를 자사 사이트에서 배포하도록 교체한다.

public/
models/
face-api/
...

이 파일은 vladmandic/face-api 리포지토리의 model/ 디렉토리에서 복사해 온다. tiny_face_detector_model.bin의 실체가 약 189KB이며, manifest를 포함하면 약 192KB이다. 본 기사에서는 대략적으로 '약 200KB'라고 표기한다.

로드 시에는 URI를 지정한다:

const mod = await import('@vladmandic/face-api')
await mod.nets.tinyFaceDetector.loadFromUri('/models/face-api')

이렇게 하면 외부 CDN에 일절 요청하지 않고, 자사 사이트 내에서 완결된다. Cloudflare Pages 배포이므로 CDN 캐시도 자동으로 적용된다. 최초 1회만 약 200KB를 다운로드하며, 이후에는 브라우저 캐시로부터 즉시 실행된다.

face-api.js는 내부에서 windownavigator를 참조하기 때문에, Next.js의 SSR(Server Side Rendering)에서 최상위(top-level) import를 하면 빌드가 통과되지 않는다. 반드시 dynamic import를 사용하여 'use client' 내에서 호출해야 한다:

'use client'
const faceApiRef = useRef<typeof import('@vladmandic/face-api') | null>(null)
const loadModel = useCallback(async () => {
...

useRef로 모듈 참조를 캐싱해 두면, 두 번째 검출 시부터는 로드를 스킵할 수 있다.

단, 타입 정보의 import는 별개다. type만은 정적으로 가능하다:

// 타입만 import (번들에 포함되지 않음)
interface FaceDetectionLike {
box: { x: number; y: number; width: number; height: number }
...

face-api.js 자체의 타입을 모듈에서 가져오려고 하면 SSR (Server-Side Rendering) 시에 평가가 실행되므로, 최소한의 타입 정의를 수동으로 작성하는 것이 무난했다.

검출은 detectAllFaces를 사용한다.

한 번에 실행:

const options = new mod.TinyFaceDetectorOptions({
inputSize: 416,
scoreThreshold: 0.5,
...

옵션의 의미:

  • inputSize: 추론 시의 입력 해상도. 224 / 320 / 416 / 512 / 608 중에서 선택 (32의 배수). 클수록 정밀도(Accuracy)는 올라가고, 속도(Speed)는 내려간다. 416이 기본값이며 밸런스가 좋다.
  • scoreThreshold: 얼굴로 인식하는 확신도의 임계값. 0.5는 "반신반의 이상"을 의미한다. 값을 낮추면 검출 누락은 줄어들지만, 오검출(배경의 무늬를 얼굴로 판단하는 경우)이 늘어난다.

반환되는 detections[i].box이미지 좌표계의 직사각형(Rectangle) { x, y, width, height }이다. 이것을 그대로 Canvas에서의 그리기 좌표로 사용할 수 있다.

이 부분이 은근히 빠지기 쉬운 함정이었다. TinyFaceDetector가 반환하는 직사각형은 "눈·코·입"을 중심으로 한 얼굴의 핵심 부분이며, 귀나 머리카락, 턱 아래까지는 커버하지 않는다. 이 직사각형 그대로 모자이크를 적용하면, 머리카락이나 귀를 통해 본인 식별 정보가 노출될 수 있다.

해결책으로, 직사각형을 일정 비율만큼 확장하는 함수를 넣었다:

export function expandRegion(
region: ImageRegion,
ratio: number,
...

ratio는 0.2(20%)를 기본값으로 설정했다. 귀·머리카락·턱까지 확실하게 가리고 싶을 때는 30~50%로 늘릴 수 있도록 UI에서 슬라이더로 구현했다. Math.max(0, ...)Math.min(maxWidth, ...)를 사용하여 이미지 경계를 벗어나지 않도록 클램핑(Clamping) 처리를 하는 것이 은근히 중요하다.

검출 결과의 직사각형에 대해 모자이크, 블러(Blur), 검은색 칠하기(Blackout) 3종류를 선택할 수 있도록 했다.

각각의 구현 패턴(축소 후 확대를 통한 유사 픽셀화, ImageData 기반의 자체 구현 Gaussian Blur, fillRect를 이용한 검은색 칠하기)은 별도 기사인 「모자이크와 블러의 차이를 구현으로 이해하기」에서 자세히 다루고 있으므로, 본 기사에서는 face-api.js의 검출 직사각형과 조합하는 부분의 요점만 정리한다.

// 검출 루프 → 이펙트 적용
for (const det of detections) {
const region = boxToRegion(det, canvas.width, canvas.height) // 직사각형 확장 완료
...

face-api.js 고유의 포인트는 다음과 같다:

  • ImageData 기반의 자체 Gaussian Blur 사용: Safari 18 미만에서는 ctx.filter = 'blur(...)'를 지원하지 않으므로, src/lib/image/gaussianBlur.ts에 분리하여 구현한 ImageData 기반의 자체 Gaussian Blur(Box Blur 3회 근사)를 사용한다.
  • 검은색 칠하기는 "절대로 복원시키고 싶지 않을 때" 사용: 모자이크는 강도가 낮으면 AI로 복원될 가능성이 제로가 아니므로, 법적·윤리적으로 강력한 은폐가 필요하다면 검은색 칠하기를 선택하게 한다.
  • 강도(Strength) 파라미터는 검출된 직사각형 크기에 맞춰 스케일링: 얼굴 크기가 이미지 내에서 제각각이더라도, strength * 5(모자이크 블록 크기)나 strength * 2(블러 반경)가 직사각형에 상대적으로 적용되도록 했다.

TinyFaceDetector는 어디까지나 약 200KB의 경량 모델이다. 다음과 같은 케이스는 애초에 검출할 수 없다:

  • 완전한 측면 프로필
  • 마스크나 선글라스로 얼굴 대부분이 가려진 경우
  • 저해상도, 흐릿함, 어두운 이미지
  • 단체 사진 구석에 작게 찍힌 얼굴

"자동"을 내세우면서 검출 누락이 발생하면 사용자는 난처해진다. 따라서 검출 건수가 0건일 때 별도의 도구(수동 범위 지정)로 유도하는 동선을 반드시 마련해 두었다 (이하는 구현부에서 발췌. 실제로는 detectedCountuseState<number | null>(null)로 다루고 있으며, 초기 상태와 처리 중인 상태를 제외하기 위해 detectedCount !== null && detectedCount === 0 && !isProcessing 조건을 붙였다):

{detectedCount !== null && detectedCount === 0 && !isProcessing && (
<div className="...">
얼굴이 감지되지 않았습니다.
...

「자동 감지 → 누락 시 수동」이라는 이중 구조로 설계하여, 사용자가 막히지 않도록 하는 것이 사용자 경험 (UX) 측면에서 중요했다.

실기기(iPhone 13)에서 4000px급 사진을 실행하면, 추론 (Inference)에 510초, Canvas 처리에 23초가 소요된다. 「처리 중...」 표시가 없으면 프리징(멈춤)된 것처럼 보이기 때문에, 버튼 상태와 진행 상황 라벨은 반드시 포함해야 한다.

사전 리사이징 (Pre-resize)을 안내하는 것도 효과적이다:

고해상도 이미지(4000px 초과)는 추론에 시간이 걸립니다.

이미지 리사이즈

를 통해 2000px 정도로 축소하여 사용하실 것을 권장합니다.

참고로 inputSize: 416을 320으로 낮추면 추론은 빨라지지만, 멀리 있는 작은 얼굴의 검출률이 떨어지기 때문에 장단점이 있다. 기본값인 416을 유지하고, 사용자에게는 사전 리사이징으로 대응하도록 설계했다.

face-api.js (vladmandic 버전) + Canvas를 이용한 브라우저 완결형 얼굴 자동 모자이크는, 약 200KB의 모델 1개만 로드하면 작동한다. 클라우드 API 수준의 정밀도는 나오지 않지만, 「이미지를 절대 서버로 보내지 않는다」, 「외부 CDN에조차 요청하지 않는다」는 설계를 실현할 수 있다.

핵심 포인트:

  • @vladmandic/face-api를 사용 (원본은 유지보수 중단됨)
  • 모델은 public/에 셀프 호스팅 (Self-host) 하여 loadFromUri로 로드
  • Dynamic import + useRef 캐싱으로 SSR 회피 및 재로드 방지
  • 검출 사각형 확장 (귀, 머리카락, 턱을 커버)
  • 블러(Blur) 처리는 ImageData로 직접 구현 (Safari 18 미만 호환성 확보)
  • 검출 누락 시 수동 지정 도구로 유도하여 대응

「브라우저 완결형 AI」의 유스케이스(Use case)로서, 얼굴 검출은 가치가 명확하게 드러나는 분야다. 텍스트 계열(OCR, 요약)은 클라우드 쪽의 정밀도가 압도적이지만, 이미지 계열은 경량 모델로도 실용적인 수준에 도달할 수 있다.

이 기사는 Zenn에도 동일한 내용으로 게시되어 있습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0