Skip to content

Commit

Permalink
✨ Big rework including working BlockHash, better hash generation
Browse files Browse the repository at this point in the history
  • Loading branch information
jenssegers committed May 10, 2018
1 parent c499721 commit 006a1bc
Show file tree
Hide file tree
Showing 13 changed files with 411 additions and 391 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
],
"require": {
"php": "^5.6|^7.0",
"ext-gd": "*",
"intervention/image": "^2.4"
"intervention/image": "^2.4",
"phpseclib/phpseclib": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^5.0|^6.0|^7.0",
Expand Down
122 changes: 122 additions & 0 deletions src/Hash.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

namespace Jenssegers\ImageHash;

use JsonSerializable;
use phpseclib\Math\BigInteger;

class Hash implements JsonSerializable
{
/**
* @var BigInteger
*/
protected $value;

/**
* @param BigInteger $value
*/
private function __construct(BigInteger $value)
{
$this->value = $value;
}

/**
* @param string $hex
* @return self
*/
public static function fromHex($hex)
{
return new self(new BigInteger($hex, 16));
}

/**
* @param string|array $bits
* @return self
*/
public static function fromBits($bits)
{
if (is_array($bits)) {
$bits = implode('', $bits);
}

return new self(new BigInteger($bits, 2));
}

/**
* @return string
*/
public function toHex()
{
return $this->value->toHex();
}

/**
* @return string
*/
public function toBytes()
{
return $this->value->toBytes();
}

/**
* @return string
*/
public function toBits()
{
return $this->value->toBits();
}

/**
* @return int
*/
public function toInt()
{
return hexdec($this->toHex());
}

/**
* @param Hash $hash
* @return int
*/
public function distance(Hash $hash)
{
if (extension_loaded('gmp')) {
return gmp_hamdist('0x' . $this->toHex(), '0x' . $hash->toHex());
}

$bits1 = $this->toBits();
$bits2 = $hash->toBits();
$length = max(strlen($bits1), strlen($bits2));

// Add leading zeros so the bit strings are the same length.
$bits1 = str_pad($bits1, $length, '0', STR_PAD_LEFT);
$bits2 = str_pad($bits2, $length, '0', STR_PAD_LEFT);

return count(array_diff_assoc(str_split($bits1), str_split($bits2)));
}

/**
* @param Hash $hash
* @return bool
*/
public function equals(Hash $hash)
{
return $this->toHex() === $hash->toHex();
}

/**
* @return string
*/
public function __toString()
{
return $this->toHex();
}

/**
* @inheritdoc
*/
public function jsonSerialize()
{
return (string) $this;
}
}
102 changes: 13 additions & 89 deletions src/ImageHash.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,81 +8,45 @@
class ImageHash
{
/**
* Return hashes as hexadecimals.
*/
const HEXADECIMAL = 'hex';

/**
* Return hashes as decimals.
*/
const DECIMAL = 'dec';

/**
* The hashing implementation.
*
* @var Implementation
*/
protected $implementation;

/**
* @var string
*/
protected $mode;

/**
* @var Image
*/
private $driver;

/**
* Constructor.
*
* @param Implementation $implementation
* @param string $mode
* @param ImageManager $driver
*/
public function __construct(
Implementation $implementation = null,
$mode = self::HEXADECIMAL,
ImageManager $driver = null
) {
$this->implementation = $implementation ?: new DifferenceHash;
$this->mode = $mode;
$this->implementation = $implementation ?: $this->defaultDriver();
$this->driver = $driver ?: $this->defaultDriver();
}

/**
* Calculate a perceptual hash of an image.
*
* @param mixed $image
* @return int
* @return Hash
*/
public function hash($image)
{
$image = $this->driver->make($image);

$hash = $this->implementation->hash($image);

return $this->formatHash($hash);
}

/**
* Calculate a perceptual hash of an image string.
*
* @deprecated
* @param mixed $data Image data
* @return string
*/
public function hashFromString($data)
{
return $this->hash($data);
return $this->implementation->hash($image);
}

/**
* Compare 2 images and get the hamming distance.
*
* @param mixed $resource1
* @param mixed $resource2
* @param mixed $resource1
* @param mixed $resource2
* @return int
*/
public function compare($resource1, $resource2)
Expand All @@ -94,52 +58,15 @@ public function compare($resource1, $resource2)
}

/**
* Calculate the Hamming Distance.
*
* @param int $hash1
* @param int $hash2
* @return int
*/
public function distance($hash1, $hash2)
{
if (extension_loaded('gmp')) {
if ($this->mode === self::HEXADECIMAL) {
$dh = gmp_hamdist('0x' . $hash1, '0x' . $hash2);
} else {
$dh = gmp_hamdist($hash1, $hash2);
}
} else {
if ($this->mode === self::HEXADECIMAL) {
$hash1 = $this->hexdec($hash1);
$hash2 = $this->hexdec($hash2);
}

$dh = 0;
for ($i = 0; $i < 64; $i++) {
$k = (1 << $i);
if (($hash1 & $k) !== ($hash2 & $k)) {
$dh++;
}
}
}

return $dh;
}

/**
* Convert hexadecimal to signed decimal.
* Calculate the Hamming Distance between 2 hashes.
*
* @param string $hex
* @param Hash $hash1
* @param Hash $hash2
* @return int
*/
public function hexdec($hex)
public function distance(Hash $hash1, Hash $hash2)
{
if (strlen($hex) === 16 && hexdec($hex[0]) > 8) {
list($higher, $lower) = array_values(unpack('N2', hex2bin($hex)));
return $higher << 32 | $lower;
}

return hexdec($hex);
return $hash1->distance($hash2);
}

/**
Expand All @@ -152,14 +79,11 @@ protected function createResource($data)
}

/**
* Format hash in hex.
*
* @param int $hash
* @return string|int
* @return Implementation
*/
protected function formatHash($hash)
protected function defaultImplementation()
{
return $this->mode === static::HEXADECIMAL ? dechex($hash) : $hash;
return new DifferenceHash();
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Implementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
interface Implementation
{
/**
* Calculate the hash for the given resource.
* Calculate the hash for the given image.
*
* @param Image $image
* @return int
* @return Hash
*/
public function hash(Image $image);
}
34 changes: 19 additions & 15 deletions src/Implementations/AverageHash.php
Original file line number Diff line number Diff line change
@@ -1,45 +1,49 @@
<?php namespace Jenssegers\ImageHash\Implementations;

use Intervention\Image\Image;
use Jenssegers\ImageHash\Hash;
use Jenssegers\ImageHash\Implementation;

class AverageHash implements Implementation
{
/**
* Downscaled image size.
* @var int
*/
const SIZE = 8;
protected $size;

/**
* @param int $size
*/
public function __construct($size = 8)
{
$this->size = $size;
}

/**
* @inheritdoc
*/
public function hash(Image $image)
{
// Resize the image.
$resized = $image->resize(static::SIZE, static::SIZE);
$resized = $image->resize($this->size, $this->size);

// Create an array of greyscale pixel values.
$pixels = [];
for ($y = 0; $y < static::SIZE; $y++) {
for ($x = 0; $x < static::SIZE; $x++) {
for ($y = 0; $y < $this->size; $y++) {
for ($x = 0; $x < $this->size; $x++) {
$rgb = $resized->pickColor($x, $y);
$pixels[] = floor(($rgb[0] + $rgb[1] + $rgb[2]) / 3);
$pixels[] = (int) floor(($rgb[0] * 0.299) + ($rgb[1] * 0.587) + ($rgb[2] * 0.114));
}
}

// Get the average pixel value.
$average = floor(array_sum($pixels) / count($pixels));

// Each hash bit is set based on whether the current pixels value is above or below the average.
$hash = 0;
$one = 1;
foreach ($pixels as $pixel) {
if ($pixel > $average) {
$hash |= $one;
}
$one = $one << 1;
}
$bits = array_map(function ($pixel) use ($average) {
return (int) ($pixel > $average);
}, $pixels);

return $hash;
return Hash::fromBits($bits);
}
}
Loading

0 comments on commit 006a1bc

Please sign in to comment.