同じ画像を検索して削除する
07
- 2月
2019
Posted By : boomin
同じ画像を検索して削除する
Advertisements

1. 同じ画像を検索して、比較し、同じならば削除する

前回、機械学習用の画像の前処理として、 教師データが重複する可能性を検討しました。

この時は、同じファイル名かつ、同じサイズ、同じチャンネル数の画像は、同じ画像である と判断して 削除するということを行いました。

が、改めてよくよく収集した画像たちを見ると、同じ画像だけど大きさが異なる(リサイズされたもの) が、時々あることが判明ェ。。。。。。

これでは、本命/義理がどう組み合わさろうとも、全く同じ画像が複数枚存在してしまっては、学習に差し障ります。 そこで、学習データ中から同じ画像を検索し、一方のみを残し一方を削除することを行います。

2. 処理手順

2.1 同じ画像を検索する

画像の検索ロジックとしては、

  1. 本命チョコ画像と義理チョコ画像の、両方の全画像一覧を作成
  2. 一覧中から1つの画像を選択し、当該画像以外のすべての画像との組み合わせで類似性を評価
  3. ある基準以上に類似性が認められば、同じ画像として判定し削除
Advertisements

2.2 同じ画像かどうか判定する

こんなディレクトリ構造とします。

├─preprocess.py   # 前処理本体
├─ImageTooles.py  # preprocess.pyから呼び出される関数をまとめた部品
└──images
   ├─compImg  # 以下のcode実行で、同じと判定された画像の比較エビデンスを格納
   ├─giri     # 学習用の義理チョコ画像を格納
   └─honmei   # 学習用の本命チョコ画像を格納
  • ImageTooles.py
# -*- coding: utf-8 -*-
"""
Created on Sat Feb  2 13:32:13 2019
@author: boomin
"""

import os
from PIL import Image
import cv2
import numpy as np
import glob
import pandas as pd

def getImages(honImgDir, girImgDir):
  # 本命チョコ写真リスト
  hmImg  = glob.glob(honImgDir + "*.*")
  hmImg = pd.DataFrame({
    "filename":[os.path.basename(f) for f in hmImg],
    "fullpath":[f for f in hmImg]
  })
  # 義理チョコ写真リスト
  grImg  = glob.glob(girImgDir + "*.*")
  grImg = pd.DataFrame({
    "filename":[os.path.basename(f) for f in grImg],
    "fullpath":[f for f in grImg]
  })
  # 本命チョコと義理チョコのファイルを1つのDaraFrameににまとめる
  ifiles = pd.concat([hmImg,grImg], axis=0).reset_index(drop=True)
  
  # 画像shapeを取得
  ifiles["shape"] = [ getImageSize(x) for x in ifiles["fullpath"] ]
  
  # アスペクト比を追加
  ifiles["aratio"] = [ round(w/h, 2) if h!=0 else 0 for (w,h,c) in ifiles["shape"] ]

  return ifiles.set_index("fullpath")

def getImageSize(im):
  try:
    rimf = Image.open(im)
    isize = np.asarray(rimf).shape
    return isize if len(isize)==3 else (isize[0],isize[1],0)
  except:
    return (0,0,0)

# 同じ画像と判定された画像が見つかったら、すべて削除する
def removefile(file):
  if os.path.exists(file):
    os.remove(file)
    print(f"\tdeleted {file}")
    return file
  else:
    return ""

def convertImage(fpath,shape):
  root, ext = os.path.splitext(fpath)
  try:
    if len(shape)<3 or shape[2]!=3:
      print(f"converted from {shape} : {os.path.basename(fpath)}")
      rimf  = Image.open(fpath)
      rgb_im = rimf.convert('RGB')
      # 変換画像を保存
      rgb_im.save(f"{root}.jpg")
      # 変換元画像を削除
      removefile(fpath)
      return 1
    else:
      return 0
  except:
    try:
      removefile(fpath)
      print(f"\t removing by error: {fpath} ")
    except:
      print(f"cannot remove {fpath}")
    finally:
      return 0
      
def getImageProp(im1,im2):
  IMG_SIZE = (96, 96)
  img1 = cv2.resize(cv2.imread(im1), IMG_SIZE)
  img2 = cv2.resize(cv2.imread(im2), IMG_SIZE)
  gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
  gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
  detector = cv2.AKAZE_create()
  kp1, des1 = detector.detectAndCompute(gray1, None)
  kp2, des2 = detector.detectAndCompute(gray2, None)  

  bf = cv2.BFMatcher(cv2.NORM_HAMMING)
  matches = bf.match(des1, des2)
  
  return matches, img1, img2, kp1, kp2

