Initial commit
This commit is contained in:
commit
ba43d87019
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
composer.lock
|
||||||
|
vendor
|
17
composer.json
Normal file
17
composer.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "travail/image-perceptualhash",
|
||||||
|
"description": "travail/image-perceptualhash",
|
||||||
|
"license": "PHP-3.0",
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Image\\": "lib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-gd": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "4.*"
|
||||||
|
}
|
||||||
|
}
|
69
lib/PerceptualHash.php
Normal file
69
lib/PerceptualHash.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Image;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Image\PerceptualHash\Algorithm;
|
||||||
|
use Image\PerceptualHash\Algorithm\AverageHash;
|
||||||
|
|
||||||
|
class PerceptualHash {
|
||||||
|
/**
|
||||||
|
@var Algorithm The hasing algorithm
|
||||||
|
*/
|
||||||
|
protected $algorithm;
|
||||||
|
protected $data;
|
||||||
|
|
||||||
|
public function __construct($file, Algorithm $algorithm = null)
|
||||||
|
{
|
||||||
|
$this->algorithm = $algorithm ?: new AverageHash;
|
||||||
|
$this->data = is_resource($file) ? $file : $this->load($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hash()
|
||||||
|
{
|
||||||
|
return $this->algorithm->calculate($this->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compare($file)
|
||||||
|
{
|
||||||
|
$algorithm_class = get_class($this->algorithm);
|
||||||
|
$objective = new self($file, new $algorithm_class);
|
||||||
|
|
||||||
|
return self::hammingDistance($this->hash(), $objective->hash());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function hammingDistance($hash1, $hash2)
|
||||||
|
{
|
||||||
|
if (!is_string($hash1) || !is_string($hash2)) {
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($hash1) !== strlen($hash2)) {
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = 0;
|
||||||
|
$split_hash1 = str_split($hash1);
|
||||||
|
$split_hash2 = str_split($hash2);
|
||||||
|
for ($i = 0; $i < count($split_hash1); $i++) {
|
||||||
|
if ($split_hash1[$i] !== $split_hash2[$i]) {
|
||||||
|
$diff++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function load($file)
|
||||||
|
{
|
||||||
|
if (!file_exists($file)) {
|
||||||
|
throw new Exception("No such a file $file");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return imagecreatefromstring(file_get_contents($file));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
lib/PerceptualHash/Algorithm.php
Normal file
14
lib/PerceptualHash/Algorithm.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Image\PerceptualHash;
|
||||||
|
|
||||||
|
interface Algorithm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Calculate the hash
|
||||||
|
*
|
||||||
|
* @param resource $resource
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function calculate($resource);
|
||||||
|
}
|
50
lib/PerceptualHash/Algorithm/AverageHash.php
Normal file
50
lib/PerceptualHash/Algorithm/AverageHash.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Image\PerceptualHash\Algorithm;
|
||||||
|
|
||||||
|
use Image\PerceptualHash\Algorithm;
|
||||||
|
|
||||||
|
class AverageHash implements Algorithm
|
||||||
|
{
|
||||||
|
const SIZE = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function calculate($resource)
|
||||||
|
{
|
||||||
|
// Resize
|
||||||
|
$resized = imagecreatetruecolor(static::SIZE, static::SIZE);
|
||||||
|
imagecopyresampled(
|
||||||
|
$resized, $resource, 0, 0, 0, 0,
|
||||||
|
static::SIZE, static::SIZE, imagesx($resource), imagesy($resource));
|
||||||
|
|
||||||
|
// Create an array of gray-scale pixel value
|
||||||
|
$pixels = array();
|
||||||
|
for ($y = 0; $y < static::SIZE; $y++) {
|
||||||
|
for ($x = 0; $x < static::SIZE; $x++) {
|
||||||
|
$rgb = imagecolorsforindex($resized, imagecolorat($resized, $x, $y));
|
||||||
|
$pixels[] = floor(($rgb['red'] + $rgb['green'] + $rgb['blue']) / 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imagedestroy($resized);
|
||||||
|
|
||||||
|
// Calculate the average pixel value
|
||||||
|
$average = floor(array_sum($pixels) / count($pixels));
|
||||||
|
|
||||||
|
$binary = '';
|
||||||
|
$one = 1;
|
||||||
|
foreach ($pixels as $pixel) {
|
||||||
|
$binary .= $pixel > $average ? 1 : 0;
|
||||||
|
$one = $one << 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hex = '';
|
||||||
|
foreach (str_split($binary, 4) as $binary) {
|
||||||
|
$hex .= dechex(bindec($binary));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hex;
|
||||||
|
}
|
||||||
|
}
|
21
phpunit.xml.dist
Normal file
21
phpunit.xml.dist
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit backupGlobals="false"
|
||||||
|
backupStaticAttributes="false"
|
||||||
|
colors="true"
|
||||||
|
convertErrorsToExceptions="true"
|
||||||
|
convertNoticesToExceptions="true"
|
||||||
|
convertWarningsToExceptions="true"
|
||||||
|
processIsolation="false"
|
||||||
|
stopOnFailure="false"
|
||||||
|
syntaxCheck="false"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Test! Test! Test!">
|
||||||
|
<directory suffix="Test.php">tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
</phpunit>
|
40
tests/AverageHashTest.php
Normal file
40
tests/AverageHashTest.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Image\PerceptualHash;
|
||||||
|
|
||||||
|
class AverageHashTest extends PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->path_inuo1 = __DIR__ . '/images/inuo1.jpg';
|
||||||
|
$this->path_inuo2 = __DIR__ . '/images/inuo2.jpg';
|
||||||
|
$this->hash_inuo1 = 'fdd7a9d1c383c1ff';
|
||||||
|
$this->hash_inuo2 = 'f7f30200c3c3c3ff';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHash()
|
||||||
|
{
|
||||||
|
$ph = new PerceptualHash($this->path_inuo1);
|
||||||
|
$hash = $ph->hash();
|
||||||
|
|
||||||
|
$this->assertEquals(16, strlen($hash));
|
||||||
|
$this->assertEquals($this->hash_inuo1, $hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCompareDifferentImage()
|
||||||
|
{
|
||||||
|
$ph = new PerceptualHash($this->path_inuo1);
|
||||||
|
$diff = $ph->compare($this->path_inuo2);
|
||||||
|
|
||||||
|
$this->assertEquals(9, $diff);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCompareSameImage()
|
||||||
|
{
|
||||||
|
$ph = new PerceptualHash($this->path_inuo1);
|
||||||
|
$diff = $ph->compare($this->path_inuo1);
|
||||||
|
|
||||||
|
$this->assertEquals(0, $diff);
|
||||||
|
}
|
||||||
|
}
|
3
tests/bootstrap.php
Normal file
3
tests/bootstrap.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
BIN
tests/images/inuo1.jpg
Normal file
BIN
tests/images/inuo1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
BIN
tests/images/inuo2.jpg
Normal file
BIN
tests/images/inuo2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
Loading…
x
Reference in New Issue
Block a user