5.4. 픽셀 읽기와 쓰기¶
이미지에 대한 대부분의 연산은 단일 메서드 호출 안에 픽셀별 작업을 숨겨 두며, 모든 픽셀을 훑는 루프는 네이티브 속도로 실행됩니다. 하지만 애플리케이션 코드가 특정 픽셀 하나에 직접 접근하고 싶은 경우도 있습니다. 특정 위치에 무엇이 있는지 읽거나, 새 값을 하나 써넣거나, 보정 단계를 위해 한 점을 샘플링하거나, 알려진 위치의 값을 디버깅하는 경우 등입니다. image 모듈은 두 가지 주소 지정 형식을 통해 이 수준의 접근을 제공하며, 각각은 픽셀이 어디에 있는지를 생각하는 서로 다른 방식에 맞춰져 있습니다.
5.4.1. 좌표로 주소 지정하기¶
가장 자연스러운 형식은 좌표(Coordinates)에서 이미 어휘를 다진 방식, 즉 픽셀을 데카르트 좌표 (x, y) 로 지칭하는 것입니다. get_pixel() 은 (x, y) 를 받아 해당 위치의 값을 반환하고, set_pixel() 은 동일한 (x, y) 와 값을 함께 받아 그 값을 써넣습니다.
이러한 호출이 반환하거나 받아들이는 것은 이미지의 형식에 따라 달라집니다. 그레이스케일, 이진, 베이어(Bayer) 이미지는 픽셀당 단일 값을 가지므로 – 그레이스케일은 밝기, 이진은 0 또는 1, 베이어는 단일 색상 채널 샘플 – get_pixel() 은 단일 정수를 반환합니다. RGB565는 세 색상 채널을 16비트에 패킹해 담으며, get_pixel 은 기본적으로 이를 (r, g, b) 튜플로 언패킹하고, 각 채널은 0 – 255 범위로 매핑됩니다.
기본 동작은 양쪽 모두에서 뒤집을 수 있습니다. RGB565 이미지에서 get_pixel 에 rgbtuple=False 를 전달하면 원시 16비트 패킹 워드로 되돌아갑니다 – 이는 선형 인덱스가 반환하는 형식과 동일하며, 애플리케이션이 동일한 패킹 값을 그대로 다시 써넣을 때 효율적인 형식입니다. 단일 채널 이미지에 rgbtuple=True 를 전달하면 그 반대로 동작합니다. 저장된 값이 반환되기 전에 RGB888 튜플로 변환되며, 베이어 이미지는 즉석 디베이어 단계를 거칩니다. 이 인수는 호출 코드가 기반 이미지가 어떻게 저장되어 있든 상관없이 균일한 색상 공간으로 픽셀을 요청할 수 있도록 존재합니다.
압축된 이미지 – JPEG와 PNG – 는 get_pixel 이나 set_pixel 에서 지원되지 않습니다. 그 바이트들은 알려진 위치의 픽셀을 나타내지 않으며, 이들 메서드는 아무 의미도 없는 값을 반환하기보다 오류를 발생시킵니다.
실제로 패턴은 다음과 같습니다:
v = img.get_pixel(40, 30) # grayscale: int 0..255
img.set_pixel(40, 30, 255) # write white
r, g, b = img.get_pixel(40, 30) # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0)) # write red
요청한 (x, y) 가 이미지 바깥에 있으면 get_pixel 은 None 을 반환하고 set_pixel 은 아무 일도 하지 않습니다. 이는 의도적으로 관대하게 설계된 것입니다. 많은 알고리즘이 이미지의 가장자리 가까이를 따라 이동하다 잠시 범위를 벗어난 위치를 인덱싱하는데, 매번 예외를 발생시키는 것보다 조용한 무동작(no-op)이 덜 방해가 되기 때문입니다.
5.4.2. 선형 인덱스로 주소 지정하기¶
다른 형식은 기반 버퍼 내에서의 위치로 픽셀을 주소 지정하는 것입니다. 버퍼의 레이아웃을 떠올려 보세요. 픽셀은 행 단위로 저장되어 맨 위 행의 모든 픽셀이 먼저 오고, 그다음 행의 모든 픽셀이 오는 식으로 맨 아래까지 이어집니다. 이 배치는 모든 픽셀이 왼쪽 위에서 0 부터 세기 시작해 각 행을 따라 차례로 증가하는 단일 정수 인덱스를 가진다는 뜻입니다. 좌표 (x, y) 의 픽셀은 선형 인덱스 y * width + x 를 가집니다.
픽셀은 데카르트 좌표 (x, y) 와, 버퍼를 행 단위로 왼쪽에서 오른쪽으로 훑는 선형 인덱스 양쪽 모두로 주소 지정됩니다.¶
image 모듈은 이 인덱스를 일반적인 Python 첨자 표기법으로 노출합니다. img[i] 는 선형 인덱스 i 의 픽셀을 읽고, img[i] = value 는 하나를 씁니다. 인덱스 형식이 반환하는 것은 해당 형식의 원시 저장 값 이며, get_pixel() 이 기본적으로 반환하는 언패킹된 튜플이 아닙니다. 이 구분이 중요한 이유는 앞서 선택한 형식이 원시 값이 어떻게 보일지를 결정하기 때문입니다:
그레이스케일과 베이어 픽셀은 8비트 정수로 돌아옵니다.
RGB565와 YUV422 픽셀은 16비트 정수 – 패킹 워드 – 로 돌아옵니다.
이진 픽셀은
0또는1로 돌아옵니다.JPEG와 PNG 픽셀은 압축 스트림을 한 번에 한 바이트씩, 8비트 정수로 돌아옵니다. 이 값들은 불투명합니다 – 일반적인 의미의 픽셀이 아니라 압축 인코딩의 조각들입니다.
인덱스 형식은 이미 버퍼 오프셋의 관점에서 생각하는 코드에 잘 맞습니다. 모든 픽셀을 한 번씩 훑는 루프, 한 번에 한 행씩 건너뛰어야 하는 알고리즘, 또는 버퍼 레이아웃 간에 변환하는 코드 같은 것들입니다. x와 y 좌표의 관점에서 생각하는 코드는 get_pixel 과 set_pixel 이 더 적합합니다. 두 형식은 서로 다른 사고 모델을 통해 동일한 픽셀을 주소 지정합니다.
Image 은 반복 가능(iterable)하기도 합니다. for v in img: 는 동일한 행 우선(row-major) 순서로 버퍼를 훑으며 한 번에 한 픽셀씩 원시 값을 산출하고, len(img) 는 비압축 형식의 경우 픽셀 개수, 압축 스트림의 경우 바이트 개수입니다.
5.4.3. 픽셀별 Python이 느린 경로인 이유¶
솔직히 짚고 넘어갈 만한 실용적인 사항입니다. Python에서 이미지를 한 번에 한 픽셀씩 훑는 것은 느립니다. 320 × 240 그레이스케일 이미지는 76,800개의 픽셀을 담습니다. for 루프에서 각 픽셀마다 get_pixel() 을 호출하면 동등한 네이티브 메서드가 수백 마이크로초면 끝낼 작업을 위해 수백만 개의 MicroPython 바이트코드 명령을 실행하게 됩니다. 이는 작은 차이가 아닙니다. 프레임을 실시간으로 처리하는 스크립트와 카메라의 프레임 레이트에 한참 못 미쳐 기어가는 스크립트의 차이입니다.
Image 표면의 거의 모든 메서드는 흔한 픽셀별 패턴에 대해 더 빠른 네이티브 버전이 존재하기 때문에 존재합니다. 두 이미지를 더하는 루프는 단일 네이티브 호출이 됩니다. 각 픽셀을 이웃들과 평균하여 부드럽게 하는 루프는 또 다른 호출이 됩니다. 각 픽셀을 임계값에 대해 분류하는 루프는 세 번째 호출이 됩니다. 애플리케이션의 임무는 대부분의 경우 그 루프가 했을 작업에 어떤 전체 이미지 메서드가 들어맞는지 알아보고, 루프를 직접 작성하는 대신 그것을 사용하는 것입니다.
픽셀 수준의 읽기와 쓰기는 다른 어떤 것도 들어맞지 않을 때 여전히 올바른 도구입니다 – 특정 측정값을 버퍼에 다시 패치하거나, 보정 단계를 위해 한 위치를 샘플링하거나, 알려진 위치의 값을 디버깅하는 경우입니다. 요점은 이것들이 느린 경로라는 점이며, 전체 이미지 메서드에 애플리케이션이 필요로 하는 형식이 없을 때 사용하는 것이지 픽셀에 대해 작업하는 기본 방식으로 쓰는 것이 아니라는 점입니다.