[PYTHON] Hashing-Algorithmus zur Bestimmung des gleichen Bildes

Einführung

Beim Erstellen eines Datensatzes für maschinelles Lernen auf der Grundlage von aus dem Internet gesammelten Bildern müssen doppelte Bilder entfernt werden. Es ist immer noch gut, wenn die Trainingsdaten doppelte Bilder enthalten, aber wenn zwischen den Trainingsdaten und den Testdaten doppelte Bilder vorhanden sind, tritt eine sogenannte Leckage auf.

Der einfachste Weg, doppelte Bilder zu erkennen, besteht darin, den Hash-Wert einer Datei wie MD5 zu verwenden. Der Hash-Wert der Datei ist jedoch nur ein Hash der Binärzeichenfolge der Bilddatei, und selbst wenn dasselbe Bild durch Ändern des Speicherformats oder der Komprimierungsparameter geändert wird, wird die Erkennung nicht erkannt.

Daher werden wir in diesem Artikel Algorithmen vorstellen, die die Merkmale von Bildern selbst hashen, und wir werden die Eigenschaften dieser Hashing-Algorithmen durch einfache Experimente untersuchen.

Bild-Hashing-Algorithmus

Average Hash (aHash) Es ist ein Hashwert, der auf den Eigenschaften des Bildes basiert (Helligkeitsmuster) und mit einem einfachen Algorithmus berechnet werden kann. Das spezifische Verfahren ist wie folgt.

  1. Verkleinern Sie das Bild auf 8 x 8 Pixel.
  2. Weiter in Graustufen konvertieren.
  3. Berechnen Sie den Durchschnitt der Pixelwerte.
  4. Binärisieren Sie für jeweils 8 x 8 Pixel (0 oder 1), je nachdem, ob es höher oder niedriger als der Durchschnittswert ist.
  5. Ordnen Sie die Binärsequenz in einer bestimmten Reihenfolge an, z. B. in der Reihenfolge des Raster-Scans, und erhalten Sie einen 64-Bit-Hash.

aHash bietet die Vorteile einfacher Algorithmen und schneller Berechnungen. Andererseits hat es auch den Nachteil, unflexibel zu sein. Beispielsweise ist der Hash-Wert eines gammakorrigierten Bildes weit vom Originalbild entfernt.

Perseptual Hash (pHash) Während aHash die Pixelwerte selbst verwendet, verwendet pHash die diskrete Cosinustransformation (DCT) des Bildes. DCT ist eine der Methoden zum Konvertieren von Signalen wie Bildern in den Frequenzbereich und wird beispielsweise für die JPEG-Komprimierung verwendet. Bei der JPEG-Komprimierung wird die Datenmenge durch DCT des Bildes reduziert und nur die Niederfrequenzkomponenten extrahiert, die vom Menschen leicht wahrgenommen werden können.

Ähnlich wie bei der JPGE-Komprimierung konzentriert sich pHash auf die Niederfrequenzkomponenten in der DCT des Bildes und hascht sie. Auf diese Weise ist es möglich, Merkmale, die vom Menschen leicht wahrgenommen werden, bevorzugt zu extrahieren, und es wird angenommen, dass ein robustes Hashing für die parallele Bewegung von Bildern und Änderungen der Helligkeit durchgeführt werden kann.

  1. Reduzieren Sie das Bild. Machen Sie es größer als 8x8 (zum Beispiel 32x32).
  2. Graustufen.
  3. DCT.
  4. Extrahieren Sie nur die Niederfrequenzkomponente 8x8.
  5. Berechnen Sie den Durchschnittswert der Niederfrequenzkomponenten ohne Gleichstromkomponenten.
  6. Binar je nachdem, ob es höher oder niedriger als der Durchschnittswert ist.
  7. Holen Sie sich einen 64-Bit-Hash, indem Sie ihn in einer bestimmten Reihenfolge anordnen, z. B. in der Reihenfolge des Raster-Scans.

Andere xHash

Es scheint verschiedene Variationen außer aHash und pHash zu geben. Einige Leute machen das Benchmarking [^ 1].

Experiment

Wenden Sie verschiedene Verarbeitungen auf das Bild an und vergleichen Sie den Hashwert mit dem Originalbild. Berechnen Sie aHash und pHash als Hash-Werte.

Probieren Sie auch die Technik aus, die Ausgabe als Hash für die Ebene unmittelbar vor der letzten Ebene von ResNet50 zu betrachten. Diese Methode wurde in einer Arbeit [^ 2] [^ 3] übernommen.

Code

