특징점 검출 (Feature Detection)

특징점 검출 (Feature Detection)

개요

특징점(Feature)은 이미지에서 고유하고 반복적으로 검출 가능한 지점입니다. 코너, 블롭, 엣지 교차점 등이 있으며, 이미지 매칭, 객체 인식, 3D 재구성 등에 활용됩니다. 이 레슨에서는 Harris, FAST, SIFT, ORB 등 다양한 특징점 검출 알고리즘을 학습합니다.


목차

  1. 특징점 기초 개념
  2. 코너 검출 - Harris
  3. 좋은 특징점 - goodFeaturesToTrack
  4. FAST 검출기
  5. SIFT 검출기
  6. ORB 검출기
  7. 키포인트와 디스크립터
  8. 연습 문제

1. 특징점 기초 개념

특징점이란?

특징점 (Feature Point / Keypoint):
이미지에서 고유하게 식별 가능한 지점

좋은 특징점의 조건:
1. 반복성 (Repeatability): 같은 물체는 항상 같은 특징점
2. 구별성 (Distinctiveness): 서로 다른 특징점은 구별 가능
3. 불변성 (Invariance): 회전, 크기, 조명 변화에 강건
4. 정확성 (Accuracy): 정확한 위치 검출

특징점의 종류:
┌─────────────────────────────────────────────────────────────┐
  코너 (Corner)              블롭 (Blob)                      
                                                             
       ┌──────              ●●●●●                            
                           ●●●●●●●                          
    ───┘                    ●●●●●●●●                         
                            ●●●●●●●                          
    방향으로 변화           ●●●●●                            
                          특정 크기의 영역                     
└─────────────────────────────────────────────────────────────┘

특징점 검출 파이프라인

1. 특징점 검출 (Detection)
   - 이미지에서 키포인트 위치 찾기
   - Harris, FAST, SIFT, ORB 
         
         
2. 특징점 기술 (Description)
   -  키포인트 주변의 특징 벡터 생성
   - SIFT descriptor, ORB descriptor, BRIEF 
         
         
3. 특징점 매칭 (Matching)
   - 다른 이미지의 디스크립터와 비교
   - BFMatcher, FLANN 

검출기 비교

┌────────────────┬───────────┬───────────┬───────────┬──────────┐
│ 알고리즘       │ 속도      │ 회전 불변 │ 크기 불변 │ 특허     │
├────────────────┼───────────┼───────────┼───────────┼──────────┤
│ Harris         │ 빠름      │ ○         │ ✗         │ 없음     │
│ FAST           │ 매우 빠름 │ ✗         │ ✗         │ 없음     │
│ SIFT           │ 느림      │ ○         │ ○         │ 만료     │
│ SURF           │ 보통      │ ○         │ ○         │ 있음     │
│ ORB            │ 빠름      │ ○         │ ○         │ 없음     │
│ AKAZE          │ 보통      │ ○         │ ○         │ 없음     │
└────────────────┴───────────┴───────────┴───────────┴──────────┘

2. 코너 검출 - Harris

개념

Harris 코너 검출:
이미지 패치를 이동시켰을  밝기 변화 분석

- 평탄 영역: 모든 방향으로 변화 없음
- 엣지: 엣지 방향은 변화 없음, 수직 방향은 변화 
- 코너: 모든 방향으로 변화 

자기상관 행렬 M:
M = Σ [Ix²    IxIy]
    [IxIy   Iy² ]

Ix, Iy: x, y 방향 미분

코너 응답 함수:
R = det(M) - k × (trace(M))²
R = λ1×λ2 - k(λ1 + λ2)²

- R > threshold: 코너
- R  0: 평탄
- R < 0: 엣지

cv2.cornerHarris()

import cv2
import numpy as np

def harris_corner_detection(image_path):
    """Harris 코너 검출"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = np.float32(gray)

    # Harris 코너 검출
    dst = cv2.cornerHarris(
        gray,
        blockSize=2,     # 이웃 크기
        ksize=3,         # Sobel 커널 크기
        k=0.04           # Harris 파라미터
    )

    # 결과 팽창 (코너 강조)
    dst = cv2.dilate(dst, None)

    # 임계값 이상의 점을 코너로 표시
    result = img.copy()
    result[dst > 0.01 * dst.max()] = [0, 0, 255]

    cv2.imshow('Harris Corners', result)
    cv2.waitKey(0)

    return dst

harris_corner_detection('chessboard.jpg')

서브픽셀 정확도

import cv2
import numpy as np

def harris_subpixel(image_path):
    """서브픽셀 정확도의 Harris 코너"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray_float = np.float32(gray)

    # Harris 코너
    dst = cv2.cornerHarris(gray_float, 2, 3, 0.04)

    # 코너 위치 추출
    dst = cv2.dilate(dst, None)
    ret, dst_thresh = cv2.threshold(dst, 0.01 * dst.max(), 255, 0)
    dst_thresh = np.uint8(dst_thresh)

    # 연결 요소로 코너 중심 찾기
    ret, labels, stats, centroids = cv2.connectedComponentsWithStats(dst_thresh)

    # 서브픽셀 정밀도로 개선
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.001)
    corners = cv2.cornerSubPix(
        gray,
        np.float32(centroids),
        (5, 5),      # 윈도우 크기
        (-1, -1),    # 제로 존
        criteria
    )

    result = img.copy()
    for i, corner in enumerate(corners):
        x, y = corner.ravel()
        if i == 0:  # 첫 번째는 배경
            continue
        cv2.circle(result, (int(x), int(y)), 5, (0, 255, 0), -1)

    cv2.imshow('SubPixel Corners', result)
    cv2.waitKey(0)

    return corners

harris_subpixel('chessboard.jpg')

3. 좋은 특징점 - goodFeaturesToTrack

cv2.goodFeaturesToTrack()

Shi-Tomasi 코너 검출 (Harris 개선):
R = min(λ1, λ2)

Harris보다 더 안정적인 코너 검출
→ 광학 흐름(Optical Flow) 추적에 적합
import cv2
import numpy as np

def good_features_demo(image_path):
    """좋은 특징점 검출"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 좋은 특징점 검출
    corners = cv2.goodFeaturesToTrack(
        gray,
        maxCorners=100,     # 최대 코너 수
        qualityLevel=0.01,  # 품질 수준 (최대 응답의 비율)
        minDistance=10,     # 코너 간 최소 거리
        blockSize=3,        # 이웃 크기
        useHarrisDetector=False,  # Shi-Tomasi 사용
        k=0.04              # Harris 파라미터 (Harris 사용시)
    )

    result = img.copy()

    if corners is not None:
        corners = np.int_(corners)
        for corner in corners:
            x, y = corner.ravel()
            cv2.circle(result, (x, y), 5, (0, 255, 0), -1)

        print(f"검출된 코너 수: {len(corners)}")

    cv2.imshow('Good Features', result)
    cv2.waitKey(0)

    return corners

good_features_demo('building.jpg')

마스크를 이용한 영역 제한

import cv2
import numpy as np

def features_with_mask(image_path):
    """특정 영역에서만 특징점 검출"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    h, w = gray.shape

    # ROI 마스크 생성 (중앙 영역만)
    mask = np.zeros((h, w), dtype=np.uint8)
    cv2.rectangle(mask, (w//4, h//4), (3*w//4, 3*h//4), 255, -1)

    # 마스크 영역에서만 특징점 검출
    corners = cv2.goodFeaturesToTrack(
        gray,
        maxCorners=50,
        qualityLevel=0.01,
        minDistance=10,
        mask=mask
    )

    result = img.copy()

    # 마스크 영역 표시
    cv2.rectangle(result, (w//4, h//4), (3*w//4, 3*h//4), (128, 128, 128), 2)

    if corners is not None:
        for corner in corners:
            x, y = corner.ravel()
            cv2.circle(result, (int(x), int(y)), 5, (0, 255, 0), -1)

    cv2.imshow('Features with Mask', result)
    cv2.waitKey(0)

features_with_mask('scene.jpg')

4. FAST 검출기

개념

FAST (Features from Accelerated Segment Test):
매우 빠른 코너 검출 알고리즘

원리:
중심 픽셀 P 주변의 원형 패턴(16픽셀) 검사

        1  2  3
     16           4
   15               5
  14        P        6
   13               7
     12          8
        11 10 9

판단 기준 (N=12):
- 연속된 N개 픽셀이 P보다 밝으면: 코너
- 연속된 N개 픽셀이 P보다 어두우면: 코너

특징:
- 매우 빠름 (실시간 처리)
- 회전 불변성 없음
- 크기 불변성 없음
- 비최대 억제(NMS)로 다중 검출 방지

cv2.FastFeatureDetector

import cv2
import numpy as np

def fast_detection(image_path):
    """FAST 특징점 검출"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # FAST 검출기 생성
    fast = cv2.FastFeatureDetector_create(
        threshold=20,           # 밝기 임계값
        nonmaxSuppression=True  # 비최대 억제
    )

    # 특징점 검출
    keypoints = fast.detect(gray, None)

    # 결과 그리기
    result = cv2.drawKeypoints(
        img, keypoints, None,
        color=(0, 255, 0),
        flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
    )

    print(f"검출된 특징점 수: {len(keypoints)}")

    cv2.imshow('FAST', result)
    cv2.waitKey(0)

    return keypoints

fast_detection('building.jpg')

FAST 파라미터 비교

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

def compare_fast_thresholds(image_path):
    """FAST 임계값 비교"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    thresholds = [10, 20, 30, 50]

    fig, axes = plt.subplots(2, 2, figsize=(12, 10))
    axes = axes.flatten()

    for ax, thresh in zip(axes, thresholds):
        fast = cv2.FastFeatureDetector_create(
            threshold=thresh,
            nonmaxSuppression=True
        )
        kps = fast.detect(gray, None)
        result = cv2.drawKeypoints(img, kps, None, color=(0, 255, 0))

        ax.imshow(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
        ax.set_title(f'Threshold={thresh}, Points={len(kps)}')
        ax.axis('off')

    plt.tight_layout()
    plt.show()

compare_fast_thresholds('building.jpg')

5. SIFT 검출기

개념

SIFT (Scale-Invariant Feature Transform):
크기와 회전에 불변한 특징점 검출  기술

단계:
1. 스케일 공간 극값 검출 (DoG: Difference of Gaussians)
2. 키포인트 정밀화 (서브픽셀 정확도, 엣지 제거)
3. 방향 할당 (그래디언트 히스토그램)
4. 디스크립터 계산 (4x4 그리드 x 8방향 = 128차원)

스케일 공간:
┌─────────────────────────────────────────────────┐
  Octave 0    Octave 1    Octave 2              
  ┌───────┐   ┌─────┐    ┌───┐                  
   σ=1.6    σ=1.6   │σ=1.6   스케일별     
   σ=2.0    σ=2.0   │σ=2.0     가우시안    
   σ=2.5    σ=2.5   │σ=2.5     블러        
   σ=3.2    σ=3.2   │σ=3.2                 
  └───────┘   └─────┘    └───┘                  
  원본 크기    1/2 축소   1/4 축소              
└─────────────────────────────────────────────────┘

DoG (Difference of Gaussians):
D(x, y, σ) = L(x, y, ) - L(x, y, σ)
 인접한 스케일 간의 차이로 블롭 검출

cv2.SIFT_create()

import cv2
import numpy as np

def sift_detection(image_path):
    """SIFT 특징점 검출"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # SIFT 검출기 생성
    sift = cv2.SIFT_create(
        nfeatures=0,          # 최대 특징점 수 (0=무제한)
        nOctaveLayers=3,      # 옥타브당 레이어 수
        contrastThreshold=0.04,  # 대비 임계값
        edgeThreshold=10,     # 엣지 임계값
        sigma=1.6             # 초기 가우시안 시그마
    )

    # 특징점과 디스크립터 계산
    keypoints, descriptors = sift.detectAndCompute(gray, None)

    # 결과 그리기
    result = cv2.drawKeypoints(
        img, keypoints, None,
        flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
    )

    print(f"검출된 특징점 수: {len(keypoints)}")
    if descriptors is not None:
        print(f"디스크립터 크기: {descriptors.shape}")

    cv2.imshow('SIFT', result)
    cv2.waitKey(0)

    return keypoints, descriptors

kps, descs = sift_detection('object.jpg')

SIFT 키포인트 분석

import cv2
import numpy as np

def analyze_sift_keypoints(image_path):
    """SIFT 키포인트 상세 분석"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    sift = cv2.SIFT_create()
    keypoints, descriptors = sift.detectAndCompute(gray, None)

    print("SIFT 키포인트 분석:")
    print("-" * 50)

    # 키포인트 속성
    for i, kp in enumerate(keypoints[:5]):
        print(f"키포인트 {i}:")
        print(f"  위치 (x, y): ({kp.pt[0]:.1f}, {kp.pt[1]:.1f})")
        print(f"  크기 (scale): {kp.size:.1f}")
        print(f"  방향 (angle): {kp.angle:.1f}도")
        print(f"  응답 (response): {kp.response:.4f}")
        print(f"  옥타브: {kp.octave}")

    # 스케일 분포
    scales = [kp.size for kp in keypoints]
    print(f"\n스케일 범위: {min(scales):.1f} ~ {max(scales):.1f}")

    # 디스크립터 분석
    if descriptors is not None:
        print(f"\n디스크립터:")
        print(f"  차원: {descriptors.shape[1]}")
        print(f"  값 범위: {descriptors.min():.1f} ~ {descriptors.max():.1f}")

analyze_sift_keypoints('object.jpg')

6. ORB 검출기

개념

ORB (Oriented FAST and Rotated BRIEF):
FAST + BRIEF의 개선 버전, 특허 무료

구성:
1. oFAST: 방향 정보가 추가된 FAST
   - 회전 불변성을 위해 방향(orientation) 계산
   - 이미지 피라미드로 스케일 불변성

2. rBRIEF: 회전된 BRIEF
   - BRIEF: 바이너리 디스크립터 (256비트)
   - 학습된 비교 패턴으로 구별력 향상
   - Hamming 거리로 빠른 매칭

특징:
- SIFT/SURF보다 훨씬 빠름
- 특허 무료
- 실시간 처리에 적합
- 바이너리 디스크립터  빠른 매칭

BRIEF 디스크립터:
패치   점의 밝기 비교
τ(P; x, y) = { 1 if P(x) < P(y)
             { 0 otherwise
 n개 비교로 n비트 바이너리 문자열

cv2.ORB_create()

import cv2
import numpy as np

def orb_detection(image_path):
    """ORB 특징점 검출"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # ORB 검출기 생성
    orb = cv2.ORB_create(
        nfeatures=500,        # 최대 특징점 수
        scaleFactor=1.2,      # 피라미드 스케일 팩터
        nlevels=8,            # 피라미드 레벨 수
        edgeThreshold=31,     # 엣지 임계값
        firstLevel=0,         # 첫 피라미드 레벨
        WTA_K=2,              # BRIEF에서 비교할 점 수 (2, 3, 4)
        scoreType=cv2.ORB_HARRIS_SCORE,  # 점수 유형
        patchSize=31,         # BRIEF 패치 크기
        fastThreshold=20      # FAST 임계값
    )

    # 특징점과 디스크립터 계산
    keypoints, descriptors = orb.detectAndCompute(gray, None)

    # 결과 그리기
    result = cv2.drawKeypoints(
        img, keypoints, None,
        color=(0, 255, 0),
        flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
    )

    print(f"검출된 특징점 수: {len(keypoints)}")
    if descriptors is not None:
        print(f"디스크립터 크기: {descriptors.shape}")
        print(f"디스크립터 타입: {descriptors.dtype}")  # uint8 (바이너리)

    cv2.imshow('ORB', result)
    cv2.waitKey(0)

    return keypoints, descriptors

kps, descs = orb_detection('object.jpg')

SIFT vs ORB 비교

import cv2
import numpy as np
import time
import matplotlib.pyplot as plt

def compare_sift_orb(image_path):
    """SIFT와 ORB 성능 비교"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # SIFT
    sift = cv2.SIFT_create()
    start = time.time()
    kps_sift, descs_sift = sift.detectAndCompute(gray, None)
    sift_time = time.time() - start

    # ORB
    orb = cv2.ORB_create(nfeatures=len(kps_sift))
    start = time.time()
    kps_orb, descs_orb = orb.detectAndCompute(gray, None)
    orb_time = time.time() - start

    print("성능 비교:")
    print("-" * 50)
    print(f"SIFT: {len(kps_sift)} points, {sift_time*1000:.1f}ms")
    print(f"ORB:  {len(kps_orb)} points, {orb_time*1000:.1f}ms")
    print(f"속도 비율: ORB가 {sift_time/orb_time:.1f}배 빠름")

    if descs_sift is not None and descs_orb is not None:
        print(f"\nSIFT 디스크립터: {descs_sift.shape}, {descs_sift.dtype}")
        print(f"ORB 디스크립터: {descs_orb.shape}, {descs_orb.dtype}")

    # 시각화
    result_sift = cv2.drawKeypoints(img, kps_sift, None, color=(0, 255, 0))
    result_orb = cv2.drawKeypoints(img, kps_orb, None, color=(0, 0, 255))

    fig, axes = plt.subplots(1, 2, figsize=(14, 6))

    axes[0].imshow(cv2.cvtColor(result_sift, cv2.COLOR_BGR2RGB))
    axes[0].set_title(f'SIFT: {len(kps_sift)} points')
    axes[0].axis('off')

    axes[1].imshow(cv2.cvtColor(result_orb, cv2.COLOR_BGR2RGB))
    axes[1].set_title(f'ORB: {len(kps_orb)} points')
    axes[1].axis('off')

    plt.tight_layout()
    plt.show()

compare_sift_orb('object.jpg')

7. 키포인트와 디스크립터

KeyPoint 구조

import cv2
import numpy as np

def keypoint_structure():
    """키포인트 구조 이해"""
    # 키포인트 수동 생성
    kp = cv2.KeyPoint(
        x=100.5,        # x 좌표
        y=200.5,        # y 좌표
        size=20,        # 특징점 크기 (직경)
        angle=45,       # 방향 (도)
        response=0.8,   # 응답 강도
        octave=0,       # 옥타브 (스케일)
        class_id=-1     # 분류 ID
    )

    print("KeyPoint 속성:")
    print(f"  위치: ({kp.pt[0]}, {kp.pt[1]})")
    print(f"  크기: {kp.size}")
    print(f"  방향: {kp.angle}")
    print(f"  응답: {kp.response}")
    print(f"  옥타브: {kp.octave}")
    print(f"  클래스 ID: {kp.class_id}")

keypoint_structure()

디스크립터 이해

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

def visualize_descriptors(image_path):
    """디스크립터 시각화"""
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

    # SIFT 디스크립터 (128차원 float)
    sift = cv2.SIFT_create()
    kps_sift, descs_sift = sift.detectAndCompute(img, None)

    # ORB 디스크립터 (32바이트 = 256비트)
    orb = cv2.ORB_create()
    kps_orb, descs_orb = orb.detectAndCompute(img, None)

    fig, axes = plt.subplots(2, 2, figsize=(14, 10))

    # SIFT 디스크립터 히스토그램
    if descs_sift is not None and len(descs_sift) > 0:
        axes[0, 0].bar(range(128), descs_sift[0])
        axes[0, 0].set_title('SIFT Descriptor (128D)')
        axes[0, 0].set_xlabel('Dimension')

        axes[0, 1].imshow(descs_sift[:50], aspect='auto', cmap='viridis')
        axes[0, 1].set_title('SIFT Descriptors (first 50)')
        axes[0, 1].set_xlabel('Dimension')
        axes[0, 1].set_ylabel('Keypoint')

    # ORB 디스크립터 (바이너리)
    if descs_orb is not None and len(descs_orb) > 0:
        # 바이너리를 비트로 변환
        bits = np.unpackbits(descs_orb[0])
        axes[1, 0].bar(range(256), bits)
        axes[1, 0].set_title('ORB Descriptor (256 bits)')
        axes[1, 0].set_xlabel('Bit')

        # 여러 디스크립터
        bits_all = np.unpackbits(descs_orb[:50], axis=1)
        axes[1, 1].imshow(bits_all, aspect='auto', cmap='binary')
        axes[1, 1].set_title('ORB Descriptors (first 50)')
        axes[1, 1].set_xlabel('Bit')
        axes[1, 1].set_ylabel('Keypoint')

    plt.tight_layout()
    plt.show()

visualize_descriptors('object.jpg')

다양한 검출기 사용

import cv2
import numpy as np

def use_various_detectors(image_path):
    """다양한 특징점 검출기 사용"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    detectors = {
        'SIFT': cv2.SIFT_create(),
        'ORB': cv2.ORB_create(),
        'BRISK': cv2.BRISK_create(),
        'AKAZE': cv2.AKAZE_create(),
        # 'KAZE': cv2.KAZE_create(),  # 느림
    }

    results = {}

    for name, detector in detectors.items():
        kps, descs = detector.detectAndCompute(gray, None)
        results[name] = {
            'keypoints': kps,
            'descriptors': descs,
            'count': len(kps),
            'desc_size': descs.shape[1] if descs is not None else 0
        }

        print(f"{name}:")
        print(f"  특징점 수: {len(kps)}")
        if descs is not None:
            print(f"  디스크립터: {descs.shape}, {descs.dtype}")
        print()

    return results

results = use_various_detectors('object.jpg')

8. 연습 문제

문제 1: 최적 특징점 선택

이미지에서 가장 강한 50개의 특징점만 선택하세요.

정답 코드
import cv2
import numpy as np

def select_best_keypoints(image_path, n=50):
    """가장 강한 N개의 특징점 선택"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # ORB로 특징점 검출 (많이 검출)
    orb = cv2.ORB_create(nfeatures=500)
    keypoints, descriptors = orb.detectAndCompute(gray, None)

    # 응답 강도로 정렬
    keypoints_sorted = sorted(keypoints, key=lambda x: x.response, reverse=True)

    # 상위 N개 선택
    best_keypoints = keypoints_sorted[:n]

    # 해당 디스크립터도 선택
    indices = [keypoints.index(kp) for kp in best_keypoints]
    best_descriptors = descriptors[indices] if descriptors is not None else None

    result = cv2.drawKeypoints(
        img, best_keypoints, None,
        color=(0, 255, 0),
        flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
    )

    cv2.imshow(f'Best {n} Keypoints', result)
    cv2.waitKey(0)

    return best_keypoints, best_descriptors

kps, descs = select_best_keypoints('building.jpg', n=50)

문제 2: 균일 분포 특징점

이미지를 그리드로 나누어 각 셀에서 하나씩 특징점을 선택하세요.

정답 코드
import cv2
import numpy as np

def uniform_keypoints(image_path, grid_size=(8, 8)):
    """그리드별로 균일하게 특징점 선택"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    h, w = gray.shape

    orb = cv2.ORB_create(nfeatures=1000)
    keypoints, descriptors = orb.detectAndCompute(gray, None)

    # 그리드 크기 계산
    cell_h = h // grid_size[0]
    cell_w = w // grid_size[1]

    # 각 셀별로 가장 강한 특징점 선택
    selected_kps = []
    selected_indices = []

    for row in range(grid_size[0]):
        for col in range(grid_size[1]):
            # 셀 영역
            x_min = col * cell_w
            x_max = (col + 1) * cell_w
            y_min = row * cell_h
            y_max = (row + 1) * cell_h

            # 셀 내의 특징점 필터링
            cell_kps = []
            for i, kp in enumerate(keypoints):
                if x_min <= kp.pt[0] < x_max and y_min <= kp.pt[1] < y_max:
                    cell_kps.append((i, kp))

            if cell_kps:
                # 가장 강한 특징점 선택
                best_idx, best_kp = max(cell_kps, key=lambda x: x[1].response)
                selected_kps.append(best_kp)
                selected_indices.append(best_idx)

    # 디스크립터
    selected_descs = descriptors[selected_indices] if descriptors is not None else None

    result = cv2.drawKeypoints(
        img, selected_kps, None,
        color=(0, 255, 0),
        flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
    )

    # 그리드 표시
    for row in range(1, grid_size[0]):
        cv2.line(result, (0, row * cell_h), (w, row * cell_h), (128, 128, 128), 1)
    for col in range(1, grid_size[1]):
        cv2.line(result, (col * cell_w, 0), (col * cell_w, h), (128, 128, 128), 1)

    cv2.imshow('Uniform Keypoints', result)
    cv2.waitKey(0)

    return selected_kps, selected_descs

kps, descs = uniform_keypoints('building.jpg', grid_size=(6, 8))

문제 3: 회전 불변성 테스트

이미지를 회전시킨 후 동일한 특징점이 검출되는지 확인하세요.

정답 코드
import cv2
import numpy as np

def test_rotation_invariance(image_path, angle=45):
    """회전 불변성 테스트"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    h, w = gray.shape

    # 이미지 회전
    center = (w // 2, h // 2)
    M = cv2.getRotationMatrix2D(center, angle, 1.0)
    rotated = cv2.warpAffine(gray, M, (w, h))

    # SIFT (회전 불변)
    sift = cv2.SIFT_create(nfeatures=100)

    kps1, descs1 = sift.detectAndCompute(gray, None)
    kps2, descs2 = sift.detectAndCompute(rotated, None)

    # 특징점 매칭
    bf = cv2.BFMatcher()
    matches = bf.knnMatch(descs1, descs2, k=2)

    # 좋은 매칭 필터링 (Lowe's ratio test)
    good_matches = []
    for m, n in matches:
        if m.distance < 0.75 * n.distance:
            good_matches.append(m)

    print(f"원본 특징점: {len(kps1)}")
    print(f"회전 이미지 특징점: {len(kps2)}")
    print(f"매칭된 특징점: {len(good_matches)}")
    print(f"매칭률: {len(good_matches) / len(kps1) * 100:.1f}%")

    # 시각화
    result = cv2.drawMatches(
        gray, kps1, rotated, kps2,
        good_matches, None,
        flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS
    )

    cv2.imshow('Rotation Invariance Test', result)
    cv2.waitKey(0)

test_rotation_invariance('object.jpg', angle=30)

추천 문제

난이도 주제 설명
기본 검출 Harris, FAST, ORB 비교
⭐⭐ 성능 비교 검출 속도와 개수 비교
⭐⭐ 파라미터 튜닝 최적 파라미터 찾기
⭐⭐⭐ 스케일 불변성 크기 변화에 대한 테스트
⭐⭐⭐ 실시간 검출 웹캠으로 실시간 특징점 표시

다음 단계


참고 자료

to navigate between lessons