AI 생성 시연 사진 / 편집 전
DEVELOPMENT JOURNAL / 04
Photoshot
GITHUB × AI
무료 포토샵 개발기

곡선을 만드는 계산

곡선을 픽셀 값으로 바꾸는 구현도 이미 GitHub에 공개돼 있었다. 무료 포토샵 개발에서는 glfx.js의 조회표 구조를 읽고, AI와 포토샷의 보간·조작·저장 규칙을 연결했다. 기존 아이디어를 이해하는 것에서 시작해 실제 편집기의 동작으로 만드는 과정을 따라간다.

PHOTOSHOT DEVELOPMENT JOURNAL / 04

AI 생성 시연용 사진. 편집 화면과 출력 이미지는 실제 포토샷에서 만들었습니다.

Photoshot무료 포토샵 개발기 / 04FOUNDATION

출처에서 시작하다.

곡선 처리의 출발점은 공개돼 있었다

필요한 모든 것을 처음부터 발명하지 않아도 되는 이유는 실제로 읽을 수 있는 구현이 있기 때문이다. 다만 공개된 곡선 필터와 우리 편집기는 렌더링 방식과 문서 구조가 달랐다. AI와 할 일은 그 구조를 이해해 필요한 부분을 포토샷 방식으로 구현하는 것이었다.

evanw/glfx.js의 src/filters/adjust/curves.js는 제어점으로 보간기를 만들고 256개 값을 준비한 뒤 채널별 조회에 사용한다. 원 개발 기록에도 이 LUT 아이디어를 검토했다고 남아 있다. 픽셀마다 점 사이를 새로 계산할 필요가 없다는 것이 핵심이었다.

glfx.js의 WebGL 필터 전체를 포토샷에 붙인 것은 아니다. 포토샷은 별도의 보간 함수를 작성하고 기존 compileAdjustment에 넣었다. 외부 코드에서 얻은 구조와 우리 코드에서 선택한 계산법을 구분해야 한다.

첫 구현은 형태 보존 Hermite

처음 작성한 curveTable은 형태 보존 Hermite 보간이다. 아래 코드는 각 점의 기울기를 정하는 부분이다. 앞뒤 기울기의 부호가 달라지면 0으로 두어 급격한 편집 주변에서 곡선이 불필요하게 튀는 것을 줄이려 했다.

이 코드는 glfx.js에서 복사한 보간 함수가 아니다. LUT로 계산을 미리 준비하는 구조를 참고하되, 포토샷에 넣을 수학과 TypeScript 구현은 별도로 작성했다.

AI와 작성한 보간 함수는 기존 조회표 엔진을 이용했다. 따라서 곡선 조정을 추가하면서도 마스크와 레이어 합성을 새로 만들지 않았다. 외부 구현에서 얻은 설계와 내부의 공통 코드가 연결되는 방식이다.

src/adjustments.ts · v0.16.0
L380–388
포토샷 구현 코드 ↗
const m = points.map((_, i) =>
  i === 0
    ? d[0]
    : i === n - 1
      ? d[n - 2]
      : d[i - 1] * d[i] <= 0
        ? 0
        : 2 / (1 / d[i - 1] + 1 / d[i]),
);
Photoshot무료 포토샵 개발기 / 04IMPLEMENTATION

계산의
안쪽으로.

비교 후 자연 3차 스플라인을 추가했다

점 네 개가 같아도 점 사이를 잇는 방식이 다르면 사진의 중간 밝기가 달라진다. 비교 작업 후 업그레이드 경로에는 naturalCurveTable을 추가했다. 아래 코드는 자연 3차 스플라인의 곡률을 계산하기 위한 전진 계산 부분이다.

“AI가 곡선 기능을 만들었다”라는 말만으로는 이 변화가 보이지 않는다. GitHub의 초기 아이디어를 우리 함수로 만들고, 결과를 비교해 보간법을 다시 선택한 것이 실제 개발 과정이었다.

화면과 픽셀에 같은 버전의 곡선을 적용하기

