4 분 소요

이번 포스팅은 저번 OpenCV 기본 연산의 연장선 입니다.
OpenCV의 수학, 통계 함수에 대한 것 입니다.

통계 연산을 하려면 기본 연산과 동일하게 여러 함수가 필요로 합니다.
예를 들면 정규화 함수, 최대최소, 난수, 선형대수, 정렬 등 통계 연산과 관련된 함수가 여럿 존재합니다.

import cv2
import numpy as np

src = cv2.imread('SamplePicture1.jpg', cv2.IMREAD_GRAYSCALE)

minVal1, maxVal1, minLoc1, maxLoc1 = cv2.minMaxLoc(src)
print('src : ', minVal1, maxVal1, minLoc1, maxLoc1)

dst = cv2.normalize(src, None, 100, 200, cv2.NORM_MINMAX)
minVal2, maxVal2, minLoc2, maxLoc2 = cv2.minMaxLoc(dst)
print('dst: ', minVal2, maxVal2, minLoc2, maxLoc2)
src :  0.0 200.0 (0, 0) (106, 128)
dst:  100.0 200.0 (0, 0) (106, 128)

minVal1, maxVal1, minLoc1, maxLoc1는 기존 영상의 최소값, 최댓값, 최소값 위치, 최대값 위치를 차례로 보여줍니다.
출력 결과 확인 시, 기존 영상의 최소 최댓값은 0, 최소값 위치는 (0,0), 최댓값 위치는 (106,128) 입니다.

cv2.normalize()함수를 사용하면, 정규화가 가능한데, cv2.NORM_MINMAX 함수로 100,200 구간으로 정규화를 했습니다.
정규화를 했기 때문에, 최소값 100, 최댓값 200이 나오는 것을 확인할 수 있습니다.

또한 난수를 설정할 수 있는데, 대표적인 함수로 cv2.randu( ), cv2.randn( )함수가 있습니다.
cv2.randu( )는 균등분포 난수, 즉 말 그대로 균등한 값의 범위를 뜻합니다. 매개변수 사이의 거리는 균등합니다.
cv2.randn( )은 정규분포 난수, 즉 평균 기준으로, 좌우대칭 모양을 이루는 분포입니다.
평균에 가까우면 가까울 수록 자주 나오고, 멀면 멀수록 값이 덜 나온다고 합니다.

import cv2
import numpy as np
import time

dst = np.full((512, 512, 3), (255, 255, 255), dtype = np.uint8)
nPoints = 100
pts = np.zeros((1, nPoints, 2), dtype = np.uint16)

cv2.setRNGSeed(int(time.time()))
cv2.randu(pts, (0, 0), (512, 512))

for k in range(nPoints):
    x, y = pts[0, k, :]
    cv2.circle(dst, (x,y), radius = 3, color = (255, 0, 0))
    
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

setRNGSeed(int(time.time()))함수는 난수 생성을 초기화 합니다. 초기화를 해주지 않으면, 실행 시 항상 같은 난수열을 생성하게 됩니다.

randu( )는 균등분포 난수를 의미하며, (0,0), (512,512) 범위로 주었습니다.

cv2.randn(pts, mean = (256,256), stddev = (75,75))

for i in range(nPoints):
    x, y = pts[0, i, :]
    cv2.circle(dst, (x,y), radius = 3, color = (0,0,255))
    
cv2.imshow('dst',dst)
cv2.waitKey()
cv2.destroyAllWindows()

randn()은 정규분포 난수, 편차비율(stddev)을 (50,50)로 주었습니다.
출력 결과를 비교해보면, 균등분포가 무엇인지, 정규분포가 무엇을 뜻하는지 시각적으로 바로 알 수 있습니다.

X = np.array([[0, 0, 0, 100, 100, 150, -100, -150],
              [0, 50, -50, 0, 30, 100, -20, -100]], dtype = np.float64)

X = X.transpose()

cov, mean = cv2.calcCovarMatrix(X, mean = None, flags = cv2.COVAR_NORMAL + cv2.COVAR_ROWS)
print('mean = ', mean)
print('cov = ', cov)

ret, icov = cv2.invert(cov)
print('cov = ', cov)

v1 = np.array([[0], [0]], dtype = np.float64)
v2 = np.array([[0], [50]], dtype = np.float64)

dist = cv2.Mahalanobis(v1, v2, icov)
print('dist = ', dist)

cv2.waitKey()
cv2.destroyAllWindows()
mean =  [[12.5   1.25]]
cov =  [[73750.  34875. ]
 [34875.  26287.5]]
cov =  [[73750.  34875. ]
 [34875.  26287.5]]
dist =  0.5051854992128457

transpose()를 사용하면, 각 행에 2차원 좌표를 위치할 수 있습니다.
통계 거리를 계산하기 위해서 사용하는 일종의 재료라고 볼 수 있습니다.
calcCovarMatrix()는 거리를 계산할 때 사용하는 함수 입니다.
cv2.COVAR_ROWS는 이름에서 유추 할 수 있듯이, 행에 데이터가 존재하기 때문에 이와 같은 함수를 사용합니다. 만약에 데이터가 열에 있으면, cv2.COVAR_COLS함수를 사용합니다.
또한 이는 공분산 행렬값을 계산하는데, 이는 어떤 변수와 다른 변수가 함께 증가 혹은 감소하게 되면 양수(positive), 이 반대면 음수(negative)를 반환합니다.

