Skip to content

Commit fc2acef

Browse files
committed
Merge PR #44: Performance improvements
Around 30% faster and between 20 to 50% less memory usage Closes #44
2 parents 84a9676 + 2c71a6b commit fc2acef

File tree

3 files changed

+70
-124
lines changed

3 files changed

+70
-124
lines changed

lib/ColorThief/ColorThief.php

Lines changed: 65 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -137,47 +137,29 @@ public static function getPalette($sourceImage, $colorCount = 10, $quality = 10,
137137
throw new \InvalidArgumentException('The quality argument must be an integer greater than one.');
138138
}
139139

140-
$pixelArray = static::loadImage($sourceImage, $quality, $area);
141-
if (!count($pixelArray)) {
140+
$histo = [];
141+
$numPixelsAnalyzed = static::loadImage($sourceImage, $quality, $histo, $area);
142+
if ($numPixelsAnalyzed === 0) {
142143
throw new \RuntimeException('Unable to compute the color palette of a blank or transparent image.', 1);
143144
}
144145

145-
// Send array to quantize function which clusters values
146+
// Send histogram to quantize function which clusters values
146147
// using median cut algorithm
147-
$cmap = static::quantize($pixelArray, $colorCount);
148+
$cmap = static::quantize($numPixelsAnalyzed, $colorCount, $histo);
148149
$palette = $cmap->palette();
149150

150151
return $palette;
151152
}
152153

153-
/**
154-
* Histo: 1-d array, giving the number of pixels in each quantized region of color space.
155-
*
156-
* @param array $pixels
157-
*
158-
* @return array
159-
*/
160-
private static function getHisto($pixels)
161-
{
162-
$histo = [];
163-
164-
foreach ($pixels as $rgb) {
165-
list($red, $green, $blue) = static::getColorsFromIndex($rgb);
166-
$bucketIndex = static::getColorIndex($red, $green, $blue);
167-
$histo[$bucketIndex] = (isset($histo[$bucketIndex]) ? $histo[$bucketIndex] : 0) + 1;
168-
}
169-
170-
return $histo;
171-
}
172-
173154
/**
174155
* @param mixed $sourceImage Path/URL to the image, GD resource, Imagick instance, or image as binary string
175-
* @param int $quality
156+
* @param int $quality Analyze every $quality pixels
157+
* @param array $histo Histogram
176158
* @param array|null $area
177159
*
178-
* @return SplFixedArray
160+
* @return int
179161
*/
180-
private static function loadImage($sourceImage, $quality, array $area = null)
162+
private static function loadImage($sourceImage, $quality, array &$histo, array $area = null)
181163
{
182164
$loader = new ImageLoader();
183165
$image = $loader->load($sourceImage);
@@ -197,25 +179,46 @@ private static function loadImage($sourceImage, $quality, array $area = null)
197179
}
198180
}
199181

200-
$pixelCount = $width * $height;
182+
// Fill a SplArray with zeroes to initialize the 5-bit buckets and avoid having to check isset in the pixel loop.
183+
// There are 32768 buckets because each color is 5 bits (15 bits total for RGB values).
184+
$totalBuckets = (1 << (3 * self::SIGBITS));
185+
$histoSpl = new SplFixedArray($totalBuckets);
186+
for ($i = 0; $i < $totalBuckets; $i++) {
187+
$histoSpl[$i] = 0;
188+
}
201189

202-
// Store the RGB values in an array format suitable for quantize function
203-
// SplFixedArray is faster and more memory-efficient than normal PHP array.
204-
$pixelArray = new SplFixedArray(ceil($pixelCount / $quality));
190+
$numUsefulPixels = 0;
191+
$pixelCount = $width * $height;
205192

