<gh_stars>0
import abc
import json
from collections import OrderedDict

import numpy as np

from menpo.io.base import Importer
from menpo.landmark.base import LandmarkGroup
from menpo.shape import PointCloud
from menpo.transform.affine import Scale


class LandmarkImporter(Importer):
    """
    Abstract base class for importing landmarks.

    Parameters
    ----------
    filepath : string
        Absolute filepath of the landmarks.
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(LandmarkImporter, self).__init__(filepath)
        self.group_label = 'default'
        self.pointcloud = None
        self.labels_to_masks = None

    def build(self, asset=None):
        """
        Overrides the :meth:`build <menpo.io.base.Importer.build>` method.

        Parse the landmark format and return the label and landmark dictionary.

        Parameters
        ----------
        asset : object, optional
            The asset that the landmarks are being built for. Can be used to
            adjust landmarks as necessary (e.g. rescaling image landmarks
            from 0-1 to image.shape)

        Returns
        -------
        landmark_group : string
            The landmark group parsed from the file.
            Every point will be labelled.
        """
        self._parse_format(asset=asset)
        return LandmarkGroup(None, self.group_label, self.pointcloud,
                             self.labels_to_masks)

    @abc.abstractmethod
    def _parse_format(self, asset=None):
        r"""
        Read the landmarks file from disk, parse it in to semantic labels and
        :class:`menpo.shape.base.PointCloud`.

        Set the ``self.label`` and ``self.pointcloud`` attributes.
        """
        pass


def _indices_to_mask(n_points, indices):
    """
    Helper function to turn an array of indices in to a boolean mask.

    Parameters
    ----------
    n_points : int
        The total number of points for the mask
    indices : ndarray of ints
        An array of integers representing the ``True`` indices.

    Returns
    -------
    boolean_mask : ndarray of bools
        The mask for the set of landmarks where each index from indices is set
        to ``True`` and the rest are ``False``
    """
    mask = np.zeros(n_points, dtype=np.bool)
    mask[indices] = True
    return mask


class ASFImporter(LandmarkImporter):
    r"""
    Abstract base class for an importer for the ASF file format.
    Currently **does not support the connectivity specified in the format**.

    Implementations of this class should override the :meth:`_build_points`
    which determines the ordering of axes. For example, for images, the
    ``x`` and ``y`` axes are flipped such that the first axis is ``y`` (height
    in the image domain).

    Landmark set label: ASF

    Landmark labels:

    +---------+
    | label   |
    +=========+
    | all     |
    +---------+

    Parameters
    ----------
    filepath : string
        Absolute filepath to landmark file.

    References
    ----------
    .. [1] http://www2.imm.dtu.dk/~aam/datasets/datasets.html
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(ASFImporter, self).__init__(filepath)

    @abc.abstractmethod
    def _build_points(self, xs, ys):
        r"""
        Determines the ordering of points within the landmarks. For meshes
        ``x`` is the first axis, where as for images ``y`` is the first axis.
        """
        pass

    def _parse_format(self, asset=None):
        with open(self.filepath, 'r') as f:
            landmarks = f.read()

        # Remove comments and blank lines
        landmarks = [l for l in landmarks.splitlines()
                     if (l.rstrip() and not '#' in l)]

        # Pop the front of the list for the number of landmarks
        count = int(landmarks.pop(0))
        # Pop the last element of the list for the image_name
        image_name = landmarks.pop()

        xs = np.empty([count, 1])
        ys = np.empty([count, 1])
        connectivity = np.empty([count, 2], dtype=np.int)
        for i in xrange(count):
            # Though unpacked, they are still all strings
            # Only unpack the first 7
            (path_num, path_type, xpos, ypos,
             point_num, connects_from, connects_to) = landmarks[i].split()[:7]
            xs[i, ...] = float(xpos)
            ys[i, ...] = float(ypos)
            connectivity[i, ...] = [int(connects_from), int(connects_to)]

        points = self._build_points(xs, ys)
        if asset is not None:
            # we've been given an asset. As ASF files are normalized,
            # fix that here
            points = Scale(np.array(asset.shape)).apply(points)

        # TODO: Use connectivity and create a graph type instead of PointCloud
        # edges = scaled_points[connectivity]

        self.group_label = 'ASF'
        self.pointcloud = PointCloud(points)
        self.labels_to_masks = {'all': np.ones(points.shape[0], dtype=np.bool)}