쉽게 말해 그냥 어떤 데이터의 통계 거리를 계산할 때 사용하는 함수로 cv2.calcCovarMatrix( )가있다 라고 이해하면 됩니다.

그리고 위의 cov로 지정해 주었던 통계거리를 cv2.invert()를 사용하여, 역행렬 값인 icov로 지정해주었습니다.

또한 cv2.Mahalanobis()함수가 나오는 것을 알 수 있습니다. 이는 ‘마하라노비스 거리’로 공분산 행렬(calcCovarMatrix)과 역행렬(invert)을 이용하여 계산합니다.

X = np.array([[0, 0, 0, 100, 100, 150, -100, -150],
              [0, 50, -50, 0, 30, 100, -20, -100]], dtype = np.float64)

X = X.transpose()

mean, eVects = cv2.PCACompute(X, mean = None)
print('mean = ', mean)
print('eVects = ', eVects)

Y = cv2.PCAProject(X, mean, eVects)
print('Y = ', Y)

X2 = cv2.PCABackProject(Y, mean, eVects)
print('X = ', X2)
print(np.allclose(X, X2))

cv2.waitKey()
cv2.destroyAllWindows()

mean =  [[12.5   1.25]]
eVects =  [[ 0.88390424  0.46766793]
 [-0.46766793  0.88390424]]
Y =  [[ -11.63338792    4.74096885]
 [  11.75000868   48.93618085]
 [ -35.01678451  -39.45424315]
 [  76.75703609  -42.02582434]
 [  90.78707404  -15.50869713]
 [ 167.71904127   22.98120308]
 [-109.37717055   33.82967723]
 [-190.9858171   -13.49926538]]
X =  [[ 1.77635684e-15  0.00000000e+00]
 [ 3.55271368e-15  5.00000000e+01]
 [ 0.00000000e+00 -5.00000000e+01]
 [ 1.00000000e+02 -7.10542736e-15]
 [ 1.00000000e+02  3.00000000e+01]
 [ 1.50000000e+02  1.00000000e+02]
 [-1.00000000e+02 -2.00000000e+01]
 [-1.50000000e+02 -1.00000000e+02]]
True

위의 코드는 투영, 역투영을 실시한 코드입니다.
cv2.PCACompute()는 평균벡터, 고유벡터 값을 각각 mean, eVects값으로 계산합니다.
cv2.PCAProject()는 고유벡터로, PCA투영을 할 때 사용합니다. 여기서 고유벡터는 서로 평행한 벡터인데, 크기만 바뀐 벡터를 뜻합니다.
cv2.PCABackProject()는 PCA역투영으로 위에서 PCA투영 한 값을, 다시 원본 X값을 복구합니다.


이제 영상을 가지고 PCA투영을 하겠습니다.

src = cv2.imread('SamplePicture1.jpg')
b, g, r = cv2.split(src)

cv2.imshow('b', b)
cv2.imshow('g', g)
cv2.imshow('r', r)

X = src.reshape(-1,3) # src 값 변환
print('X.shape = ', X.shape)

mean, eVects = cv2.PCACompute(X, mean = None)
print('mean = ', mean)
print('eVects = ', eVects)

Y = cv2.PCAProject(X, mean, eVects)
Y = Y.reshape(src.shape) # src 원래 값으로 변환
print('Y.shape = ', Y.shape)

eImage = list(cv2.split(Y))

for i in range(3):
    cv2.normalize(eImage[i], eImage[i], 0, 255, cv2.NORM_MINMAX)
    eImage[i] = eImage[i].astype(np.uint8)

cv2.imshow('eImage[0]', eImage[0])
cv2.imshow('eImage[1]', eImage[1])
cv2.imshow('eImage[2]', eImage[2])

cv2.waitKey()
cv2.destroyAllWindows()
X.shape =  (62500, 3)
mean =  [[56.84269  58.178463 65.06866 ]]
eVects =  [[ 0.56636935  0.55500287  0.6092599 ]
 [ 0.7007511   0.06480766 -0.7104561 ]
 [-0.43378985  0.8293202  -0.35221386]]
Y.shape =  (250, 250, 3)

먼저 split()함수를 써서, 채널을 분리합니다. 총 3개(b,g,r)채널로 분리 합니다.
그리고 3채널 값으로 재조정(reshape)하고, 위의 코드와 동일하게, 평균벡터, 고유벡터 값을 계산을 합니다.
그리고 PCA투영(PCAProject)을 한 후, 정규화(normalize)를 했습니다.
그렇게 for문을 돌린 후, 정규화한 값을 출력하게 됩니다.

무작위로 배열을 만들어 준 후, PCA투영, PCA역투영을 한 것과, 3채널 영상을 PCA투영, PCA역투영한 과정이 서로 동일합니다.
물론 영상 또한 특정 배열로 이루어져 있기 때문입니다.


후기: 이전 포스팅 보다는 수학적 내용도 많고, 생각할 것도 많았습니다. 그래서 그런지 이번 포스팅 내용은 굉장히 어렵게 다가왔습니다. 그래도 코드 하나하나 고민하는 과정을 반복하다보니 어느정도 감은 잡힌 듯 합니다.

업데이트: