background-removal-js — 브라우저에서 배경 제거가 돌아가는 진짜 구조
IS-Net ONNX 모델 + WASM 추론 + Canvas API, 서버 없이 클라이언트에서 완결되는 파이프라인
이미지를 넣으면 배경이 사라진 PNG가 나온다. 서버로 보내지 않는다. 브라우저 안에서 전부 처리된다. ONNX Runtime의 WASM 백엔드로 딥러닝 모델을 돌리는 구조다.
코드를 열어보면 파이프라인이 놀라울 정도로 단순하다.
5단계 파이프라인
1단계 — 이미지 디코딩 (codecs.ts)
입력이 URL이든 Blob이든 ArrayBuffer든 전부 NdArray<Uint8Array> (RGBA, [H, W, 4])로 변환한다. 브라우저에서는 createImageBitmap → Canvas → getImageData 경로. Node.js에서는 sharp 사용.
2단계 — 전처리 (inference.ts + utils.ts)
핵심은 두 가지: 리사이즈와 정규화.
이미지를 1024x1024로 리사이즈한다. 원본 비율을 유지하지 않고 강제로 정사각형으로 만든다. bilinear interpolation 사용. 그 다음 HWC uint8 RGBA를 BCHW float32 RGB로 변환한다. 정규화는 (pixel - 128) / 256 — 값 범위가 대략 [-0.5, 0.5]가 된다.
이 변환이 tensorHWCtoBCHW 함수에서 일어난다. 채널별로 float32Data[j] = (imageBufferData[i] - 128) / 256 — R, G, B 순서로 stride를 곱해서 planar 배치.
3단계 — 모델 추론 (onnx.ts)
ONNX Runtime으로 IS-Net 모델을 실행한다. 입력 텐서 [1, 3, 1024, 1024], 출력 텐서 [1024, 1024, 1].
실행 프로바이더는 두 가지: wasm(기본, CPU 연산)과 webgpu(config.device가 gpu일 때, 로컬 GPU 사용). ort.env.wasm.numThreads = navigator.hardwareConcurrency로 멀티스레드 WASM을 켠다.
IS-Net — 어떻게 배경과 전경을 구분하는가
여기서 "별도의 배경 판별 로직"같은 건 없다. IS-Net 모델 자체가 salient object segmentation 모델이다. 이미지에서 "가장 눈에 띄는 물체"를 자동으로 식별해서 전경/배경 확률 맵을 출력한다.
IS-Net은 ETH Zurich + University of Alberta 연구팀이 ECCV 2022에서 발표한 모델이다. Intermediate Supervision Network — U-Net 스타일 인코더-디코더 구조인데, 디코더의 중간 레이어마다 supervision 신호를 넣어서 각 해상도에서 점진적으로 세밀한 마스크를 만들도록 학습시켰다.
학습에 사용된 DIS5K 데이터셋이 핵심이다. 5,470장의 고해상도 이미지에 머리카락, 모피, 레이스, 자전거 바퀴살 같은 세밀한 경계까지 픽셀 단위로 라벨링되어 있다. 모델이 이 데이터로 학습했기 때문에 "이 픽셀이 전경일 확률"을 0.0~1.0 사이의 soft probability map으로 출력한다.
출력이 0/1 이진값이 아니라 연속적인 확률값이라는 게 중요하다. 머리카락 끝이 0.3이면 30% 불투명으로 렌더링된다. 하드 임계값(threshold)을 적용하지 않기 때문에 반투명 경계가 자연스럽게 표현된다.
세 가지 모델 크기
isnet(FP32, ~176MB) — 원본 정밀도. 모든 가중치가 32비트 부동소수점. 가장 정확하지만 다운로드가 크다.
isnet_fp16(FP16, ~88MB, 기본값) — 반정밀도. 가중치를 16비트로 줄였다. 추론(inference)에서는 32비트와 품질 차이가 거의 없다. 신경망의 학습된 특징이 32비트 정밀도를 필요로 하지 않기 때문.
isnet_quint8(INT8, ~44MB) — 양자화. 가중치를 8비트 정수(256단계)로 압축했다. 크기는 1/4이고 속도는 빠르지만, 머리카락이나 레이스 같은 세밀한 경계에서 알파값이 덜 부드러워질 수 있다. 단순 배경(스튜디오 사진)에서는 차이가 거의 없고, 복잡한 경계가 있는 이미지에서 차이가 보인다.
모델은 CDN(staticimgly.com)에서 청크 단위로 다운로드한다. 브라우저 HTTP 캐시가 처리하니까 두 번째 실행부터는 네트워크 요청 없이 캐시에서 로드.
4단계 — 후처리 (inference.ts)
float32 확률 맵을 uint8로 변환(×255)하고, 원본 해상도로 다시 리사이즈한다. 그 다음 원본 이미지의 알파 채널에 확률값을 덮어쓴다:
outImageTensor.data[4 * i + 3] = alphamask.data[i] — 이 한 줄이 배경 제거의 핵심이다. 확률값이 0에 가까운 픽셀(배경)은 투명해지고, 255에 가까운 픽셀(전경)은 유지된다. 중간값은 반투명으로 표현되어 경계가 자연스럽다.
5단계 — 인코딩 (codecs.ts)
RGBA 텐서를 Canvas에 putImageData로 그리고 convertToBlob으로 PNG/JPEG/WebP Blob을 반환.
근데 솔직히 말하면
파이프라인이 단순한 건 좋은데, 그 단순함에 대가가 있다.
1024x1024 강제 리사이즈가 치명적이다. 원본이 4000x3000이든 200x200이든 전부 1024x1024로 찌그러뜨린다. 비율 무시. 세로로 긴 인물 사진은 가로로 늘어나고, 가로로 긴 풍경은 세로로 찌그러진다. 모델 입력 전에 왜곡이 발생하니까 마스크 품질이 떨어질 수밖에 없다. 비율 유지 + 패딩이 기본 아닌가?
trimap도 alpha matting도 없다. 다른 배경 제거 시스템(MODNet, RVM 등)은 trimap 생성 → alpha matting으로 머리카락 같은 반투명 경계를 처리한다. 이 라이브러리는 IS-Net 한 방에 의존한다. 모델이 알아서 soft mask를 예측하니까 결과가 모델 성능에 100% 종속된다. 머리카락 경계가 뭉개지는 건 구조적 한계.
세션 메모이제이션이 lodash.memoize로 되어 있다. 설정 JSON을 키로 메모이제이션하는데, 같은 설정이면 수백 MB 모델을 다시 로드하지 않는다. 합리적인 설계다. 근데 메모리 해제 시점이 없다. 한번 로드된 ONNX 세션이 페이지가 닫힐 때까지 메모리를 잡고 있다.
AGPL-3.0 라이선스. 상업적으로 쓰려면 소스 공개 의무가 생긴다. MIT/Apache가 아니다. 이게 실무에서 쓰기 어려운 가장 큰 이유일 수 있다.
69개 오픈 이슈 중 상당수가 미응답. 메인테이너가 2명(DanielHauschildt 56커밋, mirko314 10커밋)이고 마지막 커밋이 2025년 7월이다. 8개월째 main에 아무것도 안 들어갔다.
그럼에도 브라우저 안에서 딥러닝 추론을 완결한다는 패턴은 진지하게 볼 가치가 있다.
브라우저 WASM 추론이 의미 있는 이유
서버가 필요 없다. 이미지가 사용자 디바이스를 떠나지 않는다. remove.bg 같은 서비스는 이미지를 서버로 보내야 하는데, 프라이버시 민감한 이미지(의료, 신분증, 사내 문서)는 그게 불가능하다. 이 라이브러리는 모델 파일만 CDN에서 받고, 이후 모든 처리가 로컬에서 끝난다.
비용이 0이다. GPU 서버를 운영하면 추론 1회당 비용이 발생한다. 클라이언트 사이드면 사용자의 CPU/GPU를 쓰니까 서버 비용이 없다. 트래픽이 늘어도 인프라 비용이 안 늘어난다.
기본은 CPU(WASM), 선택적으로 GPU(WebGPU). device: 'cpu'(기본값)이면 ONNX Runtime이 WebAssembly로 CPU에서 돌린다. SIMD + 멀티스레드(navigator.hardwareConcurrency 코어 수만큼)로 최적화된다. device: 'gpu'로 바꾸면 WebGPU API를 통해 로컬 GPU를 쓴다 — NVIDIA든 Apple Silicon이든 브라우저가 지원하면 된다. GPU 모드가 당연히 빠르지만, WebGPU를 지원하는 브라우저가 아직 제한적이라 기본값은 CPU다.
SharedArrayBuffer + COOP/COEP 헤더가 필요하다. 멀티스레드 WASM을 쓰려면 서버가 Cross-Origin-Opener-Policy: same-origin과 Cross-Origin-Embedder-Policy: require-corp 헤더를 보내야 한다. 이 설정이 안 되면 싱글스레드로 폴백되는데, 성능이 크게 떨어진다.
Web Worker 안에서 이 라이브러리를 돌리면 메인 스레드가 블로킹되지 않아서 UI가 안 멈춘다. web-workers-internals 포스트에서 다뤘던 바로 그 패턴이다.
리포지토리 현황 (2026-03 기준)
Star 6,973개, Fork 453개. JS 배경 제거 라이브러리 중 가장 유명하다.
메인테이너 2명. DanielHauschildt(56커밋, imgly 소속)가 핵심이고 mirko314(10커밋)가 보조. 나머지 컨트리뷰터는 1~2커밋.
main 마지막 커밋 2025-07-18. 8개월 넘게 활동 없음. 오픈 이슈 69개. PR 1개 오픈 중(jpg 지원 추가, 2026-03-24). GitHub Release 없음 — npm으로만 배포.
라이선스 AGPL-3.0. 언어 TypeScript 59% + JavaScript 31%.
npm 최신 버전 1.5.7(2024-11-27). 모노레포 구조로 @imgly/background-removal(브라우저)과 @imgly/background-removal-node(Node.js) 두 패키지를 관리한다.
파이프라인 코드 탐색
각 카드를 클릭하면 해당 부분의 실제 소스 코드가 펼쳐집니다
URL, Blob, ArrayBuffer, ImageData 등 모든 입력을 NdArray<Uint8Array> [H, W, 4] RGBA로 통일.
비율 무시 강제 리사이즈 → HWC uint8 RGBA를 BCHW float32 RGB로 변환. (pixel-128)/256 정규화.
ONNX Runtime으로 IS-Net 실행. WASM(기본) 또는 WebGPU 백엔드. 출력은 [1024,1024,1] 전경 확률.
마스크를 uint8 변환 → 원본 해상도 리사이즈 → 원본 이미지의 알파 채널에 마스크값 대입.
RGBA NdArray를 Canvas에 그리고 convertToBlob으로 최종 이미지 Blob 반환.
lodash.memoize로 config JSON을 키로 ONNX 세션을 캐싱. 같은 설정이면 모델 재다운로드 없이 재사용.
GitHub 리포지토리 현황
▶
커밋 타임라인 — 2023-06 시작, 2025-07 마지막
2023-06 ~ 2024-01 — 핵심 개발기 (imgly 팀)
IS-Net 모델 통합, WASM 백엔드, CDN 청크 로딩, WebGPU 지원 추가. v1.0 ~ v1.4
2024-02 ~ 2024-11 — 유지보수 (버그픽스 위주)
OnnxRuntime 업데이트, FP16 모델 추가, lodash 이슈 수정. v1.5.0 ~ v1.5.7
2025-01 ~ 2025-07 — 커뮤니티 PR 위주
NextJS 예제, pnpm 마이그레이션, ONNX 안정 버전 핀. 2025-07-18 이후 main 활동 없음.
2026-03-24 — jpg 지원 PR 1개 오픈 (미머지, 8개월 공백 후)
컨트리뷰터 (상위)
IS-Net 모델 3종 비교
| config 값 | 정밀도 | 크기 | 속도 | 품질 |
|---|---|---|---|---|
large / isnet |
FP32 | ~176 MB | 느림 | 최고 |
medium / isnet_fp16 |
FP16 | ~88 MB | 보통 | 기본값 |
small / isnet_quint8 |
INT8 | ~44 MB | 빠름 | 낮음 |
핵심 포인트
이미지 입력(URL/Blob/Buffer) → createImageBitmap + Canvas로 RGBA NdArray 변환
1024x1024 리사이즈(비율 무시) + HWC→BCHW 변환 + (pixel-128)/256 정규화
ONNX Runtime(WASM/WebGPU)로 IS-Net 추론 → [1024,1024,1] 전경 확률 마스크
마스크를 uint8 변환 → 원본 해상도 리사이즈 → 알파 채널에 덮어쓰기
Canvas putImageData → convertToBlob으로 PNG/JPEG/WebP 출력