Is it possible to add integration to Smart Crop the way ImageField Focus does?
I find that Smart crop does a fairly decent job on it's own, but would like to correct a few images that don't get cropped nicely with this module. Imagefield Focus doesn't work with Media module, so some hacking is required there. Since this module already works with media module, it would be great to have the smart crop integration as regular crop does a fairly poor job of cropping images automatically.

CommentFileSizeAuthor
#5 fp_smartcrop.patch5.41 KBbleen
#2 fp_smart_crop.patch2.9 KBbleen
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

bleen’s picture

I need to think through how that would work.... I guess I would need to have the image run through smartcrop on the fly and see if I cant get it to suggest a default focal point immediately as the image is uploaded. Im not familiar with the guts of smart crop so I do not know if this is even feasible, but I think that would be the ideal UX.

Any thoughts?

bleen’s picture

Status: Active » Needs review
FileSize
2.9 KB

That was actually much more straight-forward than I thought it would be ... It would be better if the smartcrop module would break out its entropy calculations into a separate function so I didnt need to copy 80% of the image_gd_smartcrop_crop function into focal_point.

bleen’s picture

Basically, this works on the initial upload of an image (and only on the initial upload). Also, I have made this configurable since smartcrop can be very slow...

Elijah Lynn’s picture

Status: Needs review » Needs work

Hah, needs work, call to undefined function!

bleen’s picture

Status: Needs work » Needs review
FileSize
5.41 KB

Forgot a file in the previous patch

Elijah Lynn’s picture

Status: Needs review » Reviewed & tested by the community

Works!

bleen’s picture

Status: Reviewed & tested by the community » Fixed

http://drupalcode.org/project/focal_point.git/commit/b41afb6 ... wrong commit. But I'm on a network that blocks SSH. Will push to origin later

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

GaëlG’s picture

Status: Closed (fixed) » Needs review

I worked on having better performance. Made many tests to get something both accurate and fast.

Many problems with the current algorithm:
* Using the smartcrop algorithm as-is is slow because in our case we try to get the most entropic point, not the most entropic area. So many loops are done.
* The calculation is done on the full-size image, which can be very large.
* The progressive cropping is done with slices of 10px. It should be proportional to the image's size.
* The vertical crop is done on an already horizontally-cropped image. In real scale and crop life, images are never cropped both ways.
* The entropy is compared between the slices to remove. It should be compared between the potentially cropped images. This problem is explained here: http://envalo.com/image-cropping-php-using-entropy-explained/

Based on this, I implemented two algorithms: one based on the linked-above post, which cut the image into 25 slices, and one which uses the former idea to crop the image progressively from edges, but with the above problems solved.

It looks like the 25-slices algorithm is better, but I post both for information and later discussion.

25-slices algorithm:

/**
 * Given an image, use smartcrop to estimate a good focal point.
 *
 * @see image_gd_smartcrop_crop()
 * @see http://envalo.com/image-cropping-php-using-entropy-explained/
 */
function focal_point_smartcrop_estimation(stdClass $image_data) {  
  // To avoid bad performance with large images, we calculate the focal point on
  // a smaller version of the image. 500px should be far enough.
  static $px_needed = 500;
  $image_data = clone($image_data);
  $image_width = $image_data->info['width'];
  $image_height = $image_data->info['height'];
  $ratio = max($px_needed / $image_width, $px_needed / $image_height);
  $resized = FALSE;
  if ($ratio <= 1) {
    $resized = image_gd_resize($image_data, round($ratio * $image_width), round($ratio * $image_height));
  }

  $full_width = imagesx($image_data->resource);
  $full_height = imagesy($image_data->resource);

  // Given that most of the time, cropped images have a width/height ratio
  // between 4:1 and 1:4, the width of a cropped image will be greater than
  // (original height)/4 and the height greater than (original width)/4.
  // Based on this assumption, we will try to find the most interesting area
  // (max entropy) of this size.
  // The focal point will simply be the center of the resulting area.
  $target_width = round($full_height / 4);
  $target_height = round($full_width / 4);

  // First, we will cut our image into 25 vertical slices, and calculate their 
  // entropy.
  static $slices_count = 25;
  $slice_width = $full_width / $slices_count;
  $slice_entropies = array();
  for ($i=0 ; $i < $slices_count ; $i++) {
    $slice_entropies[$i] = _smartcrop_gd_entropy_slice($image_data, round($i * $slice_width), 0, $slice_width, $full_height);
  }
  // We calculate how many slices are needed to get the target width.
  $target_slices_count = ceil($target_width / $slice_width);
  // We get the consecutive slices that lead to the maximum entropy.
  $best_first_slice = _focal_point_smartcrop_get_best_first_slice($slice_entropies, $target_slices_count);
  // We calculate the middle of the resulting area.
  $x = $slice_width * ($best_first_slice + ($target_slices_count / 2));

  // We do the same vertically.
  // NB: our slices are full-width because in the real world of scale and crop,
  // images are cropped vertically or horizontally, but never both ways.
  $slice_height = round($full_height / $slices_count);
  $slice_entropies = array();
  for ($i=0 ; $i < $slices_count ; $i++) {
    $slice_entropies[$i] = _smartcrop_gd_entropy_slice($image_data, 0, round($i * $slice_height), $full_width, $slice_height);
  }
  $target_slices_count = ceil($target_height / $slice_height);
  $best_first_slice = _focal_point_smartcrop_get_best_first_slice($slice_entropies, $target_slices_count);
  $y = $slice_height * ($best_first_slice + ($target_slices_count / 2));

  if ($resized) {
    // We return the center of the resulting area, but for the original image, 
    // not the small one.
    $x = $x / $ratio;
    $y = $y / $ratio;
  }

  return array(round($x), round($y));
}

function _focal_point_smartcrop_get_best_first_slice($slice_entropies, $target_slices_count) {
  $slices_count = count($slice_entropies);
  $best_first_slice = 0;
  $best_entropy = _focal_point_smartcrop_sum_entropies($slice_entropies, $best_first_slice, $target_slices_count);
  for ($first_slice = 1 ; $first_slice <= $slices_count - $target_slices_count ; $first_slice++) {
    $entropy = _focal_point_smartcrop_sum_entropies($slice_entropies, $first_slice, $target_slices_count);
    if ($entropy > $best_entropy) {
      $best_entropy = $entropy;
      $best_first_slice = $first_slice;
    }
  }
  return $best_first_slice;
}

function _focal_point_smartcrop_sum_entropies($slice_entropies, $first_slice, $target_slices_count) {
  $entropy = 0;
  for ($i=0 ; $i < $target_slices_count ; $i++) {
    $entropy += $slice_entropies[$first_slice + $i];
  }
  return $entropy;
}

Progressive crop algorithm:

/**
 * Given an image, use smartcrop to estimate a good focal point.
 *
 * @see image_gd_smartcrop_crop()
 */
function focal_point_smartcrop_estimation(stdClass $image_data) {  
  // To avoid bad performance with large images, we calculate the focal point on
  // a smaller version of the image. 500px should be far enough.
  static $px_needed = 500;
  $image_data = clone($image_data);
  $image_width = $image_data->info['width'];
  $image_height = $image_data->info['height'];
  $ratio = max($px_needed / $image_width, $px_needed / $image_height);
  $resized = FALSE;
  if ($ratio <= 1) {
    $resized = image_gd_resize($image_data, round($ratio * $image_width), round($ratio * $image_height));
  }

  $full_width = imagesx($image_data->resource);
  $full_height = imagesy($image_data->resource);
  // Given that most of the time, cropped images have a width/height ratio
  // between 4:1 and 1:4, the width of a cropped image will be greater than
  // (original height)/4 and the height greater than (original width)/4.
  // Based on this assumption, we will try to find the most interesting area
  // (max entropy) of this size.
  // The focal point will simply be the center of the resulting area.
  $target_width = round($full_height / 4);
  $target_height = round($full_width / 4);
  // We will crop 10 times to get the target width, on the left or on the right.
  $slice_width = ceil(($full_width - $target_width) / 10);
  $left_entropy = $right_entropy = 0;
  $left = 0;
  $right = $full_width;
  while ($right - $left > $target_width) {
    // We compare the entropy of the image cropped from left or right.
    $left_entropy = _smartcrop_gd_entropy_slice($image_data, 0, 0, $right - $left - $slice_width, $full_height);
    $right_entropy = _smartcrop_gd_entropy_slice($image_data, $slice_width, 0, $right - $left - $slice_width, $full_height);

    if ($left_entropy >= $right_entropy) {
      $right -= $slice_width;
    }
    else {
      $left += $slice_width;
    }
  }
  // We calculate the middle of the resulting area.
  $x = ($right + $left) / 2;
  // We do the same vertically.
  // NB: our slices are full-width because in the real world of scale and crop,
  // images are cropped vertically or horizontally, but never both ways.
  $slice_height = ceil(($full_height - $target_height) / 10);
  $top_entropy = $bottom_entropy = 0;
  $top = 0;
  $bottom = $full_height;
  while ($bottom - $top > $target_height) {
   if (!$top_entropy) {
      $top_entropy = _smartcrop_gd_entropy_slice($image_data, 0, 0, $full_width, $bottom - $top - $slice_height);
    }
    if (!$bottom_entropy) {
      $bottom_entropy = _smartcrop_gd_entropy_slice($image_data, 0, $slice_height, $full_width, $bottom - $top - $slice_height);
    }
    if ($top_entropy >= $bottom_entropy) {
      $bottom -= $slice_height;
      $bottom_entropy = 0;
    }
    else {
      $top += $slice_height;
      $top_entropy = 0;
    }
  }
  $y = ($bottom + $top) / 2;

  if ($resized) {
    // We return the center of the resulting area, but for the original image, 
    // not the small one.
    $x = $x / $ratio;
    $y = $y / $ratio;
  }
  return array(round($x), round($y));
}

I'm interested in feedback on this. :)

bleen’s picture

A few quick thoughts:

  • This is great work... thanks for looking at this
  • Can we open a new issue for this instead of reopening an old one
  • How much better is the 25-slice algorithm? In speed? In accuracy?
  • Is there value in including both and giving the user an option?
GaëlG’s picture

Status: Needs review » Closed (fixed)
Related issues: +#2370865: Algorithm for a smart default focal point

Allright, I answer in the new issue: #2370865: Algorithm for a smart default focal point.