package com.sothr.imagetools.hash import java.awt.image.BufferedImage import java.io.{File, FileInputStream} import javax.imageio.ImageIO import com.sothr.imagetools.hash.`type`.{AHash, DHash, PHash} import com.sothr.imagetools.hash.util.{HammingUtil, ImageUtil} import grizzled.slf4j.Logging import org.apache.commons.codec.digest.DigestUtils import resource.managed /** * A service that exposes the ability to construct perceptive hashes from an * image which can be used to find a perceptual difference between two or more * images */ object HashService extends Logging { /** * Given hash settings and an image path, calculate the perceptual hashes * * @param ahashSettings { @see com.sothr.imagetools.hash.HashSetting} * @param dhashSettings { @see com.sothr.imagetools.hash.HashSetting} * @param phashSettings { @see com.sothr.imagetools.hash.HashSetting} * @param imagePath Absolute path to the image file * @return { @see com.sothr.imagetools.hash.ImageHash} */ def getImageHashes(ahashSettings: HashSetting, dhashSettings: HashSetting, phashSettings: HashSetting, imagePath: String): ImageHash = { getImageHashes(ahashSettings, dhashSettings, phashSettings, ImageIO.read(new File(imagePath)), imagePath) } def getPrecisionMap(precisionSet: Set[Int], grayImage: BufferedImage): Map[Int, Array[Array[Int]]] = { precisionSet.map(p => p -> getImageData(p, grayImage, alreadyGray = true))(collection.breakOut) } /** * Given hash settings, a buffered image and an image path, calculate the perceptual hashes * * @param ahashSettings { @see com.sothr.imagetools.hash.HashSetting} * @param dhashSettings { @see com.sothr.imagetools.hash.HashSetting} * @param phashSettings { @see com.sothr.imagetools.hash.HashSetting} * @param image { @see java.awt.image.BufferedImage} * @param imagePath Absolute path to the image file * @return { @see com.sothr.imagetools.hash.ImageHash} * */ def getImageHashes(ahashSettings: HashSetting, dhashSettings: HashSetting, phashSettings: HashSetting, image: BufferedImage, imagePath: String): ImageHash = { debug(s"Creating hashes for $imagePath") //Get Image Data val grayImage = ImageUtil.convertToGray(image) val precisionSet: Set[Int] = getPrecisionSet(ahashSettings, dhashSettings, phashSettings) val precisionMap: Map[Int, Array[Array[Int]]] = getPrecisionMap(precisionSet, grayImage) val ahash: Long = if (ahashSettings.use && precisionMap.contains(ahashSettings.precision)) getHash(ahashSettings, precisionMap(ahashSettings.precision), AHash.getHash) else 0l val dhash: Long = if (dhashSettings.use && precisionMap.contains(dhashSettings.precision)) getHash(dhashSettings, precisionMap(dhashSettings.precision), DHash.getHash) else 0l val phash: Long = if (phashSettings.use && precisionMap.contains(phashSettings.precision)) getHash(phashSettings, precisionMap(phashSettings.precision), PHash.getHash) else 0l val sha1: String = getSHA1(imagePath) val hashes = new ImageHash(ahash, dhash, phash, sha1) debug(s"Generated hashes: $hashes") hashes } def getSHA1(filePath: String): String = { managed(new FileInputStream(filePath)) acquireAndGet { input => DigestUtils.sha1Hex(input) } } def getPrecisionSet(ahashSettings: HashSetting, dhashSettings: HashSetting, phashSettings: HashSetting): Set[Int] = { Set( if (ahashSettings.use) Option(ahashSettings.precision) else None, if (dhashSettings.use) Option(dhashSettings.precision) else None, if (phashSettings.use) Option(phashSettings.precision) else None ).flatten } def getImageData(precision: Int, image: BufferedImage, alreadyGray: Boolean): Array[Array[Int]] = { var grayImage: BufferedImage = null if (alreadyGray) { grayImage = image } else { grayImage = ImageUtil.convertToGray(image) } val resizedImage = ImageUtil.resize(grayImage, precision, forced = true) ImageUtil.getImageData(resizedImage) } /** * Simpler function that only works with the imageData and the hashFunction * * @param hashSettings * @param imageData * @param hashFunction * @return */ def getHash(hashSettings: HashSetting, imageData: Array[Array[Int]], hashFunction: Array[Array[Int]] => Long): Long = { if (hashSettings.use) { val hashResult = hashFunction(imageData) trace(s"${hashSettings.name} result: $hashResult") hashResult } else { 0l } } def getAhash(hashSettings: HashSetting, image: BufferedImage, alreadyGray: Boolean = false): Long = { getHash(hashSettings, image, alreadyGray, AHash.getHash) } /** * Internal function to retrieve the hash from a processed image, for a given hash function * * @param hashSettings A HashSettings that is used for the processing parameters of the Image * @param image BufferedImage representing the image data to calculate the hash for * @param alreadyGray Indicator that the grayscale processing has already been applied to the incoming image * @param hashFunction The function accepting an two dimensional array of Int and returning the hash of the data * @return */ def getHash(hashSettings: HashSetting, image: BufferedImage, alreadyGray: Boolean = false, hashFunction: Array[Array[Int]] => Long): Long = { if (hashSettings.use) { var grayImage: BufferedImage = null if (alreadyGray) { grayImage = image } else { grayImage = ImageUtil.convertToGray(image) } val resizedImage = ImageUtil.resize(grayImage, hashSettings.precision, forced = true) val hashResult = hashFunction(ImageUtil.getImageData(resizedImage)) trace(s"${hashSettings.name} result: $hashResult") hashResult } else { trace(s"${hashSettings.name} result: DISABLED") 0l } } def getDhash(hashSettings: HashSetting, image: BufferedImage, alreadyGray: Boolean = false): Long = { getHash(hashSettings, image, alreadyGray, DHash.getHash) } def getPhash(hashSettings: HashSetting, image: BufferedImage, alreadyGray: Boolean = false): Long = { getHash(hashSettings, image, alreadyGray, PHash.getHash) } def getMD5(filePath: String): String = { managed(new FileInputStream(filePath)) acquireAndGet { input => DigestUtils.md5Hex(input) } } def areHashSimilar(hashSettings: HashSetting, hash1: Long, hash2: Long): Boolean = { val tolerance = hashSettings.tolerance val distance = HammingUtil.getDistance(hash1, hash2) if (distance <= tolerance) true else false } def areImageHashesSimilar(ahashSettings: HashSetting, dhashSettings: HashSetting, phashSettings: HashSetting, imageHash1: ImageHash, imageHash2: ImageHash): Boolean = { val weightedHammingMean = getWeightedHashSimilarity(ahashSettings, dhashSettings, phashSettings, imageHash1, imageHash2) val weightedToleranceMean = getWeightedHashTolerance(ahashSettings, dhashSettings, phashSettings) if (weightedHammingMean <= weightedToleranceMean) true else false } def getWeightedHashSimilarity(ahashSettings: HashSetting, dhashSettings: HashSetting, phashSettings: HashSetting, imageHash1: ImageHash, imageHash2: ImageHash): Float = { val (weightedHammingTotal1, methodsTotal1) = sumWeightedHammingAndMethodCount(ahashSettings, imageHash1.ahash, imageHash2.ahash, 0f, 0) val (weightedHammingTotal2, methodsTotal2) = sumWeightedHammingAndMethodCount(dhashSettings, imageHash1.dhash, imageHash2.dhash, weightedHammingTotal1, methodsTotal1) val (weightedHammingTotalFinal, methodsTotalFinal) = sumWeightedHammingAndMethodCount(phashSettings, imageHash1.phash, imageHash2.phash, weightedHammingTotal2, methodsTotal2) val weightedHammingMean = weightedHammingTotalFinal / methodsTotalFinal debug(s"Calculated Weighted HammingUtil Mean: $weightedHammingMean") weightedHammingMean } def sumWeightedHammingAndMethodCount(hashSettings: HashSetting, hash1: Long, hash2: Long, weightedHamming: Float, methodCount: Int): (Float, Int) = { val (newWeightedHamming, newMethodCount) = getWeightedHamming(hashSettings, hash1, hash2) (weightedHamming + newWeightedHamming, methodCount + newMethodCount) } def getWeightedHamming(hashSettings: HashSetting, hash1: Long, hash2: Long): (Float, Int) = { if (hashSettings.use) { val hamming = HammingUtil.getDistance(hash1, hash2) trace(s"${hashSettings.name} Hamming: hash1: $hash1 hash2: $hash2 tolerance: ${hashSettings.tolerance} hamming distance: $hamming weight: ${hashSettings.weight}") (hamming * hashSettings.weight, 1) } else { trace(s"${hashSettings.name} Hamming: DISABLED") (0f, 0) } } def getWeightedHashTolerance(ahashSettings: HashSetting, dhashSettings: HashSetting, phashSettings: HashSetting): Float = { val (weightedToleranceTotal1, methodsTotal1) = sumWeightedToleranceAndMethodCount(ahashSettings, 0f, 0) val (weightedToleranceTotal2, methodsTotal2) = sumWeightedToleranceAndMethodCount(dhashSettings, weightedToleranceTotal1, methodsTotal1) val (weightedToleranceTotalFinal, methodsTotalFinal) = sumWeightedToleranceAndMethodCount(phashSettings, weightedToleranceTotal2, methodsTotal2) val weightedTolerance = weightedToleranceTotalFinal / methodsTotalFinal debug(s"Calculated Weighted Tolerance: $weightedTolerance") weightedTolerance } def sumWeightedToleranceAndMethodCount(hashSettings: HashSetting, weightedTolerance: Float, methodCount: Int): (Float, Int) = { val (newWeightedTolerance, newMethodCount) = getWeightedTolerance(hashSettings) (weightedTolerance + newWeightedTolerance, methodCount + newMethodCount) } def getWeightedTolerance(hashSettings: HashSetting): (Float, Int) = { if (hashSettings.use) { val weightedTolerance = hashSettings.tolerance * hashSettings.weight trace(s"${hashSettings.name} Tolerance: ${hashSettings.tolerance} Current Weighted Tolerance: $weightedTolerance") (weightedTolerance, 1) } else { trace(s"${hashSettings.name} Tolerance: DISABLED") (0f, 0) } } }