AI

[이미지전처리] 외곽선 검출 알고리즘 논문 리뷰

삽사 2024. 5. 23. 01:10

참고논문

 

외곽선 검출 및 잡음 제거 알고리즘

https://manuscriptlink-society-file.s3-ap-northeast-1.amazonaws.com/kips/conference/ack2021/abs/KIPS_C2021B0087.pdf

 

장점

- 동일한 객체가 여러개 있을 때 윤곽선을 비교적 잘 딸 수 있다. 

 

단점

- 컴퓨터 자원이 많이 소모된다

 

논문 코드 구현 

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의 값을 구해야한다. 구하는 과정은 다음과 같다. 

  1. val2의 초기 최소값과 최대값을 설정한다. 
  2. 중간값을 계산하고 해당 val2 값으로 이진화를 수행한 후 value() 함수를 계산한다.
  3. 계산된 value() 값이 목표 범위에 따라 val2의 범위를 조정한다. 
  4. 이 과정을 반복하여 최적의 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')

 

 

(좌) 원본이미지 (우) 결과 이미지