본문 바로가기

딥러닝

[딥러닝] ROI, 이진화 알고리즘, OTSU, 영상의 변환(translate,resize,rotation,perspective)

728x90

1.  관심 영역(ROI, Region of Interest)

  • 영상 내에서 관심이 있는 영역

 

✔️ ROI 추출, 복사하고 그 영역을 녹색 사각형으로 강조하기

import cv2

img = cv2.imread('./sun.jpg')

# ROI 영역의 좌표와 크기 설정
x = 182
y = 21
w = 120
h = 110

# ROI 영역 설정
roi = img[y:y+h, x:x+w]
roi_copy = roi.copy()   # copy()는 원본에 영향을 끼치지 않음

# 원본 이미지의 오른쪽에 ROI 영역 복사
img[y:y+h, x+w:x+w+w] = roi

# 복사된 ROI 영역 주위에 녹색 사각형 그리기
cv2.rectangle(img, (x, y), (x+w+w, y+h), (0, 255, 0), 3)

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

 

 

✔️ ROI 직접 드래그하고 Enter키 눌러 추출하기(selectROI())

import cv2

img = cv2.imread('./sun.jpg')

# ROI 선택(윈도우 상에서 직접 드래그하여 선택)
x, y, w, h = cv2.selectROI('img', img, False)    # False: Cross 표기 여부

# 선택한 ROI가 유효한 경우
if w and h:
	roi = img[y: y+h, x: x+w]
    cv2.imshow('roi', roi)
    
cv2.waitKey()

직접 드래그하기
ROI 추출됨

 

 

✔️ Enter키 누르지 않고 바로 ROI 추출

import cv2

oldx = oldy = w = h = 0    # 마우스 드래그 시작 위치, 드래그한 영역의 너비와 높이
color = (255, 0, 0)   # 사각형 색상
img_copy = None      # 드래그 중 사각형을 그리기 위한 이미지 복사본
isDrag = False      # 드래그 중인지 여부

# 마우스 이벤트 처리
def on_mouse(event, x, y, flags, param):
	global oldx, oldy, w, h, color, img_copy, isDrag
     
    if event == cv2.EVENT_LBUTTONDOWN:    # 왼쪽 버튼이 눌리면
    	isDrag = True         # 드래그 시작
        oldx = x              # 시작 좌표 저장
        oldy = y
    elif event == cv2.EVENT_MOUSEMOVE:    # 드래그 중일 때
    	if isDrag:            # 사각형을 그리며 마우스 위치 업데이트
        	img_copy = img.copy()
            cv2.rectangle(img_copy, (oldx, oldy), (x, y), color, 3)
            cv2.imshow('img', img_copy)
    elif event == cv2.EVENT_LBUTTONUP:    # 드래그가 끝나면
    	if isDrag:
        	isDrag = False
            if x > oldx and y > oldy:   # 왼쪽 위에서 오른쪽 아래로 드래그만 유효
            	w = x - oldx
                h = y - oldy
                if w > 0 and h > 0:    # 영역이 유효하면 잘라서 별도로 보여줌
                	cv2.rectangle(img_copy, (oldx, oldy), (x, y), color, 3)
                    cv2.imshow('img', img_copy)
                    roi = img[oldy: oldy+h, oldx: oldx+w]
                    cv2.imshow('roi', roi)
                    
    else:
    	cv2.imshow('img', img)
        print('영역이 잘못 되었음!')
        
img = cv2.imread('./sun.jpg')
cv2.imshow('img', img)
cv2.setMouseCallback('img', on_mouse)
cv2.waitKey()

직접 드래그

 

 

2. 영상의 이진화(Binarization)

  • 픽셀을 검은색 또는 흰색과 같이 두 분류의 값으로 나누는 작업
  • 영상에서 의미있는 관심 영역(ROI)과 비관심 영역으로 구분할 때 사용
  • 예) 배경과 객체를 둘로 나눌 때
  • 영상의 이진화 연산을 할 때 나누는 특정값을 임계값이라고 함
  • cv2.threshold(영상, 임계값, 최댓값, 플래그)
    • cv2.THRESH_BINARY: 픽셀값이 임계값을 넘으면 최댓값으로 지정하고 넘지 못하면 0으로 지정
    • 예) 픽셀값 150, 임계값 120: 픽셀값은 255로 설정
    • cv2.THRESH_BINARY_INV: cv2.THRESH_BINARY의 반대

 

✔️ 이진화(thresholding)

import cv2
import matplotlib.pyplot as plt