class PTSImporter(LandmarkImporter):
    r"""
    Importer for the PTS file format. Assumes version 1 of the format.

    Implementations of this class should override the :meth:`_build_points`
    which determines the ordering of axes. For example, for images, the
    ``x`` and ``y`` axes are flipped such that the first axis is ``y`` (height
    in the image domain).

    Note that PTS has a very loose format definition. Here we make the
    assumption (as is common) that PTS landmarks are 1-based. That is,
    landmarks on a 480x480 image are in the range [1-480]. As Menpo is
    consistently 0-based, we *subtract 1* off each landmark value
    automatically.

    If you want to use PTS landmarks that are 0-based, you will have to
    manually add one back on to landmarks post importing.

    Landmark set label: PTS

    Landmark labels:

    +---------+
    | label   |
    +=========+
    | all     |
    +---------+
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(PTSImporter, self).__init__(filepath)

    @abc.abstractmethod
    def _build_points(self, xs, ys):
        r"""
        Determines the ordering of points within the landmarks. For meshes
        ``x`` is the first axis, where as for images ``y`` is the first axis.
        """
        pass

    def _parse_format(self, asset=None):
        f = open(self.filepath, 'r')
        for line in f:
            if line.split()[0] == '{':
                break
        xs = []
        ys = []
        for line in f:
            if line.split()[0] != '}':
                xpos, ypos = line.split()[0:2]
                xs.append(xpos)
                ys.append(ypos)
        xs = np.array(xs, dtype=np.float).reshape((-1, 1))
        ys = np.array(ys, dtype=np.float).reshape((-1, 1))
        # PTS landmarks are 1-based, need to convert to 0-based (subtract 1)
        points = self._build_points(xs - 1, ys - 1)

        self.group_label = 'PTS'
        self.pointcloud = PointCloud(points)
        self.labels_to_masks = {'all': np.ones(points.shape[0], dtype=np.bool)}


class LM3Importer(LandmarkImporter):
    r"""
    Importer for the LM3 file format from the bosphorus dataset. This is a 3D
    landmark type and so it is assumed it only applies to meshes.

    Landmark set label: LM3

    Landmark labels:

    +------------------------+
    | label                  |
    +========================+
    | outer_left_eyebrow     |
    | middle_left_eyebrow    |
    | inner_left_eyebrow     |
    | inner_right_eyebrow    |
    | middle_right_eyebrow   |
    | outer_right_eyebrow    |
    | outer_left_eye_corner  |
    | inner_left_eye_corner  |
    | inner_right_eye_corner |
    | outer_right_eye_corner |
    | nose_saddle_left       |
    | nose_saddle_right      |
    | left_nose_peak         |
    | nose_tip               |
    | right_nose_peak        |
    | left_mouth_corner      |
    | upper_lip_outer_middle |
    | right_mouth_corner     |
    | upper_lip_inner_middle |
    | lower_lip_inner_middle |
    | lower_lip_outer_middle |
    | chin_middle            |
    +------------------------+
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(LM3Importer, self).__init__(filepath)

    def _parse_format(self, asset=None):
        with open(self.filepath, 'r') as f:
            landmarks = f.read()

        # Remove comments and blank lines
        landmark_text = [l for l in landmarks.splitlines()
                         if (l.rstrip() and not '#' in l)]

        # First line says how many landmarks there are: 22 Landmarks
        # So pop it off the front
        num_points = int(landmark_text.pop(0).split()[0])
        xs = []
        ys = []
        zs = []
        labels = []

        # The lines then alternate between the labels and the coordinates
        for i in xrange(num_points * 2):
            if i % 2 == 0:  # label
                # Lowercase, remove spaces and replace with underscores
                l = landmark_text[i]
                l = '_'.join(l.lower().split())
                labels.append(l)
            else:  # coordinate
                p = landmark_text[i].split()
                xs.append(float(p[0]))
                ys.append(float(p[1]))
                zs.append(float(p[2]))

        xs = np.array(xs, dtype=np.float).reshape((-1, 1))
        ys = np.array(ys, dtype=np.float).reshape((-1, 1))
        zs = np.array(zs, dtype=np.float).reshape((-1, 1))

        self.group_label = 'LM3'
        self.pointcloud = PointCloud(np.hstack([xs, ys, zs]))
        # Create the mask whereby there is one landmark per label
        # (identity matrix)
        masks = np.eye(num_points).astype(np.bool)
        masks = np.vsplit(masks, num_points)
        masks = [np.squeeze(m) for m in masks]
        self.labels_to_masks = dict(zip(labels, masks))


