Deep Studying

[이미지 프로세싱] python과 openCV로 이미지 픽셀 수정하기 1 - 리사이징 본문

컴퓨터비전

[이미지 프로세싱] python과 openCV로 이미지 픽셀 수정하기 1 - 리사이징

miniSeop 2019. 6. 26. 05:17

저번 포스트에서는 양자화와 리사이징에 대해서 설명했습니다.

이전 포스트 확인하기 -> https://minisp.tistory.com/3

 

[이미지 프로세싱] 양자화와 리사이징

저번 포스트에서 디지털 영상이 무엇인지, 그리고 샘플링에 대해서 간단하게 알아봤습니다. 이전 포스트 확인하기 -> https://minisp.tistory.com/2 [이미지 프로세싱] 디지털 신호란? 디지털 영상과 샘플링 아날..

minisp.tistory.com

 

이전 포스트에서 언급한 내용을 참고하여 python과 openCV로 이미지를 읽고 출력하고 간단히 수정하는 방법을 배워보도록 할건데요, python과 openCV, 그리고 numpy는 설치가 되어있다고 가정하고 시작하겠습니다.

1. 이미지 불러오기

1-1. 이미지 파일 불러오기( imread )

일반적으로 파이썬에서 이미지를 불러올 때 Pillow를 사용하는 것 같습니다. 하지만 앞으로의 강의에서 openCV를 사용할 것이기 때문에 이미지를 불러오는 것 부터 openCV를 사용하도록 하겠습니다.

imgArray = cv2.imread( filename , flags)

filename: 불러올 이미지 파일( BMP, JPEG, PNG 등)의 경로 입니다.

flags: 읽기 옵션에 해당합니다. 아래와 같은 값들이 올 수 있습니다.

- cv2.IMREAD_COLOR: 컬러로 읽기 (default)

- cv2.IMREAD_GRAYSCALE: 흑백으로 읽기

- cv2.IMREAD_UNCHANGED: Alpha채널 포함

 

#cv2는 python에서 사용할 수 있는 opencv 모듈입니다.
import cv2

#불러올 이미지 파일의 경로
imageFile1 = './image1.jpg'
imageFile2 = './image2.jpg'

#이미지를 불러옵니다.
imgArray1 = cv2.imread(imageFile1)
imgArray2 = cv2.imread(imageFile2, 0) #cv2.imread(imageFile2, cv2.IMREAD_GRAYSCALE)

 

imread 함수를 사용하면 파라미터로 넘겨준 경로의 이미지를 불러옵니다. 또한 불러온 이미지를 numpy.ndarray의 배열로 읽어 반환하며 읽기에 실패하면 None을 반환합니다.

1-2. 이미지 저장 형식 ( ndArray )

 

먼저 IMREAD_GRAYSCALE 옵션으로 읽은 흑백 이미지부터 살펴보겠습니다.

 

20px X 10px

 

위 이미지는 20x10사이즈의 흑백 이미지입니다. 아래 코드로 해당 이미지를 불러온 뒤 그대로 출력해보겠습니다.

※ 원본을 열어보면 낮은 해상도의 사진이라 확대하면 굉장히 각져있는 모습을 볼 수 있는데요, 업로드하여 확대하면 어느정도 보정이 들어가는 것 같습니다.

 

import cv2
import numpy as np

imageFile = './image.png'
img = cv2.imread(imageFile, 0)

print(img)

 

cv2.imread(imageFile, 0)은 cv2.imread(imageFile, cv2.IMREAD_GRAYSCALE)과 동일합니다.

 

위에 나온 숫자들은 모두 하나하나의 픽셀에 해당되는 밝기입니다.

불러온 이미지는 M=10, N=20, L=256인 이미지인 셈이죠.

픽셀의 개별 요소에 접근할 때는 2차원 배열을 접근하듯이 Array[y좌표][x좌표]로 사용할 수도 있고 Array[y좌표, x좌표]의 형식으로 접근할 수도 있습니다.

하지만 여러 픽셀을 접근할 때는 Array[y좌표1:y좌표2, x좌표1:x좌표2]의 형태로만 사용하시기 바랍니다.

 

우선은 이렇게 이미지 픽셀을 접근할 수 있다 정도만 알아두시면 될 것 같습니다. 자세한 설명은 뒤에 덧붙이겠습니다.

2. 이미지 출력하기 ( imwrite, imshow )

이미지를 불러오고, 수정했다면 어떤 방법으로든 확인할 수 있어야겠죠? 이 때 사용할 수 있는 두 가지 방법이 있습니다.

imwrite 함수는 ndArray를 이미지 파일로 저장해주고, imshow는 ndArray를 이미지로 볼 수 있게 윈도우창에 띄워줍니다. 자세한 사용 방법을 보도록 하겠습니다.

2-1. 파일로 저장하기 ( imwrite )

cv2.imwrite(filename, img, [ params ])

filename: 저장될 경로입니다. 확장자를 포함해야하며 파일 확장자에 따라 이미지 포맷이 지정됩니다.

