1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Imagecow\Crops; |
4
|
|
|
|
5
|
|
|
use Imagick; |
6
|
|
|
use Imagecow\Utils\Color; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* This class is adapted from Stig Lindqvist's great Crop library: |
10
|
|
|
* https://github.com/stojg/crop |
11
|
|
|
* Copyright (c) 2013, Stig Lindqvist. |
12
|
|
|
* |
13
|
|
|
* CropEntropy |
14
|
|
|
* |
15
|
|
|
* This class finds the a position in the picture with the most energy in it. |
16
|
|
|
* |
17
|
|
|
* Energy is in this case calculated by this |
18
|
|
|
* |
19
|
|
|
* 1. Take the image and turn it into black and white |
20
|
|
|
* 2. Run a edge filter so that we're left with only edges. |
21
|
|
|
* 3. Find a piece in the picture that has the highest entropy (i.e. most edges) |
22
|
|
|
* 4. Return coordinates that makes sure that this piece of the picture is not cropped 'away' |
23
|
|
|
*/ |
24
|
|
|
class Entropy implements CropInterface |
25
|
|
|
{ |
26
|
|
|
const POTENTIAL_RATIO = 1.5; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* {@inheritdoc} |
30
|
|
|
*/ |
31
|
|
View Code Duplication |
public static function getOffsets(Imagick $original, $targetWidth, $targetHeight) |
32
|
|
|
{ |
33
|
|
|
$measureImage = clone $original; |
34
|
|
|
// Enhance edges |
35
|
|
|
$measureImage->edgeimage(1); |
36
|
|
|
// Turn image into a grayscale |
37
|
|
|
$measureImage->modulateImage(100, 0, 100); |
38
|
|
|
// Turn everything darker than this to pitch black |
39
|
|
|
$measureImage->blackThresholdImage('#070707'); |
40
|
|
|
// Get the calculated offset for cropping |
41
|
|
|
return static::getOffsetFromEntropy($measureImage, $targetWidth, $targetHeight); |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Get the offset of where the crop should start. |
46
|
|
|
* |
47
|
|
|
* @param Imagick $originalImage |
48
|
|
|
* @param int $targetHeight |
49
|
|
|
* @param int $targetHeight |
50
|
|
|
* |
51
|
|
|
* @return array |
52
|
|
|
*/ |
53
|
|
|
protected static function getOffsetFromEntropy(Imagick $originalImage, $targetWidth, $targetHeight) |
54
|
|
|
{ |
55
|
|
|
// The entropy works better on a blured image |
56
|
|
|
$image = clone $originalImage; |
57
|
|
|
$image->blurImage(3, 2); |
58
|
|
|
|
59
|
|
|
$size = $image->getImageGeometry(); |
60
|
|
|
|
61
|
|
|
$originalWidth = $size['width']; |
62
|
|
|
$originalHeight = $size['height']; |
63
|
|
|
|
64
|
|
|
$leftX = static::slice($image, $originalWidth, $targetWidth, 'h'); |
65
|
|
|
$topY = static::slice($image, $originalHeight, $targetHeight, 'v'); |
66
|
|
|
|
67
|
|
|
return [$leftX, $topY]; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* slice. |
72
|
|
|
* |
73
|
|
|
* @param mixed $image |
74
|
|
|
* @param mixed $originalSize |
75
|
|
|
* @param mixed $targetSize |
76
|
|
|
* @param mixed $axis h=horizontal, v = vertical |
77
|
|
|
*/ |
78
|
|
|
protected static function slice($image, $originalSize, $targetSize, $axis) |
79
|
|
|
{ |
80
|
|
|
$aSlice = null; |
81
|
|
|
$bSlice = null; |
82
|
|
|
|
83
|
|
|
// Just an arbitrary size of slice size |
84
|
|
|
$sliceSize = ceil(($originalSize - $targetSize) / 25); |
85
|
|
|
|
86
|
|
|
$aBottom = $originalSize; |
87
|
|
|
$aTop = 0; |
88
|
|
|
|
89
|
|
|
// while there still are uninvestigated slices of the image |
90
|
|
|
while (($aBottom - $aTop) > $targetSize) { |
91
|
|
|
// Make sure that we don't try to slice outside the picture |
92
|
|
|
$sliceSize = min($aBottom - $aTop - $targetSize, $sliceSize); |
93
|
|
|
|
94
|
|
|
// Make a top slice image |
95
|
|
View Code Duplication |
if (!$aSlice) { |
|
|
|
|
96
|
|
|
$aSlice = clone $image; |
97
|
|
|
|
98
|
|
|
if ($axis === 'h') { |
99
|
|
|
$aSlice->cropImage($sliceSize, $originalSize, $aTop, 0); |
100
|
|
|
} else { |
101
|
|
|
$aSlice->cropImage($originalSize, $sliceSize, 0, $aTop); |
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
// Make a bottom slice image |
106
|
|
View Code Duplication |
if (!$bSlice) { |
|
|
|
|
107
|
|
|
$bSlice = clone $image; |
108
|
|
|
|
109
|
|
|
if ($axis === 'h') { |
110
|
|
|
$bSlice->cropImage($sliceSize, $originalSize, $aBottom - $sliceSize, 0); |
111
|
|
|
} else { |
112
|
|
|
$bSlice->cropImage($originalSize, $sliceSize, 0, $aBottom - $sliceSize); |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
// calculate slices potential |
117
|
|
|
$aPosition = (($axis === 'h') ? 'left' : 'top'); |
118
|
|
|
$bPosition = (($axis === 'h') ? 'right' : 'bottom'); |
119
|
|
|
|
120
|
|
|
$aPot = static::getPotential($aPosition, $aTop, $sliceSize); |
121
|
|
|
$bPot = static::getPotential($bPosition, $aBottom, $sliceSize); |
122
|
|
|
|
123
|
|
|
$canCutA = ($aPot <= 0); |
124
|
|
|
$canCutB = ($bPot <= 0); |
125
|
|
|
|
126
|
|
|
// if no slices are "cutable", we force if a slice has a lot of potential |
127
|
|
|
if (!$canCutA && !$canCutB) { |
128
|
|
|
if (($aPot * self::POTENTIAL_RATIO) < $bPot) { |
129
|
|
|
$canCutA = true; |
130
|
|
|
} elseif ($aPot > ($bPot * self::POTENTIAL_RATIO)) { |
131
|
|
|
$canCutB = true; |
132
|
|
|
} |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
// if we can only cut on one side |
136
|
|
|
if ($canCutA xor $canCutB) { |
137
|
|
|
if ($canCutA) { |
138
|
|
|
$aTop += $sliceSize; |
139
|
|
|
$aSlice = null; |
140
|
|
|
} else { |
141
|
|
|
$aBottom -= $sliceSize; |
142
|
|
|
$bSlice = null; |
143
|
|
|
} |
144
|
|
|
} elseif (static::grayscaleEntropy($aSlice) < static::grayscaleEntropy($bSlice)) { |
145
|
|
|
// bSlice has more entropy, so remove aSlice and bump aTop down |
146
|
|
|
$aTop += $sliceSize; |
147
|
|
|
$aSlice = null; |
148
|
|
|
} else { |
149
|
|
|
$aBottom -= $sliceSize; |
150
|
|
|
$bSlice = null; |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
return $aTop; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* getSafeZoneList. |
159
|
|
|
* |
160
|
|
|
* @return array |
161
|
|
|
*/ |
162
|
|
|
protected static function getSafeZoneList() |
163
|
|
|
{ |
164
|
|
|
return []; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* getPotential. |
169
|
|
|
* |
170
|
|
|
* @param mixed $position |
171
|
|
|
* @param mixed $top |
172
|
|
|
* @param mixed $sliceSize |
173
|
|
|
*/ |
174
|
|
|
protected static function getPotential($position, $top, $sliceSize) |
175
|
|
|
{ |
176
|
|
|
if (($position === 'top') || ($position === 'left')) { |
177
|
|
|
$start = $top; |
178
|
|
|
$end = $top + $sliceSize; |
179
|
|
|
} else { |
180
|
|
|
$start = $top - $sliceSize; |
181
|
|
|
$end = $top; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
$safeZoneList = static::getSafeZoneList(); |
185
|
|
|
$safeRatio = 0; |
186
|
|
|
|
187
|
|
|
for ($i = $start; $i < $end; ++$i) { |
188
|
|
|
foreach ($safeZoneList as $safeZone) { |
189
|
|
|
if (($position === 'top') || ($position === 'bottom')) { |
190
|
|
View Code Duplication |
if (($safeZone['top'] <= $i) && ($safeZone['bottom'] >= $i)) { |
|
|
|
|
191
|
|
|
$safeRatio = max($safeRatio, ($safeZone['right'] - $safeZone['left'])); |
192
|
|
|
} |
193
|
|
View Code Duplication |
} elseif (($safeZone['left'] <= $i) && ($safeZone['right'] >= $i)) { |
|
|
|
|
194
|
|
|
$safeRatio = max($safeRatio, ($safeZone['bottom'] - $safeZone['top'])); |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
return $safeRatio; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* Calculate the entropy for this image. |
204
|
|
|
* |
205
|
|
|
* A higher value of entropy means more noise / liveliness / color / business |
206
|
|
|
* |
207
|
|
|
* @param Imagick $image |
208
|
|
|
* |
209
|
|
|
* @return float |
210
|
|
|
* |
211
|
|
|
* @see http://brainacle.com/calculating-image-entropy-with-python-how-and-why.html |
212
|
|
|
* @see http://www.mathworks.com/help/toolbox/images/ref/entropy.html |
213
|
|
|
*/ |
214
|
|
|
protected static function grayscaleEntropy(Imagick $image) |
215
|
|
|
{ |
216
|
|
|
// The histogram consists of a list of 0-254 and the number of pixels that has that value |
217
|
|
|
return static::getEntropy($image->getImageHistogram(), static::area($image)); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Find out the entropy for a color image. |
222
|
|
|
* |
223
|
|
|
* If the source image is in color we need to transform RGB into a grayscale image |
224
|
|
|
* so we can calculate the entropy more performant. |
225
|
|
|
* |
226
|
|
|
* @param Imagick $image |
227
|
|
|
* |
228
|
|
|
* @return float |
229
|
|
|
*/ |
230
|
|
|
protected static function colorEntropy(Imagick $image) |
231
|
|
|
{ |
232
|
|
|
$histogram = $image->getImageHistogram(); |
233
|
|
|
$newHistogram = []; |
234
|
|
|
|
235
|
|
|
// Translates a color histogram into a bw histogram |
236
|
|
|
$colors = count($histogram); |
237
|
|
|
|
238
|
|
|
for ($idx = 0; $idx < $colors; ++$idx) { |
239
|
|
|
$colors = $histogram[$idx]->getColor(); |
240
|
|
|
$grey = Color::rgb2bw($colors['r'], $colors['g'], $colors['b']); |
241
|
|
|
|
242
|
|
|
if (isset($newHistogram[$grey])) { |
243
|
|
|
$newHistogram[$grey] += $histogram[$idx]->getColorCount(); |
244
|
|
|
} else { |
245
|
|
|
$newHistogram[$grey] = $histogram[$idx]->getColorCount(); |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
return static::getEntropy($newHistogram, static::area($image)); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* @param array $histogram - a value[count] array |
254
|
|
|
* @param int $area |
255
|
|
|
* |
256
|
|
|
* @return float |
257
|
|
|
*/ |
258
|
|
|
protected static function getEntropy($histogram, $area) |
259
|
|
|
{ |
260
|
|
|
$value = 0.0; |
261
|
|
|
$colors = count($histogram); |
262
|
|
|
|
263
|
|
|
for ($idx = 0; $idx < $colors; ++$idx) { |
264
|
|
|
// calculates the percentage of pixels having this color value |
265
|
|
|
$p = $histogram[$idx]->getColorCount() / $area; |
266
|
|
|
// A common way of representing entropy in scalar |
267
|
|
|
$value = $value + $p * log($p, 2); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
// $value is always 0.0 or negative, so transform into positive scalar value |
271
|
|
|
return -$value; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Get the area in pixels for this image. |
276
|
|
|
* |
277
|
|
|
* @param Imagick $image |
278
|
|
|
* |
279
|
|
|
* @return int |
280
|
|
|
*/ |
281
|
|
|
protected static function area(Imagick $image) |
282
|
|
|
{ |
283
|
|
|
$size = $image->getImageGeometry(); |
284
|
|
|
|
285
|
|
|
return $size['height'] * $size['width']; |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.