[ML Research]파이썬과 OpenCV를 이용한 컴퓨터 비전 학습(2)

업데이트:


안녕하세요~ 여러분!
다들 반가워요. 🤗


오늘은 OpenCV를 이용한 컴퓨터 비전의 행렬로 작업하는 방법에 대해 살펴보려고 합니다! ✍


본 글은 ‘파이썬과 OpenCV를 이용한 컴퓨터 비전 학습’ 서적을 참고하여 작성하였으며,
여러 포스트로 나누어 소개드릴 예정입니다.


제가 보여드리는 코드는 본 예제에서 사용된 일부 코드를 인용한 것입니다. 자세한 내용이 궁금하신 분은 공식 Document 또는 서적을 읽어보시길 바랍니다.



두번째 포스트의 주제는 *행렬, 색상, 필터* 입니다.

픽셀 단위의 행렬을 사용하여 수행하는 작업과 행렬 전체에 적용할 수 있는 연산과 이미지 처리 절차체 대해 알아보고, 에 대해 배워보도록 하겠습니다.


자, 이제 출발합니다! 🚀



행렬, 색상, 필터 🎛


우선 행렬을 다루는 간단한 방법부터 살펴봅시다.


행렬 조작: 생성하기, 채우기, 요소 접근하기, ROI


OpenCV의 파이썬 인터페이스에서 행렬은 NumPy 배열로 나타냅니다. NumPy의 많은 함수를 사용하기 위해서 우선 모듈을 가져오면서 시작합니다.

행렬은 다음과 같이 2가지의 독립적인 특성을 갖습니다.


  • 행렬의 특성 = 형태 + 요소 타입


첫째, 형태는 행렬의 모든 차원을 의미을 의미하며 일반적으로 너비, 높이 채널 수의 3 가지 공간차원을 갖습니다.

둘째, 요소 타입은 요소의 값을 나타내기 위해 사용하는 데이터 타입을 의미합니다.
예를 들어 각 픽셀이 0-255 범위의 값을 저장할 때의 요소타입은 np.uint8입니다.

다음으로 사용되는 함수입니다.

np.full은 행렬 생성을 위해 사용되며, 초깃값 매개변수에 단일 숫자를 전달하면 모든 픽셀이 해당 숫자로 초기화되는 특징이 있습니다.

np.fill은 모든 픽셀에 매개변수로 전달되는 값을 동일하게 할당하며, 생성이 아닌 기존 행렬의 요소 값에 할당할 수 있는 특징이 있습니다.


예제에서 해당되는 코드는 다음과 같습니다.

image = np.full((480, 640, 3), 255, np.uint8)
image.fill(0)


다음은 데이터 타입과 스케일을 변환하는 방법입니다.


데이터 타입과 스케일 변환


행렬 요소의 데이터 타입을 unit8에서 float32로 변경하고 값이 일정 범위 내에서 포함되는 것을 고려하지 않고 산술 연산을 수행한 다음에 연산 결과를 다시 unit8로 변환하는 방법에 대해 알려드리겠습니다.


NumPy 배열의 astype 함수를 사용하여 행렬의 데이터 타입을 변환해볼텐데, 이는 행렬에 직접 대수 연산을 사용해 행렬 값에 스케일링을 적용할 수 있습니다.


예제에서 사용한 코드는 다음과 같습니다.

image = image.astype(np.float32) / 255
cv2.imshow('image', np.clip(image*2, 0, 1))
image = (image * 255).astype(np.uint8)


NumPy를 사용한 비이미지 데이터 영속성


이전에는 cv2.imwritecv2.imread 함수를 사용하여 이미지만을 저장하거나 불러왔지만, NumPy의 데이터 영속성으로 이미지뿐만 아니라 어떠한 형태나 데이터 타입의 행렬 또한 저장이 가능한 행렬의 저장 방법에 대해 알아보겠습니다.


NumPy의 savetxtloadtxt 함수를 사용하여 모든 행렬을 저장하고 불러오는 것이 가능합니다. 이 함수들은 텍스트 형식을 사용하기 때문에 텍스트 편집기로 파일의 내용을 확인할 수 있습니다.


예제에서 사용한 코드는 다음과 같습니다.


np.savetxt('matrix.csv', matrix)
matrix = np.loadtxt('matrix.csv').astype(np.float32)


이미지 채널 조작


다음은 행렬의 채널을 다루는 방법입니다.


행렬의 마지막 차원이 채널이기 때문에 해당 차원을 조작하는 것과 같습니다.
차원을 조작하여 채널 간의 맞교환을 위해서는 행렬에서 해당 슬라이스에 접근해야 합니다. 하지만 슬라이스는 원본 행렬의 사본이 아니라 동일한 데이터에 대한 다른 뷰일 뿐입니다.


즉, 일반적 데이터 타입에서처럼 임시 변수를 통해 맞교환을 수행하는 것이 불가능하기 때문에, 이를 수행하기 위해서는 복잡한 과정이 필요합니다. 그래서 원하는 차원에서 단일 인덱스 대신 필요한 모든 슬라이스에 대한 여러 인덱스를 열거함으로써 NumPy를 사용해 단일 슬라이스뿐만 아니라 데이터의 새로운 뷰로 다양한 슬라이스를 얻는 것입니다.
단일 인덱스를 사용하면 특정 채널에 접근할 수 있고, 해당 슬라이스에 대수 연산을 적용할 수 있습니다.


예제에서 사용한 코드는 다음과 같습니다.

image[:,:, [0,2]] = image[:,:, [2,0]]
cv2.imshow('blue_and_red_swapped', image)


이미지의 색 공간 변환


OpenCV의 풀 컬러 이미지는 RGB 색 공간으로 표현됩니다. 하지만 강도에 대한 별도의 채널을 갖는다면 다른 색으로 표현할 필요가 있습니다. 그래서 이러한 경우 이미지의 색 공간 변환이 필요하게 됩니다.


OpenCV의 cvtColor 함수를 사용하면 이미지의 색 공간을 변경할 수 있습니다.
이 함수는 입력이미지와 입력받는 색 공간과 출력하는 색 공간에 대한 인코딩 값을 매개변수로 받으며, 변환된 이미지를 반환합니다. 그리고 OpenCV에서는 200가지가 넘는 변환 유형을 지원합니다.


예제에서 사용된 코드는 다음과 같습니다.

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
print('Converted to grayscale')


감마 보정과 요소 단위의 수학


다음으로 감마보정은 픽셀 값의 분포를 왜곡하여 이미지 픽셀의 강도를 조정하는 비선형 연산입니다. 감마보정을 사용하면 이미지를 잘 보이게 밝기를 조정할 수 있습니다.
이 연산은 입력과 출력 사이의 지수 함수 관계로 표현하며, 입력과 출력 사이의 멱법칙 관계 $Vout = V^rin$ 으로 표현됩니다. 지수 계수가 1보다 큰 값은 이미지를 어둡게, 1보다 작은 값은 이미지를 밝게 만듭니다.


예제에서 사용된 코드는 다음과 같습니다.

gamma = 0.5
corrected_image = np.power(image, gamma)


평균/분산 이미지 정규화


어떠한 값을 픽셀 값의 통계적 적률로 설정해야 할 때, 값들의 평균값을 0으로 설정하고 분산을 1로 설정할 때 이 연산은 규정화되는데, 이는 컴퓨터 비전 알고리즘에서 특정 범위와 특정 통계와 관련된 값들을 처리하는데 도움이 됩니다.


행렬은 평균과 표준편차를 계산하는 함수가 제공되는 NumPy 배열 클래스로 표현합니다.
여기서 평균과 표준 편차를 동시에 계산하는 cv2.meanStdDev 함수를 사용하여 행렬을 정규화(제로 평균과 단위 분산 행렬을 얻는 것)하기 위해 mean을 호출해 얻은 결과로 평균을 빼고 이를 다시 표준 편차로 나누는 방식을 취합니다.


예제에서 사용된 코드는 다음과 같습니다.

image = cv2.imread(path).astype(np.float32) / 255
image -= image.mean()
image /= image.std()


이미지 히스토그램 계산