206-
$size = 0;
207-
for ($i = 0; $i < $pixelCount; $i = $i + $quality) {
193+
for ($i = 0; $i < $pixelCount; $i += $quality) {
208194
$x = $startX + ($i % $width);
209195
$y = (int) ($startY + $i / $width);
210196
$color = $image->getPixelColor($x, $y);
211197

212-
if (static::isClearlyVisible($color) && static::isNonWhite($color)) {
213-
$pixelArray[$size++] = static::getColorIndex($color->red, $color->green, $color->blue, 8);
214-
// TODO : Compute directly the histogram here ? (save one iteration over all pixels)
198+
// Pixel is too transparent. Its alpha value is larger (more transparent) than THRESHOLD_ALPHA.
199+
// PHP's transparency range (0-127 opaque-transparent) is reverse that of Javascript (0-255 tranparent-opaque).
200+
if ($color->alpha > self::THRESHOLD_ALPHA) {
201+
continue;
202+
}
203+
204+
// Pixel is too white to be useful. Its RGB values all exceed THRESHOLD_WHITE
205+
if ($color->red > self::THRESHOLD_WHITE && $color->green > self::THRESHOLD_WHITE && $color->blue > self::THRESHOLD_WHITE) {
206+
continue;
215207
}
208+
209+
// Count this pixel in its histogram bucket.
210+
$numUsefulPixels++;
211+
$bucketIndex = static::getColorIndex($color->red, $color->green, $color->blue);
212+
$histoSpl[$bucketIndex] = $histoSpl[$bucketIndex] + 1;
216213
}
217214

218-
$pixelArray->setSize($size);
215+
// Copy the histogram buckets that had pixels back to a normal array.
216+
$histo = [];
217+
foreach ($histoSpl as $bucketInt => $numPixels) {
218+
if ($numPixels > 0) {
219+
$histo[$bucketInt] = $numPixels;
220+
}
221+
}
219222

220223
// Don't destroy a resource passed by the user !
221224
// TODO Add a method in ImageLoader to know if the image should be destroy
@@ -224,31 +227,7 @@ private static function loadImage($sourceImage, $quality, array $area = null)
224227
$image->destroy();
225228
}
226229

227-
return $pixelArray;
228-
}
229-
230-
/**
231-
* @param object $color
232-
*
233-
* @return bool
234-
*/
235-
protected static function isClearlyVisible($color)
236-
{
237-
return $color->alpha <= self::THRESHOLD_ALPHA;
238-
}
239-
240-
/**
241-
* @param object $color
242-
*
243-
* @return bool
244-
*/
245-
protected static function isNonWhite($color)
246-
{
247-
return !(
248-
$color->red > self::THRESHOLD_WHITE &&
249-
$color->green > self::THRESHOLD_WHITE &&
250-
$color->blue > self::THRESHOLD_WHITE
251-
);
230+
return $numUsefulPixels;
252231
}
253232

