/ lib / phash.php
phash.php
  1  <?php
  2  
  3  class Phash {
  4    const IMG_SIZE = 32;
  5    
  6    // Precalculates DCT constants matrix and its transpose
  7    private static function dct_matrices() {
  8      $N = self::IMG_SIZE;
  9      
 10      static $ret = null;
 11      
 12      if ($ret !== null) {
 13        return $ret;
 14      }
 15      
 16      $dct = [];
 17      
 18      $c0 = sqrt(1.0 / $N);
 19      $c1 = sqrt(2.0 / $N);
 20      
 21      $hpn = M_PI / 2 / $N;
 22      
 23      for ($y = 0; $y < $N; ++$y) {
 24        $row = [];
 25        
 26        if ($y === 0) {
 27          $c = $c0;
 28        }
 29        else {
 30          $c = $c1;
 31        }
 32        
 33        for ($x = 0; $x < $N; ++$x) {
 34          $row[] = $c * cos($hpn * $y * (2 * $x + 1));
 35        }
 36        
 37        $dct[] = $row;
 38      }
 39      
 40      $dct_t = array_map(null, ...$dct);
 41      
 42      $ret = [ $dct, $dct_t ];
 43      
 44      return $ret;
 45    }
 46    
 47    private static function mat_mul($mat1, $mat2) {
 48      $N = self::IMG_SIZE;
 49      
 50      $ret = [];
 51      
 52      for ($i = 0; $i < $N; $i++) {
 53        $ret[$i] = [];
 54        
 55        for ($j = 0; $j < $N; $j++) {
 56          $ret[$i][$j] = 0;
 57          
 58          for ($k = 0; $k < $N; $k++) {
 59            $ret[$i][$j] += $mat1[$i][$k] * $mat2[$k][$j];
 60          }
 61        }
 62      }
 63      
 64      return $ret;
 65    }
 66    
 67    private static function median($array) {
 68      if (!$array) {
 69        return 0;
 70      }
 71      
 72      sort($array);
 73      
 74      $count = count($array);
 75      
 76      $mid = (int)($count / 2);
 77      
 78      if ($count & 1) {
 79        return $array[$mid];
 80      }
 81      else {
 82        return ($array[$mid - 1] + $array[$mid]) / 2;
 83      }
 84    }
 85    
 86    public static function hash_distance($hash1, $hash2) {
 87      $counts = [0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4];
 88      
 89      $res = 0;
 90      
 91      for ($i = 0; $i < 16; $i++) {
 92        if ($hash1[$i] != $hash2[$i]) {
 93          $res += $counts[hexdec($hash1[$i]) ^ hexdec($hash2[$i])];
 94        }
 95      }
 96      
 97      return $res;
 98    }
 99    
100    // $input_image is a GD image instance
101    public static function hash($input_image, $width, $height) {
102      if (!$input_image || $width < 1 || $height < 1) {
103        return false;
104      }
105      
106      $N = self::IMG_SIZE;
107      
108      $img = imagecreatetruecolor($N, $N);
109      
110      if (!$img) {
111        return false;
112      }
113      
114      $_ret = imagecopyresampled($img, $input_image, 0, 0, 0, 0, $N, $N, $width, $height);
115      
116      if (!$_ret) {
117        return false;
118      }
119      
120      imagefilter($img, IMG_FILTER_GRAYSCALE);
121      
122      $colors = [];
123      
124      for ($y = 0; $y < $N; $y++){
125        $row = [];
126        
127        for ($x = 0; $x < $N; $x++){
128          $row[] = imagecolorat($img, $x, $y) & 0xFF;
129        }
130        
131        $colors[] = $row;
132      }
133      
134      imagedestroy($img);
135      
136      list($dct, $dct_t) = self::dct_matrices();
137      
138      $dct_img = self::mat_mul(self::mat_mul($dct, $colors), $dct_t);
139      
140      $subsec = [];
141      
142      for ($y = 1; $y < 9; ++$y) {
143        for ($x = 1; $x < 9; ++$x) {
144          $subsec[] = $dct_img[$y][$x];
145        }
146      }
147      
148      $median = self::median($subsec);
149      
150      $hash = 0;
151      
152      for ($i = 0; $i < 64; $i++, $hash <<= 1) {
153        if ($subsec[$i] > $median) {
154          $hash |= 0x01;
155        }
156      }
157      
158      return sprintf("%016x", $hash);
159    }
160    
161    public static function hash_file($image_path, &$err = null) {
162      if (!$image_path) {
163        return false;
164      }
165      
166      $size = getimagesize($image_path);
167      
168      if (!$size) {
169        $err = "Couldn't get file information";
170        return false;
171      }
172      
173      $type = $size[2];
174      
175      if ($type === IMAGETYPE_PNG) {
176        $img = imagecreatefrompng($image_path);
177      }
178      else if ($type === IMAGETYPE_JPEG) {
179        $img = imagecreatefromjpeg($image_path);
180      }
181      else if ($type === IMAGETYPE_GIF) {
182        $img = imagecreatefromgif($image_path);
183      }
184      else {
185        $err = "Invalid image";
186        return false;
187      }
188      
189      $width = $size[0];
190      $height = $size[1];
191      
192      $hash = self::hash($img, $width, $height);
193      
194      if ($hash === false) {
195        $err = "Couldn't process the image";
196        return false;
197      }
198      
199      return $hash;
200    }
201  }