Skip to content

Commit

Permalink
Merge pull request #587 from jabooth/crop_rm_inplace
Browse files Browse the repository at this point in the history
promote non-inplace crop methods, crop performance improvements
  • Loading branch information
jabooth committed Jun 1, 2015
2 parents 658ccad + 7b6003c commit 27408c8
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 120 deletions.
2 changes: 1 addition & 1 deletion menpo/feature/test/test_features.py
Expand Up @@ -256,7 +256,7 @@ def test_lbp_values():

def test_constrain_landmarks():
breaking_bad = mio.import_builtin_asset('breakingbad.jpg').as_masked()
breaking_bad.crop_to_landmarks_inplace(boundary=20)
breaking_bad = breaking_bad.crop_to_landmarks(boundary=20)
breaking_bad = breaking_bad.resize([50, 50])
breaking_bad.constrain_mask_to_landmarks()
hog_b = hog(breaking_bad, mode='sparse')
Expand Down
168 changes: 98 additions & 70 deletions menpo/image/base.py
Expand Up @@ -5,7 +5,7 @@
import PIL.Image as PILImage

from menpo.compatibility import basestring
from menpo.base import Vectorizable
from menpo.base import Vectorizable, MenpoDeprecationWarning
from menpo.shape import PointCloud
from menpo.landmark import Landmarkable
from menpo.transform import (Translation, NonUniformScale,
Expand Down Expand Up @@ -867,12 +867,12 @@ def gradient(self, **kwargs):
from menpo.feature import gradient as grad_feature
return grad_feature(self)

def crop_inplace(self, min_indices, max_indices,
constrain_to_boundary=True):
def crop(self, min_indices, max_indices,
constrain_to_boundary=False):
r"""
Crops this image using the given minimum and maximum indices.
Landmarks are correctly adjusted so they maintain their position
relative to the newly cropped image.
Return a cropped copy of this image using the given minimum and
maximum indices. Landmarks are correctly adjusted so they maintain
their position relative to the newly cropped image.
Parameters
----------
Expand All @@ -888,15 +888,15 @@ def crop_inplace(self, min_indices, max_indices,
Returns
-------
cropped_image : `type(self)`
This image, cropped.
A new instance of self, but cropped.
Raises
------
ValueError
``min_indices`` and ``max_indices`` both have to be of length
``n_dims``. All ``max_indices`` must be greater than
``min_indices``.
:map:`ImageBoundaryError`
ImageBoundaryError
Raised if ``constrain_to_boundary=False``, and an attempt is made
to crop the image in a way that violates the image bounds.
"""
Expand All @@ -917,59 +917,16 @@ def crop_inplace(self, min_indices, max_indices,
# points have been constrained and the user didn't want this -
raise ImageBoundaryError(min_indices, max_indices,
min_bounded, max_bounded)
slices = [slice(int(min_i), int(max_i))
for min_i, max_i in
zip(list(min_bounded), list(max_bounded))]
self.pixels = self.pixels[
[slice(0, self.n_channels, None)] + slices].copy()
# update all our landmarks
lm_translation = Translation(-min_bounded)
lm_translation.apply_inplace(self.landmarks)
return self

def crop(self, min_indices, max_indices,
constrain_to_boundary=False):
r"""
Return a cropped copy of this image using the given minimum and
maximum indices. Landmarks are correctly adjusted so they maintain
their position relative to the newly cropped image.
Parameters
----------
min_indices : ``(n_dims,)`` `ndarray`
The minimum index over each dimension.
max_indices : ``(n_dims,)`` `ndarray`
The maximum index over each dimension.
constrain_to_boundary : `bool`, optional
If ``True`` the crop will be snapped to not go beyond this images
boundary. If ``False``, an :map:`ImageBoundaryError` will be raised
if an attempt is made to go beyond the edge of the image.
Returns
-------
cropped_image : `type(self)`
A new instance of self, but cropped.

Raises
------
ValueError
``min_indices`` and ``max_indices`` both have to be of length
``n_dims``. All ``max_indices`` must be greater than
``min_indices``.
ImageBoundaryError
Raised if ``constrain_to_boundary=False``, and an attempt is made
to crop the image in a way that violates the image bounds.
"""
cropped_image = self.copy()
return cropped_image.crop_inplace(
min_indices, max_indices,
constrain_to_boundary=constrain_to_boundary)
new_shape = max_bounded - min_bounded
return self.warp_to_shape(new_shape, Translation(min_bounded),
order=0, warp_landmarks=True)

def crop_to_landmarks_inplace(self, group=None, label=None, boundary=0,
constrain_to_boundary=True):
def crop_to_landmarks(self, group=None, label=None, boundary=0,
constrain_to_boundary=True):
r"""
Crop this image to be bounded around a set of landmarks with an
optional ``n_pixel`` boundary
Return a copy of this image cropped so that it is bounded around a set
of landmarks with an optional ``n_pixel`` boundary
Parameters
----------
Expand All @@ -989,7 +946,7 @@ def crop_to_landmarks_inplace(self, group=None, label=None, boundary=0,
Returns
-------
image : :map:`Image`
This image, cropped to its landmarks.
A copy of this image cropped to its landmarks.
Raises
------
Expand All @@ -999,13 +956,12 @@ def crop_to_landmarks_inplace(self, group=None, label=None, boundary=0,
"""
pc = self.landmarks[group][label]
min_indices, max_indices = pc.bounds(boundary=boundary)
return self.crop_inplace(min_indices, max_indices,
constrain_to_boundary=constrain_to_boundary)
return self.crop(min_indices, max_indices,
constrain_to_boundary=constrain_to_boundary)

def crop_to_landmarks_proportion_inplace(self, boundary_proportion,
group=None, label=None,
minimum=True,
constrain_to_boundary=True):
def crop_to_landmarks_proportion(self, boundary_proportion,
group=None, label=None, minimum=True,
constrain_to_boundary=True):
r"""
Crop this image to be bounded around a set of landmarks with a
border proportional to the landmark spread or range.
Expand Down Expand Up @@ -1049,10 +1005,51 @@ def crop_to_landmarks_proportion_inplace(self, boundary_proportion,
boundary = boundary_proportion * np.min(pc.range())
else:
boundary = boundary_proportion * np.max(pc.range())
return self.crop_to_landmarks_inplace(
return self.crop_to_landmarks(
group=group, label=label, boundary=boundary,
constrain_to_boundary=constrain_to_boundary)

def _propagate_crop_to_inplace(self, cropped):
# helper method that sets self's state to the result of a crop call.
# only needed for the deprecation period of the inplace crop methods.
self.pixels = cropped.pixels
self.landmarks = cropped.landmarks
if hasattr(self, 'mask'):
self.mask = cropped.mask
return self

def crop_inplace(self, *args, **kwargs):
r"""
Deprecated: please use :map:`crop` instead.
"""
warn('crop_inplace() is deprecated and will be removed in the next '
'major version of menpo. '
'Please use crop() instead.', MenpoDeprecationWarning)
cropped = self.crop(*args, **kwargs)
return self._propagate_crop_to_inplace(cropped)

def crop_to_landmarks_inplace(self, *args, **kwargs):
r"""
Deprecated: please use :map:`crop_to_landmarks` instead.
"""
warn('crop_to_landmarks_inplace() is deprecated and will be removed in'
' the next major version of menpo. '
'Please use crop_to_landmarks() instead.',
MenpoDeprecationWarning)
cropped = self.crop_to_landmarks(*args, **kwargs)
return self._propagate_crop_to_inplace(cropped)

def crop_to_landmarks_proportion_inplace(self, *args, **kwargs):
r"""
Deprecated: please use :map:`crop_to_landmarks_proportion` instead.
"""
warn('crop_to_landmarks_proportion_inplace() is deprecated and will be'
' removed in the next major version of menpo. Please use '
'crop_to_landmarks_proportion() instead.',
MenpoDeprecationWarning)
cropped = self.crop_to_landmarks_proportion(*args, **kwargs)
return self._propagate_crop_to_inplace(cropped)

def constrain_points_to_bounds(self, points):
r"""
Constrains the points provided to be within the bounds of this image.
Expand Down Expand Up @@ -1366,10 +1363,33 @@ def warp_to_shape(self, template_shape, transform, warp_landmarks=False,
warped_image : `type(self)`
A copy of this image, warped.
"""
template_shape = np.array(template_shape, dtype=np.int)
if (isinstance(transform, Affine) and order in range(4) and
self.n_dims == 2):
# skimage has an optimised Cython interpolation for 2D affine
# warps

# we are going to be able to go fast.

if isinstance(transform, Translation) and order == 0:
# an integer translation (e.g. a crop) If this lies entirely
# in the bounds then we can just do a copy. We need to match
# the behavior of cython_interpolation exactly, which means
# matching its rounding behavior too:
t = transform.translation_component.copy()
pos_t = t > 0.0
t[pos_t] += 0.5
t[~pos_t] -= 0.5
min_ = t.astype(np.int)
max_ = template_shape + min_
if np.all(max_ <= np.array(self.shape)) and np.all(min_ >= 0):
# we have a crop - slice the pixels.
warped_pixels = self.pixels[:,
int(min_[0]):int(max_[0]),
int(min_[1]):int(max_[1])].copy()
return self._build_warp_to_shape(warped_pixels,
transform,
warp_landmarks)
# we couldn't do the crop, but skimage has an optimised Cython
# interpolation for 2D affine warps - let's use that
sampled = cython_interpolation(self.pixels, template_shape,
transform, order=order,
mode=mode, cval=cval)
Expand All @@ -1379,11 +1399,19 @@ def warp_to_shape(self, template_shape, transform, warp_landmarks=False,
batch_size=batch_size)
sampled = self.sample(points_to_sample,
order=order, mode=mode, cval=cval)

# set any nan values to 0
sampled[np.isnan(sampled)] = 0
# build a warped version of the image
warped_pixels = sampled.reshape((self.n_channels,) + template_shape)
warped_pixels = sampled.reshape(
(self.n_channels,) + tuple(template_shape))

return self._build_warp_to_shape(warped_pixels, transform,
warp_landmarks)

def _build_warp_to_shape(self, warped_pixels, transform, warp_landmarks):
# factored out common logic from the different paths we can take in
# warp_to_shape. Rebuilds an image post-warp, adjusting landmarks
# as necessary.
warped_image = Image(warped_pixels, copy=False)

# warp landmarks if requested.
Expand Down
56 changes: 8 additions & 48 deletions menpo/image/masked.py
Expand Up @@ -671,48 +671,6 @@ def _view_landmarks_2d(self, channels=None, masked=True, group=None,
axes_font_size, axes_font_style, axes_font_weight, axes_x_limits,
axes_y_limits, figure_size)

def crop_inplace(self, min_indices, max_indices,
constrain_to_boundary=True):
r"""
Crops this image using the given minimum and maximum indices.
Landmarks are correctly adjusted so they maintain their position
relative to the newly cropped image.
Parameters
----------
min_indices: ``(n_dims, )`` `ndarray`
The minimum index over each dimension.
max_indices: ``(n_dims, )`` `ndarray`
The maximum index over each dimension.
constrain_to_boundary : `bool`, optional
If ``True`` the crop will be snapped to not go beyond this images
boundary. If ``False``, an :map:`ImageBoundaryError` will be raised
if an attempt is made to go beyond the edge of the image.
Returns
-------
cropped_image : `type(self)`
This image, but cropped.
Raises
------
ValueError
``min_indices`` and ``max_indices`` both have to be of length
``n_dims``. All ``max_indices`` must be greater than
``min_indices``.
:map`ImageBoundaryError`
Raised if ``constrain_to_boundary=False``, and an attempt is made
to crop the image in a way that violates the image bounds.
"""
# crop our image
super(MaskedImage, self).crop_inplace(
min_indices, max_indices,
constrain_to_boundary=constrain_to_boundary)
# crop our mask
self.mask.crop_inplace(min_indices, max_indices,
constrain_to_boundary=constrain_to_boundary)
return self

def crop_to_true_mask(self, boundary=0, constrain_to_boundary=True):
r"""
Crop this image to be bounded just the `True` values of it's mask.
Expand All @@ -728,6 +686,11 @@ def crop_to_true_mask(self, boundary=0, constrain_to_boundary=True):
if an attempt is made to go beyond the edge of the image. Note that
is only possible if ``boundary != 0``.
Returns
-------
cropped_image : ``type(self)``
A copy of this image, cropped to the true mask.
Raises
------
ImageBoundaryError
Expand All @@ -737,8 +700,8 @@ def crop_to_true_mask(self, boundary=0, constrain_to_boundary=True):
min_indices, max_indices = self.mask.bounds_true(
boundary=boundary, constrain_to_bounds=False)
# no point doing the bounds check twice - let the crop do it only.
self.crop_inplace(min_indices, max_indices,
constrain_to_boundary=constrain_to_boundary)
return self.crop(min_indices, max_indices,
constrain_to_boundary=constrain_to_boundary)

def sample(self, points_to_sample, order=1, mode='constant', cval=0.0):
r"""
Expand Down Expand Up @@ -911,10 +874,7 @@ def warp_to_shape(self, template_shape, transform, warp_landmarks=False,
mode=mode, cval=cval)
# efficiently turn the Image into a MaskedImage, attaching the
# landmarks
masked_warped_image = MaskedImage(warped_image.pixels, mask=mask,
copy=False)
if warped_image.has_landmarks:
masked_warped_image.landmarks = warped_image.landmarks
masked_warped_image = warped_image.as_masked(mask=mask, copy=False)
if hasattr(warped_image, 'path'):
masked_warped_image.path = warped_image.path
return masked_warped_image
Expand Down
2 changes: 1 addition & 1 deletion menpo/transform/test/pwa_test.py
Expand Up @@ -4,7 +4,7 @@
PythonPWA)

b = menpo.io.import_builtin_asset('breakingbad.jpg').as_masked()
b.crop_to_landmarks_proportion_inplace(0.1)
b = b.crop_to_landmarks_proportion(0.1)
b = b.rescale_landmarks_to_diagonal_range(120)
b.constrain_mask_to_landmarks()
points = b.mask.true_indices()
Expand Down

0 comments on commit 27408c8

Please sign in to comment.