호기심 상자: 색공간에서 보는 세 개의 “중간”
첫 절에서 우리는 빛(덧셈)과 필터(곱셈)를 보았다. 빛은 파장마다 밝기를 더하고, 필터는 파장마다 통과율을 곱했다. 그런데 질문이 하나 더 있다. 색을 표현하는 좌표계도 하나가 아니다.
두 좌표계
RGB — 빨강, 초록, 파랑의 밝기. 컴퓨터 모니터가 쓰는 좌표. (255, 0, 0)은 빨강, (0, 255, 0)은 초록.
HSV — 색상(Hue), 채도(Saturation), 명도(Value). 화가가 쓰는 좌표. “빨간색을 좀 탁하게” = 채도를 낮추는 것.
같은 색이다. 그런데 주소가 다르다. 서울시 강남구 역삼동 123 = 37.5°N, 127.0°E.
하나 더. 모니터의 0~255 값은 빛의 밝기 그대로가 아니다. 사람 눈에 맞추느라 한 번 구부려(감마 보정) 저장한 값이다. 빛을 더하거나 필터를 곱하는 물리 계산은 구부림을 편 "선형 RGB"에서 해야 한다.
세 개의 보간
같은 양 끝 색 두 개를 고르고, 세 가지 방법으로 중간색을 만든다.
① 덧셈 — 선형 RGB에서 직선 보간. 두 빛을 섞는 것. (1−t)·RGB₁ + t·RGB₂
② 곱셈 — 선형 RGB에서 채널별 가중 기하평균. 필터를 겹치는 것. RGB₁^(1−t) · RGB₂^t
③ HSV 덧셈 — HSV 좌표에서 직선 보간. 색상(Hue)은 원형이라 짧은 호를 따라 돈다.
HSV에는 곱셈이 없다. 색상은 각도라서 "각도의 기하평균"은 뜻이 없기 때문이다. 연산은 그것이 뜻을 갖는 좌표에서만 쓸 수 있다. log가 양수에만 쓰이는 것과 같은 이야기다.
세 경로의 중간색이 다르다. 실제 셀로판처럼 어느 채널도 0이 아닌 색으로 해 보자. 노랑 (255, 230, 60) ↔ 시안 (60, 220, 255): 덧셈은 연한 초록 (191, 225, 191), 곱셈은 그보다 진한 초록 (127, 225, 127)이다. 두 색이 함께 강한 초록 채널은 225 그대로 남고, 한쪽이 약한 빨강과 파랑 채널은 줄었다. 곱셈은 두 필터가 함께 통과시키는 빛만 남긴다. AND 그대로다.
빨강 (230, 60, 50) ↔ 초록 (70, 200, 60): 덧셈은 탁한 황토색 (175, 151, 55), HSV 덧셈은 밝은 노랑 (215, 214, 56)을 거친다. 곱셈의 한가운데는 어두운 올리브색 (129, 112, 55)이다. 둘이 함께 강한 채널이 없으니 모든 채널이 줄어 어두워진다. 채널 하나가 아예 0인 순색 빨강 (255, 0, 0)과 순색 초록 (0, 255, 0)이라면 함께 가진 채널이 하나도 없어서, 곱셈의 한가운데는 검정이 된다.
빨강 (230, 50, 60) ↔ 파랑 (60, 80, 230): 덧셈 (173, 67, 173)도 HSV 덧셈도 보라를 거친다. HSV 쪽이 더 밝고 선명한 자홍 (215, 55, 230)이다.
불러오는 중…
파이썬
import numpy as np
def to_linear(c): # sRGB(0~255) → 빛의 밝기(선형 RGB, 0~1)
c = np.asarray(c) / 255
return np.where(c <= 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4)
def to_srgb(l): # 선형 RGB → sRGB(0~255)
s = np.where(l <= 0.0031308, 12.92 * l, 1.055 * l ** (1 / 2.4) - 0.055)
return np.round(s * 255).astype(int)
pairs = {"노랑↔시안": ([255, 230, 60], [60, 220, 255]),
"빨강↔초록": ([230, 60, 50], [70, 200, 60]),
"빨강↔파랑": ([230, 50, 60], [60, 80, 230]),
"순색 빨강↔초록": ([255, 0, 0], [0, 255, 0])}
for name, (c1, c2) in pairs.items():
l1, l2 = to_linear(c1), to_linear(c2)
add = to_srgb(0.5 * l1 + 0.5 * l2) # 덧셈: 두 빛을 반씩
mul = to_srgb(np.sqrt(l1 * l2)) # 곱셈: 채널별 기하평균
naive = np.round(0.5 * np.array(c1) + 0.5 * np.array(c2)).astype(int) # 감마 값을 그대로 평균
print(f"{name} 덧셈 {add} 곱셈 {mul} (감마 값 그대로 평균 {naive})")
# 노랑↔시안 덧셈 [191 225 191] 곱셈 [127 225 127] (감마 값 그대로 평균 [158 225 158])
# 빨강↔초록 덧셈 [175 151 55] 곱셈 [129 112 55] (감마 값 그대로 평균 [150 130 55])
# 빨강↔파랑 덧셈 [173 67 173] 곱셈 [120 64 120] (감마 값 그대로 평균 [145 65 145])
# 순색 빨강↔초록 덧셈 [188 188 0] 곱셈 [0 0 0] (감마 값 그대로 평균 [128 128 0])
빨강↔초록에서 감마 값을 그대로 평균하면 (150, 130, 55), 칙칙한 갈색에 가까운 올리브가 나온다. 빛을 제대로 섞으면 (175, 151, 55)로 더 밝다. 좌표계를 잘못 고르면 같은 "덧셈"도 다른 색을 낸다.
이것이 왜 중요한가
“같은 두 점 사이의 '중간’이 좌표계에 따라 다르고, 연산에 따라 다르다.” 이 장에서 우리는 연산이 다르면 중간이 다르다는 것을 보았다. 여기서 한 층 더: 좌표계가 달라도 중간이 다르다. 좌표계 × 연산 = 기하학. 앞에서 확률 좌표의 직선(m-길)과 로그 비 좌표의 직선(e-길)이 다른 곳을 지나간 것도 같은 이야기다.
수확
“연산 × 좌표계 — 이 두 선택이 '곧은 길’을 결정한다.”