printf("ho_tari\n");

ep.37 OpenCV3 본문

두산 로보틱스 부트캠프 ROKEY/Computer Vision 교육

ep.37 OpenCV3

호타리 2024. 8. 28. 16:15

2024.8.28

 

5장 기하학적 변환

  • 2 x 3 행렬 : Affine Transformation
  • 3 x 3 행렬 : Perspective Transformation

5.1 이동, 확대/축소, 회전

5.1.1 이동

 

이미지를 이동하는 방법은 간단합니다. 원래 있던 좌표에 이동시키려는 거리만큼 더하면 됩니다.

  • x_new = x_old + d₁
  • y_new = y_old + d₂

위 방정식을 행렬식으로 표현하면 아래와 같습니다.

 


행렬식을 다시 풀어서 써보겠습니다.

  • x_new = x_old + d₁ = 1x_old + 0y_old + d₁
  • y_new = y_old + d₂ = 0x_old + 1y_old + d₂

보시는 바와 같이 이미지의 좌표를 이동하는 변환 행렬은 2 x 3 행렬입니다. 변환 행렬이란 어떤 좌표를 선형 변환(linear tranformations) 해주는 행렬을 뜻합니다. 쉽게 말해서 어떤 좌표를 다른 좌표로 이동시켜주는 행렬이라는 뜻입니다. 즉, 어떤 좌표에 변환 행렬을 곱해주면 다른 좌표가 구해지는 행렬입니다. 아래는 이미지 이동에 대한 변환 행렬입니다.

 


 

OpenCV에서는 2 x 3 행렬을 이용해서 이미지를 변환하는 함수 제공합니다.

dst = cv2.warpAffine(src, matrix, dsize, dst, flags, borderMode, borderValue)

  • src: 원본 이미지, numpy 배열
  • matrix: 2 x 3 변환행렬, dtype=float32
  • dsize: 결과 이미지의 크기, (width, height)
  • flags(optional): 보간법 알고리즘 플래그
    • cv2.INTER_LINEAR : default 값, 인접한 4개 픽셀 값에 거리 가중치 사용
    • cv2.INTER_NEAREST : 가장 가까운 픽셀 값 사용
    • cv2.INTER_AREA : 픽셀 영역 관계를 이용한 재샘플링
    • cv2.INTER_CUBIC : 인정합 16개 픽셀 값에 거리 가중치 사용
  • borderMode(optional): 외곽 영역 보정 플래그
    • cv2.BORDER_CONSTANT : 고정 색상 값
    • cv2.BORDER_REPLICATE : 가장자리 복제
    • cv2.BORDER_WRAP : 반복
    • cv2.BORDER_REFLECT : 반사
  • borderValue(optional): cv2.BORDER_CONSTANT 외곽 영역 보정 플래그일 경우 사용할 색상 값 (default=0)
  • dst: 결과 이미지

파라미터가 많아서 헷갈리실 수도 있겠습니다. 원본 이미지인 src를 변환 행렬 matrix에 따라 변환하는 함수라고 보시면 됩니다. 이때 결과 이미지의 크기를 나타내는 파라미터는 dsize입니다.

 

5.1.2 확대/축소

 

이미지를 일정 비율로 확대 및 축소하는 방법은 아래와 같습니다. 기존의 좌표에 특정한 값을 곱하면 됩니다.

  • x_new = a₁ * x_old
  • y_new = a₂ * y_old

이를 다시 풀어쓰면 아래와 같습니다.

  • x_new = a₁ * x_old = a₁ * x_old + 0 * y_old + 0 * 1
  • y_new = a₂ * y_old = 0 * x_old + a₂ * y_old + 0 * 1
 

 

 

변환 행렬은 평행 이동할 때와 마찬가지로 2 x 3 행렬입니다. 2 x 2 행렬로 나타낼 수 있는데 굳이 2 x 3 행렬로 표현한 이유는 cv2.warpAffine() 함수는 변환 행렬이 2 x 3 행렬이 아니면 오류를 내기 때문입니다. 기하학적 변환은 이미지 확대 및 축소뿐만 아니라 평행 이동도 있습니다. 두 변환을 같이 하기 위해 2 x 3 행렬로 맞춘 것입니다.

아래는 변환 행렬을 이용해서 이미지를 확대하고 축소하는 예제 코드입니다. 이미지 평행 이동과 마찬가지로 cv2.warpAffine() 함수를 사용했습니다.

 

실행을 하면 원본 이미지가 2배 확대된 이미지와 1/2로 축소된 이미지를 보여줍니다. 일반적으로 보간법 파라미터로는 축소에는 cv2.INTER_AREA를 쓰고, 확대에는 cv2.INTER_CUBIC, cv2.INTER_LINEAR를 씁니다.

add코드add텍스트

변환 행렬을 쓰지 않고도 확대 및 축소를 할 수 있습니다. cv2.resize() 함수를 사용하면 됩니다.

cv2.resize(src, dsize, dst, fx, fy, interpolation)

  • src: 입력 원본 이미지
  • dsize: 출력 영상 크기(확대/축소 목표 크기, (width, height)형식), 생략하면 fx, fy 배율을 적용
  • fx, fy: 크기 배율, dsize가 주어지면 dsize를 적용함
  • interpolation: 보간법 알고리즘 선택 플래그 (cv2.warpAffine()과 동일)
  • dst: 결과 이미지
 

 

cv2.resize() 함수를 사용하면 확대/축소를 몇 픽셀로 할지 혹은 어떤 배율로 할지 선택할 수 있습니다.

dsize는 확대/축소를 원하는 목표 이미지의 크기이며, fx, fy는 변경할 배율입니다.

예를 들어 fx = 2, fy = 0.5이면 x축으로 2배, y축으로 0.5배로 스케일링한다는 뜻입니다.

아래는 cv2.resize()를 적용하여 이미지를 확대 및 축소하는 코드입니다.


5.1.3 회전

 
 

이미지 회전을 위한 변환 행렬식은 아래와 같습니다.

변환 행렬에 사용할 회전 각은 60진법에서 라디안(radian)으로 변경해야 합니다. 1 라디안(radian) = (180/π)도 입니다.
따라서 1도 = (π/180) 라디안입니다. 따라서 45도 = (45 * π)/180라는 것을 알 수 있습니다. 또한, 변환 행렬의 마지막에 0이 아닌 rows//2, -1cols//4, rows를 사용했습니다. 영상의 회전 기준 축이 좌측 상단이므로 회전한 영상은 프레임 바깥으로 벗어나게 됩니다. 프레임 바깥으로 벗어난 이미지를 프레임 안쪽으로 이동시키기 위해 rows//2, -1cols//4, rows를 사용한 것입니다. 회전을 한 뒤 평행 이동을 한 것인데, 이를 하나의 변환 행렬로 처리한 것입니다.

하지만 이렇게 변환 행렬을 직접 구하는 것은 복잡한 작업입니다. 이동시킬 좌표의 크기까지 고려해야 하기 때문입니다. OpenCV는 간단하게 변환행렬을 생성할 수 있게 아래와 같은 함수를 제공합니다.

mtrx = cv2.getRotationMatrix2D(center, angle, scale)

  • center: 회전축 중심 좌표 (x, y)
  • angle: 회전할 각도, 60진법
  • scale: 확대 및 축소비율

 

5.2 뒤틀기

 

5.2.1 어핀 변환

 

 

어핀 변환은 뒤틀기 방법 중 하나입니다. 말이 어려워 보이지만 아래 예제를 보면 어떤 변환인지 쉽게 이해가 될 겁니다.

martix = cv2.getAffineTransform(pts1, pts2)

  • pts1: 변환 전 영상의 좌표 3개, 3 x 2 배열
  • pts2: 변환 후 영상의 좌표 3개, 3 x 2 배열
  • matrix: 변환 행렬 반환, 2 x 3 행렬

5.2.2 원근 변환

 
 

어핀 변환은 이미지를 2차원으로 뒤트는 변환이었습니다. 반면 원근 변환은 이미지를 3차원으로 변환한다고 보시면 됩니다. 멀리 있는 것은 작게 보이고, 가까이 있는 것은 크게 보이는 게 원근법의 원리입니다. 이 원근법의 원리를 적용해 변환하는 방식이 원근 변환입니다. 원근 변환에 필요한 변환 행렬을 반환해주는 함수는 아래와 같습니다.

mtrx = cv2.getPerspectiveTransform(pts1, pts2)

  • pts1: 변환 이전 영상의 좌표 4개, 4 x 2 배열
  • pts2: 변환 이후 영상의 좌표 4개, 4 x 2 배열
  • mtrx: 변환행렬 반환, 3 x 3 행렬

지금까지는 변환행렬을 cv2.warpAffine() 함수에 전달해주었는데, 원근 변환은 별도의 함수 cv2.warpPerspective() 함수를 써야 합니다. 이 함수의 모든 파라미터는 cv2.warpAffine()과 동일합니다.

 

5.2.3 삼각형 어핀 변환


삼각형 어핀 변환 OpenCV가 제공하는 기하학적 변환은 기본적으로 사각형이 기준입니다. 따라서 삼각형 모양의 변환을 하려면 아래와 같이 복잡한 과정을 거쳐야 합니다.

  1. 어핀 변환 전 삼각형 좌표 3개를 정한다.
  2. 어핀 변환 후 삼각형 좌표 3개를 정한다.
  3. 변환 전 삼각형 좌표를 감싸는 외접 사각형 좌표를 구한다.
  4. 변환 후 삼각형 좌표를 감싸는 외접 사각형 좌표를 구한다.
  5. 과정 3, 4의 사각형 영역을 관심 영역(ROI, regison of interest)으로 지정한다.
  6. 과정 5의 관심 영역을 기준으로 변환 전, 후의 삼각형 좌표를 다시 계산한다.
  7. 과정 6의 변환 전 삼각형 좌표를 변환 후 삼각형 좌표로 어핀 변환해주는 변환 행렬을 구한다.
  8. 과정 7에서 구한 변환행렬을 적용해 어핀 변환을 한다.
  9. 과정 8에서 변환된 관심 영역에서 과정 2의 삼각형 좌표만 마스킹한다.
  10. 과정 9에서 구한 마스크를 이용해서 어핀 변환한 이미지와 원본 이미지를 합성한다.

글로만 읽었을 때는 상당히 복잡해 보입니다. 위 과정과 아래의 코드를 함께 보면 이해가 좀 더 수월할 겁니다. 또한, 위의 과정 3, 4처럼 삼각형 좌표를 감싸는 외접 사각형 좌표를 구하려면 cv2.boundingRect() 함수를 써야 합니다.

x, y, w, h = cv2.boudingRect(pts)

  • pts: 다각형 좌표
  • x, y, w, h = 외접 사각형의 좌표와 폭과 높이

그리고 마스크를 구하기 위해 아래 함수가 필요합니다.

cv2.fillConvexPoly(img, pts, color, lineTypes)

  • img: 입력 이미지
  • pts: 다각형 좌표
  • color: 다각형을 채울 색상
  • lineType(optional): 선 그리기 알고리즘 선택 플래그

아래는 로봇 태권 V 장난감 이미지의 얼굴 부분을 삼각형 어핀 변환하는 예제입니다.


