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

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)
}
}
}