OpenCV wird zur Berechnung von aHash und pHash verwendet, es gibt jedoch auch eine Bibliothek namens ImageHash. Außerdem können Sie in aHash und pHash den Brummabstand verwenden, um Hash-Werte zu vergleichen. Der Wertebereich ist "[0, 64]". Um diesem Bereich zu entsprechen, wird im Hash- (Schein-) Vergleich mit ResNet50 die Kosinusähnlichkeit berechnet und dann in den obigen Bereich konvertiert.

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):
    """
Eine Klasse, die experimentelle Bildpaare generiert
    """

    def __init__(self, img: np.ndarray):
        self._img = img
        self._processings = self._prepare_processings()

    def _prepare_processings(self):
        h, w, _ = self._img.shape
        #Position und Größe zum Zuschneiden um das Gesicht des Lenna-Bildes
        org = np.array([128, 128])
        size = np.array([256, 256])
        # kind (processing description), img1, img2
        processings = [
            ('Gleich',
             lambda x: x,
             lambda x: x),
            ('Graustufen',
             lambda x: x,
             lambda x: cv2.cvtColor(
                 cv2.cvtColor(x, cv2.COLOR_BGR2GRAY), cv2.COLOR_GRAY2BGR)),
            *list(map(lambda s:
                      (f'1/{s:2}Schrumpfen auf',
                       lambda x: x,
                       lambda x: cv2.resize(x, (w // s, h // s))),
                      np.power(2, range(1, 5)))),
            *list(map(lambda s:
                      (f'Glätten(kernel size = {s:2}',
                       lambda x: x,
                       lambda x: cv2.blur(x, (s, s))),
                      [3, 5, 7, 9, 11])),
            *list(map(lambda s:
                      (f'Text einfügen(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'JPEG-Komprimierung(quality = {q})',
                       lambda x: x,
                       lambda x: img_encode_decode(x, q)),
                      range(10, 100, 10))),
            *list(map(lambda gamma:
                      (f'Gamma-Korrektur(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'Parallelbewegung({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):
    """
Klasse zur Ausgabe der letzten Schicht von ResNet50 als Hashwert
    """
    _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):
        """
Berechnen Sie die Kosinusähnlichkeit. Der Wertebereich ist[0, 1]。
Vergleichen Sie aHash und pHash entsprechend der Brummstrecke.
        [0, 64]In den Wertebereich von konvertieren.
        """
        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:
    """
Beschneiden Sie einen beliebigen Bereich des Bildes.
    """
    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:
    """
Reproduzieren Sie die Verschlechterung der JPEG-Komprimierung.
Referenz: 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:
    """
Gamma-Korrektur.
Referenz: 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
        #Überprüfen Sie das Bild visuell (nur wenn die Form identisch ist, da die Ausrichtung schwierig ist).
        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()

Ergebnis

{'Gleich': {'ResNet': 0.0, 'aHash': 0.0, 'pHash': 0.0},
 'Graustufen': {'ResNet': 14.379967, 'aHash': 0.0, 'pHash': 0.0},
 '1/Reduziert auf 2': {'ResNet': 1.2773285, 'aHash': 3.0,  'pHash': 1.0},
 '1/Reduziert auf 4': {'ResNet': 6.5748253, 'aHash': 4.0,  'pHash': 1.0},
 '1/Reduziert auf 8': {'ResNet': 18.959282, 'aHash': 7.0,  'pHash': 3.0},
 '1/Reduziert auf 16': {'ResNet': 34.8299,   'aHash': 12.0, 'pHash': 0.0},
 'JPEG-Komprimierung(quality = 10)': {'ResNet': 6.4169083,  'aHash': 2.0, 'pHash': 0.0},
 'JPEG-Komprimierung(quality = 20)': {'ResNet': 2.6065674,  'aHash': 1.0, 'pHash': 0.0},
 'JPEG-Komprimierung(quality = 30)': {'ResNet': 1.8446579,  'aHash': 0.0, 'pHash': 0.0},
 'JPEG-Komprimierung(quality = 40)': {'ResNet': 1.2492218,  'aHash': 0.0, 'pHash': 1.0},
 'JPEG-Komprimierung(quality = 50)': {'ResNet': 1.0534592,  'aHash': 0.0, 'pHash': 0.0},
 'JPEG-Komprimierung(quality = 60)': {'ResNet': 0.99293137, 'aHash': 0.0, 'pHash': 0.0},
 'JPEG-Komprimierung(quality = 70)': {'ResNet': 0.7313309,  'aHash': 0.0, 'pHash': 0.0},
 'JPEG-Komprimierung(quality = 80)': {'ResNet': 0.58068085, 'aHash': 0.0, 'pHash': 0.0},
 'JPEG-Komprimierung(quality = 90)': {'ResNet': 0.354187,   'aHash': 0.0, 'pHash': 0.0},
 'Gamma-Korrektur(gamma = 0.2)': {'ResNet': 16.319721,  'aHash': 2.0, 'pHash': 1.0},
 'Gamma-Korrektur(gamma = 0.5)': {'ResNet': 4.2003975,  'aHash': 2.0, 'pHash': 0.0},
 'Gamma-Korrektur(gamma = 0.8)': {'ResNet': 0.48334503, 'aHash': 0.0, 'pHash': 0.0},
 'Gamma-Korrektur(gamma = 1.2)': {'ResNet': 0.381176,   'aHash': 0.0, 'pHash': 1.0},
 'Gamma-Korrektur(gamma = 1.5)': {'ResNet': 1.7187691,  'aHash': 2.0, 'pHash': 1.0},
 'Gamma-Korrektur(gamma = 2.0)': {'ResNet': 4.074257,   'aHash': 6.0, 'pHash': 2.0},
 'Text einfügen(fontScale = 1)': {'ResNet': 0.7838249, 'aHash': 0.0, 'pHash': 0.0},
 'Text einfügen(fontScale = 2)': {'ResNet': 1.0911484, 'aHash': 0.0, 'pHash': 1.0},
 'Text einfügen(fontScale = 3)': {'ResNet': 2.7721176, 'aHash': 0.0, 'pHash': 2.0},
 'Text einfügen(fontScale = 4)': {'ResNet': 4.646305,  'aHash': 0.0, 'pHash': 4.0},
 'Text einfügen(fontScale = 5)': {'ResNet': 8.435852,  'aHash': 2.0, 'pHash': 3.0},
 'Text einfügen(fontScale = 6)': {'ResNet': 11.267036, 'aHash': 6.0, 'pHash': 3.0},
 'Text einfügen(fontScale = 7)': {'ResNet': 15.272251, 'aHash': 2.0, 'pHash': 7.0},
 'Glätten(kernel size =  3': {'ResNet': 1.3798943, 'aHash': 2.0, 'pHash': 0.0},
 'Glätten(kernel size =  5': {'ResNet': 3.1528091, 'aHash': 4.0, 'pHash': 1.0},
 'Glätten(kernel size =  7': {'ResNet': 4.903698,  'aHash': 4.0, 'pHash': 1.0},
 'Glätten(kernel size =  9': {'ResNet': 6.8400574, 'aHash': 4.0, 'pHash': 1.0},
 'Glätten(kernel size = 11': {'ResNet': 9.477722,  'aHash': 5.0, 'pHash': 2.0},
 'Parallelbewegung( 1 pixels)': {'ResNet': 0.47764206, 'aHash': 6.0,  'pHash': 0.0},
 'Parallelbewegung( 2 pixels)': {'ResNet': 0.98942566, 'aHash': 10.0, 'pHash': 3.0},
 'Parallelbewegung( 4 pixels)': {'ResNet': 1.475399,   'aHash': 15.0, 'pHash': 5.0},
 'Parallelbewegung( 8 pixels)': {'ResNet': 2.587471,   'aHash': 20.0, 'pHash': 13.0},
 'Parallelbewegung(16 pixels)': {'ResNet': 3.1883087,  'aHash': 25.0, 'pHash': 21.0},
 'Parallelbewegung(32 pixels)': {'ResNet': 4.8445663,  'aHash': 23.0, 'pHash': 31.0},
 'Parallelbewegung(64 pixels)': {'ResNet': 9.34531,    'aHash': 28.0, 'pHash': 30.0}}

Erwägung

Bitte beachten Sie, dass ResNet das verwendet, was mit ImageNet vorab trainiert wurde. Mit anderen Worten, durch Aufbereiten von Trainingsdaten ist es möglich, ein Netzwerk mit Eigenschaften zu erhalten, die sich von den nachstehend beschriebenen unterscheiden.

Außerdem kann sich die Tendenz je nach Bild ändern. Für den praktischen Gebrauch ist es besser, mit einem anständigen Datensatz auszuwerten.

--Same: Offensichtlich ist der Abstand für alle Hashing-Methoden 0.

Zusammenfassung

Ich habe eine Methode zum Hashing von Bildern eingeführt. Zusätzlich haben wir den Abstand zwischen dem verarbeiteten Bild und dem Originalbild gemessen und die Eigenschaften jeder Hashing-Methode beobachtet. Unter den in diesem Artikel verglichenen Algorithmen können wir beobachten, dass pHash gegenüber Bildskalierung, Komprimierungsverschlechterung und Glättung tendenziell robust ist.

Ich habe kürzlich etwas über xHash-basierte Algorithmen gelernt und denke, dass sie einfach sind und auf guten Ideen basieren. Ich denke, es ist ein relativ toter Algorithmus, aber ich hoffe, er kann an der richtigen Stelle verwendet werden.

Referenz

--Versuchen Sie den Bildvergleich mit Perceptual Hash (Phash) http://hideack.hatenablog.com/entry/2015/03/16/194336

[^ 3]: Um die Wahrheit zu sagen, als ich diesen Artikel las, dachte ich: "Gibt es eine einfachere Möglichkeit, Duplikate zu identifizieren?", Was der Grund für das Schreiben dieses Artikels war.

Recommended Posts

Hashing-Algorithmus zur Bestimmung des gleichen Bildes
Programm zur Suche nach demselben Bild
Feststellen, ob das Bild Vögel enthält
Bildverarbeitung? Die Geschichte, Python für zu starten
Erkennen Sie Ordner mit demselben Bild in ImageHash
58 Das gleiche Schloss
Erstellt ein Narrbild für das Modell der Untertitelgenerierung
Python-Programm, das nach demselben Dateinamen sucht
Dikstra-Algorithmus für Anfänger
Probieren Sie die ähnliche Suche von Image Search mit Python SDK [Search] aus.
Die Bildanzeigefunktion von iTerm ist praktisch bei der Verarbeitung von Bildern.