5.3 렌즈 왜곡

 

지금까지 이미지 이동(Translation), 확대/축소(Scaling), 회전(Rotation), 어핀 변환(Affine Transformation), 원근 변환(Perspective Transformation)에 대해 배웠습니다. 이는 모두 변환 행렬을 이용해서 구할 수 있습니다. 하지만 변환 행렬로는 구할 수 없는 모양의 변환도 있습니다. 렌즈 왜곡 변환이 바로 변환 행렬로는 구할 수 없는 변환입니다. 렌즈 왜곡 변환에는 리매핑, 오목 렌즈/볼록 렌즈 왜곡, 방사 왜곡이 있습니다. 차례대로 살펴보겠습니다.

 
5.3.1 리매핑

 

리매핑이란 규칙성 없이 마음대로 이미지의 모양을 변환하는 것을 말합니다. 리매핑을 위해 OpenCV는 cv2.remap()이라는 함수를 제공합니다.

dst = cv2.remap(src, mapx, mapy, interpolation, dst, borderMode, borderValue)

  • src: 입력 이미지
  • mapx, mapy: x축과 y축으로 이동할 좌표, src와 동일한 크기, dtype=float32
  • dst(optional): 결과 이미지
  • 나머지 인자는 cv2.warpAffine()과 동일

예를 들어, mapx[0][0]=10, mapy[0][0]=5로 지정했다면 이 의미는 src 좌표 (0, 0)에 있는 픽셀을 (10, 5)로 옮기라는 것입니다. mapx와 mapy는 초기 값으로 0 같은 의미 없는 값이 아니라 원래 이미지의 좌표 값을 갖는 것이 좋습니다. 왜냐하면 전체 픽셀 중 옮기고 싶은 픽셀에 대해서만 새로운 좌표를 지정하거나 원래 위치에서 얼마만큼 이동하라고 명령하는 것이 편하기 때문입니다. mapx와 mapy를 np.zeros()로 초기화한 뒤 for문으로 초기화할 수 있지만 이렇게 하면 시간이 너무 오래 걸립니다. 대신 다음과 같이 np.indices() 함수를 쓰면 빠르게 초기화할 수 있습니다.

mapy, mapx = np.indices( (rows, cols), dtype=np.float32)

아래는 np.indices() 함수의 예시입니다. 반환된 결과의 0번째가 행 배열, 1번째가 열 배열입니다.

 

>>> np.indices((2,2))
array([[[0, 0],
        [1, 1]],

       [[0, 1],
        [0, 1]]])


>>> np.indices((3, 3))
array([[[0, 0, 0],
        [1, 1, 1],
        [2, 2, 2]],

       [[0, 1, 2],
        [0, 1, 2],
        [0, 1, 2]]])

아래는 이미지를 뒤집는 코드입니다. 변환 행렬과 cv2.remap() 함수로 각각 구현해서 똑같은 결과를 보여줄 겁니다. 참고로 영상을 뒤집기 위한 연산식은 다음과 같습니다. 이 연산식을 활용하여 변환 행렬을 구할 것입니다.

x' = cols - x - 1

y' = rows - y - 1

 

변환행렬을 이용하여 뒤집은 이미지와 cv2.remap() 함수로 리매핑하여 뒤집은 이미지의 결과는 똑같습니다. (공간 관계상 뒤집힌 배경 이미지는 하나만 삽입했습니다.) 표현만 다를 뿐이지 그 의미는 같습니다. 그러나 cv2.remap() 함수로 변환하는 것은 변환 행렬로 변환하는 것보다 수행 속도가 더 느립니다. 따라서 변환행렬로 표현할 수 있는 것은 변환행렬로 변환을 하는 것이 좋습니다. 변환행렬로 표현할 수 없는 비선형 변환에만 cv2.remap() 함수를 사용하는 것이 좋습니다.

 

5.3.2 오목 렌즈와 볼록 렌즈 왜곡


오목 렌즈, 볼록 렌즈 왜곡에 대해서 살펴보기 전에 직교 좌표계, 극좌표계에 대해 먼저 알아보겠습니다. 우선, 아래 그림을 보겠습니다.

 


 

우리는 일반적으로 x축과 y축의 직각으로 각각 선을 그어서 만나는 지점을 좌표 (x, y)로 나타냅니다. 이러한 형태의 좌표 시스템을 직교 좌표계(Cartesian coordinate system)라고 부릅니다. 반면, 원점으로부터의 거리(r)와 사잇각(Θ)을 이용해서 (r, Θ)로 나타내는 방법이 있는데, 이를 극좌표계(Polar coordinate system)라고 부릅니다. 두 좌표계는 상호 변환이 가능합니다. 좌표 변환은 연산식을 이용해도 되지만, OpenCV는 좌표 변환을 위해 다음과 같은 함수를 제공합니다.

r, theta = cv2.cartToPolar(x, y): 직교 좌표 → 극좌표 변환

x, y = cv2.polarToCart(r, theta): 극좌표 → 직교 좌표 변환

좌표의 변환뿐만 아니라 좌표의 기준점 변환도 중요합니다. 일반적으로 직교 좌표계를 사용할 때는 좌측 상단을 원점(0, 0)으로 정합니다. 그러나 극좌표에서는 이미지의 중앙을 원점으로 해야 합니다. 이미지의 중앙을 (0, 0)으로 두기 위해서 좌표의 값을 -1 ~ 1로 정규화해야 합니다.

 

exp는 이미지의 왜곡 지수를 나타내는 변수로 1이면 원본과 동일하게 하고, 1보다 작으면 오목 렌즈 효과를 내고, 1보다 크면 볼록 렌즈 효과를 냅니다. 맨 오른쪽은 exp=0.5로 설정한 결과입니다. scale은 이미지에서 렌즈 효과를 주고 싶은 원 모양 영역의 크기를 비율로 나타낸 것입니다. scale=1은 100%를 의미합니다.

그리고 아래 코드는 좌표의 기준점을 바꾸고 -1~1 범위로 정규화하는 기능을 합니다. 이는 이후에 다시 좌상단 기준점으로 변경됩니다.

mapx = 2*mapx/(cols-1)-1

mapy = 2*mapy/(rows-1)-1

실질적인 렌즈 효과는 아래 코드에서 이루어집니다.

r[r< scale] = r[r<scale] **exp

앞서 좌표의 범위를 -1~1로 정규화했습니다. 따라서 scale의 최댓값은 1입니다. 1을 넘는 값을 가질 수는 없습니다. 극좌표계로 바꿨을 때의 r은 원의 반지름입니다. 반지름이 scale보다 작은 범위에 있는 좌표에 대해서는 exp(왜곡 지수)를 곱해주었습니다. 이때 왜곡 지수가 1보다 크면 볼록 렌즈 효과를, 1보다 작으면 오목 렌즈 효과를 줍니다.

 

5.3.3 방사 왜곡

 

카메라를 통해 이미지를 촬영할 때 카메라 가장자리 부분에서 약간의 왜곡이 생기는 것을 본 적이 있을 겁니다. 이런 현상을 배럴 왜곡(barrel distortiaon)이라고 합니다.

 


위 그림과 같이 실제 이미지를 렌즈를 통해 촬영하면 가장자리 부분이 약간 둥그스름해지는 것입니다. 둥근 모양이 배럴 통 같다고 하여 배럴 왜곡이라고 합니다. 반면, 가장자리 부분이 안쪽으로 들어가는 형태의 왜곡을 핀쿠션 왜곡(pinsushion distortion)이라고 합니다.


 

opencv강좌_05.ipynb
5.59MB

6장 영상 필터

6.1.1 필터와 컨볼루션

 

 

컨볼루션 연산은 공간 영역 필터링을 위한 핵심 연산 방법입니다. 블러링 작업을 예로 들어 컨볼루션 연산이 어떻게 진행되는지 살펴보겠습니다.

공간 영역 필터링은 연산 대상 픽셀과 그 주변 픽셀들을 활용하여 새로운 픽셀 값을 얻는 방법이라고 했습니다. 이때 주변 픽셀을 어느 범위까지 활용할지 그리고 연산은 어떻게 할지를 결정해야 합니다. 이런 역할을 하는 것이 바로 커널(kernel)입니다. 커널은 윈도(window), 필터(filter), 마스크(mask)라고도 부릅니다. 아래 그림에서 가운데 있는 3 x 3 짜리 행렬이 바로 커널입니다.

위 그림은 3 x 3 커널로 컨볼루션 연산을 하는 예시입니다. 기존 영상에서 픽셀 값 6을 기준으로 주변에 있는 픽셀 값인 3, 0, 1, 2, 1, 4, 2, 2(시계 방향)까지 활용했습니다. 일대일로 대응하는 위치에 있는 커널의 요소와 대응하는 입력 픽셀 값을 곱해서 모두 합한 것을 결과 픽셀 값으로 결정했습니다. 이런 연산을 마지막 픽셀까지 반복하는 것을 컨볼루션 연산이라고 합니다.

OpenCV에서는 아래 함수로 컨볼루션 연산을 지원합니다.

dst = cv2.filter2D(src, ddepth, kernel, dst, anchor, delta, borderType)

  • src : 입력 영상, Numpy 배열
  • ddepth : 출력 영상의 dtype (-1: 입력 영상과 동일)
  • kernel : 컨볼루션 커널, float32의 n x n 크기 배열
  • dst(optional) : 결과 영상
  • anchor(optional) : 커널의 기준점, default: 중심점 (-1, -1)
  • delta(optional) : 필터가 적용된 결과에 추가할 값
  • borderType(optional) : 외곽 픽셀 보정 방법 지정

 

6.1.2 평균 블러링

 

 

앞서 설명했듯이 블러링은 초점이 맞지 않듯이 영상을 흐릿하게 하는 작업을 뜻합니다. 가장 간단한 블러링 방법으로는 평균 블러링이 있습니다. 평균 블러링은 주변 픽셀 값들의 평균을 적용합니다. 주변 픽셀들의 평균값을 적용하면 픽셀 간 차이가 적어져 선명도가 떨어져 전체적으로 흐릿해집니다.

아래는 5 x 5 평균 블러링 필터를 활용하여 컨볼루션 연산을 적용한 예시입니다.

 

원본에 평균 블러링을 적용하니 흐릿해진 것을 볼 수 있습니다. 위 코드에서 np.ones((5, 5))/5**2는 평균 블러링 필터 역할을 합니다. 위에서 맨 처음 컨볼루션 연산을 배울 때는 원본 이미지의 픽셀 값과 그에 대응하는 필터의 픽셀 값을 요소 별로 곱한 뒤 모두 합해줬습니다. 하지만 여기서는 평균 블러링을 적용해야 하므로 5 x 5 필터의 요소 개수인 5**2(=25)로 나누어 준 것입니다.

 

필터의 크기가 클수록 평균 블러링을 적용했을 때 선명도가 더 떨어집니다.

 

 

위와 같이 개발자가 직접 커널을 생성하지 않고도 평균 블러링을 적용할 수 있습니다. OpenCV에서는 아래와 같은 평균 블러링 함수를 제공합니다.

dst = cv2.blur(src, ksize, dst, anchor, borderType)

  • src: 입력 영상, numpy 배열
  • ksize: 커널의 크기
  • 나머지 파라미터는 cv2.filter2D()와 동일

dst = cv2.boxFilter(src, ddepth, ksize, dst, anchor, normalize, borderType)

  • ddepth: 출력 영상의 dtype (-1: 입력 영상과 동일)
  • normalize(optional): 커널 크기로 정규화(1/ksize²) 지정 여부 (Boolean), default=True
  • 나머지 파라미터는 cv2.filter2D()와 동일

cv2.blur() 함수는 커널의 크기만 정해주면 알아서 평균 커널을 생성해서 평균 블러링을 적용한 영상을 출력합니다. 커널 크기는 일반적으로 홀수로 정합니다. cv2.boxFilter() 함수는 normalize에 True를 전달하면 cv2.blur() 함수와 동일한 기능을 합니다.

아래는 cv2.blur() 함수와 cv2.boxFilter() 함수를 이용하여 평균 블러링을 적용하는 예제 코드입니다.

 

cv2.blur() 함수와 cv2.boxFilter() 함수가 동일한 결과를 냈음을 알 수 있습니다.

 

6.1.3 가우시안 블러링

 

가우시안 분포를 갖는 커널로 블러링 하는 것을 가우시안 블러링이라고 합니다. 가우시안 분포(gaussian distribution)란 정규 분포(normal distribution)이라고도 하는데, 평균 근처에 몰려 있는 값들의 개수가 많고 평균에서 멀어질수록 그 개수가 적어지는 분포를 말합니다.

가우시안 블러링 커널은 아래와 같이 중앙값이 가장 크고 중앙에서 멀어질수록 그 값이 작아집니다.

 

 

첫 번째 커널에서 16으로 나눈 이유는 커널의 모든 요소의 합이 16이기 때문입니다. (1+2+1+2+4+2+1+2+1 = 16) 두 번째 커널도 모든 요소의 합이 256이므로 256으로 나누어준 것입니다. 이런 가우시안 블러링 커널을 적용하면 대상 픽셀에 가까울수록 많은 영향을 주고, 멀어질수록 적은 영향을 주기 때문에 원래의 영상과 비슷하면서도 노이즈를 제거하는 효과가 있습니다.

OpenCV에서는 아래와 같이 가우시안 블러링을 적용하는 함수를 제공합니다.

cv2.GaussianBlur(src, ksize, sigmaX, sigmaY, borderType)

  • src: 입력 영상
  • ksize: 커널 크기 (주로 홀수)
  • sigmaX: X 방향 표준편차 (0: auto)
  • sigmaY(optional): Y 방향 표준편차 (default: sigmaX)
  • borderType(optional): 외곽 테두리 보정 방식
  • ret = cv2.getGaussianKernel(ksize, sigma, ktype)
  • ret: 가우시안 커널 (1차원이므로 ret * ret.T 형태로 사용해야 함)

cv2.GaussianBlur() 함수는 커널 크기와 표준 편차를 전달하면 가우시안 블러링을 적용해줍니다. sigmaX에 0을 전달하면 자동으로 표준편차를 선택해서 사용하고, sigmaY를 생략하면 sigmaX 값과 동일하게 적용합니다.

cv2.getGaussianKernel() 함수는 커널 크기와 표준 편차를 전달하면 가우시안 필터를 반환합니다. 반환된 필터는 1차원이므로 cv2.filter2D() 함수에 사용하려면 ret * ret.T와 같은 형식으로 전달해야 합니다.

 

첫 번째로 가우시안 필터를 직접 생성해서 cv2.filter2D() 함수에 전달하여 블러링을 적용했습니다. 두 번째로 cv2.getGaussianKernel() 함수를 이용해 가우시안 커널을 얻었습니다. 얻은 커널을 역시 cv2.filter2D() 함수에 전달하여 블러링을 적용했습니다. 이때 주의할 것은 k2*k2.T와 같은 형태로 전달해야 한다는 점입니다. 마지막으로 cv2.GaussianBlur() 함수를 활용하여 필터를 별도로 구하지 않고 직접 가우시안 블러링을 적용했습니다. 결과 이미지가 작아서 잘 안 보이겠지만 노이즈가 제거된 것을 알 수 있습니다. 이렇듯 가우시안 블러링은 노이즈를 제거하는 효과가 있습니다.

 

6.1.4 미디언 블러링

 
 

커널의 픽셀 값 중 중앙값을 선택하는 것을 미디언 블러링이라고 합니다. 미디언 블러링은 소금-후추 잡음을 제거하는 효과가 있습니다. 소금-후추 잡음이란 이미지에 소금과 후추를 뿌린 것과 같이 생긴 잡음을 뜻합니다. OpenCV는 미디언 블러링을 위해 아래 함수를 제공합니다.

dst = cv2.medianBlur(src, ksize)

  • src: 입력 영상
  • ksize: 커널 크기

 

6.1.5 바이레터럴 필터

 

지금까지 적용한 블러링은 잡음을 제거하는 효과는 뛰어났지만 그만큼 경계도 흐릿하게 만드는 문제가 있었습니다. 바이레터럴 필터는 이를 개선하기 위해 가우시안 필터와 경계 필터를 결합합니다. 경계도 뚜렷하고 노이즈도 제거되는 효과가 있지만 속도가 느리다는 단점이 있습니다.

dst = cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace, dst, borderType)

  • src: 입력 영상
  • d: 필터의 직경(diameter), 5보다 크면 매우 느림
  • sigmaColor: 색공간의 시그마 값
  • sigmaSpace: 좌표 공간의 시그마 값