class LM2Importer(LandmarkImporter):
    r"""
    Importer for the LM2 file format from the bosphorus dataset. This is a 2D
    landmark type and so it is assumed it only applies to images.

    Landmark set label: LM2

    Landmark labels:

    +------------------------+
    | label                  |
    +========================+
    | outer_left_eyebrow     |
    | middle_left_eyebrow    |
    | inner_left_eyebrow     |
    | inner_right_eyebrow    |
    | middle_right_eyebrow   |
    | outer_right_eyebrow    |
    | outer_left_eye_corner  |
    | inner_left_eye_corner  |
    | inner_right_eye_corner |
    | outer_right_eye_corner |
    | nose_saddle_left       |
    | nose_saddle_right      |
    | left_nose_peak         |
    | nose_tip               |
    | right_nose_peak        |
    | left_mouth_corner      |
    | upper_lip_outer_middle |
    | right_mouth_corner     |
    | upper_lip_inner_middle |
    | lower_lip_inner_middle |
    | lower_lip_outer_middle |
    | chin_middle            |
    +------------------------+
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(LM2Importer, self).__init__(filepath)

    def _parse_format(self, asset=None):
        with open(self.filepath, 'r') as f:
            landmarks = f.read()

        # Remove comments and blank lines
        landmark_text = [l for l in landmarks.splitlines()
                         if (l.rstrip() and not '#' in l)]

        # First line says how many landmarks there are: 22 Landmarks
        # So pop it off the front
        num_points = int(landmark_text.pop(0).split()[0])
        labels = []

        # The next set of lines defines the labels
        labels_str = landmark_text.pop(0)
        if not labels_str == 'Labels:':
            raise ImportError("LM2 landmarks are incorrectly formatted. "
                              "Expected a list of labels beginning with "
                              "'Labels:' but found '{0}'".format(labels_str))
        for i in xrange(num_points):
            # Lowercase, remove spaces and replace with underscores
            l = landmark_text.pop(0)
            l = '_'.join(l.lower().split())
            labels.append(l)

        # The next set of lines defines the coordinates
        coords_str = landmark_text.pop(0)
        if not coords_str == '2D Image coordinates:':
            raise ImportError("LM2 landmarks are incorrectly formatted. "
                              "Expected a list of coordinates beginning with "
                              "'2D Image coordinates:' "
                              "but found '{0}'".format(coords_str))
        xs = []
        ys = []
        for i in xrange(num_points):
            p = landmark_text.pop(0).split()
            xs.append(float(p[0]))
            ys.append(float(p[1]))

        xs = np.array(xs, dtype=np.float).reshape((-1, 1))
        ys = np.array(ys, dtype=np.float).reshape((-1, 1))

        self.group_label = 'LM2'
        # Flip the x and y
        self.pointcloud = PointCloud(np.hstack([ys, xs]))
        # Create the mask whereby there is one landmark per label
        # (identity matrix)
        masks = np.eye(num_points).astype(np.bool)
        masks = np.vsplit(masks, num_points)
        masks = [np.squeeze(m) for m in masks]
        self.labels_to_masks = dict(zip(labels, masks))


class LANImporter(LandmarkImporter):
    r"""
    Importer for the LAN file format for the GOSH dataset. This is a 3D
    landmark type and so it is assumed it only applies to meshes.

    Landmark set label: LAN

    Note that the exact meaning of each landmark in this set varies,
    so all we can do is import all landmarks found under the label 'LAN'

    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(LANImporter, self).__init__(filepath)

    def _parse_format(self, asset=None):
        with open(self.filepath, 'r') as f:
            landmarks = np.fromfile(
                f, dtype=np.float32)[3:].reshape([-1, 3]).astype(np.double)

        self.group_label = 'LM3'
        self.pointcloud = PointCloud(landmarks)
        self.labels_to_masks = {'all': np.ones(landmarks.shape[0],
                                               dtype=np.bool)}


class BNDImporter(LandmarkImporter):
    r"""
    Importer for the BND file format for the BU-3DFE dataset. This is a 3D
    landmark type and so it is assumed it only applies to meshes.

    Landmark set label: BND

    Landmark labels:

    +---------------+
    | label         |
    +===============+
    | left_eye      |
    | right_eye     |
    | left_eyebrow  |
    | right_eyebrow |
    | nose          |
    | mouth         |
    | chin          |
    +---------------+
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(BNDImporter, self).__init__(filepath)

    def _parse_format(self, asset=None):
        with open(self.filepath, 'r') as f:
            landmarks = f.read()

        # Remove blank lines
        landmark_text = [l for l in landmarks.splitlines() if l.rstrip()]
        landmark_text = [l.split() for l in landmark_text]

        n_points = len(landmark_text)
        landmarks = np.zeros([n_points, 3])
        for i, l in enumerate(landmark_text):
            # Skip the first number as it's an index into the mesh
            landmarks[i, :] = np.array([float(l[1]), float(l[2]), float(l[3])],
                                       dtype=np.float)

        self.group_label = 'BND'
        self.pointcloud = PointCloud(landmarks)
        self.labels_to_masks = {
            'left_eye': _indices_to_mask(n_points, np.arange(8)),
            'right_eye': _indices_to_mask(n_points, np.arange(8, 16)),
            'left_eyebrow': _indices_to_mask(n_points, np.arange(16, 26)),
            'right_eyebrow': _indices_to_mask(n_points, np.arange(26, 36)),
            'nose': _indices_to_mask(n_points, np.arange(36, 48)),
            'mouth': _indices_to_mask(n_points, np.arange(48, 68)),
            'chin': _indices_to_mask(n_points, np.arange(68, 83))
        }


class JSONImporter(LandmarkImporter):
    r"""
    Importer for the Menpo JSON format. This is an nD
    landmark type for both images and meshes that encodes semantic labels in
    the format

    Landmark set label: JSON

    Landmark labels: decided by file

    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(LandmarkImporter, self).__init__(filepath)

    def _parse_format(self, asset=None):
        with open(self.filepath, 'rb') as f:
            lms_dict = json.load(f)  # lms_dict is now a dict rep the JSON
        self.group_label = 'JSON'
        all_points = []
        labels = []  # label per group
        labels_slices = []  # slices into the full pointcloud per label
        # ensure that we always respect the sorting of keys
        ordered_groups = OrderedDict(sorted(lms_dict['groups'].items(),
                                            key=lambda t: t[0]))
        start = 0
        for label, group in ordered_groups.iteritems():
            lms = group['landmarks']
            labels.append(label)
            labels_slices.append(slice(start, len(lms) + start))
            start = len(lms) + start
            for p in lms:
                all_points.append(p['point'])
        self.pointcloud = PointCloud(np.array(all_points))
        self.labels_to_masks = {}
        # go through each label and build the appropriate boolean array
        for label, l_slice in zip(labels, labels_slices):
            mask = np.zeros(self.pointcloud.n_points, dtype=np.bool)
            mask[l_slice] = True
            self.labels_to_masks[label] = mask

import abc
import json
from collections import OrderedDict

import numpy as np

from menpo.io.base import Importer
from menpo.landmark.base import LandmarkGroup
from menpo.shape import PointCloud
from menpo.transform.affine import Scale


class LandmarkImporter(Importer):
    """
    Abstract base class for importing landmarks.

    Parameters
    ----------
    filepath : string
        Absolute filepath of the landmarks.
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(LandmarkImporter, self).__init__(filepath)
        self.group_label = 'default'
        self.pointcloud = None
        self.labels_to_masks = None

    def build(self, asset=None):
        """
        Overrides the :meth:`build <menpo.io.base.Importer.build>` method.

        Parse the landmark format and return the label and landmark dictionary.

        Parameters
        ----------
        asset : object, optional
            The asset that the landmarks are being built for. Can be used to
            adjust landmarks as necessary (e.g. rescaling image landmarks
            from 0-1 to image.shape)

        Returns
        -------
        landmark_group : string
            The landmark group parsed from the file.
            Every point will be labelled.
        """
        self._parse_format(asset=asset)
        return LandmarkGroup(None, self.group_label, self.pointcloud,
                             self.labels_to_masks)

    @abc.abstractmethod
    def _parse_format(self, asset=None):
        r"""
        Read the landmarks file from disk, parse it in to semantic labels and
        :class:`menpo.shape.base.PointCloud`.

        Set the ``self.label`` and ``self.pointcloud`` attributes.
        """
        pass


def _indices_to_mask(n_points, indices):
    """
    Helper function to turn an array of indices in to a boolean mask.

    Parameters
    ----------
    n_points : int
        The total number of points for the mask
    indices : ndarray of ints
        An array of integers representing the ``True`` indices.

    Returns
    -------
    boolean_mask : ndarray of bools
        The mask for the set of landmarks where each index from indices is set
        to ``True`` and the rest are ``False``
    """
    mask = np.zeros(n_points, dtype=np.bool)
    mask[indices] = True
    return mask


class ASFImporter(LandmarkImporter):
    r"""
    Abstract base class for an importer for the ASF file format.
    Currently **does not support the connectivity specified in the format**.

    Implementations of this class should override the :meth:`_build_points`
    which determines the ordering of axes. For example, for images, the
    ``x`` and ``y`` axes are flipped such that the first axis is ``y`` (height
    in the image domain).

    Landmark set label: ASF

    Landmark labels:

    +---------+
    | label   |
    +=========+
    | all     |
    +---------+

    Parameters
    ----------
    filepath : string
        Absolute filepath to landmark file.

    References
    ----------
    .. [1] http://www2.imm.dtu.dk/~aam/datasets/datasets.html
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(ASFImporter, self).__init__(filepath)

    @abc.abstractmethod
    def _build_points(self, xs, ys):
        r"""
        Determines the ordering of points within the landmarks. For meshes
        ``x`` is the first axis, where as for images ``y`` is the first axis.
        """
        pass

    def _parse_format(self, asset=None):
        with open(self.filepath, 'r') as f:
            landmarks = f.read()

        # Remove comments and blank lines
        landmarks = [l for l in landmarks.splitlines()
                     if (l.rstrip() and not '#' in l)]

        # Pop the front of the list for the number of landmarks
        count = int(landmarks.pop(0))
        # Pop the last element of the list for the image_name
        image_name = landmarks.pop()

        xs = np.empty([count, 1])
        ys = np.empty([count, 1])
        connectivity = np.empty([count, 2], dtype=np.int)
        for i in xrange(count):
            # Though unpacked, they are still all strings
            # Only unpack the first 7
            (path_num, path_type, xpos, ypos,
             point_num, connects_from, connects_to) = landmarks[i].split()[:7]
            xs[i, ...] = float(xpos)
            ys[i, ...] = float(ypos)
            connectivity[i, ...] = [int(connects_from), int(connects_to)]

        points = self._build_points(xs, ys)
        if asset is not None:
            # we've been given an asset. As ASF files are normalized,
            # fix that here
            points = Scale(np.array(asset.shape)).apply(points)

        # TODO: Use connectivity and create a graph type instead of PointCloud
        # edges = scaled_points[connectivity]

        self.group_label = 'ASF'
        self.pointcloud = PointCloud(points)
        self.labels_to_masks = {'all': np.ones(points.shape[0], dtype=np.bool)}


class PTSImporter(LandmarkImporter):
    r"""
    Importer for the PTS file format. Assumes version 1 of the format.

    Implementations of this class should override the :meth:`_build_points`
    which determines the ordering of axes. For example, for images, the
    ``x`` and ``y`` axes are flipped such that the first axis is ``y`` (height
    in the image domain).

    Note that PTS has a very loose format definition. Here we make the
    assumption (as is common) that PTS landmarks are 1-based. That is,
    landmarks on a 480x480 image are in the range [1-480]. As Menpo is
    consistently 0-based, we *subtract 1* off each landmark value
    automatically.

    If you want to use PTS landmarks that are 0-based, you will have to
    manually add one back on to landmarks post importing.

    Landmark set label: PTS

    Landmark labels:

    +---------+
    | label   |
    +=========+
    | all     |
    +---------+
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(PTSImporter, self).__init__(filepath)

    @abc.abstractmethod
    def _build_points(self, xs, ys):
        r"""
        Determines the ordering of points within the landmarks. For meshes
        ``x`` is the first axis, where as for images ``y`` is the first axis.
        """
        pass

    def _parse_format(self, asset=None):
        f = open(self.filepath, 'r')
        for line in f:
            if line.split()[0] == '{':
                break
        xs = []
        ys = []
        for line in f:
            if line.split()[0] != '}':
                xpos, ypos = line.split()[0:2]
                xs.append(xpos)
                ys.append(ypos)
        xs = np.array(xs, dtype=np.float).reshape((-1, 1))
        ys = np.array(ys, dtype=np.float).reshape((-1, 1))
        # PTS landmarks are 1-based, need to convert to 0-based (subtract 1)
        points = self._build_points(xs - 1, ys - 1)

        self.group_label = 'PTS'
        self.pointcloud = PointCloud(points)
        self.labels_to_masks = {'all': np.ones(points.shape[0], dtype=np.bool)}


class LM3Importer(LandmarkImporter):
    r"""
    Importer for the LM3 file format from the bosphorus dataset. This is a 3D
    landmark type and so it is assumed it only applies to meshes.

    Landmark set label: LM3

    Landmark labels:

    +------------------------+
    | label                  |
    +========================+
    | outer_left_eyebrow     |
    | middle_left_eyebrow    |
    | inner_left_eyebrow     |
    | inner_right_eyebrow    |
    | middle_right_eyebrow   |
    | outer_right_eyebrow    |
    | outer_left_eye_corner  |
    | inner_left_eye_corner  |
    | inner_right_eye_corner |
    | outer_right_eye_corner |
    | nose_saddle_left       |
    | nose_saddle_right      |
    | left_nose_peak         |
    | nose_tip               |
    | right_nose_peak        |
    | left_mouth_corner      |
    | upper_lip_outer_middle |
    | right_mouth_corner     |
    | upper_lip_inner_middle |
    | lower_lip_inner_middle |
    | lower_lip_outer_middle |
    | chin_middle            |
    +------------------------+
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(LM3Importer, self).__init__(filepath)

    def _parse_format(self, asset=None):
        with open(self.filepath, 'r') as f:
            landmarks = f.read()

        # Remove comments and blank lines
        landmark_text = [l for l in landmarks.splitlines()
                         if (l.rstrip() and not '#' in l)]

        # First line says how many landmarks there are: 22 Landmarks
        # So pop it off the front
        num_points = int(landmark_text.pop(0).split()[0])
        xs = []
        ys = []
        zs = []
        labels = []

        # The lines then alternate between the labels and the coordinates
        for i in xrange(num_points * 2):
            if i % 2 == 0:  # label
                # Lowercase, remove spaces and replace with underscores
                l = landmark_text[i]
                l = '_'.join(l.lower().split())
                labels.append(l)
            else:  # coordinate
                p = landmark_text[i].split()
                xs.append(float(p[0]))
                ys.append(float(p[1]))
                zs.append(float(p[2]))

        xs = np.array(xs, dtype=np.float).reshape((-1, 1))
        ys = np.array(ys, dtype=np.float).reshape((-1, 1))
        zs = np.array(zs, dtype=np.float).reshape((-1, 1))

        self.group_label = 'LM3'
        self.pointcloud = PointCloud(np.hstack([xs, ys, zs]))
        # Create the mask whereby there is one landmark per label
        # (identity matrix)
        masks = np.eye(num_points).astype(np.bool)
        masks = np.vsplit(masks, num_points)
        masks = [np.squeeze(m) for m in masks]
        self.labels_to_masks = dict(zip(labels, masks))


class LM2Importer(LandmarkImporter):
    r"""
    Importer for the LM2 file format from the bosphorus dataset. This is a 2D
    landmark type and so it is assumed it only applies to images.

    Landmark set label: LM2

    Landmark labels:

    +------------------------+
    | label                  |
    +========================+
    | outer_left_eyebrow     |
    | middle_left_eyebrow    |
    | inner_left_eyebrow     |
    | inner_right_eyebrow    |
    | middle_right_eyebrow   |
    | outer_right_eyebrow    |
    | outer_left_eye_corner  |
    | inner_left_eye_corner  |
    | inner_right_eye_corner |
    | outer_right_eye_corner |
    | nose_saddle_left       |
    | nose_saddle_right      |
    | left_nose_peak         |
    | nose_tip               |
    | right_nose_peak        |
    | left_mouth_corner      |
    | upper_lip_outer_middle |
    | right_mouth_corner     |
    | upper_lip_inner_middle |
    | lower_lip_inner_middle |
    | lower_lip_outer_middle |
    | chin_middle            |
    +------------------------+
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(LM2Importer, self).__init__(filepath)

    def _parse_format(self, asset=None):
        with open(self.filepath, 'r') as f:
            landmarks = f.read()

        # Remove comments and blank lines
        landmark_text = [l for l in landmarks.splitlines()
                         if (l.rstrip() and not '#' in l)]

        # First line says how many landmarks there are: 22 Landmarks
        # So pop it off the front
        num_points = int(landmark_text.pop(0).split()[0])
        labels = []

        # The next set of lines defines the labels
        labels_str = landmark_text.pop(0)
        if not labels_str == 'Labels:':
            raise ImportError("LM2 landmarks are incorrectly formatted. "
                              "Expected a list of labels beginning with "
                              "'Labels:' but found '{0}'".format(labels_str))
        for i in xrange(num_points):
            # Lowercase, remove spaces and replace with underscores
            l = landmark_text.pop(0)
            l = '_'.join(l.lower().split())
            labels.append(l)

        # The next set of lines defines the coordinates
        coords_str = landmark_text.pop(0)
        if not coords_str == '2D Image coordinates:':
            raise ImportError("LM2 landmarks are incorrectly formatted. "
                              "Expected a list of coordinates beginning with "
                              "'2D Image coordinates:' "
                              "but found '{0}'".format(coords_str))
        xs = []
        ys = []
        for i in xrange(num_points):
            p = landmark_text.pop(0).split()
            xs.append(float(p[0]))
            ys.append(float(p[1]))

        xs = np.array(xs, dtype=np.float).reshape((-1, 1))
        ys = np.array(ys, dtype=np.float).reshape((-1, 1))

        self.group_label = 'LM2'
        # Flip the x and y
        self.pointcloud = PointCloud(np.hstack([ys, xs]))
        # Create the mask whereby there is one landmark per label
        # (identity matrix)
        masks = np.eye(num_points).astype(np.bool)
        masks = np.vsplit(masks, num_points)
        masks = [np.squeeze(m) for m in masks]
        self.labels_to_masks = dict(zip(labels, masks))


class LANImporter(LandmarkImporter):
    r"""
    Importer for the LAN file format for the GOSH dataset. This is a 3D
    landmark type and so it is assumed it only applies to meshes.

    Landmark set label: LAN

    Note that the exact meaning of each landmark in this set varies,
    so all we can do is import all landmarks found under the label 'LAN'

    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(LANImporter, self).__init__(filepath)

    def _parse_format(self, asset=None):
        with open(self.filepath, 'r') as f:
            landmarks = np.fromfile(
                f, dtype=np.float32)[3:].reshape([-1, 3]).astype(np.double)

        self.group_label = 'LM3'
        self.pointcloud = PointCloud(landmarks)
        self.labels_to_masks = {'all': np.ones(landmarks.shape[0],
                                               dtype=np.bool)}


class BNDImporter(LandmarkImporter):
    r"""
    Importer for the BND file format for the BU-3DFE dataset. This is a 3D
    landmark type and so it is assumed it only applies to meshes.

    Landmark set label: BND

    Landmark labels:

    +---------------+
    | label         |
    +===============+
    | left_eye      |
    | right_eye     |
    | left_eyebrow  |
    | right_eyebrow |
    | nose          |
    | mouth         |
    | chin          |
    +---------------+
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(BNDImporter, self).__init__(filepath)

    def _parse_format(self, asset=None):
        with open(self.filepath, 'r') as f:
            landmarks = f.read()

        # Remove blank lines
        landmark_text = [l for l in landmarks.splitlines() if l.rstrip()]
        landmark_text = [l.split() for l in landmark_text]

        n_points = len(landmark_text)
        landmarks = np.zeros([n_points, 3])
        for i, l in enumerate(landmark_text):
            # Skip the first number as it's an index into the mesh
            landmarks[i, :] = np.array([float(l[1]), float(l[2]), float(l[3])],
                                       dtype=np.float)

        self.group_label = 'BND'
        self.pointcloud = PointCloud(landmarks)
        self.labels_to_masks = {
            'left_eye': _indices_to_mask(n_points, np.arange(8)),
            'right_eye': _indices_to_mask(n_points, np.arange(8, 16)),
            'left_eyebrow': _indices_to_mask(n_points, np.arange(16, 26)),
            'right_eyebrow': _indices_to_mask(n_points, np.arange(26, 36)),
            'nose': _indices_to_mask(n_points, np.arange(36, 48)),
            'mouth': _indices_to_mask(n_points, np.arange(48, 68)),
            'chin': _indices_to_mask(n_points, np.arange(68, 83))
        }


class JSONImporter(LandmarkImporter):
    r"""
    Importer for the Menpo JSON format. This is an nD
    landmark type for both images and meshes that encodes semantic labels in
    the format

    Landmark set label: JSON

    Landmark labels: decided by file

    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(LandmarkImporter, self).__init__(filepath)

    def _parse_format(self, asset=None):
        with open(self.filepath, 'rb') as f:
            lms_dict = json.load(f)  # lms_dict is now a dict rep the JSON
        self.group_label = 'JSON'
        all_points = []
        labels = []  # label per group
        labels_slices = []  # slices into the full pointcloud per label
        # ensure that we always respect the sorting of keys
        ordered_groups = OrderedDict(sorted(lms_dict['groups'].items(),
                                            key=lambda t: t[0]))
        start = 0
        for label, group in ordered_groups.iteritems():
            lms = group['landmarks']
            labels.append(label)
            labels_slices.append(slice(start, len(lms) + start))
            start = len(lms) + start
            for p in lms:
                all_points.append(p['point'])
        self.pointcloud = PointCloud(np.array(all_points))
        self.labels_to_masks = {}
        # go through each label and build the appropriate boolean array
        for label, l_slice in zip(labels, labels_slices):
            mask = np.zeros(self.pointcloud.n_points, dtype=np.bool)
            mask[l_slice] = True
            self.labels_to_masks[label] = mask
Abstract base class for importing landmarks.      Parameters     ----------     filepath : string         Absolute filepath of the landmarks. Overrides the :meth:`build <menpo.io.base.Importer.build>` method.          Parse the landmark format and return the label and landmark dictionary.          Parameters         ----------         asset : object, optional             The asset that the landmarks are being built for. Can be used to             adjust landmarks as necessary (e.g. rescaling image landmarks             from 0-1 to image.shape)          Returns         -------         landmark_group : string             The landmark group parsed from the file.             Every point will be labelled. Read the landmarks file from disk, parse it in to semantic labels and         :class:`menpo.shape.base.PointCloud`.          Set the ``self.label`` and ``self.pointcloud`` attributes. Helper function to turn an array of indices in to a boolean mask.      Parameters     ----------     n_points : int         The total number of points for the mask     indices : ndarray of ints         An array of integers representing the ``True`` indices.      Returns     -------     boolean_mask : ndarray of bools         The mask for the set of landmarks where each index from indices is set         to ``True`` and the rest are ``False`` Abstract base class for an importer for the ASF file format.     Currently **does not support the connectivity specified in the format**.      Implementations of this class should override the :meth:`_build_points`     which determines the ordering of axes. For example, for images, the     ``x`` and ``y`` axes are flipped such that the first axis is ``y`` (height     in the image domain).      Landmark set label: ASF      Landmark labels:      +---------+     | label   |     +=========+     | all     |     +---------+      Parameters     ----------     filepath : string         Absolute filepath to landmark file.      References     ----------     .. [1] http://www2.imm.dtu.dk/~aam/datasets/datasets.html Determines the ordering of points within the landmarks. For meshes         ``x`` is the first axis, where as for images ``y`` is the first axis. # Remove comments and blank lines # Pop the front of the list for the number of landmarks # Pop the last element of the list for the image_name # Though unpacked, they are still all strings # Only unpack the first 7 # we've been given an asset. As ASF files are normalized, # fix that here # TODO: Use connectivity and create a graph type instead of PointCloud # edges = scaled_points[connectivity] Importer for the PTS file format. Assumes version 1 of the format.      Implementations of this class should override the :meth:`_build_points`     which determines the ordering of axes. For example, for images, the     ``x`` and ``y`` axes are flipped such that the first axis is ``y`` (height     in the image domain).      Note that PTS has a very loose format definition. Here we make the     assumption (as is common) that PTS landmarks are 1-based. That is,     landmarks on a 480x480 image are in the range [1-480]. As Menpo is     consistently 0-based, we *subtract 1* off each landmark value     automatically.      If you want to use PTS landmarks that are 0-based, you will have to     manually add one back on to landmarks post importing.      Landmark set label: PTS      Landmark labels:      +---------+     | label   |     +=========+     | all     |     +---------+ Determines the ordering of points within the landmarks. For meshes         ``x`` is the first axis, where as for images ``y`` is the first axis. # PTS landmarks are 1-based, need to convert to 0-based (subtract 1) Importer for the LM3 file format from the bosphorus dataset. This is a 3D     landmark type and so it is assumed it only applies to meshes.      Landmark set label: LM3      Landmark labels:      +------------------------+     | label                  |     +========================+     | outer_left_eyebrow     |     | middle_left_eyebrow    |     | inner_left_eyebrow     |     | inner_right_eyebrow    |     | middle_right_eyebrow   |     | outer_right_eyebrow    |     | outer_left_eye_corner  |     | inner_left_eye_corner  |     | inner_right_eye_corner |     | outer_right_eye_corner |     | nose_saddle_left       |     | nose_saddle_right      |     | left_nose_peak         |     | nose_tip               |     | right_nose_peak        |     | left_mouth_corner      |     | upper_lip_outer_middle |     | right_mouth_corner     |     | upper_lip_inner_middle |     | lower_lip_inner_middle |     | lower_lip_outer_middle |     | chin_middle            |     +------------------------+ # Remove comments and blank lines # First line says how many landmarks there are: 22 Landmarks # So pop it off the front # The lines then alternate between the labels and the coordinates # label # Lowercase, remove spaces and replace with underscores # coordinate # Create the mask whereby there is one landmark per label # (identity matrix) Importer for the LM2 file format from the bosphorus dataset. This is a 2D     landmark type and so it is assumed it only applies to images.      Landmark set label: LM2      Landmark labels:      +------------------------+     | label                  |     +========================+     | outer_left_eyebrow     |     | middle_left_eyebrow    |     | inner_left_eyebrow     |     | inner_right_eyebrow    |     | middle_right_eyebrow   |     | outer_right_eyebrow    |     | outer_left_eye_corner  |     | inner_left_eye_corner  |     | inner_right_eye_corner |     | outer_right_eye_corner |     | nose_saddle_left       |     | nose_saddle_right      |     | left_nose_peak         |     | nose_tip               |     | right_nose_peak        |     | left_mouth_corner      |     | upper_lip_outer_middle |     | right_mouth_corner     |     | upper_lip_inner_middle |     | lower_lip_inner_middle |     | lower_lip_outer_middle |     | chin_middle            |     +------------------------+ # Remove comments and blank lines # First line says how many landmarks there are: 22 Landmarks # So pop it off the front # The next set of lines defines the labels # Lowercase, remove spaces and replace with underscores # The next set of lines defines the coordinates # Flip the x and y # Create the mask whereby there is one landmark per label # (identity matrix) Importer for the LAN file format for the GOSH dataset. This is a 3D     landmark type and so it is assumed it only applies to meshes.      Landmark set label: LAN      Note that the exact meaning of each landmark in this set varies,     so all we can do is import all landmarks found under the label 'LAN' Importer for the BND file format for the BU-3DFE dataset. This is a 3D     landmark type and so it is assumed it only applies to meshes.      Landmark set label: BND      Landmark labels:      +---------------+     | label         |     +===============+     | left_eye      |     | right_eye     |     | left_eyebrow  |     | right_eyebrow |     | nose          |     | mouth         |     | chin          |     +---------------+ # Remove blank lines # Skip the first number as it's an index into the mesh Importer for the Menpo JSON format. This is an nD     landmark type for both images and meshes that encodes semantic labels in     the format      Landmark set label: JSON      Landmark labels: decided by file # lms_dict is now a dict rep the JSON # label per group # slices into the full pointcloud per label # ensure that we always respect the sorting of keys # go through each label and build the appropriate boolean array