diff --git a/src/Implementations/PerceptualHash2.php b/src/Implementations/PerceptualHash2.php new file mode 100644 index 0000000..68d813d --- /dev/null +++ b/src/Implementations/PerceptualHash2.php @@ -0,0 +1,207 @@ +dct11 = self::DCT_11_16; + break; + case 32: + $this->dct11 = self::DCT_11_32; + break; + case 64: + $this->dct11 = self::DCT_11_64; + break; + default: + throw new LogicException('$size must be 16, 32 or 64'); + } + + $this->size = $size; + $this->sizeSqrt = sqrt(2 / $size); + } + + public function hash(Image $image): Hash + { + // Resize the image. + $resized = $image->resize($this->size, $this->size); + + $matrix = []; + $row = []; + $rows = []; + $col = []; + + $matrixSize = 11; + + for ($y = 0; $y < $this->size; $y++) { + for ($x = 0; $x < $this->size; $x++) { + $rgb = $resized->pickColor($x, $y); + $row[$x] = (int) floor(($rgb[0] * 0.299) + ($rgb[1] * 0.587) + ($rgb[2] * 0.114)); + } + $rows[$y] = $this->calculateDCT($row, $matrixSize); + } + + $rowMatrixSize = $matrixSize; + + for ($x = 0; $x < $matrixSize; $x++) { + for ($y = 0; $y < $this->size; $y++) { + $col[$y] = $rows[$y][$x]; + } + $matrix[$x] = $this->calculateDCT($col, $rowMatrixSize); + $rowMatrixSize--; + } + + $pixels = $this->diagonalMatrix($matrix, $matrixSize); + + $pixels = array_slice($pixels, 1, 64); // discard first and cut to size + + $compare = $this->average($pixels); + + // Calculate hash. + $bits = []; + foreach ($pixels as $pixel) { + $bits[] = (int) ($pixel > $compare); + } + + return Hash::fromBits($bits); + } + + /** + * Perform a 1 dimension Discrete Cosine Transformation. + * + * @param int[]|float[] $matrix + * + * @return float[] + */ + private function calculateDCT(array $matrix, int $partialSize): array + { + $dctCos = $this->dct11; + $transformed = []; + + for ($i = 0; $i < $partialSize; $i++) { + $sum = 0; + for ($j = 0; $j < $this->size; $j++) { + $sum += $matrix[$j] * $dctCos[$i][$j]; + } + $sum *= $this->sizeSqrt; + if ($i === 0) { + $sum *= 0.70710678118655; + } + $transformed[$i] = $sum; + } + + return $transformed; + } + + private function diagonalMatrix(array $mat, int $size = 11, bool $half = true) + { + $mode = 0; + $it = 0; + $lower = 0; + $result = []; + $max = ($half ? ceil((($size * $size) / 2) + ($size * 0.5)) : 0); + for ($t = 0; $t < (2 * $size - 1); $t++) { + $t1 = $t; + if ($t1 >= $size) { + $mode++; + $t1 = $size - 1; + $it--; + $lower++; + } else { + $lower = 0; + $it++; + } + for ($i = $t1; $i >= $lower; $i--) { + if ($half && count($result) >= $max) { + return $result; + } + if (($t1 + $mode) % 2 == 0) { + $result[] = $mat[$i][$t1 + $lower - $i]; + } else { + $result[] = $mat[$t1 + $lower - $i][$i]; + } + } + } + + return $result; + } + + /** + * Get the average of the pixel values. + */ + private function average(array $pixels): float + { + // Calculate the average value from top 8x8 pixels, except for the first one. + $n = count($pixels) - 1; + + return array_sum(array_slice($pixels, 1, $n)) / $n; + } +} diff --git a/tests/ImplementationTest.php b/tests/ImplementationTest.php index e0c6a24..e2c4897 100644 --- a/tests/ImplementationTest.php +++ b/tests/ImplementationTest.php @@ -6,6 +6,7 @@ use Jenssegers\ImageHash\Implementations\BlockHash; use Jenssegers\ImageHash\Implementations\DifferenceHash; use Jenssegers\ImageHash\Implementations\PerceptualHash; +use Jenssegers\ImageHash\Implementations\PerceptualHash2; use PHPUnit\Framework\TestCase; class ImplementationTest extends TestCase @@ -27,6 +28,9 @@ public function provideImplementations() [new DifferenceHash()], [new PerceptualHash(32, PerceptualHash::AVERAGE)], [new PerceptualHash(32, PerceptualHash::MEDIAN)], + [new PerceptualHash2(16)], + [new PerceptualHash2(32)], + [new PerceptualHash2(64)], [new BlockHash(8, BlockHash::QUICK)], [new BlockHash(8, BlockHash::PRECISE)], ];