img: 이미지파일로 저장할 ndArray입니다.

params: 압축률, 이미지품질 등을 지정합니다. 이에 대한 설명은 아직은 불필요하다 생각하여 생략하도록 하겠습니다.

 

import cv2
import numpy as np

imageFile='./dog.jpg'
src = cv2.imread(imageFile, 0)
cv2.imwrite('./gray_dog.jpg')

 

위 코드를 실행하면 dog.jpg 이미지를 흑백으로 열어 gray_dog.jpg 이미지로 저장할 수 있습니다.

2-2. 윈도우에 출력하기 ( imshow )

cv2.imshow(winname, mat)

winname: 윈도우의 이름입니다.

mat: 이미지로 출력할 배열입니다.

imshow를 이용하여 이미지를 화면에 출력해보겠습니다.

코드에 대한 설명은 아래에 하도록 하겠습니다.

 

import cv2
import numpy as np

imageFile='./dog.jpg'
src = cv2.imread(imageFile, 0) ... (1)

cv2.imshow('sample',src) ...(2)

cv2.waitKey(0) ...(3)
cv2.destroyAllWindows() ...(4)

 

 

(1) imread - 이미지를 불러옵니다. flags에 0을 넣어 흑백으로 불러왔습니다.

(2) imshow - 이미지를 출력합니다. 'sample' 이란 창에 불러온 src를 이미지로 출력합니다.

(3) waitKey - 이미지를 출력하는 윈도우에서 키보드 입력이 될 때 까지 대기합니다.

※waitKey함수는 다양한 쓰임으로 사용할 수 있습니다. 아래에 덧붙이도록 하겠습니다.

(4) destroyAllWindows - 모든 윈도우를 파괴합니다.

 

코드를 실행하면 위와 같이 윈도우를 생성하고 이미지를 출력합니다.

2-3. waitKey 함수

waitKey 함수는 엄밀히 따지자면 이미지를 불러오는 것 하고는 관련이 없는데요. 그래도 여기서 설명을 하고 넘어가야 할 것 같아서 설명을 하도록 하겠습니다.

input = cv2.waitKey( delay )

키보드를 입력할 수 있는 delay 만큼의 시간을 줍니다. (단위는 ms, milli second)

- 주어진 시간 안에 키를 누르면 해당 키에 해당하는 코드를 반환합니다.

- 만약 지정된 시간에 키를 누르지 않으면 -1을 반환합니다.

그런데 delay의 default값은 0입니다. delay가 0이면 입력할 시간을 주지 않는걸까요?

사실 delay의 값이 0으로 주어지면 이 함수는 키 입력이 될 때 까지 무한히 대기합니다. 따라서 imshow 함수 설명을 위해 적었던 코드에서 cv2.waitKey(0)는 "키보드를 입력할 때 까지 계속 기다려라." 라는 의미가 되는 셈이죠.

 

3. 픽셀 수정하기

3-1. 이미지 크기 확인하기 ( shape )

이미지의 크기가 항상 같지는 않죠. 그렇기 때문에 불러온 이미지의 크기를 아는게 가장 기본입니다. 이 때 활용할 수 있는 것이 numpy.ndarray의 shape 속성입니다. shape는 배열의 크기를 출력해 주는데요. 사용법은 아래와 같습니다.

 

 

import cv2
import numpy as np

imageFile='./dog.jpg'
src_color = cv2.imread(imageFile)
src_gray = cv2.imread(imageFile, 0)
height, width = src_gray.shape ... (1)

print(src_color.shape)
print(src_gray.shape)
print(height, width)

 

>>> (400, 400, 3)
>>> (400, 400)
>>> 400, 400

 

 

결론만 말씀드리자면 shape값은 아래와 같은 형태의 튜플입니다.

  - 컬러 영상의 경우: ( y길이, x길이, 채널 수 )

  - 흑백 영상의 경우: ( y길이, x길이 )

또한 위 코드 (1)에서 처럼 튜플의 각 요소를 한 번에 받아올 수도 있습니다.

> 컬러 영상에 대한 설명은 아직 이 문서에 추가해야할지, 다른 문서에 따로 설명해야할지 모르겠어서 하지 않았습니다. 처음엔 흑백 영상에 대해서만 하고 필요하게 된다면 어떠한 방식으로든 추가하도록 하겠습니다.

3-2. 픽셀 수정 ( 단일 픽셀 )

위에서 잠깐 언급한 내용입니다. ( y, x )픽셀에 접근할 때는 Array[y좌표, x좌표] 형식을 사용하면 접근할 수 있었습니다.

400x400크기의 강아지 사진 중 아래 절반을 검은색 화소로 바꾸려면 어떻게 할까요?

 

import cv2
import numpy as np

imageFile='./dog.jpg'
img = cv2.imread(imageFile, 0)
height, width = img.shape

