공부 내용


기본 사진

기본 사진

# [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")

이미지의 수평/수직 엣지를 감지한 사진

이미지의 수평/수직 엣지를 감지한 사진

소감

이론으로만 배우다가 실습으로 직접 이미지에 필터를 적용해보니 이해가 훨씬 잘되는 기분이었습니다. 확실히 눈에 결과물이 보이니 어떤 목적으로 코드를 작성했고 필터를 적용 했는지 알게 되었습니다. 이 실습을 바탕으로 다시 이론을 복습해 좀 더 완벽하게 공부하고 싶다는 생각이 들었습니다.