mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-14 18:23:14 +00:00
Merge pull request #752 from kaedroho/images-refactor
Images: Remove FocalPoint class
This commit is contained in:
commit
2bb56510c1
14 changed files with 201 additions and 158 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue