사진 필터 프리셋을 3D LUT(.cube)로 구우면 얼마나 달라질까, 직접 잰 오차와 JSON 구조

톤 커브와 8색 HSL로 만든 사진 필터 프리셋을 33×33×33 3D LUT(.cube)로 구워 두면, 다른 엔진은 픽셀마다 LUT를 한 번 조회하는 것으로 거의 같은 보정을 다시 낼 수 있습니다. 그렇게 구운 결과가 원래 계산과 얼마나 다른지 브라우저용 사진 필터 프리셋 데모를 만들어 직접 쟀습니다. 960×640 합성 사진에서 채널값(0~255) 차이는 평균 0.17~0.39, 최대 2~4였습니다. 데모 엔진의 자동 시험 29건은 모두 통과했습니다.

데모에서는 톤 커브·8색 HSL·3D LUT·그레인을 조절해 원본과 비교하고, 값을 JSON 프리셋으로 저장하고, 현재 보정을 .cube 파일로 내려받을 수 있습니다. 사진은 서버로 보내지 않고 브라우저 안에서만 처리합니다. 사진 필터 프리셋 데모에서 바로 조작해 볼 수 있습니다.

이 데모는 포토부스 필터 엔진을 구하는 외주 공고의 문제를 바탕으로 합성 데이터만으로 만든 독립 데모입니다. 의뢰받거나 계약한 작업이 아니며, 공고 고객의 발주서·테스트 사진·LUT 파일은 받지도 사용하지도 않았습니다. 공고 요구와의 대조는 글 끝 부록에 두었습니다.

합성 풍경 사진의 왼쪽은 원본, 오른쪽은 웜 필름 프리셋을 적용한 사진 필터 프리셋 데모의 전후 비교 화면

직접 잰 값

모든 값은 작성자 기기(Apple M4 Mac)에서 잰 것입니다. 입력은 코드로 생성한 합성 사진입니다. 그레인은 픽셀 위치마다 값이 달라 LUT에 넣을 수 없으므로 굽기 비교에서 뺐습니다.

무엇을 쟀나조건결과
33³ LUT로 구운 결과와 직접 처리한 결과의 차이기본 프리셋 3개(웜 필름·맑은 하늘·핑크 부스), 960×640 합성 사진, 채널값 0~255평균 0.17~0.39, 최대 2~4
17³ LUT로 구운 결과와 직접 처리한 결과의 차이같은 조건평균 0.19~0.56, 최대 3~4
LUT 조회와 직접 계산의 처리 시간960×640 한 장, Node.js33³ LUT만 19ms, 커브와 HSL 직접 계산 26ms
미리보기 한 장 처리 시간960×640Chrome 약 30~50ms, Node.js 약 22~27ms
브라우저에서 구운 .cube와 Node.js에서 구운 값의 차이33³, 내려받은 파일을 다시 읽어 비교최대 5.07×10⁻⁷
엔진 자동 시험커브·HSL·그레인·프리셋 16건, .cube·LUT 13건29건 통과
실제 Chrome 자동 조작 시험저장·새로고침·삭제, 내보내기 3종, 3D LUT 탭, 390px 모바일 화면실패 0건, 콘솔 오류 없음

33³에서는 세 프리셋 모두 평균 차이가 채널값 1단계 미만이었고, 격자를 17³로 줄이면 차이가 조금 커졌습니다. JavaScript에서는 LUT 조회가 직접 계산보다 빨랐지만 차이가 크지 않았고, 네이티브 엔진에서의 차이는 재지 않았습니다. 브라우저에서 내려받은 33³ .cube는 머리말 5줄과 데이터 35,937줄(33³), 약 970KB였습니다. 이 파일을 다시 읽은 값과 Node.js에서 구운 값의 차이는 소수 6자리로 적을 때 생기는 반올림 오차 정도였습니다.

이 수치는 합성 사진과 작성자 기기에서 잰 값이며, 실제 사진이나 GPU 없는 매장 PC의 결과를 뜻하지 않습니다.

3D LUT(.cube) 적용과 굽기

3D LUT 탭에서는 .cube 파일을 불러와 적용합니다. 파일의 입력 범위에 맞춰 색을 격자 좌표로 바꾸고, 둘러싼 8개 격자점을 삼선형 보간한 뒤 강도(0~100)만큼 원래 색과 섞습니다. 데모에는 수식으로 만든 샘플 LUT 두 개(33³ 틸 오렌지, 17³ 바랜 필름)가 들어 있습니다.

현재 보정을 .cube로 받기는 지금의 커브·HSL·LUT 결과를 33×33×33 격자점마다 계산해 새 .cube 파일로 굽습니다. 위 측정의 “구운 결과”가 이 파일입니다. 그레인은 픽셀 위치마다 달라서 LUT에 넣을 수 없으므로 빠집니다.

.cube 파일은 아래 규칙으로 읽습니다. 다른 곳에서 만든 LUT를 넣을 때 무엇이 거부되는지 미리 볼 수 있게 정리했습니다.

항목처리
TITLE, # 주석TITLE은 읽고, # 주석은 줄 끝 것까지 건너뜀
LUT_3D_SIZE2~65 정수만 받음. 데이터보다 앞에 있어야 함
DOMAIN_MIN·DOMAIN_MAX, LUT_3D_INPUT_RANGE입력 범위로 씀. 채널마다 MAX가 MIN보다 커야 함
데이터 순서와 개수빨강이 가장 빨리 바뀌는 순서, 정확히 size³줄
LUT_1D_SIZE(1D LUT)거부
숫자가 아닌 값, 개수가 맞지 않는 데이터, 데이터 뒤에 오는 키워드거부하고 이유를 보임. 줄 단위 오류는 행 번호 포함
모르는 키워드(예 LUT_IN_VIDEO_RANGE)경고만 하고 건너뜀
16MB를 넘는 파일거부

굽기 결과를 .cube로 썼다가 같은 규칙으로 다시 읽어도 값이 같은지는 자동 시험으로 확인했습니다.

틸 오렌지 샘플 3D LUT를 강도 80으로 적용한 결과를 원본과 비교하는 3D LUT 탭 화면

톤 커브·8색 HSL·그레인 처리 방식

처리는 톤 커브 → 8색 HSL → 3D LUT → 그레인 순서입니다. 톤 커브는 RGB 전체 커브를 적용한 뒤 R·G·B 채널 커브를 적용합니다. 점 사이를 단조 3차 보간(Fritsch-Carlson)으로 이어서, 점을 옮겨도 점과 점 사이에서 곡선이 출렁이지 않습니다. 채널마다 2~16개 점을 둘 수 있고 양 끝점은 높이만 바뀝니다.

8색 HSL은 빨강 0°·주황 30°·노랑 60°·초록 120°·청록 180°·파랑 240°·보라 270°·자홍 300°를 중심으로 색조·채도·휘도를 각각 -100~100으로 조절합니다. 픽셀의 색상각이 두 대역 사이에 있으면 가까운 쪽에 더 많이 반영되도록 두 대역에 나눠 적용합니다. 회색처럼 채도가 없는 픽셀은 HSL 조절의 영향을 받지 않습니다. 슬라이더 값이 실제로 얼마나 바꾸는지는 아래와 같습니다.

조절계산
색조 ±100색상각 ±30°
채도채도에 (1 + 값/100)을 곱함. -100이면 무채색
휘도밝기에 값/100 × 0.25 × 채도를 더함. 채도가 낮은 색일수록 덜 바뀜
그레인좌표와 시드로 정한 의사난수를 R·G·B에 같은 양으로 더함. 양 100이면 최대 ±48단계

그레인은 같은 시드면 몇 번을 다시 처리해도 같은 결과가 나옵니다. 이 순서와 계수는 데모를 위해 정한 것이며 Lightroom의 내부 계산과 같지 않습니다.

기본 프리셋 3개를 색 4개에 적용한 값

입력은 코드로 생성한 1440×960 합성 풍경 사진입니다. 하늘·해·언덕·꽃밭과 함께 아래쪽에 8색·피부톤·중간 회색 패치와 검정→흰색 계조 띠를 넣어 색 대역별 변화를 눈으로 확인할 수 있게 했습니다. 아래 표는 기본 프리셋을 그레인 없이 단일 픽셀에 적용해 계산한 값입니다.

입력 RGB웜 필름맑은 하늘소프트 모노
검정 (0, 0, 0)(26, 24, 32)(0, 0, 0)(20, 20, 20)
중간 회색 (128, 128, 128)(144, 135, 126)(129, 129, 129)(132, 132, 132)
파랑 패치 (50, 80, 200)(76, 92, 181)(13, 46, 226)(128, 128, 128)
피부톤 패치 (225, 175, 145)(225, 188, 151)(234, 192, 157)(178, 178, 178)

웜 필름은 커브의 검정점을 24로 올려 그림자를 옅게 하고 파랑 채널을 낮춰 따뜻하게 만듭니다. 맑은 하늘은 파랑 채도 +30, 휘도 -20으로 하늘을 진하게 하고 S자 커브로 대비를 올립니다. 회색은 거의 그대로이고, 밝은 피부톤은 S자 커브 때문에 조금 밝아집니다. 소프트 모노는 8개 대역의 채도를 모두 -100으로 내려 흑백을 만듭니다. 이때 파랑 패치가 중간 회색 128이 되는 점은 아래 한계에서 다시 설명합니다.

파랑 채도와 청록 색조를 올린 결과를 원본과 나란히 비교하는 8색 HSL 조절 화면

사진 필터 프리셋 JSON 구조

{
  "schema": "protocol-ai-lab.photo-filter-preset",
  "version": 1,
  "id": "warm-film",
  "name": "웜 필름",
  "created_at": "2026-10-01T00:00:00+09:00",
  "updated_at": "2026-10-01T00:00:00+09:00",
  "params": {
    "curve": { "rgb": [[0, 24], [64, 70], [192, 200], [255, 245]], "r": [[0, 0], [128, 138], [255, 255]], "g": [[0, 0], [255, 255]], "b": [[0, 12], [128, 118], [255, 238]] },
    "hsl": { "orange": { "hue": 0, "sat": -10, "lum": 10 }, "green": { "hue": -40, "sat": -30, "lum": 0 } },
    "lut": null,
    "grain": { "amount": 18, "seed": 11 }
  }
}

위 예시는 길이를 줄이려고 HSL 대역 일부만 적었습니다. 실제 파일에는 8개 대역이 모두 들어 있어야 검사를 통과합니다. schema와 version으로 형식을 구분하고, id는 영문 소문자·숫자·하이픈만 써서 다른 프로그램이 부를 필터 ID로 그대로 쓸 수 있게 했습니다. updated_at은 나중에 서버에서 프리셋을 나눠 줄 때 어느 쪽이 새 값인지 판단하는 데 쓸 수 있습니다.

LUT를 쓰는 프리셋은 "lut": { "id": "teal-orange-33", "amount": 80 }처럼 LUT의 id와 강도만 적습니다. 33³ .cube 하나가 데이터 35,937줄, 약 970KB이므로 프리셋마다 복사하지 않고 .cube 파일을 따로 배포하기 위해서입니다. 참조한 LUT가 없으면 LUT 없이 처리하고 화면에 경고합니다.

데모에서 프리셋을 만들고 내려받는 순서

조절한 값은 이름을 붙여 새 프리셋으로 저장하고, 고른 프리셋을 수정 저장하거나 삭제합니다. 가져온 JSON 파일과 브라우저에 저장된 값은 모두 같은 형식 검사를 거쳐서, 범위를 벗어난 값이나 순서가 뒤집힌 커브는 들어오지 않습니다. 결과 이미지는 최대 2400px JPG로, 대표 썸네일은 360×240 PNG로 내려받습니다.

  1. 데모를 엽니다. 처음에는 합성 샘플 사진에 ‘웜 필름’ 프리셋이 적용돼 있습니다. 자기 사진을 시험하려면 내 사진 불러오기를 누릅니다. 사람 얼굴이나 개인정보가 담긴 사진은 쓰지 않는 것이 좋습니다.
  2. 사진 아래 전후 비교 위치 슬라이더나 사진을 끌어 원본과 필터 결과의 경계를 옮깁니다.
  3. 오른쪽 톤 커브 탭에서 빈 곳을 눌러 점을 추가하고 끌어서 옮깁니다. 두 번 누르면 점이 지워집니다. HSL 탭에서는 색조·채도·휘도를 고른 뒤 8색 슬라이더를, 3D LUT 탭에서는 샘플 LUT를 고르거나 .cube 파일 불러오기로 자기 LUT를 올리고 강도를 조절합니다. 그레인 탭에서는 양과 시드를 조절합니다.
  4. 아래 프리셋 이름을 적고 새 프리셋으로 저장을 누릅니다. 썸네일 카드가 생기고, 나중에 고쳐서 선택 프리셋 수정 저장이나 선택 프리셋 삭제를 할 수 있습니다.
  5. 현재 값 JSON 받기, 대표 썸네일 PNG 받기, 결과 이미지 JPG 받기로 결과를 내려받습니다. 받은 JSON은 JSON 프리셋 가져오기로 다시 불러올 수 있습니다. 3D LUT 탭의 현재 보정을 .cube로 받기로 지금 보정을 LUT 파일로 받을 수도 있습니다.
기본 프리셋 5개와 새로 저장한 하늘 강조 프리셋의 썸네일, 프리셋 JSON 내용이 보이는 화면

시험 29건과 Chrome 자동 조작

엔진 자동 시험 29건에서는 기본 커브가 모든 값을 그대로 두는지, 커브가 지정한 점을 정확히 지나고 그 사이에서 내려가지 않는지 확인했습니다. 8색 가중치의 합이 항상 1인지, 빨강 채도 -100이 순수 빨강을 회색으로 바꾸면서 파랑은 건드리지 않는지도 봤습니다. 이 밖에 그레인이 같은 시드에서 같은 결과를 내는지, JSON으로 저장했다가 다시 읽어도 값이 같은지, 잘못된 범위·순서의 프리셋을 거부하는지를 시험했습니다. 3D LUT 쪽은 항등 LUT가 사진을 바꾸지 않는지, 색을 뒤집는 LUT가 (10, 200, 30)을 (245, 55, 225)로 바꾸는지 확인했습니다. 강도 50%가 절반만 섞는지, 잘못된 .cube 9종을 거부하는지, 굽기 결과를 .cube로 썼다가 다시 읽어도 값이 같은지도 시험했습니다.

설치된 Chrome을 자동으로 조작하는 시험도 돌렸습니다. HSL 조절과 새 프리셋 저장, 새로고침 뒤 유지, 삭제를 거치며 카드 수 5 → 6 → 6 → 5를 확인했습니다. 3D LUT 탭에서는 샘플 LUT 적용, 강도 40으로 변경, .cube 파일 불러오기, 개수가 모자란 .cube 거부, 없는 LUT를 참조하는 프리셋의 경고를 확인했습니다. 내보내기 세 가지는 실제 다운로드 파일을 열어 검사했습니다. 결과 JPG는 1440×960, 썸네일 PNG는 360×240이었고, JSON은 엔진의 형식 검사를 다시 통과했습니다. 390px 폭 모바일 화면에서 가로 넘침이 없는지도 확인했습니다. 위 화면 이미지는 이 시험 중에 저장한 것입니다.

다른 엔진으로 옮기기 전 점검표

이 데모의 프리셋을 키오스크나 네이티브 엔진으로 옮길 때, 데모에서 이미 확인한 것과 대상 환경에서 따로 재야 할 것을 나눴습니다.