img = cv2.imread('./cells.png', cv2.IMREAD_GRAYSCALE)
hist = cv2.calcHist([img], [0], None, [256], [0, 255])   # 이미지의 밝기 값에 대한 히스토그램 계산
                                                         # [256]: 밝기 값의 범위, [0,255]까지의 값이 계산됨

# THRESH_BINARY: 임계값보다 큰 픽셀 값을 255, 작은 픽셀 값을 0으로 설정
a, dst1 = cv2.threshold(img, 100, 255, cv2.THRESH_BINARY)  # 임계값: 100, 픽셀 값을 0 또는 255로 변환
b, dst2 = cv2.threshold(img, 210, 255, cv2.THRESH_BINARY)  # 임계값: 210

cv2.imshow('img', img)
cv2.imshow('dst1', dst1)
cv2.imshow('dst2', dst2)

plt.plot(hist)
plt.show()
cv2.waitKey()

 

 

 

3. 오츠의 이진화 알고리즘

  • 자동 이진화 알고리즘
  • 자동으로 임계값을 구해줌(임계값을 구분하는 가장 좋은 방법으로 찾아줌)
  • cv2.threshold(영상, 임계값, 최댓값, 플래그 | cv2.THRESH_OTSH)
  • 임계값을 임의로 정해 픽셀을 두 부류로 나누고 두 부류의 명암 분포를 구하는 작업을 반복하여 모든 경우의 수 중에서 두 부류의 명암 분류가 가장 균일할 때의 임계값을 선택

 

✔️ 오츠의 자동 이진화 알고리즘

import cv2

img = cv2.imread('./rice.png', cv2.IMREAD_GRAYSCALE)

# 0: 임계값을 지정하지 않음, 255: 이진화된 이미지에서의 최대 픽셀 값
# 오츠의 알고리즘을 추가하여 임계값을 자동으로 결정하게 함
# th: 오츠의 알고리즘으로 계산된 임계값, dst: 이진화된 이미지
th, dst = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print('otsh: ', th)

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

 

 

 

4. 지역 이진화

  • 균일하지 않은 조명 환경에서 사용하는 이진화 방법
  • 전체 구역을 N등분하고 각각의 구역에 이진화 한 뒤에 이어 붙이는 방법
  • 여러 개의 임계값을 이용할 수 있음

 

✔️ 이미지 전체 이진화 vs 지역 이진화

import cv2
import numpy as np

img = cv2.imread('./rice.png', cv2.IMREAD_GRAYSCALE)

_, dst1 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

# 이진화된 부분 이미지들을 결합할 빈 이미지, 원본 크기를 가지며 초기 값은 모두 0
dst2 = np.zeros(img.shape, np.uint8)  
# 이미지의 가로와 세로를 4등분한 블록의 너비(bw)와 높이(bh)
bw = img.shape[1] // 4
bh = img.shape[0] // 4

# 4x4 블록으로 나누어 부분 이진화
for y in range(4):
	for x in range(4):
    	img_ = img[y*bh: (y+1)*bh, x*bw: (x+1)*bw]    # 각 블록에 해당하는 부분 이미지
        dst_ = dst2[y*bh: (y+1)*bh, x*bw:: (x+1)*bw]    # 이진화된 블록을 저장할 부분
        cv2.threshold(img_, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU, dst_) 
        
cv2.imshow('img', img)
cv2.imshow('dst1', dst1)  # 전체 이진화
cv2.imshow('dst2', dst2)  # 지역 이진화
cv2.waitKey()

 

 

 

5. 적응형 이진화

  • 영상을 여러 영역으로 나눈 뒤, 그 주변 픽셀 값만 활용하여 임계값을 구하는 방법
  • cv2.adaptiveThreshold()
    • cv2.ADAPTIVE_THRESH_MEAN_C
      • 이웃 픽셀의 평균으로 결정
      • 선명하지만 잡티가 많아질 가능성이 있음
      • 문서 스캔과 같이 이미지가 고르게 조명된 경우에 효과적
    • cv2.ADAPTIVE_THRESH_GAUSSIAN_C
      • 가우시안 분포에 따른 가중치의 합으로 결정
      • 선명도는 조금 떨어지지만 잡티가 적음
      • 조명이 불균일한 이미지나 디테일을 유지하고자 할 때 효과적

 

import cv2
import matplotlib.pyplot as plt

img = cv2.imread('./sudoku.jpg', cv2.IMREAD_GRAYSCALE)

# 오츠의 이진화 알고리즘
th, dst1 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

