388 lines
15 KiB
Python
388 lines
15 KiB
Python
import cv2
|
|
import numpy as np
|
|
from PyQt5.QtCore import QThread, pyqtSignal
|
|
from PyQt5.QtGui import QImage, QPixmap
|
|
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton, QFileDialog, QLineEdit
|
|
import requests
|
|
from PIL import Image
|
|
import os
|
|
import tempfile
|
|
import logging
|
|
|
|
# 로거 인스턴스 가져오기
|
|
logger = logging.getLogger('default_logger')
|
|
|
|
class ImageThread(QThread):
|
|
finished = pyqtSignal(list)
|
|
|
|
def __init__(self, original_img_path, target_img_paths):
|
|
super().__init__()
|
|
self.original_img_path = original_img_path
|
|
self.target_img_paths = target_img_paths
|
|
|
|
def run(self):
|
|
# 원본 이미지 읽기
|
|
original_img = cv2.imread(self.original_img_path)
|
|
|
|
# 대상 이미지 연관도 분석
|
|
similarities = []
|
|
for target_img_path in self.target_img_paths:
|
|
target_img = cv2.imread(target_img_path)
|
|
similarity = self.calculate_similarity_with_ORB(original_img, target_img)
|
|
# similarity = self.calculate_similarity_with_L2(original_img, target_img)
|
|
# similarity = self.calculate_similarity_new(original_img, target_img)
|
|
similarities.append(similarity)
|
|
logger.debug(f"{target_img_path} 의 연관도 : {similarity}")
|
|
|
|
# 연관도가 높은 순으로 정렬 및 결과 추출
|
|
# sorted_indices = np.argsort(similarities)[::-1]
|
|
sorted_indices = np.argsort(similarities)
|
|
sorted_target_img_paths = [self.target_img_paths[i] for i in sorted_indices]
|
|
|
|
self.finished.emit(sorted_target_img_paths)
|
|
|
|
def calculate_similarity_with_L2(img1, img2, method="L2"):
|
|
# 이미지 전처리
|
|
img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
|
|
img1 = cv2.GaussianBlur(img1, (5, 5), 0)
|
|
img1 = cv2.equalizeHist(img1)
|
|
|
|
img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
|
|
img2 = cv2.GaussianBlur(img2, (5, 5), 0)
|
|
img2 = cv2.equalizeHist(img2)
|
|
|
|
# 유사도 계산
|
|
if method == "L1":
|
|
similarity = cv2.norm(img1, img2, cv2.NORM_L1)
|
|
elif method == "L2":
|
|
similarity = cv2.norm(img1, img2, cv2.NORM_L2)
|
|
elif method == "cosine":
|
|
similarity = cv2.compareHist(img1, img2, cv2.CV_COMP_COSINE)
|
|
elif method == "jaccard":
|
|
similarity = cv2.compareHist(img1, img2, cv2.CV_COMP_JACCARD)
|
|
else:
|
|
raise ValueError("Invalid similarity method")
|
|
|
|
return similarity
|
|
|
|
|
|
def calculate_similarity_with_ORB(self, img1, img2):
|
|
# 이미지 특징 추출 (ORB)
|
|
orb = cv2.ORB_create()
|
|
kp1, des1 = orb.detectAndCompute(img1, None)
|
|
kp2, des2 = orb.detectAndCompute(img2, None)
|
|
|
|
# 특징 매칭 (BFMatcher)
|
|
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
|
|
matches = bf.knnMatch(des1, des2, k=2)
|
|
|
|
|
|
# # 좋은 매칭만 추출
|
|
# good_matches = [m for m in matches if m.distance < 0.55]
|
|
|
|
# 좋은 매칭 추출 (lowe's ratio test)
|
|
good_matches = []
|
|
if len(matches) > 0: # matches 리스트가 비어 있는지 확인
|
|
for m, n in matches:
|
|
if m.distance < 0.7 * n.distance:
|
|
good_matches.append(m)
|
|
logger.debug(f"good_matches : {good_matches}")
|
|
|
|
# 연관도 계산
|
|
if len(kp1) > 0 and len(kp2) > 0:
|
|
similarity = len(good_matches) / ((len(kp1) + len(kp2)) / 2)
|
|
else:
|
|
similarity = 0.0
|
|
else:
|
|
similarity = 0.0
|
|
logger.debug("No matches found.") # 매칭이 없는 경우 메시지 출력
|
|
|
|
logger.debug(f"similarity : {similarity}")
|
|
|
|
return similarity
|
|
|
|
def calculate_similarity_new(self, img1, img2):
|
|
# 이미지 전처리
|
|
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
|
|
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
|
|
gray1 = cv2.GaussianBlur(gray1, (5, 5), 0)
|
|
gray2 = cv2.GaussianBlur(gray2, (5, 5), 0)
|
|
gray1 = cv2.equalizeHist(gray1)
|
|
gray2 = cv2.equalizeHist(gray2)
|
|
|
|
# 히스토그램 Equalize 후 dtype을 32bit float으로 변환
|
|
gray1 = cv2.equalizeHist(gray1).astype(np.float32)
|
|
gray2 = cv2.equalizeHist(gray2).astype(np.float32)
|
|
|
|
# Hu 불변 모멘트 계산
|
|
moments1 = cv2.HuMoments(cv2.moments(gray1))
|
|
moments2 = cv2.HuMoments(cv2.moments(gray2))
|
|
|
|
# Hu 불변 모멘트 벡터 간의 L2 거리 계산
|
|
l2_distance = cv2.norm(moments1, moments2, cv2.NORM_L2)
|
|
|
|
# L2 거리 계산
|
|
# l2_distance = cv2.norm(gray1, gray2, cv2.NORM_L2)
|
|
|
|
# 코사인 유사도 계산
|
|
# cosine_similarity = cv2.compareHist(gray1, gray2, cv2.CV_COMP_COSINE)
|
|
cosine_similarity = cv2.compareHist(gray1, gray2, 3)
|
|
|
|
# Jaccard 지수 계산
|
|
# jaccard_index = cv2.compareHist(gray1, gray2, cv2.CV_COMP_JACCARD)
|
|
jaccard_index = cv2.compareHist(gray1, gray2, 3)
|
|
|
|
|
|
gray11 = cv2.cvtColor(gray1, cv2.COLOR_BGR2GRAY).astype('uint8')
|
|
gray22 = cv2.cvtColor(gray2, cv2.COLOR_BGR2GRAY).astype('uint8')
|
|
|
|
# SIFT 특징 추출 및 매칭
|
|
sift = cv2.SIFT_create()
|
|
kp1, des1 = sift.detectAndCompute(gray11, None)
|
|
kp2, des2 = sift.detectAndCompute(gray22, None)
|
|
|
|
|
|
# FLANN 매칭
|
|
flann = cv2.FlannBasedMatcher()
|
|
matches = flann.knnMatch(des1, des2, k=2)
|
|
|
|
# 좋은 매칭 추출 (Lowe's ratio test)
|
|
good_matches = []
|
|
for m, n in matches:
|
|
if m.distance < 0.8 * n.distance:
|
|
good_matches.append([m])
|
|
|
|
# 모양 기반 유사도 계산
|
|
shape_similarity = len(good_matches) / (len(kp1) + len(kp2)) / 2
|
|
|
|
# 최종 유사도 계산 (가중치 조절 가능)
|
|
similarity = (0.4 * l2_distance) + (0.2 * cosine_similarity) + (0.2 * jaccard_index) + (0.2 * shape_similarity)
|
|
return similarity
|
|
|
|
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.setWindowTitle("Image Similarity Analysis")
|
|
self.main_widget = QWidget()
|
|
self.setCentralWidget(self.main_widget)
|
|
|
|
# 레이아웃 설정
|
|
layout = QVBoxLayout()
|
|
self.main_widget.setLayout(layout)
|
|
|
|
ori_url = "https://img.danawa.com/prod_img/500000/800/949/img/9949800_1.jpg"
|
|
default_urls = ["https://m.hwangso114.com/web/product/big/202209/880f46bb32f942ec327140e0fe042afc.png", "https://ae01.alicdn.com/kf/S31b266a3d66a493d9e7f3a9dd4e25e49o.png"] # 기본값 리스트
|
|
|
|
# 원본 이미지 선택
|
|
self.original_img_label = QLabel("원본 이미지")
|
|
layout.addWidget(self.original_img_label)
|
|
self.original_img_path_edit = QLineEdit()
|
|
self.original_img_path_edit.setPlaceholderText("원본 이미지 경로를 입력하세요")
|
|
self.original_img_path_edit.setText(ori_url) # 기본값 설정
|
|
layout.addWidget(self.original_img_path_edit)
|
|
|
|
# 대상 이미지 선택
|
|
self.target_img_labels = []
|
|
self.target_img_path_edits = []
|
|
for i, default_url in enumerate(default_urls):
|
|
label = QLabel(f"대상 이미지 {i+1}")
|
|
layout.addWidget(label)
|
|
self.target_img_labels.append(label)
|
|
edit = QLineEdit()
|
|
edit.setPlaceholderText(f"대상 이미지 {i+1} 경로를 입력하세요")
|
|
layout.addWidget(edit)
|
|
edit.setText(default_url) # 기본값 설정
|
|
self.target_img_path_edits.append(edit)
|
|
|
|
# 비교 버튼
|
|
self.compare_button = QPushButton("비교")
|
|
self.compare_button.clicked.connect(self.compare_images)
|
|
layout.addWidget(self.compare_button)
|
|
|
|
# 팝업 창 설정
|
|
self.popup_window = QWidget()
|
|
self.popup_window.setWindowTitle("비교 결과")
|
|
self.popup_layout = QVBoxLayout()
|
|
self.popup_window.setLayout(self.popup_layout)
|
|
|
|
# 팝업 창에 원본 이미지 표시
|
|
self.original_img_popup_label = QLabel()
|
|
self.popup_layout.addWidget(self.original_img_popup_label)
|
|
|
|
# 팝업 창에 결과 표시
|
|
self.target_img_popup_labels = []
|
|
for i in range(3):
|
|
label = QLabel()
|
|
self.popup_layout.addWidget(label)
|
|
self.target_img_popup_labels.append(label)
|
|
|
|
|
|
def download_and_process_images(self, urls):
|
|
"""
|
|
URL 리스트에서 이미지를 다운받아 'img' 폴더에 저장합니다.
|
|
|
|
Args:
|
|
urls (list): 이미지 URL 리스트
|
|
|
|
Returns:
|
|
list: 로컬 파일 경로 리스트
|
|
"""
|
|
# 'img' 폴더 경로 확인 및 생성
|
|
img_folder_path = os.path.join(os.getcwd(), 'img')
|
|
if not os.path.exists(img_folder_path):
|
|
os.makedirs(img_folder_path)
|
|
|
|
local_paths = []
|
|
for i, url in enumerate(urls):
|
|
# if i == 0:
|
|
# # filename = "original.jpg"
|
|
# else:
|
|
filename = f"target{i}.jpg"
|
|
|
|
local_path = os.path.join(img_folder_path, filename)
|
|
|
|
# 이미지 다운로드
|
|
headers = {
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
|
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
|
"Accept-Language": "en-US,en;q=0.9",
|
|
"Accept-Encoding": "gzip, deflate, br",
|
|
"DNT": "1", # Do Not Track 요청 헤더 (사용자의 추적을 거부)
|
|
"Connection": "keep-alive",
|
|
"Upgrade-Insecure-Requests": "1", # https로의 업그레이드를 요청
|
|
"Cache-Control": "max-age=0", # 캐시된 콘텐츠를 재사용하지 않도록 요청
|
|
}
|
|
response = requests.get(url, headers=headers)
|
|
|
|
if response.status_code == 200:
|
|
with open(local_path, "wb") as f:
|
|
f.write(response.content)
|
|
|
|
# 이미지 처리 로직 (필요한 경우)
|
|
# 예: 썸네일 생성, 이미지 사이즈 조정 등
|
|
|
|
local_paths.append(local_path)
|
|
else:
|
|
logger.debug(f"Error downloading image: {url}")
|
|
|
|
return local_paths
|
|
|
|
def download_and_process_image(self, urls):
|
|
"""
|
|
URL 리스트에서 이미지를 다운받아 'img' 폴더에 저장합니다.
|
|
|
|
Args:
|
|
urls (list): 이미지 URL 리스트
|
|
|
|
Returns:
|
|
list: 로컬 파일 경로 리스트
|
|
"""
|
|
# 'img' 폴더 경로 확인 및 생성
|
|
img_folder_path = os.path.join(os.getcwd(), 'ori_img')
|
|
if not os.path.exists(img_folder_path):
|
|
os.makedirs(img_folder_path)
|
|
|
|
local_paths = []
|
|
for i, url in enumerate(urls):
|
|
if i == 0:
|
|
filename = "original.jpg"
|
|
|
|
local_path = os.path.join(img_folder_path, filename)
|
|
|
|
# 이미지 다운로드
|
|
headers = {
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
|
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
|
"Accept-Language": "en-US,en;q=0.9",
|
|
"Accept-Encoding": "gzip, deflate, br",
|
|
"DNT": "1", # Do Not Track 요청 헤더 (사용자의 추적을 거부)
|
|
"Connection": "keep-alive",
|
|
"Upgrade-Insecure-Requests": "1", # https로의 업그레이드를 요청
|
|
"Cache-Control": "max-age=0", # 캐시된 콘텐츠를 재사용하지 않도록 요청
|
|
}
|
|
response = requests.get(url, headers=headers)
|
|
|
|
if response.status_code == 200:
|
|
with open(local_path, "wb") as f:
|
|
f.write(response.content)
|
|
|
|
# 이미지 처리 로직 (필요한 경우)
|
|
# 예: 썸네일 생성, 이미지 사이즈 조정 등
|
|
|
|
local_paths.append(local_path)
|
|
else:
|
|
logger.debug(f"Error downloading image: {url}")
|
|
|
|
return local_paths
|
|
|
|
def compare_images(self):
|
|
"""
|
|
입력받은 URL 이미지들을 로컬에 저장하고, 비교 스레드를 시작합니다.
|
|
"""
|
|
# 입력 경로 확인
|
|
original_img_url = self.original_img_path_edit.text()
|
|
if not original_img_url:
|
|
return
|
|
|
|
# 원본 이미지 로컬 저장 및 경로
|
|
local_paths = self.download_and_process_image([original_img_url])
|
|
if not local_paths:
|
|
return
|
|
original_img_path = local_paths[0]
|
|
self.original_img_path = local_paths[0] # 클래스 변수 업데이트
|
|
|
|
# 대상 이미지 로컬 저장 및 경로 리스트
|
|
target_img_urls = [edit.text() for edit in self.target_img_path_edits]
|
|
target_img_paths = self.download_and_process_images(target_img_urls)
|
|
if not target_img_paths:
|
|
return
|
|
|
|
# 이미지 비교 스레드 시작
|
|
self.thread = ImageThread(original_img_path, target_img_paths)
|
|
self.thread.finished.connect(self.show_results)
|
|
self.thread.start()
|
|
|
|
def add_text_to_image(self, img, text):
|
|
font = cv2.FONT_HERSHEY_SIMPLEX
|
|
font_scale = 1
|
|
font_color = (255, 0, 0) # 빨간색
|
|
font_thickness = 2
|
|
cv2.putText(img, text, (10, 30), font, font_scale, font_color, font_thickness)
|
|
|
|
|
|
def show_results(self, sorted_target_img_paths):
|
|
# 원본 이미지 팝업
|
|
original_img = cv2.imread(self.original_img_path)
|
|
original_img = cv2.resize(original_img, (200, 200))
|
|
self.add_text_to_image(original_img, "Original") # 원본 이미지에 텍스트 추가
|
|
qimage = QImage(original_img.data, original_img.shape[1], original_img.shape[0], QImage.Format_RGB888).rgbSwapped()
|
|
pixmap = QPixmap.fromImage(qimage)
|
|
self.original_img_popup_label.setPixmap(pixmap)
|
|
|
|
# 대상 이미지 팝업 업데이트
|
|
for i, label in enumerate(self.target_img_popup_labels):
|
|
if i < len(sorted_target_img_paths):
|
|
target_img_path = sorted_target_img_paths[i]
|
|
target_img = cv2.imread(target_img_path)
|
|
target_img = cv2.resize(target_img, (200, 200))
|
|
self.add_text_to_image(target_img, f"Target {i+1}") # 대상 이미지에 텍스트 추가
|
|
qimage = QImage(target_img.data, target_img.shape[1], target_img.shape[0], QImage.Format_RGB888).rgbSwapped()
|
|
pixmap = QPixmap.fromImage(qimage)
|
|
label.setPixmap(pixmap)
|
|
else:
|
|
label.setPixmap(QPixmap()) # 이미지가 없는 경우 비워둠
|
|
|
|
|
|
# 팝업 창 보여주기
|
|
self.popup_window.show()
|
|
|
|
if __name__ == "__main__":
|
|
app = QApplication([])
|
|
window = MainWindow()
|
|
window.show()
|
|
app.exec_()
|