Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialize fit results #445

Merged
merged 7 commits into from Sep 16, 2014
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
125 changes: 94 additions & 31 deletions menpo/fit/fittingresult.py
@@ -1,5 +1,6 @@
from __future__ import division
import abc
from hdf5able import HDF5able

from menpo.shape.pointcloud import PointCloud
from menpo.image import Image
Expand All @@ -9,25 +10,21 @@

class FittingResult(Viewable):
r"""
Object that holds the state of a :map:`Fitter` object before, during
and after it has fitted a particular image.
Object that holds the state of a single fitting object, during and after it
has fitted a particular image.

Parameters
-----------
image : :map:`Image` or subclass
The fitted image.
fitter : :map:`Fitter`
The fitter object used to fitter the image.
gt_shape: :map:`PointCloud`
gt_shape : :map:`PointCloud`
The ground truth shape associated to the image.
error_type : 'me_norm', 'me' or 'rmse', optional.
Specifies the way in which the error between the fitted and
ground truth shapes is to be computed.
"""
def __init__(self, image, fitter, gt_shape=None):

def __init__(self, image, gt_shape=None):
self.image = image
self.fitter = fitter
self._gt_shape = gt_shape
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this really need to be underscored?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm ok I see we want setters in subclasses.

self.parameters = None
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All fitting results have parameters now, it just defaults to None

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I wonder if it's better if this constructor takes parameters as an argument and sets it on self:

def __init__(self, image, parameters, gt_shape=None):
    self.image = image
    self._gt_shape = gt_shape
    self.parameters = parameters

then it's clear that subclasses have to provide the parametrization on initialization. Or is this a problem with MultilevelFittingResult not having a direct parametrization?


@property
def n_iters(self):
Expand Down Expand Up @@ -95,7 +92,7 @@ def fitted_image(self):
@property
def iter_image(self):
r"""
Returns a copy of the fitted image with a as many landmark groups as
Returns a copy of the fitted image with as many landmark groups as
iteration run by fitting procedure:
- ``iter_0``, containing the initial shape.
- ``iter_1``, containing the the fitted shape at the first
Expand All @@ -107,7 +104,8 @@ def iter_image(self):
"""
image = Image(self.image.pixels)
for j, s in enumerate(self.shapes()):
image.landmarks['iter_'+str(j)] = s
key = 'iter_{}'.format(j)
image.landmarks[key] = s
return image

def errors(self, error_type='me_norm'):
Expand Down Expand Up @@ -185,6 +183,27 @@ def _view(self, figure_id=None, new_figure=False, **kwargs):
return FittingViewer(figure_id, new_figure, self.image.n_dims, pixels,
targets).render(**kwargs)

def as_serializable(self):
r""""
Returns a serializable version of the fitting result. This is a much
lighter weight object than the initial fitting result. For example,
it won't contain the original fitting object.

Returns
-------
serializable_fitting_result : :map:`SerializableFittingResult`
The lightweight serializable version of this fitting result.
"""
if self.parameters is not None:
parameters = [p.copy() for p in self.parameters]
else:
parameters = []
gt_shape = self.gt_shape.copy() if self.gt_shape else None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how the hell have I missed that this is a valid Python construct the last 2 years?! Nice

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(have tried the old ternary if x = a < b ? y : None but never bothered to see if it was there in a more Pythonic guise)

return SerializableFittingResult(self.image.copy(),
parameters,
[s.copy() for s in self.shapes()],
gt_shape)


class NonParametricFittingResult(FittingResult):
r"""
Expand All @@ -199,39 +218,42 @@ class NonParametricFittingResult(FittingResult):
The Fitter object used to fitter the image.
shapes : `list` of :map:`PointCloud`
The list of fitted shapes per iteration of the fitting procedure.
gt_shape: :map:`PointCloud`
gt_shape : :map:`PointCloud`
The ground truth shape associated to the image.
"""

def __init__(self, image, fitter, shapes=None, gt_shape=None):
super(NonParametricFittingResult, self).__init__(
image, fitter, gt_shape=gt_shape)
super(NonParametricFittingResult, self).__init__(image,
gt_shape=gt_shape)
self.fitter = fitter
self._shapes = shapes
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to shapes instead of parameters

# The parameters are the shapes for Non-Parametric algorithms
self.parameters = shapes

def shapes(self, as_points=False):
if as_points:
return [s.points.copy() for s in self.parameters]

return [s.points.copy() for s in self._shapes]
else:
return self.parameters
return self._shapes

@property
def final_shape(self):
return self.parameters[-1].copy()
return self._shapes[-1].copy()

@property
def initial_shape(self):
return self.parameters[0].copy()
return self._shapes[0].copy()

@FittingResult.gt_shape.setter
def gt_shape(self, value):
r"""
Setter for the ground truth shape associated to the image.
"""
if type(value) is PointCloud:
if isinstance(value, PointCloud):
self._gt_shape = value
else:
raise ValueError("Accepted values for gt_shape setter are "
"`menpo.shape.PointClouds`.")
"PointClouds.")


