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