Drew Short
7 years ago
33 changed files with 709 additions and 499 deletions
-
8engine/pom.xml
-
4engine/src/main/resources/application.conf
-
2engine/src/main/resources/hibernate/Image.hbm.xml
-
2engine/src/main/resources/hibernate/ImageHash.hbm.xml
-
10engine/src/main/scala/com/sothr/imagetools/engine/ConcurrentEngine.scala
-
210engine/src/main/scala/com/sothr/imagetools/engine/hash/HashService.scala
-
25engine/src/main/scala/com/sothr/imagetools/engine/image/Image.scala
-
147engine/src/main/scala/com/sothr/imagetools/engine/image/ImageService.scala
-
54engine/src/main/scala/com/sothr/imagetools/engine/util/PropertiesService.scala
-
8engine/src/main/scala/com/sothr/imagetools/engine/vo/ImageHashVO.scala
-
4engine/src/test/resources/application.conf
-
2engine/src/test/resources/hibernate/Image.hbm.xml
-
2engine/src/test/resources/hibernate/ImageHash.hbm.xml
-
121hash/pom.xml
-
BINhash/sample/sample_01_large.jpg
-
BINhash/sample/sample_01_medium.jpg
-
BINhash/sample/sample_01_small.jpg
-
186hash/src/main/scala/com/sothr/imagetools/hash/HashService.scala
-
8hash/src/main/scala/com/sothr/imagetools/hash/dto/HashSettingDTO.scala
-
46hash/src/main/scala/com/sothr/imagetools/hash/dto/ImageHashDTO.scala
-
8hash/src/main/scala/com/sothr/imagetools/hash/type/AHash.scala
-
8hash/src/main/scala/com/sothr/imagetools/hash/type/DHash.scala
-
6hash/src/main/scala/com/sothr/imagetools/hash/type/PHash.scala
-
2hash/src/main/scala/com/sothr/imagetools/hash/type/PerceptualHasher.scala
-
18hash/src/main/scala/com/sothr/imagetools/hash/util/HammingUtil.scala
-
117hash/src/main/scala/com/sothr/imagetools/hash/util/ImageUtil.scala
-
35hash/src/main/scala/com/sothr/imagetools/hash/util/TimingUtil.scala
-
9hash/src/test/scala/com/sothr/imagetools/hash/BaseTest.scala
-
141hash/src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala
-
7hash/src/test/scala/com/sothr/imagetools/hash/TestParams.scala
-
6parent/pom.xml
-
2pom.xml
@ -1,210 +0,0 @@ |
|||
package com.sothr.imagetools.engine.hash |
|||
|
|||
import java.awt.image.BufferedImage |
|||
import java.io.{File, FileInputStream} |
|||
import javax.imageio.ImageIO |
|||
|
|||
import com.sothr.imagetools.engine.dto.ImageHashDTO |
|||
import com.sothr.imagetools.engine.image.ImageService |
|||
import com.sothr.imagetools.engine.util.{Hamming, PropertiesService} |
|||
import grizzled.slf4j.Logging |
|||
import org.apache.commons.codec.digest.DigestUtils |
|||
import resource._ |
|||
|
|||
/** |
|||
* 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 { |
|||
|
|||
def getImageHashes(imagePath: String): ImageHashDTO = { |
|||
//debug(s"Creating hashes for $imagePath") |
|||
getImageHashes(ImageIO.read(new File(imagePath)), imagePath) |
|||
} |
|||
|
|||
def getImageHashes(image: BufferedImage, imagePath: String): ImageHashDTO = { |
|||
//debug("Creating hashes for an image") |
|||
|
|||
var ahash: Long = 0L |
|||
var dhash: Long = 0L |
|||
var phash: Long = 0L |
|||
val sha1: String = getSHA1(imagePath) |
|||
|
|||
//Get Image Data |
|||
val grayImage = ImageService.convertToGray(image) |
|||
|
|||
if (PropertiesService.useAhash) { |
|||
ahash = getAhash(grayImage, alreadyGray = true) |
|||
} |
|||
if (PropertiesService.useDhash) { |
|||
dhash = getDhash(grayImage, alreadyGray = true) |
|||
} |
|||
if (PropertiesService.usePhash) { |
|||
phash = getPhash(grayImage, alreadyGray = true) |
|||
} |
|||
|
|||
val hashes = new ImageHashDTO(ahash, dhash, phash, sha1) |
|||
debug(s"Generated hashes: $hashes") |
|||
|
|||
hashes |
|||
} |
|||
|
|||
def getAhash(image: BufferedImage, alreadyGray: Boolean = false): Long = { |
|||
//debug("Started generating an AHash") |
|||
var grayImage: BufferedImage = null |
|||
if (alreadyGray) { |
|||
grayImage = image |
|||
} else { |
|||
grayImage = ImageService.convertToGray(image) |
|||
} |
|||
val resizedImage = ImageService.resize(grayImage, PropertiesService.aHashPrecision, forced = true) |
|||
val imageData = ImageService.getImageData(resizedImage) |
|||
AHash.getHash(imageData) |
|||
} |
|||
|
|||
def getDhash(image: BufferedImage, alreadyGray: Boolean = false): Long = { |
|||
//debug("Started generating an DHash") |
|||
var grayImage: BufferedImage = null |
|||
if (alreadyGray) { |
|||
grayImage = image |
|||
} else { |
|||
grayImage = ImageService.convertToGray(image) |
|||
} |
|||
val resizedImage = ImageService.resize(grayImage, PropertiesService.dHashPrecision, forced = true) |
|||
val imageData = ImageService.getImageData(resizedImage) |
|||
DHash.getHash(imageData) |
|||
} |
|||
|
|||
def getPhash(image: BufferedImage, alreadyGray: Boolean = false): Long = { |
|||
//debug("Started generating an PHash") |
|||
var grayImage: BufferedImage = null |
|||
if (alreadyGray) { |
|||
grayImage = image |
|||
} else { |
|||
grayImage = ImageService.convertToGray(image) |
|||
} |
|||
val resizedImage = ImageService.resize(grayImage, PropertiesService.pHashPrecision, forced = true) |
|||
val imageData = ImageService.getImageData(resizedImage) |
|||
PHash.getHash(imageData) |
|||
} |
|||
|
|||
def getMD5(filePath: String): String = { |
|||
managed(new FileInputStream(filePath)) acquireAndGet { |
|||
input => DigestUtils.md5Hex(input) |
|||
} |
|||
} |
|||
|
|||
def getSHA1(filePath: String): String = { |
|||
managed(new FileInputStream(filePath)) acquireAndGet { |
|||
input => DigestUtils.sha1Hex(input) |
|||
} |
|||
} |
|||
|
|||
def areAhashSimilar(ahash1: Long, ahash2: Long): Boolean = { |
|||
val tolerence = PropertiesService.aHashTolerance |
|||
val distance = Hamming.getDistance(ahash1, ahash2) |
|||
//debug(s"hash1: $ahash1 hash2: $ahash2 tolerence: $tolerence hamming distance: $distance") |
|||
if (distance <= tolerence) true else false |
|||
} |
|||
|
|||
def areDhashSimilar(dhash1: Long, dhash2: Long): Boolean = { |
|||
val tolerence = PropertiesService.dHashTolerance |
|||
val distance = Hamming.getDistance(dhash1, dhash2) |
|||
//debug(s"hash1: $dhash1 hash2: $dhash2 tolerence: $tolerence hamming distance: $distance") |
|||
if (distance <= tolerence) true else false |
|||
} |
|||
|
|||
def arePhashSimilar(phash1: Long, phash2: Long): Boolean = { |
|||
val tolerence = PropertiesService.pHashTolerance |
|||
val distance = Hamming.getDistance(phash1, phash2) |
|||
//debug(s"hash1: $phash1 hash2: $phash2 tolerence: $tolerence hamming distance: $distance") |
|||
if (distance <= tolerence) true else false |
|||
} |
|||
|
|||
def areImageHashesSimilar(imageHash1: ImageHashDTO, imageHash2: ImageHashDTO): Boolean = { |
|||
val weightedHammingMean = getWeightedHashSimilarity(imageHash1, imageHash2) |
|||
val weightedToleranceMean = getWeightedHashTolerence |
|||
if (weightedHammingMean <= weightedToleranceMean) true else false |
|||
} |
|||
|
|||
def getWeightedHashSimilarity(imageHash1: ImageHashDTO, imageHash2: ImageHashDTO): Float = { |
|||
//ahash |
|||
val aHashTolerance = PropertiesService.aHashTolerance |
|||
val aHashWeight = PropertiesService.aHashWeight |
|||
val useAhash = PropertiesService.useAhash |
|||
//dhash |
|||
val dHashTolerance = PropertiesService.dHashTolerance |
|||
val dHashWeight = PropertiesService.dHashWeight |
|||
val useDhash = PropertiesService.useAhash |
|||
//phash |
|||
val pHashTolerance = PropertiesService.pHashTolerance |
|||
val pHashWeight = PropertiesService.pHashWeight |
|||
val usePhash = PropertiesService.useAhash |
|||
|
|||
//calculate weighted values |
|||
var weightedHammingTotal: Float = 0 |
|||
var methodsTotal = 0 |
|||
|
|||
if (useAhash) { |
|||
val hamming = Hamming.getDistance(imageHash1.ahash, imageHash2.ahash) |
|||
weightedHammingTotal += hamming * aHashWeight |
|||
//debug(s"hash1: ${imageHash1.ahash} hash2: ${imageHash1.ahash} tolerence: $aHashTolerance hamming distance: $hamming weight: $aHashWeight") |
|||
methodsTotal += 1 |
|||
} |
|||
if (useDhash) { |
|||
val hamming = Hamming.getDistance(imageHash1.dhash, imageHash2.dhash) |
|||
weightedHammingTotal += hamming * dHashWeight |
|||
//debug(s"hash1: ${imageHash1.dhash} hash2: ${imageHash1.dhash} tolerence: $dHashTolerance hamming distance: $hamming weight: $dHashWeight") |
|||
methodsTotal += 1 |
|||
} |
|||
if (usePhash) { |
|||
val hamming = Hamming.getDistance(imageHash1.phash, imageHash2.phash) |
|||
weightedHammingTotal += hamming * pHashWeight |
|||
//debug(s"hash1: ${imageHash1.phash} hash2: ${imageHash1.phash} tolerence: $pHashTolerance hamming distance: $hamming weight: $pHashWeight") |
|||
methodsTotal += 1 |
|||
} |
|||
val weightedHammingMean = weightedHammingTotal / methodsTotal |
|||
//debug(s"Calculated Weighted Hamming Mean: $weightedHammingMean") |
|||
weightedHammingMean |
|||
} |
|||
|
|||
def getWeightedHashTolerence: Float = { |
|||
//ahash |
|||
val aHashTolerance = PropertiesService.aHashTolerance |
|||
val aHashWeight = PropertiesService.aHashWeight |
|||
val useAhash = PropertiesService.useAhash |
|||
//dhash |
|||
val dHashTolerance = PropertiesService.dHashTolerance |
|||
val dHashWeight = PropertiesService.dHashWeight |
|||
val useDhash = PropertiesService.useAhash |
|||
//phash |
|||
val pHashTolerance = PropertiesService.pHashTolerance |
|||
val pHashWeight = PropertiesService.pHashWeight |
|||
val usePhash = PropertiesService.useAhash |
|||
|
|||
//calculate weighted values |
|||
var weightedToleranceTotal: Float = 0 |
|||
var methodsTotal = 0 |
|||
|
|||
if (useAhash) { |
|||
weightedToleranceTotal += aHashTolerance * aHashWeight |
|||
//debug(s"Ahash Tolerance: $aHashTolerance Current Weighted Tolerance: $weightedToleranceTotal") |
|||
methodsTotal += 1 |
|||
} |
|||
if (useDhash) { |
|||
weightedToleranceTotal += dHashTolerance * dHashWeight |
|||
//debug(s"Dhash Tolerance: $dHashTolerance Current Weighted Tolerance: $weightedToleranceTotal") |
|||
methodsTotal += 1 |
|||
} |
|||
if (usePhash) { |
|||
weightedToleranceTotal += pHashTolerance * pHashWeight |
|||
//debug(s"Phash Tolerance: $pHashTolerance Current Weighted Tolerance: $weightedToleranceTotal") |
|||
methodsTotal += 1 |
|||
} |
|||
val weightedTolerance = weightedToleranceTotal / methodsTotal |
|||
//debug(s"Calculated Weighted Tolerance: $weightedTolerance") |
|||
weightedTolerance |
|||
} |
|||
|
|||
} |
@ -0,0 +1,121 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<groupId>com.sothr.imagetools</groupId> |
|||
<artifactId>Parent</artifactId> |
|||
<version>1.0.0</version> |
|||
<relativePath>../parent</relativePath> |
|||
</parent> |
|||
|
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<artifactId>ImageTools-Hash</artifactId> |
|||
<version>0.1.0</version> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>ImageTools-Hash</name> |
|||
<description>An image collection management utility</description> |
|||
<url>http://imagetools.sothr.com</url> |
|||
<organization> |
|||
<name>Sothr Software</name> |
|||
</organization> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>org.scala-lang</groupId> |
|||
<artifactId>scala-library</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.jsuereth</groupId> |
|||
<artifactId>scala-arm_${scala.binary.version}</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.slf4j</groupId> |
|||
<artifactId>slf4j-api</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.clapper</groupId> |
|||
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>net.coobird</groupId> |
|||
<artifactId>thumbnailator</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>commons-codec</groupId> |
|||
<artifactId>commons-codec</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>net.sourceforge.jtransforms</groupId> |
|||
<artifactId>jtransforms</artifactId> |
|||
</dependency> |
|||
<!-- TEST --> |
|||
<dependency> |
|||
<groupId>org.scalatest</groupId> |
|||
<artifactId>scalatest_${scala.binary.version}</artifactId> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<!-- enable surefire for java tests--> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-surefire-plugin</artifactId> |
|||
<version>2.7</version> |
|||
<configuration> |
|||
<skipTests>false</skipTests> |
|||
</configuration> |
|||
</plugin> |
|||
<!-- enable scalatest for scala tests--> |
|||
<plugin> |
|||
<groupId>org.scalatest</groupId> |
|||
<artifactId>scalatest-maven-plugin</artifactId> |
|||
<version>1.0-RC2</version> |
|||
<configuration> |
|||
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory> |
|||
<junitxml>.</junitxml> |
|||
<filereports>WDF TestSuite.txt</filereports> |
|||
<argLine>-Xmx128m</argLine> |
|||
</configuration> |
|||
<executions> |
|||
<execution> |
|||
<id>test</id> |
|||
<goals> |
|||
<goal>test</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
<!-- Packaging Configuration --> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-jar-plugin</artifactId> |
|||
<version>2.4</version> |
|||
<executions> |
|||
<execution> |
|||
<phase>package</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
<configuration> |
|||
<archive> |
|||
<manifest> |
|||
<addClasspath>true</addClasspath> |
|||
<classpathPrefix>lib/</classpathPrefix> |
|||
</manifest> |
|||
</archive> |
|||
<outputDirectory> |
|||
${project.build.directory}/release |
|||
</outputDirectory> |
|||
</configuration> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
|
|||
</project> |
After Width: 3648 | Height: 2736 | Size: 5.0 MiB |
After Width: 1824 | Height: 1368 | Size: 1.5 MiB |
After Width: 912 | Height: 684 | Size: 519 KiB |
@ -0,0 +1,186 @@ |
|||
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.dto.{HashSettingDTO, ImageHashDTO} |
|||
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 { |
|||
|
|||
def getImageHashes(ahashSettings: HashSettingDTO, |
|||
dhashSettings: HashSettingDTO, |
|||
phashSettings: HashSettingDTO, |
|||
imagePath: String): ImageHashDTO = { |
|||
getImageHashes(ahashSettings, dhashSettings, phashSettings, ImageIO.read(new File(imagePath)), imagePath) |
|||
} |
|||
|
|||
def getImageHashes(ahashSettings: HashSettingDTO, |
|||
dhashSettings: HashSettingDTO, |
|||
phashSettings: HashSettingDTO, |
|||
image: BufferedImage, |
|||
imagePath: String): ImageHashDTO = { |
|||
|
|||
debug(s"Creating hashes for $imagePath") |
|||
|
|||
var ahash: Long = 0L |
|||
var dhash: Long = 0L |
|||
var phash: Long = 0L |
|||
val sha1: String = getSHA1(imagePath) |
|||
|
|||
//Get Image Data |
|||
val grayImage = ImageUtil.convertToGray(image) |
|||
|
|||
if (ahashSettings.use) { |
|||
ahash = getAhash(ahashSettings, grayImage, alreadyGray = true) |
|||
} |
|||
if (dhashSettings.use) { |
|||
dhash = getDhash(dhashSettings, grayImage, alreadyGray = true) |
|||
} |
|||
if (phashSettings.use) { |
|||
phash = getPhash(phashSettings, grayImage, alreadyGray = true) |
|||
} |
|||
|
|||
val hashes = new ImageHashDTO(ahash, dhash, phash, sha1) |
|||
debug(s"Generated hashes: $hashes") |
|||
|
|||
hashes |
|||
} |
|||
|
|||
def getAhash(hashSettings: HashSettingDTO, image: BufferedImage, alreadyGray: Boolean = false): Long = { |
|||
//debug("Started generating an AHash") |
|||
var grayImage: BufferedImage = null |
|||
if (alreadyGray) { |
|||
grayImage = image |
|||
} else { |
|||
grayImage = ImageUtil.convertToGray(image) |
|||
} |
|||
val resizedImage = ImageUtil.resize(grayImage, hashSettings.precision, forced = true) |
|||
val imageData = ImageUtil.getImageData(resizedImage) |
|||
AHash.getHash(imageData) |
|||
} |
|||
|
|||
def getDhash(hashSettings: HashSettingDTO, image: BufferedImage, alreadyGray: Boolean = false): Long = { |
|||
//debug("Started generating an DHash") |
|||
var grayImage: BufferedImage = null |
|||
if (alreadyGray) { |
|||
grayImage = image |
|||
} else { |
|||
grayImage = ImageUtil.convertToGray(image) |
|||
} |
|||
val resizedImage = ImageUtil.resize(grayImage, hashSettings.precision, forced = true) |
|||
val imageData = ImageUtil.getImageData(resizedImage) |
|||
DHash.getHash(imageData) |
|||
} |
|||
|
|||
def getPhash(hashSettings: HashSettingDTO, image: BufferedImage, alreadyGray: Boolean = false): Long = { |
|||
//debug("Started generating an PHash") |
|||
var grayImage: BufferedImage = null |
|||
if (alreadyGray) { |
|||
grayImage = image |
|||
} else { |
|||
grayImage = ImageUtil.convertToGray(image) |
|||
} |
|||
val resizedImage = ImageUtil.resize(grayImage, hashSettings.precision, forced = true) |
|||
val imageData = ImageUtil.getImageData(resizedImage) |
|||
PHash.getHash(imageData) |
|||
} |
|||
|
|||
def getSHA1(filePath: String): String = { |
|||
managed(new FileInputStream(filePath)) acquireAndGet { |
|||
input => DigestUtils.sha1Hex(input) |
|||
} |
|||
} |
|||
|
|||
def getMD5(filePath: String): String = { |
|||
managed(new FileInputStream(filePath)) acquireAndGet { |
|||
input => DigestUtils.md5Hex(input) |
|||
} |
|||
} |
|||
|
|||
def areHashSimilar(hashSettings: HashSettingDTO, hash1: Long, hash2: Long): Boolean = { |
|||
val tolerence = hashSettings.tolerance |
|||
val distance = HammingUtil.getDistance(hash1, hash2) |
|||
if (distance <= tolerence) true else false |
|||
} |
|||
|
|||
def areImageHashesSimilar(ahashSettings: HashSettingDTO, |
|||
dhashSettings: HashSettingDTO, |
|||
phashSettings: HashSettingDTO, |
|||
imageHash1: ImageHashDTO, |
|||
imageHash2: ImageHashDTO): Boolean = { |
|||
val weightedHammingMean = getWeightedHashSimilarity(ahashSettings, dhashSettings, phashSettings, imageHash1, imageHash2) |
|||
val weightedToleranceMean = getWeightedHashTolerence(ahashSettings, dhashSettings, phashSettings) |
|||
if (weightedHammingMean <= weightedToleranceMean) true else false |
|||
} |
|||
|
|||
def getWeightedHashSimilarity(ahashSettings: HashSettingDTO, |
|||
dhashSettings: HashSettingDTO, |
|||
phashSettings: HashSettingDTO, |
|||
imageHash1: ImageHashDTO, |
|||
imageHash2: ImageHashDTO): Float = { |
|||
//calculate weighted values |
|||
var weightedHammingTotal: Float = 0 |
|||
var methodsTotal = 0 |
|||
|
|||
if (ahashSettings.use) { |
|||
val hamming = HammingUtil.getDistance(imageHash1.ahash, imageHash2.ahash) |
|||
weightedHammingTotal += hamming * ahashSettings.weight |
|||
trace(s"hash1: ${imageHash1.ahash} hash2: ${imageHash1.ahash} tolerance: ${ahashSettings.tolerance} hamming distance: $hamming weight: ${ahashSettings.weight}") |
|||
methodsTotal += 1 |
|||
} |
|||
if (dhashSettings.use) { |
|||
val hamming = HammingUtil.getDistance(imageHash1.dhash, imageHash2.dhash) |
|||
weightedHammingTotal += hamming * dhashSettings.weight |
|||
trace(s"hash1: ${imageHash1.dhash} hash2: ${imageHash1.dhash} tolerance: ${dhashSettings.tolerance} hamming distance: $hamming weight: ${dhashSettings.weight}") |
|||
methodsTotal += 1 |
|||
} |
|||
if (phashSettings.use) { |
|||
val hamming = HammingUtil.getDistance(imageHash1.phash, imageHash2.phash) |
|||
weightedHammingTotal += hamming * phashSettings.weight |
|||
trace(s"hash1: ${imageHash1.phash} hash2: ${imageHash1.phash} tolerance: ${phashSettings.tolerance} hamming distance: $hamming weight: ${phashSettings.weight}") |
|||
methodsTotal += 1 |
|||
} |
|||
val weightedHammingMean = weightedHammingTotal / methodsTotal |
|||
debug(s"Calculated Weighted HammingUtil Mean: $weightedHammingMean") |
|||
weightedHammingMean |
|||
} |
|||
|
|||
def getWeightedHashTolerence(ahashSettings: HashSettingDTO, |
|||
dhashSettings: HashSettingDTO, |
|||
phashSettings: HashSettingDTO): Float = { |
|||
//calculate weighted values |
|||
var weightedToleranceTotal: Float = 0 |
|||
var methodsTotal = 0 |
|||
|
|||
if (ahashSettings.use) { |
|||
weightedToleranceTotal += ahashSettings.tolerance * ahashSettings.weight |
|||
trace(s"Ahash Tolerance: ${ahashSettings.tolerance} Current Weighted Tolerance: $weightedToleranceTotal") |
|||
methodsTotal += 1 |
|||
} |
|||
if (dhashSettings.use) { |
|||
weightedToleranceTotal += dhashSettings.tolerance * dhashSettings.weight |
|||
trace(s"Dhash Tolerance: ${dhashSettings.tolerance} Current Weighted Tolerance: $weightedToleranceTotal") |
|||
methodsTotal += 1 |
|||
} |
|||
if (phashSettings.use) { |
|||
weightedToleranceTotal += phashSettings.tolerance * phashSettings.weight |
|||
trace(s"Phash Tolerance: ${phashSettings.tolerance} Current Weighted Tolerance: $weightedToleranceTotal") |
|||
methodsTotal += 1 |
|||
} |
|||
val weightedTolerance = weightedToleranceTotal / methodsTotal |
|||
debug(s"Calculated Weighted Tolerance: $weightedTolerance") |
|||
weightedTolerance |
|||
} |
|||
|
|||
} |
@ -0,0 +1,8 @@ |
|||
package com.sothr.imagetools.hash.dto |
|||
|
|||
class HashSettingDTO( |
|||
val use: Boolean, |
|||
val precision: Int, |
|||
val tolerance: Int, |
|||
val weight: Float) { |
|||
} |
@ -0,0 +1,46 @@ |
|||
package com.sothr.imagetools.hash.dto |
|||
|
|||
import grizzled.slf4j.Logging |
|||
|
|||
class ImageHashDTO(var ahash: Long, var dhash: Long, var phash: Long, var fileHash: String) extends Serializable with Logging { |
|||
|
|||
def getAhash: Long = ahash |
|||
|
|||
def setAhash(hash: Long) = { |
|||
ahash = hash |
|||
} |
|||
|
|||
def getDhash: Long = dhash |
|||
|
|||
def setDhash(hash: Long) = { |
|||
dhash = hash |
|||
} |
|||
|
|||
def getPhash: Long = phash |
|||
|
|||
def setPhash(hash: Long) = { |
|||
phash = hash |
|||
} |
|||
|
|||
def getFileHash: String = fileHash |
|||
|
|||
def setFileHash(hash: String) = { |
|||
fileHash = hash |
|||
} |
|||
|
|||
def cloneHashes: ImageHashDTO = { |
|||
new ImageHashDTO(ahash, dhash, phash, fileHash) |
|||
} |
|||
|
|||
override def hashCode(): Int = { |
|||
var result = 365 |
|||
result = 41 * result + (this.ahash ^ (this.ahash >>> 32)).toInt |
|||
result = 37 * result + (this.dhash ^ (this.dhash >>> 32)).toInt |
|||
result = 2 * result + (this.phash ^ (this.phash >>> 32)).toInt |
|||
result |
|||
} |
|||
|
|||
override def toString: String = { |
|||
s"fileHash: $fileHash ahash: $ahash dhash: $dhash phash: $phash" |
|||
} |
|||
} |
@ -1,4 +1,4 @@ |
|||
package com.sothr.imagetools.engine.hash |
|||
package com.sothr.imagetools.hash.`type` |
|||
|
|||
/** |
|||
* Interface for perceptual hashing |
@ -0,0 +1,18 @@ |
|||
package com.sothr.imagetools.hash.util |
|||
|
|||
object HammingUtil { |
|||
|
|||
/** |
|||
* Calculate the hamming distance between two longs |
|||
* |
|||
* @param hash1 The first hash to compare |
|||
* @param hash2 The second hash to compare |
|||
* @return |
|||
*/ |
|||
def getDistance(hash1: Long, hash2: Long): Int = { |
|||
//The XOR of hash1 and hash2 is converted to a binary string |
|||
//then the number of '1's is counted. This is the hamming distance |
|||
(hash1 ^ hash2).toBinaryString.count(_ == '1') |
|||
} |
|||
|
|||
} |
@ -0,0 +1,117 @@ |
|||
package com.sothr.imagetools.hash.util |
|||
|
|||
import java.awt.image.{BufferedImage, ColorConvertOp, DataBufferByte} |
|||
|
|||
import grizzled.slf4j.Logging |
|||
import net.coobird.thumbnailator.Thumbnails |
|||
|
|||
object ImageUtil extends Logging { |
|||
|
|||
/** |
|||
* Quickly convert an image to grayscale |
|||
* |
|||
* @param image image to convert to greyscale |
|||
* @return |
|||
*/ |
|||
def convertToGray(image: BufferedImage): BufferedImage = { |
|||
val grayImage = new BufferedImage(image.getWidth, image.getHeight, BufferedImage.TYPE_BYTE_GRAY) |
|||
|
|||
val op = new ColorConvertOp( |
|||
image.getColorModel.getColorSpace, |
|||
grayImage.getColorModel.getColorSpace, |
|||
null |
|||
) |
|||
|
|||
op.filter(image, grayImage) |
|||
} |
|||
|
|||
def resize(image: BufferedImage, size: Int, forced: Boolean = false): BufferedImage = { |
|||
//debug(s"Resizing an image to size: ${size}x${size} forced: $forced") |
|||
if (forced) { |
|||
Thumbnails.of(image).forceSize(size, size).asBufferedImage |
|||
} else { |
|||
Thumbnails.of(image).size(size, size).asBufferedImage |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get the raw data for an image |
|||
* Convert a buffered image into a 2d pixel data array |
|||
* |
|||
* @param image image to convert without using RGB |
|||
* @return |
|||
*/ |
|||
def getImageData(image: BufferedImage): Array[Array[Int]] = { |
|||
|
|||
val pixels = image.getRaster.getDataBuffer.asInstanceOf[DataBufferByte].getData |
|||
val numPixels = pixels.length |
|||
val width = image.getWidth |
|||
val height = image.getHeight |
|||
val isSingleChannel = if (numPixels == (width * height)) true else false |
|||
val hasAlphaChannel = image.getAlphaRaster != null |
|||
//debug(s"Converting image to 2d. width:$width height:$height") |
|||
|
|||
val result = Array.ofDim[Int](height, width) |
|||
if (isSingleChannel) { |
|||
//debug(s"Processing Single Channel Image") |
|||
val pixelLength = 1 |
|||
var row = 0 |
|||
var col = 0 |
|||
//debug(s"Processing pixels 0 until $numPixels by $pixelLength") |
|||
for (pixel <- 0 until numPixels by pixelLength) { |
|||
//debug(s"Processing pixel: $pixel/${numPixels - 1}") |
|||
val argb: Int = pixels(pixel).toInt //singleChannel |
|||
//debug(s"Pixel data: $argb") |
|||
result(row)(col) = argb |
|||
col += 1 |
|||
if (col == width) { |
|||
col = 0 |
|||
row += 1 |
|||
} |
|||
} |
|||
} |
|||
else if (hasAlphaChannel) { |
|||
//debug(s"Processing Four Channel Image") |
|||
val pixelLength = 4 |
|||
var row = 0 |
|||
var col = 0 |
|||
//debug(s"Processing pixels 0 until $numPixels by $pixelLength") |
|||
for (pixel <- 0 until numPixels by pixelLength) { |
|||
//debug(s"Processing pixel: $pixel/${numPixels - 1}") |
|||
var argb: Int = 0 |
|||
argb += pixels(pixel).toInt << 24 //alpha |
|||
argb += pixels(pixel + 1).toInt //blue |
|||
argb += pixels(pixel + 2).toInt << 8 //green |
|||
argb += pixels(pixel + 3).toInt << 16 //red |
|||
result(row)(col) = argb |
|||
col += 1 |
|||
if (col == width) { |
|||
col = 0 |
|||
row += 1 |
|||
} |
|||
} |
|||
} else { |
|||
//debug(s"Processing Three Channel Image") |
|||
val pixelLength = 3 |
|||
var row = 0 |
|||
var col = 0 |
|||
//debug(s"Processing pixels 0 until $numPixels by $pixelLength") |
|||
for (pixel <- 0 until numPixels by pixelLength) { |
|||
//debug(s"Processing pixel: $pixel/${numPixels - 1}") |
|||
var argb: Int = 0 |
|||
argb += -16777216; // 255 alpha |
|||
argb += pixels(pixel).toInt //blue |
|||
argb += pixels(pixel + 1).toInt << 8 //green |
|||
argb += pixels(pixel + 2).toInt << 16 //red |
|||
result(row)(col) = argb |
|||
col += 1 |
|||
if (col == width) { |
|||
col = 0 |
|||
row += 1 |
|||
} |
|||
} |
|||
} |
|||
result |
|||
} |
|||
|
|||
} |
@ -0,0 +1,35 @@ |
|||
package com.sothr.imagetools.hash.util |
|||
|
|||
import grizzled.slf4j.Logging |
|||
|
|||
trait TimingUtil extends Logging { |
|||
|
|||
def time[R](block: => R): R = { |
|||
val t0 = System.currentTimeMillis |
|||
val result = block // call-by-name |
|||
val t1 = System.currentTimeMillis |
|||
debug("Elapsed time: " + (t1 - t0) + "ms") |
|||
result |
|||
} |
|||
|
|||
def getTime[R](block: => R): Long = { |
|||
val t0 = System.currentTimeMillis |
|||
val result = block // call-by-name |
|||
val t1 = System.currentTimeMillis |
|||
debug("Elapsed time: " + (t1 - t0) + "ms") |
|||
t1 - t0 |
|||
} |
|||
|
|||
def getMean(times: Long*): Long = { |
|||
getMean(times.toArray[Long]) |
|||
} |
|||
|
|||
def getMean(times: Array[Long]): Long = { |
|||
var ag = 0L |
|||
for (i <- times.indices) { |
|||
ag += times(i) |
|||
} |
|||
ag / times.length |
|||
} |
|||
|
|||
} |
@ -0,0 +1,9 @@ |
|||
package com.sothr.imagetools.hash |
|||
|
|||
import com.sothr.imagetools.hash.util.TimingUtil |
|||
import grizzled.slf4j.Logging |
|||
import org.scalatest.{BeforeAndAfter, FunSuite, Inside, Inspectors, Matchers, OptionValues} |
|||
|
|||
abstract class BaseTest extends FunSuite with Matchers with OptionValues with Inside with Inspectors with BeforeAndAfter with Logging with TimingUtil { |
|||
|
|||
} |
@ -0,0 +1,7 @@ |
|||
package com.sothr.imagetools.hash |
|||
|
|||
object TestParams { |
|||
val LargeSampleImage1 = "sample/sample_01_large.jpg" |
|||
val MediumSampleImage1 = "sample/sample_01_medium.jpg" |
|||
val SmallSampleImage1 = "sample/sample_01_small.jpg" |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue