영상처리


영상처리

핀홀 카메라 모델

  • 영상 획득 과정은 매우 복잡
  • 핀홀 카메라 모델 : 물체에 반사된 빛은 카메라구멍을 통해 영상평면에 맺힘

디지털 변환

  • M x N영상으로 샘플링
  • L 단계로 양자화(quantization)
  • 엄밀히 말해 해상도는 물리적 단위 공간에서 식별 가능한 점의 개수를 뜻함
  • 여기선 화소의 개수를 해상도라 함

영상 좌표계(opencv)

  • 왼쪽 위 구석이 원점
  • y축은 원점에서 밑으로 뻗어나감. 주의할 필요 있음
  • (y, x) 표기
  • 함수에 따라 (x, y)표기 사용. 주의할 필요가 있다(ex - cv.line())
  • opencv는 numpy.ndarray로 영상 표현
  • 넘파이가 지원하는다양한 함수를 사용할 수 있음(min, max, argmin, argmax, mean…)

  • 흑백 : 2차원 행렬
  • 컬러 : 3차원 텐서
  • 영상 : 4차원 텐서
  • 점구름 영상 : 라이다로 획득(텐서로 읽어지긴 하지만 나타나는 정보가 다름)
  • RGB-D : 깊이 또는 레인지 추가. 빛 변화에 강건

RGB 컬러 모델

  • RGB 삼원색의 혼합(빛의 삼원색)
  • 0~1사이의 값으로 나타냄(1에 가까워 질수록 밝아짐)

HSV 컬러 모델

  • 빛의 밝기가 V요소에 집중
  • RGB보다 빛의 밝기에 강건

넘파이의 슬라이싱 기능을 이용하여 RGB 채널별로 디스플레이

이진화

\(b(i, j) = \begin{cases} 1,\;if\; f(j, i)\geq T \\ 0, \;if\; f(j, i) \lt T \end{cases}\)

  • f : 원래 명암 영상, b : 이진 영상
  • 가장 중요한 일 : 임계값 T 결정
    • 보통 히스토그램의 계곡 근처
    • 히스토그램 : 0, 1, …, L - 1의 명암 단계 각각에 대해 화소의 발생 빈도를 나타내는 1차원 배열

이진 영상

  • 화소가 0 또는 1인 영상
  • 1비트면 저장할 수 있는데, 편의상 1바이트 사용하는 경우 많음
  • 에지 검출 결과를 표시하거나 물체와 배경을 구분하여 표시하는 응용 등에 사용

실제 영상에서 히스토그램 구하기

import cv2 as cv
import matplotlib.pyplot as plt

img = cv.imread('img')
h = cv.calcHist([img], [2], None, [256], [0, 256]) #2번 채널인 R채널에서 히스토그램 구함
#None : 히스토그램을 구할 영역을 지정하는 마스크. None이면 전체 영상에서 히스토그램 구함
plt.plot(h, linewidth = 1)

알고리즘

  • 임계값 T보다 큰 화소는 1, 그렇지 않으면 0으로 바꿈. 임계값 결정이 중요
  • 히스토그램에서 계곡 부근으로 결정하는 방법
  • 실제 영상에서는 계곡이 아주 많이 나타나서 구현이 쉽지 않음

오츄 알고리즘

  • 이진화를 최적화 문제로 바라봄. 최적값을 임계값으로 이용
  • 목적 함수 J(t)는 임계값 t의 좋은 정도를 측정함 \(\hat{t} = \displaystyle argmin_{t \in \{0, 1, 2, ..., L-1\}}\, J(t)\)
    • t로 이진화했을 때 0이 되는 화소들의 분산과 1이 되는 화소들의 분산의 가중치 합을 J로 사용
    • 밑의 식은 J를 정의. 원리는 윗줄에서 설명한 내용. \(J(t) = n_0(t)v_0(t) + n_1(t)v_1(t)\)
import cv2 as cv
import sys

