OpenCV 프로그래밍 - Chapter 03 OpenCV 기본연산(1)

업데이트:

카테고리:

태그:

이 포스팅은 ‘python으로 배우는 OpenCV 프로그래밍’ 교재를 참고하여 작성하였습니다.

01 영상 속성과 화소 접근

OpenCV는 영상을 numpy.ndarray을 이용하여 표현한다.
영상의 중요 속성(shape, dtype)을 확인하고, numpy의 astype(), reshape로 속성을 변경하고, 영상 화소를 y(행), x(열) 순으로 인덱스를 지정 접근하여 밝기 또는 컬러 값을 접근한다.

구분 numpy 자료형 OpenCV 자료형, 1-채널
8비트 unsigned 정수 np.uint8 cv2.CV_8U(1)
8비트 signed 정수 np.int8 cv2.CV_8S(1)
16비트 unsigned 정수 np.uint16 cv2.CV_16U(1)
16비트 signed 정수 np.int16 cv2.CV_16S(1)
32비트 signed 정수 np.int32 cv2.CV_32S(1)
32비트 실수 np.float32 cv2.CV_32F(1)
64비트 실수 np.float64 cv2.CV_64F(1)

OpenCV 함수에서 결과 영상의 화소 자료형을 요구하는 경우는 화소비트, 자료형, 채널수를 명시한 OpenCV 자료형 상수를 사용한다.
1-채널을 자료형으로 할 경우 1은 생략가능하다.

영상 속성 1 : 모양, 자료형

import cv2
import numpy as np

img = cv2.imread('./data/lena.jpg') # cv2.IMREAD_COLOR
##img = cv2.imread('./data/lena.jpg', cv2.IMREAD_GRAYSCALE)

print('img.ndim=', img.ndim)
print('img.shape=', img.shape)
print('img.dtype=', img.dtype)

## np.bool, np.uint16, np.uint32, np.float32, np.float64, np.complex64
img=img.astype(np.int32)
print('img.dtype=',img.dtype)

img=np.uint8(img)
print('img.dtype=',img.dtype)

위 코드를 주석을 수정하여 두번에 걸쳐 실행시키면 다음과 같은 결과가 출력된다.

img.ndim= 3
img.shape= (512, 512, 3)
img.dtype= uint8
img.dtype= int32
img.dtype= uint8

첫번째 출력 결과는 cv2.IMREAD_COLOR메서드를 사용해서 이미지를 읽었으므로 3차원 배열이며 img.shape[0]은 영상의 세로 화소 크기, img.shape[1]은 영상의 가로 화소 크기, img.shape[2]는 영상의 채널 개수이다.
또한 img.astype()메서드를 사용해서 자료형을 변환할 수 있고, 그냥 변수자체로도 변환 가능하다.

img.ndim= 2
img.shape= (512, 512)
img.dtype= uint8
img.dtype= int32
img.dtype= uint8

두번째 출력 결과는 cv2.IMREAD_GRAYSCALE메서드를 사용해서 이미지를 읽었으므로 채널이 1차원이 되어서, 2차원 배열이며 영상의 세로, 가로 화소 크기를 의미한다.
영상처리 계산을 위해서 다양한 자료형으로 변경할 필요가 있는데, 이때 계산을 마친 후 윈도우에 영상을 표시하기 위한 cv2.imshow()메서드는 uint8 자료형의 영상만을 화면에 표시하므로 윈도우에 영상을 표시하려면 반드시 코드 마지막에 영상의 자료형을 uint8로 변환해줘야 한다.

영상 속성 2 : 모양 변경하기

import cv2
##import numpy as np

img = cv2.imread('./data/lena.jpg', cv2.IMREAD_GRAYSCALE)
print('img.shape=', img.shape)

#img = img.reshape(img.shape[0]*img.shape[1])
img = img.flatten()
print('img.shape=', img.shape)

img = img.reshape(-1, 512, 512)
print('img.shape=', img.shape)
print('img[0].shape=',img[0].shape)

cv2.imshow('img', img[0])
cv2.waitKey()
cv2.destroyAllWindows()

위 코드를 실행한 결과는 다음과 같다.

img.shape= (512, 512)
img.shape= (262144,)
img.shape= (1, 512, 512)
img[0].shape= (512, 512)

image 첫번째 출력문에서는 cv2.IMREAD_GRAYSCALE메서드를 사용한 이미지의 shape인 (512, 512)를 출력한다.
두번째 출력문에서는 img.flatten()메서드에 의해 2차원 배열에서 1차원 배열로 변환되어 512 * 512인 (262144, )를 출력한다.
세번째 출력문에서는 img.reshape(-1, 512, 512)에 의해 1차원 배열을 다시 3차원 배열로 확장한다. -1로 설정하면 채널의 개수를 자동으로 계산한다.
위 코드에서의 img는 화소 크기가 512*512이므로 (1, 512, 512)로 확장된다.(계산적으로 512 * 512 = 262144이므로 1이 될 수 밖에 없다) 따라서 네번째 출력문에서는 img[0].shape= (512, 512)임을 확인할 수 있고, cv2.imshow('img', img[0])메서드를 통해 그레이스케일 영상을 윈도우에 표시할 수 있다.
또한 윈도우에 이미지가 정상적으로 표시되므로, img.reshape()메서드는 실제 데이터(각 픽셀의 값)를 변경하지는 않고, 모양을 변경한다는 것을 알 수 있다.
위 코드를 컬러로 읽어와서 실행시켰을때는 윈도우에 이미지가 정상적으로 출력되지 않는데 추후에 이를 디버깅 해봐야겠다…!

영상속성 2 : 모양 변경하기(custom)

import cv2
##import numpy as np

img = cv2.imread('./data/lena.jpg')
print('img.shape=', img.shape)

#img = img.reshape(img.shape[0]*img.shape[1])
img = img.flatten()
print('img.shape=', img.shape)

img = img.reshape(-1, 512, 512)
print('img.shape=', img.shape)
print('img[0].shape=',img[0].shape)

cv2.imshow('img', img[0])
cv2.waitKey()
cv2.destroyAllWindows()

실행 결과는 다음과 같다.

img.shape= (512, 512, 3)
img.shape= (786432,)
img.shape= (3, 512, 512)
img[0].shape= (512, 512)

image
윈도우에 다음과 같이 표시되는 이유가 뭘까….??? 에 대한 해답을 알아냈다(2022-02-08)

import cv2
##import numpy as np

img = cv2.imread('./data/lena.jpg')
print('img.shape=', img.shape)
print('before flatten :\n',img[100:105, 100:105, 0:3])


# img = img.reshape(img.shape[0]*img.shape[1])
img = img.flatten()
# print('img.shape=', img.shape)

img = img.reshape(-1, 512, 512)
# print('img.shape=', img.shape)
print('after flatten :\n',img[0:3, 100:105, 100:105])

cv2.imshow('img', img[0])
cv2.waitKey()
cv2.destroyAllWindows()

우선적으로 img를 컬러로 읽어왔을때는 img는 3차원 배열을 담는 변수라는 것을 필수적으로 인지해야한다.(기초 중에 기초)
위 코드는 flatten후 다시 reshape하는 코드인데 책에서 나온 reshape형식을 따라서 해봤을 때 출력 결과는 다음과 같았다.

img.shape= (512, 512, 3)
before flatten :
 [[[ 87  74 182]
  [ 80  67 175]
  [ 81  71 177]
  [ 77  67 173]
  [ 83  73 179]]

 [[ 84  74 180]
  [ 79  69 175]
  [ 81  71 177]
  [ 77  67 173]
  [ 76  68 175]]

 [[ 76  66 172]
  [ 78  68 174]
  [ 80  73 178]
  [ 77  70 175]
  [ 72  66 173]]

 [[ 71  61 167]
  [ 77  67 173]
  [ 79  72 177]
  [ 74  67 172]
  [ 79  73 180]]

 [[ 82  71 175]
  [ 77  65 171]
  [ 82  72 178]
  [ 79  69 176]
  [ 76  67 177]]]
img.shape= (3, 512, 512)
after flatten :
 [[[ 90  93 208  87  90]
  [220 104 135 226 103]
  [144 229 112 145 231]
  [ 89  91 209  87  89]
  [220 109 134 226 108]]

 [[125 230 106 132 232]
  [ 89  38 106 105  54]
  [250 139 177 231 108]
  [125 230 105 131 231]
  [115  62 135 112  60]]

 [[103  86  54 131 129]
  [116 217 107 123 220]
  [ 62  23  98  60  24]
  [105  82  50 127 128]
  [111 216 107 117 218]]]

출력된 결과를 보면 출력이 안되는것이 당연하다.(3차원 배열의 순서가 중요함을 나타내는 대목…!!, 위 출력 결과의 배열 형태의 차이를 비교 분석해보는 것을 추천)
그레이스케일의 경우 채널이 1개이므로(열이 1개밖에 존재하지 않음) img[0]의 shpae는 당연하게도 512 * 512 이므로 cv2.imshow('img', img[0])를 통한 윈도우 표시가 가능하지만, 위 코드에 따르면 애초에 컬러값의 인덱스(3)를 이미지 3차원 배열에서 열로 인식해서 BGR채널에 알맞은 값이 입력되지 않은 상황이다.
즉, img.shape= (3, 512, 512) 이 문장의 파라미터는 순서대로 열, 행, 또 다른 3차원 축을 의미하게 되고 원래의 이미지배열이라면 또 다른 3차원 축이 컬러값의 인덱스로 들어가야하지만, 책에서 나타낸 형태로는 그렇지 못해서 출력값이 옳지 않았던 것이다…!!!!

import cv2
##import numpy as np

img = cv2.imread('./data/lena.jpg')
print('img.shape=', img.shape)
print('before flatten :\n',img[100:105, 100:105, 0:3])


# img = img.reshape(img.shape[0]*img.shape[1])
img = img.flatten()
# print('img.shape=', img.shape)

img = img.reshape(512, 512, -1)
print('img.shape=', img.shape)
print('after flatten :\n',img[100:105, 100:105, 0:3])

cv2.imshow('img', img)
cv2.waitKey()
cv2.destroyAllWindows()

코드 결과는 따로 첨부하지 않겠다.
직접 실행시켜보고 완벽히 숙지하도록 해야한다.
이미지 배열(3차원)의 형태를 익히기에 아주 좋은 예제가 될 것이라고 자부하며 이를 숙지하는 것이 앞으로 OpenCV학습에 매우 큰 도움이 될 것이다.

화소 접근 1 : 그레이스케일 영상

import cv2
##import numpy as np

img = cv2.imread('./data/lena.jpg', cv2.IMREAD_GRAYSCALE)
img[100, 200] = 0  # 화소값(밝기,그레이스케일) 변경
print(img[100:110, 200:210]) # ROI 접근

##for y in range(100, 400):
##    for x in range(200, 300):
##        img[y, x] = 0

img[100:400, 200:300] = 0    # ROI 접근

cv2.imshow('img', img)
cv2.waitKey()
cv2.destroyAllWindows()

위 코드의 실행 결과는 다음과 같다.

befor ROI:
 [[146 143 145 132 147 144 142 139 132 138]
 [138 138 143 151 137 144 139 139 138 138]
 [132 139 153 140 133 136 143 138 137 128]
 [137 146 138 125 132 145 139 142 130 128]
 [149 139 130 137 140 145 136 133 132 141]
 [141 139 134 149 149 137 132 127 140 140]
 [142 148 139 142 144 138 146 135 131 130]
 [151 146 136 131 142 144 149 135 126 132]
 [147 131 135 138 147 139 128 125 134 138]
 [135 132 149 142 134 128 122 135 138 129]]
after ROI:
 [[0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]]