새 계산을 모든 저장 파일에 강제로 적용하면 예전에 만든 작업의 색이 달라질 수 있다. 그래서 revision 2는 자연 스플라인, 기존 작업은 Hermite 경로를 유지한다. 곡선 편집기의 표도 같은 조건으로 선택한다.

픽셀 처리에서는 개별 채널 표를 먼저 읽고 공통 RGB 표를 이어서 적용한다. 소수 위치는 인접 두 칸 사이를 보간한다. 외부에서 찾은 LUT 개념이 포토샷의 채널 순서와 버전 규칙에 맞게 조합되는 지점이다.

PHOTOSHOT / SOURCE NOTES04

코드를 읽다.

src/adjustments.ts · v0.16.0
L414–424
포토샷 구현 코드 ↗
for (let i = 1; i < n - 1; i++) {
  const span = points[i + 1].x - points[i - 1].x,
    sig = (points[i].x - points[i - 1].x) / span,
    p = sig * y2[i - 1] + 2;
  y2[i] = (sig - 1) / p;
  const slope =
    (points[i + 1].y - points[i].y) / (points[i + 1].x - points[i].x) -
    (points[i].y - points[i - 1].y) / (points[i].x - points[i - 1].x);
  u[i] = ((6 * slope) / span - sig * u[i - 1]) / p;
}
for (let i = n - 2; i >= 0; i--) y2[i] = y2[i] * y2[i + 1] + u[i];
src/curves-editor.tsx · v0.16.0
L43–43
포토샷 구현 코드 ↗
const table = (value.revision === 2 ? naturalCurveTable : curveTable)(points),
src/adjustments.ts · v0.16.0
L529–535
포토샷 구현 코드 ↗
if (t === 'curves') {
  x = ct![channel as Channel][i];
  const f = clamp(x) * 255,
    lo = Math.floor(f),
    hi = Math.min(255, lo + 1);
  x = ct!.rgb[lo] + (ct!.rgb[hi] - ct!.rgb[lo]) * (f - lo);
}
Photoshot무료 포토샵 개발기 / 04IN PRACTICE

편집 화면에서.

실제 포토샷 작업 화면. 설정과 사진을 함께 확인할 수 있습니다.
실제 포토샷 작업 화면. 설정과 사진을 함께 확인할 수 있습니다.

같은 점, 다른 곡선이라는 문제를 검증하기

사진 예시의 RGB 점은 (0,0), (64,80), (192,176), (255,255)다. 인물 원본은 AI 생성 예시 이미지이고, 아래 화면과 결과는 포토샷에서 얻었다.

과거 비교 차트는 (0,0), (64,48), (192,208), (255,255)를 사용했다. 기록된 해당 차트에서는 수정 후 평균·최대 차이가 모두 0이었다. 특정 입력의 일치 결과이며, 모든 점 배치와 사진에서 같은 결과를 보장한다는 뜻은 아니다.

Photoshot무료 포토샵 개발기 / 04BEFORE / AFTER

사진으로 보는 결과.

AI 생성 시연 사진 / 편집 전
01 / AI 생성 시연 사진 / 편집 전
포토샷 출력 / 편집 결과
02 / 포토샷 출력 / 편집 결과

가져온 아이디어를 제품의 규칙에 맞추기

무료 포토샵의 곡선 개발에서 glfx.js는 읽을 수 있는 실제 출발점이었다. LUT라는 구조는 참고했고, Hermite·자연 스플라인 함수와 편집기 연결, 예전 작업 호환 처리는 AI와 별도로 작성·수정했다.

이 사례의 핵심은 공개된 곡선 아이디어와 기존 엔진 사이를 AI와 작성한 코드가 이어 주었다는 데 있다. 결과를 비교하고 보간법과 호환 처리를 수정하는 일도 그 연결 작업에 포함됐다. 조합은 복사 한 번이 아니라 제품의 규칙에 맞추는 과정이었다.

공개 코드의 가치는 완성된 기능을 그대로 가져오는 데만 있지 않았다. 구현을 읽어 설계를 이해하고, 우리 레이어 구조 안에서 다른 기능과 함께 움직이게 만드는 데 있었다. 다음 편에서는 노출 계산과 버전 호환 코드를 이어서 본다.