일반적으로 sigmaColor와 sigmaSpace는 같은 값을 사용하며, 값의 범위는 10~150을 권장합니다.

 

가우시안 필터를 적용했을 때는 경곗값이 흐릿하지만, 바이레터럴 필터를 적용했을 때는 노이즈는 줄면서 경곗값을 유지되는 것을 볼 수 있습니다.

 

6.2 경계 검출

 

지금까지는 영상을 흐릿하게 하는 블러링에 대해 알아봤습니다. 이번 포스팅에서는 반대로 영상의 경계를 뚜렷하게 만드는 방법에 대해 알아보겠습니다. 영상의 경계를 선명하고 뚜렷하게 만드는 작업을 샤프닝(sharpening)이라고 합니다. 샤프닝은 영상에서 경계를 검출하여 경계에 있는 픽셀을 강조합니다.


6.2.1 기본 미분 필터

 

경계(엣지)를 검출하기 위해서는 픽셀 값이 급격하게 변하는 지점을 찾아야 합니다. 경계 부분에서는 당연히 픽셀 값이 급격히 변하겠죠? 이는 연속된 픽셀 값에 미분을 하여 찾아낼 수 있습니다. 하지만 픽셀은 연속 공간 안에 있지 않으므로 미분 근사값을 구해야 합니다. 미분 근사값은 간단합니다. 서로 붙어 있는 픽셀 값을 빼면 됩니다. x방향, y방향으로 각각 픽셀 값을 빼면 미분 근사값이 됩니다.

 

영상 내 픽셀 값의 미분 근사값 연산을 위한 컨볼루션 커널은 아래와 같습니다. 요소가 -1과 1인 이유는 단지 접해있는 픽셀 값을 빼기 때문입니다.

x방향, y방향의 미분 커널을 생성하여 필터링을 적용했습니다. x방향 미분 필터는 세로 방향의 경계를 검출했고, y방향 미분 필터는 가로 방향의 경계를 검출했습니다. x방향 미분 필터는 좌우 픽셀 값의 차를 기반으로 필터링했기 때문에 세로 방향의 경계를 검출한 것이고, 반대로 y방향 미분 필터는 상하 픽셀 값의 차를 기반으로 필터링했기 때문에 가로 방향의 경계를 검출한 것입니다.

 

6.2.2 로버츠 교차 필터

 

로렌스 로버츠라는 미국 엔지니어는 기본 미분 필터를 개선한 로버츠 교차 필터를 제안했습니다. 로버츠 교차 필터를 위한 컨볼루션 커널은 아래와 같습니다.

이 커널은 대각선 방향으로 +1과 -1을 배치시켜 사선 경계 검출 효과를 높였습니다. 하지만 노이즈에 민감하다는 단점이 있습니다.

 

6.2.3 프리윗 필터

 

프리윗 필터는 x축과 y축의 각 방향으로 차분을 세 번 계산하여 경계를 검출하는 필터입니다. 프리윗 필터는 상하/좌우 경계는 뚜렷하게 잘 검출하지만 대각선 검출이 약합니다.

확실히 기본 미분 필터나 로버츠 교차 필터에 비해 상하/좌우 경계 검출 강도가 강합니다. 마지막은 상하/좌우 경계 검출 필터로 필터링한 결과를 합친 것입니다.

 

6.2.4 소벨 필터

소벨 필터는 중심 픽셀의 차분 비중을 두 배로 준 필터입니다. 따라서 소벨 필터는 x축, y축, 대각선 방향의 경계 검출에 모두 강합니다.

앞서 설명한 로버츠 필터와 프리윗 필터는 현재는 거의 쓰이지 않습니다. 반면 소벨 필터는 실무적으로도 쓰이므로 OpenCV에서 별도의 함수를 제공합니다.

dst = cv2.Sobel(src, ddepth, dx, dy, dst, ksize, scale, delta, borderType)

  • src: 입력 영상
  • ddepth: 출력 영상의 dtype (-1: 입력 영상과 동일)
  • dx, dy: 미분 차수 (0, 1, 2 중 선택, 둘 다 0일 수는 없음)
  • ksize: 커널의 크기 (1, 3, 5, 7 중 선택)
  • scale: 미분에 사용할 계수
  • delta: 연산 결과에 가산할 값

 

소벨 필터를 직접 생성하여 필터링을 적용해보고, cv2.Sobel() 함수로도 필터링을 적용해봤습니다. 두 결과는 동일한 것을 볼 수 있습니다.

 

6.2.5 샤르 필터

 

소벨 필터는 커널의 중심에서 멀어질수록 엣지 방향성의 정확도가 떨어집니다. 이를 개선한 필터가 샤르 필터입니다. 샤를 필터를 위한 컨볼루션 커널은 아래와 같습니다.

 cv2.Scharr(src, ddepth, dx, dy, dst, scale, delta, borderType) ksize가 없다는 것을 제외하면 모든 파라미터는 cv2.Sobel()과 동일합니다.

6.2.6 라플라시안 필터

 

라플라시안 필터는 2차 미분을 적용한 필터입니다. 경계를 더 제대로 검출할 수 있습니다.

dst = cv2.Laplacian(src, ddepth, dst, ksize, scale, delta, borderType)

  • 파라미터는 cv2.Sobel()과 동일합니다.

6.2.7 캐니 엣지

 
 

캐니 엣지는 지금까지 살펴본 것처럼 한 가지 필터만 사용하는 것이 아니라 다음의 4단계 알고리즘에 따라 경계를 검출합니다.

  1. 노이즈 제거: 5 x 5 가우시안 블러링 필터로 노이즈 제거
  2. 경계 그레디언트 방향 계산: 소벨 필터로 경계 및 그레디언트 방향 검출
  3. 비최대치 억제(Non-Maximum Suppression): 그레디언트 방향에서 검출된 경계 중 가장 큰 값만 선택하고 나머지는 제거
  4. 이력 스레시홀딩: 두 개의 경계 값(Max, Min)을 지정해서 경계 영역에 있는 픽셀들 중 큰 경계 값(Max) 밖의 픽셀과 연결성이 없는 픽셀 제거

