영상 배열을 먼저 읽는다
컬러 영상은 흔히 높이 H/너비 W/채널 C의 배열이다. H는 행 수, W는 열 수이며 C=3은 세 색 성분을 뜻할 수 있다. 픽셀 좌표를 가로 u/세로 v로 쓰면 배열 접근은 image[v,u] 순서다. 가로/세로와 행/열을 바꾸면 같은 shape의 정사각형 영상에서도 다른 점을 읽는다. 여러 영상의 batch나 채널 우선 배열은 별도 축 계약이므로 모양만 보고 추정하지 않는다.
그림 M12.01-1. 각 칸은 서로 다른 역할/근거다. 나란히 비교할 항목이며 위아래가 시간 순서를 뜻하지 않는다. 직접 제작한 개념도이며 본문의 조건과 함께 읽는다.
OpenCV의 일반적인 imread 컬러 경로는 BGR 순서를 쓴다. RGB를 기대하는 모델/표시기로 옮길 때 cvtColor의 변환 코드를 명시한다. 공식 색변환 문서의 COLOR_BGR2RGB와 COLOR_BGR2GRAY 등은 이 입력 의미의 일부다. ‘모든 영상은 BGR’이라는 뜻은 아니며 ROS Image.encoding이나 외부 라이브러리 출력 계약도 따로 확인한다. 파일 읽기가 실패하면 빈 값을 다음 연산에 넘기기 전에 경로/형식과 읽기 결과를 검사한다.
색 순서 오류는 단순한 모양 오류처럼 즉시 예외가 나지 않을 수 있다. 회색 변환의 가중 합을 G=0.299R+0.587G_c+0.114B라고 하자. 여기서 G는 회색 출력, G_c는 입력 초록 성분으로 구분한다. RGB[255,0,0]을 올바르게 읽은 부동소수 합은 76.245다. 같은 배열을 BGR로 오해하면 파란색이라 29.07이 된다. 두 값은 정수 반올림 전 계산이며 실제 OpenCV의 정수 구현을 실행해 얻은 결과라고 쓰지 않는다. 이 회색 가중합을 곧바로 물리 조도나 선형 광량 측정값으로 해석하지도 않는다.
HSV는 hue/채도/명도 성분으로 색을 표현하는 방식이다. 일반 uint8 COLOR_BGR2HSV 경로의 H는 0~179, S/V는 0~255 범위다. 다른 dtype/변환 코드·소프트웨어는 범위가 다를 수 있다. H의 양 끝은 색상환에서 이어지므로 빨강처럼 경계를 가로지르는 색은 한 연속 범위만으로 잡기 어려울 수 있다. 저채도에서 hue만으로 안정된 색을 판단하거나 조명/노출이 달라도 같은 threshold가 통한다고 가정하지 않는다.
정수 산술과 수치 범위는 다르다
uint8은 0~255를 저장한다. dtype을 float로 바꾸는 것과 값을 255로 나누어 0~1로 정규화하는 것은 다른 연산이다. NumPy uint8 배열끼리 250+10을 계산하면 랩어라운드로 4가 될 수 있지만, OpenCV의 uint8 add는 포화시켜 255로 만든다. 넓은 정수형으로 바꿔 더하면 260을 보존한다. 공식 산술 비교처럼 같은 덧셈 기호라도 소비자/자료형 규칙을 확인한다.
차분에 음수가 필요할 때도 uint8 결과만 보면 부호를 잃을 수 있다. 먼저 적절한 signed/float 형식으로 변환하고 계산한 다음 표시용 범위 변환을 별도로 한다. 색/범위 오류는 필터나 모델을 더 크게 만들어 해결할 문제가 아니다. 원본 encoding과 dtype/범위, 변환 순서, 모델이 기대하는 채널/정규화를 기록한다.
필터는 어떤 정보를 희생하는가
필터는 이웃 픽셀을 결합해 새 값을 만든다. 평균은 작은 변동을 줄일 수 있지만 경계도 퍼뜨린다. 중앙값 median은 창 안 값을 정렬한 가운데 값을 쓰므로 고립된 극단값에 덜 끌릴 수 있다. 어느 쪽이 좋은지는 잡음과 과제에 달려 있다. 공식 필터 자료의 함수에는 kernel/출력 dtype/경계 처리 규약이 있으므로 결과를 비교할 때 함께 맞춘다.
종이 예제는 1차원 float 신호 x=[0,0,90,0,0], 창 크기 3, stride=1의 valid 모드를 쓴다. valid는 창이 완전히 들어가는 위치만 계산하여 경계 바깥 값을 보충하지 않는 규약이다. OpenCV의 기본 경계 처리와 동일하다고 주장하지 않는다. 창[0,0,90]/[0,90,0]/[90,0,0]의 평균은 모두 30, 중앙값은 모두 0이다. 90이 잡음이면 중앙값이 유용할 수 있지만 작은 밝은 표식이었다면 없애 버린 실패다.
변형 신호[0,0,90,90,90]은 경계가 있는 계단이다. 평균은[30,60,90]으로 경계를 퍼뜨리고 중앙값은[0,90,90]이다. 필터 크기를 키워 매끈해졌다는 이유만으로 물체 위치 정확도가 좋아졌다고 보지 않는다. 얇은 차선/작은 표식 보존과 잡음 제거를 함께 평가한다.
원래 신호의 인접 차분 d_i=x_(i+1)−x_i는[0,90,−90,0]이다. i는 왼쪽부터 0으로 세는 인덱스이며 d_i는 픽셀i와i+1 사이의 밝기 변화다. |d_i|≥50이면 두 위치가 후보가 되지만 이것이 물체 두 개라는 뜻은 아니다. 한 밝은 점의 양쪽 경계일 수 있다. 실제 2차원 Sobel/Canny와 같은 전체 알고리즘을 이 한 식으로 재현한 것도 아니다.
특징은 대응을 위한 단서다
edge는 한 방향의 밝기 변화, corner는 여러 방향으로 옮겼을 때 주변 밝기 패턴이 달라지는 지점으로 이해할 수 있다. 평평한 단색 영역은 위치를 조금 옮겨도 비슷하고 긴 직선 경계는 선 방향의 이동을 구분하기 어려울 수 있다. 공식 Harris 자료는 주변 변화로 corner/edge/평탄 영역을 구분하는 이유를 설명한다. 특징의 점수는 이 정의의 반응이지 물체 종류나 3차원 깊이의 측정값이 아니다.
descriptor는 특징점 주변의 모양/밝기 패턴을 비교 가능한 수치 벡터나 이진 부호 등으로 요약한 표현이다. descriptor 거리는 정한 비교 규칙의 비유사도이며, 작다고 같은 물리점이 확정되는 것은 아니다. 특징점/descriptor를 이용해 다른 영상과 대응할 수 있지만 반복 무늬에서는 서로 다른 점이 비슷할 수 있다. 조명 변화·흐림·가림·크기 변화로 특징이 사라지거나 잘못 짝지어질 수도 있다. 대응의 기하 일관성/시간 연속성·외부 보정이나 추가 센서로 확인해야 한다. corner가 많다는 사실만으로 추적 성공을 선언하지 않는다. 프레임 번호/시각·좌표/색 변환·필터와 특징 설정·거부된 대응도 기록한다. 기존 4.1 영상 좌표와 연결해 픽셀의 단서와 공간 기하를 나누어 본다.
선택 CPU 개념 확인
작성 시 cv2는 로컬에 없으므로 실제 OpenCV 함수를 실행하지 않았다. 아래 완전 코드는 Darwin/NumPy 2.4.4 CPU에서 실행한 개념 대체 경로다. valid 평균/중앙값/차분과 정수 범위만 확인하며 OpenCV의 경계/정수 반올림을 검증하지 않는다.
import numpy as np
x = np.array([0, 0, 90, 0, 0], dtype=np.float64)
print("mean", np.convolve(x, np.ones(3) / 3, mode="valid").tolist())
print("median", [float(np.median(x[i:i+3])) for i in range(3)])
print("diff", np.diff(x).tolist())
a = np.array([250], dtype=np.uint8)
b = np.array([10], dtype=np.uint8)
print("uint8", (a + b).tolist())
print("wide", (a.astype(np.int16) + b.astype(np.int16)).tolist())
mean [30.0, 30.0, 30.0]
median [0.0, 0.0, 0.0]
diff [0.0, 90.0, -90.0, 0.0]
uint8 [4]
wide [260]