for j in range( height//2, height):
    for i in range(width):
        img[j,i] = 0

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

 

간단한 for문을 이용하여 j값이 height/2 ~ height 사이의 모든 픽셀 값을 0으로 바꿨습니다.

 

아래 절반의 픽셀이 0 (검은색)으로 바뀐 모습을 확인할 수 있었습니다.

3-3. 픽셀 수정 ( 영역별 수정 )

> 바꾸려는 영역의 크기가 유동적이라면?

> 특정한 패턴으로 for문이 중첩된다면? ( 특정 범위의 평균 밝기를 구하는 경우 등 )

모두 for문이나 기타 반복문을 사용해 픽셀을 하나하나 접근해 처리할 수 있습니다. 하지만 경우에 따라서는 코드가 복잡해 보일 수 있기 때문에 픽셀 하나하나의 개별적인 접근법 외에도 픽셀의 영역별로 접근할 수 있는 방법이 있습니다.

Array[y1:y2, x1:x2] 형식으로 사용하며 y좌표가 y1~y2이고 x좌표가 x1~x2인 모든 요소를 접근한다는 의미입니다.

Array[y1:y2, x1:x2] = value 처럼 사용하면 해당 범위의 화소값이 전부 value로 바뀝니다. 그렇다면 위의 예제를 영역 접근으로 바꿔보겠습니다.

 

import cv2
import numpy as np

imageFile='./dog.jpg'
img = cv2.imread(imageFile, 0)
height, width = img.shape

img[height//2:height, 0:width] = 0

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

 

 처음 작성한 코드와 같은 결과가 나오는 것을 확인할 수 있습니다. 이러한 영역별 접근은 위에서 말한 '특정 범위의 평균 밝기를 구하는 경우'에 유용하게 사용할 수 있습니다. 저번 포스트에서 언급했던 리사이징을 기억하시나요?

 

400x400 크기의 강아지 사진을 위처럼 16x16 해상도의 이미지로 바꾸려면

 

1. 강아지 이미지를 읽어오고 400 x 400 배열에 저장합니다.
2-1. 16 x 16 행렬을 만듭니다. 이 때 결과의 한 화소에 대응되는 원본은 25x25 사이즈의 화소들이 됩니다.

2-2. 가로 세로가 각각 25 화소인 영역의 밝기 평균을 구한다.

3-1. 구한 밝기를 소수점 버림하고 newArray의 해당 칸에 넣는다.

3-2. 2부터 3까지 반복하여 newArray의 모든 칸을 채운다.

위와 같은 과정을 거쳐 만들 수 있습니다.

 

import cv2
import numpy as np

imageFile='./dog.jpg'
img = cv2.imread(imageFile, 0)
img_height, img_width = img.shape

newImage = np.zeros( (16,16), dtype = img.dtype )  ... (1)


new_height = img_height//16 
new_width = img_width//16  ... (2)

for j in range(16):
    for i in range(16):
        y = j*new_height
        x = i*new_width  ... (3)
        pixel = img[y:y+new_height, x:x+new_width]  ... (4)
        newImage[j,i] = pixel.sum(dtype='int64')//(new_height*new_width) ... (5)
        #newImage[j,i] = cv2.mean(pixel)[0]  ... (6)

cv2.imshow('sample',newImage)
cv2.waitKey()
cv2.destroyAllWindows()

 

(1) - np.zeros 함수로 (16,16) 사이즈의 배열을 만듭니다. 이 때 배열안의 데이터 타입은 img와 동일하게 설정했습니다.

(2) - new_height과 new_width는 위에서 계산한 400/16=25 로 선택 영역의 세로,가로 크기입니다.

(3) - y와 x는 선택 영역의 시작 좌표입니다. 자세한 설명은 (4)에서 이어가겠습니다.

(4) - 픽셀 (y,x) 부터 가로 사이즈 new_width, 세로 사이즈 new_height 크기의 사각형 영역을 위와 같이 img[y:y+new_height, x:x+new_width] 로 지정하였습니다. 따라서 변수 pixel은 해당 범위를 슬라이스한 ndarray가 됩니다.

(5) - numpy의 내장 함수 sum을 사용하여 해당 배열의 원소 합을 구합니다. 영역의 밝기 합을 구한 뒤, 영역의 크기인 new_height*new_width로 나누어 평균값을 구했고, new_image 배열에 저장했습니다.

(6) - numpy의 sum을 사용한 것이 아닌 openCV의 내장함수 mean을 사용한 예입니다. [0]이 붙는 이유는 위 함수는 채널별로 합을 구해서 리턴해주기 때문입니다. 자세한 설명은 컬러이미지 내용을 추가할 때 덧붙이겠습니다. 다만 흑백영상에서는 이런식으로 사용할 수 있다 정도로 넘어가면 될 것 같습니다.

※ 정확히 1/2, 1/3 이렇게 딱 떨어지는 리사이징은 영역별 평균값을 구해 처리해도 비슷해 보일 수 있지만 그렇지 않은 경우도 있습니다. 이럴 때는 최대한 원본과 비슷한 이미지를 구하기 위해 다른 복잡한 방법을 사용하기도 합니다.