img[100:110, 200:210] = 0 문장을 실행하기 전에는 각 픽셀마다 그레이스케일의 밝기값을 갖고 있었지만, 위 문장이 실행된 후에는 각 픽셀의 값이 0으로 변환된다.
위 문법은 슬라이싱 기본 문법이다.
주석처리된 for문도 img[100:110, 200:210] = 0와 같은 결과를 나타낼 것이다.
또한 img[y ,x]를 나타내므로 윈도우에 표시되는 결과는 다음과 같다.
image
자세히 보면 레나의 모자쪽에 밝기가 0(검정색)인 (10 * 10)크기의 여백이 생겼음을 알 수 있다.

화소 접근 2 : 컬러영상(BGR)

import cv2
##import numpy as np

img = cv2.imread('./data/lena.jpg') # cv2.IMREAD_COLOR

##for y in range(100, 400):
##    for x in range(200, 300):
##        img[y, x] = [255, 0, 0]    # 파랑색(blue)으로 변경

img[100:400, 200:300] = [255, 0, 0]  # ROI 접근
    
cv2.imshow('img', img)
cv2.waitKey()
cv2.destroyAllWindows()

BGR컬러 영상도 어려울 것 없다.
이미지를 디폴트로 BGR컬러로 읽어온 후 마찬가지 방법으로 일정 범위의 색을 지정할 수 있는데, 그레이스케일과 달리 채널이 3개임을 유의해야 한다.
윈도우에 표시되는 결과는 다음과 같다.
image

화소 접근 3 : 컬러영상(채널 접근)

import cv2
##import numpy as np

img = cv2.imread('./data/lena.jpg') # cv2.IMREAD_COLOR

##for y in range(100, 400):
##    for x in range(200, 300):
##        img[y, x, 0] = 255      # B-채널을 255로 변경
        
img[100:400, 200:300, 0] = 255  # B-채널을 255로 변경
img[100:400, 300:400, 1] = 255  # G-채널을 255로 변경
img[100:400, 400:500, 2] = 255  # R-채널을 255로 변경

cv2.imshow('img', img)
cv2.waitKey()
cv2.destroyAllWindows()

앞선 코드에선 일정 영역의 픽셀을 3채널의 값을 모두 입력해주어서 완전한 파란색 영역을 만들었지만. 위 코드는 각 영역의 B,G,R값을 각각 조절하여 본래 픽셀의 다른 채널 값을 유지하며 B,G,R값만 최대치로 조절한다.
윈도우에 표시되는 결과는 다음과 같다.
image
즉, 0 - Blue, 1 - Green, 2- Red를 의미하며 기초 중에 기초 내용이므로 반드시 숙지하자.

02 관심 영역과 ROI

ROI(Region Of Interest)는 영상의 사각 관심 영역을 의미하며 numpy의 슬라이싱으로 지정하여 접근한다.
위에서 일정영역을 지정한 색으로 조절했을 때 지정한 일정영역이 우리에게는 일시적으로 ROI가 되었던 것이다.
이 장에서는 ROI를 사용하여 블록 평균 영상을 생성하고, 마우스를 이용한 ROI 영역지정 함수 selectROI()selectROIs()를 설명한다.
selectROI(windowName, img[,showCrosshair[,fromCenter]]) -> retval
windowName 윈도우에 img 영상을 표시하고, 사용자가 마우스 클릭과 드래그로 ROI를 선택할 수 있게 한다.
showCrosshair = True이면 선택 영역에 격자가 표시되고, fromCenter = True이면 마우스 클릭 위치 중심을 기준으로 박스가 선택된다.
space키 또는 enter키로 선택을 종료하면 (x, y, width, height)로 이루어진 선택영역의 튜플을 반환한다.
(x, y)는 영역박스의 왼쪽 상단 좌표이고, (width, height)는 영역박스의 가로, 세로 크기이다.
selectROIs(windowName, img[,showCrosshair[,fromCenter]]) -> boundingBoxes
위 메서드로는 여러 개의 ROI를 생성할 수 있다.