이미지 히스토그램은 값의 집합에 대한 분포 수준을 보여줍니다.

OpenCV에서는 히스토그램을 계산하는 cv.calcHist 함수가 제공되지만 간결하게 하기 위해 NumPy를 사용하기도 합니다. NumPy는 히스토그램을 계산하는 np.histogram 함수를 제공하고, 히스토그램을 그림으로 나타내기 위하여 matplotlib 모듈의 기능을 사용합니다.


예제에서 사용된 코드는 다음과 같습니다.

hist, bins = np.histogram(gery, 256, [0, 255])
plt.fill(hist)
plt.xlabel('pixel value')
plt.show()


이미지 히스토그램 평활화


이미지 히스토그램으로 강도의 분포를 반영하고, 히스토그램의 프로퍼티는 이미지 프로퍼티와 관련이 있습니다. 예를 들어 대비가 낮은 이미지는 대부분 픽셀 값의 번위가 좁기 때문에 특정 값으로 빈이 군집되는 히스토그램을 갖습니다. 이러한 이미지는 작고 세부적인 부분이 제대로 표현되지 않기 때문에 작업에 사용하기 어렵습니다. 이러한 문제를 해결하기 위해 히스토그램 평활화가 사용됩니다.


그래서 OpenCV를 활용하여 그레이스케일과 풀 컬러 이미지에서 히스토그램 평활화를 수행하는 방법에 대해 알아보겠습니다.

이번에는 예제에서 사용된 코드를 먼저 보겠습니다.


grey_eq = cv2.equalizeHist(grey)
hist, bins = np.histogram(grey_eq, 256, [0, 255])
plt.fill_between(range(256), hist, 0)
plt.xlabel('pixel value')
plt.show()
hsv[..., 2] = cv2.equalizeHist(hsv[..., 2])
color_eq = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
cv2.inshow('original color', color)

히스토그램의 평활화는 OpenCV의 equalizeHist 함수를 사용하여 대비를 개선할 이미지를 입력받습니다. 단일 채널 이미지만 입력받기 때문에 오직 그레이스케일 이미지에 직접 적용이 가능하며, 단일 채널의 평활화된 이미지를 반환합니다.

이 함수를 풀 컬러 이미지애 적용하기 위해서는 강도 정보를 갖는 단일 채널과 색상 정보를 갖는 다른 채널들을 갖게 변환해야 합니다. HSV 색 공간은 마지막 V 채널이 밝기를 인코딩하기 때문에 적용 요구 사항에 완벽하게 부합합니다. 먼저 입력 이미지를 HSV 색 공간으로 변환하고, V 채널에 equalizeHist를 적용하고 결과를 RGB로 다시 변환하는 것으로 풀컬러 이미지에 대한 히스토그램 평활화를 수행할 수 있습니다.


가우시안 필터, 중앙값 필터, 양방향 필터를 사용한 노이즈 제거


모든 실제 이미지에는 많은 노이즈가 포함되어 있습니다. 노이즈는 이미지의 형태를 망가뜨릴 뿐더러 알고리즘이 입력으로 처리하기에 좋지 않은 영향을 주기도 합니다.
그래서 노이즈를 제거하거나 극적으로 감소시키는 방법에 대해 알려드리겠습니다.


소개드릴 필터는 크게 3가지입니다.

먼저 가우시안 필터가 있습니다.
가우시안 필터를 적용하기 위해 cv2.GaussianBlur 함수를 사용합니다. 이 함수는 입력 이미지, 커널 크기, 너비와 높이에 대한 표준 편차를 매개변수로 받으며 매개변수 중 커널 크기는 홀수인 양수입니다.
높이에 대한 표준 편차가 지정되지 않았거나 0으로 설정된 경우 X 표준 편차의 값이 양방향에 사용됩니다. X 표준 편차를 0으로 변경하면 커널 크기로 표준 편차가 계산됩니다.

다음은 중앙값 필터입니다.
중앙값 블러링을 적용하기 위해 cv2.medianBlur 함수를 사용합니다. 이 함수는 첫 번째 인자로 입력이미지를, 두 번째 인자로 커널의 크기를 받습니다. 커널의 크기는 홀수인 양수입니다.