def getSimFeature(im1,im2):
  try:
    matches, img1, img2, kp1, kp2 = getImageProp(im1,im2)
    dist = [m.distance for m in matches]
    ret = sum(dist) / len(dist)
    if ret<10:
      dname = "images" + os.sep + "compImg" + os.sep
      fname = os.path.basename(im2)
      showImg(dname+"comp_"+fname, matches, img1, img2, kp1, kp2)
    return ret
  except:
    return 100000
  
def feature_detection(im1,splist):
  return [ (im2, getSimFeature(im1,im2)) for im2 in splist ]


import matplotlib.pyplot as plt
def showImg(title, matches, img1, img2, kp1, kp2):
	matches = sorted(matches, key = lambda x:x.distance)
	img3 = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, flags=2)
	plt.imshow(cv2.cvtColor(img3, cv2.COLOR_BGR2RGB))
	plt.savefig(f"{title}.png", bbox_inches='tight')

  • preprocess.py
# -*- coding: utf-8 -*-
"""
Created on Wed Jan 15 14:00:06 2019
@author: boomin
"""

import os
import numpy as np

import multiprocessing
from joblib import Parallel, delayed
from itertools import chain

from ImageTooles import getImages, convertImage, feature_detection, removefile

# 本命チョコ画像の格納ディレクトリ
honImgDir  = "images" + os.sep + "honmei" + os.sep
# 義理チョコ画像の格納ディレクトリ
girImgDir  = "images" + os.sep + "giri"   + os.sep

print("loading Image files...")
ifiles = getImages(honImgDir, girImgDir)

print("deleting images which have strange aspect ratio...")
# アスペクト比が、極端に大きかったり小さかったりするものは削除する
iratio = 16./9. # アスペクト比の閾値は16:9にする
_ = [ removefile(fpath) for fpath in ifiles[(ifiles["aratio"]>iratio)|(ifiles["aratio"]<1/iratio)|(ifiles["aratio"]==0)].index ]
print(f"\tdelete {len(_)} files")

print("deleting images which have same shape and the name...")
# ファイル名と画像サイズ、チャンネル数を元に、重複しているファイルを抽出して削除
dupfiles = ifiles[ifiles.duplicated(subset=['filename', 'shape'], keep=False)]
_ = [ removefile(fpath) for fpath in dupfiles.index ]
print(f"\tdelete {len(_)} files")

# 画像の変換を行う
print("converting file format...")
done = [ convertImage(fpath,shape) for fpath,shape in zip(ifiles.index, ifiles['shape']) ]
print(f"{sum(done)} files are converted.")

# 今残っているファイル一覧を取得しなおす
print("re-get all target files...")
ifiles = getImages(honImgDir, girImgDir)

# 削除対象とする類似度の閾値
th = 10

print("deleting images which have same features...")
donelist = []
# 画像の特徴からして、同じものと判定された画像を削除
for i, im1 in enumerate(ifiles.index):
  # 処理済listに追加
  donelist.append(im1)
  # 対象と、同じようなアスペクト比の画像を抽出
  nsp = ifiles.at[im1,"aratio"]
  # 同じようなアスペクト比の画像であれば、im1の画像はdonelistへの追加処理処理時点で評価されたはずだから、これを除外する
  silist = ifiles[(ifiles["aratio"]>nsp*0.99)&(ifiles["aratio"]<nsp*1.01)]
  silist = [ n for n in silist.index if n not in donelist ]
  print(f"{i}/{len(ifiles)}: proving {len(silist)} files")
  # 画像の類似の評価はCPUを使うので、並列処理させる
  n_jobs = multiprocessing.cpu_count()
  splited = np.array_split(silist, n_jobs)
  results = Parallel(n_jobs=n_jobs, verbose=0) (
    [ delayed(feature_detection)(im1,splist) for splist in splited ]
  )
  # 結果をまとめる
  results = list(chain.from_iterable(results))
  # 類似度が閾値以下のものを抽出
  _ = [ removefile(f) for f,v in results if v<th ]

画像同士を総当たりで比較する必要があるため、非常に処理時間がかかってしまいます。 そこで、

  • 同じようなアスペクト比の画像だけを類似評価の比較対象にする
  • multiprocessingで並列処理をさせる

ことで、処理時間の短縮を図っています。

2.3 同じ画像として判定された画像の比較

こんな風に、同じ画像だと判断されたら、その比較画像として特徴点とともに出力します。 たとえば、こんな感じです。

確かに、特徴的な点が示されていて、同じ画像であることがわかりますね! 一覧にすると、こんな感じでした。

これらの画像を削除することで、いよいよ学習データがそろいました。 次回以降、機械学習のモデルなどについて書いていきます。


Advertisements

コメントを残す