NearIMG

2026-07-20

브라우저에서 완전히 동작하는 AI 사진 업스케일 모델, 어떻게 고르고 검증했나

NearIMG의 AI 사진 업스케일 기능에는 완전히 클라이언트에서만 돌아가는(업로드 없음, NearIMG의 다른 모든 도구와 동일한 원칙) 진짜 초해상도 모델이 필요했습니다. 이 제약에 비하면 "모델을 구해서 브라우저에서 돌린다"는 쉬운 일처럼 들리지만, 여기엔 실제 함정이 있습니다. 이런 모델의 상당수 ONNX export는 입력 해상도를 조용히 고정값으로 박아 넣는데, 데모에서는 문제없이 작동하다가 실제 사용자가 자기 크기의 사진을 올리는 순간 깨집니다. 이 글은 nearimg가 실제로 거친 모델 선정 과정, 우리가 당하기 전에 먼저 알아챈 함정, 조작되거나 미묘하게 잘못된 모델을 배포하고 있지 않은지 검증한 방법, 그리고 출시를 결정하기 전 실제 브라우저에서 실측한 숫자들을 다룹니다.

실제로 검토한 후보들

진공 상태에서 고른 게 아닙니다. 기준은 "자체 호스팅할 수 있을 만큼 작고, 상업적 재배포 라이선스가 명확하며, 캔버스 리사이즈보다 실제로 나아 보일 만큼 품질이 좋을 것"이었습니다.

후보결과
Real-ESRGAN x4plus (풀사이즈)기각 — 공식 릴리스가 약 64MB로, 기능 하나를 위해 자체 호스팅하기엔 지나치게 큼.
Qualcomm AI Hub의 동일 가중치 사전변환 ONNX export기각 — 입력이 128×128로 고정, dynamic axes 없음. 아래 참조.
FSRCNN / ESPCN (고전적인 소형 초해상도 모델)조사만 하고 채택하지 않음 — 이 세션에서 찾아본 범위 내에서는 라이선스가 명확하고 배포 가능한 ONNX export를 확보하지 못했고, 이미 채택한 모델이 크기·라이선스·품질 기준을 다 만족했기에 더 파고들 급한 이유가 없었음.
realesr-general-x4v3 (Real-ESRGAN "compact" 변형, SRVGGNetCompact 아키텍처)채택. BSD-3-Clause, 가중치 약 4.9MB, 올바르게 export하면 입력 크기가 동적(아래 참조).

함정: 입력 크기가 고정된 "바로 쓸 수 있는" ONNX export

가장 편해 보였던 선택지는 가중치 자체가 아니라, 같은 모델을 Qualcomm AI Hub에서 미리 변환해 둔 qualcomm/Real-ESRGAN-General-x4v3였습니다. 사전변환이라는 건 PyTorch도, export 단계도, 변환을 잘못할 위험도 없다는 뜻이니까요. 그런데 이 그래프는 입력 크기가 128×128 픽셀로 고정돼 있었고, dynamic axes가 아예 선언돼 있지 않았습니다. 고정된 벤치마크에는 문제가 없지만, "가지고 있는 아무 작은 사진이나 올려보세요"가 전제인 도구에는 쓸모가 없습니다. 실제 사용자 사진이 128×128일 리 없고, 사후에 ONNX 그래프의 기대치를 다시 늘릴 방법도 없습니다.

이건 처음 겪는 놀라운 일도 아니었습니다. NearBG 팀이 배경 제거용 BiRefNet을 export할 때 이미 같은 모양의 함정에 걸렸던 적이 있습니다 — 모델은 다르지만, 컨버터가 만들어내는 함정의 모양은 같습니다: 그래프가 애초에 새겨진 크기가 아니면 무엇을 넣어도 소용없는, 겉보기엔 완성된 것 같은 고정 크기 ONNX export. 이런 선례가 있다는 걸 알았기 때문에 Qualcomm export를 "짐작"이 아니라 직접 확인한 구체적 증거로 기각할 수 있었습니다.

가중치가 변조되지 않았는지 검증

사전변환 export를 배제한 뒤 계획은 이렇게 정리됐습니다: 공식 .pth 가중치를 받아서 우리가 직접, 제대로 변환한다. 어떤 다운로드 파일이든 신뢰하기 전에, 서로 다른 두 개의 독립적인 HuggingFace 미러 — clavidk/realesr-general-x4v3와 rippertnt/upscale — 에서 각각 realesr-general-x4v3.pth를 받아 해시를 비교했습니다.

sha256sum realesr-general-x4v3.pth
8dc7edb9ac80ccdc30c3a5dca6616509367f05fbc184ad95b731f05bece96292  (4.89 MB)

서로 무관한 두 재배포처에서 받은 파일의 해시가 완전히 같다는 건, 이 파일이 xinntao/Real-ESRGAN(BSD-3-Clause)의 변조되지 않은 공식 체크포인트라는 실질적인 (다소 소박하더라도) 증거입니다 — 같은 파일명으로 돌고 있는 재학습본이나 변형본이 아니라는 뜻입니다.

아키텍처가 정말 일치하는지 검증

