Skip to content

Commit

Permalink
Add ability to define custom pixel difference calculators
Browse files Browse the repository at this point in the history
Add default calculation method and scaled alpha calculation method.
  • Loading branch information
bennothommo committed Feb 21, 2024
1 parent 3322a78 commit b71c795
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 31 deletions.
24 changes: 24 additions & 0 deletions src/DiffCalculator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace AssertGD;

use AssertGD\GDImage;

/**
* Difference calculator.
*
* Determines the difference between two given images.
*/
interface DiffCalculator
{
/**
* Calculates the difference between two pixels at the given coordinates.
*
* This method will be provided with two `GDImage` objects representing the images being compared, and co-ordinates
* of the pixel being compared.
*
* The method should return a float value between 0 and 1 inclusive, with 0 meaning that the pixels of both images
* at the given co-ordinates are an exact match, and 1 meaning that the pixels are the complete opposite.
*/
public function calculate(GDImage $imageA, GDImage $imageB, int $pixelX, int $pixelY): float;
}
31 changes: 31 additions & 0 deletions src/DiffCalculator/RgbaChannels.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace AssertGD\DiffCalculator;

use AssertGD\DiffCalculator;
use AssertGD\GDImage;

/**
* Calculate the difference between two pixels using the RGBA channels.
*
* This is the default calculation method used by the `AssertGD` package. It simply takes each individual channel and
* compares the delta between the channel values of the two images.
*
* This works well for most images, but may not work for images with transparent pixels if the transparent pixels have
* different RGB values.
*/
class RgbaChannels implements DiffCalculator
{
public function calculate(GDImage $imageA, GDImage $imageB, int $pixelX, int $pixelY): float
{
$pixelA = $imageA->getPixel($pixelX, $pixelY);
$pixelB = $imageB->getPixel($pixelX, $pixelY);

$diffR = abs($pixelA['red'] - $pixelB['red']) / 255;
$diffG = abs($pixelA['green'] - $pixelB['green']) / 255;
$diffB = abs($pixelA['blue'] - $pixelB['blue']) / 255;
$diffA = abs($pixelA['alpha'] - $pixelB['alpha']) / 127;

return ($diffR + $diffG + $diffB + $diffA) / 4;
}
}
45 changes: 45 additions & 0 deletions src/DiffCalculator/ScaledRgbChannels.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace AssertGD\DiffCalculator;

use AssertGD\DiffCalculator;
use AssertGD\GDImage;

/**
* Calculate the difference between two pixels using the RGB channels, and scales down the RGB difference by the alpha
* channel.
*
* This calculation will pre-multiply the RGB channels by the opacity percentage (alpha) of the pixel, meaning that a
* translucent pixel will have less of an impact on the overall difference than an opaque pixel. For transparent pixels,
* this will mean that the RGB difference will be scaled down to zero, effectively meaning that transparent pixels will
* match regardless of their RGB values.
*
* This calculation method is useful for images with transparent pixels or images that have been anti-aliased or
* blurred over a transparent background, effectively making translucent pixels less likely to cause a false positive as
* being different.
*/
class ScaledRgbChannels implements DiffCalculator
{
public function calculate(GDImage $imageA, GDImage $imageB, int $pixelX, int $pixelY): float
{
$pixelA = $this->premultiply($imageA->getPixel($pixelX, $pixelY));
$pixelB = $this->premultiply($imageB->getPixel($pixelX, $pixelY));

$diffR = abs($pixelA['red'] - $pixelB['red']) / 255;
$diffG = abs($pixelA['green'] - $pixelB['green']) / 255;
$diffB = abs($pixelA['blue'] - $pixelB['blue']) / 255;

return ($diffR + $diffG + $diffB) / 4;
}

protected function premultiply(array $pixel)
{
$alpha = 1 - ($pixel['alpha'] / 127);

return [
'red' => $pixel['red'] * $alpha,
'green' => $pixel['green'] * $alpha,
'blue' => $pixel['blue'] * $alpha,
];
}
}
38 changes: 32 additions & 6 deletions src/GDAssertTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@

namespace AssertGD;

use AssertGD\DiffCalculator\RgbaChannels;