img = cv.imread('img.jpg')
t, bin_img = cv.threshold(img[:,:,2], 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
print('오츄 알고리즘 최적 임계값 : ', t)
cv.imshow('R channel', img[:,:,2]) #R 채널 영상
cv.imshow('R channel binarization', bin_img) #R채널이진화영상
cv.waitKey()
cv.destroyAllWindows()

최적화 문제를 푸는 알고리즘

  • 오츄는 최적화를 구현하는데 낱낱 탐색 알고리즘사용
    • 낱낱 탐색 알고리즘 : 모든 해를 다 검사하는 방법
    • 매개변수 t가 해공간을 구성하는데, 해공간이 작아 낱낱탐색 가능
    • L이 256이라면 해공간은 {0, 1, 2, …, 255}
  • 컴퓨터 비전은 문제를 최적화로 푸는 경우가 많음. 해공간이 커서 낱낱탐색 불가능 -> 부최적해를 찾는 효율적인 알고리즘 사용
    • 스네이크는 탐욕 알고리즘 사용
    • 신경망 학습 : 역전파 알고리즘 사용. 역전파 알고리즘은 미분 사용

4-연결성과 8-연결성

  • 4-연결성은 기준을 중심으로 네방향을 십자가 모양으로
  • 8-연결성은 4-연결성에 네방향 대각선도 포함

연결 요소

  • 이진 영상에서 1의 값을 가진 연결된 화소듸 집합
  • 연결 요소는 고유한 정수 번호로 구분
  • opencv에서 연결요소는 connectedComponents 함수로 찾을 수 있음

모폴로지는 구조 요소를 이용하여 영역의 모양을 조작

  • 영상을 변환하는 과정에서 하나의 물체가 여러 영역으로 분리되거나 다른 물체가 한 영역으로 붙는 경우를 방지하기 위해 사용
  • 구조 요소를 이용해 영역의 모양을 조작

팽창, 침식, 열림 닫힘

  • 모폴로지의 기본 연산 : 팽창, 침식
  • 팽창은 작은 홈을 메우거나 끊어진 영역을 연결하는 효과. 영역을 키움. 구조 요소의 중심을 1인 화소에 씌운 다음 구조 요소에 해당하는 모든 화소를 1로 바꿈
  • 침식은 경계에 솟은 돌출 부분을 깎는 효과. 영역을 작게 만듦. 구조 요소의 중심을 1인 화소 p에 씌운 다음 구조 요소에 해당하는 모든 화소가 1인 경우 p를 1로 유지, 그렇지 않으면 0으로 바꿈.
  • 열림은 침식한 결과에 팽창 적용. 원래 영역 크기 유지
  • 닫힘은 팽창한 결과에 침식을 적용. 원래 영역 크기 유지

모폴로지 연산 적용

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

img = cv.imread('sign.png', cv.IMREAD_UNCHANGED)
t, bin_img = cv.threshold(img[:,:,3], 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
plt.imshow(bin_img, cmap = 'gray'), plt.xticks([]), plt.yticks([])
plt.show()
b = bin_img[bin_img.shape[0]//2:bin_img.shape[0],0:bin_img.shape[0]//2+1]
plt.imshow(b, cmap = 'gray'), plt.xticks([]), plt.yticks([])
plt.show()
se = np.uint8([[0, 0, 1, 0, 0],    #구조 요소
               [0, 1, 1, 1, 0],
               [1, 1, 1, 1, 1],
               [0, 1, 1, 1, 0],
               [0, 0, 1, 0, 0]])
b_dilation = cv.dilate(b, se, iterations=1) #팽창
plt.imshow(b_dilation, cmap = 'gray'), plt.xticks([]), plt.yticks([])
plt.show()

b_erosion = cv.erode(b, se, iterations = 1) #침식
plt.imshow(b_erosion, cmap = 'gray'), plt.xticks([]), plt.yticks([])
plt.show()

b_closing = cv.erode(cv.dilate(b, se, iterstions=1), se, iterations = 1) #닫힘
plt.imshow(b_closing, cmap = 'gray'), plt.xticks([]), plt.yticks([])
plt.show()

세 종류의 영상 처리 연산

  • 영상 처리는 화소의 값을 바꾸는 작업 : 새로운 화소의 값이 어느 값에 따라 결정이 되는가에 따라 세 가지로 구분

  • 점 연산 : 자기 자신의 값으로부터 결정. ex) 오츄 이진화
  • 영역 연산 : 이웃 화소들까지 고려해서 결정. ex) 모폴로지
  • 기하 연산 : 기하학적 변환으로 결정

명암 조절

  • 점 연산을 활용
    • 선형 연산 : 상수를 곱해서 더하기만 하는 연산 \(f'(j, i) = \begin{cases} min(f(j, i) + a, L-1) \\ max(f(j, i)-a, 0) \\ (L-1)-f(j, i) \end{cases}\)
      • 원래 영상에 양수 a를 더해 밝게 만듬.
      • 화소가 가질 수 있는 최댓값 L-1을 넘지 않게 min을 취함
      • 가운데 식은 원래 영상에 양수 a를 빼서 어둡게 만듬
      • max를 취해 음수를 방지
      • 마지막 식은 L-1에서 원래 명암값을 빼서 반전시킴
    • 비선형 연산(ex 감마 연산) \(f'\ j, i = L-1 \times \dot{f} \; j, i \ ^{\gamma}\)
      • 비선형적인 시각 반응을 수학적으로 표현
      • \(\dot{f}\)는 [0, L-1] 범위의 화소의 값을 L-1으로 나누어 [0, 1] 범위로 정규화한 영상
      • \(\gamma\)는 사용자가 조정하는 값. 1이면 원래 영상 유지. 1보다 작으면 밝아짐. 1보다 크면 어두워짐
감마 보정 실험
import cv2 as cv
import numpy as np

img = cv.imread('img')
img = cv.resize(img, dsize(0, 0), fx = 0.25, fy = 0.25)

def gamma(f, gamma = 1.0):
  f1 = f/255.0
  return np.uint8(255*(f1**gamma))

gc = np.hstack((gamma(img, 0.5), gamma(img, 0.75), gamma(img, 1.0), gamma(img, 2.0), gamma(img, 3.0)))
cv.imshow('gamma', gc)

cv.waitKey()
cv.destroyAllWindows()

히스토그램 평활화

  • 점 연산을 활용
  • 히스토그램이 평평하게 되도록 영상을 조작해 영상의 명암 대비를 높이는 기법 \(l' = round(\ddot h(l) \times (L-1))\)
    • 모든 칸의 값을 더하면 1.0이 되는 정규화 히스토그램 \(\dot h\)와 i번 칸은 0~i번 칸을 더한 값을 가진 누적 정규화 히스토그램\(\ddot h\)을 가지고 식을 수행
    • l은 원래 명암값이고 l`는 평활화로 얻은 새로운 명암값
히스토그램 평활화
import cv2 as cv
import matplotlib.pyplot as plt

img = cv.imread('img')

gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
plt.imshow(gray, cmap = 'gray'), plt.xticks([]), plt.yticks([]), plt.show()

h = cv.calcHist([gray], [0], None, [256], [0, 256])
plt.plot(h, color = 'r', linewidth = 1), plt.show()

equal = cv.equalizeHist(gray)
plt.imshow(equal, cmap = 'gray'), plt.xticks([]), plt.yticks([]), plt.show()

h = cv.calcHist([equal], [0], None, [256], [0, 256])
plt.plot(h, color = 'r', linewidth = 1), plt.show()

영역 연산

  • 이웃 화소를 고려해서 새로운 값을 결정
  • 주로 합성곱 연산을 통해 이루어짐

컨볼루션

  • 각 화소에 필터 u를 적용해 곱의 합을 구하는 연산
  • 목적에 따라 다양한 필터를 사용할 수 있음(컨볼루션 자체는 특정한 목적이 없는 일반 연산)
    • 스무딩 필터, 가우시안 필터, 샤프닝 필터, 엠보싱 필터
컨볼루션 연산 식
  • 1차원 \(f'(x) = \displaystyle \sum_{i = -(w-1)/2}^{(w-1)/2}u(i)f(x+i)\)
  • 2차원 \(f'(y, x) = \displaystyle \sum_{j = -(h-1)/2}^{(h-1)/2}\displaystyle \sum_{i = -(w-1)/2}^{(w-1)/2}u(j, i)f(y + j, x + i)\)
가우시안 필터
  • 흔히 말하는 정규분포 형태의 필터(함수의 모양은 \(\sigma\)에 따름)
  • 가운데 있는 값이 평균, 최빈값. 주변으로 점점 더 작아지는 가중치를 줌
  • 1차원 가우시안 \(g(x) = \frac{1}{\sigma \sqrt{2\pi}}e^{-\frac{x^2}{2\sigma^2}}\)
  • 2차원 가우시안 \(g(y, x) = \frac{1}{\sigma^22\pi}e^{-\frac{y^2+x^2}{2\sigma^2}}\)

연산 결과를 저장하는 변수의 유효 값 범위

  • opencv는 주의를 기울여 작성되어 있음
  • 때로 프로그래머가 직접 신경써야 하는 경우가 있음(ex. filter2D)

데이터형

  • opencv는 영상 화소를 주로 numpy.uint8형으로 표현 [0, 255] 범위
  • [0, 255]범위를 벗어나는 경우 문제가 발생
  • 엠보싱의 경우 [-255, 255]
  • 절대값을 사용하면 [0, 255]의 값을 가짐:표현이 왜곡될 수 있음
    • -255가 255로 인식되어 가장 차이가 많이 나는 곳이 같은 값으로 인식
  • 어느 정도 정보의 손실을 감안. 밝은 값과 어두운 값의 차이를 최대화 하는 방향으로 변환
    • 128을 더함 : [-127, 383]의 범위
    • 0보다 작으면 0, 255보다 크면 255로 수치를 바꿈 -> 값의 범위가 [0, 255]로 제한됨
    • np.clip(a, p, q) -> a가 p보다 작으면 p, q보다 크면 q
컨볼루션 적용
import cv2 as cv
import numpy as np

img = cv.imread('img.jpg')
img = cv.resize(img, dsize(0, 0), fx = 0.4, fy = 0.4)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
cv.putText(gray, 'soccer', (10, 20), cv.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv.imshow('original', gray)

smooth = np.hstack((cv.GaussianBlur(gray, (5, 5), 0.0), cv.GaussianBlur(gray, (9, 9), 0.0), cv,GaussianBlur(gray, (15, 15), 0.0)))
cv.imshow('smooth', smooth)

femboss = np.array([[-1.0, 0.0, 0.0],
                    [0.0, 0.0, 0.0],
                    [0.0, 0.0, -1.0]])

gray16 = np.int16(gray)
emboss = np.uint8(np.clip(cv.filter2D(gray16, -1, femboss)+128, 0, 255))
emboss_bad = np.uint8(cv.filter2D(gray16, -1, femboss)+128)
emboss_worse = cv.filter2D(gray, -1, femboss)

cv.imshow('emboss', emboss)
cv.imshow('emboss_bad', emboss_bad)
cv.imshow('emboss_worse', emboss_worse)

cv.waitKey()
cv.destroyAllWindows()

기하 연산

  • 기하 연산으로 정해진 위치의 화소에서 값을 가져옴
    • 주로 물체의 이동, 크기, 회전에 따른 기하 변환

동차 좌표와 동차 행렬

  • 동차 좌표
    • 2차원 좌표에 1을 추가해 3차원 벡터로 표현
    • 3개 요소에 같은 값을 곱하면 같은 좌표((-2, 4, 1)과 (-4, 8, 2)는 (-2, 4)에 해당) \(\bar{p} = (x, y, 1)\)
  • 기하 연산을 동차 행렬로 표현
    • 이동 \(T(t_x, t_y) = \begin{pmatrix} 1 & 0 & t_x\\ 0 & 1 & t_y\\ 0 & 0 & 1 \end{pmatrix}\)

      • x방향으로 \(t_x\), y방향으로 \(t_y\)만큼 이동
    • 회전 \(R(\theta) = \begin{pmatrix} cos\theta & sin\theta & 0 \\ -sin\theta & cos\theta & 0 \\ 0 & 0 & 1 \end{pmatrix}\)

      • 원점을 중심으로 ㅇ반시계 방향으로 세타만큼 회전
    • 크기 \(S(s_x, s_y) = \begin{pmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{pmatrix}\)

      • x방향으로 \(s_x\), y방향으로 \(s_x\)만큼 크기 조정(1보다 크면 확대, 1보다 작으면 축소)
  • 어파인 변환 : 평행을 평행으로 유지

  • 동차 행렬을 이용하여 효율적인 계산이 가능
    • 복합 변환을 위한 행렬을 미리 곱해 놓으면, 모든 점에 대해 한번의 행렬 곱셈으로 기하 변환 가능

영상의 기하 변환

  • 화소 좌표에 동차 행렬을 적용하여 기하 변환
    • 값을 받지 못하는 화소가 생기는 에일리어싱 현상 가능성
      • 값을 받지 못한다는 것은 원재료의 어느 값을 쓸지 결정이 되지 않았음을 의미
    • 후방 변환을 통한 안티 에일리어싱을 활용
      • 변환 영상에서 각 위치에 해당하는 값을 원재료에 찾아서 매칭시키는 방식

영상 보간

  • 실수 좌표를 정수로 변환하는 방법
    • 최근접 이웃 방법 : 반올림 사용(계단현상 발생)
    • 양선형 보간법 : 걸치는 비율에 따라 선형 곱을 함으로써 안티 에일리어싱
      • 변환된 이미지의 픽셀이 값이 원자료에서 해당하는 위치가 실수로 표현되는 경우, 원자료에서 주변의 픽셀값의 가중합으로 해당 픽셀의 값 결정 \(f(j', i') = \alpha\beta f(j, i) + (1-\alpha)\beta f(j, i + 1)+\alpha (1-\beta)f(j+1, i) + (1-\alpha)(1-\beta)f(j+1, i+1)\)
보간을 이용해 영상의 기하 변환
import cv2 as cv
img = cv.imread('img.png')
patch = img[250:350, 170:270, :]

img = cv.rectangle(img, (170, 250), (270, 350), (255, 0, 0), 3)
patch1 = cv.resize(patch, dsize = (0, 0), fx = 5, fy = 5, interpolation = cv.INTER_NEAREST) #최근접이웃방법
patch2 = cv.resize(patch, dsize = (0, 0), fx = 5, fy = 5, interpolation = cv.INTER_LINEAR) #양선형 보간법
patch3 = cv.resize(patch, dsize = (0, 0), fx = 5, fy = 5, interpolation = cv.INTER_CUBIC) #양3차 보간법

cv.imshow('original', img)
cv.imshow('resize nearest', patch1)
cv.imshow('resize bilinear', patch2)
cv.imshow('resize bicubic', patch3)

cv.waitKey()
cv.destroyAllWindows()

opencv의 시간 효율

  • 컴퓨터 비전은 인식 정확도 뿐만 아니라 시간 효율도 중요함
    • 특히 실시간 처리가 요구되는 응용
  • opencv는 효율적으로 구성되었기 때문에 opencv함수를 사용하는 것이 유리
    • C/C++로 구현하고 인텔 마이크로프로세서에 최적화
  • 직접 구현하는 경우 파이썬의 배열 연산을 사용하는 것이 유리