ハッシュの束のペアごとのハミング距離を計算し、それらをクラスター化する方法について、誰かがガイダンスを提供できることを願っています。私は自分が何をしているのか、何をしたいのかを見ると、パフォーマンスほど気にしません。何をしても遅くなり、何度も何度も実行されるものではありません。
つまり...一言で言えば、ドライブから何千もの写真を誤って消去し、バックアップがありませんでした(私は知っています...悪い習慣です)。さまざまなツールを使用して、ドライブからそれらの非常に高い割合を回復することができましたが、数百枚の写真が残っていました. 一部の写真を復元するために使用された手法 (ファイル カービングなど) により、一部の画像はさまざまな程度に破損しており、他の画像は同一のコピーであり、他の画像は視覚的には本質的に同一であるものの、バイトごとに異なっていました。
状況を助けるために私がやっていることは次のとおりです。
- 各画像をチェックし、画像ファイルが構造的に破損しているかどうかを特定します (完了)
- 各画像の知覚ハッシュ (フィンガープリント) を生成して、画像の類似性を比較し、クラスター化できるようにします (フィンガープリントの部分は実行されます)。
- 指紋のペアワイズ距離を計算する
- ペアごとの距離をクラスター化して、同様の画像を一緒に表示して手動のクリーンアップを支援します
添付のスクリプトでは、ハッシュを計算するいくつかの場所に気付くでしょう。混乱を招かないように説明します...
- PIL でサポートされている画像の場合、3 つのハッシュを生成します。1 番目は元の画像用、2 番目は 90 度回転したもの、3 番目は 180 度回転したものです。これは、ペアワイズ計算が完了したときに、向きが異なるだけの画像を説明できるようにするためです。
- PIL でサポートされていない生の画像の場合は、代わりに、抽出された埋め込みプレビュー画像から生成されたハッシュを優先します。生の画像を使用する代わりにこれを行いました。これは、生の画像ファイルが破損している場合、サイズが小さいためにプレビュー画像が無傷である可能性が高く、画像がその他
- ハッシュが生成される他の場所は、破損した生の画像を特定するための最後の努力の最中です。抽出/変換された raw 画像のハッシュと、抽出された埋め込みプレビュー画像のハッシュを比較します。類似性が定義されたしきい値を満たさない場合は、raw ファイル全体が破損している可能性があると想定されます。
ガイダンスが必要なのは、次のことを達成する方法です。
- 各画像に対して私が持っている3つのハッシュを取り、ハミングペアワイズ距離を計算します
- 画像比較ごとに、最も類似しているハミング距離のみを保持します
- 同様の画像をグループ化できるように、結果を scipy 階層的クラスタリングにフィードします
私はちょうどPythonを学んでいるので、それは私の挑戦の一部です...私はGoogleから得たものから、最初にscipy.spatial.distance.pdistを使用してペアごとの距離を取得することでこれを行うことができ、次にこれを処理して画像比較ごとに最も類似した距離を取得し、これを scipy クラスタリング関数にフィードします。しかし、これを整理して適切な形式で提供する方法などを理解できません。これに関するガイダンスを提供できますか?
これは、何らかのハッシュの辞書またはディスク ストレージ上の何らかの種類を保存するために変更する必要があることに興味を持っている人がいる場合に備えて、現在のスクリプトを参照するためのものです。
from PIL import Image
from PIL import ImageFile
import os, sys, imagehash, pyexiv2, rawpy, re
from tempfile import NamedTemporaryFile
from subprocess import check_call, call
# allow PIL to load truncated images (so that perceptual hashes can be created for truncated/damaged images still)
ImageFile.LOAD_TRUNCATED_IMAGES = True
# image files this script will handle
# PIL supported image formats
stdimageext = ('.jpg','.jpeg', '.bmp', '.png', '.gif', '.tif', '.tiff')
# libraw/ufraw supported formats
rawimageext = ('.nef', '.dng', '.tif', '.tiff')
devnull = open(os.devnull, 'w')
corruptRegex = re.compile(r'_\[.+\]\..{3,4}$')
for root, dirs, files in os.walk(sys.argv[1]):
for filename in files:
ext = os.path.splitext(filename.lower())[1]
filepath = os.path.join(root, filename)
if ext in (stdimageext + rawimageext):
hashes = [None] * 4
print(filename)
# reset corrupt string
corrupt_str = None
if ext in (stdimageext):
metadata = pyexiv2.ImageMetadata(filepath)
metadata.read()
rotate = 0
try:
im = Image.open(filepath)
except:
None
else:
for x in range(3):
hashes[x] = imagehash.dhash(im.rotate(90 * (x + 1)),32)
# use jpeginfo against all jpg images as its pretty accurate
if ext in ('.jpg','.jpeg'):
rc = 0
rc = call(["jpeginfo", "--check", filepath], stdout=devnull, stderr=devnull)
if rc == 1:
corrupt_str = 'JpegInfo'
if corrupt_str is None:
try:
im = Image.open(filepath)
im.verify()
except:
e = sys.exc_info()[0]
corrupt_str = 'PIL_Verify'
else:
try:
im = Image.open(filepath)
im.load()
except:
e = sys.exc_info()[0]
corrupt_str = 'PIL_Load'
# raw image processing
else:
# extract largest embedded preview image first
metadata_orig = pyexiv2.ImageMetadata(filepath)
metadata_orig.read()
if len(metadata_orig.previews) > 0:
preview = metadata_orig.previews[-1]
# save preview to temp file
temp_preview = NamedTemporaryFile()
preview.write_to_file(temp_preview.name)
os.rename(temp_preview.name + preview.extension, temp_preview.name)
rotate = 0
try:
im = Image.open(temp_preview.name)
except:
None
else:
for x in range(4):
hashes[x] = imagehash.dhash(im.rotate(90 * (x + 1)),32)
# close temp file
temp_preview.close()
# try to load raw using libraw via rawpy first,
# generally if libraw can't load it then ufraw extraction would also fail
try:
with rawpy.imread(filepath) as im:
None
except:
e = sys.exc_info()[0]
corrupt_str = 'Libraw_Load'
else:
# as a final last ditch effort compare perceptual hashes of extracted
# raw and embedded preview to detect possible internal corruption
if len(metadata_orig.previews) > 0:
# extract and convert raw to jpeg image using ufraw
temp_raw = NamedTemporaryFile(suffix='.jpg')
try:
check_call(['ufraw-batch', '--wb=camera', '--rotate=camera', '--out-type=jpg', '--compression=95', '--noexif', '--lensfun=none', '--output=' + temp_raw.name, '--overwrite', '--silent', filepath],stdout=devnull, stderr=devnull)
except:
e = sys.exc_info()[0]
corrupt_str = 'Ufraw-conv'
else:
rhash = imagehash.dhash(Image.open(temp_raw.name),32)
# compare preview with raw image and compute the most similar hamming distance (best)
hamdiff = .0
for h in range(4):
# calculate hamming distance to compare similarity
hamdiff = max((256 - sum(bool(ord(ch1) - ord(ch2)) for ch1, ch2 in zip(str(hashes[h]), str(rhash))))/256,hamdiff)
if hamdiff < .7: # raw file is probably corrupt
corrupt_str = 'hash' + str(round(hamdiff*100,2))
# close temp files
temp_raw.close()
print(hamdiff)
print(rhash)
print(hashes[0])
print(hashes[1])
print(hashes[2])
print(hashes[3])
# prefix file if corruption was detected ensuring that existing files already prefixed are re prefixed
mo = corruptRegex.search(filename)
if corrupt_str is not None:
if mo is not None:
os.rename(filepath,os.path.join(root, re.sub(corruptRegex, '_[' + corrupt_str + ']', filename) + ext))
else:
os.rename(filepath,os.path.join(root, os.path.splitext(filename)[0] + '_[' + corrupt_str + ']' + ext))
else:
if mo is not None:
os.rename(filepath,os.path.join(root, re.sub(corruptRegex, '', filename) + ext))
編集 済み最終的に思いついたもので更新を提供したいだけで、意図した目的に対して非常にうまく機能しているようで、同様の状況で他のユーザーに役立つかもしれません. スクリプトはまだいくらかの研磨を使用できますが、それ以外はすべて肉がそこにあります. 私はPythonの使用に関して環境に優しいので、大幅に改善できるものを誰かが見つけたら教えてください.
スクリプトは次のことを行います。
- さまざまな方法を使用して、ファイル構造に関して画像の破損を検出しようとします。生の画像形式 (NEF、DNG、TIF) の場合、破損した画像でも問題なくロードできることが時々あるので、プレビュー画像と生の画像の抽出された .jpg の両方をハッシュし、ハッシュを比較して、それらが類似していないかどうかを調べることにしました。画像が何らかの形で破損していると思います。
- ロードできる各画像の知覚ハッシュを作成します。ベース ファイル用に 3 つ作成されます (オリジナル、90 度回転したオリジナル、180 度回転したオリジナル)。さらに、未加工の画像の場合、抽出されたプレビュー画像用に追加の 3 つのハッシュが作成されました。これは、未加工の画像が破損している場合でも完全な画像に基づくハッシュが残るようにするためです (プレビューが正常であると仮定します)。
- 破損していると識別された画像は、破損していることとその原因を示すサフィックスを付けて名前が変更されます。
- ペアごとのハミング距離は、すべてのファイル ペアに対してハッシュを比較することによって計算され、numpy 配列に格納されました。
- ペアごとの距離の平方形式は、クラスタリングのために fastcluster に供給されます
- fastcluster からの出力を使用してデンドログラムを生成し、類似した画像のクラスターを視覚化します
numpy 配列をディスクに保存して、遅い各ファイルのハッシュを再計算せずに fastcluster/dendrogram 部分を後で再実行できるようにします。これは、まだ許可するようにスクリプトを変更する必要があるものです....
from PIL import Image
from PIL import ImageFile
import os, sys, imagehash, pyexiv2, rawpy, re
from tempfile import NamedTemporaryFile
from subprocess import check_call, call
import numpy as np
from scipy.cluster.hierarchy import dendrogram
from scipy.spatial.distance import squareform
import fastcluster
import matplotlib.pyplot as plt
# allow PIL to load truncated images (so that perceptual hashes can be created for truncated/damaged images still)
ImageFile.LOAD_TRUNCATED_IMAGES = True
# image files this script will handle
# PIL supported image formats
stdimageext = ('.jpg','.jpeg', '.bmp', '.png', '.gif', '.tif', '.tiff')
# libraw/ufraw supported formats
rawimageext = ('.nef', '.dng', '.tif', '.tiff')
devnull = open(os.devnull, 'w')
corruptRegex = re.compile(r'_\[.+\]\..{3,4}$')
hashes = []
filelist = []
for root, _, files in os.walk(sys.argv[1]):
for filename in files:
ext = os.path.splitext(filename.lower())[1]
relpath = os.path.relpath(root, sys.argv[1])
filepath = os.path.join(root, filename)
if ext in (stdimageext + rawimageext):
hashes_tmp = []
rhash = []
# reset corrupt string
corrupt_str = None
if ext in (stdimageext):
try:
im=Image.open(filepath)
for x in range(3):
hashes_tmp.append(str(imagehash.dhash(im.rotate(90 * x, expand=1),32)))
except:
None
# use jpeginfo against all jpg images as its pretty accurate
if ext in ('.jpg','.jpeg'):
rc = 0
rc = call(["jpeginfo", "--check", filepath], stdout=devnull, stderr=devnull)
if rc == 1:
corrupt_str = 'JpegInfo'
if corrupt_str is None:
try:
im = Image.open(filepath)
im.verify()
except:
e = sys.exc_info()[0]
corrupt_str = 'PIL_Verify'
else:
try:
im = Image.open(filepath)
im.load()
except:
e = sys.exc_info()[0]
corrupt_str = 'PIL_Load'
# raw image processing
if ext in (rawimageext):
# extract largest embedded preview image first
metadata_orig = pyexiv2.ImageMetadata(filepath)
metadata_orig.read()
if len(metadata_orig.previews) > 0:
preview = metadata_orig.previews[-1]
# save preview to temp file
temp_preview = NamedTemporaryFile()
preview.write_to_file(temp_preview.name)
os.rename(temp_preview.name + preview.extension, temp_preview.name)
try:
im = Image.open(temp_preview.name)
for x in range(3):
hashes_tmp.append(str(imagehash.dhash(im.rotate(90 * x,expand=1),32)))
except:
None
# try to load raw using libraw via rawpy first,
# generally if libraw can't load it then ufraw extraction would also fail
try:
im = rawpy.imread(filepath)
except:
e = sys.exc_info()[0]
corrupt_str = 'Libraw_Load'
else:
# as a final last ditch effort compare perceptual hashes of extracted
# raw and embedded preview to detect possible internal corruption
# extract and convert raw to jpeg image using ufraw
temp_raw = NamedTemporaryFile(suffix='.jpg')
try:
check_call(['ufraw-batch', '--wb=camera', '--rotate=camera', '--out-type=jpg', '--compression=95', '--noexif', '--lensfun=none', '--output=' + temp_raw.name, '--overwrite', '--silent', filepath],stdout=devnull, stderr=devnull)
except:
e = sys.exc_info()[0]
corrupt_str = 'Ufraw-conv'
else:
try:
im = Image.open(temp_raw.name)
for x in range(3):
rhash.append(str(imagehash.dhash(im.rotate(90 * x,expand=1),32)))
except:
None
# compare preview with raw image and compute the most similar hamming distance (best)
if len(hashes_tmp) > 0 and len(rhash) > 0:
hamdiff = 1
for rh in rhash:
# calculate hamming distance to compare similarity
hamdiff = min(hamdiff,(sum(bool(ord(ch1) - ord(ch2)) for ch1, ch2 in zip(hashes_tmp[0], rh))/len(hashes_tmp[0])))
if hamdiff > .3: # raw file is probably corrupt
corrupt_str = 'hash' + str(round(hamdiff*100,2))
hashes_tmp = hashes_tmp + rhash
# prefix file if corruption was detected ensuring that existing files already prefixed are re prefixed
mo = corruptRegex.search(filename)
newfilename = None
if corrupt_str is not None:
if mo is not None:
newfilename = re.sub(corruptRegex, '_[' + corrupt_str + ']', filename) + ext
else:
newfilename = os.path.splitext(filename)[0] + '_[' + corrupt_str + ']' + ext
else:
if mo is not None:
newfilename = re.sub(corruptRegex, '', filename) + ext
if newfilename is not None:
os.rename(filepath,os.path.join(root, newfilename))
if len(hashes_tmp) > 0:
hashes.append(hashes_tmp)
if newfilename is not None:
filelist.append(os.path.join(relpath, newfilename))
else:
filelist.append(os.path.join(relpath, filename))
print(len(filelist))
print(len(hashes))
a = np.empty(shape=(len(filelist),len(filelist)))
for hash_idx1, hash in enumerate(hashes):
a[hash_idx1,hash_idx1] = 0
hash_idx2 = hash_idx1 + 1
while hash_idx2 < len(hashes):
ham_dist = 1
for h1 in hash:
for h2 in hashes[hash_idx2]:
ham_dist = min(ham_dist, (sum(bool(ord(ch1) - ord(ch2)) for ch1, ch2 in zip(h1, h2)))/len(h1))
a[hash_idx1,hash_idx2] = ham_dist
a[hash_idx2,hash_idx1] = ham_dist
hash_idx2 = hash_idx2 + 1
print(a)
X = squareform(a)
print(X)
linkage = fastcluster.single(X)
clustdict = {i:[i] for i in range(len(linkage)+1)}
fig = plt.figure(figsize=(25,25))
plt.title('test title')
plt.xlabel('perpetual hash hamming distance')
plt.axvline(x=.15,c='red',linestyle='--')
dg = dendrogram(linkage, labels=filelist, orientation='right', show_leaf_counts=True)
ax = fig.gca()
ax.set_xlim(-.01,ax.get_xlim()[1])
plt.show
plt.savefig('foo1.pdf', bbox_inches='tight', dpi=100)
with open('numpyarray.npy','wb') as f:
np.save(f,a)