점검 항목이 데모에서 확인한 것대상 환경에서 따로 확인할 것
Lightroom 결과와 색 맞추기확인하지 않음. 계수(색조 ±30° 등)는 데모용으로 정한 값원본 JPG와 Lightroom 결과 JPG 쌍으로 패치·영역별 색 차이(ΔE)를 재고, 차이가 큰 조절의 계수를 고침
프리셋을 3D LUT로 굽기합성 사진에서 33³ 평균 차이 0.17~0.39, 최대 2~4실제 사진과 실제로 쓸 LUT로 같은 비교. 삼선형 보간과 사면체 보간 비교
그레인처럼 위치에 따라 달라지는 효과LUT에 넣을 수 없어 굽기에서 빠짐LUT 조회 뒤 따로 적용
처리 속도Apple M4, JavaScript 기준 값만 있음대상 PC 사양과 실제 사진 해상도로 측정
다른 곳에서 만든 .cube수식으로 만든 샘플 2개와 굽기 결과만 시험실제로 쓸 LUT 파일이 위 읽기 규칙을 통과하는지 확인
호출 방식프리셋 id를 필터 ID로 쓸 수 있게 형식만 정함입력(이미지 경로·필터 ID)과 출력(결과 경로·오류 코드)을 고정해 문서화. 프로세스를 띄우는 CLI와 상주하는 로컬 API 가운데 선택
요구의 뜻“노이즈”를 그레인으로 해석노이즈 감소인지 그레인인지, 결과 해상도와 JPG 품질, 프리셋 개수와 배포 주기

Lightroom의 내부 계산은 공개돼 있지 않아서 같은 이름의 슬라이더라도 결과가 같다고 가정할 수 없습니다. 보간은 이 데모의 삼선형 대신 색 경계에서 더 정확하다고 알려진 사면체 보간도 비교할 대상입니다. 호출 방식은 CLI가 가장 단순하고, 연속 촬영에서 시작 지연이 문제가 되면 로컬 API가 대안이 될 수 있습니다.

이 데모에 없는 것과 흑백·그레인·LUT의 한계

이 데모는 브라우저 JavaScript로만 동작합니다. Windows 프로그램, 네이티브 엔진, Flutter 키오스크 연동, 설치 프로그램은 없습니다. 처리 순서와 계수는 데모를 위해 정한 것이라 Lightroom과 같은 결과를 보장하지 않습니다.

흑백은 HSL의 밝기(최댓값과 최솟값의 평균)를 그대로 써서 만듭니다. 그래서 눈에는 어둡게 보이는 파랑 패치도 128의 중간 회색이 됩니다. 사람 눈의 밝기에 맞춘 흑백이나 Lightroom의 흑백 혼합과는 다르며, 실제 엔진에서는 따로 설계해야 합니다. 그레인은 픽셀 단위라 미리보기와 큰 결과 이미지에서 입자 크기가 달라 보입니다. 내 프리셋은 이 브라우저의 저장소에만 최대 50개까지 남고, 브라우저 데이터를 지우면 사라집니다. 큰 사진은 처리와 내보내기를 위해 긴 변 2400px로 줄입니다.

3D LUT는 삼선형 보간만 쓰고 사면체 보간은 지원하지 않습니다. 직접 만든 샘플 LUT와 굽기 결과로만 시험했으므로 다른 곳에서 만든 .cube 파일이 그대로 읽히는지는 확인이 필요합니다. 샘플 LUT 두 개는 수식으로 만든 것이며 실제 필름이나 상용 LUT를 흉내 낸 것이 아닙니다. 불러온 LUT는 화면을 닫으면 사라지고 프리셋에는 id만 남습니다.

부록. 포토부스 필터 엔진 공고 요구와의 대조

이 데모는 위시켓에 올라온 ‘포토부스 전용 필터 관리 프로그램 및 렌더링 엔진 구축’ 공고의 요구를 2026년 10월 1일에 읽고 만들었습니다. 아래 표는 그 요구와 이 데모가 실제로 하는 일을 나란히 놓은 것입니다.