해시가 맞다는 건 파일이 변조되지 않았다는 뜻이지, 실제로 추론을 돌릴 우리 쪽 모델 코드가 올바른 아키텍처를 재현하고 있다는 뜻은 아닙니다. 그래서 공식 저장소의 realesrgan/archs/srvgg_arch.py (동일한 BSD-3-Clause 소스)에서 원본 SRVGGNetCompact 정의를 그대로 포팅해 srvgg_arch.py를 만들고, 체크포인트가 기대하는 그대로 구성한 뒤 (num_feat=64, num_conv=32, upscale=4, act_type='prelu') 공식 state dict를 strict=True로 로드했습니다.

model.load_state_dict(sd, strict=True)
# missing_keys=[], unexpected_keys=[]

missing_keys와 unexpected_keys가 모두 0이라는 건 짐작이 아니라 실측된 결과입니다 — PyTorch의 strict 로더는 레이어 이름, 형태(shape), 개수 중 하나라도 어긋나면 그 즉시 실패합니다. 이 검증 덕분에, 이제 ONNX로 export하려는 네트워크 그래프가 그럴듯한 근사치가 아니라 실제로 가중치가 학습된 그 아키텍처와 정확히 같다는 걸 신뢰할 수 있었습니다.

해결책: 우리가 직접 dynamic axes로 export

가중치와 아키텍처를 모두 검증한 뒤, Qualcomm의 함정을 피하는 export 방법은 torch.onnx.export에 인자 하나를 추가하는 것이었습니다.

torch.onnx.export(
    model, dummy, 'realesr-general-x4v3.onnx',
    input_names=['input'], output_names=['output'],
    dynamic_axes={'input': {2: 'height', 3: 'width'}, 'output': {2: 'height', 3: 'width'}},
    opset_version=13, dynamo=False,
)

2번, 3번 축(높이·너비)을 dynamic axes로 지정하면, 결과 그래프는 export 시점의 더미 텐서 크기가 아니라 어떤 입력 해상도든 받아들입니다 — Qualcomm 빌드가 하지 않았던 바로 그 부분입니다. 결과물은 고정 입력 함정이 없는 4.87MB짜리 ONNX 파일이었습니다.

여기까지 가는 길이 완전히 순탄하지는 않았습니다 — 사소하지만 솔직히 밝힐 만한 버그가 하나 있었습니다. 처음 export를 PyTorch 기본값으로 시도했을 때 실패했는데, 최신 PyTorch(2.9 이상)가 기본으로 새로운 "dynamo" ONNX exporter를 쓰고, 이게 이 환경에 설치돼 있지 않던 별도 패키지 onnxscript를 요구했기 때문입니다. 해결책은 dynamo=False로 예전의, 여전히 완전히 지원되는 TorchScript 기반 exporter로 되돌리는 것이었습니다 — 실제 에러 메시지가 (짐작이 아니라) 누락된 패키지를 정확히 가리켜 준 덕분에 한 줄로 해결된 문제였습니다.

짐작이 아니라 실제로 측정하기

깔끔하게 로드되고 export되는 모델도 실제로는 느릴 수 있고, 단순 리사이즈보다 딱히 나아 보이지 않을 수도 있습니다. 그래서 이건 서류상으로 채점하지 않고 실제로 돌렸습니다. 테스트 하네스는 실제 Playwright Chromium을(브라우저를 흉내 내는 Node 쪽 onnxruntime-web import가 아니라) WASM 실행 백엔드로 구동했고, 디코드·리사이즈·PNG 인코딩까지 실제 <canvas> 호출을 그대로 거쳤습니다 — 실제 배포되는 것과 같은 코드 경로입니다. 모든 실행의 네트워크 트래픽은 Playwright의 요청 가로채기로 캡처했고, 로컬 테스트 서버 외의 호스트로 나간 요청 수는 모든 실행에서 0건이었습니다.

케이스입력 → 목표모델 로드추론 캔버스 기준선선명도 (기준선 / AI / 진짜 원본)AI/기준선 비율
fruits.jpg (OpenCV 샘플)128×120 → 512×480 1814ms (콜드)1706ms0.9ms 14.16 / 40.79 / 122.562.88×
실사진 (picsum.photos id 237, 강아지)75×50 → 300×200 20ms (웜)451ms0.2ms 61.86 / 300.33 / 730.764.85×
fruits.jpg, 다운스케일 없이 (최악의 경우) 512×480 → 2048×1920— 27,757ms (약 27.8초)———

여기서 "선명도"는 고주파 에너지 지표 — 인접 픽셀 간 휘도 차이의 제곱 평균으로, "실제로 얼마나 세밀한 디테일이 있는가"를 (단순하지만) 실측에 가깝게 근사한 값이며, 짐작이 아니라 각 실행에서 실제로 렌더링된 PNG에서 측정했습니다. 절대값보다 상대 비교가 중요합니다: 서로 매우 다른 두 실제 이미지에서, AI 결과물은 동일한 목표 크기로 맞춘 단순 imageSmoothingQuality: 'high' 캔버스 리사이즈보다 2.9~4.9배의 고주파 디테일을 담고 있었습니다 — 하나만 골라낸 수치가 아니라 일관되게 측정된 격차입니다. 세 번째 행은 실제 제품 결정을 좌우한 최악의 경우 수치이며, 바로 다음에 다룹니다.

못 하는 것 — 숨기지 않는 트레이드오프

보기 좋은 부분만이 아니라, 이 기능을 출시하는 데 실제로 치른 비용도 솔직하게 밝힙니다.

  • 큰 입력에서는 느리고, 그게 UI를 결정했습니다. 작은 입력(한 변이 75~130px 정도)은 2초도 안 걸려 업스케일되므로 바로 화면에서 돌려도 괜찮습니다. 하지만 약 500px 너비의 이미지를 다운스케일 없이 그대로 모델에 넣으면, 싱글스레드 WASM·콜드 샌드박스 기준으로 약 28초가 걸렸습니다. 이 숫자가 실제 제품 결정을 이끌었습니다: 추론은 워커에서 돌려 메인 스레드를 절대 얼리지 않고, 약 4초를 넘기면 "시간이 걸릴 수 있어요" 안내가 뜨며, 기능 자체도 내부적으로만이 아니라 제목과 설명에서부터 "무엇이든 순식간에 확대"가 아니라 "작고 저해상도인 사진을 키우는" 용도로 명시적으로 프레이밍했습니다. 실제 배포된 엔진(packages/near-upscale-engine/upscale.js)도 원본 사진 크기와 무관하게 모델에 실제로 넣는 변의 길이를 768px로 상한선을 두는데, 사용자가 무엇을 올리든 최악의 추론 시간이 무한정 늘어나지 않도록 묶어두기 위해서입니다.
  • 이 체크포인트는 4배 고정입니다 — 별도의 2배 모델은 없습니다. 이 아키텍처의 PixelShuffle 업샘플러는 출력 채널 수가 학습된 가중치에 그대로 박혀 있고, 이 체크포인트에는 2배 전용 자매 체크포인트가 존재하지 않습니다. 그래서 UI의 "200%" 옵션은 더 작고 다른 모델이 아니라 — 똑같은 4배 모델을 유일하게 지원하는 배율로 돌린 뒤, 결과물을 캔버스로 정확히 절반 축소한 것입니다. 이렇게 하면 원본이 아니라 AI의 4배 결과물에서 출발하기 때문에, 단순 캔버스 리사이즈에게 2배만큼의 픽셀을 무에서 만들어내라고 요구하는 것보다 눈에 띄게 더 많은 실제 디테일이 남습니다. 임의의 절대 픽셀 목표도 같은 원리입니다: 모델이 지원하는 유일한 배율로 먼저 돌린 뒤, 요청된 크기에 맞춰 캔버스로 정확히 crop/fit합니다 — 모드마다 따로 덧붙인 게 아니라 엔진 소스의 planUpscale() 주석에 그대로 문서화돼 있습니다.
  • 진짜 원본에는 도달하지 못합니다. 모델에 넣기 위해 일부러 축소했던 입력이 아니라 실제 전체 해상도 원본 사진과 비교하면, AI 결과물은 그 격차를 완전히 메우지 못합니다 — 이건 버그가 아니라 예상된 결과입니다. 초해상도는 그럴듯한 디테일을 만들어내는 것이지,애초에 저해상도 입력에 담기지 않았던 정보를 복원하는 게 아니기 때문입니다. 이는 더 나은 모델 설정으로 고칠 수 있는 문제가 아니라 이 기법 자체의 본질적 한계입니다. 대신 실측으로 확실히 내놓는 것은, 같은 목표 크기의 단순 리사이즈보다 2.9~4.9배 더 많은 고주파 디테일입니다 — 몇 단계를 더 거친 "블러 확대"가 아니라, 실질적으로 다른 결과물입니다.

솔직한 요약

사전변환된, 바로 쓸 수 있는 ONNX export는 가장 편한 길이었지만, 이 제품에는 구체적이고 확인 가능한 이유로 맞지 않았습니다 — 임의의 사용자 사진을 다루는 게 전부인 도구에 128×128 고정 입력이라니, 이건 이미 다른 팀이 다른 모델로 겪었던 것과 같은 모양의 컨버터 함정이었습니다. 이를 피하는 데 든 비용은 추가 단계 하나(공식 가중치를 우리가 직접 dynamic axes로 export)와 금방 진단된 작은 버그 하나(최신 PyTorch exporter가 우리에게 없던 패키지를 요구한 것)뿐이었습니다. 그 대가로 얻은 것은, 출처가 해시로 검증되고, 아키텍처 일치가 strict=True로 검증되고, 단순 리사이즈 대비 실제 성능·품질 격차를 벤치마크 표가 아니라 진짜 브라우저에서 실측한 모델입니다 — 출시 전 이 기능이 어떻게 프레이밍되고 제한됐는지를 정직하게 좌우한 최악의 경우 수치까지 포함해서요.

광고
← NearIMG

이 페이지는 동의하신 경우에만 광고를 표시합니다.