OpenCV에서 제공하는 캐니 엣지는 함수는 아래와 같습니다.

edges = cv2.Canny(img, threshold1, threshold2, edges, apertureSize, L2gardient)

  • img: 입력 영상
  • threshold1, threshold2: 이력 스레시홀딩에 사용할 Min, Max 값
  • apertureSize: 소벨 마스크에 사용할 커널 크기
  • L2gradient: 그레디언트 강도를 구할 방식 (True: 제곱 합의 루트 False: 절댓값의 합)
  • edges: 엣지 결과 값을 갖는 2차원 배열

 

확실히 하나의 필터를 적용한 것보다 경계 검출이 잘 됩니다. 그만큼 많이 쓰이는 필터입니다.

 

6.3 모폴로지

모폴로지(morphology)란 '형태학'이라는 뜻입니다. 이는 영상 분야에서 노이즈 제거, 구멍 채우기, 끊어진 선 이어 붙이기 등에 쓰이는 형태학적 연산을 말합니다. 모폴로지 연산은 검은색과 흰색으로만 구성되어 있는 바이너리(binary) 이미지에 적용할 수 있습니다. 모폴로지 연산으로는 침식, 팽창, 열림, 닫힘이 있는데 이에 대해 차례대로 배워보겠습니다.

 

6.3.1 침식 연산

 

침식(erosion)이란 말 그대로 형태를 깎아 내는 것입니다. 따라서 침식 연산은 이미지를 깎아 내는 연산을 뜻합니다. 침식 연산을 위해서는 구조화 요소 커널(structuring element kernel)이라는 0과 1로 구성된 커널이 필요합니다. 구조화 요소 커널은 1이 채워진 모양에 따라 사각형, 타원형, 십자형 등으로 사용할 수 있습니다.

침식 연산은 구조화 요소 커널을 입력 이미지에 적용해서 1로 채워진 영역을 온전히 올려 놓을 수 없으면 해당 픽셀을 0으로 변경합니다. 아래는 십자형 구조화 요소 커널로 침식 연산을 하는 과정을 보여줍니다.

 

A 이미지에서 흰색 배경은 0이고, 하늘색 전경은 1이라고 합시다. B는 십자형 구조화 요소 커널입니다. 가운데를 포함해서 회색 부분이 다 1로 구성되어 있다고 보면 됩니다. 이때 십자형 구조화 요소 커널의 중심부(빨간 점 부분)는 A 이미지의 파란색 부분을 쭉 훑습니다. 한 칸 한 칸 훑으면서 구조화 요소 커널이 A 이미지의 하늘색 부분과 완전히 겹치지 않을 때는 0으로 변경합니다. 완전히 겹치면 1로 그대로 둡니다. 오른쪽은 침식 연산의 결과입니다. 원래 하늘색 부분이 모두 1이었는데, 침식 연산 결과 빨간 1이 적혀 있는 부분만 1로 남아 있고 나머지 부분은 모두 0으로 변경됩니다. 원본 이미지보다 조금 깎인 것을 볼 수 있습니다.

 

구조화 요소 커널 생성을 위한 함수는 다음과 같습니다.

cv2.getStructuringElement(shape, ksize, anchor)

  • shape: 구조화 요소 커널 모양
    • cv2.MORPH_RECT: 사각형,
    • cv2.MORPH_EPLIPSE: 타원형,
    • cv2.MORPH_CROSS: 십자형)
  • ksize: 커널 크기
  • anchor(optional): 구조화 요소의 기준점, cv2.MORPH_CROSS에만 의미 있으며 기본 값은 중심점 (-1, -1)

위 함수로 생성한 구조화 요소 커널로 침식 연산을 수행하는 함수는 다음과 같습니다.

dst = cv2.erode(src, kernel, anchor, iterations, borderType, borderValue)

  • src: 입력 영상, 바이너리
  • kernel: 구조화 요소 커널
  • anchor(optional): cv2.getStructuringElement()와 동일
  • iterations(optional): 침식 연산 적용 반복 횟수
  • boderType(optional): 외곽 영역 보정 방법
  • boderValue(optional): 외곽 영역 보정 값

침식 연산은 큰 물체의 주변을 깎는 기능을 합니다. 더불어 작은 물체는 아예 없애버리므로 노이즈 제거 효과도 있고, 원래는 떨어져 있는 물체인데 겹쳐 있는 것을 서로 떼어내는 데도 효과적입니다.

 

침식 연산을 위한 구조화 요소 커널은 3 x 3의 사각형 모양으로 생성했습니다. 결과는 보시는 바와 같이 노이즈가 제거되었고, 글씨가 전반적으로 가늘어졌습니다.

 

6.3.2 팽창 연산

팽창(dilatation)은 침식과 반대로 물체의 주변을 확장하는 연산입니다. 연산 방법도 반대입니다. 침식 연산은 구조화 요소 커널이 입력 영상에서 1로 채워진 영역과 완전히 겹치지 않으면 0으로 변경했습니다. 그러나 팽창 연산은 이와 반대로 완전히 겹치지 않으면 1로 변경합니다. 아래 예시에서 구조화 요소 커널은 십자형이 아님에 유의하기 바랍니다.

 

팽창을 위한 함수는 아래와 같습니다.