# adaptiveThreshold(): 작은 영역마다 이진화 적용
# 255: 이진화된 이미지에서 흰색으로 변환할 최대값
# ADAPTIVE_THRESH_MEAN_C: 주변 픽셀들의 평균값을 기준으로 임계값 설정
# ADAPTIVE_THRESH_GAUSSIAN_C: 가우시안 가중 평균을 기준으로 임계값 설정
# 9: 블록 크기, 5: 계산된 임계값에서 빼는 상수
dst2 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 5)
dst3 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 9, 5)

dic = {'img': img, 'dst1': dst1, 'dst2':dst2, 'dst3': dst3}

for i, (k, v) in enumerate(dic.items()):
	plt.subplot(2, 2, i+1)
    plt.title(k)
    plt.imshow(v, 'gray')
    
plt.show()

 

 

 

6. 영상의 변환

  • 구성하는 픽셀의 배치 구조를 변경함으로 전체 영상의 모양을 바꾸는 작업

 

  • 이미지 이동(translate)
    • 변환행렬
    • M= [ 1 0 a ]       x방향으로 a만큼
    •       [ 0 1 b ]       y방향으로 b만큼
    • cv2.warpaffine(영상, aff행렬, (0, 0)) :  입력 영상과 크기가 같은 행렬을 반환한다
import cv2
import numpy as np

img = cv2.imread('./dog.bmp')
# x축으로 200픽셀, y축으로 100픽셀 평행 이동
aff = np.array([[1, 0, 200], [0, 1, 100]], dtype=np.float32)
dst = cv2.warpAffine(img, aff, (0, 0))  # 주어진 2x3 변환행렬(aff)을 사용하여 이미지 변환
                                        # (0, 0)은 출력 이미지의 크기를 지정하는 부분, 기본적으로 원본 이미지 크기와 동일하게 처리됨
cv2.imshow('img', img)
cv2.imshow('dst', dst)
cv2.waitKey()

 

 

  • 크기 변환(resize)
    • 영상의 크기를 원본 영상보다 크게 또는 작게 만드는 변환
    • cv2.resize(영상, 크기, 보간법)
      • 보간법 알고리즘
      • cv2.INTER_LINEAR: 인접한 4개의 픽셀 값에 거리 가중치를 사용(속도는 빠르지만 퀄리티가 조금 떨어짐)
      • cv2.INTER_NEAREST: 가장 가까운 픽셀 값을 사용(속도는 빠르지만 퀄리티가 조금 떨어짐)
      • cv2.INTER_CUBIC: 인접한 16개의 픽셀 값에 가중치를 사용(퀄리티는 가장 높지만 속도가 떨어짐)
      • cv2.INTER_AREA: 픽셀 영역 관계를 이용한 재샘플링(영역적인 정보를 추출해서 영상을 다시 세팅하기 때문에 다운 샘플링시 효과적)
import cv2

img = cv2.imread('./dog.bmp')
dst1 = cv2.resize(img, (1280, 1024), interpolation=cv2.INTER_NEAREST) 
dst2 = cv2.resize(img, (1280, 1024), interpolation=cv2.INTER_NEAREST)

cv2.imshow('img', img)
cv2.imshow('dst1', dst1[200:500, 200:500])  #이미지의 (200, 200)부터 (500, 500)까지의 영역을 보여줌
cv2.imshow('dst2', dst2[200:500, 200:500])
cv2.waitKey()

dst2가 더 선명함

 

  • 회전(rotation)
    • 영상을 특정 각도만큼 회전시키는 변환(반시계 방향, 음수는 시계 방향)
    • cv2.getRotatioinMatrix2D(중심좌표, 회전각도, 확대비율) -> affine 행렬
    • cv2.warpAffine(영상, affine행렬, (0, 0))
import cv2

img = cv2.imread('./dog.bmp')
# 중심점 계산, cp: 회전의 중심점
cp = (img.shape[1] / 2, img.shape[0] / 2)  # 가로/2, 세로/2
# 회전 변환 행렬 생성, 30: 회전 각도(시계 반대 방향), 0.7: 스케일링 인자 -> 30도 회전, 크기는 70%로 축소
rot = cv2.getRotationMatrix2D(cp, 30, 0.7)
# 이미지 변환 행렬 rot을 사용하여 이미지 회전하고 스케일링한 결과를 dst에 저장
dst = cv2.warpAffine(img, rot, (0, 0)

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

 

 

  • 투시 변환(perspective)
    • 직사각형 형태의 영상을 임의의 입체감 있는 사각형 형태로 변환
    • 원본 영상에 있는 직선은 결과 영상에서 그대로 유지 되지 않고 평행 관계가 깨질 수 있음
    • 투시 변환은 보통 3*3 크기의 실수 행렬로 표현
      • cv2.getPerspectiveTransform(영상, 4개의 결과 좌표점) -> 투시변환 행렬
      • cv2.warpPerspective(영상, 투시변환행렬, 결과영상크기)   
import cv2
import numpy as np

img = cv2.imread('./pic.jpg')
w, h = 600, 400   # 출력 이미지의 폭과 높이

# 원본 이미지에서 변환할 사각형의 4개의 좌표
srcQuad = np.array([[369, 172], [1228, 156], [1424, 846],  [207, 801]], np.float32)
# 출력 이미지에서 목표로 하는 4개의 좌표
dstQuad = np.array([[0, 0], [w, 0], [w, h], [0, h]], np.float32)

# 원근 변환 행렬 계산: 원본 이미지의 사각형을 새로운 사각형으로 변환하기 위함
pers = cv2.getPerspectiveTransform(srcQuad, dstQuad)
# 원근 변환 적용
dst = cv2.warpPerspective(img, pers, (w, h))

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

 

 

 


 

 

💡문제

  • 직사각형 이미지를 정렬하고, 왜곡된 이미지를 바로 잡아라.
  • 이미지를 읽어와서 초기 사각형 영역을 설정한다.
  • 마우스를 사용해 4개의 코너를 드래그하여 원하는 위치로 조정한다.
  • 엔터키를 누르면 설정된 코너에 맞춰 원근 변환이 적용되고, 결과 이미지를 보여준다.
  • ESC키를 누르면 프로그램이 종료된다.
import cv2
import numpy as np
import sys

# 이미지의 코너 지점을 시각적으로 표시하는 함수
def drawROI(img, corners):
	cpy = img.copy()
    c1 = (192, 192, 255)  # 코너 점의 색상
    c2 = (128, 128, 255)  # 코너를 연결하는 선의 색상
    
    for pt in corners:  # 코너에 원 그리기
    	cv2.circle(cpy, tuple(pt.astype(int)), 25, c1, -1)
    
    # 코너를 연결하는 선 그리기
    cv2.line(cpy, tuple(corners[0].astype(int)), tuple(corners[1].astype(int)), c2, 2)
    cv2.line(cpy, tuple(corners[1].astype(int)), tuple(corners[2].astype(int)), c2, 2)
    cv2.line(cpy, tuple(corners[2].astype(int)), tuple(corners[3].astype(int)), c2, 2)
    cv2.line(cpy, tuple(corners[3].astype(int)), tuple(corners[0].astype(int)), c2, 2)

	return cpy
    
# 마우스 이벤트 처리 함수
ptOld = None

def onMouse(event, x, y, flags, parma):
	global arcQuad, dragSrc, ptOld, img
    
    if event == cv2.EVENT_LBUTTONDOWN:   # 왼쪽 버튼을 눌렀을 때
    	for i in range(4):
        	if cv2.norm(srcQuad[i] - (x, y)) < 25:
            	dragSrc[i] = True
                ptOld = (x, y)
                break
                
    if event == cv2.EVENT_LBUTTONUP:   # 왼쪽 버튼을 뗐을 때
    	for i in range(4):
        	dragSrc[i] = False
            
    if event == cv2.EVENT_MOUSEMOVE:   # 마우스를 움직였을 때
    	for i in range(4):
        	if dragSrc[i]:
            	srcQuad[i] = (x, y)
                cpy = drawROI(img, srcQuad)
                cv2.imshow('img', cpy)
                ptOld = (x, y)
                break
                
img = cv2.imread('./namecard.jpg')
h, w = img.shape[:2]

dh = 500   # 출력 이미지의 높이
dw = round(dh * 297 / 210)   # 출력 이미지의 폭(A4비율)

srcQuad = np.array([[30, 30], [30, h-30], [w-30, h-30], [w-30, 30], np.float32)
dstQuad = np.array([[0, 0], [0, dh], [dw, dh], [dw, 0]], np.float32)

dragSrc = [False, False, False, False, False]

# ROI 표시 및 마우스 콜백 설정
disp = drawROI(img, srcQuad)
cv2.imshow('img', disp)
cv2.setMouseCallback('img', onMouse)

# 원근 변환 및 결과 출력
while True:
	key = cv2.waitKey()
    if key == 13:   # 엔터키 눌러서 코너 설정 완료하면, 원근 변환 행렬 구하고 이미지 변환
    	break
    elif key == 27:   # ESC키
    	sys.exit()
        
pers = cv2.getPerspectiveTransform(srcQuaad, dstQuad)
dst = cv2.warpPerspective(img, pers, (dw, dh), flags=cv2.INTER_CUBIC)
cv2.imshow('dst', dst)
cv2.waitKey()

728x90