root/chrome/browser/thumbnails/content_analysis.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. SlidingWindowMinMax
  2. FindOtsuThresholdingIndex
  3. ComputeScaledHistogram
  4. ConstrainedProfileThresholding
  5. ApplyGaussianGradientMagnitudeFilter
  6. ExtractImageProfileInformation
  7. AutoSegmentPeaks
  8. AdjustClippingSizeToAspectRatio
  9. ConstrainedProfileSegmentation
  10. ComputeDecimatedImage
  11. CreateRetargetedThumbnailImage

// 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(SkBitmap::kA8_Config, input_bitmap->config());

  // 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.setConfig(
      input_bitmap->config(), image_size.width(), image_size.height());
  intermediate.allocPixels();

  SkBitmap intermediate2;
  intermediate2.setConfig(
      input_bitmap->config(), image_size.width(), image_size.height());
  intermediate2.allocPixels();


  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(SkBitmap::kA8_Config, input_bitmap.config());
  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.setConfig(bitmap.config(), target_column_count, target_row_count);
  target.allocPixels();

  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.setConfig(
      SkBitmap::kA8_Config, source_bitmap.width(), source_bitmap.height());
  reduced_color.allocPixels();

  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

/* [<][>][^][v][top][bottom][index][help] */