Skip to content

Instantly share code, notes, and snippets.

@VijitCoder
Forked from akosnikhazy/compare-images-class
Last active August 1, 2023 09:19
Show Gist options
  • Save VijitCoder/55ed9c336ba4db8d91f18e5437ce6096 to your computer and use it in GitHub Desktop.
Save VijitCoder/55ed9c336ba4db8d91f18e5437ce6096 to your computer and use it in GitHub Desktop.
PHP class for comparing two images and determine how much they similar, in percentages.
<?php
/**
* Compare two images and determine how much they similar, in percentages.
*/
class ImagesComparator
{
private const ACCEPTED_IMAGE_TYPES = [IMAGETYPE_JPEG, IMAGETYPE_PNG];
/**
* Calculate the images similarity in percentages.
*
* Based on calculation of the hammering distance between color bit values of two images.
*
* What is about the snapshot size? Images comparison makes a lot of calculations based on pixels color.
* These calculations will give roughly the same result for the image in a different sizes, but the bigger image
* required more time for calculation. So it is make sense to use a resized images instead originals, which is
* called here the snapshots. Recommended value in the range of [8, 100] px.
*
* The smaller snapshots size, the rough the comparison you"ll get (in theory).
*
* @param string $file1 absolute path + file name to the first image
* @param string $file2 absolute path + file name to the second image
* @param int $snapshotSize snapshot with (or height) in pixels. The snapshot is a square image, always.
* @return int|null percentages of similarity
*/
public function compare(string $file1, string $file2, int $snapshotSize): ?int
{
$image1 = $this->prepareSnapshot($file1, $snapshotSize);
$image2 = $this->prepareSnapshot($file2, $snapshotSize);
if (!($image1 && $image2)) {
return null;
}
$colorsMap1 = $this->buildColorsMap($image1);
$colorsMap2 = $this->buildColorsMap($image2);
$deviationMap1 = $this->determineDeviationFromMeanValue($colorsMap1);
$deviationMap2 = $this->determineDeviationFromMeanValue($colorsMap2);
$similarityPercentage = $this->calcImagesSimilarity($deviationMap1, $deviationMap2);
imagedestroy($image1);
imagedestroy($image2);
return $similarityPercentage;
}
/**
* Create resized image resource from the source file. Apply grayscale filter.
*
* Accept jpeg or png source only.
*
* @param string $source absolute path + file name to the image
* @param int $snapshotSize
* @return resource|null - image resource identifier or NULL if failed
*/
private function prepareSnapshot(string $source, int $snapshotSize)
{
if (!file_exists($source)) {
return $this->fail("File not found: " . $source);
}
$heap = getimagesize($source);
$imageType = $heap[2];
$mimeType = $heap['mime'];
if (!in_array($imageType, static::ACCEPTED_IMAGE_TYPES)) {
return $this->fail("Unsupported image type. Its MIME type is " . $mimeType);
}
$sourceImage = $this->createImage($source, $imageType);
if (!$source) {
return $this->fail("Failed to create image resource");
}
$snapshot = $this->resizeImage($sourceImage, $snapshotSize);
if (!$snapshot) {
return $this->fail("Failed to resize the image");
}
$isFiltered = imagefilter($snapshot, IMG_FILTER_GRAYSCALE);
if (!$isFiltered) {
return $this->fail("Failed to apply grayscale filter");
}
return $snapshot;
}
/**
* Create image resource. Supports jpg and png only.
*
* @param string $source absolute path + file name to the image
* @param string $imageType image type, see php::IMAGETYPE_* constants
* @return resource|null - image resource identifier or NULL if failed
*/
private function createImage(string $source, string $imageType)
{
switch ($imageType) {
case IMAGETYPE_JPEG:
return imagecreatefromjpeg($source) ?: null;
case IMAGETYPE_PNG:
return imagecreatefrompng($source) ?: null;
default:
return null;
}
}
/**
* Resize the source image to specified size. The result will be a square image despite of original aspect ratio.
*
* @param $sourceImage
* @param int $resizeTo
* @return resource|null
*/
private function resizeImage($sourceImage, int $resizeTo)
{
$resizedImage = imagescale($sourceImage, $resizeTo, $resizeTo);
return $resizedImage ?: null;
}
/**
* Build the map of all pixels color in the image
*
* @param resource image resource identifier
* @return array
*/
private function buildColorsMap($image): array
{
$width = imagesx($image);
$height = imagesy($image);
$colorsMap = [];
for ($w = 0; $w < $width; $w++) {
for ($h = 0; $h < $height; $h++) {
$rgbIndex = imagecolorat($image, $w, $h);
$colorsMap[] = $rgbIndex & 0xFF;
}
}
return $colorsMap;
}
/**
* Determine a deviation of each pixel from the colors mean value.
*
* If a color is bigger than the mean value of colors - it is 1, other vise it"s 0.
*
* @param array $colorsMap
* @return array
*/
private function determineDeviationFromMeanValue(array $colorsMap): array
{
$colorMeanValue = array_sum($colorsMap) / count($colorsMap);
$deviations = [];
foreach ($colorsMap as $color) {
$deviations[] = (int)($color >= $colorMeanValue);
}
return $deviations;
}
/**
* Calculate the images similarity
*
* Calculate the Hamming distance between two maps of color deviations. The relation of that distance to the map
* count can be interpreted as the images difference in percentages.
*
* @param array $deviations1
* @param array $deviations2
* @return int percentages of similarity
*/
private function calcImagesSimilarity(array $deviations1, array $deviations2): int
{
$distance = 0;
// Note: it doesn"t matter which exactly map to take, they all have the same size.
$mapSize = count($deviations1);
// Don't use this. It gives a wrong result for 256 elements per array.
// $distance = count(array_diff($deviations1, $deviations2));
for ($i = 0; $i < $mapSize; $i++) {
if ($deviations1[$i] !== $deviations2[$i]) {
$distance++;
}
}
return 100 - $distance * 100 / $mapSize;
}
/**
* Report about fail and return nothing.
*
* In the perspective, you can replace `php::echo()` here with logging or throwing exception.
*
* @param string $message
* @return null
*/
private function fail(string $message)
{
echo $message;
return null;
}
}
@VijitCoder
Copy link
Author

I found a theoretical basis for this solution.

Now I believe my resizing improvements were redundant. It's not bad, but useless. I have no time to refactoring this class one more time. And I don't want to do that, because here is a vendor library that provides the same logic using Jenssegers\ImageHash\Implementation\AverageHash and three more solutions. I suggest to use this library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment