PageRenderTime 54ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/app/parsers/slir/croppers/smart.class.php

https://github.com/intermedialab/stacey
PHP | 837 lines | 460 code | 109 blank | 268 comment | 111 complexity | 4452e70e28dea9759cf04b49cf2b6d91 MD5 | raw file
Possible License(s): GPL-3.0
  1. <?php
  2. /**
  3. * Class definition file for the smart SLIR cropper
  4. *
  5. * This file is part of SLIR (Smart Lencioni Image Resizer).
  6. *
  7. * SLIR is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * SLIR is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with SLIR. If not, see <http://www.gnu.org/licenses/>.
  19. *
  20. * @copyright Copyright © 2012, Joe Lencioni
  21. * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public License version 3 (GPLv3)
  22. * @since 2.0
  23. * @package SLIR
  24. * @subpackage Croppers
  25. */
  26. require_once 'slircropper.interface.php';
  27. /**
  28. * Smart SLIR cropper
  29. *
  30. * @since 2.0
  31. * @author Joe Lencioni <joe@shiftingpixel.com>
  32. * @package SLIR
  33. * @subpackage Croppers
  34. */
  35. class SLIRCropperSmart implements SLIRCropper
  36. {
  37. const COLOR_SNAP = 20;
  38. const OFFSET_NEAR = 0;
  39. const OFFSET_FAR = 1;
  40. const PIXEL_RGB = 0;
  41. const PIXEL_ROUNDED_COLOR = 1;
  42. const PIXEL_LAB = 2;
  43. const PIXEL_DELTA_E = 3;
  44. const PIXEL_INTERESTINGNESS = 4;
  45. const RGB_RED = 0;
  46. const RGB_GREEN = 1;
  47. const RGB_BLUE = 2;
  48. const XYZ_X = 0;
  49. const XYZ_Y = 1;
  50. const XYZ_Z = 2;
  51. const LAB_L = 0;
  52. const LAB_A = 1;
  53. const LAB_B = 2;
  54. /**
  55. * @var array
  56. */
  57. private $colors;
  58. /**
  59. * @var array
  60. */
  61. private $rows;
  62. /**
  63. * @var array
  64. */
  65. private $rgbs;
  66. /**
  67. * @var array Cached LAB color values for integer color indexes
  68. */
  69. private $labColors;
  70. /**
  71. * @var array Cached deltas for color index comparisons
  72. */
  73. private $deltas;
  74. /**
  75. * Destruct method. Try to clean up memory a little.
  76. *
  77. * @return void
  78. * @since 2.0
  79. */
  80. public function __destruct()
  81. {
  82. unset($this->colors, $this->rows, $this->rgbs, $this->labColors, $this->deltas);
  83. }
  84. /**
  85. * Determines if the top and bottom need to be cropped
  86. *
  87. * @since 2.0
  88. * @param SLIRImage $image
  89. * @return boolean
  90. */
  91. private function shouldCropTopAndBottom(SLIRImage $image)
  92. {
  93. if ($image->getCropRatio() > $image->getRatio()) {
  94. return true;
  95. } else {
  96. return false;
  97. }
  98. }
  99. /**
  100. * Determines the optimal number of rows in from the top or left to crop
  101. * the source image
  102. *
  103. * @since 2.0
  104. * @param SLIRImage $image
  105. * @return integer|boolean
  106. */
  107. private function cropSmartOffsetRows(SLIRImage $image)
  108. {
  109. // @todo Change this method to resize image, determine offset, and then extrapolate the actual offset based on the image size difference. Then we can cache the offset in APC (all just like we are doing for face detection)
  110. if ($this->shouldCropTopAndBottom($image)) {
  111. $length = $image->getCropHeight();
  112. $lengthB = $image->getCropWidth();
  113. $originalLength = $image->getHeight();
  114. } else {
  115. $length = $image->getCropWidth();
  116. $lengthB = $image->getCropHeight();
  117. $originalLength = $image->getWidth();
  118. }
  119. // To smart crop an image, we need to calculate the difference between
  120. // each pixel in each row and its adjacent pixels. Add these up to
  121. // determine how interesting each row is. Based on how interesting each
  122. // row is, we can determine whether or not to discard it. We start with
  123. // the closest row and the farthest row and then move on from there.
  124. // All colors in the image will be stored in the colors array.
  125. // This array will also include information about each pixel's
  126. // interestingness.
  127. //
  128. // For example (rough representation):
  129. //
  130. // $this->colors = array(
  131. // x1 => array(
  132. // x1y1 => array(
  133. // self::PIXEL_LAB => array(l, a, b),
  134. // self::PIXEL_DELTA_E => array(TL, TC, TR, LC, LR, BL, BC, BR),
  135. // self::PIXEL_INTERESTINGNESS => computedInterestingness
  136. // ),
  137. // x1y2 => array( ... ),
  138. // ...
  139. // ),
  140. // x2 => array( ... ),
  141. // ...
  142. // );
  143. $this->colors = array();
  144. // Offset will remember how far in from each side we are in the
  145. // cropping game
  146. $offset = array(
  147. self::OFFSET_NEAR => 0,
  148. self::OFFSET_FAR => 0,
  149. );
  150. $rowsToCrop = $originalLength - $length;
  151. // $pixelStep will sacrifice accuracy for memory and speed. Essentially
  152. // it acts as a spot-checker and scales with the size of the cropped area
  153. $pixelStep = round(sqrt($rowsToCrop * $lengthB) / 10);
  154. // We won't save much speed if the pixelStep is between 4 and 1 because
  155. // we still need to sample adjacent pixels
  156. if ($pixelStep < 4) {
  157. $pixelStep = 1;
  158. }
  159. $tolerance = 0.5;
  160. $upperTol = 1 + $tolerance;
  161. $lowerTol = 1 / $upperTol;
  162. // Fight the near and far rows. The stronger will remain standing.
  163. $returningChampion = null;
  164. $ratio = 1;
  165. for ($rowsCropped = 0; $rowsCropped < $rowsToCrop; ++$rowsCropped) {
  166. $a = $this->rowInterestingness($image, $offset[self::OFFSET_NEAR], $pixelStep, $originalLength);
  167. $b = $this->rowInterestingness($image, $originalLength - $offset[self::OFFSET_FAR] - 1, $pixelStep, $originalLength);
  168. if ($a == 0 && $b == 0) {
  169. $ratio = 1;
  170. } else if ($b == 0) {
  171. $ratio = 1 + $a;
  172. } else {
  173. $ratio = $a / $b;
  174. }
  175. if ($ratio > $upperTol) {
  176. ++$offset[self::OFFSET_FAR];
  177. // Fightback. Winning side gets to go backwards through fallen rows
  178. // to see if they are stronger
  179. if ($returningChampion == self::OFFSET_NEAR) {
  180. $offset[self::OFFSET_NEAR] -= ($offset[self::OFFSET_NEAR] > 0) ? 1 : 0;
  181. } else {
  182. $returningChampion = self::OFFSET_NEAR;
  183. }
  184. } else if ($ratio < $lowerTol) {
  185. ++$offset[self::OFFSET_NEAR];
  186. if ($returningChampion == self::OFFSET_FAR) {
  187. $offset[self::OFFSET_FAR] -= ($offset[self::OFFSET_FAR] > 0) ? 1 : 0;
  188. } else {
  189. $returningChampion = self::OFFSET_FAR;
  190. }
  191. } else {
  192. // There is no strong winner, so discard rows from the side that
  193. // has lost the fewest so far. Essentially this is a draw.
  194. if ($offset[self::OFFSET_NEAR] > $offset[self::OFFSET_FAR]) {
  195. ++$offset[self::OFFSET_FAR];
  196. } else {
  197. // Discard near
  198. ++$offset[self::OFFSET_NEAR];
  199. }
  200. // No fightback for draws
  201. $returningChampion = null;
  202. } // if
  203. } // for
  204. // Bounceback for potentially important details on the edge.
  205. // This may possibly be better if the winning side fights a hard final
  206. // push multiple-rows-at-stake battle where it stands the chance to gain
  207. // ground.
  208. if ($ratio > (1 + ($tolerance * 1.25))) {
  209. $offset[self::OFFSET_NEAR] -= round($length * .03);
  210. } else if ($ratio < (1 / (1 + ($tolerance * 1.25)))) {
  211. $offset[self::OFFSET_NEAR] += round($length * .03);
  212. }
  213. return min($rowsToCrop, max(0, $offset[self::OFFSET_NEAR]));
  214. }
  215. /**
  216. * Calculate the interestingness value of a row of pixels
  217. *
  218. * @since 2.0
  219. * @param SLIRImage $image
  220. * @param integer $row
  221. * @param integer $pixelStep Number of pixels to jump after each step when comparing interestingness
  222. * @param integer $originalLength Number of rows in the original image
  223. * @return float
  224. */
  225. private function rowInterestingness(SLIRImage $image, $row, $pixelStep, $originalLength)
  226. {
  227. if (!isset($this->rows[$row])) {
  228. $interestingness = 0;
  229. $max = 0;
  230. if ($this->shouldCropTopAndBottom($image)) {
  231. for ($totalPixels = 0; $totalPixels < $image->getWidth(); $totalPixels += $pixelStep) {
  232. $i = $this->pixelInterestingness($image, $totalPixels, $row);
  233. // Content at the very edge of an image tends to be less interesting than
  234. // content toward the center, so we give it a little extra push away from the edge
  235. //$i += min($row, $originalLength - $row, $originalLength * .04);
  236. $max = max($i, $max);
  237. $interestingness += $i;
  238. }
  239. } else {
  240. for ($totalPixels = 0; $totalPixels < $image->getHeight(); $totalPixels += $pixelStep) {
  241. $i = $this->pixelInterestingness($image, $row, $totalPixels);
  242. // Content at the very edge of an image tends to be less interesting than
  243. // content toward the center, so we give it a little extra push away from the edge
  244. //$i += min($row, $originalLength - $row, $originalLength * .04);
  245. $max = max($i, $max);
  246. $interestingness += $i;
  247. }
  248. }
  249. $this->rows[$row] = $interestingness + (($max - ($interestingness / ($totalPixels / $pixelStep))) * ($totalPixels / $pixelStep));
  250. }
  251. return $this->rows[$row];
  252. }
  253. /**
  254. * Get the interestingness value of a pixel
  255. *
  256. * @since 2.0
  257. * @param SLIRImage $image
  258. * @param integer $x x-axis position of pixel to calculate
  259. * @param integer $y y-axis position of pixel to calculate
  260. * @return float
  261. */
  262. private function pixelInterestingness(SLIRImage $image, $x, $y)
  263. {
  264. if (!isset($this->colors[$x][$y][self::PIXEL_INTERESTINGNESS])) {
  265. // Ensure this pixel's color information has already been loaded
  266. $this->loadPixelInfo($image, $x, $y);
  267. // Calculate each neighboring pixel's Delta E in relation to this
  268. // pixel
  269. $this->calculateDeltas($image, $x, $y);
  270. // Calculate the interestingness of this pixel based on neighboring
  271. // pixels' Delta E in relation to this pixel
  272. $this->calculateInterestingness($x, $y);
  273. } // if
  274. return $this->colors[$x][$y][self::PIXEL_INTERESTINGNESS];
  275. }
  276. /**
  277. * Load the color information of the requested pixel into the $colors array
  278. *
  279. * @since 2.0
  280. * @param SLIRImage $image
  281. * @param integer $x x-axis position of pixel to calculate
  282. * @param integer $y y-axis position of pixel to calculate
  283. * @return boolean
  284. */
  285. private function loadPixelInfo(SLIRImage $image, $x, $y)
  286. {
  287. if ($x < 0 || $y < 0 || $x >= $image->getWidth() || $y >= $image->getHeight()) {
  288. return false;
  289. }
  290. if (!isset($this->colors[$x][$y][self::PIXEL_INTERESTINGNESS], $this->colors[$x][$y][self::PIXEL_LAB])) {
  291. $this->colors[$x][$y][self::PIXEL_RGB] = $this->colorIndexToRGB(imagecolorat($image->getImage(), $x, $y));
  292. $this->colors[$x][$y][self::PIXEL_ROUNDED_COLOR] = $this->RGBAToColorIndex(
  293. $this->colors[$x][$y][self::PIXEL_RGB][self::RGB_RED],
  294. $this->colors[$x][$y][self::PIXEL_RGB][self::RGB_GREEN],
  295. $this->colors[$x][$y][self::PIXEL_RGB][self::RGB_BLUE]
  296. );
  297. $this->colors[$x][$y][self::PIXEL_LAB] = $this->evaluateColor($this->colors[$x][$y][self::PIXEL_ROUNDED_COLOR]);
  298. }
  299. return true;
  300. }
  301. /**
  302. * Calculates each adjacent pixel's Delta E in relation to the pixel requested
  303. *
  304. * @since 2.0
  305. * @param SLIRImage $image
  306. * @param integer $x x-axis position of pixel to calculate
  307. * @param integer $y y-axis position of pixel to calculate
  308. * @return boolean
  309. */
  310. private function calculateDeltas(SLIRImage $image, $x, $y)
  311. {
  312. // Calculate each adjacent pixel's Delta E in relation to the current
  313. // pixel (top left, top center, top right, center left, center right,
  314. // bottom left, bottom center, and bottom right)
  315. if (!isset($this->colors[$x][$y][self::PIXEL_DELTA_E]['d-1-1'])) {
  316. $this->calculateDelta($image, $x, $y, -1, -1);
  317. }
  318. if (!isset($this->colors[$x][$y][self::PIXEL_DELTA_E]['d0-1'])) {
  319. $this->calculateDelta($image, $x, $y, 0, -1);
  320. }
  321. if (!isset($this->colors[$x][$y][self::PIXEL_DELTA_E]['d1-1'])) {
  322. $this->calculateDelta($image, $x, $y, 1, -1);
  323. }
  324. if (!isset($this->colors[$x][$y][self::PIXEL_DELTA_E]['d-10'])) {
  325. $this->calculateDelta($image, $x, $y, -1, 0);
  326. }
  327. if (!isset($this->colors[$x][$y][self::PIXEL_DELTA_E]['d10'])) {
  328. $this->calculateDelta($image, $x, $y, 1, 0);
  329. }
  330. if (!isset($this->colors[$x][$y][self::PIXEL_DELTA_E]['d-11'])) {
  331. $this->calculateDelta($image, $x, $y, -1, 1);
  332. }
  333. if (!isset($this->colors[$x][$y][self::PIXEL_DELTA_E]['d01'])) {
  334. $this->calculateDelta($image, $x, $y, 0, 1);
  335. }
  336. if (!isset($this->colors[$x][$y][self::PIXEL_DELTA_E]['d11'])) {
  337. $this->calculateDelta($image, $x, $y, 1, 1);
  338. }
  339. return true;
  340. }
  341. /**
  342. * Calculates and stores requested pixel's Delta E in relation to comparison pixel
  343. *
  344. * @since 2.0
  345. * @param SLIRImage $image
  346. * @param integer $xA x-axis position of pixel to calculate
  347. * @param integer $yA y-axis position of pixel to calculate
  348. * @param integer $xMove number of pixels to move on the x-axis to find comparison pixel
  349. * @param integer $yMove number of pixels to move on the y-axis to find comparison pixel
  350. * @return boolean
  351. */
  352. private function calculateDelta(SLIRImage $image, $xA, $yA, $xMove, $yMove)
  353. {
  354. $xB = $xA + $xMove;
  355. $yB = $yA + $yMove;
  356. // Pixel is outside of the image, so we cant't calculate the Delta E
  357. if ($xB < 0 || $xB >= $image->getWidth() || $yB < 0 || $yB >= $image->getHeight()) {
  358. return null;
  359. }
  360. if (!isset($this->colors[$xA][$yA][self::PIXEL_LAB])) {
  361. $this->loadPixelInfo($image, $xA, $yA);
  362. }
  363. if (!isset($this->colors[$xB][$yB][self::PIXEL_LAB])) {
  364. $this->loadPixelInfo($image, $xB, $yB);
  365. }
  366. $lowerColor = min(
  367. $this->colors[$xA][$yA][self::PIXEL_ROUNDED_COLOR],
  368. $this->colors[$xB][$yB][self::PIXEL_ROUNDED_COLOR]
  369. );
  370. $upperColor = max(
  371. $this->colors[$xA][$yA][self::PIXEL_ROUNDED_COLOR],
  372. $this->colors[$xB][$yB][self::PIXEL_ROUNDED_COLOR]
  373. );
  374. if (!isset($this->deltas[$lowerColor][$upperColor])) {
  375. $this->deltas[$lowerColor][$upperColor] = $this->deltaE($this->colors[$xA][$yA][self::PIXEL_LAB], $this->colors[$xB][$yB][self::PIXEL_LAB]);
  376. }
  377. $this->colors[$xA][$yA][self::PIXEL_DELTA_E]["d$xMove$yMove"] = $this->deltas[$lowerColor][$upperColor];
  378. $xBMove = $xMove * -1;
  379. $yBMove = $yMove * -1;
  380. $this->colors[$xB][$yB][self::PIXEL_DELTA_E]["d$xBMove$yBMove"] =& $this->colors[$xA][$yA][self::PIXEL_DELTA_E]["d$xMove$yMove"];
  381. return true;
  382. }
  383. /**
  384. * Calculates and stores a pixel's overall interestingness value
  385. *
  386. * @since 2.0
  387. * @param integer $x x-axis position of pixel to calculate
  388. * @param integer $y y-axis position of pixel to calculate
  389. * @return boolean
  390. */
  391. private function calculateInterestingness($x, $y)
  392. {
  393. // The interestingness is the average of the pixel's Delta E values
  394. $this->colors[$x][$y][self::PIXEL_INTERESTINGNESS] = array_sum($this->colors[$x][$y][self::PIXEL_DELTA_E])
  395. / count(array_filter($this->colors[$x][$y][self::PIXEL_DELTA_E], 'is_numeric'));
  396. return true;
  397. }
  398. /**
  399. * @since 2.0
  400. * @param integer $int
  401. * @return array
  402. */
  403. private function evaluateColor($int)
  404. {
  405. if (!isset($this->labColors[$int])) {
  406. $rgb = $this->colorIndexToRGB($int);
  407. $xyz = $this->RGBtoXYZ($rgb);
  408. $this->labColors[$int] = $this->XYZtoHunterLab($xyz);
  409. }
  410. return $this->labColors[$int];
  411. }
  412. /**
  413. * @since 2.0
  414. * @param integer $int
  415. * @return array
  416. */
  417. private function colorIndexToRGB($int)
  418. {
  419. if (!isset($this->rgbs[$int])) {
  420. $a = (255 - (($int >> 24) & 0xFF)) / 255;
  421. $r = (round((($int >> 16) & 0xFF) / self::COLOR_SNAP) * self::COLOR_SNAP) * $a;
  422. $g = (round((($int >> 8) & 0xFF) / self::COLOR_SNAP) * self::COLOR_SNAP) * $a;
  423. $b = (round(($int & 0xFF) / self::COLOR_SNAP) * self::COLOR_SNAP) * $a;
  424. $this->rgbs[$int] = array(
  425. self::RGB_RED => $r,
  426. self::RGB_GREEN => $g,
  427. self::RGB_BLUE => $b,
  428. );
  429. }
  430. return $this->rgbs[$int];
  431. }
  432. /**
  433. * This function builds a 32 bit integer from 4 values which must be 0-255 (8 bits)
  434. *
  435. * Example 32 bit integer: 00100000010001000000100000010000
  436. * The first 8 bits define the alpha
  437. * The next 8 bits define the blue
  438. * The next 8 bits define the green
  439. * The next 8 bits define the red
  440. *
  441. * @since 2.0
  442. * @param integer $r
  443. * @param integer $g
  444. * @param integer $b
  445. * @return integer
  446. *
  447. * @link http://www.php.net/manual/en/function.imagecolorat.php#85849
  448. */
  449. private function RGBAToColorIndex($r, $g, $b, $a = 1)
  450. {
  451. return ($a << 24) + ($b << 16) + ($g << 8) + $r;
  452. }
  453. /**
  454. * @since 2.0
  455. * @param array $rgb
  456. * @return array XYZ
  457. * @link http://easyrgb.com/index.php?X=MATH&H=02#text2
  458. */
  459. private function RGBtoXYZ($rgb)
  460. {
  461. $r = $rgb[self::RGB_RED] / 255;
  462. $g = $rgb[self::RGB_GREEN] / 255;
  463. $b = $rgb[self::RGB_BLUE] / 255;
  464. if ($r > 0.04045) {
  465. $r = pow((($r + 0.055) / 1.055), 2.4);
  466. } else {
  467. $r = $r / 12.92;
  468. }
  469. if ($g > 0.04045) {
  470. $g = pow((($g + 0.055) / 1.055), 2.4);
  471. } else {
  472. $g = $g / 12.92;
  473. }
  474. if ($b > 0.04045) {
  475. $b = pow((($b + 0.055) / 1.055), 2.4);
  476. } else {
  477. $b = $b / 12.92;
  478. }
  479. $r *= 100;
  480. $g *= 100;
  481. $b *= 100;
  482. //Observer. = 2°, Illuminant = D65
  483. return array(
  484. self::XYZ_X => $r * 0.4124 + $g * 0.3576 + $b * 0.1805,
  485. self::XYZ_Y => $r * 0.2126 + $g * 0.7152 + $b * 0.0722,
  486. self::XYZ_Z => $r * 0.0193 + $g * 0.1192 + $b * 0.9505,
  487. );
  488. }
  489. /**
  490. * @link http://www.easyrgb.com/index.php?X=MATH&H=05#text5
  491. */
  492. private function XYZtoHunterLab($xyz)
  493. {
  494. if ($xyz[self::XYZ_Y] == 0) {
  495. return array(
  496. self::LAB_L => 0,
  497. self::LAB_A => 0,
  498. self::LAB_B => 0,
  499. );
  500. }
  501. return array(
  502. self::LAB_L => 10 * sqrt($xyz[self::XYZ_Y]),
  503. self::LAB_A => 17.5 * (((1.02 * $xyz[self::XYZ_X]) - $xyz[self::XYZ_Y]) / sqrt($xyz[self::XYZ_Y])),
  504. self::LAB_B => 7 * (($xyz[self::XYZ_Y] - (0.847 * $xyz[self::XYZ_Z])) / sqrt($xyz[self::XYZ_Y])),
  505. );
  506. }
  507. /**
  508. * Converts a color from RGB colorspace to CIE-L*ab colorspace
  509. * @since 2.0
  510. * @param array $xyz
  511. * @return array LAB
  512. * @link http://www.easyrgb.com/index.php?X=MATH&H=05#text5
  513. */
  514. private function XYZtoCIELAB($xyz)
  515. {
  516. $refX = 100;
  517. $refY = 100;
  518. $refZ = 100;
  519. $x = $xyz[self::XYZ_X] / $refX;
  520. $y = $xyz[self::XYZ_Y] / $refY;
  521. $z = $xyz[self::XYZ_Z] / $refZ;
  522. if ($x > 0.008856) {
  523. $x = pow($x, 1/3);
  524. } else {
  525. $x = (7.787 * $x) + (16 / 116);
  526. }
  527. if ($y > 0.008856) {
  528. $y = pow($y, 1/3);
  529. } else {
  530. $y = (7.787 * $y) + (16 / 116);
  531. }
  532. if ($z > 0.008856) {
  533. $z = pow($z, 1/3);
  534. } else {
  535. $z = (7.787 * $z) + (16 / 116);
  536. }
  537. return array(
  538. self::LAB_L => (116 * $y) - 16,
  539. self::LAB_A => 500 * ($x - $y),
  540. self::LAB_B => 200 * ($y - $z),
  541. );
  542. }
  543. /**
  544. * @since 2.0
  545. * @param array $labA LAB color array
  546. * @param array $labB LAB color array
  547. * @return float
  548. */
  549. private function deltaE($labA, $labB)
  550. {
  551. return sqrt(
  552. (pow($labA[self::LAB_L] - $labB[self::LAB_L], 2))
  553. + (pow($labA[self::LAB_A] - $labB[self::LAB_A], 2))
  554. + (pow($labA[self::LAB_B] - $labB[self::LAB_B], 2))
  555. );
  556. }
  557. /**
  558. * Compute the Delta E 2000 value of two colors in the LAB colorspace
  559. *
  560. * @link http://en.wikipedia.org/wiki/Color_difference#CIEDE2000
  561. * @link http://easyrgb.com/index.php?X=DELT&H=05#text5
  562. * @since 2.0
  563. * @param array $labA LAB color array
  564. * @param array $labB LAB color array
  565. * @return float
  566. */
  567. private function deltaE2000($labA, $labB)
  568. {
  569. $weightL = 1; // Lightness
  570. $weightC = 1; // Chroma
  571. $weightH = 1; // Hue
  572. $xCA = sqrt($labA[self::LAB_A] * $labA[self::LAB_A] + $labA[self::LAB_B] * $labA[self::LAB_B]);
  573. $xCB = sqrt($labB[self::LAB_A] * $labB[self::LAB_A] + $labB[self::LAB_B] * $labB[self::LAB_B]);
  574. $xCX = ($xCA + $xCB) / 2;
  575. $xGX = 0.5 * (1 - sqrt((pow($xCX, 7)) / ((pow($xCX, 7)) + (pow(25, 7)))));
  576. $xNN = (1 + $xGX) * $labA[self::LAB_A];
  577. $xCA = sqrt($xNN * $xNN + $labA[self::LAB_B] * $labA[self::LAB_B]);
  578. $xHA = $this->LABtoHue($xNN, $labA[self::LAB_B]);
  579. $xNN = (1 + $xGX) * $labB[self::LAB_A];
  580. $xCB = sqrt($xNN * $xNN + $labB[self::LAB_B] * $labB[self::LAB_B]);
  581. $xHB = $this->LABtoHue($xNN, $labB[self::LAB_B]);
  582. $xDL = $labB[self::LAB_L] - $labA[self::LAB_L];
  583. $xDC = $xCB - $xCA;
  584. if (($xCA * $xCB) == 0) {
  585. $xDH = 0;
  586. } else {
  587. $xNN = round($xHB - $xHA, 12);
  588. if (abs($xNN) <= 180) {
  589. $xDH = $xHB - $xHA;
  590. } else {
  591. if ($xNN > 180) {
  592. $xDH = $xHB - $xHA - 360;
  593. } else {
  594. $xDH = $xHB - $xHA + 360;
  595. }
  596. } // if
  597. } // if
  598. $xDH = 2 * sqrt($xCA * $xCB) * sin(rad2deg($xDH / 2));
  599. $xLX = ($labA[self::LAB_L] + $labB[self::LAB_L]) / 2;
  600. $xCY = ($xCA + $xCB) / 2;
  601. if (($xCA * $xCB) == 0) {
  602. $xHX = $xHA + $xHB;
  603. } else {
  604. $xNN = abs(round($xHA - $xHB, 12));
  605. if ($xNN > 180) {
  606. if (($xHB + $xHA) < 360) {
  607. $xHX = $xHA + $xHB + 360;
  608. } else {
  609. $xHX = $xHA + $xHB - 360;
  610. }
  611. } else {
  612. $xHX = $xHA + $xHB;
  613. } // if
  614. $xHX /= 2;
  615. } // if
  616. $xTX = 1 - 0.17 * cos(rad2deg($xHX - 30))
  617. + 0.24 * cos(rad2deg(2 * $xHX))
  618. + 0.32 * cos(rad2deg(3 * $xHX + 6))
  619. - 0.20 * cos(rad2deg(4 * $xHX - 63));
  620. $xPH = 30 * exp(- (($xHX - 275) / 25) * (($xHX - 275) / 25));
  621. $xRC = 2 * sqrt((pow($xCY, 7)) / ((pow($xCY, 7)) + (pow(25, 7))));
  622. $xSL = 1 + ((0.015 * (($xLX - 50) * ($xLX - 50)))
  623. / sqrt(20 + (($xLX - 50) * ($xLX - 50))));
  624. $xSC = 1 + 0.045 * $xCY;
  625. $xSH = 1 + 0.015 * $xCY * $xTX;
  626. $xRT = - sin(rad2deg(2 * $xPH)) * $xRC;
  627. $xDL = $xDL / $weightL * $xSL;
  628. $xDC = $xDC / $weightC * $xSC;
  629. $xDH = $xDH / $weightH * $xSH;
  630. $delta = sqrt(pow($xDL, 2) + pow($xDC, 2) + pow($xDH, 2) + $xRT * $xDC * $xDH);
  631. return (is_nan($delta)) ? 1 : $delta / 100;
  632. }
  633. /**
  634. * Compute the Delta CMC value of two colors in the LAB colorspace
  635. *
  636. * @since 2.0
  637. * @param array $labA LAB color array
  638. * @param array $labB LAB color array
  639. * @return float
  640. * @link http://easyrgb.com/index.php?X=DELT&H=06#text6
  641. */
  642. private function deltaCMC($labA, $labB)
  643. {
  644. // if $weightL is 2 and $weightC is 1, it means that the lightness
  645. // will contribute half as much importance to the delta as the chroma
  646. $weightL = 2; // Lightness
  647. $weightC = 1; // Chroma
  648. $xCA = sqrt((pow($labA[self::LAB_A], 2)) + (pow($labA[self::LAB_B], 2)));
  649. $xCB = sqrt((pow($labB[self::LAB_A], 2)) + (pow($labB[self::LAB_B], 2)));
  650. $xff = sqrt((pow($xCA, 4)) / ((pow($xCA, 4)) + 1900));
  651. $xHA = $this->LABtoHue($labA[self::LAB_A], $labA[self::LAB_B]);
  652. if ($xHA < 164 || $xHA > 345) {
  653. $xTT = 0.36 + abs(0.4 * cos(deg2rad(35 + $xHA)));
  654. } else {
  655. $xTT = 0.56 + abs(0.2 * cos(deg2rad(168 + $xHA)));
  656. }
  657. if ($labA[self::LAB_L] < 16) {
  658. $xSL = 0.511;
  659. } else {
  660. $xSL = (0.040975 * $labA[self::LAB_L]) / (1 + (0.01765 * $labA[self::LAB_L]));
  661. }
  662. $xSC = ((0.0638 * $xCA) / (1 + (0.0131 * $xCA))) + 0.638;
  663. $xSH = (($xff * $xTT) + 1 - $xff) * $xSC;
  664. $xDH = sqrt(pow($labB[self::LAB_A] - $labA[self::LAB_A], 2) + pow($labB[self::LAB_B] - $labA[self::LAB_B], 2) - pow($xCB - $xCA, 2));
  665. $xSL = ($labB[self::LAB_L] - $labA[self::LAB_L]) / $weightL * $xSL;
  666. $xSC = ($xCB - $xCA) / $weightC * $xSC;
  667. $xSH = $xDH / $xSH;
  668. $delta = sqrt(pow($xSL, 2) + pow($xSC, 2) + pow($xSH, 2));
  669. return (is_nan($delta)) ? 1 : $delta;
  670. }
  671. /**
  672. * @since 2.0
  673. * @param integer $a
  674. * @param integer $b
  675. * @return CIE-H° value
  676. */
  677. private function LABtoHue($a, $b)
  678. {
  679. $bias = 0;
  680. if ($a >= 0 && $b == 0) {
  681. return 0;
  682. }
  683. if ($a < 0 && $b == 0) {
  684. return 180;
  685. }
  686. if ($a == 0 && $b > 0) {
  687. return 90;
  688. }
  689. if ($a == 0 && $b < 0) {
  690. return 270;
  691. }
  692. if ($a > 0 && $b > 0) {
  693. $bias = 0;
  694. }
  695. if ($a < 0) {
  696. $bias = 180;
  697. }
  698. if ($a > 0 && $b < 0) {
  699. $bias = 360;
  700. }
  701. return (rad2deg(atan($b / $a)) + $bias);
  702. }
  703. /**
  704. * Calculates the crop offset using an algorithm that tries to determine
  705. * the most interesting portion of the image to keep.
  706. *
  707. * @since 2.0
  708. * @param SLIRImage $image
  709. * @return array Associative array with the keys of x and y that specify the top left corner of the box that should be cropped
  710. */
  711. public function getCrop(SLIRImage $image)
  712. {
  713. // Try contrast detection
  714. $o = $this->cropSmartOffsetRows($image);
  715. $crop = array(
  716. 'x' => 0,
  717. 'y' => 0,
  718. );
  719. if ($o === false) {
  720. return true;
  721. } else if ($this->shouldCropTopAndBottom($image)) {
  722. $crop['y'] = $o;
  723. } else {
  724. $crop['x'] = $o;
  725. }
  726. return $crop;
  727. }
  728. }