[이미지전처리] 외곽선 검출 알고리즘 논문 리뷰
참고논문
외곽선 검출 및 잡음 제거 알고리즘
장점
- 동일한 객체가 여러개 있을 때 윤곽선을 비교적 잘 딸 수 있다.
단점
- 컴퓨터 자원이 많이 소모된다
논문 코드 구현
1. 픽셀 2개의 유사도를 비교하는 함수 구현
이미지 내 임의의 점 p(x, y), 점 q(i, j)가 있을 때 p와 q의 픽셀 유사도를 구현하는 식이다. 유사도를 판단하는 기준은 두 픽셀의 차이의 절대값이 2 미만인 경우 유사하다고 판단, 그렇지 않으면 유사하지 않다고 판단한다.
1번 식을 파이썬으로 구현하면 다음과 같다.
import numpy as np
def A_function(P, x, y, i, j):
# P는 이미지의 픽셀 값이 저장된 2D numpy 배열
# (x, y)와 (i, j)는 각 픽셀의 좌표
if abs(P[x][y] - P[i][j]) < 2:
return 1
else:
return 0
2. 7X7 마스크 내에서 구한 누적 함수 합 함수 구현
이 식은 A함수의 값을 7x7 크기의 격자(마스크) 내에서 누적하여 합산하는 것을 의미한다. 구체적으로, 중심이 인 위치를 기준으로, 에서 까지의 범위와 에서 까지의 범위 내에 있는 모든 위치에 대해 함수의 값을 계산하고, 이 값들을 모두 합한다. 이로써 는 주어진 위치의 픽셀이 그 주변 7x7 격자 내의 다른 픽셀과 얼마나 유사한지를 수치로 나타낸다. 함수가 1을 반환하는 경우는 두 픽셀이 서로 유사할 때이므로, 의 값이 크다는 것은 위치의 픽셀이 그 주변에 많은 유사한 픽셀을 가지고 있다는 것을 의미한다.
2번 식을 파이썬으로 구현하면 다음과 같다.
import numpy as np
from PIL import Image
def B_function(P, x, y):
height, width = P.shape
B_value = 0
# 7x7 마스크 범위 내에서 누적 합 계산
for i in range(max(0, x-3), min(height, x+4)):
for j in range(max(0, y-3), min(width, y+4)):
B_value += A_function(P, x, y, i, j)
return B_value
3. 이미지에서 임의의 픽셀 p(를 중심으로 하는 7x7 마스크(이웃) 내의 모든 픽셀 값들의 평균을 계산하는 함수
이 평균은 주변 픽셀들의 값을 모두 더한 뒤, 총 픽셀 수인 49로 나누어 계산한다. 이 평균 계산은 이미지의 로컬 영역 내에서의 균일성 또는 변동성을 평가할 때 사용한다. 즉, 주변 픽셀에 비해 확 튀는 픽셀을 찾는 함수이다. 이런 연산은 노이즈 제거, 이미지 스무딩, 또는 특성 강조 등 다양한 목적으로 활용할 수 있다.
3번 식을 파이썬으로 구현하면 다음과 같다.
import numpy as np
from PIL import Image
def load_image(image_path):
with Image.open(image_path) as img:
return np.array(img)
def calculate_average(P, x, y):
height, width = P.shape
sum_pixels = 0
count = 0
for i in range(max(0, x-3), min(height, x+4)):
for j in range(max(0, y-3), min(width, y+4)):
sum_pixels += P[i, j]
count += 1
return sum_pixels / count
# 이미지 경로
image_path = '이미지경로'
P = load_image(image_path)
# 특정 위치 p(x, y)에서 평균 계산
x, y = 10, 10 # 예시 위치
average_value = calculate_average(P, x, y)
print("Average value at (", x, ",", y, "):", average_value)
4. 이미지의 각 픽셀을 이진화하는 함수
1~3 함수를 활용하여 만든 이진화 판단 식이며, 이미지의 모든 픽셀을 돌며 해당 픽셀을 이진화 할지 말지 결정한다. 파이썬으로 구현한 코드는 다음과 같다.
def C_function(P, val2):
height, width = P.shape
C_image = np.full((height, width), 255, dtype=np.uint8)
for x in range(height):
for y in range(width):
B_value = B_function(P, x, y)
average_value = calculate_average(P, x, y)
if B_value < val2 and P[x, y] < average_value - 2:
C_image[x, y] = 0
return C_image
문제는 val2를 모른다는 것이다. 식5번에 val2 값을 찾기 위한 방법이 정의되어있다.
5. 외곽선 점수 함수 구현
일단 임의로 val2값을 정하고 이미지를 n*n으로 분할한다. (대충 감으로 결정하며, 논문에서는 12*12영역으로 분할함). 각 영역을 식 4의 함수로 이진화하여 윤곽선 픽셀의 수(0이 나오는 픽셀, 검은픽셀)를 센다. 각 영역에서 윤곽선에 해당하는 점의 개수를 구하고 k라고 한다. k를 식5의 함수에 대입해 영역별로 sorce를 구한다. k가 40까지는 영역은 점수가 0이고, 40 이상부터 점수가 증가하기 시작하여 최대 10점까지 받을 수 있다. 12*12 영역으로 분할하였으므로, 최저 점수는 0(모든 영역에서 0점), 최대 점수는 1140(모든 영역에서 10점)이다. 각 영역의 score(외곽선 빈도 점수) 합을 value()라고 한다.
너무 복잡하니 일단 이정도로 정리하자.
value()값이 크다 => 각 영역에 윤곽선 점수가 높다. 즉 윤곽선을 많이 표시했음 좋겠다.
value()값이 적다 => 각 영역의 윤곽선 점수가 낮다. 즉 윤곽선을 적게 표시했으면 좋겠다.
이분 탐색을 통해 value() 함수의 결과가 원하는 범위에 들어오도록 조정하여 val2의 값을 구해야한다. 구하는 과정은 다음과 같다.
- val2의 초기 최소값과 최대값을 설정한다.
- 중간값을 계산하고 해당 val2 값으로 이진화를 수행한 후 value() 함수를 계산한다.
- 계산된 value() 값이 목표 범위에 따라 val2의 범위를 조정한다.
- 이 과정을 반복하여 최적의 val2 값을 찾는다.
이 과정을 파이썬 코드로 구현하면 다음과 같다.
def calculate_score(k):
if k < 40:
return 0
else:
return min(10, ((k - 40) ** 2) / 16)
def calculate_value(C):
scores = []
height, width = C.shape
# 12x12 영역으로 나눔
region_size = height // 12
for x in range(12):
for y in range(12):
# 각 구역별로 C[x][y] = 0인 점의 개수 계산
count = np.sum(C[x*region_size:(x+1)*region_size, y*region_size:(y+1)*region_size] == 0)
scores.append(calculate_score(count))
return sum(scores)
def binary_search_for_val2(P, target_range):
low, high = 0, 255
while low <= high:
mid = (low + high) // 2
C_image = C_function(P, mid)
value = calculate_value(C_image)
if value < target_range[0]:
low = mid + 1
elif value > target_range[1]:
high = mid - 1
else:
return mid # 목표 범위 내의 val2 값
return -1 # 적절한 val2 값이 없음
# 예시 실행
P = load_image('path_to_your_image.jpg')
target_range = (150, 250) # 원하는 value() 범위. 전체 범위는 (0, 1440)
optimal_val2 = binary_search_for_val2(P, target_range)
전체 코드 및 이미지 테스트
import numpy as np
from PIL import Image
class Contour:
def __init__(self, n):
self.n = n
# 이미지를 로드하여 np 배열로 변환
def load_image(self, image_path):
with Image.open(image_path) as img:
gray_image = img.convert('L') # 흑백이미지로 ㅣ변환
return np.array(gray_image, dtype=np.int32) # numpy 배열로 변환
# 점수 계산 식 구현
def calculate_score(self, k):
if k < 40:
return 0
else:
return min(10, ((k - 40) ** 2) / 16)
# 각 영역의 점의 개수를 구함
def calculate_value(self, C):
scores = []
height, width = C.shape
# nxn 영역으로 나눔
region_size = height // self.n
for x in range(self.n):
for y in range(self.n):
# 각 구역별로 C[x][y] = 0인 점의 개수 계산
count = np.sum(C[x*region_size:(x+1)*region_size, y*region_size:(y+1)*region_size] == 0)
scores.append(self.calculate_score(count))
return sum(scores)
# 두 픽셀 사이의 유사도 구하기
def A_function(self, P, x, y, i, j):
if abs(P[x, y] - P[i, j]) < 2:
return 1
else:
return 0
# 7x7 크기의 격자(마스크) 내에서 정 가운데 값과 나머지 값들의 유사도를 구해서(A 함수 이용) 값을 누적 합산
# B의 값이 크다 -> 주변에 비슷한 픽셀이 많다!
def B_function(self, P, x, y):
height, width = P.shape
B_value = 0
for i in range(max(0, x-3), min(height, x+4)):
for j in range(max(0, y-3), min(width, y+4)):
B_value += self.A_function(P, x, y, i, j)
return B_value
# 7x7 크기의 격자(마스크) 내에서 픽셀 값의 평균을 구함
def calculate_average(self, P, x, y):
height, width = P.shape
sum_pixels = 0
count = 0
for i in range(max(0, x-3), min(height, x+4)):
for j in range(max(0, y-3), min(width, y+4)):
sum_pixels += P[i, j]
count += 1
return sum_pixels / count
# 각 픽셀 이진화 함수
def C_function(self, P, val2):
height, width = P.shape
C_image = np.full((height, width), 255, dtype=np.uint8) #흰색 이미지 생성(배경 생성)
for x in range(height):
for y in range(width):
B_value = self.B_function(P, x, y)
average_value = self.calculate_average(P, x, y)
if B_value < val2 and P[x, y] < average_value - 2:
C_image[x, y] = 0
return C_image
# 이분 탐색으로 최적의 value2 값 찾기
def binary_search_for_val2(self, P, target_range):
low, high = 0, 255
while low <= high:
mid = (low + high) // 2
C_image = self.C_function(P, mid)
value = self.calculate_value(C_image)
if value < target_range[0]:
low = mid + 1
elif value > target_range[1]:
high = mid - 1
else:
return mid # 목표 범위 내의 val2 값
return -1 # 적절한 val2 값이 없음
n = 12
contour = Contour(n)
img_path = './worm.jpg'
P = contour.load_image(img_path )
target_range = (0, 1440) #적당히 범위 조정
optimal_val2 = contour.binary_search_for_val2(P, target_range)
P = contour.load_image(img_path)
binary_image = contour.C_function(P, optimal_val2)
# 이진화 결과 저장
Image.fromarray(binary_image).save(f'./worm_result.jpg')