기본 사진
# [Otsu algorithm] : 이미지에서 물체와 배경을 분리하기 위해 사용되는 이미지 이진화 기술
# 핵심 아이디어 : 이미지의 히스토그램을 분석하여 이미지의 foreground와 background를 분리하는 threshold(임계값)를 찾는 것
def otsu(img):
# histogram and CDF(cumulative distribution function, 누적분포함수)
# 누적 분포 함수는 랜덤 변수가 특정 값보다 작거나 같을 확률을 나타내는 함수,
#'누적'이라는 이름은 특정 값보다 작은 값들의 확률을 모두 누적해서 구한다는 의미에서 붙여진 이름
# ex) Fx(x) = Px(X<=x)
# 0부터 원하는 항까지 누적확률 분포, 전경, 후경은 왼쪽과 오른쪽
# cv2.calcHist() 함수를 사용하여 이미지의 히스토그램을 계산
# cv2.calcHist(images, channels, maskm histSize, ranges, hist=None, accumulate=None)
hist = cv2.calcHist([img], [0], None, [256], [0, 256])
# 정규화
hist_norm = hist.ravel() / hist.max() # 히스토그램 값 / max값 : 각 픽셀 값의 확률
# numpy.cumsum 함수는 지정된 축을 따라 어레이 요소의 누적 합을 계산
CDF = np.cumsum(hist_norm)
# initialization(초기화) 정의
bins = np.arange(256)
fn_min = np.inf
threshold = -1
for i in range(1, 256):
# 분할된 히스토그램은 각 클래스(배경 및 전경)의 확률 분포
p1, p2 = np.hsplit(hist_norm, [i]) # probabilities, ex) np.hsplit(x, 2) : 배열을 가로(열 방향으로) 2개의 하위 배열로 분할
q1, q2 = CDF[i], CDF[255] - CDF[i] # cumsum of classes, 값을 사용하여 각 클래스의 가중치 및 평균을 계산
# 0으로 나눌 수 없기 때문에 아주 작은 값으로 설정
if q1 == 0:
q1 = 0.00000001
if q2 == 0:
q2 = 0.00000001
# 각 픽셀 값의 weights 계산을 위해 사용
b1, b2 = np.hsplit(bins, [i]) # 이렇게 나눈 값 들은 각 클래스의 픽셀 값의 범위
# finding means and variances
m1, m2 = np.sum(p1 * b1) / q1, np.sum(p2 * b2) / q2 # 각 클래스 내부의 픽셀 값에 대한 평균 밝기 값 계산
v1, v2 = np.sum(((b1 - m1) ** 2) * p1) / q1, np.sum(((b2 - m2) ** 2) * p2) / q2 # 각 클래스 내부의 픽셀 값에 대한 분산을 계산
# calculates the minimization function
fn = v1 * q1 + v2 * q2 # 누적 분산 계산
if fn < fn_min: # 각 클래스의 현재 분산 값이 기존의 최소 분산 값보다 작으면 업데이트
fn_min = fn
threshold = i
return threshold
# 이미지 shape 만들기
binary_img = np.zeros((img.shape[1], img.shape[0]), np.uint8)
# Otsu algorithm으로 threshold 값 구하기
threshold = otsu(img)
# 위에서 구한 threshold 값을 기준으로 바이너리화 진행
for i in range(0, img.shape[1]):
for j in range(0, img.shape[0]):
if img[i][j] < threshold:
binary_img[i][j] = 0
else:
binary_img[i][j] = 255
# find otsu's threshold value with OpenCV function
# cv2.threshold() 함수를 사용하여 오츠 알고리즘을 실행하고, ret에 계산된 임계값을 저장하고, otsu에는 이진화된 이미지를 저장
ret, otsu_img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f'직접 구한 값 : {threshold}\\ncv를 이용한 값 : {ret}\\n') # 비슷한 값을 가지는걸 확인
# 이미지를 1행 2열 그리드에 표시
fig, axes = plt.subplots(1, 2, figsize=(8, 4))
# 첫 번째 축에 binary 이미지 표시
axes[0].imshow(binary_img, cmap="gray")
axes[0].set_title("Binary Image")
# 두 번째 축에 cv binary 이미지 표시
axes[1].imshow(otsu_img, cmap="gray")
axes[1].set_title("CV Binary Image")
물체와 배경을 분리한 사진
# 박스 필터(Box Filter)를 사용하여 이미지를 평활화(스무딩)하는 역할
# 각 픽셀 주변의 픽셀들의 평균 값을 사용하여 해당 픽셀의 값을 대체하는 필터
# 이를 통해 이미지의 노이즈를 줄이고 부드러운 효과를 얻음
def box_filter(img):
height, width = img.shape
# 가중치 1을 가지는 3x3 커널 정의
kernel = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.float64)
filter_img = np.zeros((height, width), dtype=int)
# 이미지의 가장자리에서 2픽셀만큼 필터를 적용하지 않음
for i in range(2, height - 2):
for j in range(2, width - 2):
sum = 0
# 현재 픽셀의 기준으로 -1, 0, 1의 값의 주변 픽셀을 순회
for x in range(-1, 2):
for y in range(-1, 2):
# 현재 픽셀의 주변 픽셀에 박스 필터 커널 값을 곱하고 총합에 더하여 평균을 구함
sum += kernel[x + 1][y + 1] * img[i + x][j + y]
filter_img[i][j] = sum // 9 # 필터 커널의 가중치의 합이 9이므로 나눠줌
return filter_img
height, width = img.shape
# 정규분포를 따르는 노이즈 이미지 생성
noisy_sigma = 35 # 노이즈의 표준편차
noise = np.random.randn(height, width) * noisy_sigma
# 초기화된 노이즈 배열 생성 후 노이즈값을 더함
noisy_img = np.zeros(img.shape, np.uint8)
noisy_img = img + noise
# box filter 함수와 cv2를 사용하여 노이즈 이미지에 필터를 적용
box_img = box_filter(noisy_img)
# 커널은 주변 3x3 픽셀 영역에서 각 픽셀의 값을 평균화하여 중심 픽셀을 대체
# -1을 사용하면 입력 이미지와 동일한 데이터 유형을 사용하도록 자동으로 결정
box_img2 = cv2.boxFilter(noisy_img, -1, (3, 3))
# 이미지를 1행 2열 그리드에 표시
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(noisy_img, cmap="gray")
axes[0].set_title("Noisy Image")
# 첫 번째 축에 box filter 이미지 표시
axes[1].imshow(box_img, cmap="gray")
axes[1].set_title("Box_filter Image")
# 두 번째 축에 cv box filter 이미지 표시
axes[2].imshow(box_img2, cmap="gray")
axes[2].set_title("CV Box_filter Image")
각 픽셀 주변의 픽셀의 평균값으로 해당 픽셀의 값을 대체해 노이즈를 줄인 사진
# 소벨(Sobel) 엣지 검출을 수행하는 함수로, 이미지에서 엣지(경계)를 감지하기 위해 사용
# 소벨 엣지 검출은 주어진 이미지에서 수직 방향과 수평 방향의 엣지를 감지
def sobel_edge_detect(img):
width, height = img.shape
# 정해져 있는 값들임 수평, 수직을 확인하기 위해
kernel_horizon = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float64) # 수평 방향 엣지 검출을 위한 수평 방향 소벨 커널
kernel_vertical = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]], dtype=np.float64) # 수직 방향 엣지 검출을 위한 수평 방향 소벨 커널
# 출력 이미지 초기화
edge_img = np.zeros((height, width), dtype=int)
horizon_edge_img = np.zeros((height, width), dtype=int)
vertical_edge_img = np.zeros((height, width), dtype=int)
# 이미지의 가장자리에서 2픽셀만큼 필터를 적용하지 않음
for i in range(2, height - 2):
for j in range(2, width - 2):
# 3x3 크기의 소벨 커널 주변의 픽셀을 순회
for n in range(0, 3):
for m in range(0, 3):
# 수평 방향 및 수직 방향 엣지를 검출하기 위해 소벨 커널을 사용하여 가중치 합을 계산
horizon_edge_img[i][j] += (img[i + n - 1][j + m - 1] * kernel_horizon[n][m])
vertical_edge_img[i][j] += (img[i + n - 1][j + m - 1] * kernel_vertical[n][m])
# 수평 방향 엣지와 수직 방향 엣지의 크기를 계산, 엣지의 강도(밝기 차이)를 나타냄
edge_img[i][j] = np.sqrt(horizon_edge_img[i][j] * horizon_edge_img[i][j]
+ vertical_edge_img[i][j] * vertical_edge_img[i][j])
max_val = np.max(edge_img)
min_val = np.min(edge_img)
for i in range(0, height):
for j in range(0, width):
# 이미지의 모든 픽셀에 대해 엣지 값을 정규화하여 엣지 값의 범위를 [0, 255]로 변환
edge_img[i][j] = 255.0 * (edge_img[i][j] - min_val) / (max_val - min_val)
return edge_img
# sobel_edge_detect 함수와 cv2를 사용하여 이미지에 Sobel 필터를 적용
edge_img_1 = sobel_edge_detect(img)
# cv2.CV_8U는 출력 이미지의 픽셀 값이 0에서 255 사이의 값을 가지게함
# (1, 0)은 수평 방향, (0, 1)은 수직 방향의 소벨 필터를 적용하겠다는 것을 의미,
dx = cv2.Sobel(img, cv2.CV_8U, 1, 0, ksize=3)
dy = cv2.Sobel(img, cv2.CV_8U, 0, 1, ksize=3)
edge_img_2 = dx + dy
# 이미지를 1행 2열 그리드에 표시
fig, axes = plt.subplots(1, 2, figsize=(8, 4))
# 첫 번째 축에 sobel filter 이미지 표시
axes[0].imshow(edge_img_1, cmap="gray")
axes[0].set_title("Sobel_filter Image")
# 두 번째 축에 cv sobel filter 이미지 표시
axes[1].imshow(edge_img_2, cmap="gray")
axes[1].set_title("CV Sobel_filter Image")
이미지의 수평/수직 엣지를 감지한 사진
이론으로만 배우다가 실습으로 직접 이미지에 필터를 적용해보니 이해가 훨씬 잘되는 기분이었습니다. 확실히 눈에 결과물이 보이니 어떤 목적으로 코드를 작성했고 필터를 적용 했는지 알게 되었습니다. 이 실습을 바탕으로 다시 이론을 복습해 좀 더 완벽하게 공부하고 싶다는 생각이 들었습니다.