Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(105)

Side by Side Diff: ui/gfx/color_analysis.cc

Issue 2690513002: Port Android palette API for deriving a prominent color from an image. (Closed)
Patch Set: git cl try Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "ui/gfx/color_analysis.h" 5 #include "ui/gfx/color_analysis.h"
6 6
7 #include <limits.h> 7 #include <limits.h>
8 #include <stdint.h> 8 #include <stdint.h>
9 9
10 #include <algorithm> 10 #include <algorithm>
11 #include <cmath>
11 #include <limits> 12 #include <limits>
13 #include <map>
12 #include <memory> 14 #include <memory>
15 #include <queue>
13 #include <vector> 16 #include <vector>
14 17
15 #include "base/logging.h" 18 #include "base/logging.h"
16 #include "third_party/skia/include/core/SkBitmap.h" 19 #include "third_party/skia/include/core/SkBitmap.h"
17 #include "third_party/skia/include/core/SkUnPreMultiply.h" 20 #include "third_party/skia/include/core/SkUnPreMultiply.h"
18 #include "ui/gfx/codec/png_codec.h" 21 #include "ui/gfx/codec/png_codec.h"
22 #include "ui/gfx/color_palette.h"
19 #include "ui/gfx/color_utils.h" 23 #include "ui/gfx/color_utils.h"
24 #include "ui/gfx/range/range.h"
20 25
21 namespace color_utils { 26 namespace color_utils {
22 namespace { 27 namespace {
23 28
24 // RGBA KMean Constants 29 // RGBA KMean Constants
25 const uint32_t kNumberOfClusters = 4; 30 const uint32_t kNumberOfClusters = 4;
26 const int kNumberOfIterations = 50; 31 const int kNumberOfIterations = 50;
27 32
28 const HSL kDefaultLowerHSLBound = {-1, -1, 0.15}; 33 const HSL kDefaultLowerHSLBound = {-1, -1, 0.15};
29 const HSL kDefaultUpperHSLBound = {-1, -1, 0.85}; 34 const HSL kDefaultUpperHSLBound = {-1, -1, 0.85};
(...skipping 106 matching lines...) Expand 10 before | Expand all | Expand 10 after
136 // approximately 10 microseconds for a 16x16 icon on an Intel Core i5. 141 // approximately 10 microseconds for a 16x16 icon on an Intel Core i5.
137 void UnPreMultiply(const SkBitmap& bitmap, uint32_t* buffer, int buffer_size) { 142 void UnPreMultiply(const SkBitmap& bitmap, uint32_t* buffer, int buffer_size) {
138 SkAutoLockPixels auto_lock(bitmap); 143 SkAutoLockPixels auto_lock(bitmap);
139 uint32_t* in = static_cast<uint32_t*>(bitmap.getPixels()); 144 uint32_t* in = static_cast<uint32_t*>(bitmap.getPixels());
140 uint32_t* out = buffer; 145 uint32_t* out = buffer;
141 int pixel_count = std::min(bitmap.width() * bitmap.height(), buffer_size); 146 int pixel_count = std::min(bitmap.width() * bitmap.height(), buffer_size);
142 for (int i = 0; i < pixel_count; ++i) 147 for (int i = 0; i < pixel_count; ++i)
143 *out++ = SkUnPreMultiply::PMColorToColor(*in++); 148 *out++ = SkUnPreMultiply::PMColorToColor(*in++);
144 } 149 }
145 150
151 // Prominent color utilities ---------------------------------------------------
Nico 2017/02/17 21:37:05 kind of looks long enough that it could go into it
Evan Stade 2017/02/17 23:23:18 I like having all the moving parts to this algorit
152
153 // A color value with an associated weight (equivalent to the popularity of that
154 // color).
155 struct WeightedColor {
156 SkColor color;
157 int weight;
sky 2017/02/17 21:42:19 Initialize color and weight?
sky 2017/02/17 21:42:19 Document what the range for this is. Always positi
Evan Stade 2017/02/17 23:23:18 done
Evan Stade 2017/02/17 23:23:18 changed it to use an explicit constructor
158 };
159
160 // A |ColorBox| represents a region in a color space (an ordered set of colors).
sky 2017/02/17 21:42:19 Did you consider the name OrderedColorSet? IMO tha
Evan Stade 2017/02/17 23:23:18 no, I didn't consider that name. I drew the names
sky 2017/02/18 00:01:34 I actually think 'box' is misleading in this conte
161 // It is a range in the ordered set, with a low index and a high index. The
162 // diversity (volume) of the box is computed by looking at the range of color
163 // values it spans, where r, g, and b components are considered separately.
164 class ColorBox {
165 public:
166 explicit ColorBox(std::vector<SkColor>* color_space)
167 : ColorBox(color_space, gfx::Range(0, color_space->size() - 1)) {}
sky 2017/02/17 21:42:20 DCHECK color_space !empty. That said, CanSplit() s
Evan Stade 2017/02/17 23:23:18 An empty color_space would trip the second DCHECK
168 ColorBox(const ColorBox& other) = default;
169 ColorBox& operator=(const ColorBox& other) = default;
170 ~ColorBox() {}
171
172 // Can't split if there's only one color in the box.
173 bool CanSplit() const { return !color_range_.is_empty(); }
174
175 // Splits |this| in two and returns the other half.
176 ColorBox Split() {
sky 2017/02/17 21:42:19 This also sorts. Maybe SortAndSplit?
Evan Stade 2017/02/17 23:26:26 oops, missed this one. It sorts in an unpredictabl
177 // Calculate which component has the largest range...
178 int r_dimension = max_r_ - min_r_;
sky 2017/02/17 21:42:19 As you have min/max as uint8_t, why did you use in
Evan Stade 2017/02/17 23:23:18 updated these all to uint8_t. int8_t could overflo
179 int g_dimension = max_g_ - min_g_;
180 int b_dimension = max_b_ - min_b_;
181 int long_dimension = std::max({r_dimension, g_dimension, b_dimension});
182 enum ColorChannel {
183 RED,
184 GREEN,
185 BLUE,
186 };
Nico 2017/02/17 21:37:05 optional nit: Since you don't use the name, I'd do
Evan Stade 2017/02/17 23:23:18 I like it, done.
187 ColorChannel channel = long_dimension == r_dimension
188 ? RED
189 : long_dimension == g_dimension ? GREEN : BLUE;
190
191 // ... and sort along that axis.
192 auto sort_function = [channel](SkColor a, SkColor b) {
193 switch (channel) {
194 case RED:
195 return SkColorGetR(a) < SkColorGetR(b);
196 case GREEN:
197 return SkColorGetG(a) < SkColorGetG(b);
198 case BLUE:
199 return SkColorGetB(a) < SkColorGetB(b);
200 }
201 NOTREACHED();
202 return SkColorGetB(a) < SkColorGetB(b);
203 };
204 // Just the portion of |color_space_| that's covered by this box should be
205 // sorted.
206 std::sort(color_space_->begin() + color_range_.start(),
207 color_space_->begin() + color_range_.end(), sort_function);
208
209 // Split at the first color value that's not less than the median.
Nico 2017/02/17 21:37:06 You mean "mean" not "median" here, right? Else you
Evan Stade 2017/02/17 23:23:18 hmm, I copied this terminology without thinking mu
210 uint32_t split_point = color_range_.end();
211 for (uint32_t i = color_range_.start() + 1; i < color_range_.end(); ++i) {
212 bool past_median = false;
213 switch (channel) {
214 case RED:
215 past_median =
216 SkColorGetR(color_space_->at(i)) > (min_r_ + max_r_) / 2;
sky 2017/02/17 21:42:19 optional: at -> (*color_space_)[i]
Evan Stade 2017/02/17 23:23:18 Done.
217 break;
218 case GREEN:
219 past_median =
220 SkColorGetG(color_space_->at(i)) > (min_g_ + max_g_) / 2;
221 break;
222 case BLUE:
223 past_median =
224 SkColorGetB(color_space_->at(i)) > (min_b_ + max_b_) / 2;
225 break;
226 }
227 if (past_median) {
228 split_point = i;
229 break;
230 }
231 }
232
233 // Break off half and return it.
234 gfx::Range other_range = color_range_;
sky 2017/02/17 21:42:19 Does this do the right thing with 1 color? If ther
Evan Stade 2017/02/17 23:23:18 it's an error to call this function in that case (
235 other_range.set_end(split_point - 1);
236 ColorBox other_box(color_space_, other_range);
237
238 // Keep the other half in |this| and recalculate our color bounds.
239 color_range_.set_start(split_point);
240 RecomputeBounds();
241 return other_box;
242 }
243
244 // Returns the average color of this box, weighted by its popularity in
245 // |color_counts|.
246 WeightedColor GetWeightedAverageColor(
247 const std::map<SkColor, int>& color_counts) const {
248 int sum_r = 0;
Nico 2017/02/17 21:37:05 For a 4096x4096 all-red image I think this will ov
Evan Stade 2017/02/17 23:23:18 that's bad. I've changed this to uint64_t. I do e
249 int sum_g = 0;
250 int sum_b = 0;
251 int total_count_in_box = 0;
252
253 for (uint32_t i = color_range_.start(); i <= color_range_.end(); ++i) {
254 const SkColor color = color_space_->at(i);
sky 2017/02/17 21:42:19 If you're going to use const (which I like) use it
Evan Stade 2017/02/17 23:23:18 Done.
255 auto color_count_iter = color_counts.find(color);
256 DCHECK(color_count_iter != color_counts.end());
257 const int color_count = color_count_iter->second;
258
259 total_count_in_box += color_count;
260 sum_r += color_count * SkColorGetR(color);
261 sum_g += color_count * SkColorGetG(color);
262 sum_b += color_count * SkColorGetB(color);
263 }
264
265 WeightedColor weighted_color;
266 weighted_color.weight = total_count_in_box;
267 weighted_color.color = SkColorSetRGB(
268 std::round(static_cast<float>(sum_r) / total_count_in_box),
269 std::round(static_cast<float>(sum_g) / total_count_in_box),
270 std::round(static_cast<float>(sum_b) / total_count_in_box));
271 return weighted_color;
272 }
273
274 // Comparisons are done by volume.
sky 2017/02/17 21:42:19 Using operator < to mean compare by volume is obsc
Evan Stade 2017/02/17 23:23:18 done. (If there's a more elegant way to do this, p
275 bool operator<(const ColorBox& other) const {
276 return volume_ < other.volume_;
277 }
278
279 private:
280 ColorBox(std::vector<SkColor>* color_space, const gfx::Range& color_range)
281 : color_space_(color_space), color_range_(color_range) {
282 RecomputeBounds();
283 }
284
285 void RecomputeBounds() {
sky 2017/02/17 21:42:19 optional: There are no bounds in here, and the nam
Evan Stade 2017/02/17 23:23:18 min_* and max_* describe upper and lower bounds fo
286 DCHECK(!color_range_.is_reversed());
287 DCHECK_LT(color_range_.end(), color_space_->size());
288
289 min_r_ = 0xFF;
290 min_g_ = 0xFF;
291 min_b_ = 0xFF;
292 max_r_ = 0;
293 max_g_ = 0;
294 max_b_ = 0;
295
296 for (uint32_t i = color_range_.start(); i < color_range_.end(); ++i) {
297 SkColor color = color_space_->at(i);
298 min_r_ = std::min<uint8_t>(SkColorGetR(color), min_r_);
299 min_g_ = std::min<uint8_t>(SkColorGetG(color), min_g_);
300 min_b_ = std::min<uint8_t>(SkColorGetB(color), min_b_);
301 max_r_ = std::max<uint8_t>(SkColorGetR(color), max_r_);
302 max_g_ = std::max<uint8_t>(SkColorGetG(color), max_g_);
303 max_b_ = std::max<uint8_t>(SkColorGetB(color), max_b_);
304 }
305
306 volume_ =
307 (max_r_ - min_r_ + 1) * (max_g_ - min_g_ + 1) * (max_b_ - min_b_ + 1);
308 }
309
310 // The set of colors of which this box captures a subset. This vector is not
311 // owned but may be modified during the split operation.
312 std::vector<SkColor>* color_space_;
313
314 // The range of indexes into |color_space_| that are part of this box.
sky 2017/02/17 21:42:19 Why did you make this inclusive vs exclusive? Incl
Evan Stade 2017/02/17 23:23:18 changed. In fact I think there was a bug in the st
315 gfx::Range color_range_;
316
317 // Cached min and max color component values for the colors in this box.
318 uint8_t min_r_ = 0;
319 uint8_t min_g_ = 0;
320 uint8_t min_b_ = 0;
321 uint8_t max_r_ = 0;
322 uint8_t max_g_ = 0;
323 uint8_t max_b_ = 0;
324
325 // Cached volume value, which is the product of the range of each color
326 // component.
327 int volume_ = 0;
328 };
329
330 // Some color values should be ignored for the purposes of determining prominent
331 // colors.
332 bool IsInterestingColor(SkColor color) {
333 float average_channel_value =
334 (SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color)) / 3.0f;
335 // If a color is too close to white or black, ignore it.
336 if (average_channel_value >= 237 || average_channel_value <= 22)
337 return false;
338
339 // Also rule out skin tones.
340 HSL hsl;
341 SkColorToHSL(color, &hsl);
342 return !(hsl.h >= 0.028f && hsl.h <= 0.10f && hsl.s <= 0.82f);
343 }
344
345 // This algorithm is a port of Android's Palette API. Compare to package
346 // android.support.v7.graphics and see that code for additional high-level
347 // explanation of this algorithm. There are some minor differences:
348 // * This code doesn't exclude the same color from being used for
349 // different color profiles.
350 // * This code doesn't try to heuristically derive missing colors from
351 // existing colors.
352 SkColor CalculateProminentColorOfBuffer(uint8_t* decoded_data,
353 int img_width,
354 int img_height,
355 const HSL& lower_bound,
356 const HSL& upper_bound,
357 const HSL& goal) {
358 DCHECK_GT(img_width, 0);
359 DCHECK_GT(img_height, 0);
360 std::map<SkColor, int> color_counts;
361
362 // First extract all colors.
363 for (int i = 0; i < img_width * img_height; ++i) {
364 // TODO(port): This code assumes the CPU architecture is little-endian.
sky 2017/02/17 21:42:19 Can you DCHECK on the image type? That it's 32bit?
Evan Stade 2017/02/17 23:23:18 I was kind of just blindly copying this bit. I've
365 uint8_t b = decoded_data[i * 4];
366 uint8_t g = decoded_data[i * 4 + 1];
367 uint8_t r = decoded_data[i * 4 + 2];
368 uint8_t a = decoded_data[i * 4 + 3];
369 if (a == SK_AlphaTRANSPARENT)
370 continue;
371
372 SkColor pixel = SkColorSetRGB(r, g, b);
373 color_counts[pixel]++;
374 }
375
376 // Now throw out some uninteresting colors.
377 std::vector<SkColor> interesting_colors;
378 for (auto color_count : color_counts) {
379 SkColor color = color_count.first;
380 if (IsInterestingColor(color))
381 interesting_colors.push_back(color);
382 }
383
384 if (interesting_colors.empty())
sky 2017/02/17 21:42:19 If there is only one color, is it the prominent co
Evan Stade 2017/02/17 23:23:18 it could be, but only if it's "interesting" and wi
385 return SK_ColorTRANSPARENT;
386
387 // Group the colors into "boxes" and repeatedly split the most voluminous box.
388 // We stop the process when a box can no longer be split (there's only one
389 // color in it) or when the number of color boxes reaches 12. As per the
390 // Android docs,
391 //
392 // For landscapes, good values are in the range 12-16. For images which
393 // are largely made up of people's faces then this value should be increased
394 // to 24-32.
395 const int kMaxColors = 12;
396 // Boxes are sorted by volume with the most voluminous at the front of the PQ.
397 std::priority_queue<ColorBox> boxes;
398 boxes.emplace(&interesting_colors);
399 while (boxes.size() < kMaxColors) {
400 auto box = boxes.top();
401 if (!box.CanSplit())
402 break;
403 boxes.pop();
404 boxes.push(box.Split());
405 boxes.push(box);
406 }
407
408 // Now extract a single color to represent each box. This is the average color
409 // in the box, weighted by the frequency of that color in the source image.
410 std::vector<WeightedColor> box_colors;
411 int max_weight = 0;
412 while (!boxes.empty()) {
413 box_colors.push_back(boxes.top().GetWeightedAverageColor(color_counts));
414 boxes.pop();
415 max_weight = std::max(max_weight, box_colors.back().weight);
416 }
417
418 // Given these box average colors, find the best one for the desired color
419 // profile. "Best" in this case means the color which fits in the provided
420 // bounds and comes closest to |goal|. It's possible that no color will fit in
421 // the provided bounds, in which case we'll return an empty color.
422 double best_suitability = 0;
423 SkColor best_color = SK_ColorTRANSPARENT;
424 for (const auto& box_color : box_colors) {
425 HSL hsl;
426 SkColorToHSL(box_color.color, &hsl);
427 if (!IsWithinHSLRange(hsl, lower_bound, upper_bound))
428 continue;
429
430 double suitability =
431 (1 - std::abs(hsl.s - goal.s)) * 3 +
432 (1 - std::abs(hsl.l - goal.l)) * 6.5 +
433 (box_color.weight / static_cast<float>(max_weight)) * 0.5;
434 if (suitability > best_suitability) {
435 best_suitability = suitability;
436 best_color = box_color.color;
437 }
438 }
439
440 return best_color;
441 }
442
146 } // namespace 443 } // namespace
147 444
148 KMeanImageSampler::KMeanImageSampler() { 445 KMeanImageSampler::KMeanImageSampler() {
149 } 446 }
150 447
151 KMeanImageSampler::~KMeanImageSampler() { 448 KMeanImageSampler::~KMeanImageSampler() {
152 } 449 }
153 450
154 GridSampler::GridSampler() : calls_(0) { 451 GridSampler::GridSampler() : calls_(0) {
155 } 452 }
(...skipping 238 matching lines...) Expand 10 before | Expand all | Expand 10 after
394 const HSL& upper_bound, 691 const HSL& upper_bound,
395 KMeanImageSampler* sampler) { 692 KMeanImageSampler* sampler) {
396 // SkBitmap uses pre-multiplied alpha but the KMean clustering function 693 // SkBitmap uses pre-multiplied alpha but the KMean clustering function
397 // above uses non-pre-multiplied alpha. Transform the bitmap before we 694 // above uses non-pre-multiplied alpha. Transform the bitmap before we
398 // analyze it because the function reads each pixel multiple times. 695 // analyze it because the function reads each pixel multiple times.
399 int pixel_count = bitmap.width() * bitmap.height(); 696 int pixel_count = bitmap.width() * bitmap.height();
400 std::unique_ptr<uint32_t[]> image(new uint32_t[pixel_count]); 697 std::unique_ptr<uint32_t[]> image(new uint32_t[pixel_count]);
401 UnPreMultiply(bitmap, image.get(), pixel_count); 698 UnPreMultiply(bitmap, image.get(), pixel_count);
402 699
403 return CalculateKMeanColorOfBuffer(reinterpret_cast<uint8_t*>(image.get()), 700 return CalculateKMeanColorOfBuffer(reinterpret_cast<uint8_t*>(image.get()),
404 bitmap.width(), 701 bitmap.width(), bitmap.height(),
405 bitmap.height(), 702 lower_bound, upper_bound, sampler);
406 lower_bound,
407 upper_bound,
408 sampler);
409 } 703 }
410 704
411 SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap) { 705 SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap) {
412 GridSampler sampler; 706 GridSampler sampler;
413 return CalculateKMeanColorOfBitmap( 707 return CalculateKMeanColorOfBitmap(
414 bitmap, kDefaultLowerHSLBound, kDefaultUpperHSLBound, &sampler); 708 bitmap, kDefaultLowerHSLBound, kDefaultUpperHSLBound, &sampler);
415 } 709 }
416 710
711 SkColor CalculateProminentColorOfBitmap(const SkBitmap& bitmap,
712 LumaRange luma,
713 SaturationRange saturation) {
714 // SkBitmap uses pre-multiplied alpha but the prominent color algorithm
715 // above uses non-pre-multiplied alpha. Transform the bitmap before we
716 // analyze it because the function reads each pixel multiple times.
717 int pixel_count = bitmap.width() * bitmap.height();
718 if (pixel_count == 0)
719 return SK_ColorTRANSPARENT;
720
721 std::unique_ptr<uint32_t[]> image(new uint32_t[pixel_count]);
sky 2017/02/17 21:42:19 image_data? bitmap_data?
Evan Stade 2017/02/17 23:23:18 removed
722 UnPreMultiply(bitmap, image.get(), pixel_count);
723
724 // The hue is not relevant to our bounds or goal colors.
725 HSL lower_bound = {
726 -1,
727 };
728 HSL upper_bound = {
729 -1,
730 };
731 HSL goal = {
732 -1,
733 };
734
735 switch (luma) {
736 case LumaRange::LIGHT:
737 lower_bound.l = 0.55f;
738 upper_bound.l = 1;
739 goal.l = 0.74f;
740 break;
741 case LumaRange::NORMAL:
742 lower_bound.l = 0.3f;
743 upper_bound.l = 0.7f;
744 goal.l = 0.5f;
745 break;
746 case LumaRange::DARK:
747 lower_bound.l = 0;
748 upper_bound.l = 0.45f;
749 goal.l = 0.26f;
750 break;
751 }
752
753 switch (saturation) {
754 case SaturationRange::VIBRANT:
755 lower_bound.s = 0.35f;
756 upper_bound.s = 1;
757 goal.s = 1;
758 break;
759 case SaturationRange::MUTED:
760 lower_bound.s = 0;
761 upper_bound.s = 0.4f;
762 goal.s = 0.3f;
763 break;
764 }
765
766 return CalculateProminentColorOfBuffer(
767 reinterpret_cast<uint8_t*>(image.get()), bitmap.width(), bitmap.height(),
768 lower_bound, upper_bound, goal);
769 }
770
417 gfx::Matrix3F ComputeColorCovariance(const SkBitmap& bitmap) { 771 gfx::Matrix3F ComputeColorCovariance(const SkBitmap& bitmap) {
418 // First need basic stats to normalize each channel separately. 772 // First need basic stats to normalize each channel separately.
419 SkAutoLockPixels bitmap_lock(bitmap); 773 SkAutoLockPixels bitmap_lock(bitmap);
420 gfx::Matrix3F covariance = gfx::Matrix3F::Zeros(); 774 gfx::Matrix3F covariance = gfx::Matrix3F::Zeros();
421 if (!bitmap.getPixels()) 775 if (!bitmap.getPixels())
422 return covariance; 776 return covariance;
423 777
424 // Assume ARGB_8888 format. 778 // Assume ARGB_8888 format.
425 DCHECK(bitmap.colorType() == kN32_SkColorType); 779 DCHECK(bitmap.colorType() == kN32_SkColorType);
426 780
(...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after
576 gfx::Matrix3F covariance = ComputeColorCovariance(source_bitmap); 930 gfx::Matrix3F covariance = ComputeColorCovariance(source_bitmap);
577 gfx::Matrix3F eigenvectors = gfx::Matrix3F::Zeros(); 931 gfx::Matrix3F eigenvectors = gfx::Matrix3F::Zeros();
578 gfx::Vector3dF eigenvals = covariance.SolveEigenproblem(&eigenvectors); 932 gfx::Vector3dF eigenvals = covariance.SolveEigenproblem(&eigenvectors);
579 gfx::Vector3dF principal = eigenvectors.get_column(0); 933 gfx::Vector3dF principal = eigenvectors.get_column(0);
580 if (eigenvals == gfx::Vector3dF() || principal == gfx::Vector3dF()) 934 if (eigenvals == gfx::Vector3dF() || principal == gfx::Vector3dF())
581 return false; // This may happen for some edge cases. 935 return false; // This may happen for some edge cases.
582 return ApplyColorReduction(source_bitmap, principal, true, target_bitmap); 936 return ApplyColorReduction(source_bitmap, principal, true, target_bitmap);
583 } 937 }
584 938
585 } // color_utils 939 } // color_utils
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698