본문 바로가기

컴퓨터비전 (CV)

[컴퓨터비전] (2) Texture 에 대하여

 안녕하세요, 개강 무렵 시작한 첫 글이지만 여러 핑계로 종강 무렵에 후속편이 나오게 됐습니다.

게으름을 반성하며 잡설은 줄이고 바로 공부 시작하겠습니다.

 

 지난 글에서 Image feature 중에서 low-level에 해당하는 Color와 Edge에 대해서 알아보았습니다. 

오늘은 mid-level에 해당하는 Texture에 대해 알아보겠습니다. 

 

인식 피라미드

 

 high-level에는 Object, Scene 등이 존재하겠네요.

사담이지만, 저는 수업 시간에 Texture가 mid-level feature 라고 배웠으나 GPT님은 low-level로 분류했습니다.

저는 저희 교수님을 믿기에 당당히 mid-level로 적어놓았으나, 확실히 아시는 분이 계시다면 댓글 좀 남겨주십쇼.

 

 

 

[1] Texture : 이미지에서 반복되는 패턴이나 구조

 

 Texture에 대해 간단히 말해보자면, Image 상에서 반복되는 패턴입니다.

예시로 체크셔츠 사진 속 옷의 체크무늬, 잔디밭 사진 속 잔디들의 배열, 나무 껍질 사진의 껍질 표면 등이 되겠습니다.

이러한 Texture는 Image 공간 안에서 반복되기에 단위는 region입니다.

 

 Texture를 분석할 때 참고할 수 있는 3요소가 있는데, 밑의 3가지라고 수업시간에 배웠습니다.

 

- 1. 패턴이 Regular하게 or Random하게 반복되는가? 

- 2. 패턴의 Size가 큰가 or 작은가?

- 3. 패턴의 방향성?

 

 1번의 예시로 regular한 패턴은 체크셔츠의 체크무늬일 것이고, random한 패턴은 바위산 사진 속 무작위하게 나열된 바위들일 것입니다.

바위들을 찍은 image와 모래들을 찍은 image를 비교해 size를 크다(coarse) / 작다(dense)로 나눌 수 있을 것이고,

하늘의 구름을 찍은 사진은 대부분 가로 방향성을 띄고, 숲의 나무들을 찍은 사진은 세로 방향성을 띌 것입니다.

 

 

출처: 무신사 스튜피드 체크셔츠 시리즈

 

 예시로 위의 Image 속 공대생의 상징... 체크셔츠의 경우 패턴은 Regular 하며, 사이즈는 상대적이지만 coarse한 것으로 보입니다. 그리고 방향성 같은 경우는 가로/세로 양방향의 十 방향성을 가지겠습니다!

 

 이러한 texture를 분석할 때는 3가지 방법이 사용되는데 구조적 방법과 통계적 방법, 모델링이 있습니다.

이 중에서 통계적 방법은 variance, gradient, histogram 등을 사용하여 정량적으로 이미지를 분석하는 기법인데

color를 기반으로 하는 기법엔 LBP, GLCM 등이 있으며 edge 기반 분석 방법에는 Law's energy 가 있습니다.

 이 대표적인 텍스쳐 분석 기법의 자세한 사항은 추후에 다루어보도록 하겠습니다.

 

 다음은 없을 것 같아 지금 다루겠습니다.

 

 

 [1] - 1. LBP (Local Binary Pattern)

 

 남들과의 비교는 좋지 않지만, 항상 좋은 것만 하고 살 순 없습니다.

3x3 Image 에서 가운데 위치한 픽셀이 '나'라고 해봅시다. LBP는 나의 주변 픽셀 8개와 나의 값을 비교합니다.

 

3x3 Image

 

 나(4)와 주변 픽셀을 하나씩 비교합니다.  나보다 해당 픽셀의 값이 크면 그 픽셀에 1을 쓰고, 같거나 작으면 0을 씁니다.

그럼 밑에 있는 그림과 같은 새로운 3x3 LBP matrix가 나오게 됩니다.

 

3x3 matrix

 

 우측의 3x3 LBP matrix의 각 픽셀의 좌상단에 위치한 파란색 숫자는 순서를 의미합니다. 순서는 바로 뒤에 나올 비트 계산에 쓰이는데, 순서를 어떻게 설정하는지 정해진 국룰은 없고 그냥 개발자 맘이라고 합니다. (와우~)

 

3x3 matrix

 

 그렇게 완성된 LBP Matrix를 순서에 맞추어 8-bit 배열로 만들어 이진법으로 계산하게 되면 197이라는 숫자가 나오고, 이 값이 원래 '나' 였던 빨간색으로 표시된 픽셀의 값이 됩니다. 이 작업을 모든 픽셀에 해주어 나오게 되는 것이 LBP matrix일 것이고, LBP matrix 값이 크다는 것은 주변보다 어둡다는 뜻으로 해석할 수 있겠습니다.

 

import cv2
import numpy as np
from skimage.feature import local_binary_pattern
import matplotlib.pyplot as plt

# 이미지 읽기
image_path = 'Write your image path'
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

# LBP 계산
radius = 1
n_points = 8 * radius
lbp = local_binary_pattern(image, n_points, radius, method='uniform')

# 결과 시각화
plt.figure(figsize=(8, 8))
plt.subplot(1, 2, 1)
plt.title('Original Image')
plt.imshow(image, cmap='gray')
plt.subplot(1, 2, 2)
plt.title('LBP Image')
plt.imshow(lbp, cmap='gray')
plt.show()

 

 

위는 LBP 예제 코드입니다. example.jpg라는 이미지를 읽어 해당 이미지의 LBP matrix(image)를 출력해줍니다.

환경이 갖추어져 있다면, 여러분들이 분석하고자 하는 image의 path만 image_path에 넣어주시면 됩니다.

저는 여기에 버스 사진을 넣어보았고, 결과는 아래와 같습니다.

 

LBP Image

 

 

 

 [1] - 2. GLCM (Grey Level Co-occurrence Matrix)

 

 grey 이미지 내에서 특정한 패턴을 이루는 픽셀의 쌍이 일정 거리와 각도에서 얼마나 자주 발생하는지를 행렬로 나타냅니다. 이를 통해 반복되는 패턴에 대한 대비, 상관, 에너지, 동질성 등의 성질을 알 수 있습니다.

 

 여담이지만, 강의를 듣거나 공부를 하다보면 grey와 gray를 항상 혼용했어서 뭐가 맞는 표현인지 헷갈렸었는데, 둘 다 맞다고 합니다. (gray는 미국식, grey는 영국식 표기)

 

 이건 개념이 조금 어렵습니다. 기본적으로 greyscale의 (N x N) Image Matrix가 있고, 이 matrix의 greyscale 값을 L 이라고 합시다. 설정한 co-occurrence Matrix를 P[i,j]라고 합시다. 거리 d = (dx,dy)는 기준 픽셀 (위의 LBP에서 '나'에 해당합니다)로부터 떨어진 거리입니다.

 

 기준 픽셀의 값과 그로부터 d만큼 떨어진 픽셀의 값이 각각 i,j가 됩니다. 그리고, P[i,j]값을  +1 해주면 됩니다. 여기서 생각을 해보면, 당연히 새로이 만들어지는 co-occurrence Matrix는 (L x L) 의 size를 가지게 됩니다.

 이해가 잘 안되시죠? 저도 잘 안됩니다. 같이 예시로 살펴보겠습니다.

 

d(1,1) 이니까 빨간 네모 안의 2가지 픽셀을 보면 됩니다. 오른쪽 Pd는 모든 시행이 끝난 후의 GLCM

 

 여기서 Pd(i,j)의 값은 카운트 된 횟수라고 생각하시면 됩니다. 

아래는 GLCM의 예제 코드입니다.

 

import cv2
import numpy as np
import skimage.feature
import matplotlib.pyplot as plt

# 이미지 읽기
image_path = 'Write your image path'
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

# GLCM 계산
glcm = skimage.feature.greycomatrix(image, distances=[1], angles=[0], levels=256, symmetric=True, normed=True)

# GLCM 특징 추출
contrast = skimage.feature.graycoprops(glcm, 'contrast')[0, 0]
correlation = skimage.feature.graycoprops(glcm, 'correlation')[0, 0]
energy = skimage.feature.graycoprops(glcm, 'energy')[0, 0]
homogeneity = skimage.feature.graycoprops(glcm, 'homogeneity')[0, 0]

# 결과 출력
print(f'Contrast: {contrast}')
print(f'Correlation: {correlation}')
print(f'Energy: {energy}')
print(f'Homogeneity: {homogeneity}')

 

 아까 LBP에서 사용했던 버스 이미지를 돌려보았고, 밑은 그 결과입니다.

 

(좌) 사용 이미지 / (우) GLCM 결과

 

 여기서 각 결과값의 의미에 대한 설명을 드리겠습니다.

 

- Contrast : 대비, 클 수록 명확한 경계와 큰 grey-level 차이가 있다

- Correlation : 상관성, 클 수록 이미지에 linear한 구조가 있다는 것 (일정한 패턴)

- Energy : 에너지, 값이 클 수록 이미지에 규칙적이고 반복적인 패턴이 있다는 것

- Homogeneity : 동질성, 클 수록 이미지가 균일하고 부드러우며 큰 밝기 변화 X

 

 

 

 [1] - 3. Law's energy

 

 앞서 설명드린 2가지 방법은 Color를 이용하여 Texture를 분석하는 방법이었다면, 이건 Edge를 이용합니다.

1x5 Size의 5가지 필터(혹은 마스크)가 존재하는데 이 필터들을 이용해 Imgae와 convolution하는 방법입니다.

 

 5가지 필터는 다음과 같습니다.

- L5 = [1 4 6 4 1]            (Level, 이미지의 밝기 분포를 감지하고 텍스쳐의 전반적인 수준 측정)

- E5 = [-1 -2 0 2 1]         (Edge, 경계선을 감지하여 텍스쳐의 변화 지점 포착 - 1차 미분)

- S5 = [-1 0 2 0 -1]         (Spot, 점 패턴을 강조하여 작은 객체나 점들을 탐지 - 2차 미분)

- R5 = [1 -4 6 -4 1]         (Ripple, 주기적인 패턴을 감지하여 regular한 반복 텍스쳐 포착)

- W5 = [-1 2 0 -2 -1]       (Wave, 파형 패턴을 감지하여 random한 반복 텍스쳐 포착)

 

 여기서 각 필터들끼리 convolution을 진행합니다. 예시로, L5와 S5를 convolution하여 L5S5 필터를 만들게 되면

이 L5S5 필터는 밝기 분포와 경계선 탐지에 특화되어있을 것이며, 이러한 필터의 조합은 총 25개가 나올 것입니다.

 (근데 L5L5 필터는 밝기만 나타내기에 kernel로는 쓰이지 않고, 추후에 정규화 시에는 사용되게 됩니다.)

 그리고 이 때의 L5S5와 같은 필터를 Texture Descriptor 라고 볼 수 있습니다.

 

 또한 L5S5 와 S5L5는 각기 다른 필터일 것이기에, 이 둘을 합쳐주면 rotation에 invarient한 필터가 완성될 것이고,

이 때 만들어진 필터는 L5S5R 이라고 합니다.

 

L5S5R 필터

 

 밑은 Law's Energy 예시 코드입니다.

 

# Law's texture
import cv2
import numpy as np
import skimage.feature
import matplotlib.pyplot as plt

from scipy import signal as sg

def laws_texture(gray):
    (rows, cols) = gray.shape[:2]

    smooth_kernel = (1/25)*np.ones((5,5))
    gray_smooth = sg.convolve(gray, smooth_kernel,"same")
    gray_processed = np.abs(gray - gray_smooth)

    filter_vectors = np.array([[ 1,  4,  6,  4, 1],    # L5
                               [-1, -2,  0,  2, 1],    # E5
                               [-1,  0,  2,  0, -1],    # S5
                               [ 1, -4,  6, -4, 1]])   # R5

    # 0:L5L5, 1:L5E5, 2:L5S5, 3:L5R5,
    # 4:E5L5, 5:E5E5, 6:E5S5, 7:E5R5,
    # 8:S5L5, 9:S5E5, 10:S5S5, 11:S5R5,
    # 12:R5L5, 13:R5E5, 14:R5S5, 15:R5R5
    filters = list()
    for i in range(4):
        for j in range(4):
            filters.append(np.matmul(filter_vectors[i][:].reshape(5,1),
                                     filter_vectors[j][:].reshape(1,5)))

    conv_maps = np.zeros((rows, cols,16))
    for i in range(len(filters)):
        conv_maps[:, :, i] = sg.convolve(gray_processed,
                                         filters[i],'same')

    texture_maps = list()
    texture_maps.append((conv_maps[:, :, 1]+conv_maps[:, :, 4])//2)     # L5E5 / E5L5
    texture_maps.append((conv_maps[:, :, 2]+conv_maps[:, :, 8])//2)     # L5S5 / S5L5
    texture_maps.append((conv_maps[:, :, 3]+conv_maps[:, :, 12])//2)    # L5R5 / R5L5
    texture_maps.append((conv_maps[:, :, 7]+conv_maps[:, :, 13])//2)    # E5R5 / R5E5
    texture_maps.append((conv_maps[:, :, 6]+conv_maps[:, :, 9])//2)     # E5S5 / S5E5
    texture_maps.append((conv_maps[:, :, 11]+conv_maps[:, :, 14])//2)   # S5R5 / R5S5
    texture_maps.append(conv_maps[:, :, 10])                            # S5S5
    texture_maps.append(conv_maps[:, :, 5])                             # E5E5
    texture_maps.append(conv_maps[:, :, 15])                            # R5R5
    texture_maps.append(conv_maps[:, :, 0])                             # L5L5 (use to norm TEM)

    TEM = list()
    for i in range(9):
        TEM.append(np.abs(texture_maps[i]).sum() / np.abs(texture_maps[9]).sum())

    return TEM

# 이미지 읽기
image_path = 'Write your image path'
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

laws = laws_texture(image)    # 9-d
print(laws)

 

 아래는 마찬가지로 버스 이미지를 입력 이미지로 주었을 때의 결과입니다.

 

최하단의 데이터 값들이 결과값에 해당