Пример #1
0
class FixtureImage(object):

    MARKER_DETECTION_DPI = 150
    EXPECTED_IM_SIZE = (6000, 4800)
    EXPECTED_IM_DPI = 600

    def __init__(self, fixture=None):
        """

        :type fixture: scanomatic.io.fixtures.FixtureSettings
        """
        self._logger = Logger("Fixture Image")

        self._reference_fixture_settings = fixture
        self._current_fixture_settings = None
        """:type : scanomatic.io.fixtures.Fixture_Settings"""
        if fixture:
            self._history = GriddingHistory(fixture)
        else:
            self._history = None

        self.im = None
        self.im_path = None
        self._original_dpi = None

        self._name = "default"

    def __getitem__(self, key):

        if key in ['current']:
            if self._current_fixture_settings is None:
                self._current_fixture_settings = FixtureSettings(self.name)
            return self._current_fixture_settings

        elif key in ['fixture', 'reference']:
            if self._reference_fixture_settings is None:
                self._reference_fixture_settings = FixtureSettings(self.name)
                self._name = self.name

            return self._reference_fixture_settings

        else:

            raise KeyError(key)

    @property
    def name(self):

        if self._reference_fixture_settings:
            return self._reference_fixture_settings.model.name
        else:
            return self._name

    @name.setter
    def name(self, value):

        if self._reference_fixture_settings:
            self._reference_fixture_settings.model.name = value
        else:
            self._name = value

    def get_dpi_factor_to_target(self, target_scale):

        return target_scale / float(self._original_dpi)

    @staticmethod
    def coordinate_to_original_dpi(coordinate, as_ints=False, scale=1.0):

        rtype = type(coordinate)

        if as_ints:
            return rtype(int(round(val / scale)) for val in coordinate)

        return rtype(val / scale for val in coordinate)

    @staticmethod
    def coordinate_to_local_dpi(coordinate, as_ints=False, scale=1.0):

        rtype = type(coordinate)

        if as_ints:
            return rtype(int(round(val * scale)) for val in coordinate)

        return rtype(val * scale for val in coordinate)

    def set_image(self, image=None, image_path=None, dpi=None):

        self.im_path = image_path

        if image is not None:

            self.im = image

        elif image_path is not None:

            try:
                self.im = load_image_to_numpy(image_path)
            except IOError:
                self.im = None

            if self.im is None:
                self._logger.error("Could not load image")

            else:
                self._logger.info("Loaded image {0} with shape {1}".format(
                    image_path, self.im.shape))
        else:

            self._logger.warning(
                "No information supplied about how to load image, thus none loaded"
            )

            self.im = None

        if self.im is None:
            self._original_dpi = None
        else:
            self._original_dpi = dpi if dpi else self.guess_dpi()

    def guess_dpi(self):

        dpi = ((a / float(b)) * self.EXPECTED_IM_DPI
               for a, b in zip(self.im.shape, self.EXPECTED_IM_SIZE))
        if dpi:
            guess = 0
            for val in dpi:
                if guess > 0 and guess != val:

                    self._logger.warning(
                        "Image dimensions don't agree with expected size " +
                        "X {1} != Y {2} on image '{3}', can't guess DPI, using {0}"
                        .format(self.EXPECTED_IM_DPI, guess, val, self.im_path)
                    )

                    return self.EXPECTED_IM_DPI

                guess = val

            if guess > 0:
                return guess

        return self.EXPECTED_IM_DPI

    def analyse_current(self):

        logger = self._logger

        t = time.time()
        logger.debug("Threading invokes marker analysis")

        self.run_marker_analysis()

        logger.debug("Threading marker detection complete," +
                     "invokes setting area positions (acc-time {0} s)".format(
                         time.time() - t))

        self.set_current_areas(issues={})

        logger.debug(
            "Threading areas set(acc-time: {0} s)".format(time.time() - t))

        self.analyse_grayscale()

        logger.debug("Grayscale ({0}) analysed (acc-time: {1} s)".format(
            self['grayscale_type'],
            time.time() - t))

        logger.debug("Threading done (took: {0} s)".format(time.time() - t))

    def run_marker_analysis(self, markings=None):

        _logger = self._logger

        t = time.time()
        if markings is None:
            markings = len(self["fixture"].model.orientation_marks_x)

        analysis_img = self._get_image_in_correct_scale(
            self.MARKER_DETECTION_DPI)
        scale_factor = self.get_dpi_factor_to_target(self.MARKER_DETECTION_DPI)

        _logger.info(
            "Running marker detection ({0} markers on {1} ({2}) using {3}, scale {4})"
            .format(markings, self.im_path, analysis_img.shape,
                    self["reference"].get_marker_path(), scale_factor))

        im_analysis = image_fixture.FixtureImage(
            image=analysis_img,
            pattern_image_path=self["reference"].get_marker_path(),
            scale=scale_factor)

        x_positions_correct_scale, y_positions_correct_scale = im_analysis.find_pattern(
            markings=markings)

        self["current"].model.orientation_marks_x = x_positions_correct_scale
        self["current"].model.orientation_marks_y = y_positions_correct_scale

        if x_positions_correct_scale is None or y_positions_correct_scale is None:

            _logger.error("No markers found")

        _logger.debug(
            "Marker Detection complete (acc {0} s)".format(time.time() - t))

    def _get_image_in_correct_scale(self, target_dpi):

        if self._original_dpi != target_dpi:

            return image_basics.Quick_Scale_To_im(
                im=self.im, scale=self.get_dpi_factor_to_target(target_dpi))

        return self.im

    def _set_current_mark_order(self):

        x_centered, y_centered = self._get_centered_mark_positions("current")
        x_centered_ref, y_centered_ref = self._get_centered_mark_positions(
            "reference")

        if all(o is not None and o.size > 0
               for o in (x_centered, y_centered, x_centered_ref,
                         y_centered_ref)):

            length = np.sqrt(x_centered**2 + y_centered**2)
            length_ref = np.sqrt(x_centered_ref**2 + y_centered_ref**2)

            sort_order, sort_error = self._get_sort_order(length, length_ref)

            self._logger.debug(
                "Found sort order that matches the reference {0} (error {1})".
                format(sort_order, sort_error))

            self.__set_current_mark_order(sort_order)

        else:

            self._logger.critical("Missmatch in number of markings!")

    def __set_current_mark_order(self, sort_order):

        current_model = self["current"].model
        current_model.orientation_marks_x = current_model.orientation_marks_x[
            sort_order]
        current_model.orientation_marks_y = current_model.orientation_marks_y[
            sort_order]

    def _get_centered_mark_positions(self, source="current"):

        x_positions = self[source].model.orientation_marks_x
        y_positions = self[source].model.orientation_marks_y

        if x_positions is None or y_positions is None:
            return None, None

        x_positions = np.array(x_positions)
        y_positions = np.array(y_positions)

        marking_center = np.array((x_positions.mean(), y_positions.mean()))

        x_centered = x_positions - marking_center[0]
        y_centered = y_positions - marking_center[1]

        return x_centered, y_centered

    @staticmethod
    def _get_sort_order(length, reference_length):

        s = range(len(length))

        length_deltas = []
        sort_orders = []
        for sort_order in itertools.permutations(s):

            length_deltas.append(
                (length[list(sort_order)] - reference_length)**2)
            sort_orders.append(sort_order)

        length_deltas = np.array(length_deltas).sum(1)
        return list(sort_orders[length_deltas.argmin()]), np.sqrt(
            length_deltas.min())

    def _get_rotation(self):

        x_centered, y_centered = self._get_centered_mark_positions("current")
        x_centered_ref, y_centered_ref = self._get_centered_mark_positions(
            "reference")
        length = np.sqrt(x_centered**2 + y_centered**2)
        length_ref = np.sqrt(x_centered_ref**2 + y_centered_ref**2)

        rotations = np.arccos(x_centered / length)
        rotations = rotations * (y_centered > 0) + -1 * rotations * (y_centered
                                                                     < 0)

        rotations_ref = np.arccos(x_centered_ref / length_ref)
        rotations_ref = rotations_ref * (
            y_centered_ref > 0) + -1 * rotations_ref * (y_centered_ref < 0)

        rotation = (rotations - rotations_ref).mean()
        """:type : float"""
        if np.abs(rotation) < 0.001:
            return 0
        else:
            return rotation

    def _get_offset(self):

        current_model = self['current'].model
        ref_model = self['reference'].model

        x_delta = current_model.orientation_marks_x - ref_model.orientation_marks_x
        y_delta = current_model.orientation_marks_y - ref_model.orientation_marks_y
        return x_delta.mean(), y_delta.mean()

    def get_plate_im_section(self, plate_model, scale=1.0):
        """

        :type plate_model: scanomatic.models.fixture_models.FixturePlateModel
        """
        im = self.im

        if im is not None and plate_model is not None:

            try:

                return im[max(plate_model.y1 *
                              scale, 0):min(plate_model.y2 *
                                            scale, im.shape[0]),
                          max(plate_model.x2 *
                              scale, 0):min(plate_model.x2 *
                                            scale, im.shape[1])]

            except (IndexError, TypeError):

                return None

    def get_grayscale_im_section(self, grayscale_model, scale=1.0):
        """

        :type grayscale_model: scanomatic.models.fixture_models.GrayScaleAreaModel
        """
        im = self.im

        if im is not None and grayscale_model is not None:

            try:

                return im[
                    int(round(max(grayscale_model.y1 * scale, 0))):int(
                        round(min(grayscale_model.y2 * scale, im.shape[0]))),
                    int(round(max(grayscale_model.x1 * scale, 0))):int(
                        round(min(grayscale_model.x2 * scale, im.shape[1])))]

            except (IndexError, TypeError):

                return None

    def analyse_grayscale(self):

        current_model = self["current"].model
        im = self.get_grayscale_im_section(current_model.grayscale, scale=1.0)

        if im is None or 0 in im.shape:
            err = "No valid grayscale area. "
            if self.im is None:
                self._logger.error(err + "No image loaded")
            elif current_model.grayscale is None:
                self._logger.error(err + "Grayscale area model not set")
            elif im is None:
                self._logger.error(
                    err +
                    "Image (shape {0}) could not be sliced according to {1}".
                    format(self.im.shape, dict(**current_model.grayscale)))
            elif 0 in im.shape:
                self._logger.error(
                    err +
                    "Grayscale area has bad shape ({0})".format(im.shape))

            return False

        current_model.grayscale.values = image_grayscale.get_grayscale(
            self, current_model.grayscale)[1]
        if current_model.grayscale.values is None:
            self._logger.error("Grayscale detection failed")
            return False

    def _set_area_relative(self,
                           area,
                           rotation=None,
                           offset=(0, 0),
                           issues={}):
        """

        :type area: scanomatic.models.fixture_models.FixturePlateModel |
            scanomatic.models.fixture_models.GrayScaleAreaModel
        :type issues: dict
        :type offset: tuple(int)
        """

        if rotation:
            self._logger.warning(
                "Not supporting rotations yet (got {0})".format(rotation))
            # area.x1, area.y1 = _get_rotated_vector(area.x1, area.y1, rotation)
            # area.x2, area.y2 = _get_rotated_vector(area.x2, area.y2, rotation)

        for dim, keys in {1: ('x1', 'x2'), 0: ('y1', 'y2')}.items():
            for key in keys:
                area[key] = round(area[key] + offset[dim])
                if area[key] > self.EXPECTED_IM_SIZE[dim]:
                    self._logger.warning(
                        "{0} value ({1}) outside image, setting to img border".
                        format(key, area[key]))
                    area[key] = self.EXPECTED_IM_SIZE[dim]
                    issues['overflow'] = area.index if hasattr(
                        area, "index") else "Grayscale"
                elif area[key] < 0:
                    self._logger.warning(
                        "{0} value ({1}) outside image, setting to img border".
                        format(key, area[key]))
                    area[key] = 0
                    issues['overflow'] = area.index if hasattr(
                        area, "index") else "Grayscale"

    def set_current_areas(self, issues):
        """

        :param issues: reported issues
         :type issues: dict
        :return:
        """

        self._set_current_mark_order()
        offset = self._get_offset()
        rotation = self._get_rotation()
        if abs(rotation) > 0.05:
            issues['rotation'] = rotation
        current_model = self["current"].model
        ref_model = self["fixture"].model

        self._logger.info(
            "Positions on current '{0}' will be moved {1} and rotated {2} due to diff to reference {3}"
            .format(current_model.name, offset, rotation, ref_model.name))

        current_model.plates = type(current_model.plates)(
            FixturePlateFactory.copy(plate) for plate in ref_model.plates)

        for plate in current_model.plates:
            self._set_area_relative(plate, rotation, offset, issues)

        self._set_area_relative(current_model.grayscale, rotation, offset,
                                issues)
class CompilationResults(object):
    def __init__(self,
                 compilation_path=None,
                 compile_instructions_path=None,
                 scanner_instructions_path=None,
                 sort_mode=FIRST_PASS_SORTING.Time):

        self._logger = Logger("Compilation results")
        self._compilation_path = compilation_path
        self._compile_instructions = None
        self._scanner_instructions = None
        """:type : scanomatic.models.scanning_model.ScanningModel"""
        self.load_scanner_instructions(scanner_instructions_path)
        self._plates = None
        self._plate_position_keys = None
        self._image_models = []
        self._used_models = []
        self._current_model = None
        self._loading_length = 0
        if compile_instructions_path:
            self._load_compile_instructions(compile_instructions_path)
        if compilation_path:
            self._load_compilation(self._compilation_path, sort_mode=sort_mode)

    @classmethod
    def create_from_data(cls,
                         path,
                         compile_instructions,
                         image_models,
                         used_models=None,
                         scan_instructions=None):

        if used_models is None:
            used_models = []

        new = cls()
        new._compilation_path = path
        new._compile_instructions = CompileProjectFactory.copy(
            compile_instructions)
        new._image_models = CompileImageAnalysisFactory.copy_iterable_of_model(
            list(image_models))
        new._used_models = CompileImageAnalysisFactory.copy_iterable_of_model(
            list(used_models))
        new._loading_length = len(new._image_models)
        new._scanner_instructions = scan_instructions
        return new

    def load_scanner_instructions(self, path=None):
        """

        Args:
            path:  Path to the instrucitons or None to infer it


        """
        if path is None:
            try:
                path = glob(
                    os.path.join(
                        os.path.dirname(self._compilation_path),
                        Paths().scan_project_file_pattern.format('*')))[0]
            except IndexError:
                self._logger.warning(
                    "No information of start time of project, can't safely be joined with others"
                )
                return

        self._scanner_instructions = ScanningModelFactory.serializer.load_first(
            path)

    def _load_compile_instructions(self, path):

        try:
            self._compile_instructions = CompileProjectFactory.serializer.load_first(
                path)
        except IndexError:
            self._logger.error("Could not load path {0}".format(path))
            self._compile_instructions = None

    def _load_compilation(self, path, sort_mode=FIRST_PASS_SORTING.Time):

        images = CompileImageAnalysisFactory.serializer.load(path)
        self._logger.info("Loaded {0} compiled images".format(len(images)))

        self._reindex_plates(images)

        if sort_mode is FIRST_PASS_SORTING.Time:
            self._image_models = list(
                CompileImageAnalysisFactory.
                copy_iterable_of_model_update_indices(images))
        else:
            self._image_models = list(
                CompileImageAnalysisFactory.copy_iterable_of_model_update_time(
                    images))

        self._loading_length = len(self._image_models)

    @staticmethod
    def _reindex_plates(images):

        for image in images:

            if image and image.fixture and image.fixture.plates:

                for plate in image.fixture.plates:
                    plate.index -= 1

    def __len__(self):

        return self._loading_length

    def __getitem__(self, item):
        """


        :rtype: scanomatic.models.compile_project_model.CompileImageAnalysisModel
        """
        if not self._image_models:
            return None

        if item < 0:
            item %= len(self._image_models)

        try:
            return sorted(self._image_models,
                          key=lambda x: x.image.time_stamp)[item]
        except (ValueError, IndexError):
            return None

    def __add__(self, other):
        """

        :type other: CompilationResults
        """

        start_time_difference = other.start_time - self.start_time

        other_start_index = len(self)
        other_image_models = []
        other_directory = os.path.dirname(other._compilation_path)
        for index in range(len(other)):
            model = CompileImageAnalysisFactory.copy(other[index])
            """:type : scanomatic.models.compile_project_model.CompileImageAnalysisModel"""

            model.image.time_stamp += start_time_difference
            model.image.index += other_start_index
            self._update_image_path_if_needed(model, other_directory)
            other_image_models.append(model)

        other_image_models += self._image_models
        other_image_models = sorted(other_image_models,
                                    key=lambda x: x.image.time_stamp)

        return CompilationResults.create_from_data(self._compilation_path,
                                                   self._compile_instructions,
                                                   other_image_models,
                                                   self._used_models,
                                                   self._scanner_instructions)

    def _update_image_path_if_needed(self, model, directory):
        if not os.path.isfile(model.image.path):
            image_name = os.path.basename(model.image.path)
            if os.path.isfile(os.path.join(directory, image_name)):
                model.image.path = os.path.join(directory, image_name)
                return
        self._logger.warning("Couldn't locate the file {0}".format(
            model.image.path))

    @property
    def start_time(self):

        if self._scanner_instructions:
            return self._scanner_instructions.start_time
        self._logger.warning(
            "No scanner instructions have been loaded, start time unknown")
        return 0

    @property
    def compile_instructions(self):
        """


        :rtype: scanomatic.models.compile_project_model.CompileInstructionsModel
        """

        return self._compile_instructions

    @property
    def plates(self):

        res = self[-1]
        if res:
            return res.fixture.plates
        return None

    @property
    def last_index(self):

        return len(self._image_models) - 1

    @property
    def total_number_of_images(self):

        return len(self._image_models) + len(self._used_models)

    @property
    def current_image(self):
        """

        :rtype : scanomatic.models.compile_project_model.CompileImageAnalysisModel
        """
        return self._current_model

    @property
    def current_absolute_time(self):

        return self.current_image.image.time_stamp + self.compile_instructions.start_time

    def recycle(self):

        self._image_models += self._used_models
        self._used_models = []
        self._current_model = None

    def get_next_image_model(self):
        """

        :rtype : scanomatic.models.compile_project_model.CompileImageAnalysisModel
        """
        model = self[-1]
        self._current_model = model
        if model:
            self._image_models.remove(model)
            self._used_models.append(model)
        return model

    def dump(self,
             directory,
             new_name=None,
             force_dump_scan_instructions=False):

        self._logger.warning(
            """This functionality has not fully been tested, if you test it and it works fine let Martin konw.
            If it doesn't work, let him know too.""")
        directory = os.path.abspath(directory)
        os.makedirs(directory)
        if new_name is None:
            new_name = os.path.basename(directory)

        try:
            with open(
                    os.path.join(
                        directory,
                        Paths().project_compilation_pattern.format(new_name)),
                    'w') as fh:
                while True:
                    model = CompileImageAnalysisFactory.copy(
                        self.get_next_image_model())
                    self._update_image_path_if_needed(model, directory)
                    if model is None:
                        break
                    CompileImageAnalysisFactory.serializer.dump_to_filehandle(
                        model, fh)
        except IOError:
            self._logger.error("Could not save to directory")
            return

        compile_instructions = os.path.join(
            directory,
            Paths().project_compilation_pattern.format(new_name))
        CompileProjectFactory.serializer.dump(self._compile_instructions,
                                              compile_instructions)

        if not glob(os.path.join(directory, Paths().scan_project_file_pattern.format('*'))) or \
                force_dump_scan_instructions:

            scan_instructions = os.path.join(
                directory,
                Paths().scan_project_file_pattern.format(new_name))
            ScanningModelFactory.serializer.dump(self._scanner_instructions,
                                                 scan_instructions)
Пример #3
0
class GriddingHistory(object):
    """This class keeps track of the gridding-histories of the fixture
    using the configuration-file in the fixtures-directory"""

    plate_pinning_pattern = "plate_{0}_pinning_{1}"
    pinning_formats = ((8, 12), (16, 24), (32, 48), (64, 96))
    plate_area_pattern = "plate_{0}_area"

    def __init__(self, fixture_settings):
        """
        :type fixture_settings: scanomatic.io.fixtures.FixtureSettings
        """
        self._logger = Logger("Gridding History {0}".format(
            fixture_settings.model.name))
        self._fixture_settings = fixture_settings
        self._models_per_plate_pinning = defaultdict(dict)

    def load(self):

        self._models_per_plate_pinning.clear()
        history = GridHistoryFactory.serializer.load(self.path)
        for model in history:
            """:type : scanomatic.models.analysis_model.GridHistoryModel"""
            self._models_per_plate_pinning[(model.plate,
                                            model.pinning)].append(model)

    @property
    def path(self):

        return Paths().fixture_grid_history_pattern.format(
            self._fixture_settings.path)

    @property
    def _name(self):
        return self._fixture_settings.model.name

    def _get_gridding_history(self, plate, pinning_format):

        return [(model.offset_x, model.offset_y, model.delta_x, model.delta_y)
                for model in self._models_per_plate_pinning[(
                    plate, pinning_format)].values()]

    def get_history_model(self, project_id, plate, pinning):

        models = [
            model for model in self._models_per_plate_pinning[(plate, pinning)]
        ]

        if project_id in models:
            return models[project_id]

        self._logger.warning(
            "No history exists for project {0} plate {1} pinnig {2}".format(
                project_id, plate, pinning))
        return None

    def get_gridding_history(self, plate, pinning_format):

        history = self._get_gridding_history(plate, pinning_format)

        if not history:
            self._logger.info(
                "No history in {2} on plate {0} format {1}".format(
                    plate, pinning_format, self._name))

            return None

        self._logger.info(
            "Returning history for {0} plate {1} format {2}".format(
                self._name, plate, pinning_format))
        return np.array(history)

    def set_gridding_parameters(self, project_id, pinning_format, plate,
                                center, spacings):

        model = GridHistoryFactory.create(project_id=project_id,
                                          pinnig=pinning_format,
                                          plate=plate,
                                          center=center,
                                          delta=spacings)

        if GridHistoryFactory.validate(model):

            if not GridHistoryFactory.serializer.dump(model, self.path):

                self._logger.warning("Could not write grid history")
                return False
        else:

            self._logger.warning("This is not a valid grid history")
            return False

        self._logger.info(
            "Setting history {0} on fixture {1} for {2} {3}".format(
                center + spacings, self._name, project_id, plate))

        self._models_per_plate_pinning[(
            model.plate, model.pinning)][model.project_id] = model
        return True

    def unset_gridding_parameters(self, project_id, pinning_format, plate):

        model = self.get_history_model(project_id, plate, pinning_format)
        if model:
            GridHistoryFactory.serializer.purge(model, self.path)

    def reset_gridding_history(self, plate):

        for plate_in_history, pin_format in self._models_per_plate_pinning:
            if plate == plate_in_history:
                for model in self._models_per_plate_pinning[(plate_in_history,
                                                             pin_format)]:
                    GridHistoryFactory.serializer.purge(model, self.path)
            del self._models_per_plate_pinning[(plate_in_history, pin_format)]

    def reset_all_gridding_histories(self):

        GridHistoryFactory.serializer.purge_all(self.path)
        self._models_per_plate_pinning.clear()
Пример #4
0
class ProjectImage(object):
    def __init__(self, analysis_model, scanning_meta_data):
        """

        :param analysis_model: The model
         :type analysis_model : scanomatic.models.analysis_model.AnalysisModel
        :param scanning_meta_data: scanning info
        :type scanning_meta_data : scanomatic.models.scanning_model.ScanningModel
        :return:
        """
        self._analysis_model = analysis_model
        self._scanning_meta_data = scanning_meta_data
        self._logger = Logger("Analysis Image")

        self._im_loaded = False
        self.im = None

        self._grid_arrays = self._new_grid_arrays
        """:type : dict[int|scanomatic.image_analysis.grid_array.GridArray]"""
        self.features = _get_init_features(self._grid_arrays)

    @property
    def active_plates(self):
        return len(self._grid_arrays)

    def __getitem__(self, key):

        return self._grid_arrays[key]

    @property
    def _new_grid_arrays(self):
        """

        :rtype : dict[int|scanomatic.imageAnalysis.grid_array.GridArray]
        """
        grid_arrays = {}

        for index, pinning in enumerate(self._analysis_model.pinning_matrices):

            if pinning and self._plate_is_analysed(index):

                grid_arrays[index] = grid_array.GridArray(
                    index, pinning, self._analysis_model)

            else:

                if pinning:

                    self._logger.info(
                        "Skipping plate {0} because suppressing non-focal positions"
                        .format(index))

                else:

                    self._logger.info(
                        "Plate {0} not analysed because lacks pinning".format(
                            index))

        return grid_arrays

    def _plate_is_analysed(self, index):

        return not self._analysis_model.suppress_non_focal or index == self._analysis_model.focus_position[
            0]

    def set_grid(self, image_model):
        """

        :type image_model: scanomatic.models.compile_project_model.CompileImageAnalysisModel
        """
        if image_model is None:
            self._logger.critical("No image model to grid on")
            return False

        self.load_image(image_model.image.path)

        if self._im_loaded:

            threads = set()

            for index in self._grid_arrays:

                plate_models = [
                    plate_model for plate_model in image_model.fixture.plates
                    if plate_model.index == index
                ]
                if plate_models:
                    plate_model = plate_models[0]
                else:
                    self._logger.error(
                        "Expected to find a plate model with index {0}, but only have {1}"
                        .format(index, [
                            plate_model.index
                            for plate_model in image_model.fixture.plates
                        ]))
                    continue

                im = self.get_im_section(plate_model)

                if im is None:
                    self._logger.error(
                        "Plate model {0} could not be used to slice image".
                        format(plate_model))
                    continue

                if self._analysis_model.grid_model.gridding_offsets is not None and \
                        index < len(self._analysis_model.grid_model.gridding_offsets) \
                        and self._analysis_model.grid_model.gridding_offsets[index]:

                    reference_folder = self._analysis_model.grid_model.reference_grid_folder
                    if reference_folder:
                        reference_folder = os.path.join(
                            os.path.dirname(
                                self._analysis_model.output_directory),
                            reference_folder)
                    else:
                        reference_folder = self._analysis_model.output_directory

                    if not self._grid_arrays[index].set_grid(
                            im,
                            analysis_directory=self._analysis_model.
                            output_directory,
                            offset=self._analysis_model.grid_model.
                            gridding_offsets[index],
                            grid=os.path.join(
                                reference_folder,
                                Paths().grid_pattern.format(index + 1))):

                        self._logger.error(
                            "Could not use previous gridding with offset")

                else:

                    t = Thread(target=self._grid_arrays[index].detect_grid,
                               args=(im, ),
                               kwargs=dict(analysis_directory=self.
                                           _analysis_model.output_directory))
                    t.start()
                    threads.add(t)

            while threads:
                threads = set(t for t in threads if t.is_alive())
                sleep(0.01)

            call([
                "python", "-c",
                "from scanomatic.util.analysis import produce_grid_images; produce_grid_images('{0}')"
                .format(self._analysis_model.output_directory)
            ])

        return True

    def load_image(self, path):

        try:

            self.im = load_image_to_numpy(path, IMAGE_ROTATIONS.Portrait)
            self._im_loaded = True

        except (TypeError, IOError):

            alt_path = os.path.join(
                os.path.dirname(self._analysis_model.compilation),
                os.path.basename(path))

            self._logger.warning(
                "Failed to load image at '{0}', trying '{1}'.".format(
                    path, alt_path))
            try:

                self.im = load_image_to_numpy(alt_path,
                                              IMAGE_ROTATIONS.Portrait)
                self._im_loaded = True

            except (TypeError, IOError):

                self._im_loaded = False

        if self._im_loaded:
            self._logger.info("Image loaded")
        else:
            self._logger.error("Failed to load image")

        self.validate_rotation()
        self._convert_to_grayscale()

    def _convert_to_grayscale(self):
        if self.im.ndim == 3:
            self.im = np.dot(self.im[..., :3], [0.299, 0.587, 0.144])

    def validate_rotation(self):

        pass

    @property
    def orientation(self):
        """The currently loaded image's rotation considered as first dimension of image array being image rows
        :return:
        """
        if not self._im_loaded:
            return IMAGE_ROTATIONS.Unknown
        elif self.im.shape[0] > self.im.shape[1]:
            return IMAGE_ROTATIONS.Portrait
        else:
            return IMAGE_ROTATIONS.Landscape

    def get_im_section(self, plate_model, im=None):
        def _flip_axis(a, b):

            return b, a

        def _bound(bounds, a, b):
            def bounds_check(bound, val):

                if 0 <= val < bound:
                    return val
                elif val < 0:
                    return 0
                else:
                    return bound - 1

            return ((bounds_check(bounds[0],
                                  a[0]), bounds_check(bounds[0], a[1])),
                    (bounds_check(bounds[1],
                                  b[0]), bounds_check(bounds[1], b[1])))

        if not im:
            if self._im_loaded:
                im = self.im
            else:
                return

        x = sorted((plate_model.x1, plate_model.x2))
        y = sorted((plate_model.y1, plate_model.y2))

        if self.orientation == IMAGE_ROTATIONS.Landscape:
            x, y = _flip_axis(x, y)

        y, x = _bound(im.shape, y, x)

        # In images, the first dimension is typically the y-axis
        section = im[y[0]:y[1], x[0]:x[1]]

        return self._flip_short_dimension(section, im.shape)

    @staticmethod
    def _flip_short_dimension(section, im_shape):

        short_dim = [p == min(im_shape) for p in im_shape].index(True)

        def get_slicer(idx):
            if idx == short_dim:
                # noinspection PyTypeChecker
                return slice(None, None, -1)
            else:
                # noinspection PyTypeChecker
                return slice(None)

        slicer = []
        for i in range(len(im_shape)):
            slicer.append(get_slicer(i))

        return section[slicer]

    def _set_current_grid_move(self, d1, d2):

        self._grid_corrections = np.array((d1, d2))

    def clear_features(self):
        for grid_array in self._grid_arrays.itervalues():
            grid_array.clear_features()

    def analyse(self, image_model):
        """

        :type image_model: scanomatic.models.compile_project_model.CompileImageAnalysisModel
        """
        self.load_image(image_model.image.path)
        self._logger.info("Image loaded")
        if self._im_loaded is False:
            self.clear_features()
            return

        if not image_model.fixture.grayscale.values or not is_valid_grayscale(
                getGrayscale(image_model.fixture.grayscale.name)['targets'],
                image_model.fixture.grayscale.values):

            self._logger.warning("Not a valid grayscale")
            self.clear_features()
            return

        self.features.index = image_model.image.index
        grid_arrays_processed = set()
        # threads = set()
        for plate in image_model.fixture.plates:

            if plate.index in self._grid_arrays:
                grid_arrays_processed.add(plate.index)
                im = self.get_im_section(plate)
                grid_arr = self._grid_arrays[plate.index]
                """:type: scanomatic.image_analysis.grid_array.GridArray"""
                grid_arr.analyse(im, image_model)
                """
                t = Thread(target=grid_arr.analyse, args=(im, image_model))
                t.start()
                threads.add(t)

        while threads:
            threads = set(t for t in threads if t.is_alive())
            sleep(0.01)
        """
        for index, grid_array in self._grid_arrays.iteritems():
            if index not in grid_arrays_processed:
                grid_array.clear_features()

        self._logger.info("Image {0} processed".format(
            image_model.image.index))