Lors de la création d'un ensemble de données d'apprentissage automatique basé sur des images collectées sur Internet, il est nécessaire de supprimer les images en double. Il est toujours bon qu'il y ait des images en double dans les données d'entraînement, mais s'il y a des images en double entre les données d'entraînement et les données de test, une soi-disant fuite se produira.
Le moyen le plus simple de détecter les images en double consiste à utiliser la valeur de hachage d'un fichier tel que MD5. Cependant, la valeur de hachage du fichier est juste un hachage de la chaîne binaire du fichier image, et même si la même image est modifiée en modifiant le format de stockage ou les paramètres de compression, cela entraînera une omission de détection.
Par conséquent, dans cet article, nous présenterons des algorithmes qui hachent les caractéristiques des images elles-mêmes, et nous examinerons les caractéristiques de ces algorithmes de hachage à travers des expériences simples.
Average Hash (aHash) Il s'agit d'une valeur de hachage basée sur les caractéristiques de l'image (modèle de luminosité) et peut être calculée avec un algorithme simple. La procédure spécifique est la suivante.
aHash présente les avantages d'algorithmes simples et de calculs rapides. D'autre part, il présente également l'inconvénient d'être inflexible. Par exemple, la valeur de hachage d'une image corrigée gamma sera loin de l'image d'origine.
Perseptual Hash (pHash) Alors que aHash utilisait les valeurs de pixel elles-mêmes, pHash utilise la transformation cosinus discrète (DCT) de l'image. La DCT est l'une des méthodes pour convertir des signaux tels que des images dans la gamme de fréquences, et est utilisée pour la compression JPEG, par exemple. Dans la compression JPEG, la quantité de données est réduite par DCT l'image et en extrayant uniquement les composantes basse fréquence qui sont facilement perçues par les humains.
Semblable à la compression JPGE, pHash se concentre sur les composants basse fréquence dans le DCT de l'image et les hache. En faisant cela, il est possible d'extraire préférentiellement des caractéristiques qui sont facilement perçues par les humains, et on pense qu'un hachage robuste peut être effectué pour le mouvement parallèle des images et les changements de luminosité.
Il semble y avoir diverses variantes autres que aHash et pHash. Certaines personnes font le benchmarking [^ 1].
Appliquez divers traitements à l'image et comparez la valeur de hachage avec l'image d'origine. Calculez respectivement aHash et pHash comme valeurs de hachage.
Essayez également la technique consistant à considérer la sortie comme un hachage pour la couche immédiatement avant la dernière couche de ResNet50. Cette méthode a été adoptée dans un article [^ 2] [^ 3].
OpenCV est utilisé pour calculer aHash et pHash, mais il existe également une bibliothèque appelée ImageHash. De plus, dans aHash et pHash, vous pouvez utiliser la distance de bourdonnement pour comparer les valeurs de hachage. La plage de valeurs est «[0, 64]». Afin de correspondre à cette plage, dans la comparaison de hachage (simulation) à l'aide de ResNet50, la similitude cosinus est calculée puis convertie dans la plage ci-dessus.
import copy
import pprint
import cv2.cv2 as cv2
import numpy as np
from keras import models
from keras.applications.resnet50 import ResNet50, preprocess_input
from sklearn.metrics.pairwise import cosine_similarity
class ImagePairGenerator(object):
"""
Une classe qui génère des paires d'images expérimentales
"""
def __init__(self, img: np.ndarray):
self._img = img
self._processings = self._prepare_processings()
def _prepare_processings(self):
h, w, _ = self._img.shape
#Position et taille pour le recadrage autour du visage de l'image de la lenna
org = np.array([128, 128])
size = np.array([256, 256])
# kind (processing description), img1, img2
processings = [
('Même',
lambda x: x,
lambda x: x),
('Échelle de gris',
lambda x: x,
lambda x: cv2.cvtColor(
cv2.cvtColor(x, cv2.COLOR_BGR2GRAY), cv2.COLOR_GRAY2BGR)),
*list(map(lambda s:
(f'1/{s:2}Réduire à',
lambda x: x,
lambda x: cv2.resize(x, (w // s, h // s))),
np.power(2, range(1, 5)))),
*list(map(lambda s:
(f'Lissage(kernel size = {s:2}',
lambda x: x,
lambda x: cv2.blur(x, (s, s))),
[3, 5, 7, 9, 11])),
*list(map(lambda s:
(f'Insérer du texte(fontScale = {s})',
lambda x: x,
lambda x: cv2.putText(x, 'Text', org=(10, 30*s),
fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=s,
color=(255, 255, 255),
thickness=3*s,
lineType=cv2.LINE_AA)),
range(1, 8))),
*list(map(lambda q:
(f'Compression JPEG(quality = {q})',
lambda x: x,
lambda x: img_encode_decode(x, q)),
range(10, 100, 10))),
*list(map(lambda gamma:
(f'Correction gamma(gamma = {gamma})',
lambda x: x,
lambda x: img_gamma(x, gamma)),
[0.2, 0.5, 0.8, 1.2, 1.5, 2.0])),
*list(map(lambda d:
(f'Mouvement parallèle({d:2} pixels)',
lambda x: img_crop(x, org, size),
lambda x: img_crop(x, org + d, size)),
np.power(2, range(7)))),
]
return processings
def __iter__(self):
for kind, p1, p2 in self._processings:
yield (kind,
p1(copy.deepcopy(self._img)),
p2(copy.deepcopy(self._img)))
class ResNet50Hasher(object):
"""
Classe pour la sortie de la couche finale de ResNet50 en tant que valeur de hachage
"""
_input_size = 224
def __init__(self):
self._model = self._prepare_model()
def _prepare_model(self):
resnet50 = ResNet50(include_top=False, weights='imagenet',
input_shape=(self._input_size, self._input_size, 3),
pooling='avg')
model = models.Sequential()
model.add(resnet50)
return model
def compute(self, img: np.ndarray) -> np.ndarray:
img_arr = np.array([
cv2.resize(img, (self._input_size, self._input_size))
])
img_arr = preprocess_input(img_arr)
embeddings = self._model.predict(img_arr)
return embeddings
@staticmethod
def compare(x1: np.ndarray, x2: np.ndarray):
"""
Calculez la similitude cosinus. La plage de valeurs est[0, 1]。
Comparez aHash et pHash en fonction de la distance de bourdonnement,
[0, 64]Convertir dans la plage de valeurs de.
"""
cs = cosine_similarity(x1, x2)
distance = 64 + (0 - 64) * ((cs - 0) / (1 - 0))
return distance.ravel()[0] # np.array -> float
def img_crop(img: np.ndarray, org: np.ndarray, size: np.ndarray) -> np.ndarray:
"""
Recadrez n'importe quelle zone de l'image.
"""
y, x = org
h, w = size
return img[y:y + h, x:x + w, :]
def img_encode_decode(img: np.ndarray, quality=90) -> np.ndarray:
"""
Reproduisez la détérioration de la compression Jpeg.
Référence: https://qiita.com/ka10ryu1/items/5fed6b4c8f29163d0d65
"""
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
_, enc_img = cv2.imencode('.jpg', img, encode_param)
dec_img = cv2.imdecode(enc_img, cv2.IMREAD_COLOR)
return dec_img
def img_gamma(img: np.ndarray, gamma=0.5) -> np.ndarray:
"""
Correction gamma.
Référence: https://www.dogrow.net/python/blog99/
"""
lut = np.empty((1, 256), np.uint8)
for i in range(256):
lut[0, i] = np.clip(pow(i / 255.0, gamma) * 255.0, 0, 255)
return cv2.LUT(img, lut)
def image_hashing_test():
image_path = 'resources/lena_std.tif'
img = cv2.imread(image_path, cv2.IMREAD_COLOR)
h, w, _ = img.shape
hashers = [
('aHash', cv2.img_hash.AverageHash_create()),
('pHash', cv2.img_hash.PHash_create()),
('ResNet', ResNet50Hasher())
]
pairs = ImagePairGenerator(img)
result_dict = {}
for pair_kind, img1, img2 in pairs:
result_dict[pair_kind] = {}
for hasher_kind, hasher in hashers:
hash1 = hasher.compute(img1)
hash2 = hasher.compute(img2)
distance = hasher.compare(hash1, hash2)
result_dict[pair_kind][hasher_kind] = distance
#Vérifiez visuellement l'image (uniquement lorsque la forme est la même, car il est difficile à aligner)
if img1.shape == img2.shape:
window_name = pair_kind
cv2.imshow(window_name, cv2.hconcat((img1, img2)))
cv2.waitKey()
cv2.destroyWindow(window_name)
pprint.pprint(result_dict)
if __name__ == '__main__':
image_hashing_test()
{'Même': {'ResNet': 0.0, 'aHash': 0.0, 'pHash': 0.0},
'Échelle de gris': {'ResNet': 14.379967, 'aHash': 0.0, 'pHash': 0.0},
'1/Réduit à 2': {'ResNet': 1.2773285, 'aHash': 3.0, 'pHash': 1.0},
'1/Réduit à 4': {'ResNet': 6.5748253, 'aHash': 4.0, 'pHash': 1.0},
'1/Réduit à 8': {'ResNet': 18.959282, 'aHash': 7.0, 'pHash': 3.0},
'1/Réduit à 16': {'ResNet': 34.8299, 'aHash': 12.0, 'pHash': 0.0},
'Compression JPEG(quality = 10)': {'ResNet': 6.4169083, 'aHash': 2.0, 'pHash': 0.0},
'Compression JPEG(quality = 20)': {'ResNet': 2.6065674, 'aHash': 1.0, 'pHash': 0.0},
'Compression JPEG(quality = 30)': {'ResNet': 1.8446579, 'aHash': 0.0, 'pHash': 0.0},
'Compression JPEG(quality = 40)': {'ResNet': 1.2492218, 'aHash': 0.0, 'pHash': 1.0},
'Compression JPEG(quality = 50)': {'ResNet': 1.0534592, 'aHash': 0.0, 'pHash': 0.0},
'Compression JPEG(quality = 60)': {'ResNet': 0.99293137, 'aHash': 0.0, 'pHash': 0.0},
'Compression JPEG(quality = 70)': {'ResNet': 0.7313309, 'aHash': 0.0, 'pHash': 0.0},
'Compression JPEG(quality = 80)': {'ResNet': 0.58068085, 'aHash': 0.0, 'pHash': 0.0},
'Compression JPEG(quality = 90)': {'ResNet': 0.354187, 'aHash': 0.0, 'pHash': 0.0},
'Correction gamma(gamma = 0.2)': {'ResNet': 16.319721, 'aHash': 2.0, 'pHash': 1.0},
'Correction gamma(gamma = 0.5)': {'ResNet': 4.2003975, 'aHash': 2.0, 'pHash': 0.0},
'Correction gamma(gamma = 0.8)': {'ResNet': 0.48334503, 'aHash': 0.0, 'pHash': 0.0},
'Correction gamma(gamma = 1.2)': {'ResNet': 0.381176, 'aHash': 0.0, 'pHash': 1.0},
'Correction gamma(gamma = 1.5)': {'ResNet': 1.7187691, 'aHash': 2.0, 'pHash': 1.0},
'Correction gamma(gamma = 2.0)': {'ResNet': 4.074257, 'aHash': 6.0, 'pHash': 2.0},
'Insérer du texte(fontScale = 1)': {'ResNet': 0.7838249, 'aHash': 0.0, 'pHash': 0.0},
'Insérer du texte(fontScale = 2)': {'ResNet': 1.0911484, 'aHash': 0.0, 'pHash': 1.0},
'Insérer du texte(fontScale = 3)': {'ResNet': 2.7721176, 'aHash': 0.0, 'pHash': 2.0},
'Insérer du texte(fontScale = 4)': {'ResNet': 4.646305, 'aHash': 0.0, 'pHash': 4.0},
'Insérer du texte(fontScale = 5)': {'ResNet': 8.435852, 'aHash': 2.0, 'pHash': 3.0},
'Insérer du texte(fontScale = 6)': {'ResNet': 11.267036, 'aHash': 6.0, 'pHash': 3.0},
'Insérer du texte(fontScale = 7)': {'ResNet': 15.272251, 'aHash': 2.0, 'pHash': 7.0},
'Lissage(kernel size = 3': {'ResNet': 1.3798943, 'aHash': 2.0, 'pHash': 0.0},
'Lissage(kernel size = 5': {'ResNet': 3.1528091, 'aHash': 4.0, 'pHash': 1.0},
'Lissage(kernel size = 7': {'ResNet': 4.903698, 'aHash': 4.0, 'pHash': 1.0},
'Lissage(kernel size = 9': {'ResNet': 6.8400574, 'aHash': 4.0, 'pHash': 1.0},
'Lissage(kernel size = 11': {'ResNet': 9.477722, 'aHash': 5.0, 'pHash': 2.0},
'Mouvement parallèle( 1 pixels)': {'ResNet': 0.47764206, 'aHash': 6.0, 'pHash': 0.0},
'Mouvement parallèle( 2 pixels)': {'ResNet': 0.98942566, 'aHash': 10.0, 'pHash': 3.0},
'Mouvement parallèle( 4 pixels)': {'ResNet': 1.475399, 'aHash': 15.0, 'pHash': 5.0},
'Mouvement parallèle( 8 pixels)': {'ResNet': 2.587471, 'aHash': 20.0, 'pHash': 13.0},
'Mouvement parallèle(16 pixels)': {'ResNet': 3.1883087, 'aHash': 25.0, 'pHash': 21.0},
'Mouvement parallèle(32 pixels)': {'ResNet': 4.8445663, 'aHash': 23.0, 'pHash': 31.0},
'Mouvement parallèle(64 pixels)': {'ResNet': 9.34531, 'aHash': 28.0, 'pHash': 30.0}}
Veuillez noter que ResNet utilise ce qui a été pré-formé avec ImageNet tel quel. En d'autres termes, en préparant des données d'apprentissage, il est possible d'acquérir un réseau avec des caractéristiques différentes de celles décrites ci-dessous.
De plus, la tendance peut changer en fonction de l'image. Pour une utilisation pratique, il est préférable d'évaluer avec un ensemble de données décent.
J'ai introduit une méthode pour hacher les images. De plus, nous avons mesuré la distance entre l'image traitée et l'image originale et observé les caractéristiques de chaque méthode de hachage. Parmi les algorithmes comparés dans cet article, nous pouvons observer que pHash a tendance à être robuste à la mise à l'échelle de l'image, à la dégradation par compression et au lissage.
J'ai récemment découvert les algorithmes basés sur xHash, et je pense qu'ils sont simples et basés sur de bonnes idées. Je pense que c'est un algorithme relativement mort, mais j'espère qu'il pourra être utilisé au bon endroit.
Looks Like It - The Hacker Factor Blog http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
Essayez la comparaison d'images en utilisant le hachage perceptif (phash) http://hideack.hatenablog.com/entry/2015/03/16/194336
[^ 3]: Pour dire la vérité, quand je lisais cet article, j'ai eu l'idée: "Y a-t-il un moyen plus simple d'identifier les doublons?"
Recommended Posts