마지막으로 양방향 필터입니다.
양방향 블러링은 cv2.belateralFilter 함수를 적용합니다. 이 함수는 입력 이미지, 윈도우의 크기와 색상, 공간 시그마 값을 매개 변수로 받습니다. 윈도우 크기가 음수면 공간 시그마 값을 사용합니다.


예제에서 사용된 코드는 다음과 같습니다.

noised = (image + 0.2 * np.random.rand(*image.shape).astype(np.float32))
noised = noised.clip(0, 1)
plt.imshow(noised[:,:,[2,1,0]])
plt.show()

gauss_blur = cv2.GaussianBlur(noised, (7, 7), 0)
plt.show(gauss_blur[:,:, [2,1,0]])
plt.show()

median_blur = cv2.medianBlur((noised * 255).astype(np.uint8), 7)
plt.show(median_blur[:,:, [2,1,0]])
plt.show()

bilat = cv2.bilateralFilter(noised, -1, 0.3, 10)
plt.show(bilat[:,:, [2,1,0]])
plt.show()


자체 필터 생성과 적용


이번에는 선형 필터를 자체적으로 생성하고 이미지에 적용하는 방법에 대해 알아보겠습니다.

OpenCV의 cv2.filter2d 함수는 매개변수로 입력 이미지, 출력 결과 데이터 타입, OpenCV ID, 필터 커널을 받아 이미지를 선형적으로 필터링합니다.


예제에서 사용된 코드는 다음과 같습니다.

kernel = cv2.getGaussianKernal(KSIZE, 0)
kernel = -ALPHA * kernel @ kernel.T
kernel[KSIZE//2, KSIZE//2] += 1 + ALPHA

filtered = cv2.filter2D(image, -1, kernel)


실수 값으로 된 가버 필터로 이미지 처리


다음은 가버 필터 커널을 구성해 이미지에 적용하는 방법에 대해 알아보겠습니다.

가버 필터는 코사인파로 변조된 2D 가우시안 커널을 갖는 선형 필터입니다. 가버 필터 커널을 얻기 위한 cv2.getGaborKernel 함수는 매개변수로 커널 크기, 가우시안 표준 편차, 파형 방향, 파형 길이, 공간 비율, 위상을 받으며 이미지의 방향을 아는 경우 에지를 검출할 수 있는 유용한 필터입니다.


예제에서 사용된 코드는 다음과 같습니다.

kernel = cv2.getGaborKernel((21, 21), 5, 1, 10, 1, 0, cv2.CV_32F)
kernel /= math.sqrt((kernel * kernel).sum())

filtered = cv2.filter2D(image, -1, kernel)


다양한 이미지 임계 처리법


마지막으로 다양한 임계 처리법을 사용하여 그레이스케일 이미지를 이진 이미지로 변환하는 방법에 대해 알아보겠습니다.


OpenCV에는 다양한 유형의 임계값과 임계 처리법이 있습니다.


  • 모든 처리법 = 전역적 방식 + 적응형 방식


1) 전역적 방식 : 모든 픽셀에 동일한 임계값을 사용합니다.

cv2.threshold 함수를 사용하며 임계값 타입을 지정하는 매개변수를 받습니다.


2) 적응형 방식 : 픽셀에 따라 적용하는 임계값이 다릅니다.

cv2.adaptiveThreshold 함수를 사용하며 각 픽셀은 주변 픽셀 값에 따라 독자적인 임계값을 갖습니다.


예제에서 사용된 코드는 다음과 같습니다.

thr, mask = cv2.threshold(image, 200, 1, cv2.THRESH_BINARY)
print('Threshold used:', thr)

adapt_mask = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 11, 10)


🤝 마치며

이상 OpenCV에서 다루는 <행렬, 색상, 필터>에 대해 간략히 알아보았습니다. 다들 긴 글 읽어주셔서 감사하고 다음 포스트도 기대해주세요~ 🎉


글쓴이

DSC Yonsei 오세빈

E-mail: osb3372@yonsei.ac.kr

카테고리:

업데이트: