import numpy as np
from scipy.ndimage import gaussian_filter

from menpo.base import name_of_callable
from menpo.model import PCAModel
from menpo.feature import no_op, ndfeature
from menpo.transform import Scale
from menpo.visualize import print_dynamic

from menpofit import checks
from menpofit.builder import (build_reference_frame, compute_reference_shape,
                              rescale_images_to_reference_shape,
                              compute_features, scale_images, warp_images)
from menpofit.aam.algorithm.lk import LucasKanadeStandardInterface
from menpofit.clm import CorrelationFilterExpertEnsemble
from menpofit.clm.expert.ensemble import ConvolutionBasedExpertEnsemble
from menpofit.modelinstance import OrthoPDM
from menpofit.transform import DifferentiablePiecewiseAffine, OrthoMDTransform


@ndfeature
def fsmooth(pixels, sigma, mode='constant'):
    return gaussian_filter(pixels, sigma, mode=mode)


class UnifiedAAMCLM(object):
    r"""
    Class for training a multi-scale unified holistic AAM and CLM as 
    presented in [1].
    Please see the references for AAMs and CLMs in their respective
    base classes.

    Parameters
    ----------
    images : `list` of `menpo.image.Image`
        The `list` of training images.
    group : `str` or ``None``, optional
        The landmark group that will be used to train the model. If ``None`` and
        the images only have a single landmark group, then that is the one
        that will be used. Note that all the training images need to have the
        specified landmark group.
    holistic_features : `closure` or `list` of `closure`, optional
        The features that will be extracted from the training images. Note
        that the features are extracted before warping the images to the
        reference shape. If `list`, then it must define a feature function per
        scale. Please refer to `menpo.feature` for a list of potential features.
    reference_shape : `menpo.shape.PointCloud` or ``None``, optional
        The reference shape that will be used for building the AAM. The purpose
        of the reference shape is to normalise the size of the training images.
        The normalization is performed by rescaling all the training images
        so that the scale of their ground truth shapes matches the scale of
        the reference shape. Note that the reference shape is rescaled with
        respect to the `diagonal` before performing the normalisation. If
        ``None``, then the mean shape will be used.
    diagonal : `int` or ``None``, optional
        This parameter is used to rescale the reference shape so that the
        diagonal of its bounding box matches the provided value. In other
        words, this parameter controls the size of the model at the highest
        scale. If ``None``, then the reference shape does not get rescaled.
    scales : `float` or `tuple` of `float`, optional
        The scale value of each scale. They must provided in ascending order,
        i.e. from lowest to highest scale. If `float`, then a single scale is
        assumed.
    expert_ensemble_cls : `subclass` of :map:`ExpertEnsemble`, optional
        The class to be used for training the ensemble of experts. The most
        common choice is :map:`CorrelationFilterExpertEnsemble`.
    patch_shape : (`int`, `int`) or `list` of (`int`, `int`), optional
        The shape of the patches to be extracted. If a `list` is provided,
        then it defines a patch shape per scale.
    context_shape : (`int`, `int`) or `list` of (`int`, `int`), optional
        The context shape for the convolution. If a `list` is provided,
        then it defines a context shape per scale.
    sample_offsets : ``(n_offsets, n_dims)`` `ndarray` or ``None``, optional
        The sample_offsets to sample from within a patch. So ``(0, 0)`` is the
        centre of the patch (no offset) and ``(1, 0)`` would be sampling the
        patch from 1 pixel up the first axis away from the centre. If ``None``,
        then no sample_offsets are applied.
    transform : `subclass` of :map:`DL` and :map:`DX`, optional
        A differential warp transform object, e.g.
        :map:`DifferentiablePiecewiseAffine` or
        :map:`DifferentiableThinPlateSplines`.
    shape_model_cls : `subclass` of :map:`OrthoPDM`, optional
        The class to be used for building the shape model. The most common
        choice is :map:`OrthoPDM`.
    max_shape_components : `int`, `float`, `list` of those or ``None``, optional
        The number of shape components to keep. If `int`, then it sets the exact
        number of components. If `float`, then it defines the variance
        percentage that will be kept. If `list`, then it should
        define a value per scale. If a single number, then this will be
        applied to all scales. If ``None``, then all the components are kept.
        Note that the unused components will be permanently trimmed.
    max_appearance_components : `int`, `float`, `list` of those or ``None``, optional
        The number of appearance components to keep. If `int`, then it sets the
        exact number of components. If `float`, then it defines the variance
        percentage that will be kept. If `list`, then it should define a value
        per scale. If a single number, then this will be applied to all
        scales. If ``None``, then all the components are kept. Note that the
        unused components will be permanently trimmed.
    sigma : `float` or ``None``, optional
        If not ``None``, the input images are smoothed with an isotropic
        Gaussian filter with the specified standard deviation.
    boundary : `int`, optional
        The number of pixels to be left as a safe margin on the boundaries
        of the reference frame (has potential effects on the gradient
        computation).
    response_covariance : `int`, optional
        The covariance of the generated Gaussian response.
    patch_normalisation : `callable`, optional
        The normalisation function to be applied on the extracted patches.
    cosine_mask : `bool`, optional
        If ``True``, then a cosine mask (Hanning function) will be applied on
        the extracted patches.    
    verbose : `bool`, optional
        If ``True``, then the progress of building the model will be printed.

    References
    ----------
    .. [1] J. Alabort-i-Medina, and S. Zafeiriou. "Unifying holistic and 
        parts-based deformable model fitting", Proceedings of the IEEE
        Conference on Computer Vision and Pattern Recognition (CVPR), 2015.
    """
    def __init__(self, images, group=None, holistic_features=no_op,
                 reference_shape=None, diagonal=None, scales=(0.5, 1.0),
                 expert_ensemble_cls=CorrelationFilterExpertEnsemble,
                 patch_shape=(17, 17), context_shape=(34, 34),
                 sample_offsets=None,  transform=DifferentiablePiecewiseAffine,
                 shape_model_cls=OrthoPDM, max_shape_components=None,
                 max_appearance_components=None, sigma=None, boundary=3,
                 response_covariance=2, patch_normalisation=no_op,
                 cosine_mask=True, verbose=False):
        # Check parameters
        checks.check_diagonal(diagonal)
        scales = checks.check_scales(scales)
        n_scales = len(scales)
        holistic_features = checks.check_callable(holistic_features, n_scales)
        shape_model_cls = checks.check_callable(shape_model_cls, n_scales)
        max_shape_components = checks.check_max_components(
            max_shape_components, n_scales, 'max_shape_components')
        max_appearance_components = checks.check_max_components(
            max_appearance_components, n_scales, 'max_appearance_components')
        # Assign attributes
        self.expert_ensemble_cls = checks.check_callable(expert_ensemble_cls,
                                                         n_scales)
        self.expert_ensembles = []
        self.patch_shape = checks.check_patch_shape(patch_shape, n_scales)
        self.context_shape = checks.check_patch_shape(context_shape, n_scales)
        self.holistic_features = holistic_features
        self.transform = transform
        self.diagonal = diagonal
        self.scales = scales
        self.max_shape_components = max_shape_components
        self.max_appearance_components = max_appearance_components
        self.reference_shape = reference_shape
        self.shape_model_cls = shape_model_cls
        self.sigma = sigma
        self.boundary = boundary
        self.sample_offsets = sample_offsets
        self.response_covariance = response_covariance
        self.patch_normalisation = patch_normalisation
        self.cosine_mask = cosine_mask
        self.shape_models = []
        self.appearance_models = []
        self.expert_ensembles = []
        
        self._train(images=images, group=group, verbose=verbose)

    def _build_reference_frame(self, mean_shape):
        return build_reference_frame(mean_shape, boundary=self.boundary)

    def _warp_images(self, images, shapes, reference_shape, scale_index,
                     prefix, verbose):
        reference_frame = build_reference_frame(reference_shape)
        return warp_images(images, shapes, reference_frame, self.transform,
                           prefix=prefix, verbose=verbose)
  
    def _train(self, images, group=None, verbose=False):
        checks.check_landmark_trilist(images[0], self.transform, group=group)
        self.reference_shape = compute_reference_shape(
            [i.landmarks[group] for i in images],
            self.diagonal, verbose=verbose)
        
        # normalize images
        images = rescale_images_to_reference_shape(
            images, group, self.reference_shape, verbose=verbose)
        if self.sigma:
            images = [fsmooth(i, self.sigma) for i in images]

        # Build models at each scale
        if verbose:
            print_dynamic('- Building models\n')

        feature_images = []
        # for each scale (low --> high)
        for j in range(self.n_scales):
            if verbose:
                if len(self.scales) > 1:
                    scale_prefix = '  - Scale {}: '.format(j)
                else:
                    scale_prefix = '  - '
            else:
                scale_prefix = None

            # Handle holistic features
            if j == 0 and self.holistic_features[j] == no_op:
                # Saves a lot of memory
                feature_images = images
            elif j == 0 or self.holistic_features[j] is not self.holistic_features[j - 1]:
                # Compute features only if this is the first pass through
                # the loop or the features at this scale are different from
                # the features at the previous scale
                feature_images = compute_features(images,
                                                  self.holistic_features[j],
                                                  prefix=scale_prefix,
                                                  verbose=verbose)
            # handle scales
            if self.scales[j] != 1:
                # Scale feature images only if scale is different than 1
                scaled_images = scale_images(feature_images, self.scales[j],
                                             prefix=scale_prefix,
                                             verbose=verbose)
            else:
                scaled_images = feature_images

            # Extract potentially rescaled shapes
            scale_shapes = [i.landmarks[group] for i in scaled_images]

            # Build the shape model
            if verbose:
                print_dynamic('{}Building shape model'.format(scale_prefix))

            shape_model = self._build_shape_model(scale_shapes, j)
            self.shape_models.append(shape_model)

            # Obtain warped images - we use a scaled version of the
            # reference shape, computed here. This is because the mean
            # moves when we are incrementing, and we need a consistent
            # reference frame.
            scaled_reference_shape = Scale(self.scales[j], n_dims=2).apply(
                self.reference_shape)
            warped_images = self._warp_images(scaled_images, scale_shapes,
                                              scaled_reference_shape,
                                              j, scale_prefix, verbose)

            # obtain appearance model
            if verbose:
                print_dynamic('{}Building appearance model'.format(
                    scale_prefix))

            appearance_model = PCAModel(warped_images)
            # trim appearance model if required
            if self.max_appearance_components[j] is not None:
                appearance_model.trim_components(
                    self.max_appearance_components[j])
            # add appearance model to the list
            self.appearance_models.append(appearance_model)

            expert_ensemble = self.expert_ensemble_cls[j](
                images=scaled_images, shapes=scale_shapes,
                patch_shape=self.patch_shape[j],
                patch_normalisation=self.patch_normalisation,
                cosine_mask=self.cosine_mask,
                context_shape=self.context_shape[j],
                sample_offsets=self.sample_offsets,
                prefix=scale_prefix, verbose=verbose)
            self.expert_ensembles.append(expert_ensemble)

            if verbose:
                print_dynamic('{}Done\n'.format(scale_prefix))

    def _build_shape_model(self, shapes, scale_index):
        return self.shape_model_cls[scale_index](
            shapes, max_n_components=self.max_shape_components[scale_index])

    @property
    def n_scales(self):
        """
        Returns the number of scales.

        :type: `int`
        """
        return len(self.scales)

    @property
    def _str_title(self):
        return 'Unified Holistic AAM and CLM'

    def shape_instance(self, shape_weights=None, scale_index=-1):
        r"""
        Generates a novel shape instance given a set of shape weights. If no
        weights are provided, the mean shape is returned.

        Parameters
        ----------
        shape_weights : ``(n_weights,)`` `ndarray` or `list` or ``None``, optional
            The weights of the shape model that will be used to create a novel
            shape instance. If ``None``, the weights are assumed to be zero,
            thus the mean shape is used.
        scale_index : `int`, optional
            The scale to be used.

        Returns
        -------
        instance : `menpo.shape.PointCloud`
            The shape instance.
        """
        if shape_weights is None:
            shape_weights = [0]
        sm = self.shape_models[scale_index].model
        return sm.instance(shape_weights, normalized_weights=True)

    def instance(self, shape_weights=None, appearance_weights=None,
                 scale_index=-1):
        r"""
        Generates a novel instance of the AAM part of the model given a set of
        shape and appearance weights. If no weights are provided, then the mean
        AAM instance is returned.

        Parameters
        ----------
        shape_weights : ``(n_weights,)`` `ndarray` or `list` or ``None``, optional
            The weights of the shape model that will be used to create a novel
            shape instance. If ``None``, the weights are assumed to be zero,
            thus the mean shape is used.
        appearance_weights : ``(n_weights,)`` `ndarray` or `list` or ``None``, optional
            The weights of the appearance model that will be used to create a
            novel appearance instance. If ``None``, the weights are assumed
            to be zero, thus the mean appearance is used.
        scale_index : `int`, optional
            The scale to be used.

        Returns
        -------
        image : `menpo.image.Image`
            The AAM instance.
        """
        if shape_weights is None:
            shape_weights = [0]
        if appearance_weights is None:
            appearance_weights = [0]
        sm = self.shape_models[scale_index].model
        am = self.appearance_models[scale_index]

        shape_instance = sm.instance(shape_weights, normalized_weights=True)
        appearance_instance = am.instance(appearance_weights,
                                          normalized_weights=True)

        return self._instance(scale_index, shape_instance, appearance_instance)

    def random_instance(self, scale_index=-1):
        r"""
        Generates a random instance of the AAM part of the model.

        Parameters
        ----------
        scale_index : `int`, optional
            The scale to be used.

        Returns
        -------
        image : `menpo.image.Image`
            The AAM instance.
        """
        sm = self.shape_models[scale_index].model
        am = self.appearance_models[scale_index]

        # TODO: this bit of logic should to be transferred down to PCAModel
        shape_weights = np.random.randn(sm.n_active_components)
        shape_instance = sm.instance(shape_weights, normalized_weights=True)
        appearance_weights = np.random.randn(sm.n_active_components)
        appearance_instance = am.instance(appearance_weights,
                                          normalized_weights=True)

        return self._instance(scale_index, shape_instance, appearance_instance)

    def _instance(self, scale_index, shape_instance, appearance_instance):
        template = self.appearance_models[scale_index].mean()
        landmarks = template.landmarks['source']

        reference_frame = build_reference_frame(shape_instance)

        transform = self.transform(
            reference_frame.landmarks['source'], landmarks)

        return appearance_instance.as_unmasked(copy=False).warp_to_mask(
            reference_frame.mask, transform, warp_landmarks=True)

    def view_shape_models_widget(self, n_parameters=5,
                                 parameters_bounds=(-3.0, 3.0),
                                 mode='multiple', figure_size=(10, 8)):
        r"""
        Visualizes the shape models of the Unified AAM CLM object using an
        interactive widget.

        Parameters
        ----------
        n_parameters : `int` or `list` of `int` or ``None``, optional
            The number of shape principal components to be used for the
            parameters sliders. If `int`, then the number of sliders per
            scale is the minimum between `n_parameters` and the number of
            active components per scale. If `list` of `int`, then a number of
            sliders is defined per scale. If ``None``, all the active
            components per scale will have a slider.
        parameters_bounds : ``(float, float)``, optional
            The minimum and maximum bounds, in std units, for the sliders.
        mode : {``single``, ``multiple``}, optional
            If ``'single'``, only a single slider is constructed along with a
            drop down menu. If ``'multiple'``, a slider is constructed for
            each parameter.
        figure_size : (`int`, `int`), optional
            The size of the rendered figure.
        """
        try:
            from menpowidgets import visualize_shape_model
            visualize_shape_model(
                [sm.model for sm in self.shape_models],
                n_parameters=n_parameters, parameters_bounds=parameters_bounds,
                figure_size=figure_size, mode=mode)
        except ImportError:
            from menpo.visualize.base import MenpowidgetsMissingError
            raise MenpowidgetsMissingError()

    def view_appearance_models_widget(self, n_parameters=5,
                                      parameters_bounds=(-3.0, 3.0),
                                      mode='multiple', figure_size=(10, 8)):
        r"""
        Visualizes the appearance models of the Unified AAM CLM object using an
        interactive widget.

        Parameters
        ----------
        n_parameters : `int` or `list` of `int` or ``None``, optional
            The number of appearance principal components to be used for the
            parameters sliders. If `int`, then the number of sliders per
            scale is the minimum between `n_parameters` and the number of
            active components per scale. If `list` of `int`, then a number of
            sliders is defined per scale. If ``None``, all the active
            components per scale will have a slider.
        parameters_bounds : ``(float, float)``, optional
            The minimum and maximum bounds, in std units, for the sliders.
        mode : {``single``, ``multiple``}, optional
            If ``'single'``, only a single slider is constructed along with a
            drop down menu. If ``'multiple'``, a slider is constructed for
            each parameter.
        figure_size : (`int`, `int`), optional
            The size of the rendered figure.
        """
        try:
            from menpowidgets import visualize_appearance_model
            visualize_appearance_model(self.appearance_models,
                                       n_parameters=n_parameters,
                                       parameters_bounds=parameters_bounds,
                                       figure_size=figure_size, mode=mode)
        except ImportError:
            from menpo.visualize.base import MenpowidgetsMissingError
            raise MenpowidgetsMissingError()

    def view_expert_ensemble_widget(self, figure_size=(10, 8)):
        r"""
        Visualizes the ensemble of experts of the Unified AAM CLM object using
        an interactive widget.

        Parameters
        ----------
        figure_size : (`int`, `int`), optional
            The size of the plotted figures.

        Raises
        ------
        ValueError
            Only convolution-based expert ensembles can be visualized.
        """
        if not isinstance(self.expert_ensembles[0],
                          ConvolutionBasedExpertEnsemble):
            raise ValueError('Only convolution-based expert ensembles can be '
                             'visualized.')
        try:
            from menpowidgets import visualize_expert_ensemble
            centers = [sp.model.mean() for sp in self.shape_models]
            visualize_expert_ensemble(self.expert_ensembles, centers,
                                      figure_size=figure_size)
        except:
            from menpo.visualize.base import MenpowidgetsMissingError
            raise MenpowidgetsMissingError()

    def view_clm_widget(self, n_shape_parameters=5,
                        parameters_bounds=(-3.0, 3.0), mode='multiple',
                        figure_size=(10, 8)):
        r"""
        Visualizes the CLM part of the model using an interactive widget.

        Parameters
        ----------
        n_shape_parameters : `int` or `list` of `int` or ``None``, optional
            The number of shape principal components to be used for the
            parameters sliders. If `int`, then the number of sliders per
            scale is the minimum between `n_parameters` and the number of
            active components per scale. If `list` of `int`, then a number of
            sliders is defined per scale. If ``None``, all the active
            components per scale will have a slider.
        parameters_bounds : ``(float, float)``, optional
            The minimum and maximum bounds, in std units, for the sliders.
        mode : {``single``, ``multiple``}, optional
            If ``'single'``, only a single slider is constructed along with a
            drop down menu. If ``'multiple'``, a slider is constructed for
            each parameter.
        figure_size : (`int`, `int`), optional
            The size of the rendered figure.

        Raises
        ------
        ValueError
            Only convolution-based expert ensembles can be visualized.
        """
        if not isinstance(self.expert_ensembles[0],
                          ConvolutionBasedExpertEnsemble):
            raise ValueError('Only convolution-based expert ensembles can be '
                             'visualized.')
        try:
            from menpowidgets import visualize_clm
            visualize_clm(self, n_shape_parameters=n_shape_parameters,
                          parameters_bounds=parameters_bounds,
                          figure_size=figure_size, mode=mode)
        except:
            from menpo.visualize.base import MenpowidgetsMissingError
            raise MenpowidgetsMissingError()

    def view_aam_widget(self, n_shape_parameters=5, n_appearance_parameters=5,
                        parameters_bounds=(-3.0, 3.0), mode='multiple',
                        figure_size=(10, 8)):
        r"""
        Visualizes the AAM part of the model using an interactive widget.

        Parameters
        ----------
        n_shape_parameters : `int` or `list` of `int` or ``None``, optional
            The number of shape principal components to be used for the
            parameters sliders. If `int`, then the number of sliders per
            scale is the minimum between `n_parameters` and the number of
            active components per scale. If `list` of `int`, then a number of
            sliders is defined per scale. If ``None``, all the active
            components per scale will have a slider.
        n_appearance_parameters : `int` or `list` of `int` or ``None``, optional
            The number of appearance principal components to be used for the
            parameters sliders. If `int`, then the number of sliders per
            scale is the minimum between `n_parameters` and the number of
            active components per scale. If `list` of `int`, then a number of
            sliders is defined per scale. If ``None``, all the active
            components per scale will have a slider.
        parameters_bounds : ``(float, float)``, optional
            The minimum and maximum bounds, in std units, for the sliders.
        mode : {``single``, ``multiple``}, optional
            If ``'single'``, only a single slider is constructed along with a
            drop down menu. If ``'multiple'``, a slider is constructed for
            each parameter.
        figure_size : (`int`, `int`), optional
            The size of the rendered figure.
        """
        try:
            from menpowidgets import visualize_aam
            visualize_aam(self, n_shape_parameters=n_shape_parameters,
                          n_appearance_parameters=n_appearance_parameters,
                          parameters_bounds=parameters_bounds,
                          figure_size=figure_size, mode=mode)
        except ImportError:
            from menpo.visualize.base import MenpowidgetsMissingError
            raise MenpowidgetsMissingError()

    def build_fitter_interfaces(self, sampling):
        r"""
        Method that builds the correct fitting interface for a
        :map:`UnifiedAAMCLMFitter`.

        Parameters
        ----------
        sampling : `list` of `int` or `ndarray` or ``None``
            It defines a sampling mask per scale. If `int`, then it
            defines the sub-sampling step of the sampling mask. If `ndarray`,
            then it explicitly defines the sampling mask. If ``None``, then no
            sub-sampling is applied.

        Returns
        -------
        fitter_interfaces : `list`
            The `list` of fitting interfaces per scale.
        """
        interfaces = []
        for am, sm, s in zip(self.appearance_models, self.shape_models,
                             sampling):
            template = am.mean()
            md_transform = OrthoMDTransform(
                sm, self.transform,
                source=template.landmarks['source'])

            interface = LucasKanadeStandardInterface(
                am, md_transform, template, sampling=s)
            interfaces.append(interface)
        return interfaces

    def appearance_reconstructions(self, appearance_parameters,
                                   n_iters_per_scale):
        r"""
        Method that generates the appearance reconstructions given a set of
        appearance parameters. This is to be combined with a
        :map:`UnifiedAAMCLMResult` object, in order to generate the appearance
        reconstructions of a fitting procedure.

        Parameters
        ----------
        appearance_parameters : `list` of ``(n_params,)`` `ndarray`
            A set of appearance parameters per fitting iteration. It can be
            retrieved as a property of an :map:`UnifiedAAMCLMResult` object.
        n_iters_per_scale : `list` of `int`
            The number of iterations per scale. This is necessary in order to
            figure out which appearance parameters correspond to the model of
            each scale. It can be retrieved as a property of a
            :map:`UnifiedAAMCLMResult` object.

        Returns
        -------
        appearance_reconstructions : `list` of `menpo.image.Image`
            `List` of the appearance reconstructions that correspond to the
            provided parameters.
        """
        appearance_reconstructions = []
        previous = 0
        for scale, n_iters in enumerate(n_iters_per_scale):
            for c in appearance_parameters[previous:previous+n_iters+1]:
                instance = self.appearance_models[scale].instance(c)
                appearance_reconstructions.append(instance)
            previous = n_iters + 1
        return appearance_reconstructions

    def __str__(self):
        if self.diagonal is not None:
            diagonal = self.diagonal
        else:
            y, x = self.reference_shape.range()
            diagonal = np.sqrt(x ** 2 + y ** 2)

        # Compute scale info strings
        scales_info = []
        lvl_str_tmplt = r"""   - Scale {}
     - Holistic feature: {}
     - Ensemble of experts class: {}
       - {} experts
       - {} class
       - Patch shape: {} x {}
       - Patch normalisation: {}
       - Context shape: {} x {}
       - Cosine mask: {}
     - Appearance model class: {}
       - {} appearance components
     - Shape model class: {}
       - {} shape components
       - {} similarity transform parameters"""
        for k, s in enumerate(self.scales):
            scales_info.append(lvl_str_tmplt.format(
                s, name_of_callable(self.holistic_features[k]),
                name_of_callable(self.expert_ensemble_cls[k]),
                self.expert_ensembles[k].n_experts,
                name_of_callable(self.expert_ensembles[k]._icf),
                self.expert_ensembles[k].patch_shape[0],
                self.expert_ensembles[k].patch_shape[1],
                name_of_callable(self.expert_ensembles[k].patch_normalisation),
                self.expert_ensembles[k].context_shape[0],
                self.expert_ensembles[k].context_shape[1],
                self.expert_ensembles[k].cosine_mask,
                name_of_callable(self.appearance_models[k]),
                self.appearance_models[k].n_components,
                name_of_callable(self.shape_models[k]),
                self.shape_models[k].model.n_components,
                self.shape_models[k].n_global_parameters))

        scales_info = '\n'.join(scales_info)

        if self.transform is not None:
            transform_str = 'Images warped with {} transform'.format(
                name_of_callable(self.transform))
        else:
            transform_str = 'No image warping performed'

        cls_str = r"""{class_title}
  - Images scaled to diagonal: {diagonal:.2f}
  - {transform}
  - Scales: {scales}
{scales_info}
    """.format(class_title=self._str_title,
               diagonal=diagonal,
               transform=transform_str,
               scales=self.scales,
               scales_info=scales_info)
        return cls_str
