Merge pull request #752 from kaedroho/images-refactor

Images: Remove FocalPoint class
This commit is contained in:
Matt Westcott 2014-11-04 17:18:22 +00:00
commit 2bb56510c1
14 changed files with 201 additions and 158 deletions

View file

@ -83,6 +83,6 @@ You can manually run feature detection on all images by running the following co
from wagtail.wagtailimages.models import Image
for image in Image.objects.all():
if image.focal_point is None:
image.focal_point = image.get_suggested_focal_point()
if not image.has_focal_point():
image.set_focal_point(image.get_suggested_focal_point())
image.save()

View file

@ -2,8 +2,7 @@ from __future__ import division
from django.conf import settings
from wagtail.wagtailimages.utils.rect import Rect
from wagtail.wagtailimages.utils.focal_point import FocalPoint
from wagtail.wagtailimages.rect import Rect
class BaseImageBackend(object):
@ -183,8 +182,7 @@ class BaseImageBackend(object):
# Find focal point UV
if focal_point is not None:
fp_x = focal_point.x
fp_y = focal_point.y
fp_x, fp_y = focal_point.centroid
else:
# Fall back to positioning in the centre
fp_x = im_width / 2
@ -205,10 +203,10 @@ class BaseImageBackend(object):
# Make sure the entire focal point is in the crop box
if focal_point is not None:
focal_point_left = focal_point.x - focal_point.width / 2
focal_point_top = focal_point.y - focal_point.height / 2
focal_point_right = focal_point.x + focal_point.width / 2
focal_point_bottom = focal_point.y + focal_point.height / 2
focal_point_left = focal_point.left
focal_point_top = focal_point.top
focal_point_right = focal_point.right
focal_point_bottom = focal_point.bottom
if left > focal_point_left:
right -= left - focal_point_left

View file

@ -20,7 +20,7 @@ else:
opencv_available = False
from wagtail.wagtailimages.utils.focal_point import FocalPoint, combine_focal_points
from wagtail.wagtailimages.rect import Rect
class FeatureDetector(object):
@ -50,7 +50,7 @@ class FeatureDetector(object):
points = cv.GoodFeaturesToTrack(image, eig_image, temp_image, 20, 0.04, 1.0, useHarris=False)
if points:
return [FocalPoint(x, y, 1) for x, y in points]
return points
return []
@ -73,19 +73,6 @@ class FeatureDetector(object):
)
if faces:
return [FocalPoint.from_square(face[0][0], face[0][1], face[0][2], face[0][3]) for face in faces]
return [Rect(face[0][0], face[0][1], face[0][0] + face[0][2], face[0][1] + face[0][3]) for face in faces]
return []
def get_focal_point(self):
# Face detection
faces = self.detect_faces()
if faces:
return combine_focal_points(faces)
# Feature detection
features = self.detect_features()
if features:
return combine_focal_points(features)

View file

@ -23,8 +23,8 @@ from unidecode import unidecode
from wagtail.wagtailadmin.taggable import TagSearchable
from wagtail.wagtailimages.backends import get_image_backend
from wagtail.wagtailsearch import index
from wagtail.wagtailimages.utils.focal_point import FocalPoint
from wagtail.wagtailimages.utils.feature_detection import FeatureDetector, opencv_available
from wagtail.wagtailimages.feature_detection import FeatureDetector, opencv_available
from wagtail.wagtailimages.rect import Rect
from wagtail.wagtailadmin.utils import get_object_usage
@ -73,26 +73,30 @@ class AbstractImage(models.Model, TagSearchable):
def __str__(self):
return self.title
@property
def focal_point(self):
def get_rect(self):
return Rect(0, 0, self.width, self.height)
def get_focal_point(self):
if self.focal_point_x is not None and \
self.focal_point_y is not None and \
self.focal_point_width is not None and \
self.focal_point_height is not None:
return FocalPoint(
return Rect.from_point(
self.focal_point_x,
self.focal_point_y,
width=self.focal_point_width,
height=self.focal_point_height,
self.focal_point_width,
self.focal_point_height,
)
@focal_point.setter
def focal_point(self, focal_point):
if focal_point is not None:
self.focal_point_x = focal_point.x
self.focal_point_y = focal_point.y
self.focal_point_width = focal_point.width
self.focal_point_height = focal_point.height
def has_focal_point(self):
return self.get_focal_point() is not None
def set_focal_point(self, rect):
if rect is not None:
self.focal_point_x = rect.centroid_x
self.focal_point_y = rect.centroid_y
self.focal_point_width = rect.width
self.focal_point_height = rect.height
else:
self.focal_point_x = None
self.focal_point_y = None
@ -118,14 +122,38 @@ class AbstractImage(models.Model, TagSearchable):
# Use feature detection to find a focal point
feature_detector = FeatureDetector(image.size, image_data[0], image_data[1])
focal_point = feature_detector.get_focal_point()
# Add 20% extra room around the edge of the focal point
if focal_point:
focal_point.width *= 1.20
focal_point.height *= 1.20
faces = feature_detector.detect_faces()
if faces:
# Create a bounding box around all faces
left = min(face.left for face in faces)
top = min(face.top for face in faces)
right = max(face.right for face in faces)
bottom = max(face.bottom for face in faces)
focal_point = Rect(left, top, right, bottom)
else:
features = feature_detector.detect_features()
if features:
# Create a bounding box around all features
left = min(feature.x for feature in features)
top = min(feature.y for feature in features)
right = max(feature.x for feature in features)
bottom = max(feature.y for feature in features)
focal_point = Rect(left, top, right, bottom)
else:
return None
return focal_point
# Add 20% to width and height and give it a minimum size
x, y = focal_point.centroid
width, height = focal_point.size
width *= 1.20
height *= 1.20
width = max(width, 100)
height = max(height, 100)
return Rect.from_point(x, y, width, height)
def get_rendition(self, filter):
if not hasattr(filter, 'process_image'):
@ -134,10 +162,10 @@ class AbstractImage(models.Model, TagSearchable):
filter, created = Filter.objects.get_or_create(spec=filter)
try:
if self.focal_point:
if self.has_focal_point():
rendition = self.renditions.get(
filter=filter,
focal_point_key=self.focal_point.get_key(),
focal_point_key=self.get_focal_point().get_key(),
)
else:
rendition = self.renditions.get(
@ -150,11 +178,11 @@ class AbstractImage(models.Model, TagSearchable):
# If we have a backend attribute then pass it to process
# image - else pass 'default'
backend_name = getattr(self, 'backend', 'default')
generated_image = filter.process_image(file_field.file, backend_name=backend_name, focal_point=self.focal_point)
generated_image = filter.process_image(file_field.file, backend_name=backend_name, focal_point=self.get_focal_point())
# generate new filename derived from old one, inserting the filter spec and focal point key before the extension
if self.focal_point is not None:
focal_point_key = "focus-" + self.focal_point.get_key()
if self.has_focal_point():
focal_point_key = "focus-" + self.get_focal_point().get_key()
else:
focal_point_key = "focus-none"
@ -165,10 +193,10 @@ class AbstractImage(models.Model, TagSearchable):
output_filename = filename_without_extension + '.' + extension
generated_image_file = File(generated_image, name=output_filename)
if self.focal_point:
if self.has_focal_point():
rendition, created = self.renditions.get_or_create(
filter=filter,
focal_point_key=self.focal_point.get_key(),
focal_point_key=self.get_focal_point().get_key(),
defaults={'file': generated_image_file}
)
else:
@ -222,9 +250,9 @@ def image_feature_detection(sender, instance, **kwargs):
raise ImproperlyConfigured("pyOpenCV could not be found.")
# Make sure the image doesn't already have a focal point
if instance.focal_point is None:
if not instance.has_focal_point():
# Set the focal point
instance.focal_point = instance.get_suggested_focal_point()
instance.set_focal_point(instance.get_suggested_focal_point())
# Receive the pre_delete signal and delete the file associated with the model instance.

View file

@ -23,6 +23,18 @@ class Rect(object):
def size(self):
return self.width, self.height
@property
def centroid_x(self):
return (self.left + self.right) / 2
@property
def centroid_y(self):
return (self.top + self.bottom) / 2
@property
def centroid(self):
return self.centroid_x, self.centroid_y
def as_tuple(self):
return self.left, self.top, self.right, self.bottom
@ -36,3 +48,22 @@ class Rect(object):
return 'Rect(left: %d, top: %d, right: %d, bottom: %d)' % (
self.left, self.top, self.right, self.bottom
)
@classmethod
def from_point(cls, x, y, width, height):
return cls(
x - width / 2,
y - height / 2,
x + width / 2,
y + height / 2,
)
# DELETEME
def get_key(self):
return "%(x)d-%(y)d-%(width)dx%(height)d" % {
'x': int(self.centroid_x),
'y': int(self.centroid_y),
'width': int(self.width),
'height': int(self.height),
}

View file

@ -7,7 +7,7 @@ from django.contrib.auth.models import Permission
from django.core.files.uploadedfile import SimpleUploadedFile
from wagtail.tests.utils import WagtailTestUtils
from wagtail.wagtailimages.utils.crypto import generate_signature
from wagtail.wagtailimages.utils import generate_signature
from .utils import Image, get_test_image_file

View file

@ -13,6 +13,7 @@ from wagtail.tests.models import EventPage, EventPageCarouselItem
from wagtail.wagtailimages.models import Rendition
from wagtail.wagtailimages.backends import get_image_backend
from wagtail.wagtailimages.backends.pillow import PillowBackend
from wagtail.wagtailimages.rect import Rect
from .utils import Image, get_test_image_file
@ -31,6 +32,52 @@ class TestImage(TestCase):
def test_is_landscape(self):
self.assertTrue(self.image.is_landscape())
def test_get_rect(self):
self.assertTrue(self.image.get_rect(), Rect(0, 0, 640, 480))
def test_get_focal_point(self):
self.assertEqual(self.image.get_focal_point(), None)
# Add a focal point to the image
self.image.focal_point_x = 100
self.image.focal_point_y = 200
self.image.focal_point_width = 50
self.image.focal_point_height = 20
# Get it
self.assertEqual(self.image.get_focal_point(), Rect(75, 190, 125, 210))
def test_has_focal_point(self):
self.assertFalse(self.image.has_focal_point())
# Add a focal point to the image
self.image.focal_point_x = 100
self.image.focal_point_y = 200
self.image.focal_point_width = 50
self.image.focal_point_height = 20
self.assertTrue(self.image.has_focal_point())
def test_set_focal_point(self):
self.assertEqual(self.image.focal_point_x, None)
self.assertEqual(self.image.focal_point_y, None)
self.assertEqual(self.image.focal_point_width, None)
self.assertEqual(self.image.focal_point_height, None)
self.image.set_focal_point(Rect(100, 150, 200, 350))
self.assertEqual(self.image.focal_point_x, 150)
self.assertEqual(self.image.focal_point_y, 250)
self.assertEqual(self.image.focal_point_width, 100)
self.assertEqual(self.image.focal_point_height, 200)
self.image.set_focal_point(None)
self.assertEqual(self.image.focal_point_x, None)
self.assertEqual(self.image.focal_point_y, None)
self.assertEqual(self.image.focal_point_width, None)
self.assertEqual(self.image.focal_point_height, None)
class TestImagePermissions(TestCase):
def setUp(self):

View file

@ -7,7 +7,8 @@ from django import template
from django.utils import six
from django.core.urlresolvers import reverse
from wagtail.wagtailimages.utils.crypto import generate_signature, verify_signature
from wagtail.wagtailimages.utils import generate_signature, verify_signature
from wagtail.wagtailimages.rect import Rect
from wagtail.wagtailimages.formats import Format, get_image_format, register_image_format
from .utils import Image, get_test_image_file
@ -177,3 +178,52 @@ class TestFrontendServeView(TestCase):
# Check response
self.assertEqual(response.status_code, 400)
class TestRect(TestCase):
def test_init(self):
rect = Rect(100, 150, 200, 250)
self.assertEqual(rect.left, 100)
self.assertEqual(rect.top, 150)
self.assertEqual(rect.right, 200)
self.assertEqual(rect.bottom, 250)
def test_equality(self):
self.assertEqual(Rect(100, 150, 200, 250), Rect(100, 150, 200, 250))
self.assertNotEqual(Rect(100, 150, 200, 250), Rect(10, 15, 20, 25))
def test_getitem(self):
rect = Rect(100, 150, 200, 250)
self.assertEqual(rect[0], 100)
self.assertEqual(rect[1], 150)
self.assertEqual(rect[2], 200)
self.assertEqual(rect[3], 250)
self.assertRaises(IndexError, rect.__getitem__, 4)
def test_as_tuple(self):
rect = Rect(100, 150, 200, 250)
self.assertEqual(rect.as_tuple(), (100, 150, 200, 250))
def test_size(self):
rect = Rect(100, 150, 200, 350)
self.assertEqual(rect.size, (100, 200))
self.assertEqual(rect.width, 100)
self.assertEqual(rect.height, 200)
def test_centroid(self):
rect = Rect(100, 150, 200, 350)
self.assertEqual(rect.centroid, (150, 250))
self.assertEqual(rect.centroid_x, 150)
self.assertEqual(rect.centroid_y, 250)
def test_repr(self):
rect = Rect(100, 150, 200, 250)
self.assertEqual(repr(rect), "Rect(left: 100, top: 150, right: 200, bottom: 250)")
def test_from_point(self):
rect = Rect.from_point(100, 200, 50, 20)
self.assertEqual(rect, Rect(75, 190, 125, 210))
def test_get_key(self):
rect = Rect(100, 150, 200, 250)
self.assertEqual(rect.get_key(), '150-200-100x100')

View file

@ -1,98 +0,0 @@
# https://github.com/thumbor/thumbor/blob/8a50bfba9443e8d2a1a691ab20eeb525815be597/thumbor/point.py
# thumbor imaging service
# https://github.com/globocom/thumbor/wiki
# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license
# Copyright (c) 2011 globo.com timehome@corp.globo.com
class FocalPoint(object):
ALIGNMENT_PERCENTAGES = {
'left': 0.0,
'center': 0.5,
'right': 1.0,
'top': 0.0,
'middle': 0.5,
'bottom': 1.0
}
def to_dict(self):
return {
'x': self.x,
'y': self.y,
'z': self.weight,
'height': self.height,
'width': self.width,
'origin': self.origin
}
@classmethod
def from_dict(cls, values):
return cls(
x=float(values['x']),
y=float(values['y']),
weight=float(values['z']),
width=float(values.get('width', 1)),
height=float(values.get('height', 1)),
origin=values.get('origin', 'alignment')
)
def __init__(self, x, y, height=1, width=1, weight=1.0, origin="alignment"):
self.x = x
self.y = y
self.height = height
self.width = width
self.weight = weight
self.origin = origin
@classmethod
def from_square(cls, x, y, width, height, origin='detection'):
center_x = x + (width / 2)
center_y = y + (height / 2)
return cls(center_x, center_y, height=height, width=width, weight=width * height, origin=origin)
@classmethod
def from_alignment(cls, halign, valign, width, height):
x = width * cls.ALIGNMENT_PERCENTAGES[halign]
y = height * cls.ALIGNMENT_PERCENTAGES[valign]
return cls(x, y)
def __repr__(self):
return 'FocalPoint(x: %d, y: %d, width: %d, height: %d, weight: %d, origin: %s)' % (
self.x, self.y, self.width, self.height, self.weight, self.origin
)
def get_key(self):
return "%(x)d-%(y)d-%(width)dx%(height)d" % self.to_dict()
def combine_focal_points(focal_points):
# https://github.com/thumbor/thumbor/blob/fc75f2d617942e3548986fe8403ad717fc9978ba/thumbor/transformer.py#L255-L269
if not focal_points:
return
total_weight = 0.0
total_x = 0.0
total_y = 0.0
for focal_point in focal_points:
total_weight += focal_point.weight
total_x += focal_point.x * focal_point.weight
total_y += focal_point.y * focal_point.weight
x = total_x / total_weight
y = total_y / total_weight
min_x = min([point.x - point.width / 2 for point in focal_points])
min_y = min([point.y - point.height / 2 for point in focal_points])
max_x = max([point.x + point.width / 2 for point in focal_points])
max_y = max([point.y + point.height / 2 for point in focal_points])
width = max_x - min_x
height = max_y - min_y
return FocalPoint(x, y, width=width, height=height, weight=total_weight)

View file

@ -4,7 +4,7 @@ from django.core.exceptions import PermissionDenied
from django.views.decorators.cache import cache_control
from wagtail.wagtailimages.models import get_image_model, Filter
from wagtail.wagtailimages.utils.crypto import verify_signature
from wagtail.wagtailimages.utils import verify_signature
@cache_control(max_age=60*60*24*60) # Cache for 60 days
@ -15,6 +15,6 @@ def serve(request, signature, image_id, filter_spec):
raise PermissionDenied
try:
return Filter(spec=filter_spec).process_image(image.file.file, HttpResponse(content_type='image/jpeg'), focal_point=image.focal_point)
return Filter(spec=filter_spec).process_image(image.file.file, HttpResponse(content_type='image/jpeg'), focal_point=image.get_focal_point())
except Filter.InvalidFilterSpecError:
return HttpResponse("Invalid filter spec: " + filter_spec, content_type='text/plain', status=400)

View file

@ -16,7 +16,7 @@ from wagtail.wagtailsearch.backends import get_search_backends
from wagtail.wagtailimages.models import get_image_model, Filter
from wagtail.wagtailimages.forms import get_image_form, URLGeneratorForm
from wagtail.wagtailimages.utils.crypto import generate_signature
from wagtail.wagtailimages.utils import generate_signature
from wagtail.wagtailimages.fields import MAX_UPLOAD_SIZE