V2 로직(OpenCV 활용) - 100-hours-a-week/5-yeosa-wiki GitHub Wiki
1. 기존 로직의 문제점
문제점 | 설명 |
---|---|
1. 의미적 유사성까지 포함됨 | CLIP은 의미 기반 임베딩이므로, 내용이 다르더라도 비슷한 상황/구도/개념이라면 유사하게 판단됨예: 다른 장소에서 책 읽는 사진 → 중복으로 분류될 수 있음 |
2. 외형이 유사한 사진 오탐지 | 배경, 조명, 인물 포즈가 유사한 다른 장면도 클러스터로 묶임 |
3. 정작 진짜 중복은 누락 가능 | 예를 들어 해상도만 다른 사진, 확대·크롭된 동일 사진이 유사도 낮게 나올 수 있음 |
4. 분류 기준 모호함 | 의미 중심이므로, ‘진짜 중복’과 ‘비슷한 사진’의 경계가 불명확 |
2. 개선된 방식: pHash + Hamming Distance
a. pHash (Perceptual Hash)란?
가. 특징
-
이미지의 시각적 특성을 정수형 hash 값(64bit 등)으로 표현하는 알고리즘
-
주요 특징:
- 이미지의 구조, 윤곽, 명암 등 perceptual 정보만 유지
- 해상도, 색상, 포맷 변화에 강건
- 동일 이미지의 hash는 거의 같고, 유사 이미지도 비슷하게 생성됨
→ pHash는 "이 이미지가 시각적으로 같은가?"를 판단하는 데 최적화된 fingerprint 방식
나. 계산 방식
[ pHash는 이미지의 시각적 구조(형태, 명암, 윤곽 등) 을 압축해 64비트(또는 그 이상)의 해시를 생성 ]
- 이미지 전처리
- 이미지를 흑백(그레이스케일) 로 변환
- 크기를 고정된 크기 (예: 32×32)로 리사이징
- 주파수 변환 (DCT)
- Discrete Cosine Transform(이산 코사인 변환)을 적용
- 이미지의 저주파 성분(전체적인 윤곽과 구조)을 추출
- DCT 결과의 상위 8×8 블록 선택
- DC 성분(맨 왼쪽 상단 1개)을 제외한 상위 8×8의 DCT 계수만 추출
- 즉, 총 64개의 숫자 → hash의 각 비트로 사용
- 평균값과 비교해 비트화
- 추출한 64개의 값의 평균을 구함
- 각 값이 평균보다 크면 1, 작으면 0 → 64비트 해시 생성
b. Hamming Distance란?
-
두 binary 벡터 간 서로 다른 비트 수를 측정하는 거리 지표
-
예시:
hash1 = 10110100 hash2 = 10111110 → ↑ ↑ → 해밍 거리 = 2
-
0
에 가까울수록 거의 동일한 이미지 -
실무에서는 10 이하를 중복 기준으로 삼는 경우가 많음 (64bit hash 기준)
3. 개선된 로직 구조
a. 이미지 로딩
images = await image_loader.load_images(image_refs, 'GRAY')
- 입력 이미지들을 회색조로 로딩 (pHash는 회색 이미지 기반이 효과적)
b. Perceptual Hash (pHash) 계산
def compute_hashes(images: List[np.ndarray]) -> np.ndarray:
"""
입력 이미지 리스트에 대해 OpenCV pHash를 계산합니다.
Args:
images (List[np.ndarray]): OpenCV 이미지 배열 리스트
Returns:
np.ndarray: shape (N, 64) pHash 결과
"""
hasher = cv2.img_hash.PHash_create()
return np.vstack([hasher.compute(img) for img in images])
hashes = compute_hashes(images) # → shape: (N, 64)
- OpenCV의
cv2.img_hash.PHash_create()
사용 - 각 이미지의 64비트 perceptual hash 벡터 계산
c. 해밍 거리 행렬 생성
def compute_hamming_matrix(hashes: np.ndarray) -> np.ndarray:
"""
이미지 해시 간 해밍 거리 행렬을 계산합니다.
Args:
hashes (np.ndarray): shape (N, 64) 해시 배열
Returns:
np.ndarray: shape (N, N) 해밍 거리 행렬
"""
expanded = np.expand_dims(hashes, axis=1)
xor_matrix = np.bitwise_xor(expanded, hashes)
hamming_matrix = np.unpackbits(xor_matrix, axis=-1).sum(axis=-1)
return hamming_matrix.astype(np.uint8)
hamming_matrix = compute_hamming_matrix(hashes) # → shape: (N, N)
- 두 hash 간 비트 단위 XOR →
np.unpackbits
로 해밍 거리 계산
d. DBSCAN 클러스터링
def cluster_with_dbscan(
hamming_matrix: np.ndarray, eps: int = 10, min_samples: int = 2
) -> np.ndarray:
"""
해밍 거리 기반 DBSCAN 클러스터링을 수행합니다.
Args:
hamming_matrix (np.ndarray): 사전 계산된 거리 행렬 (N, N)
eps (int, optional): 최대 해밍 거리 임계값. Defaults to 10.
min_samples (int, optional): 최소 클러스터 크기. Defaults to 2.
Returns:
np.ndarray: shape (N,) 클러스터 레이블 (-1은 노이즈)
"""
clustering = DBSCAN(
metric="precomputed", eps=eps, min_samples=min_samples
)
return clustering.fit_predict(hamming_matrix)
labels = cluster_with_dbscan(hamming_matrix, eps=10, min_samples=2)
eps=10
이하는 거의 동일한 이미지로 간주min_samples=2
: 최소 2장 이상이 있어야 클러스터로 인정
e. 결과 그룹핑
def group_by_labels(labels: np.ndarray, file_names: List[str]) -> List[List[str]]:
"""
DBSCAN 레이블을 기반으로 파일명을 클러스터별로 그룹화합니다.
Args:
labels (np.ndarray): DBSCAN 결과 레이블 (N,)
file_names (List[str]): 파일명 리스트
Returns:
List[List[str]]: 유사 이미지 그룹 목록
"""
groups = {}
for idx, label in enumerate(labels):
if label == -1:
continue # 노이즈 제외
groups.setdefault(label, []).append(file_names[idx])
return list(groups.values())
groups = group_by_labels(labels, image_refs)
- 클러스터 ID 기준으로 중복 이미지 그룹 생성
4. 기존 로직 vs 개선 로직 비교 요약
항목 | 기존 방식 (CLIP 기반) | 개선 방식 (pHash 기반) |
---|---|---|
유사도 기준 | CLIP 임베딩 간 cosine similarity | pHash 간 Hamming distance |
주요 특징 | 의미 기반 유사도 (semantic similarity) | 이미지 구조 기반 유사도 (visual similarity) |
단점 | 다른 장면이라도 ‘비슷한 의미’면 묶임 (ex: 두 사람이 각기 다른 장소에서 책을 읽는 장면) | - |
장점 | 조명, 색상, 배경이 조금 달라도 같은 개념이면 유사함 | 픽셀 기반이므로 ‘실제 중복 사진’ 판별에 적합 |
클러스터링 | DBSCAN (cosine distance 기반) | DBSCAN (Hamming distance 기반) |
입력 | 이미지 임베딩 (torch.Tensor) | OpenCV 이미지 (np.ndarray, GRAY) |
성능 | 의미적 유사 이미지 포함 위험 있음 | 오직 ‘거의 동일한 이미지’만 묶음 |
5. 개선 효과 요약
효과 | 설명 |
---|---|
‘진짜 중복 사진’만 탐지 | 의미적 유사 이미지가 아니라 시각적으로 동일한 이미지만 묶임 |
빠른 처리 속도 | pHash는 경량 연산으로 GPU 없이도 빠르게 처리 가능 |
노이즈에 강함 | 색상·밝기 변화, 경미한 회전 등에도 동일한 hash 출력 가능 |
DBSCAN 기반 확장성 유지 | threshold 조정 (eps )만으로 유연한 판별 가능 |