본문 바로가기
Programming/Image Processing

[Image Processing][Python]이미지 그레이스케일 변환

by NoiB 2021. 12. 6.
반응형

문득 최대한 opencv를 안 쓰고 이미지 프로세싱을 진행해보면 어떨까 하는 생각이 들었습니다. 기초부터 이미지 프로세싱을 진행한다는 느낌도 들고 최소한 내가 사용하는 기술이 어떤 식으로 이뤄져 있는지는 알고 쓰는 게 나중에도 도움이 될 것이라 생각이 들어서 간단한 것부터 진행해보려 합니다.

 

이 글은 현재 opencv의 inshow, imread, imwrite 등의 이미지를 불러오고 보여주는 기술은 아직 구현하지 못했기에 전혀 opencv의 기능을 사용하지 않는 것이 아니라는 점을 미리 고지합니다.

흑백 이미지로 변환할 이미지를 아무거나 골라주세요. 저는 이 사진으로 해보겠습니다. 먼저 이미지에 대한 간단한 내용을 짚고 넘어가야 하는데요. 우리가 눈으로 볼 때는 이미지에 들어있는 무언가의 형태나 색을 보게 되는데요. 컴퓨터는 이것을 픽셀 단위로 인식합니다. 예를 들어 이 그림 같은 경우는

import cv2

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

뭐 이런 방식으로 출력해보면 출력값이 [[[101 54 72], [101 54 71],... [60 49 23]]] 이런 식으로 BGR순서(opencv는 RGB가 아닌 BGR의 순서로 이미지를 불러옵니다)의 픽셀 값이 나옵니다. 이렇게 컴퓨터의 경우 이미지를 해당 픽셀의 색상 채널 값으로 인지하는데요. 이런 컬러 이미지의 경우 3 색상으로 이미지를 구현하므로 3 채널 이미지라고 부릅니다. 흑백 이미지는 어떨까요? 예상하신 분들도 있으실지 모르겠습니다만 흑백 이미지는 1 채널입니다. B, G, R 중에서 하나의 값만 가지는 경우 흑백 이미지로 나와요. 위와 같은 코드로 흑백 이미지를 출력시켜보면 [[0 34 26 12 56... ]] 이런 식의 출력 값이 나옵니다(인터넷에서 찾은 이미지는 이렇지 않을 수도 있습니다). 

 

그러면 도대체 흑백 이미지는 어떻게 만들까요? 예를 들면 b_img, g_img, r_img = cv2.split(img)라고 하면 BGR 순서대로 채널별로 분리해줍니다. 하지만 저 함수를 이용해서 직접 흑백 이미지로 변환을 해보시면 흑백 이미지이긴 하나 서로가 약간씩 차이가 있는 것을 보실수 있는데요. 이는 해당 채널에서의 값이 다르기 때문입니다. 예를 들면 위 사진에서 하늘의 경우 파란색 값이 다른 값들보다 높겠죠? 이런 경우 b_img는 하늘 부분이 다른 사진들에 비해 밝게 표시될 겁니다. 이처럼 해당 채널의 값이 크고 작은 정도에 따라 밝기에 차이가 생기게 되는 것이죠.

 

그렇다면 opencv의  cv2.CVT_COLOR_BGR2GRAY는 어떤 로직을 가지고 동작하는 것일까요? 각 채널별로 다 이미지가 다른데 3개를 다 출력해주진 않고 하나만 보여주는데 말이죠. 저 같은 경우는https://docs.opencv.org/3.4/de/d25/imgproc_color_conversions.html

 

OpenCV: Color conversions

See cv::cvtColor and cv::ColorConversionCodes Todo:document other conversion modes RGB \(\leftrightarrow\) GRAY Transformations within RGB space like adding/removing the alpha channel, reversing the channel order, conversion to/from 16-bit RGB color (R5:G6

docs.opencv.org

이곳에서 정보를 찾았습니다. 해당 알고리즘은 RGB [A] to Gray:Y←0.299⋅R+0.587⋅G+0.114⋅B 이런 식에 따라 구현이 된다고 적혀있는데요. 그렇다면 바로 따라 해 봐야겠죠.

 

코드 : 

import cv2

def cvt_to_gray(img):
    height,width,channel = img.shape
    if channel == 3:
        for i in range(height):
            for j in range(width):
                img[i][j] = img[i][j][0]*0.114 + img[i][j][1]*0.587 + img[i][j][2]*0.299
        return img
    else:
        return img
    
img_name = input('Input Image Name : ')
img = cv2.imread(f'./img_sample/{img_name}.jpg')
gray_img = cvt_to_gray(img)

cv2.imwrite(f'./processed_img/{img_name}_gray.jpg', gray_img)
cv2.imshow('gray_img', gray_img)
cv2.waitKey(0)

#print(img)
#img[i][j][0]*0.114 + img[i][j][1]*0.587 + img[i][j][2]*0.299 = intensity of 1 channel

빨리 결과를 확인하기 위해서 이중 반복문을 사용했습니다만 해당 사진이 거의 fhd 규격의 사진이다 보니 시간이 엄청 걸리더라고요. 아마 훨씬 빠른 시간 복잡도를 가진 방법이 있을 것이라 생각됩니다(파이썬의 리스트 계산을 사용하지 않고 numpy 라이브러리에서 행렬 연산을 사용하면 훨씬 빨리 가능합니다). 변환은 상당히 잘된 듯 보이네요. 다만 앞으로도 대부분의 이미지 프로세싱은 1 채널 이미지로 이뤄질 텐데 좀 더 해상도가 낮은 이미지로 사용을 해야 할 것 같습니다.

 

막상 다 쓰고 보니 전부 사족이고 한 게 별로 없는 것 같지만 항상 처음에 정보를 미리 알아놓고 가는 것이 좋다고 생각하기에 이번에도 좀 무리해봤습니다. 다음에는 정말 필요한 정보만 적도록 노력해봐야겠어요. 오늘도 긴 글 읽어주셔서 감사합니다.

 

포스팅을 하고 나서 좀 찾아보니 opencv는 내부적으로 numpy를 이용해서 대부분의 연산을 처리하는 것 같더군요. 그러면서 이미지의 자료형이 ndarray로 나오는데 list에 비해 적은 메모리나 빠른 연산 속도를 갖는 장점이 있어서 ndarray를 사용한다고 합니다. 아마 numpy를 이용해서 연산을 진행하면 좀 더 빠르게 이미지 처리가 될 것 같네요. 다음에 시간내서 손을 좀 봐야겠습니다.

반응형