Average Hash
PIL(Pillow) 라이브러리
간단한 형식 인식하기
Average Hash란?
: 이미지를 비교 가능한 해시 값으로 나타낸 것
해시 함수 MD5, SHA256 등을 이용하면 데이터 값을 간단히 해시 값으로 변환할 수 있다.
그리고 이 해시 값을 기반으로 같은 데이터(완전히 동일한 바이너리)를 검출할 수 있다.
그러나,
이미지 데이터는 해상도 크기 조정, 색조 보정, JPEG/PNG 등 압축 형식 변경 등을 하므로 완전히 같은 바이너리를 찾는 것은 의미가 없다.
※ 이미지가 조금 다르더라도 유사한지를 검출할 때 Average Hash를 사용한다.
<구체적인 방법>
1. 이미지 크기를 축소한다.
2. 색을 그레이스케일로 변환한다.
3. 이미지의 각 픽셀 평균을 계산한다.
4. 각 픽셀의 어두운 정도가 평균보다 크면 1, 평균보다 작으면 0으로 입력한다.
이미지 데이터 읽기
img = Image.open(fname) # 이미지 데이터 열기
- JPEG/PNG/GIF/TIFF/BMP 등등 모두 지원
이미지를 그레이스케일로 변환하기
img = img.convert('L') # 그레이스케일로 변환하기
- convert 매개변수 지정
"L" : 그레이스케일
"1" : 이진화
"RGB", "RGBA", "CMYK" 등의 모드 지정 가능
* RGB(디지털 스크린): Red, Green, Blue - Tv, 모니터, 카메라 등에서 사용되는 가산혼합 방식
* RGBA(웹 개발): Red, Green, Blue, Alpha - Alpha는 투명도 (0-1 사이 값을 가지며, 0: 완전히 투명, 1: 완전히 불투명)
* CMYK(출력물, 프린터): Cyan, Magenta, Yellow, Key/Black - 인쇄에서 사용하는 감산혼합 방식
✓ 그레이스케일이란?
: 색상을 회색 계열로 표현하는 방식
- 흰색(255)부터 검정색(0)까지의 밝기 값으로만 표현 / 256단계의 밝기 수준
✓ 그레이스케일 주요 특징
- 단일 채널: 하나의 채널만 가지며 각 픽셀은 밝기 값으로만 표현된다.
- 메모리 절약: 한 픽셀당 8비트로 표현, 일반적으로 24비트(3채널)로 표현되는 컬러 이미지에 비해 메모리 사용량이 적다.
이미지 리사이즈하기
img = img.resize((size, size), Image.Resampling.LANCZOS) # 리사이즈하기
* Pillow 라이브러리 버전이 10.x.x 이상인 경우 ANTIALIAS 대신 LANCZOS를 사용해야 한다.
✓ LANCZOS 필터란?
: 고급 리사이징 필터 중 하나로, 신호 처리에서 사용하는 Lanczos Resapling 방법을 기반으로 한 보간법
- 이미지 축소에 적합하며, 고품질의 리사이징 결과를 얻을 수 있지만, 다른 보간법에 비해 속도가 느릴 수 있다.
- 픽셀의 주변 값을 고려해 보간하는 방식으로, 이미지의 가장자리나 세밀한 부분에서 날카로움을 잘 유지한다.
✓ 보간법: 이미 존재하는 값들 사이에 새로운 값을 계산하는 방식
- 중간 값을 예측하는 방법으로, 두 개의 값(숫자나 위치) 사이에서 그 중간에 들어갈 값을 찾아내는 과정
[전체 코드]
# 유사 이미지 검출하기
from PIL import Image # pip install Pillow
import numpy as np
# 이미지 데이터를 Average Hash로 변환하기
def average_hash(fname, size = 16):
img = Image.open(fname) # 이미지 데이터 열기
img = img.convert('L') # 그레이스케일로 변환하기
img = img.resize((size, size), Image.Resampling.LANCZOS) # 리사이즈하기 - Pillow version 10.x.x
pixel_data = img.getdata() # 픽셀 데이터 가져오기
pixels = np.array(pixel_data) # Numpy 배열로 변환하기
pixels = pixels.reshape((size, size)) # 2차원 배열로 변환하기
avg = pixels.mean() # 평균 구하기
diff = 1 * (pixels > avg) # 평균보다 크면 1, 작으면 0으로 변환하기
return diff
# 이전 해시로 변환하기
def np2hash(ahash):
bhash = []
for nl in ahash.tolist():
sl = [str(i) for i in nl]
s2 = "".join(sl)
i = int(s2, 2) # 이진수를 정수로 변환하기
bhash.append("%04x" % i)
return "".join(bhash)
# Average Hash 출력하기
ahash = average_hash('./tokyotower.jpg')
print(ahash)
print(np2hash(ahash))
[결과]
많은 이미지에서 유사한 이미지 검색하기
* 사용할 이미지 데이터 위의 사이트에서 다운 받는다. (이미지 클릭 시 이동)
1. 파일 경로 설정
search_dir = "./Deep-Learning/ImageAndDL/image/101_ObjectCategories"
cache_dir = "./Deep-Learning/ImageAndDL/image/cache_avhash"
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
- search_dir: 이미지가 저장된 디렉토리 경로 지정
- cache_dir: average hash 결과를 캐시해두는 디렉토리 경로
- 디렉토리가 없다면, os.mkdir(cache_dir) 로 새로 만든다.
2. Average hash 생성 함수
def average_hash(fname, size = 16):
fname2 = fname[len(search_dir):]
cache_file = cache_dir + "/" + fname2.replace('/', '_') + ".csv"
if not os.path.exists(cache_file): # 해시 생성하기
img = Image.open(fname) # fname: 처리할 이미지 파일 경로
img = img.convert('L').resize((size, size), Image.ANTIALIAS)
pixels = np.array(img.getdata()).reshape((size, size))
avg = pixels.mean()
px = 1 * (pixels > avg)
np.savetxt(cache_file, px, fmt="%.0f", delimiter=",")
else: # 캐시돼 있다면 읽지 않기
px = np.loadtxt(cache_file, delimiter=",")
return px
3. 해밍 거리 계산
- 두 이미지의 해시값을 비교하여 해밍 거리를 계산한다.
해밍 거리(Hamming distance)란?
: 같은 문자 수를 가진 2개의 문자열에서 대응하는 위치에 있는 문자 중 다른 것의 개수
def hamming_dist(a, b):
aa = a.reshape(1, -1) # 1차원 배열로 변환하기
ab = b.reshape(1, -1)
dist = (aa != ab).sum()
return dist
- a, b: 두 이미지의 해시값
- reshape(1, -1): 해시 배열의 1차원 배열로 변환하고, 두 배열 간의 차이를 계산
- dist = (aa != ab).sum(): 서로 다른 비트의 개수를 세어 해밍 거리 반환
4. 폴더 내 모든 파일 열거 함수
def enum_all_files(path):
for root, dirs, files in os.walk(path):
for f in files:
fname = os.path.join(root, f)
if re.search(r'\.(jpg|jpeg|png)$', fname):
yield fname
- os.walk(path): path 경로 하위에 있는 모든 파일을 순회하며, 이미지 파일만 찾아서 해당 경로를 반환한다.
- yield: 사용해서 제너레이터 함수로 생성 -> for 반복문을 활용해 효율적으로 파일을 찾을 수 있다.
✓ 제너레이터 함수란?
: 데이터를 한 번에 전부 만들어 주는 게 아닌, 필요한 순간에 하나씩 만들어주는 함수
쉽게 말해, 데이터를 조금씩 꺼내 쓸 수 있는 박스
yield 는 그 박스에서 데이터를 하나씩 꺼내서 사용자에게 주는 도구
➢ yield를 만나면 제너레이터 함수는 잠깐 멈추고, 그때까지 만든 데이터를 하나씩 주고 있다가, 다시 불리면 멈춘 자리에서 이어서 데이터를 만든다.
✓ yield 를 언제 쓸까?
➢ 한 번에 모든 데이터를 준비할 필요 없이, 필요할 때마다 하나씩 데이터를 만들어주기 때문에 메모리를 아낄 수 있다. !
✓ 제너레이터와 return 차이
- return: 함수가 끝나면서 값을 한 번에 한 번만 돌려준다. / 더 이상 실행되지 않음
- 제너레이터: 함수를 끝내지 않고 잠깐 멈춘다. 다음 번에 다시 불리면 계속 이어서 실행한다 / 데이터를 조금 씩 나눠준다.
5. 이미지 찾기
def find_image(fname, rate):
src = average_hash(fname) # 원본 이미지의 해시 값
for fname in enum_all_files(search_dir):
dst = average_hash(fname) # 폴더 내 다른 이미지들의 해시 값
diff_r = hamming_dist(src, dst) / 256
if diff_r < rate: # 유사도가 rate보다 작으면, 해당 이미지와의 차이와 파일명을 반환
yield (diff_r, fname)
6. 유사한 이미지 출력
# srcfile = search_dir + "/chair/image_0016.jpg" - 상대경로
srcfile = os.path.abspath(search_dir + "/chair/image_0016.jpg") # 절대 경로 설정
html = ""
sim = list(find_image(srcfile, 0.25))
sim = sorted(sim, key=lambda x:x[0])
for r, f in sim:
abs_f = os.path.abspath(f) # 절대 경로로 변환
print(r, ">", abs_f)
s = '<div style="float:left;"><h3>[ 차이 :' + str(r) + '-' + \
os.path.basename(abs_f) + ']</h3>'+ \
'<p><a href="' + abs_f + '"><img src="' + abs_f + '" width=200>'+ \
'</a></p></div>'
html += s
- find_image(srcfile, 0.25): 원본 이미지와 차이가 0.25 이하인 이미지를 찾는다.
- 결과를 차이 순으로 정렬하고, HTML 형식으로 변환
[Trouble Shooting]
💥 생성된 HTML 파일을 열었을 때, 사진이 출력되지 않는 현상 발생
상대 경로로 설정해서 그렇다. 절대경로로 설정해주면, 사진이 출력된다.
# srcfile = search_dir + "/chair/image_0016.jpg" - 상대경로
srcfile = os.path.abspath(search_dir + "/chair/image_0016.jpg") # 절대 경로 설정
abs_f = os.path.abspath(f) # 절대 경로로 변환
✓ 상대경로를 사용했을 때 이미지가 HTML에서 보이지 않았던 이유
- 브라우저가 파일을 찾는 경로와 실행하는 HTML 파일의 위치가 일치하지 않았기 때문일 가능성이 높다.
1. HTML 파일을 다른 위치에서 실행
2. 경로 설정이 잘못됨
3. 브라우저 기준 경로 다름
✓ 상대경로와 절대경로의 차이
- 상대경로: 현재 파일이 있는 위치를 기준으로 경로를 설정하는 방식
- 절대경로: 파일 시스템의 루트 디렉토리 (/) 에서 시작하는 전체경로
✓ 왜 절대경로로 바꾸니 될까?
- 절대경로는 파일 시스템에서 파일의 정확한 위치를 지정하기 때문에, 브라우저가 어디에서 HTML 파일을 실행하든 항상 동일한 경로에서 이미지를 찾을 수 있다.
[전체 코드]
from PIL import Image
import numpy as np
import os, re
# 파일 경로 지정하기
search_dir = "./Deep-Learning/ImageAndDL/image/101_ObjectCategories"
cache_dir = "./Deep-Learning/ImageAndDL/image/cache_avhash"
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
# 이미지 데이터를 Average Hash로 변환하기
def average_hash(fname, size = 16):
fname2 = fname[len(search_dir):]
# 이미지 캐시하기
cache_file = cache_dir + "/" + fname2.replace('/', '_') + ".csv"
if not os.path.exists(cache_file): # 해시 생성하기
img = Image.open(fname)
img = img.convert('L').resize((size, size), Image.ANTIALIAS)
pixels = np.array(img.getdata()).reshape((size, size))
avg = pixels.mean()
px = 1 * (pixels > avg)
np.savetxt(cache_file, px, fmt="%.0f", delimiter=",")
else: # 캐시돼 있다면 읽지 않기
px = np.loadtxt(cache_file, delimiter=",")
return px
# 해밍 거리 구하기
def hamming_dist(a, b):
aa = a.reshape(1, -1) # 1차원 배열로 변환하기
ab = b.reshape(1, -1)
dist = (aa != ab).sum()
return dist
# 모든 폴더에 처리 적용하기
def enum_all_files(path):
for root, dirs, files in os.walk(path):
for f in files:
fname = os.path.join(root, f)
if re.search(r'\.(jpg|jpeg|png)$', fname):
yield fname
# 이미지 찾기
def find_image(fname, rate):
src = average_hash(fname)
for fname in enum_all_files(search_dir):
dst = average_hash(fname)
diff_r = hamming_dist(src, dst) / 256
# print("[check] ",fname)
if diff_r < rate:
yield (diff_r, fname)
# 찾기
# srcfile = search_dir + "/chair/image_0016.jpg" - 상대경로
srcfile = os.path.abspath(search_dir + "/chair/image_0016.jpg") # 절대 경로 설정
html = ""
sim = list(find_image(srcfile, 0.25))
sim = sorted(sim, key=lambda x:x[0])
for r, f in sim:
abs_f = os.path.abspath(f) # 절대 경로로 변환
print(r, ">", abs_f)
s = '<div style="float:left;"><h3>[ 차이 :' + str(r) + '-' + \
os.path.basename(abs_f) + ']</h3>'+ \
'<p><a href="' + abs_f + '"><img src="' + abs_f + '" width=200>'+ \
'</a></p></div>'
html += s
# HTML로 출력하기
html = """<html><head><meta charset="utf8"></head>
<body><h3>원래 이미지</h3><p>
<img src='{0}' width=300></p>{1}
</body></html>""".format(srcfile, html)
with open("./Deep-Learning/ImageAndDL/avhash-search-output.html", "w", encoding="utf-8") as f:
f.write(html)
print("ok")
'🤖 AI > Computer Vision' 카테고리의 다른 글
[CH 7.3] 이미지 판정하기 (0) | 2024.10.10 |
---|---|
[CH 7.2] CNN으로 이미지 분류하기 (1) | 2024.10.10 |