You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
256 lines
10 KiB
256 lines
10 KiB
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)
|
|
}
|
|
}
|
|
}
|