원 공고 요구이 데모상태
원본 이미지 불러오기, HSL·톤 커브·노이즈 등 조절 UI합성 샘플 또는 내 사진(JPG·PNG·WebP) 불러오기, 톤 커브(RGB·R·G·B), 8색 HSL, 그레인웹으로 구현. 노이즈는 그레인으로 해석
실시간 미리보기와 원본 전후 비교960px 미리보기, 좌우 분할 비교 슬라이더구현
JSON 프리셋 저장·수정·삭제브라우저 저장소에 저장·수정·삭제, JSON 내보내기·가져오기, 형식 검사구현 (서버 저장 없음)
대표 썸네일 자동 생성·Export불러온 사진으로 프리셋별 썸네일 생성, PNG 내려받기구현
3D LUT(.CUBE) 파싱·적용.cube 파일 불러오기(샘플 2개 포함), 강도 0~100 적용, 현재 보정을 33³ .cube로 굽기구현. 고객 LUT 파일 호환성은 미검증
GPU 없는 매장 PC 최적화브라우저 JavaScript 처리 시간만 표시미검증
Flutter 키오스크에서 이미지 경로·Filter ID로 호출없음. 프리셋 id를 Filter ID로 쓸 수 있게 형식만 정함미구현
Windows 관리자 프로그램, 소스·연동 문서·설치 프로그램웹 데모와 프리셋 구조 설명(README)범위 밖

공고의 “노이즈”가 Lightroom의 노이즈 감소(세부 패널)인지 그레인 효과인지 원문만으로는 알 수 없었습니다. 포토부스 필터의 질감 표현에 가까운 그레인으로 구현했고, 실제로 필요한 쪽은 확인이 필요한 항목으로 남깁니다.

요구사항 반영률

공고 요구를 낱개 28개로 나눠 대조하면 반영 14개, 부분 반영 9개, 미반영 5개입니다. 부분 반영을 반으로 쳐서 계산하면 66%, 완전히 반영한 것만 세면 50%입니다.

영역요구 수반영부분미반영반영률
1. 수행 범위402225%
2-1. 관리자 프로그램(Filter Manager)12111096%
2-2. 렌더링 엔진(Filter Engine)523070%
2-3. Flutter 키오스크 연동302133%
결과물411238%
합계28149566%

부분·미반영 14개 가운데 7개는 플랫폼이 달라서(Windows 앱·네이티브 엔진·Flutter·설치), 4개는 고객 자료(Lightroom 기준 사진·LUT·더미 호출 프로그램)가 없어서 생겼습니다. 나머지 3개는 서버·배포 환경 없음, 요구 해석 미확정, 매장 PC 없음이 원인입니다. 웹 데모가 공고 범위의 어디까지 닿는지 보여 주는 숫자일 뿐, 납품물의 완성도를 잰 값은 아닙니다.

AI 코딩 도구(Claude Code)로 만든 부분

공고 요구사항 정리, 데모 코드, 샘플 LUT 생성 스크립트, 자동 시험, 화면 캡처 스크립트, 이 글의 초안을 AI 코딩 도구(Claude Code)로 작성했습니다. 시험과 측정도 이 도구로 작성자 기기에서 실행했습니다. 사람은 후보를 고르고 화면과 글을 검토한 뒤 공개를 결정했습니다. 데모 자체는 AI 모델을 호출하지 않습니다. 사진 처리는 모두 규칙 기반 JavaScript이고 외부 라이브러리를 쓰지 않습니다. 이 데모는 고객의 의뢰로 제작하거나 납품한 결과물이 아닙니다. 이번 개정본에서 측정값을 앞으로 옮기고 공고 대조를 부록으로 내린 재구성도 같은 AI 코딩 도구로 했으며, 글의 숫자는 데모 저장소의 시험·측정 기록과 다시 대조했습니다.

2026년 10월 8일 J. Kim이 이 개정본을 읽고 승인했습니다.

사진 필터 프리셋 데모 열기

직접 만들고 시험한 다른 계산기·통신 도구·데모는 Tools 페이지에 모아 두었습니다.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top