[PYTHON] Algorithme de hachage pour déterminer la même image

introduction

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.

Algorithme de hachage d'image

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.

  1. Réduisez l'image à 8x8 pixels.
  2. Convertissez davantage en échelle de gris.
  3. Calculez la moyenne des valeurs de pixel.
  4. Pour chaque 8x8 pixels, binarisez (0 ou 1) selon qu'il est supérieur ou inférieur à la valeur moyenne.
  5. Pour la séquence binaire, organisez-la dans une ligne dans un ordre tel que l'ordre de balayage des trames, et obtenez un hachage 64 bits.

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é.

  1. Réduisez l'image. Rendez-le plus grand que 8x8 (par exemple, 32x32).
  2. Échelle de gris.
  3. DCT.
  4. Extrayez uniquement le composant basse fréquence 8x8.
  5. Calculez la valeur moyenne des composants basse fréquence à l'exclusion des composants CC.
  6. Binar selon qu'elle est supérieure ou inférieure à la valeur moyenne.
  7. Obtenez un hachage 64 bits en les alignant dans un certain ordre, tel que l'ordre de balayage des trames.

Autre xHash

Il semble y avoir diverses variantes autres que aHash et pHash. Certaines personnes font le benchmarking [^ 1].

Expérience

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].

code

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()

résultat

{'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}}

Considération

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.

Résumé

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.

référence

[^ 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

Algorithme de hachage pour déterminer la même image
Programme pour rechercher la même image
Déterminer s'il y a des oiseaux dans l'image
Traitement d'image? L'histoire du démarrage de Python pour
Détecter les dossiers avec la même image dans ImageHash
58 Le même château
Création d'une image trompeuse pour le modèle de génération de légende
Programme Python qui recherche le même nom de fichier
Algorithme Dikstra pour les débutants
Essayez une recherche similaire de recherche d'images à l'aide du SDK Python [Recherche]
La fonction d'affichage d'image d'iTerm est pratique lors du traitement d'images.