254233
/**
@@ -377,15 +356,20 @@ private static function sumColors($axis, $histo, $vBox)
377356
$sum = 0;
378357
foreach ($secondRange as $secondColor) {
379358
foreach ($thirdRange as $thirdColor) {
380-
list($redBucket, $greenBucket, $blueBucket) = static::rearrangeColors(
381-
$colorIterateOrder,
382-
$firstColor,
383-
$secondColor,
384-
$thirdColor
385-
);
359+
// Rearrange color components
360+
$bucket = [
361+
$colorIterateOrder[0] => $firstColor,
362+
$colorIterateOrder[1] => $secondColor,
363+
$colorIterateOrder[2] => $thirdColor,
364+
];
386365

387366
// The getColorIndex function takes RGB values instead of buckets. The left shift converts our bucket into its RGB value.
388-
$bucketIndex = static::getColorIndex($redBucket << self::RSHIFT, $greenBucket << self::RSHIFT, $blueBucket << self::RSHIFT, self::SIGBITS);
367+
$bucketIndex = static::getColorIndex(
368+
$bucket['r'] << self::RSHIFT,
369+
$bucket['g'] << self::RSHIFT,
370+
$bucket['b'] << self::RSHIFT,
371+
self::SIGBITS
372+
);
389373

390374
if (isset($histo[$bucketIndex])) {
391375
$sum += $histo[$bucketIndex];
@@ -399,29 +383,6 @@ private static function sumColors($axis, $histo, $vBox)
399383
return [$total, $partialSum];
400384
}
401385

402-
/**
403-
* @param array $order
404-
* @param int $color1
405-
* @param int $color2
406-
* @param int $color3
407-
*
408-
* @return array
409-
*/
410-
private static function rearrangeColors(array $order, $color1, $color2, $color3)
411-
{
412-
$data = [
413-
$order[0] => $color1,
414-
$order[1] => $color2,
415-
$order[2] => $color3,
416-
];
417-
418-
return [
419-
$data['r'],
420-
$data['g'],
421-
$data['b'],
422-
];
423-
}
424-
425386
/**
426387
* @param VBox $vBox
427388
* @param array $order
@@ -490,20 +451,24 @@ private static function quantizeIter(&$priorityQueue, $target, $histo)
490451
}
491452

492453
/**
493-
* @param SplFixedArray|array $pixels
454+
* @param $numPixels Number of image pixels analyzed
494455
* @param $maxColors
456+
* @param array $histo Histogram
495457
*
496458
* @return bool|CMap
497459
*/
498-
private static function quantize($pixels, $maxColors)
460+
private static function quantize($numPixels, $maxColors, array &$histo)
499461
{
500-
// short-circuit
501-
if (!count($pixels) || $maxColors < 2 || $maxColors > 256) {
502-
// echo 'wrong number of maxcolors'."\n";
503-
return false;
462+
// Short-Circuits
463+
if ($numPixels === 0) {
464+
throw new \InvalidArgumentException('Zero useable pixels found in image.');
465+
}
466+
if ($maxColors < 2 || $maxColors > 256) {
467+
throw new \InvalidArgumentException('The maxColors parameter must be between 2 and 256 inclusive.');
468+
}
469+
if (count($histo) === 0) {
470+
throw new \InvalidArgumentException('Image produced an empty histogram.');
504471
}
505-
506-
$histo = static::getHisto($pixels);
507472

508473
// check that we aren't below maxcolors already
509474
//if (count($histo) <= $maxcolors) {

lib/ColorThief/VBox.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,11 @@ public function contains(array $rgbValue, $rshift = ColorThief::RSHIFT)
168168
*/
169169
public function longestAxis()
170170
{
171-
$colorWidth['r'] = $this->r2 - $this->r1;
172-
$colorWidth['g'] = $this->g2 - $this->g1;
173-
$colorWidth['b'] = $this->b2 - $this->b1;
171+
// Color-Width for RGB
172+
$red = $this->r2 - $this->r1;
173+
$green = $this->g2 - $this->g1;
174+
$blue = $this->b2 - $this->b1;
174175

175-
return array_search(max($colorWidth), $colorWidth);
176+
return $red >= $green && $red >= $blue ? 'r' : ($green >= $red && $green >= $blue ? 'g' : 'b');
176177
}
177178
}

tests/ColorThiefTest.php

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -306,26 +306,6 @@ public function testNaturalOrder($left, $right, $expected)
306306
);
307307
}
308308

309-
public function testGetHisto()
310-
{
311-
$method = new \ReflectionMethod('\ColorThief\ColorThief', 'getHisto');
312-
$method->setAccessible(true);
313-
314-
// [[229, 210, 51], [133, 24, 135], [216, 235, 108], [132, 25, 134], [223, 46, 29],
315-
// [135, 28, 132], [233, 133, 213], [225, 212, 48]]
316-
$pixels = [15061555, 8722567, 14216044, 8657286, 14626333, 8854660, 15304149, 14799920];
317-
318-
$expectedHisto = [
319-
29510 => 2,
320-
16496 => 3,
321-
28589 => 1,
322-
27811 => 1,
323-
30234 => 1,
324-
];
325-
326-
$this->assertSame($expectedHisto, $method->invoke(null, $pixels));
327-
}
328-
329309
public function testVboxFromPixels()
330310
{
331311
$method = new \ReflectionMethod('\ColorThief\ColorThief', 'vboxFromHistogram');

0 commit comments

Comments
 (0)