Skip to content

Instantly share code, notes, and snippets.

@vinaysshenoy
Created October 2, 2019 13:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vinaysshenoy/2d9968ee7e92978efa435e56b61ced8e to your computer and use it in GitHub Desktop.
Save vinaysshenoy/2d9968ee7e92978efa435e56b61ced8e to your computer and use it in GitHub Desktop.
Image similarity approvals for Java Approvals (https://github.com/approvals/ApprovalTests.Java)
package com.vinaysshenoy
import org.approvaltests.Approvals
import org.approvaltests.approvers.ApprovalApprover
import org.approvaltests.core.ApprovalFailureReporter
import org.approvaltests.core.ApprovalReporterWithCleanUp
import org.approvaltests.core.ApprovalWriter
import org.approvaltests.namer.ApprovalNamer
import org.approvaltests.writers.ImageApprovalWriter
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
class ImageApprover(
namer: ApprovalNamer,
private val writer: ApprovalWriter,
private val minimumSimilarity: Double
) : ApprovalApprover {
private val baseFilePath = "${namer.sourceFilePath}${namer.approvalName}"
private val received: File = File(writer.getReceivedFilename(baseFilePath))
private val approved: File = File(writer.getApprovalFilename(baseFilePath))
companion object {
fun create(image: BufferedImage, minimumSimilarity: Double): ImageApprover {
return ImageApprover(
namer = Approvals.createApprovalNamer(),
writer = ImageApprovalWriter(image),
minimumSimilarity = minimumSimilarity
)
}
}
override fun cleanUpAfterSuccess(reporter: ApprovalFailureReporter) {
received.delete()
(reporter as? ApprovalReporterWithCleanUp)?.cleanUp(received.absolutePath, approved.absolutePath)
}
override fun approve(): Boolean {
writer.writeReceivedFile(received.absolutePath)
if (!approved.exists() || !received.exists()) {
return false
}
val receivedImage = ImageIO.read(received)
val approvedImage = ImageIO.read(approved)
val similarity = ImageSimilarity.similarity(receivedImage, approvedImage)
return similarity >= minimumSimilarity
}
override fun reportFailure(reporter: ApprovalFailureReporter) {
reporter.report(received.absolutePath, approved.absolutePath)
}
override fun fail() {
throw Error("Failed Approval\n Approved:${approved.absolutePath}\n Received:${received.absolutePath}")
}
}
package com.vinaysshenoy
import java.awt.image.BufferedImage
import kotlin.math.abs
/**
* Method was copied from [RosettaCode](https://rosettacode.org/wiki/Percentage_difference_between_images#Kotlin)
**/
object ImageSimilarity {
fun differenceBetween(img1: BufferedImage, img2: BufferedImage): Double {
val width = img1.width
val height = img1.height
val width2 = img2.width
val height2 = img2.height
if (width != width2 || height != height2) {
val f = "(%d,%d) vs. (%d,%d)".format(width, height, width2, height2)
throw IllegalArgumentException("Images must have the same dimensions: $f")
}
var diff = 0L
for (y in 0 until height) {
for (x in 0 until width) {
diff += pixelDiff(img1.getRGB(x, y), img2.getRGB(x, y))
}
}
val maxDiff = 3L * 255 * width * height
return diff / maxDiff.toDouble()
}
fun similarity(img1: BufferedImage, img2: BufferedImage): Double {
return 1.0 - differenceBetween(img1, img2)
}
private fun pixelDiff(rgb1: Int, rgb2: Int): Int {
val r1 = (rgb1 shr 16) and 0xff
val g1 = (rgb1 shr 8) and 0xff
val b1 = rgb1 and 0xff
val r2 = (rgb2 shr 16) and 0xff
val g2 = (rgb2 shr 8) and 0xff
val b2 = rgb2 and 0xff
return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment