blob: 96c93a030a0d5be484d9c2a3a3902224a9acd03e [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/thumbnails/content_analysis.h"
#include <algorithm>
#include <cmath>
#include <deque>
#include <functional>
#include <limits>
#include <numeric>
#include <vector>
#include "base/logging.h"
#include "skia/ext/convolver.h"
#include "skia/ext/recursive_gaussian_convolution.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkSize.h"
#include "ui/gfx/color_analysis.h"
namespace {
const float kSigmaThresholdForRecursive = 1.5f;
const float kAspectRatioToleranceFactor = 1.02f;
template<class InputIterator, class OutputIterator, class Compare>
void SlidingWindowMinMax(InputIterator first,
InputIterator last,
OutputIterator output,
int window_size,
Compare cmp) {
typedef std::deque<
std::pair<typename std::iterator_traits<InputIterator>::value_type, int> >
deque_type;
deque_type slider;
int front_tail_length = window_size / 2;
int i = 0;
DCHECK_LT(front_tail_length, last - first);
// This min-max filter functions the way image filters do. The min/max we
// compute is placed in the center of the window. Thus, first we need to
// 'pre-load' the window with the slider with right-tail of the filter.
for (; first < last && i < front_tail_length; ++i, ++first)
slider.push_back(std::make_pair(*first, i));
for (; first < last; ++i, ++first, ++output) {
while (!slider.empty() && !cmp(slider.back().first, *first))
slider.pop_back();
slider.push_back(std::make_pair(*first, i));
while (slider.front().second <= i - window_size)
slider.pop_front();
*output = slider.front().first;
}
// Now at the tail-end we will simply need to use whatever value is left of
// the filter to compute the remaining front_tail_length taps in the output.
// If input shorter than window, remainder length needs to be adjusted.
front_tail_length = std::min(front_tail_length, i);
for (; front_tail_length >= 0; --front_tail_length, ++i) {
while (slider.front().second <= i - window_size)
slider.pop_front();
*output = slider.front().first;
}
}
size_t FindOtsuThresholdingIndex(const std::vector<int>& histogram) {
// Otsu's method seeks to maximize variance between two classes of pixels
// correspondng to valleys and peaks of the profile.
double w1 = histogram[0]; // Total weight of the first class.
double t1 = 0.5 * w1;
double w2 = 0.0;
double t2 = 0.0;
for (size_t i = 1; i < histogram.size(); ++i) {
w2 += histogram[i];
t2 += (0.5 + i) * histogram[i];
}
size_t max_index = 0;
double m1 = t1 / w1;
double m2 = t2 / w2;
double max_variance_score = w1 * w2 * (m1 - m2) * (m1 - m2);
// Iterate through all possible ways of splitting the histogram.
for (size_t i = 1; i < histogram.size() - 1; i++) {
double bin_volume = (0.5 + i) * histogram[i];
w1 += histogram[i];
w2 -= histogram[i];
t2 -= bin_volume;
t1 += bin_volume;
m1 = t1 / w1;
m2 = t2 / w2;
double variance_score = w1 * w2 * (m1 - m2) * (m1 - m2);
if (variance_score >= max_variance_score) {
max_variance_score = variance_score;
max_index = i;
}
}
return max_index;
}
bool ComputeScaledHistogram(const std::vector<float>& source,
std::vector<int>* histogram,
std::pair<float, float>* minmax) {
DCHECK(histogram);
DCHECK(minmax);
histogram->clear();
histogram->resize(256);
float value_min = std::numeric_limits<float>::max();
float value_max = 0.0f;
std::vector<float>::const_iterator it;
for (it = source.begin(); it < source.end(); ++it) {
value_min = std::min(value_min, *it);
value_max = std::max(value_max, *it);
}
*minmax = std::make_pair(value_min, value_max);
if (value_max - value_min <= std::numeric_limits<float>::epsilon() * 100.0f) {
// Scaling won't work and there is nothing really to segment anyway.
return false;
}
float value_span = value_max - value_min;
float scale = 255.0f / value_span;
for (it = source.begin(); it < source.end(); ++it) {
float scaled_value = (*it - value_min) * scale;
(*histogram)[static_cast<int>(scaled_value)] += 1;
}
return true;
}
void ConstrainedProfileThresholding(const std::vector<float>& profile,
const std::vector<int>& histogram,
int current_clip_index,
float current_threshold,
const std::pair<float, float>& range,
int size_for_threshold,
int target_size,
std::vector<bool>* result) {
DCHECK(!profile.empty());
DCHECK_EQ(histogram.size(), 256U);
DCHECK(result);
// A subroutine performing thresholding on the |profile|.
if (size_for_threshold != target_size) {
// Find a cut-off point (on the histogram) closest to the desired size.
int candidate_size = profile.size();
int candidate_clip_index = 0;
for (std::vector<int>::const_iterator it = histogram.begin();
it != histogram.end(); ++it, ++candidate_clip_index) {
if (std::abs(candidate_size - target_size) <
std::abs(candidate_size - *it - target_size)) {
break;
}
candidate_size -= *it;
}
if (std::abs(candidate_size - target_size) <
std::abs(candidate_size -size_for_threshold)) {
current_clip_index = candidate_clip_index;
current_threshold = (range.second - range.first) *
current_clip_index / 255.0f + range.first;
// Recount, rather than assume. One-offs due to rounding can be very
// harmful when eroding / dilating the result.
size_for_threshold = std::count_if(
profile.begin(), profile.end(),
std::bind2nd(std::greater<float>(), current_threshold));
}
}
result->resize(profile.size());
for (size_t i = 0; i < profile.size(); ++i)
(*result)[i] = profile[i] > current_threshold;
while (size_for_threshold > target_size) {
// If the current size is larger than target size, erode exactly as needed.
std::vector<bool>::iterator mod_it = result->begin();
std::vector<bool>::const_iterator lead_it = result->begin();
bool prev_value = true;
for (++lead_it;
lead_it < result->end() && size_for_threshold > target_size;
++lead_it, ++mod_it) {
bool value = *mod_it;
// If any neighbour is false, switch the element off.
if (!prev_value || !*lead_it) {
*mod_it = false;
--size_for_threshold;
}
prev_value = value;
}
if (lead_it == result->end() && !prev_value) {
*mod_it = false;
--size_for_threshold;
}
}
while (size_for_threshold < target_size) {
std::vector<bool>::iterator mod_it = result->begin();
std::vector<bool>::const_iterator lead_it = result->begin();
bool prev_value = false;
for (++lead_it;
lead_it < result->end() && size_for_threshold < target_size;
++lead_it, ++mod_it) {
bool value = *mod_it;
// If any neighbour is false, switch the element off.
if (!prev_value || !*lead_it) {
*mod_it = true;
++size_for_threshold;
}
prev_value = value;
}
if (lead_it == result->end() && !prev_value) {
*mod_it = true;
++size_for_threshold;
}
}
}
} // namespace
namespace thumbnailing_utils {
void ApplyGaussianGradientMagnitudeFilter(SkBitmap* input_bitmap,
float kernel_sigma) {
// The purpose of this function is to highlight salient
// (attention-attracting?) features of the image for use in image
// retargeting.
SkAutoLockPixels source_lock(*input_bitmap);
DCHECK(input_bitmap);
DCHECK(input_bitmap->getPixels());
DCHECK_EQ(kAlpha_8_SkColorType, input_bitmap->colorType());
// To perform computations we will need one intermediate buffer. It can
// very well be just another bitmap.
const SkISize image_size = SkISize::Make(input_bitmap->width(),
input_bitmap->height());
SkBitmap intermediate;
intermediate.allocPixels(input_bitmap->info().makeWH(image_size.width(),
image_size.height()));
SkBitmap intermediate2;
intermediate2.allocPixels(input_bitmap->info().makeWH(image_size.width(),
image_size.height()));
if (kernel_sigma <= kSigmaThresholdForRecursive) {
// For small kernels classic implementation is faster.
skia::ConvolutionFilter1D smoothing_filter;
skia::SetUpGaussianConvolutionKernel(
&smoothing_filter, kernel_sigma, false);
skia::SingleChannelConvolveX1D(
input_bitmap->getAddr8(0, 0),
static_cast<int>(input_bitmap->rowBytes()),
0, input_bitmap->bytesPerPixel(),
smoothing_filter,
image_size,
intermediate.getAddr8(0, 0),
static_cast<int>(intermediate.rowBytes()),
0, intermediate.bytesPerPixel(), false);
skia::SingleChannelConvolveY1D(
intermediate.getAddr8(0, 0),
static_cast<int>(intermediate.rowBytes()),
0, intermediate.bytesPerPixel(),
smoothing_filter,
image_size,
input_bitmap->getAddr8(0, 0),
static_cast<int>(input_bitmap->rowBytes()),
0, input_bitmap->bytesPerPixel(), false);
skia::ConvolutionFilter1D gradient_filter;
skia::SetUpGaussianConvolutionKernel(&gradient_filter, kernel_sigma, true);
skia::SingleChannelConvolveX1D(
input_bitmap->getAddr8(0, 0),
static_cast<int>(input_bitmap->rowBytes()),
0, input_bitmap->bytesPerPixel(),
gradient_filter,
image_size,
intermediate.getAddr8(0, 0),
static_cast<int>(intermediate.rowBytes()),
0, intermediate.bytesPerPixel(), true);
skia::SingleChannelConvolveY1D(
input_bitmap->getAddr8(0, 0),
static_cast<int>(input_bitmap->rowBytes()),
0, input_bitmap->bytesPerPixel(),
gradient_filter,
image_size,
intermediate2.getAddr8(0, 0),
static_cast<int>(intermediate2.rowBytes()),
0, intermediate2.bytesPerPixel(), true);
} else {
// For larger sigma values use the recursive filter.
skia::RecursiveFilter smoothing_filter(kernel_sigma,
skia::RecursiveFilter::FUNCTION);
skia::SingleChannelRecursiveGaussianX(
input_bitmap->getAddr8(0, 0),
static_cast<int>(input_bitmap->rowBytes()),
0, input_bitmap->bytesPerPixel(),
smoothing_filter,
image_size,
intermediate.getAddr8(0, 0),
static_cast<int>(intermediate.rowBytes()),
0, intermediate.bytesPerPixel(), false);
unsigned char smoothed_max = skia::SingleChannelRecursiveGaussianY(
intermediate.getAddr8(0, 0),
static_cast<int>(intermediate.rowBytes()),
0, intermediate.bytesPerPixel(),
smoothing_filter,
image_size,
input_bitmap->getAddr8(0, 0),
static_cast<int>(input_bitmap->rowBytes()),
0, input_bitmap->bytesPerPixel(), false);
if (smoothed_max < 127) {
int bit_shift = 8 - static_cast<int>(
std::log10(static_cast<float>(smoothed_max)) / std::log10(2.0f));
for (int r = 0; r < image_size.height(); ++r) {
uint8* row = input_bitmap->getAddr8(0, r);
for (int c = 0; c < image_size.width(); ++c, ++row) {
*row <<= bit_shift;
}
}
}
skia::RecursiveFilter gradient_filter(
kernel_sigma, skia::RecursiveFilter::FIRST_DERIVATIVE);
skia::SingleChannelRecursiveGaussianX(
input_bitmap->getAddr8(0, 0),
static_cast<int>(input_bitmap->rowBytes()),
0, input_bitmap->bytesPerPixel(),
gradient_filter,
image_size,
intermediate.getAddr8(0, 0),
static_cast<int>(intermediate.rowBytes()),
0, intermediate.bytesPerPixel(), true);
skia::SingleChannelRecursiveGaussianY(
input_bitmap->getAddr8(0, 0),
static_cast<int>(input_bitmap->rowBytes()),
0, input_bitmap->bytesPerPixel(),
gradient_filter,
image_size,
intermediate2.getAddr8(0, 0),
static_cast<int>(intermediate2.rowBytes()),
0, intermediate2.bytesPerPixel(), true);
}
unsigned grad_max = 0;
for (int r = 0; r < image_size.height(); ++r) {
const uint8* grad_x_row = intermediate.getAddr8(0, r);
const uint8* grad_y_row = intermediate2.getAddr8(0, r);
for (int c = 0; c < image_size.width(); ++c) {
unsigned grad_x = grad_x_row[c];
unsigned grad_y = grad_y_row[c];
grad_max = std::max(grad_max, grad_x * grad_x + grad_y * grad_y);
}
}
int bit_shift = 0;
if (grad_max > 255)
bit_shift = static_cast<int>(
std::log10(static_cast<float>(grad_max)) / std::log10(2.0f)) - 7;
for (int r = 0; r < image_size.height(); ++r) {
const uint8* grad_x_row = intermediate.getAddr8(0, r);
const uint8* grad_y_row = intermediate2.getAddr8(0, r);
uint8* target_row = input_bitmap->getAddr8(0, r);
for (int c = 0; c < image_size.width(); ++c) {
unsigned grad_x = grad_x_row[c];
unsigned grad_y = grad_y_row[c];
target_row[c] = (grad_x * grad_x + grad_y * grad_y) >> bit_shift;
}
}
}
void ExtractImageProfileInformation(const SkBitmap& input_bitmap,
const gfx::Rect& area,
const gfx::Size& target_size,
bool apply_log,
std::vector<float>* rows,
std::vector<float>* columns) {
SkAutoLockPixels source_lock(input_bitmap);
DCHECK(rows);
DCHECK(columns);
DCHECK(input_bitmap.getPixels());
DCHECK_EQ(kAlpha_8_SkColorType, input_bitmap.colorType());
DCHECK_GE(area.x(), 0);
DCHECK_GE(area.y(), 0);
DCHECK_LE(area.right(), input_bitmap.width());
DCHECK_LE(area.bottom(), input_bitmap.height());
// Make sure rows and columns are allocated and initialized to 0.
rows->clear();
columns->clear();
rows->resize(area.height(), 0);
columns->resize(area.width(), 0);
for (int r = 0; r < area.height(); ++r) {
// Points to the first byte of the row in the rectangle.
const uint8* image_row = input_bitmap.getAddr8(area.x(), r + area.y());
unsigned row_sum = 0;
for (int c = 0; c < area.width(); ++c, ++image_row) {
row_sum += *image_row;
(*columns)[c] += *image_row;
}
(*rows)[r] = row_sum;
}
if (apply_log) {
// Generally for processing we will need to take logarithm of this data.
// The option not to apply it is left principally as a test seam.
std::vector<float>::iterator it;
for (it = columns->begin(); it < columns->end(); ++it)
*it = std::log(1.0f + *it);
for (it = rows->begin(); it < rows->end(); ++it)
*it = std::log(1.0f + *it);
}
if (!target_size.IsEmpty()) {
// If the target size is given, profiles should be further processed through
// morphological closing. The idea is to close valleys smaller than what
// can be seen after scaling down to avoid deforming noticable features
// when profiles are used.
// Morphological closing is defined as dilation followed by errosion. In
// normal-speak: sliding-window maximum followed by minimum.
int column_window_size = 1 + 2 *
static_cast<int>(0.5f * area.width() / target_size.width() + 0.5f);
int row_window_size = 1 + 2 *
static_cast<int>(0.5f * area.height() / target_size.height() + 0.5f);
// Dilate and erode each profile with the given window size.
if (column_window_size >= 3) {
SlidingWindowMinMax(columns->begin(),
columns->end(),
columns->begin(),
column_window_size,
std::greater<float>());
SlidingWindowMinMax(columns->begin(),
columns->end(),
columns->begin(),
column_window_size,
std::less<float>());
}
if (row_window_size >= 3) {
SlidingWindowMinMax(rows->begin(),
rows->end(),
rows->begin(),
row_window_size,
std::greater<float>());
SlidingWindowMinMax(rows->begin(),
rows->end(),
rows->begin(),
row_window_size,
std::less<float>());
}
}
}
float AutoSegmentPeaks(const std::vector<float>& input) {
// This is a thresholding operation based on Otsu's method.
std::vector<int> histogram;
std::pair<float, float> minmax;
if (!ComputeScaledHistogram(input, &histogram, &minmax))
return minmax.first;
// max_index refers to the bin *after* which we need to split. The sought
// threshold is the centre of this bin, scaled back to the original range.
size_t max_index = FindOtsuThresholdingIndex(histogram);
return (minmax.second - minmax.first) * (max_index + 0.5f) / 255.0f +
minmax.first;
}
gfx::Size AdjustClippingSizeToAspectRatio(const gfx::Size& target_size,
const gfx::Size& image_size,
const gfx::Size& computed_size) {
DCHECK_GT(target_size.width(), 0);
DCHECK_GT(target_size.height(), 0);
// If the computed thumbnail would be too wide or to tall, we shall attempt
// to fix it. Generally the idea is to re-add content to the part which has
// been more aggressively shrunk unless there is nothing to add there or if
// adding there won't fix anything. Should that be the case, we will
// (reluctantly) take away more from the other dimension.
float desired_aspect =
static_cast<float>(target_size.width()) / target_size.height();
int computed_width = std::max(computed_size.width(), target_size.width());
int computed_height = std::max(computed_size.height(), target_size.height());
float computed_aspect = static_cast<float>(computed_width) / computed_height;
float aspect_change_delta = std::abs(computed_aspect - desired_aspect);
float prev_aspect_change_delta = 1000.0f;
const float kAspectChangeEps = 0.01f;
const float kLargeEffect = 2.0f;
while ((prev_aspect_change_delta - aspect_change_delta > kAspectChangeEps) &&
(computed_aspect / desired_aspect > kAspectRatioToleranceFactor ||
desired_aspect / computed_aspect > kAspectRatioToleranceFactor)) {
int new_computed_width = computed_width;
int new_computed_height = computed_height;
float row_dimension_shrink =
static_cast<float>(image_size.height()) / computed_height;
float column_dimension_shrink =
static_cast<float>(image_size.width()) / computed_width;
if (computed_aspect / desired_aspect > kAspectRatioToleranceFactor) {
// Too wide.
if (row_dimension_shrink > column_dimension_shrink) {
// Bring the computed_height to the least of:
// (1) image height (2) the number of lines that would
// make up the desired aspect or (3) number of lines we would get
// at the same 'aggressivity' level as width or.
new_computed_height = std::min(
static_cast<int>(image_size.height()),
static_cast<int>(computed_width / desired_aspect + 0.5f));
new_computed_height = std::min(
new_computed_height,
static_cast<int>(
image_size.height() / column_dimension_shrink + 0.5f));
} else if (row_dimension_shrink >= kLargeEffect ||
new_computed_width <= target_size.width()) {
// Even though rows were resized less, we will generally rather add than
// remove (or there is nothing to remove in x already).
new_computed_height = std::min(
static_cast<int>(image_size.height()),
static_cast<int>(computed_width / desired_aspect + 0.5f));
} else {
// Rows were already shrunk less aggressively. This means there is
// simply no room left too expand. Cut columns to get the desired
// aspect ratio.
new_computed_width = desired_aspect * computed_height + 0.5f;
}
} else {
// Too tall.
if (column_dimension_shrink > row_dimension_shrink) {
// Columns were shrunk more aggressively. Try to relax the same way as
// above.
new_computed_width = std::min(
static_cast<int>(image_size.width()),
static_cast<int>(desired_aspect * computed_height + 0.5f));
new_computed_width = std::min(
new_computed_width,
static_cast<int>(
image_size.width() / row_dimension_shrink + 0.5f));
} else if (column_dimension_shrink >= kLargeEffect ||
new_computed_height <= target_size.height()) {
new_computed_width = std::min(
static_cast<int>(image_size.width()),
static_cast<int>(desired_aspect * computed_height + 0.5f));
} else {
new_computed_height = computed_width / desired_aspect + 0.5f;
}
}
new_computed_width = std::max(new_computed_width, target_size.width());
new_computed_height = std::max(new_computed_height, target_size.height());
// Update loop control variables.
float new_computed_aspect =
static_cast<float>(new_computed_width) / new_computed_height;
if (std::abs(new_computed_aspect - desired_aspect) >
std::abs(computed_aspect - desired_aspect)) {
// Do not take inferior results.
break;
}
computed_width = new_computed_width;
computed_height = new_computed_height;
computed_aspect = new_computed_aspect;
prev_aspect_change_delta = aspect_change_delta;
aspect_change_delta = std::abs(new_computed_aspect - desired_aspect);
}
return gfx::Size(computed_width, computed_height);
}
void ConstrainedProfileSegmentation(const std::vector<float>& row_profile,
const std::vector<float>& column_profile,
const gfx::Size& target_size,
std::vector<bool>* included_rows,
std::vector<bool>* included_columns) {
DCHECK(included_rows);
DCHECK(included_columns);
std::vector<int> histogram_rows;
std::pair<float, float> minmax_rows;
bool rows_well_behaved = ComputeScaledHistogram(
row_profile, &histogram_rows, &minmax_rows);
float row_threshold = minmax_rows.first;
size_t clip_index_rows = 0;
if (rows_well_behaved) {
clip_index_rows = FindOtsuThresholdingIndex(histogram_rows);
row_threshold = (minmax_rows.second - minmax_rows.first) *
(clip_index_rows + 0.5f) / 255.0f + minmax_rows.first;
}
std::vector<int> histogram_columns;
std::pair<float, float> minmax_columns;
bool columns_well_behaved = ComputeScaledHistogram(column_profile,
&histogram_columns,
&minmax_columns);
float column_threshold = minmax_columns.first;
size_t clip_index_columns = 0;
if (columns_well_behaved) {
clip_index_columns = FindOtsuThresholdingIndex(histogram_columns);
column_threshold = (minmax_columns.second - minmax_columns.first) *
(clip_index_columns + 0.5f) / 255.0f + minmax_columns.first;
}
int auto_segmented_width = count_if(
column_profile.begin(), column_profile.end(),
std::bind2nd(std::greater<float>(), column_threshold));
int auto_segmented_height = count_if(
row_profile.begin(), row_profile.end(),
std::bind2nd(std::greater<float>(), row_threshold));
gfx::Size computed_size = AdjustClippingSizeToAspectRatio(
target_size,
gfx::Size(column_profile.size(), row_profile.size()),
gfx::Size(auto_segmented_width, auto_segmented_height));
// Apply thresholding.
if (rows_well_behaved) {
ConstrainedProfileThresholding(row_profile,
histogram_rows,
clip_index_rows,
row_threshold,
minmax_rows,
auto_segmented_height,
computed_size.height(),
included_rows);
} else {
// This is essentially an error condition, invoked when no segmentation was
// possible. This will result in applying a very low threshold and likely
// in producing a thumbnail which should get rejected.
included_rows->resize(row_profile.size());
for (size_t i = 0; i < row_profile.size(); ++i)
(*included_rows)[i] = row_profile[i] > row_threshold;
}
if (columns_well_behaved) {
ConstrainedProfileThresholding(column_profile,
histogram_columns,
clip_index_columns,
column_threshold,
minmax_columns,
auto_segmented_width,
computed_size.width(),
included_columns);
} else {
included_columns->resize(column_profile.size());
for (size_t i = 0; i < column_profile.size(); ++i)
(*included_columns)[i] = column_profile[i] > column_threshold;
}
}
SkBitmap ComputeDecimatedImage(const SkBitmap& bitmap,
const std::vector<bool>& rows,
const std::vector<bool>& columns) {
SkAutoLockPixels source_lock(bitmap);
DCHECK(bitmap.getPixels());
DCHECK_GT(bitmap.bytesPerPixel(), 0);
DCHECK_EQ(bitmap.width(), static_cast<int>(columns.size()));
DCHECK_EQ(bitmap.height(), static_cast<int>(rows.size()));
unsigned target_row_count = std::count(rows.begin(), rows.end(), true);
unsigned target_column_count = std::count(
columns.begin(), columns.end(), true);
if (target_row_count == 0 || target_column_count == 0)
return SkBitmap(); // Not quite an error, so no DCHECK. Just return empty.
if (target_row_count == rows.size() && target_column_count == columns.size())
return SkBitmap(); // Equivalent of the situation above (empty target).
// Allocate the target image.
SkBitmap target;
target.allocPixels(bitmap.info().makeWH(target_column_count,
target_row_count));
int target_row = 0;
for (int r = 0; r < bitmap.height(); ++r) {
if (!rows[r])
continue; // We can just skip this one.
uint8* src_row =
static_cast<uint8*>(bitmap.getPixels()) + r * bitmap.rowBytes();
uint8* insertion_target = static_cast<uint8*>(target.getPixels()) +
target_row * target.rowBytes();
int left_copy_pixel = -1;
for (int c = 0; c < bitmap.width(); ++c) {
if (left_copy_pixel < 0 && columns[c]) {
left_copy_pixel = c; // Next time we will start copying from here.
} else if (left_copy_pixel >= 0 && !columns[c]) {
// This closes a fragment we want to copy. We do it now.
size_t bytes_to_copy = (c - left_copy_pixel) * bitmap.bytesPerPixel();
memcpy(insertion_target,
src_row + left_copy_pixel * bitmap.bytesPerPixel(),
bytes_to_copy);
left_copy_pixel = -1;
insertion_target += bytes_to_copy;
}
}
// We can still have the tail end to process here.
if (left_copy_pixel >= 0) {
size_t bytes_to_copy =
(bitmap.width() - left_copy_pixel) * bitmap.bytesPerPixel();
memcpy(insertion_target,
src_row + left_copy_pixel * bitmap.bytesPerPixel(),
bytes_to_copy);
}
target_row++;
}
return target;
}
SkBitmap CreateRetargetedThumbnailImage(
const SkBitmap& source_bitmap,
const gfx::Size& target_size,
float kernel_sigma) {
// First thing we need for this method is to color-reduce the source_bitmap.
SkBitmap reduced_color;
reduced_color.allocPixels(SkImageInfo::MakeA8(source_bitmap.width(),
source_bitmap.height()));
if (!color_utils::ComputePrincipalComponentImage(source_bitmap,
&reduced_color)) {
// CCIR601 luminance conversion vector.
gfx::Vector3dF transform(0.299f, 0.587f, 0.114f);
if (!color_utils::ApplyColorReduction(
source_bitmap, transform, true, &reduced_color)) {
DLOG(WARNING) << "Failed to compute luminance image from a screenshot. "
<< "Cannot compute retargeted thumbnail.";
return SkBitmap();
}
DLOG(WARNING) << "Could not compute principal color image for a thumbnail. "
<< "Using luminance instead.";
}
// Turn 'color-reduced' image into the 'energy' image.
ApplyGaussianGradientMagnitudeFilter(&reduced_color, kernel_sigma);
// Extract vertical and horizontal projection of image features.
std::vector<float> row_profile;
std::vector<float> column_profile;
ExtractImageProfileInformation(reduced_color,
gfx::Rect(reduced_color.width(),
reduced_color.height()),
target_size,
true,
&row_profile,
&column_profile);
std::vector<bool> included_rows, included_columns;
ConstrainedProfileSegmentation(row_profile,
column_profile,
target_size,
&included_rows,
&included_columns);
// Use the original image and computed inclusion vectors to create a resized
// image.
return ComputeDecimatedImage(source_bitmap, included_rows, included_columns);
}
} // thumbnailing_utils