dst = cv2.dilate(src, kernel, dst, anchor, iterations, bordeType, borderValue)

  • 모든 파라미터는 cv2.erode()와 동일합니다.

 

실행 결과 글씨가 더 팽창되긴 했지만 글씨 안에 있던 검정색 노이즈가 제거된 것을 볼 수 있습니다. 흰색 글씨가 바깥으로도 안으로도 팽창이 되어 노이즈는 제거되고 더 뚱뚱해진 거라고 생각하시면 됩니다.

 

6.3.3 열림과 닫힘, 그밖의 모폴로지 연산

 

침식은 어두운 부분의 노이즈를 제거하는 효과가 있고 팽창은 밝은 부분의 노이즈를 제거하는 효과가 있다는 것을 살펴봤습니다. 노이즈 제거 효과는 좋으나 원래 모양이 홀쭉해지거나 뚱뚱해지는 변형이 일어납니다. 하지만 침식과 팽창의 연산을 조합하면 원래의 모양을 유지하면서 노이즈를 제거하는 효과를 거둘 수 있습니다.

 

침식 연산 후 팽창 연산을 적용하는 것을 열림(opening) 연산이라고 하고, 팽창 연산 후 침식 연산을 적용하는 것을 닫힘(closing) 연산이라고 합니다. 열림 연산은 주변보다 밝은 노이즈를 제거하는데 효과적입니다. 또한 맞닿아 있는 것처럼 보이는 독립된 개체를 분리하거나 돌출된 모양을 제거하는 데 효과적입니다. 반면, 닫힘 연산은 주변보다 어두운 노이즈를 제거하는데 효과적이면서 끊어져 보이는 개체를 연결하거나 구멍을 메우는 데 효과적입니다.

열림 = 침식 + 팽창

닫힘 = 팽창 + 침식

팽창 연산을 적용한 이미지에서 침식 연산을 적용한 이미지를 빼면 경계 픽셀만 얻게 되는데, 이는 앞서 살펴본 경계 검출과 비슷합니다. 이런 연산을 그레디언트(gradient) 연산이라고 합니다.

그레디언트 = 팽창 - 침식

또한, 원본에서 열림 연산 적용 결과를 빼면 값이 크게 튀는 밝은 영역을 강조할 수 있고, 닫힘 연산 적용 결과에서 원본을 빼면 어두운 부분을 강조할 수 있습니다. 이것을 각각 탑햇(top hat)과 블랙햇(black hat) 연산이라고 합니다.

탑햇 = 원본 - 열림

블랙햇 = 닫힘 - 원본

OpenCV는 열림, 닫힘, 그레디언트, 탑햇, 블랙햇 연산을 위해서 아래의 함수를 제공합니다.

dst = cv2.morphologyEx(src, op, kernel, dst, anchor, iteration, borderType, borderValue)

  • src: 입력 영상
  • op: 모폴로지 연산 종류
    • cv2.MORPH_OPEN : 열림 연산
    • cv2.MORPH_COLSE : 닫힘 연산
    • cv2.MORPH_GRADIENT : 그레디언트 연산
    • cv2.MORPH_TOPHAT : 탑햇 연산
    • cv2.MORPH_BLACKHAT : 블랙햇 연산
  • kernel: 구조화 요소 커널
  • dst(optional): 결과 영상
  • anchor(optional): 커널의 기준점
  • iteration(optional): 연산 반복 횟수
  • borderType(optional): 외곽 영역 보정 방법
  • borderValue(optional): 외곽 영역 보정 값

6.4 이미지 피라미드

 

이미지 피라미드(image pyramid)란 이미지의 크기를 피라미드처럼 단계적으로 확대하거나 축소하는 작업을 말합니다.

 
6.4.1 가우시안 피라미드
 

가우시안 필터를 적용한 뒤 이미지 피라미드를 구성하는 것을 가우시안 피라미드(gaussian pyramid)라고 합니다. OpenCV에서는 아래와 같은 가우시안 피라미드 함수를 제공합니다.

dst = cv2.pyrDown(src, dst, dstsize, borderType)

dst = cv2.pyrUp(src, dst, dstsize, borderType)

  • src: 입력 영상
  • dst: 결과 영상
  • distsize: 결과 영상 크기
  • borderType: 외곽 보정 방식

cv2.pyrDown()은 가우시안 필터를 적용한 뒤 모든 짝수 행과 열을 삭제해 입력 영상의 1/4 크기로 축소합니다. 반면 cv2.pyrUp()은 0으로 채워진 짝수 행과 열을 새롭게 삽입하고 나서 가우시안 필터를 적용해 주변 픽셀과 비슷하게 만드는 방법으로 크기는 4배 확대합니다.

 

6.4.2 라플라시안 피라미드

 

cv2.pyUp() 함수로 이미지를 확대하면 0으로 채워진 행과 열이 새롭게 삽입되므로 원본 이미지보다 화질이 떨어집니다. 따라서 cv2.pyDown() 함수를 적용한 뒤 다시 cv2.pyUp()을 하면 원본 이미지보다 화질이 많이 떨어집니다. 이런 문제점을 개선한 방식이 라플라시안 피라미드(laplacian pyramid)입니다.

opencv강좌_06.ipynb
7.65MB
DR-01408_박성호_딥러닝개론_11차시.ipynb
3.33MB

'두산 로보틱스 부트캠프 ROKEY > Computer Vision 교육' 카테고리의 다른 글

ep.39 OpenCV5  (0) 2024.08.30
ep.38 OpenCV4  (0) 2024.08.29
ep.36 OpenCV2  (0) 2024.08.27
ep.35 OpenCV1  (0) 2024.08.26
ep.34 딥러닝개론8  (0) 2024.08.23