指定フォルダ以下の画像(種別問わず)をすべてピックアップして、それぞれMD5チェックサムを計算し、重複しているものをピックアップしてデータとして出力するAppleScriptです。実行にはScript Debuggerを必要とします。内蔵のMD5計算Frameworkはx64/Apple Silicon(ARM64E)のUniversal Binaryでビルドしてあります。
–> –> Download photoDupLister(Script Bundle with Framework in its bundle)
# 本Scriptのファイル収集ルーチンが、再帰で下位フォルダの内容をすべてピックアップするものではありませんでした
# 実際に指定フォルダ以下すべての画像を収集する(Spotlightで)ようにしてみたら5.5万ファイルの処理に26分ほどかかりました
MD5チェックサムが同じということは、同じ画像である可能性が高いものです。
ここで、各画像のチェックサムを計算するさいに、サムネイルを生成してからチェックサムを計算するかどうかという話があります。
サムネイルを作成すべき派の言い分は、そのほうが計算量が減らせるし、同一画像の縮尺違い(拡大/縮小率違い)を求めることもできるというものです。
サムネイル作成否定派の言い分は、そんなもん作る前に画像のチェックサムを計算してしまえ、逆に手間だというものでした。
これは、どちらの意見ももっともだったので、実際にシミュレーションを行ってみるしかないでしょう。そこで、ありもののルーチンを集めて実際に作ってみたのが本Scriptです。サムネイルは作らないで処理してみました。
自分のMacBook Air M2のPicturesフォルダに入っていた約5,000の画像ファイルを処理したところ、16ペアの重複画像がみつかりました。処理にかかる時間はおよそ9秒です(実行するたびに所要時間が若干変化)。おそらく、Intel Macで実行すると数十秒から数分かかるのではないかと。
実用性を確保したい場合には、画像を回転しつつチェックサムを1画像あたり4パターン求めるとか、やはり同じサイズのサムネイル画像を生成してサムネイルに対してMD5チェックサムを計算するとか、画像の類似度を計算するオプションなども欲しいところです。
また、処理内容が並列処理向きなので、並列で処理してみてもよいでしょう。マシン環境を調べてSoCのPコアの個数をかぞえて、Pコアと同数の処理アプレットを生成して並列実行。……余計な処理を行うせいで速くならない可能性が高そうです。
AppleScript名:指定フォルダ以下の画像のMD5チェックサムを求めて、重複しているものをピックアップ |
— Created 2015-10-01 by Takaaki Naganoya — Modified 2015-10-01 by Shane Stanley–With Cocoa-Style Filtering — Modified 2018-12-01 by Takaaki Naganoya — Modified 2024-12-19 by Takaaki Naganoya use AppleScript version "2.8" use scripting additions use framework "Foundation" use framework "AppKit" use framework "md5Lib" –https://github.com/JoeKun/FileMD5Hash property |NSURL| : a reference to current application’s |NSURL| property NSArray : a reference to current application’s NSArray property NSPredicate : a reference to current application’s NSPredicate property NSCountedSet : a reference to current application’s NSCountedSet property NSURLTypeIdentifierKey : a reference to current application’s NSURLTypeIdentifierKey script spd property fList : {} property fRes : {} property md5List : {} property fmdList : {} property dupRes : {} property outList : {} end script set anUTI to "public.image" set aFol to choose folder –set aFol to path to pictures folder set (fList of spd) to getFilePathList(aFol) of me –指定のFile listのうち画像のみ抽出 set (fRes of spd) to filterAliasListByUTI((fList of spd), "public.image") of me if (fRes of spd) = {} then return –すべての画像のMD5チェックサムを計算 set (md5List of spd) to {} set (fmdList of spd) to {} repeat with i in (fRes of spd) set j to contents of i set md5Res to (current application’s FileHash’s md5HashOfFileAtPath:(j)) as string set the end of (md5List of spd) to md5Res set the end of (fmdList of spd) to {filePath:j, md5:md5Res} end repeat –チェックサムが重複している画像を取り出す set fmdArray to NSArray’s arrayWithArray:(fmdList of spd) set (dupRes of spd) to returnDuplicatesOnly((md5List of spd)) of me set (outList of spd) to {} set procMDs to {} repeat with i in (dupRes of spd) set j to contents of i if j is not in procMDs then set aRes to filterDictArrayByLabel(fmdArray, "md5 == ’" & j & "’") of me set tmpMD5 to filePath of (first item of aRes) set tmpRes to {} repeat with ii in aRes set jj to contents of ii set the end of tmpRes to filePath of jj end repeat set aRec to {md5:j, fileRes:tmpRes} set the end of (outList of spd) to aRec set the end of procMDs to j end if end repeat return (outList of spd) –リストに入れたレコードを、指定の属性ラベルの値で抽出 on filterDictArrayByLabel(aArray, aPredicate as string) –抽出 set aPredicate to current application’s NSPredicate’s predicateWithFormat:aPredicate set filteredArray to aArray’s filteredArrayUsingPredicate:aPredicate –NSArrayからListに型変換して返す set bList to filteredArray as list return bList end filterDictArrayByLabel on getFilePathList(aFol) set aURL to current application’s |NSURL|’s fileURLWithPath:(POSIX path of aFol) set aFM to current application’s NSFileManager’s defaultManager() set urlArray to aFM’s contentsOfDirectoryAtURL:aURL includingPropertiesForKeys:{} options:(current application’s NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value) return urlArray as anything end getFilePathList –Alias listから指定UTIに含まれるものをPOSIX pathのリストで返す on filterAliasListByUTI(aList, targUTI) set newList to {} repeat with i in aList set j to POSIX path of i set tmpUTI to my retUTIfromPath(j) set utiRes to my filterUTIList({tmpUTI}, targUTI) if utiRes is not equal to {} then set the end of newList to j end if end repeat return newList end filterAliasListByUTI –指定のPOSIX pathのファイルのUTIを求める on retUTIfromPath(aPOSIXPath) set aURL to |NSURL|’s fileURLWithPath:aPOSIXPath set {theResult, theValue} to aURL’s getResourceValue:(reference) forKey:NSURLTypeIdentifierKey |error|:(missing value) if theResult = true then return theValue as string else return theResult end if end retUTIfromPath –UTIリストが指定UTIに含まれているかどうか演算を行う on filterUTIList(aUTIList, aUTIstr) set anArray to NSArray’s arrayWithArray:aUTIList set aPred to NSPredicate’s predicateWithFormat_("SELF UTI-CONFORMS-TO %@", aUTIstr) set bRes to (anArray’s filteredArrayUsingPredicate:aPred) as list return bRes end filterUTIList on returnDuplicatesOnly(aList as list) set aSet to NSCountedSet’s alloc()’s initWithArray:aList set bList to (aSet’s allObjects()) as list set dupList to {} repeat with i in bList set aRes to (aSet’s countForObject:i) if aRes > 1 then set the end of dupList to (contents of i) end if end repeat return dupList end returnDuplicatesOnly |