- RGB 정규화에서 낯선 이미지 파일을 처리해 다시 8비트로 저장하는 일반 상황이라면 255로 나누는 표준 방식이 적합함
- 255 방식은 0을 0.0, 255를 1.0으로 매핑해 검은색과 흰색을 직접 다루기 쉽고, GPU의 UNORM-to-float 변환 방식과도 맞음
- 256 방식은 (img + 0.5) / 256.0으로 각 값을 구간 중앙에 놓아 디더링 같은 작업에서 경계 처리를 단순하게 만들 수 있지만, 0이 0.0이 아니어서 처리 로직이 8비트 입력에 묶임
- 255 방식은 양 끝 구간이 절반 폭이라 균일한 [0, 1] 난수를 다시 8비트로 반올림하면 0과 255가 다른 값보다 절반 빈도로 나오지만, 실제 이미지 왕복 변환은 손실 없이 동작함
- 256 방식은 이론상 평균 절대 오차가 1 / 1024로 255 방식의 1 / 1020보다 작지만, 이미 255 방식으로 양자화된 이미지를 잘못된 스케일로 읽으면 오히려 오차를 더함
문제 설정
이미지 처리 프로그램은 8비트 이미지를 부동소수점으로 바꾸고, 처리를 수행한 뒤 다시 8비트 색상으로 저장함
두 변환 방식은 다음과 같음
# 표준: 255로 나누기 pixels = img / 255.0 result = process(pixels) output = np.trunc(result * 255 + 0.5) # 대안: 0.5를 더하고 256으로 나누기 pixels = (img + 0.5) / 256.0 result = process(pixels) output = np.trunc(result * 256)두 방식 모두 최종 변환 전에 값을 0~255로 제한함
output_8bit = output.clip(0, 255).astype(np.uint8)표준 방식은 정수 0을 0.0, 255를 1.0에 매핑하며 GPU의 UNORM-to-float 변환 방식과 같음
대안 방식은 0을 0.5 / 256 = 0.001953125에 매핑하므로, 검은 픽셀을 감지하려면 이 상수를 알아야 함
255로 나누는 표준 방식의 특성
표준 방식은 [0, 1] 범위 안에서 양 끝 값의 구간이 다른 구간보다 사실상 절반 폭이 됨
균일한 [0, 1] 난수를 만들고 trunc(result * 255 + 0.5)로 반올림하면 0과 255는 다른 정수보다 절반 빈도로 나타남
하지만 원래 8비트 이미지는 uint8 → float → uint8 왕복 변환에서 손실 없이 돌아옴
또한 처리 결과가 0.0이나 1.0을 약간 벗어나도 클램프와 반올림으로 올바른 정수 구간에 들어갈 수 있음
예를 들어 부동소수점 색상에서 0.005를 빼면 표준 방식의 검정은 음수가 되지만, 최종 결과는 여전히 정수 0이 됨
trunc(255 * (-0.005) + 0.5) = 0부동소수점 정확성과 구간 중앙 배치
255 방식의 값은 일부가 정확히 표현되지 않음
예를 들어 128 / 255.0 ≈ 0.501961이지만 128 / 256.0 = 0.5임
이 차이는 32비트 부동소수점의 23비트 가수에서 최하위 비트 수준의 반올림 오차이며, 크기는 2^-23보다 작음
따라서 이 부정확성은 실제 기술적 문제라기보다 미적인 문제에 가까움
256 방식은 각 부동소수점 값을 두 정수 사이의 정확한 중앙에 놓음
이 성질은 원래 양자화된 값이 정확히 무엇이었는지 모를 때 두 연속 정수 사이의 평균점을 쓰는 절충으로 볼 수 있음
Andrew Kesler의 2015년 글 “Converting Color Depth”는 이 방식이 디더링에서 노이즈를 더할 때 경계 처리를 덜 신경 쓰게 만든다고 봄
반대로 표준 방식의 양끝 구간은 노이즈 분포를 일관되게 유지하려면 주의 깊은 처리가 필요함
양자화 관점
두 방식은 균일 스칼라 양자화기(uniform scalar quantizer)로 볼 수 있음
Wikipedia의 quantization 설명)은 signed input data의 균일 양자화기를 주로 mid-riser와 mid-tread로 나눔
mid-tread는 0값 재구성 레벨을 가지며, mid-riser는 0값 분류 임계값을 가짐
공식은 다음처럼 대응됨
| mid-tread | k = trunc(x L + 0.5) | y_k = k / L |
| mid-riser | k = trunc(x L) | y_k = (k + 0.5) / L |
표준 방식은 L=255를 쓰는 mid-tread 형태이고, 대안 방식은 L=256을 쓰는 mid-riser 형태임
표준 방식은 0.0과 1.0에 양끝을 맞추는 프로그래밍 편의를 얻는 대신, 8비트 입력에 최적인 구간 배치와는 다름
재구성 오차와 실제 이미지 처리
균일 분포의 실수 x ∈ [0, 1]를 8비트 정수로 인코딩하고 다시 실수로 재구성하는 시스템을 직접 설계한다면 256 방식이 이론상 더 정밀함
표준 방식의 표현 가능 범위는 [-0.5 / 255, 255.5 / 255]가 되어 [0, 1]에 꼭 필요한 것보다 구간 간격이 넓어짐
StackOverflow 사용자 Peter Mudrievskij의 계산에 따르면 평균 절대 오차는 255 나누기에서 1 / 1020, 256 나누기에서 1 / 1024임
하지만 이미 저장된 8비트 RGB 이미지를 읽어 처리하는 상황에서는 저장 당시 잃어버린 정보가 복원되지 않음
이미지가 255를 곱하고 반올림하는 방식으로 양자화됐다면, 로드할 때 256으로 나누어도 정밀도가 돌아오지 않음
다른 사람이 만든 이미지는 대부분 표준 방식으로 양자화됐을 가능성이 높으므로, 대안 공식으로 읽으면 이론적으로 잘못된 스케일 팩터를 쓰게 됨
실제로는 색상이 절대 측정값처럼 동작하지 않아, 약간 더 작은 범위와 작은 오프셋에서 처리하는 결과가 됨
두 양자화기의 인코딩 단계와 디코딩 단계를 섞으면 깨진 코드가 됨
결론
낯선 사람이 제공한 이미지를 처리한다면 RGB 값은 255로 정규화해야 함
부동소수점 값이 정확하지 않다는 이유나 추상적인 재구성 오차가 더 크다는 느낌만으로 256 방식을 선택할 근거는 약함
이미지 저장과 로딩을 모두 제어하고, 0이 0에 매핑될 필요가 없으며, 처리 코드가 8비트 동적 범위에 묶여도 괜찮다면 256으로 나누어 약간 더 높은 이론적 정밀도를 노릴 수 있음

1 week ago
10








English (US) ·