class SemiParametricFittingResult(FittingResult):
Expand All @@ -248,12 +270,13 @@ class SemiParametricFittingResult(FittingResult):
parameters : `list` of `ndarray`
The list of optimal transform parameters per iteration of the fitting
procedure.
gt_shape: :map:`PointCloud`
gt_shape : :map:`PointCloud`
The ground truth shape associated to the image.
"""

def __init__(self, image, fitter, parameters=None, gt_shape=None):
super(SemiParametricFittingResult, self).__init__(
image, fitter, gt_shape=gt_shape)
FittingResult.__init__(self, image, gt_shape=gt_shape)
self.fitter = fitter
self.parameters = parameters

@property
Expand Down Expand Up @@ -307,7 +330,7 @@ def gt_shape(self, value):
self._gt_shape = transform.target
else:
raise ValueError("Accepted values for gt_shape setter are "
"`menpo.shape.PointClouds` or float lists"
"PointClouds or float lists "
"specifying transform shapes.")


Expand All @@ -328,14 +351,13 @@ class ParametricFittingResult(SemiParametricFittingResult):
weights : `list` of `ndarray`
The list of optimal appearance parameters per iteration of the fitting
procedure.
gt_shape: :map:`PointCloud`
gt_shape : :map:`PointCloud`
The ground truth shape associated to the image.
"""
def __init__(self, image, fitter, parameters=None, weights=None,
gt_shape=None):
super(ParametricFittingResult, self).__init__(
image, fitter, gt_shape=gt_shape)
self.parameters = parameters
SemiParametricFittingResult.__init__(self, image, fitter, parameters,
gt_shape=gt_shape)
self.weights = weights

@property
Expand All @@ -351,7 +373,6 @@ def warped_images(self):
return [self.image.warp_to_mask(mask, transform.from_vector(p))
for p in self.parameters]


@property
def appearance_reconstructions(self):
r"""
Expand Down Expand Up @@ -385,3 +406,45 @@ def error_images(self):
error_images.append(error_image)

return error_images


class SerializableFittingResult(HDF5able, FittingResult):
r"""
Designed to allow the fitting results to be easily serializable. In
comparison to the other fitting result objects, the serializable fitting
results contain a much stricter set of data. For example, the major data
components of a serializable fitting result are the fitted shapes, the
parameters and the fitted image.

Parameters
-----------
image : :map:`Image`
The fitted image.
parameters : `list` of `ndarray`
The list of optimal transform parameters per iteration of the fitting
procedure.
shapes : `list` of :map:`PointCloud`
The list of fitted shapes per iteration of the fitting procedure.
gt_shape : :map:`PointCloud`
The ground truth shape associated to the image.
"""
def __init__(self, image, parameters, shapes, gt_shape):
FittingResult.__init__(self, image, gt_shape=gt_shape)
HDF5able.__init__(self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to initialize HDF5able (doesn't harm but adds noise). Maybe we should discuss this in person actually...


self.parameters = parameters
self._shapes = shapes

def shapes(self, as_points=False):
if as_points:
return [s.points.copy() for s in self._shapes]
else:
return self._shapes

@property
def initial_shape(self):
return self._shapes[0]

@property
def final_shape(self):
return self._shapes[-1]
20 changes: 10 additions & 10 deletions menpo/fit/test/fittingresult_test.py
Expand Up @@ -10,9 +10,9 @@

class MockedFittingResult(FittingResult):

def __init__(self, image, fitter, **kwargs):
FittingResult.__init__(self, MaskedImage.blank((10, 10)), fitter,
**kwargs)
def __init__(self, gt_shape=None):
FittingResult.__init__(self, MaskedImage.blank((10, 10)),
gt_shape=gt_shape)
@property
def n_iters(self):
return 1
Expand All @@ -35,46 +35,46 @@ def initial_shape(self):
@attr('fuzzy')
def test_fittingresult_errors_me_norm():
pcloud = PointCloud(np.array([[1., 2], [3, 4], [5, 6]]))
fr = MockedFittingResult(None, None, gt_shape=pcloud)
fr = MockedFittingResult(gt_shape=pcloud)

assert_approx_equal(fr.errors()[0], 0.9173896)


@raises(ValueError)
def test_fittingresult_errors_no_gt():
fr = MockedFittingResult(None, None)
fr = MockedFittingResult()
fr.errors()


def test_fittingresult_gt_shape():
pcloud = PointCloud(np.ones([3, 2]))
fr = MockedFittingResult(None, None, gt_shape=pcloud)
fr = MockedFittingResult(gt_shape=pcloud)
assert (is_same_array(fr.gt_shape.points, pcloud.points))


@attr('fuzzy')
def test_fittingresult_final_error_me_norm():
pcloud = PointCloud(np.array([[1., 2], [3, 4], [5, 6]]))
fr = MockedFittingResult(None, None, gt_shape=pcloud)
fr = MockedFittingResult(gt_shape=pcloud)

assert_approx_equal(fr.final_error(), 0.9173896)


@raises(ValueError)
def test_fittingresult_final_error_no_gt():
fr = MockedFittingResult(None, None)
fr = MockedFittingResult()
fr.final_error()


@attr('fuzzy')
def test_fittingresult_initial_error_me_norm():
pcloud = PointCloud(np.array([[1., 2], [3, 4], [5, 6]]))
fr = MockedFittingResult(None, None, gt_shape=pcloud)
fr = MockedFittingResult(gt_shape=pcloud)

assert_approx_equal(fr.initial_error(), 0.9173896)


@raises(ValueError)
def test_fittingresult_initial_error_no_gt():
fr = MockedFittingResult(None, None)
fr = MockedFittingResult()
fr.initial_error()