もっと詳しく

挨拶と前書き

みなさんこんにちは、ウーバーイーツやりすぎてめっちゃ首とか腕とか足とか痛いなおこです。

今回は、類似ハッシュというものを使って高画質じゃない画像を削除したい!という問題を解決していく。

例えば、いいなって思った可愛い2次元キャラのイラストを保存したりしてると、過去に保存してたりと、まあかぶって保存しちゃうときあるんですね。

そういうときに高画質の画像のみを残して削除しちゃえというのを自動化したいと思います。

イラストAC素材を使用して解説

本題

必要なライブラリ

使うライブラリは下記の通りです。

  • os
  • PIL(Pillowのフォーク版)
  • imagehash

GitHub – python-pillow/Pillow: The friendly PIL fork (Python Imaging Library)
The friendly PIL fork (Python Imaging Library). Contribute to python-pillow/Pillow development by creating an account on GitHub.

GitHub – JohannesBuchner/imagehash: A Python Perceptual Image Hashing Module
A Python Perceptual Image Hashing Module. Contribute to JohannesBuchner/imagehash development by creating an account on GitHub.

osライブラリ以外は事前にpipコマンドなどでインストールしておいてください。

sudo pip install imagehash
sudo pip install PIL

プログラムを書いていく

プログラムの解説はいらん全て見たいんじゃって方は下記のボタンをタップやクリックで表示してください。

import os
from PIL import Image
import imagehash

# 比較対象のディレクトリを指定
targetpath = './'  

# プログラム内で使う変数を初期化
imgf = [] # 画像ファイルのpathを保存する変数
imgd = [] # 最後の削除する画像ファイルを保存する変数
imgs = {} # 一致しなかった際に一時的な変数。

# 指定されたディレクトリ内からファイル一覧を回しjpeg、jpg、png拡張子のpathを配列に入れる。
for i in [os.path.join(targetpath, path) for path in os.listdir(targetpath)]:
    if i.endswith('.jpeg') or i.endswith('.jpg') or i.endswith('.png'):
        imgf.append(i)

# 取得した画像のpathデータをループに回し類似ハッシュで検証していく
for img in imgf:
    hash = imagehash.average_hash(Image.open(img))
    # 類似ハッシュが含まれる場合
    if hash in imgs:
        # 比較対象の横幅縦幅を取得
        w1, h1= Image.open(img).size
        w2, h2= Image.open(imgs[hash]).size
        # 横幅と縦幅を合計した数値が小さい方を削除対象にする。
        if w1+h1 > w2+h2:
            imgd.append(imgs[hash])
        else:
            imgd.append(img)
    # 類似ハッシュが含まれない場合
    else:
        imgs[hash] = img

# 削除対象の画像ファイルを削除する
for img in list(set(imgd)):
    os.remove(img)
    print("DEL"+img)

ライブラリを読み込む

まずはpythonでおなじみのライブラリを読み込む部分ですね

os、imagehashに関してはそのまま読み込んでPILだけは名前を変更してインポートしましたがここは好きなようにって感じで。

import os
import imagehash
from PIL import Image

変数の初期化

プログラム内で使う変数を初期化や設定しておきます。

targetpathには画像解析したいpath(フォルダの場所)を指定してください

imgdやimgfは配列を扱うため[]を代入しておきます。

imgsに関してはImageHashのデータを入れるため[]ではなく{}を入れます。



# 比較対象のディレクトリを指定
targetpath = '/hogehoge/'  # 任意で変更

# プログラム内で使う変数を初期化
imgf = [] # 画像ファイルのpathを保存する変数
imgd = [] # 最後の削除する画像ファイルを保存する変数
imgs = {} # 一致しなかった際に一時的な変数。

画像ファイルのpathを読み込む

osライブラリ用いてファイルやディレクトリのリストを取得してfor文で一つずつ文字列の最後が画像拡張の.jpgや.jpeg、pngかどうかを比較して画像拡張だったら配列変数に追記していく形です。



# 指定されたディレクトリ内からファイル一覧を回しjpeg、jpg、png拡張子のpathを配列に入れる。
for i in [os.path.join(targetpath, path) for path in os.listdir(targetpath)]:
    if i.endswith('.jpeg') or i.endswith('.jpg') or i.endswith('.png'):
        imgf.append(i)

このやり方だと例外処理がないため、画像じゃないときでも拡張子が.jpgや.png、.jpegだと認識してしまう。ファイルが破損してたり、中身が違うけど拡張子だけ.pngみたいな状態だと次のプログラム部分でエラー出たりします。

画像のリストを使って削除対象を厳選していく

先程配列に追記した変数をfor文でループを行い、1枚ずつ類似ハッシュを生成し、類似ハッシュが比較先に含まれる場合は縦と横のpx数を足し小さい方を削除対象の配列に入れる処理を行っています。




# 取得した画像のpathデータをループに回し類似ハッシュで検証していく
for img in imgf:
    hash = imagehash.average_hash(Image.open(img))
    # 類似ハッシュが含まれる場合
    if hash in imgs:
        # 比較対象の横幅縦幅を取得
        w1, h1= Image.open(img).size
        w2, h2= Image.open(imgs[hash]).size
        # 横幅と縦幅を合計した数値が小さい方を削除対象にする。
        if w1+h1 > w2+h2:
            imgd.append(imgs[hash])
        else:
            imgd.append(img)
    # 類似ハッシュが含まれない場合
    else:
        imgs[hash] = img

削除対象のリストを元に画像ファイルを削除する

最後に先程厳選した削除対象配列をlist関数とset関数で重複内容を統合しfor文で一つずつ削除して全処理は完了。



# 削除対象の画像ファイルを削除する
for img in list(set(imgd)):
    os.remove(img)
    print("DEL"+img)

実際に使ってみた

今回書いたプログラムを使ってダウンロードフォルダの画像を整理してみました。

18件ぐらい似たハッシュの画像があったぽい。こういうのが溜まっていくとめんどいし一括削除できて便利。

最後に

実際使うとしたら削除する前に確認画面あったほうがいいかも思ったところや一部の画像でWARNINGが出る問題の修正という改善点は見えた。

PILを使ったけど内容的にはOpenCVの方でもできそうだし機械学習とかで今回のプログラムみたいなことを行う場合はOpenCVで完結させたほうがスマートかもしれない。

色々書いたけど画像の類似ハッシュはなかなか面白いという話でした。

今回のブログで参考にしたサイト↓

The post Python 類似ハッシュ用いて 類似画像を削除する方法 imagehash Pillow first appeared on FascodeNetwork Blog.