Data_Analysis_Track_33/Python

Python_opencv_02(기본 영상 처리)

lsc99 2023. 10. 6. 17:32

히스토그램

 - 영상을 구성하는 픽셀들의 빈도수를 측정하여 그래프(히스토그램)으로 표현

  • cv2.calcHist(images, channels, mask, histSize, ranges, hist=None, accumulate=None)
  • images: 히스토그램을 구할 입력영상 리스트
  • channels: 히스토그램을 구할 채널 리스트
  • mask: 마스크 영상. 영상전체일 경우 None지정
  • histSize: 히스토그램 각 채널의 bin의 개수 리스트. 여기서는 각각의 픽셀값을 셀 것이므로 255개로 지정
  • ranges: 히스토그램을 이용해 빈도수를 셀 최솟값과 최대값을 리스트로 설정. [0, 256] 0 ~ 255 (마지막은 포함 안함)

 

Grayscale 영상의 히스토그램 구하기

import cv2
import matplotlib.pyplot as plt

img = cv2.imread('images/window.jpg', cv2.IMREAD_GRAYSCALE)
plt.imshow(img, cmap='gray')

 

hist = cv2.calcHist([img]      # pixcel값의 분포를 계산할 이미지 (ndarray)
                    ,[0]       # pixcel값의 분포를 계산할 채널
                    ,None      # pixcel값의 분포를 계산할 영역. 전체영역일 경우 None으로 지정
                    ,[256]     # bins : 몇 계급으로 나눌지 결정
                    ,[0, 256]  # 빈도수를 셀 pixcel값의 범위
                   )

 

type(hist), hist.shape # index : pixcel값, value : 개수

 

히스토그램 구하기

import numpy as np

plt.bar(np.arange(hist.size), hist.flatten())
plt.xlabel('픽셀값')
plt.ylabel('개수')
plt.show()

 

컬러영상의 히스토그램 구하기 -> channel 별로 그린다.

 

이미지 구해오기

lenna = cv2.imread('images/Lenna.png')
print(lenna.shape)

blue = lenna[:, :, 0]
green = lenna[:, :, 1]
red = lenna [:, :, 2]

print(blue.shape, green.shape, red.shape)

 

split을 이용하여 채널별로 나눌 수 있다.

bgr = cv2.split(lenna) # 채널별로 나눠진 배열들을 튜플로 묶어서 반환
print(type(bgr), len(bgr))

 

 

color_labels = ['blue', 'green', 'red']

for channel, color in zip(bgr, color_labels):
    hist = cv2.calcHist([channel], [0], None, [256], [0, 256])
    plt.plot(np.arange(hist.size), hist.flatten(), color = color)

plt.xlabel('픽셀값')
plt.ylabel('개수')
plt.show()

밝기 조절하기(brightness)

  • 영상을 전체적으로 밝게 또는 어둡게 만드는 연산
  • 밝기 조절 수식
        - 원본좌표(x,y) + n
        - n이 양수면 각 좌표의 픽셀값이 커지므로 밝아진다.
        - n이 음수이면 각 좌표의 픽셀값이 작아지므로 어두워 진다.
  • cv2.add(src1, src2)
        - 밝기 조절이나 두 영상을 합치기에 사용할 수 있다.
        - src1: 첫번째 영상 또는 스칼라
        - src2: 두번째 영상 또는 스칼라
        - src2를 src1에 더한다.
  • pixel에 스칼라 값을 더하거나 빼면 0 ~ 255의 범위를 넘어설 수 있다. ndarray의 type은 uint8(unsigned int8) 로 0 ~ 255범위를 넘어가는 숫자는 순환되어 버린다.
        - 계산결과가 0이하가 되면 255로 부터 작아지게 되고 255가 넘어가면 0으로 가서 커지게 된다.
        - 그래서 cv2.add() 함수는 값이 순환되지 않도록 0이하의 값은 0으로 255이상의 값은 255가 되도록 처리한다. 이것을 saturate연산이라고 한다.

 

import cv2
import numpy as np

img = cv2.imread('images/penguin.jpg', cv2.IMREAD_GRAYSCALE)
result1 = img - 100 # 어둡게
result2 = img + 100 # 밝게
cv2.imshow('src', img)
cv2.imshow('result1', result1)
cv2.imshow('result2', result2)

cv2.waitKey()
cv2.destroyAllWindows()

 

원본과 더 밝게(+100) 만든 result2의 값 변화 히스토그램으로 관찰

hist1 = cv2.calcHist([img], [0], None, [256], [0, 256])
hist2 = cv2.calcHist([result2], [0], None, [256], [0, 256])

plt.plot(hist1.flatten(), label = '원본')
plt.plot(hist2.flatten(), label = '+100')

plt.legend()
plt.show()

0 ~ 255의 범위를 넘어가버리는 연산이 되는 값들은 더 밝아져야 할 값(255에 가깝게 되야할)이 어두워져버리는(0에 가깝게) 현상이 발생한다. -> saturation 연산의 필요성

 

saturaion 연산

# saturation 연산 처리
img = cv2.imread('images/penguin.jpg', cv2.IMREAD_GRAYSCALE)
result1 = cv2.add(img, - 100) # 어둡게
result2 = cv2.add(img, + 100) # 밝게
cv2.imshow('src', img)
cv2.imshow('result1', result1)
cv2.imshow('result2', result2)

cv2.waitKey()
cv2.destroyAllWindows()

 

원본과 더 밝게(+100) 만든 result2의 값, 더 어둡게(-100)만든 result1의 값 변화 히스토그램으로 관찰

hist1 = cv2.calcHist([img], [0], None, [256], [0, 256])
hist2 = cv2.calcHist([result2], [0], None, [256], [0, 256])
hist3 = cv2.calcHist([result1], [0], None, [256], [0, 256])

plt.plot(hist1.flatten(), label = '원본')
plt.plot(hist2.flatten(), label = '+100')
plt.plot(hist3.flatten(), label = '-100')

plt.legend()
plt.show()

원하는 결과가 출력되는 것을 확인할 수 있다.

 

numpy함수를 이용

np.clip(연산, min, max)

연산결과가 min보다 작은 값들은 min으로, max보다 큰 값들은 max로 변경.

# bool < uint < int < float < str
# img - 100.0 # uint8 -> float64 ==> float64 - float64

# saturation 연산 처리 - numpy 함수를 이용
img = cv2.imread('images/penguin.jpg', cv2.IMREAD_GRAYSCALE)
# 1. 계산시 float으로 변환후 계산
# 2. np.clip() 사용. 0 ~ 255 범위로 조정
# 3. float -> uint8 타입으로 변환.
result1 = np.clip(img-100.0, 0, 255).astype('uint8')
result2 = np.clip(img+100.0, 0, 255).astype('uint8')

cv2.imshow('src', img)
cv2.imshow('result1', result1)
cv2.imshow('result2', result2)

cv2.waitKey()
cv2.destroyAllWindows()

 

이미지 두개 합치기

# 두 이미지 합치기
lenna_gray = cv2.imread('images/Lenna.png', cv2.IMREAD_GRAYSCALE)
img2 = cv2.resize(img, (512, 512))

result = cv2.add(lenna_gray, img2) # element-wise 연산    # image + image = 두 이미지를 겹친다.(합친다.)
cv2.imshow('result', result)
cv2.waitKey()
cv2.destroyAllWindows()

 

컬러이미지 밝기 조절하기 (HSV 모드)

# 컬러이미지 밝기 조절 -> HSV 색공간으로 변환. H: 색, S: 채도, V: 명암(밝기)
lenna = cv2.imread('images/Lenna.png')
lenna_hsv = cv2.cvtColor(lenna, cv2.COLOR_BGR2HSV)

lenna_hsv[:, :, 2] = cv2.add(lenna_hsv[:, :, 2], -100)

cv2.imshow('src', lenna)
cv2.imshow('result', cv2.cvtColor(lenna_hsv, cv2.COLOR_HSV2BGR))
cv2.waitKey()
cv2.destroyAllWindows()

 

컬러이미지 밝기 조절하기 (BGR 모드)

# BGR 모드에서 덧셈을 처리
# channel이 있는 경우 -> channel 별로 계산. (0, 1, 2, 3)
#                        상수를 지정한 경우: 0번 channel이랑만 계산한다.
result = cv2.add(lenna, (100, 100, 100, 0)) 

cv2.imshow('src', lenna)
cv2.imshow('result', result)
cv2.waitKey()
cv2.destroyAllWindows()

명암비(Contrast) 조정

명암비/대비(Contrast)란

  • 밝은 부분과 어두분 부분 사이의 밝기의 정도 차이
        - 영상이 전체적으로 밝은 픽셀들로만 구성되거나 어두운 픽셀들로만 구성되면 명암비가 낮다고 표현한다.
        - 영상에 밝은 영역과 어두운 영역이 골고루 섞여있으면 명암비가 높다고 표현한다.
  • 명암비가 낮으면 객체간의 구분이 되지 않아 흐릿한 느낌이 나고 명암비가 높으면 선명한 느낌이 든다.

 

  • 명암비를 변환하는 함수공식
        - g(x, y)=f(x,y)+(f(x,y)-128)alpha == (1+alpha)f(x, y)-alpha 128
  • g는 결과 image
  • f는 원본 image
  • x: x좌표
  • y: y좌표
  • alpha: 대비를 조절하는 값.
        - 0: 원본과 동일
        - 음수: 명암비를 낮춘다.
        - 양수: 명암비를 높인다.

 

명암비 변환 함수공식 함수 정의

def control_contrast(img, alpha = 0.0):
    return np.clip((1.0 + alpha) * img - alpha * 128, 0 , 255).astype('uint8')

 

원본과 명암비를 높인것과 낮춘것의 그래프 그리기

a1 = np.arange(256)
a2 = control_contrast(a1, alpha = -0.5)
a3 = control_contrast(a1, alpha = 1.0)

plt.plot(a1, linestyle = ':', label = '0')
plt.plot(a2, label = '-0.5')
plt.plot(a3, label = '1')

plt.legend(title = 'alpha')
plt.show()

 

gray scale 명암비 조정

# gray scale
lenna_gray = cv2.imread('images/Lenna.png', cv2.IMREAD_GRAYSCALE)
result1 = control_contrast(lenna_gray, -0.5)
result2 = control_contrast(lenna_gray, 1.0)

 

원본과 명암비를 높인것과 낮춘것 차이 확인

plt.figure(figsize = (15, 5))

plt.subplot(1, 3, 1)
plt.imshow(lenna_gray, cmap = 'gray')
plt.title('원본')

plt.subplot(1, 3, 2)
plt.imshow(result1, cmap = 'gray')
plt.title('-0.5')

plt.subplot(1, 3, 3)
plt.imshow(result2, cmap = 'gray')
plt.title('1.0')

plt.tight_layout()
plt.show()

채도 (Saturation) 변환

  • 채도: 색의 선명도를 말한다.
  • 채도를 변환할 때는 HSV color 타입으로 변환한 뒤 S의 값을 바꾼다.
        HSV : 색상(Hue), 채도(Saturation), 명도(Value)

 

H -> 0, S -> 1, V -> 2

채도 S는 1에 있기 때문에 1을 변경

lenna = cv2.imread('images/Lenna.png')
lenna_hsv = cv2.cvtColor(lenna, cv2.COLOR_BGR2HSV)

value = 50

lenna_hsv[:, :, 1] = cv2.add(lenna_hsv[:, :, 1], value)

 

원본과 채도를 변경한 것의 차이 확인

plt.figure(figsize = (10, 5))

plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(lenna, cv2.COLOR_BGR2RGB))
plt.title('원본')

plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(lenna_hsv, cv2.COLOR_HSV2RGB))
plt.title(f"{value}")

plt.tight_layout()
plt.show()

Filter

Filtering 개요

  • 영상에서 필요한 정보만 통과시키고 필요없는 정보는 걸러내는 작업을 말한다.
  • N x N행렬을 영상과 Convolution연산(Correlation연산)을 하여 처리한다.
        - 영상과 연산하는 N x N 행렬을 커널(Kernel), 필터(filter), 마스크(Mask), 윈도우(Window)라고 한다.
        - Convolution연산
            - Filter를 영상을 이동하면서 같은 index의 값끼리 곱한뒤 다 더한다.
        - 최외곽처리는 바깥에 가상의 픽셀들이 있다고 생각하고 거기에 임의의 값을 채워 계산한다.
        - 가상의 바깥픽셀을 Padding이라고 한다.
        - 바깥픽셀을 채우는 방법
            - BORDER_CONSTANT : 같은 값으로 채운다. 보통 0으로 채움 000|abcd|000
            - BORDER_REPLICATE: 외곽에 있는 값으로 채운다. aaa|abcd|ddd
            - BORDER_REFLECT: 거울에 반사되는 형식으로 채운다. cba|abcd|dcb
            - BORDER_REFLECT_101: (OpenCV 기본방식)BORDER_REFLECT와 같은 형식인데 가장 외곽의                         값은 반사시키지 않는다. dcb|abcd|cba
        - N x N 행렬이 어떤 값을 가지고 있느냐에 따라 다양한 영상처리가 가능하다.
            - Bluring: 영상을 부드럽게 만들기
            - Shapening: 영상을 날카롭게 만들기
            - 엣지 검출
            - Noise 제거
        - 다양한 형태의 Kernel이 있으나 보통 3 x 3 행렬을 많이 쓴다
        - Anchor(고정점): 영상에서 Filtering을 하려는 지점을 가리키는 필터의 중앙점을 사용한다.
        - Receptive Field(수용장): 결과 픽셀에 영향을 주는 원본 이미지의 영역

 

기본적인 2차원 Filter계산

  • cv2.filter2D(src, ddepth, kernel, dst=None, anchor=None, delta=None, borderType=None)
        - src: 입력영상
        - ddepth: 출력 영상데이터의 타입 (opencv제공 변수 사용. cv2.CV_8U, cv2.CV_32F), -1 지정하면 입력영상과        같은 타입의 결과를 출력
        - kernel: Filtering을 수행할 kernel 행렬. (실수형)
        - anchor: kernel의 achor 위치. (-1,-1) 지정하면 kernel의 중앙을 사용
        - delta: 추가적으로 더할값.
        - borderType: padding 타입
        - 반환값
            - Filtering된 결과영상

 

Blurring

  • 영상을 흐릿/부드럽게 만드는 작업.
  • 픽셀간의 값의 차이를 줄여서 날카로운 엣지가 무뎌지고 영상에 있는 Noise(잡음)이 사라지도록 하는 효과가 있다.
        - 엣지(Edge): 물체간의 경계부분으로 contrast(대비)가 크다.

 

평균값 블러링(Average Blur)

  • 주변 픽셀의 평균값을 합성곱하여 적용한다.
  • 보통 3 x 3이나 5 x 5 필터를 많이 쓰며 필터의 크기가 커질수록 더 흐릿하게 변환된다.
  • 영상이 뭉게져 세밀한 부분이 잘 안보이는 단점이 있다.
  • cv2.blur(src, ksize)
        - src: blur 처리할 입력영상
        - ksize: 필터의 크기. (width, height) 튜플로 지정

 

이미지 불러오기

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

lenna = cv2.imread('images/Lenna.png')

 

cv2.blur(src, ksize)

lenna_blur = cv2.blur(lenna, (3, 3)) # 3 x 3 : 모든 값이 1/9
lenna_blur2 = cv2.blur(lenna, (5, 5)) # 5 x 5 : 1/25
# 필터의 크기가 커질수록 더 흐릿해진다.

cv2.imshow("src", lenna)
cv2.imshow('result', lenna_blur)
cv2.imshow('result2', lenna_blur2)

cv2.waitKey()
cv2.destroyAllWindows()

 

noise 감소 확인
 - noise는 pixcel값에 차이가 큰 픽셀이 중간 중간 껴있는 상태

 - ex) 200, 200, 5, 200, 10, 200, 200

plt.figure(figsize = (10, 3))
plt.plot(lenna[30,:,2])
plt.show()
# 급경사는 edge일 가능성이 높다

 

blur처리한 것의 그래프는 edge는 있지만 다른값들의 차이(noise)를 적게 만들어 준다.

plt.figure(figsize = (10, 3))
plt.plot(lenna_blur[30,:,2])
plt.show()

 

filter2D() 함수 이용

# filter2D() 함수 이용 -> 직접 filter를 전달해서 convolution 연산을 처리

# 3 X 3 필터
kernel = np.ones(shape = (3, 3)) / 9
result = cv2.filter2D(lenna, -1, kernel)

cv2.imshow("result", result)
cv2.waitKey()
cv2.destroyAllWindows()

 

Gaussian Blur

  • 대상 픽셀(Anchor)와 가까운 픽셀은 큰 가중치 멀리있는 픽셀은 작은 가중치를 사용해서 평균(가중평균)을 계산해서 필터링 한다.
        - Gaussian 분포(정규분포)의 확률밀도 함수를 사용해 필터에 들어갈 값들을 계산하여 대상픽셀을 기준으로 멀어질 수록 작은 값이 곱해지도록 한다. 
        - 평균은 0으로 하고 표준편차를 조정하여 흐림의 정도를 조절한다. 표준편차 값을 크게할 수록 흐려진다.
        - Filter의 shape은 (8sigma + 1,8sigma + 1) 나 (6sigma + 1,6sigma + 1) 으로 정해진다. sigma: 표준편차
  • Gaussian Blurring은 blur효과 뿐아니라 노이즈제거에도 많이 사용된다.
  • cv2.GaussianBlur(src, ksize, sigmaX, sigmaY)
        - src: blur를 적용할 영상. 
        - ksize: 커널크기. (0,0)으로 지정하면 sigma 값에 의해 결정된다. (보통 0,0 으로 설정)
        - sigamX: X축방향 sigma(표준편차) 값
        - sigamY: Y축방향 sigma(표준편차) 값. 생략하면 sigmaX와 같은 값 사용

 

import cv2

lenna = cv2.imread("images/Lenna.png")
lenna_gb = cv2.GaussianBlur(lenna, 
                            ksize=(0, 0), # 커널(필터) 크기: (0, 0) -> 표준편차에 크기에 맞춰 크기를 정한다.
                            sigmaX = 1, # 커널크기: (W, H) => Width 표준편차
                            #sigmaY = 1, # Height 표준편차. 생략-sigmaX와 동일한 값
                           )
# 표준편차를 크게할 수록 필터 크기가 커진다. -> 커지면 blur 효과가 강해진다.
cv2.imshow("src", lenna)
cv2.imshow("result", lenna_gb)

cv2.waitKey()
cv2.destroyAllWindows()

잡음제거

  • cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace)
        - 양방향 필터로 선명도는 유지하면서 bluring을 이용해 노이즈를 제거한다.
        - src: 잡음을 제거할 입력영상
        - d: 필터의 크기 지정(가우시안 blur의 ksize). 음수(-1)을 입력하면 sigmaSpace값에 의해 자동 결정된다. 일반         적으로 -1을 설정한다.
        - sigmaColor
            - 엣지인지 아닌지를 판단하는 기준값 설정. pixcel간의 차이가 지정한 값보다 크면 엣지라고 생각하고 작으               면 엣지가 아니라고 생각해 그 지점을 blurring 한다.
            - 이 값을 너무 크게 주면 엣지가 검출이 안되서 그냥 Gaussian Filter 사용하는 것과 같다.
        - sigmaSpace: Gaussian Filter에서 지정한 표준편차

 

sigma = 10
sigmaColor = 30

lenna_bf = cv2.bilateralFilter(lenna, 
                               d=-1, # filter크기를 표준편차로 결정.
                               sigmaColor=sigmaColor, # 엣지로 판단할 pixcel값의 차이.
                               sigmaSpace=sigma, # 표준편차
                              )

 

결과값 확인

import matplotlib.pyplot as plt

plt.figure(figsize=(15, 7))
plt.subplot(1, 2, 1)
plt.imshow(lenna[:,:,::-1])

plt.subplot(1,2,2)
plt.imshow(lenna_bf[:, :, ::-1])
plt.title(f"잡음제거: sigma={sigma}")
plt.tight_layout()
plt.show()

원본 lenna와 잡음이 제거된 lenna_bf의 그래프 확인

- 그래프의 모양을 보면 잡음 제거된 그래프에서 매끈하게 이루어진 것을 확인할 수 있다.

plt.figure(figsize=(20,6))
plt.subplot(2, 1, 1)
plt.plot(lenna[30, :, 0])
           
plt.subplot(2, 1, 2)
plt.plot(lenna_bf[30, :, 0])

plt.show()

샤프닝(Sharppen)

  • Blurring의 반대로 흐린영상을 선명한 영상으로 만드는 작업.
  • 선명한 영상을 만들기 위해서는 이미지의 엣지(edge-사물의 윤곽부분)의 대비를 크게 만들어 준다.
        - 이미지 전체를 다 선명하게 하는 게 아니라 윤곽(edge)만 선명하게 만들어 명암비(contrast)를 크게해 선명하게 보이게 한다. 
  • Unsharp mask(언샤프 마스크) 필터링
        - Unsharp한 영상을 이용해 Sharp한 영상을 만든다고 해서 Unsharp mask filtering 이라고 한다.
        - 원본 이미지에서 blurring 한 이미지를 뺀다음 그것을 원본이미지에 다시 더한다.

    h(x) = f(x)+alpha(f(x)-bar{f}(x))f(x)

 

  • 제공 함수는 없고 위 공식을 구현한다.
        - addWeighted(src, src_가중치, src2, src2_가중치, 더해줄값)
            - 가중합 계산 함수
            - src * src가중치 + src2 * src2가중치 + 더해줄값

 

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

def sharppen(src_img, alpha=0.0):
    # 원본 + (원본 - 블러) * alpha
    # blur -> 가우시안 블러
    src_blur = cv2.GaussianBlur(src_img, (0, 0), 1)
    src_img = src_img.astype('float64')
    return np.clip(src_img + (src_img - src_blur) * alpha, 0, 255).astype('uint8')

 

def sharppen2(src_img, alpha=0.0):
    src_blur = cv2.GaussianBlur(src_img, (0, 0), 1)
    return cv2.addWeighted(src_img, (1+alpha), src_blur, -alpha, 0)

 

rose = cv2.imread("images/rose.bmp")
rose_1 = sharppen(rose, 1.0)
rose_2 = sharppen(rose, 2.0)
rose_3 = sharppen(rose, 3.0)

결과 확인

- 원본과 rose_3의 그림들의 중심과 외곽부분의 명암대비를 살펴보면 rose_3의 그림이 더 대비가 명확하다. 

plt.figure(figsize=(13, 10))
plt.subplot(2, 2, 1)
plt.imshow(rose[:,:,::-1])
plt.title("원본")

plt.subplot(2, 2, 2)
plt.imshow(rose_1[:,:,::-1])
plt.title("alpha 1")


plt.subplot(2, 2, 3)
plt.imshow(rose_2[:,:,::-1])
plt.title("alpha 2")


plt.subplot(2, 2, 4)
plt.imshow(rose_3[:,:,::-1])
plt.title("alpha 3")

plt.tight_layout()
plt.show()