ROI에 의한 블록 평균 영상

import cv2
import numpy as np

src = cv2.imread('./data/lena.jpg', cv2.IMREAD_GRAYSCALE)
dst = np.zeros(src.shape, dtype=src.dtype)

N = 4 #8 32 64
height, width = src.shape    # 그레이스케일 영상
##height, width,_ = src.shape # 컬러영상

h = height // N
w = width  // N
for i in range(N):
    for j in range(N):
        y = i*h
        x = j*w       
        roi = src[y:y+h, x:x+w]
        dst[y:y+h, x:x+w] = cv2.mean(roi)[0]   # 그레이스케일 영상
##        dst[y:y+h, x:x+w] = cv2.mean(roi)[0:3] # 컬러영상
        
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

원본 영상 src와 같은 자료형과 같은 크기이며 각 픽셀에 0이 담겨있는 dst를 생성하고, for 문에서 roi를 이용하여, 하나의 블록 크기가 w * h인 N * N블록 평균 영상을 dst에 담는다.
이중 반복문은 위에서 지정한 N에 의해서 이미지를 영역별로 나눠서 각 영역별 밝기 평균을 계산한다.
cv2.mean()은 4 채널 값을 반환하므로 그레이스케일은 cv2.mean(roi)[0], 컬러는 cv2.mean(roi)[0:3]로 계산해야한다.
N이 커질수록 원본 이미지와 유사한 이미지가 윈도우에 표시될 것이다.
cv2.mean(roi)의 반환값은 다음과 같다.

(123.74676513671875, 0.0, 0.0, 0.0)

N = 4로 했을때 윈도우에 표시되는 결과는 다음과 같다.
image

마우스로 ROI 영역 지정 : selectROI()

import cv2
 
src = cv2.imread('./data/lena.jpg', cv2.IMREAD_GRAYSCALE)
roi = cv2.selectROI(src)
print('roi =', roi)

img = src[roi[1]:roi[1]+roi[3],
               roi[0]:roi[0]+roi[2]]

cv2.imshow('Img', img)
cv2.waitKey()
cv2.destroyAllWindows()

cv2.selectROI()의 반환값은 (x, y, width, height)로 이루어진 선택영역의 튜플임을 앞서 설명했다.
따라서 위 코드를 이해하는데에 어려움이 없을 것이다.
위 코드를 실행하면 다음과 같은 결과가 윈도우에 출력된다.
image
마우스로 영역을 선택할 수 있고, 앞서 설명한 반환값에 의해

roi = (219, 211, 153, 92)

를 결과로 출력하게 된다.
또한 cv2.selectROIs()를 사용하면 여러개의 영역을 출력할 수 있다.

03 영상 복사

원본 영상의 데이터를 그대로 유지하고, 원본 영상의 복사본에 라인, 사각형, 원 등을 표시하는 경우가 많다.
필자도 영상처리 프로젝트로 라인 트레이싱을 계획하고 있는데, 영상의 복사본에 라인을 표시하게 될 것이다.
원본 영상의 복사는 numpy.copy()로 복사하거나, np.zeros()같은 함수로 영상을 생성한 후에 복사할 수 있다.
복사와 달리 dst = src와 같은 지정문은 복사가 아니라 참조(reference)를 생성하기 때문에 한 영상을 변경하면, 다른 영상도 변경된다.

영상 복사 1

import cv2
src = cv2.imread('./data/lena.jpg', cv2.IMREAD_GRAYSCALE)

dst = src.copy()     #복사 
##dst = src          #참조

dst[100:400, 200:300] = 0

cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()    
cv2.destroyAllWindows()

주석을 수정해가며 코드를 실행해보았다.
우선 복사를 하게 될 경우 원본은 유지되고 복사본의 일정영역만 변경된다.
image
복사가 아니라 참조를 하게 될 경우 복사본이 변경되면 원본도 같이 변경된다.
image

영상 복사 2

import cv2
import numpy as np
 