/**
* Use this trait in a test case class to gain access to the image similarity
* assertions.
*/
trait GDAssertTrait
{
/**
* @var DiffCalculator The difference calculator to compare the images with.
*/
protected $diffCalculator;

/**
* Asserts that the difference between $expected and $actual is AT MOST
* $threshold. $expected and $actual can be GD image resources or paths to
Expand All @@ -22,14 +29,15 @@ trait GDAssertTrait
* @param string|resource $actual The actual image.
* @param string $message The failure message.
* @param float $threshold Error threshold between 0 and 1.
* @param DiffCalculator|null $diffCalculator The difference calculator to use.
*
* @return void
*
* @throws PHPUnit\Framework\AssertionFailedError
*/
public function assertSimilarGD($expected, $actual, $message = '', $threshold = 0)
public function assertSimilarGD($expected, $actual, $message = '', $threshold = 0, $diffCalculator = null)
{
$constraint = $this->isSimilarGD($expected, $threshold);
$constraint = $this->isSimilarGD($expected, $threshold, $diffCalculator);
$this->assertThat($actual, $constraint, $message);
}

Expand All @@ -44,15 +52,16 @@ public function assertSimilarGD($expected, $actual, $message = '', $threshold =
* @param string|resource $actual The actual image.
* @param string $message The failure message.
* @param float $threshold Error threshold between 0 and 1.
* @param DiffCalculator|null $diffCalculator The difference calculator to use.
*
* @return void
*
* @throws PHPUnit\Framework\AssertionFailedError
*/
public function assertNotSimilarGD($expected, $actual, $message = '', $threshold = 0)
public function assertNotSimilarGD($expected, $actual, $message = '', $threshold = 0, $diffCalculator = null)
{
$constraint = $this->logicalNot(
$this->isSimilarGD($expected, $threshold)
$this->isSimilarGD($expected, $threshold, $diffCalculator)
);
$this->assertThat($actual, $constraint, $message);
}
Expand All @@ -66,11 +75,28 @@ public function assertNotSimilarGD($expected, $actual, $message = '', $threshold
*
* @param string|resource $expected The expected image.
* @param float $threshold Error threshold between 0 and 1.
* @param DiffCalculator|null $diffCalculator The difference calculator to use.
*
* @return GDSimilarityConstraint The constraint.
*/
public function isSimilarGD($expected, $threshold = 0)
public function isSimilarGD($expected, $threshold = 0, $diffCalculator = null)
{
return new GDSimilarityConstraint($expected, $threshold);
return new GDSimilarityConstraint($expected, $threshold, $diffCalculator ?? new RgbaChannels());
}

/**
* Sets the difference calculator to use for image comparisons in this test case.
*
* @var DiffCalculator $diffCalculator
*/
public function setDiffCalculator($diffCalculator): void
{
if (!($diffCalculator instanceof DiffCalculator)) {
throw new \InvalidArgumentException(
'The difference calculator must implement the `AssertGD\DiffCalculator` interface'
);
}

$this->diffCalculator = $diffCalculator;
}
}
34 changes: 9 additions & 25 deletions src/GDSimilarityConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace AssertGD;

use AssertGD\DiffCalculator\RgbaChannels;
use PHPUnit\Framework\Constraint\Constraint;

/**
Expand All @@ -16,18 +17,24 @@ class GDSimilarityConstraint extends Constraint
{
private $expected;
private $threshold;
/**
* @var DiffCalculator The difference calculator to compare the images with.
*/
private $diffCalculator;

/**
* Constructs a new constraint. A threshold of 0 means only exactly equal
* images are allowed, while a threshold of 1 matches every image.
*
* @param string|resource $expected File name or resource to match against.
* @param float $threshold Error threshold between 0 and 1.
* @param DiffCalculator|null $diffCalculator The difference calculator to use.
*/
public function __construct($expected, $threshold = 0)
public function __construct($expected, $threshold = 0, $diffCalculator = null)
{
$this->expected = $expected;
$this->threshold = $threshold;
$this->diffCalculator = $diffCalculator ?? new RgbaChannels();
}

/**
Expand Down Expand Up @@ -64,7 +71,7 @@ public function matches($other): bool
$delta = 0;
for ($x = 0; $x < $w; ++$x) {
for ($y = 0; $y < $h; ++$y) {
$delta += $this->getPixelError($imgExpec, $imgOther, $x, $y);
$delta += $this->diffCalculator->calculate($imgExpec, $imgOther, $x, $y);
}
}

Expand All @@ -76,27 +83,4 @@ public function matches($other): bool

return $error <= $this->threshold;
}

/**
* Calculates the error between 0 and 1 (inclusive) of a specific pixel.
*
* @param GDImage $imgA The first image.
* @param GDImage $imgB The second image.
* @param int $x The pixel's x coordinate.
* @param int $y The pixel's y coordinate.
*
* @return float The pixel error.
*/
private function getPixelError(GDImage $imgA, GDImage $imgB, $x, $y)
{
$pixelA = $imgA->getPixel($x, $y);
$pixelB = $imgB->getPixel($x, $y);

$diffR = abs($pixelA['red'] - $pixelB['red']) / 255;
$diffG = abs($pixelA['green'] - $pixelB['green']) / 255;
$diffB = abs($pixelA['blue'] - $pixelB['blue']) / 255;
$diffA = abs($pixelA['alpha'] - $pixelB['alpha']) / 127;

return ($diffR + $diffG + $diffB + $diffA) / 4;
}
}
12 changes: 12 additions & 0 deletions tests/GDAssertTraitTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use AssertGD\DiffCalculator\ScaledRgbChannels;
use PHPUnit\Framework\TestCase;

use AssertGD\GDAssertTrait;
Expand Down Expand Up @@ -49,4 +50,15 @@ public function testJpeg()
$this->assertSimilarGD('./tests/images/jpeg.jpg',
'./tests/images/jpeg-alt.jpg', '', 0.1);
}

public function testAlternativeDiffCalculator()
{
// the default method of calculating images will not consider these images exact due to the transparent pixels
// having different RGB values
$this->assertNotSimilarGD('./tests/images/transparent-black.gif', './tests/images/transparent-white.gif');

// using the ScaledRgbChannels diff calculator, the images will be considered exact
$this->assertSimilarGD('./tests/images/transparent-black.gif', './tests/images/transparent-white.gif',
'', 0, new ScaledRgbChannels());
}
}
Binary file added tests/images/transparent-black.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/transparent-white.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b71c795

Please sign in to comment.