from PIL import Image
import numpy as np
from matplotlib import pyplot as plt
im = Image.open('building.jpg')
im = np.asarray(im)
print(im.shape) # (400,600,3)
plt.imshow(im)
먼저 PIL의 Image를 통해 이미지를 불러온 후 ndarray로 변환해서 plt로 이미지를 출력하면 아래와 같은 사진이 나온다.
이때 이미지의 RGB값은 0부터 255 사이의 값이므로 ndarray로 변환될 때 자동적으로 dtype이 uint8 (usigned int8)로 지정된다.
Point Processing
Point Processing은 Point Operation을 사용해 이미지를 처리한다.
Point Operation은 하나의 점을 변경할 때 해당 점 하나만 보고 값을 변경하는 방식이다.
이와 반대로 Filtering이 사용하는 Neighborhood Operation은 하나의 점을 변경할 때 주변의 n개의 점을 보고 값을 변경하는 방식이다.
Point Processing으로 아래와 같은 효과들이 있고 각 효과들은 아래의 식을 이용해 픽셀값을 변경한다.
Brightness
각 RGB 값은 0 ~ 255 사이의 값을 가지는데 값이 크면 클수록 밝다.
이미지를 밝게 하는 Brightness 효과를 주려면 RGB 값을 증가시키면 된다.
하지만 3차원 행렬인 im 객체에 그냥 숫자를 더하게 되면 아래와 같은 이상한 이미지로 변환된다.
plt.imshow(im + 100)
그 이유는 그냥 100을 더하기만 하면 100을 더했을 때 255를 넘는 값이 생기기는데 dtype이 uint8이므로 255를 넘는 값은 다시 0부터 시작하기 때문이다.
고로 100이 아니라 100.0을 더하게 되면 dtype이 float64로 변경되면서 255를 넘어도 0부터 시작하지 않게 된다.
그리고 255가 넘은 값들을 255로 변경해주면 정상적으로 Brightness 효과를 줄 수 있다.
im2 = im + 100.0
im2[im2>=255] = 255
plt.imshow(im2/255.0)
Contrast
Contrast는 값을 감소시키는데, 빼서 감소시키는게 아니라 비율을 낮춰서 감소시키는 효과이다.
그래서 아래 코드를 보면 2를 나누는데, 이때 나누면서 dtype이 float64로 변경되기 때문에 astype을 이용해 dtype을 다시 uint8로 변경해서 사용해야 한다.
im3 = im/2
plt.imshow(im3.astype(np.uint8))
Filtering
정확히는 Linear shift-invariant Filtering.
의미는 값이 변하지 않는(invariant) 커널을 이동(shift)하면서 선형결합(Linear Combination)을 통해 필터링을 한다는 뜻이다.
Box filter
kernel 또는 box filter 또는 2D rect filter 또는 square mean filter라고 불리는 이 필터는 주변의 평균(local average)을 보고 픽셀값을 변경하게 한다. 고로 이는 smoothing(blurring) 효과를 가진다.
이 kernel의 값을 이용해 선형결합을 하는 것이므로 kernel의 원소들을 coefficient라고도 한다.
3x3 커널이 있을 때 input으로 들어온 image의 파란색 부분을 보고 일대일로 대응되는 값을 곱한후 다 더하면 270이 나오고 이를 9로 나누면 30이 나온다. (사실 커널 자체에 9를 나누는게 포함되어 있지만 이해를 위해 풀어썼다.)
이를 output의 파란색 부분에 집어넣게 된다.
이는 자기 자신과 주변 8개의 픽셀을 사용해 평균값을 내는 것이므로 blurring 효과를 가져옴을 알 수 있다.
이를 수식으로 나타내면,
$$ h[m,n] = \sum_{k,l} g[k,l]f[m+k, n+l] $$
이다.
Padding
지금 30은 output의 (3,2) 지점에 넣어졌지만, (0,0) 지점은 왼쪽 픽셀과 위쪽 픽셀이 없기 때문에 계산할 수 없다.
이를 패딩(padding)이라 한다.
이 padding의 크기는 kernel_size를 2로 나눈 몫을 이용해 구할 수 있다.
위의 3x3 커널의 사이즈는 3이므로 2로 나누면 padding은 1이 된다.
Codes
def filtering(im, kernel_size):
if (kernel_size % 2) == 0:
print('kernel_size sholud be odd')
return
p = kernel_size//2
kernel = np.ones((kernel_size, kernel_size)) / (kernel_size*kernel_size)
shape = im.shape
im2 = np.zeros_like(im)
for i in range(p, shape[0]-p):
for j in range(p, shape[1]-p):
for k in range(3):
im2[i][j][k] = np.sum(kernel * im[i-p:i+p+1, j-p:j+p+1, k])
#np.dot(kernel.reshape(1,-1)[0], im[i-p:i+p+1, j-p:j+p+1, k].reshape(1,-1)[0])
return im2[p:shape[0]-p, p:shape[1]-p]
im4 = filtering(im, 3)
plt.imshow(im4)
먼저 kernel은 한가운데에 픽셀을 기준으로 주변을 계산하는 것이므로 kernel_size는 홀수여야 한다.
위의 수식을 그대로 구현하면 되는데, 처음에는 np.dot을 사용해 계산을 하려했다.
하지만 np.dot은 2차원이상의 행렬은 반환값으로 행렬을 내보내는 함수라서 내가 하려는 계산을 하려면 1차원으로 변경한 후 np.dot을 사용해야 해서 코드가 불필요하게 길어졌다.
그냥 간단하게 커널과 구하고 싶은 부분의 행렬을 *으로 곱하고 이를 np.sum으로 모두 더하면 되는 문제였다.
마지막으로 구한 행렬의 패딩을 제거한 후 반환하면 우리가 얻고자하는 blurring 된 이미지를 얻을 수 있다.
np.dot
np.dot은 1차원 행렬일 때는 모든 값을 각자 곱한 후 더한 스칼라 값을 반환하지만,
2차원 행렬일 때는 각 벡터끼리의 내적값을 가진 행렬을 반환하는 행렬곱을 수행한다.
고로 2차원 행렬일 때는 두 행렬의 각 원소의 곱의 합을 구하려면 두 ndarray를 곱한 후 np.sum을 통해 더해주면 된다.
Gaussian Filtering
기존 Filtering은 blurring이 너무 많이 되어서 디테일이 많이 사라진다는 문제점을 가지고 있다.
이때 가우시안 필터로 필터링을 하면 어느 정도의 디테일을 살리면서 blurring을 할 수 있다. (대부분의 필터링에서는 가우시안 필터를 사용한다.)
기존 Filtering에서는 커널을 모두 똑같은 가중치로 사용했지만, 가우시안 필터에서는 가우시안 함수를 이용해 가중치를 결정한다.
$$ f(i, j) = \frac {1} {2\pi\sigma^2} e^{-\frac{i^2 + j^2} {2\sigma^2}} $$
soft shadow effect
가우시안 필터로 필터링한 이미지에 기존 이미지를 overlay하면 soft shadow 효과를 얻을 수 있다.
* overlay - 단순히 더하기가 아닌 기존 이미지 값이 있는 픽셀은 기본 이미지 값으로 하고 없는 픽셀은 가우시안 블러 처리된 값으로 함.
Sharpening
이전에 Filtering에서는 커널의 모든 값을 동일하게 줬기 때문에 blur 효과를 얻을 수 있었다.
Sharpening은 커널의 가운데 값을 높게 주고 주변의 값들은 음수의 값을 준다.
이렇게 함으로써 주변 픽셀과의 차이가 없는 픽셀은 이전과 똑같지만 주변 픽셀과의 차이가 많은 픽셀은 그 차이가 더 두드러지게 보이게 한다.
하지만 커널의 가중치를 너무 심하게 주면 노이즈가 생길 수 있다. (overshapened)
'대학 강의 > 컴퓨터 비전' 카테고리의 다른 글
[컴퓨터 비전] Image Classification (0) | 2023.06.21 |
---|---|
[컴퓨터 비전] 2. Edge (0) | 2023.03.27 |