src = cv2.imread('./data/lena.jpg')
shape = src.shape
dst = np.zeros(shape, dtype=np.uint8)
dst [:,:,0] = src [:,:,0]  # 이 문장 자체가 참조가 아닌 복사와 같은 역할을 하는 것으로 추정됨
# dst [:,:,1] = src [:,:,1] 
# dst [:,:,2] = src [:,:,2] 
dst[100:400, 200:300, :] = [255, 255, 255]

cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()    
cv2.destroyAllWindows()

위 코드는 dst [:,:,0] = src [:,:,0]가 참조 형식의 문장이지만 참조처럼 dst를 변경하면 src도 변경되진 않는 예시를 보여주는 것 같다.
즉, dst [:,:,0] = src [:,:,0]는 참조의 형식이지만 참조가 아니라 복사처럼 작용한다.
윈도우에 표시되는 결과는 다음과 같다.
image

04 영상 채널 분리와 합성

영상 채널을 분리할때는 cv2.split()메서드를 사용해서 다중 채널 영상을 튜플에 단일 채널 영상으로 분리하고, 영상 채널을 병합 할 때는 cv2.split()매서드를 사용해서 단일 채널 영상을 병합하여 다중 채널 영상을 생성한다.
사용 문법은 다음과 같다.
cv2.split(m[, mv]) -> mv
다중 채널 배열(영상)m을 단일 채널의 배열로 분리하여 튜플 mv에 출력한다.
cv2.merge(mv[, dst]) -> dst
단일 채널 배열(영상)의 튜플 mv를 하나의 다중 채널 배열(영상) dst로 생선한다.

채널 분리

import cv2
src = cv2.imread('./data/lena.jpg')

dst = cv2.split(src) 
print(type(dst)) # dst는 분리된 채널값을 담고있는 튜플
print(type(dst[0])) # type(dst[1]), type(dst[2]), 모두 다차원 배열이므로 윈도우에 표시 가능
print(type(src)) # src는 이미지의 정보를 담고있는 다차원 배열
print(type(src[:,:,0]))

#cv2.imshow('src',src[:,:,0]) 이 문장들과 바로 아래 문장들은 같은 출력을 한다.
#cv2.imshow('src',src[:,:,1]) 이 문장들과 바로 아래 문장들은 같은 출력을 한다.
#cv2.imshow('src',src[:,:,2]) 이 문장들과 바로 아래 문장들은 같은 출력을 한다. -> 다차원 배열의 원리를 이해!
cv2.imshow('blue',  dst[0]) 
cv2.imshow('green', dst[1])
cv2.imshow('red',   dst[2])
cv2.waitKey()    
cv2.destroyAllWindows()

dst = cv2.split(src)는 3채널 BGR 컬러영상 src를 채널 분리하여 튜플 dst에 저장한다. dst[0], dst[1], dst[2]는 모두 numpy.ndarray이며 순서대로 B, G, R값을 단일채널로 담고있다. 즉, 기존에 3개의 채널로 표시되었던 이미지를 B, G, R 각각 분리해서 표시할 수 있게된다.
실행 결과는 다음과 같다.

<class 'tuple'>
<class 'numpy.ndarray'>
<class 'numpy.ndarray'>
<class 'numpy.ndarray'>

image

채널 병합

import cv2
src = cv2.imread('./data/lena.jpg')

b, g, r = cv2.split(src)
dst = cv2.merge([b, g, r]) # cv2.merge([r, g, b])

print(type(dst))
print(dst.shape)
cv2.imshow('dst',  dst)
cv2.waitKey()    
cv2.destroyAllWindows()

원본 영상 src에서 b, g, r 을 분리한 후 다시 병합하면 다음과 같은 결과가 출력된다.

<class 'numpy.ndarray'>
(512, 512, 3)

image

이 장에선 다중 채널 영상에서 단일 채널 영상으로 각각 분리하고, 이를 다시 병합하는 방법에 대해 알아보았다.
다음 포스팅에 이어서 OpenCV 기본연산에 대해 알아보도록 하자.

댓글남기기