예제 #1
0
    def __init__(self,
                 image,
                 position=(0, 0),
                 rgb_should_ascend=True,
                 max_valid_back_movement=0,
                 cyclic=False,
                 enabled=True):
        """
        Constructor - invoked when you create a new object by writing MoveByGradientValidator()

        :param image: Name of a BMP file, or the actual image (rectangular matrix of colors)
        :param position: See :attr:`~trajtracker.validators.MoveByGradientValidator.enabled`
        :param position: See :attr:`~trajtracker.validators.MoveByGradientValidator.position`
        :param rgb_should_ascend: See :attr:`~trajtracker.validators.MoveByGradientValidator.rgb_should_ascend`
        :param max_valid_back_movement: See :attr:`~trajtracker.validators.MoveByGradientValidator.max_valid_back_movement`
        :param cyclic: See :attr:`~trajtracker.validators.MoveByGradientValidator.cyclic`
        """
        trajtracker.TTrkObject.__init__(self)
        EnabledDisabledObj.__init__(self, enabled=enabled)

        self._lcm = LocationColorMap(image,
                                     position=position,
                                     use_mapping=True,
                                     colormap="RGB")
        self.rgb_should_ascend = rgb_should_ascend
        self.max_valid_back_movement = max_valid_back_movement
        self.cyclic = cyclic
        self.single_color = None
        self.reset()
        self._calc_min_max_colors()
    def __init__(self,
                 image,
                 position=(0, 0),
                 rgb_should_ascend=True,
                 max_valid_back_movement=0,
                 last_validated_rgb=None,
                 enabled=True):
        """
        Constructor

        :param image: Name of a BMP file, or the actual image (rectangular matrix of colors)
        :param position: See :attr:`~trajtracker.movement.MoveByGradientValidator.enabled`
        :param position: See :attr:`~trajtracker.movement.MoveByGradientValidator.position`
        :param rgb_should_ascend: See :attr:`~trajtracker.movement.MoveByGradientValidator.rgb_should_ascend`
        :param max_valid_back_movement: See :attr:`~trajtracker.movement.MoveByGradientValidator.max_valid_back_movement`
        :param last_validated_rgb: See :attr:`~trajtracker.movement.MoveByGradientValidator.last_validated_rgb`
        """
        super(MoveByGradientValidator, self).__init__(enabled=enabled)

        self._lcm = LocationColorMap(image,
                                     position=position,
                                     use_mapping=True,
                                     colormap="RGB")
        self.rgb_should_ascend = rgb_should_ascend
        self.max_valid_back_movement = max_valid_back_movement
        self.last_validated_rgb = last_validated_rgb
        self.reset()
 def test_colormap_default(self):
     lcm = LocationColorMap(testimage)
     lcm.colormap = "default"
     print("Default colormap:")
     print(lcm.colormap)
     self.assertEqual(lcm.colormap[(0,)], 0)
     self.assertEqual(lcm.colormap[(2,)], 1)
     self.assertEqual(lcm.colormap[(30,)], len(all_colors)-1)
 def test_colormap_rgb(self):
     lcm = LocationColorMap([[(0,0,255), (0,255,0), (255,0,0)]])
     lcm.colormap = "RGB"
     print("RGB colormap:")
     print(lcm.colormap)
     self.assertEqual(lcm.colormap[(0,0,255)], 255)
     self.assertEqual(lcm.colormap[(0,255,0)], 255*256)
     self.assertEqual(lcm.colormap[(255,0,0)], 255*256*256)
    def test_colormap_missing_colors(self):
        lcm = LocationColorMap(testimage)
        codes = {}
        i = 0
        for c in {0, 2, 5, 10}:
            codes[(c,)] = i
            i += 1

        try:
            lcm.colormap = codes
            self.fail("Succeeded setting an invalid value")
        except ValueError:
            pass
    def test_use_mapping_invalid(self):
        lcm = LocationColorMap(testimage)
        lcm.use_mapping = True
        lcm.use_mapping = False

        try:
            lcm.use_mapping = ""
            self.fail("Succeeded setting an invalid value for LocationColorMap.use_mapping")
        except TypeError:
            pass

        try:
            lcm.use_mapping = None
            self.fail("Succeeded setting an invalid value for LocationColorMap.use_mapping")
        except TypeError:
            pass
    def test_get_raw_colors(self):
        lcm = LocationColorMap(testimage)
        self.assertEqual(lcm.get_color_at(-3, -2), (0,))
        self.assertEqual(lcm.get_color_at(0, 0), (30,))
        self.assertEqual(lcm.get_color_at(2, 1), (15,))

        self.assertIsNone(lcm.get_color_at(-4, 0))
        self.assertIsNone(lcm.get_color_at(4, 0))
        self.assertIsNone(lcm.get_color_at(0, -3))
        self.assertIsNone(lcm.get_color_at(0, 3))
 def test_invalid_get_color_at_args(self):
     lcm = LocationColorMap(testimage)
     self.assertRaises(TypeError, lambda: lcm.get_color_at("", 0))
     self.assertRaises(TypeError, lambda: lcm.get_color_at(0.5, 0))
     self.assertRaises(TypeError, lambda: lcm.get_color_at(0, ""))
     self.assertRaises(TypeError, lambda: lcm.get_color_at(0, 0.5))
     self.assertRaises(TypeError, lambda: lcm.get_color_at(0, 0, use_mapping=""))
    def test_get_mapped_colors(self):
        lcm = LocationColorMap(testimage)
        codes = dict(zip([(c,) for c in all_colors], all_colors))  # map each (c,) to c
        lcm.colormap = codes
        self.assertEqual(lcm.get_color_at(-3, -2, use_mapping=True), 0)
        lcm.use_mapping = True
        self.assertEqual(lcm.get_color_at(0, 0), 30)
        self.assertEqual(lcm.get_color_at(2, 1), 15)

        self.assertIsNone(lcm.get_color_at(4, 0))
        self.assertIsNone(lcm.get_color_at(4, 0, use_mapping=True))
    def test_get_with_coord(self):
        lcm = LocationColorMap(testimage, position=(3,2))
        self.assertEqual(lcm.get_color_at(0, 0), (0,))
        self.assertEqual(lcm.get_color_at(3, 2), (30,))
        self.assertEqual(lcm.get_color_at(3, 4), (17,))

        self.assertIsNone(lcm.get_color_at(7, 0))
        self.assertIsNone(lcm.get_color_at(-1, 0))
        self.assertIsNone(lcm.get_color_at(0, -1))
        self.assertIsNone(lcm.get_color_at(0, 5))
예제 #11
0
    def __init__(self,
                 image,
                 enabled=True,
                 position=None,
                 default_valid=False):
        """
        Constructor

        :param image: Name of a BMP file, or the actual image (rectangular matrix of colors)
        :param enabled: See :attr:`~trajtracker.validators.LocationsValidator.enabled`
        :param position: See :attr:`~trajtracker.validators.LocationsValidator.position`
        :param default_valid: See :attr:`~trajtracker.validators.LocationsValidator.default_valid`
        """
        super(LocationsValidator, self).__init__(enabled=enabled)

        self._lcm = LocationColorMap(image,
                                     position=position,
                                     use_mapping=True,
                                     colormap="RGB")
        self.default_valid = default_valid
        self.valid_colors = set()
        self.invalid_colors = set()
 def test_rgb_mapping(self):
     lcm = LocationColorMap([[(0,0,255), (0,255,0), (255,0,0), (1,2,4), (0,0,0)]])
     lcm.colormap = "RGB"
     lcm.use_mapping = True
     self.assertEqual(lcm.get_color_at(-2, 0), 255)
     self.assertEqual(lcm.get_color_at(-1, 0), 255*256)
     self.assertEqual(lcm.get_color_at(0, 0), 255*256*256)
     self.assertEqual(lcm.get_color_at(1, 0), 4 + 2*256 + 1*256*256)
예제 #13
0
    def __init__(self,
                 image,
                 enabled=True,
                 position=None,
                 default_valid=False):
        """
        Constructor - invoked when you create a new object by writing LocationsValidator()

        :param image: Name of a BMP file, or the actual image (rectangular matrix of colors)
        :param enabled: See :attr:`~trajtracker.validators.LocationsValidator.enabled`
        :param position: See :attr:`~trajtracker.validators.LocationsValidator.position`
        :param default_valid: See :attr:`~trajtracker.validators.LocationsValidator.default_valid`
        """
        trajtracker.TTrkObject.__init__(self)
        EnabledDisabledObj.__init__(self, enabled=enabled)

        self._lcm = LocationColorMap(image,
                                     position=position,
                                     use_mapping=True,
                                     colormap="RGB")
        self.default_valid = default_valid
        self.valid_colors = set()
        self.invalid_colors = set()
예제 #14
0
class LocationsValidator(_BaseValidator):
    """
    This validator gets an image, and validates that the mouse/finger would be placed
    only on pixels of certain color(s).
    You can define either the valid colors or the invalid colors.
    """

    err_invalid_coordinates = "invalid_coords"
    arg_color = 'color'  # ValidationFailed exception argument: the color in the invalid location

    #------------------------------------------------------------
    def __init__(self,
                 image,
                 enabled=True,
                 position=None,
                 default_valid=False):
        """
        Constructor

        :param image: Name of a BMP file, or the actual image (rectangular matrix of colors)
        :param enabled: See :attr:`~trajtracker.validators.LocationsValidator.enabled`
        :param position: See :attr:`~trajtracker.validators.LocationsValidator.position`
        :param default_valid: See :attr:`~trajtracker.validators.LocationsValidator.default_valid`
        """
        super(LocationsValidator, self).__init__(enabled=enabled)

        self._lcm = LocationColorMap(image,
                                     position=position,
                                     use_mapping=True,
                                     colormap="RGB")
        self.default_valid = default_valid
        self.valid_colors = set()
        self.invalid_colors = set()

    #======================================================================
    #   Properties
    #======================================================================

    #-------------------------------------------------
    @property
    def position(self):
        """
        The position of the image: (x,y) tuple/list, indicating the image center
        For even-sized images, use the Expyriment standard.
        The position is used to align the image's coordinate space with that of check_xy()
        """
        return self._lcm.position

    @position.setter
    def position(self, value):
        self._lcm.position = value
        self._log_setter("position")

    #-------------------------------------------------
    @property
    def default_valid(self):
        """
        Indicates whether by default, all points are valid or not.
        If True: use :func:`~trajtracker.misc.LocationColorMap.invalid_colors` to indicate exceptions
        If False: use :func:`~trajtracker.misc.LocationColorMap.valid_colors` to indicate exceptions
        """
        return self._default_valid

    @default_valid.setter
    def default_valid(self, value):
        _u.validate_attr_type(self, "default_valid", value, bool)
        self._default_valid = value
        self._log_setter("default_valid")

    #-------------------------------------------------
    @property
    def valid_colors(self):
        return self._valid_colors

    @valid_colors.setter
    def valid_colors(self, value):
        self._valid_colors = self._get_colors_as_ints(value, "valid_colors")
        self._log_setter("valid_colors")

    #-------------------------------------------------
    @property
    def invalid_colors(self):
        return self._invalid_colors

    @invalid_colors.setter
    def invalid_colors(self, value):
        self._invalid_colors = self._get_colors_as_ints(value, "valid_colors")
        self._log_setter("invalid_colors")

    def _get_colors_as_ints(self, value, attr_name):
        if u.is_rgb(value):
            value = (value, )

        _u.validate_attr_type(self, attr_name, value, (list, tuple, set))

        colors = set()
        for c in value:
            if not u.is_rgb(c):
                raise ValueError(
                    _u.ErrMsg.attr_invalid_type(type(self), attr_name, "color",
                                                value))
            colors.add(u.color_rgb_to_num(c))

        return colors

    #======================================================================
    #   Validate
    #======================================================================

    def reset(self, time0=None):
        pass

    def check_xyt(self, x_coord, y_coord, time=None):
        """
        Check whether the given coordinate is a valid one

        :param time: ignored
        :return: None if all OK, ValidationFailed if error
        """
        self._check_xyt_validate_and_log(x_coord, y_coord, time, False)

        if not self._enabled:
            return None

        color = self._lcm.get_color_at(x_coord, y_coord)
        if self._default_valid:
            ok = color not in self._invalid_colors
        else:
            ok = color in self._valid_colors

        if ok:
            return None

        else:
            return self._create_validation_error(
                self.err_invalid_coordinates,
                "You moved to an invalid location", {self.arg_color: color})
예제 #15
0
class MoveByGradientValidator(trajtracker.TTrkObject, EnabledDisabledObj):

    max_irrelevant_color_value = 10
    cyclic_ratio = 5

    err_gradient = "GradientViolation"

    def __init__(self,
                 image,
                 position=(0, 0),
                 rgb_should_ascend=True,
                 max_valid_back_movement=0,
                 cyclic=False,
                 enabled=True):
        """
        Constructor - invoked when you create a new object by writing MoveByGradientValidator()

        :param image: Name of a BMP file, or the actual image (rectangular matrix of colors)
        :param position: See :attr:`~trajtracker.validators.MoveByGradientValidator.enabled`
        :param position: See :attr:`~trajtracker.validators.MoveByGradientValidator.position`
        :param rgb_should_ascend: See :attr:`~trajtracker.validators.MoveByGradientValidator.rgb_should_ascend`
        :param max_valid_back_movement: See :attr:`~trajtracker.validators.MoveByGradientValidator.max_valid_back_movement`
        :param cyclic: See :attr:`~trajtracker.validators.MoveByGradientValidator.cyclic`
        """
        trajtracker.TTrkObject.__init__(self)
        EnabledDisabledObj.__init__(self, enabled=enabled)

        self._lcm = LocationColorMap(image,
                                     position=position,
                                     use_mapping=True,
                                     colormap="RGB")
        self.rgb_should_ascend = rgb_should_ascend
        self.max_valid_back_movement = max_valid_back_movement
        self.cyclic = cyclic
        self.single_color = None
        self.reset()
        self._calc_min_max_colors()

    #======================================================================
    #   Properties
    #======================================================================

    #-------------------------------------------------
    @property
    def position(self):
        """
        The position of the image: (x,y) tuple/list, indicating the image center
        For even-sized images, use the Expyriment standard.
        The position is used to align the image's coordinate space with that of update_xyt()
        """
        return self._lcm.position

    @position.setter
    def position(self, value):
        self._lcm.position = value

    #-------------------------------------------------
    @property
    def rgb_should_ascend(self):
        """
        Whether the valid movement is from lower RGB codes to higher RGB codes (True) or vice versa (False)
        """
        return self._rgb_should_ascend

    @rgb_should_ascend.setter
    def rgb_should_ascend(self, value):
        _u.validate_attr_type(self, "rgb_should_ascend", value, bool)
        self._rgb_should_ascend = value
        self._log_property_changed("rgb_should_ascend")

    #-------------------------------------------------
    @property
    def max_valid_back_movement(self):
        """
        The maximal valid delta of color-change in the opposite direction that would still be allowed
        """
        return self._max_valid_back_movement

    @max_valid_back_movement.setter
    def max_valid_back_movement(self, value):
        _u.validate_attr_numeric(self, "max_valid_back_movement", value)
        _u.validate_attr_not_negative(self, "max_valid_back_movement", value)
        self._max_valid_back_movement = value
        self._log_property_changed("max_valid_back_movement")

    #-------------------------------------------------
    @property
    def single_color(self):
        """
        Consider only one color out of the three (red / green / blue) available in the image.
        Each pixel in the image has a value between 0 and 255 for each of the 3 colors.
        If you set a single color (by setting this attribute to "R"/"G"/"B"), the validator will consider
        only the value of the selected color. Furthermore, the validator considers only pixels that are purely of
        this color (e.g., if you select "B", it means that only pixels with blue=0-255, red=0 and green=0 are
        relevant for validation).

        To accomodate small possible mistakes in the generation of the BMP image, the validator allows for
        miniscule presence of the irrelevant colors: i.e., if you set single_color="B", the validator will
        consider only pixels with blue=0-255, red<10, and green<10 (the treshold 10 can be changed by setting
        MoveByGradientValidator.max_irrelevant_color_value); and for this pixels, the validator will consider
        only the blue value.
        """
        return self._single_color

    @single_color.setter
    def single_color(self, value):
        _u.validate_attr_type(self,
                              "single_color",
                              value,
                              str,
                              none_allowed=True)
        if value is not None and value not in self._colormaps:
            raise trajtracker.ValueError(
                "invalid value for {:}.single_color ({:}) - valid values are {:}"
                .format(_u.get_type_name(self), value,
                        ",".join(self._colormaps.keys())))

        self._single_color = value
        self._lcm.colormap = u.color_rgb_to_num if value is None else self._colormaps[
            value]
        self._calc_min_max_colors()
        self._log_property_changed("single_color")

    def _calc_min_max_colors(self):
        if self._single_color is None:
            mapping_func = u.color_rgb_to_num
        else:
            mapping_func = self._colormaps[self._single_color]

        colors = [mapping_func(color) for color in self._lcm.available_colors]
        colors = [c for c in colors if c is not None]
        self._min_available_color = None if len(colors) == 0 else min(colors)
        self._max_available_color = None if len(colors) == 0 else max(colors)

    _colormaps = {
        'R':
        lambda color: None if color[
            1] > MoveByGradientValidator.max_irrelevant_color_value or color[2]
        > MoveByGradientValidator.max_irrelevant_color_value else color[0],
        'G':
        lambda color: None if color[
            0] > MoveByGradientValidator.max_irrelevant_color_value or color[2]
        > MoveByGradientValidator.max_irrelevant_color_value else color[1],
        'B':
        lambda color: None if color[
            0] > MoveByGradientValidator.max_irrelevant_color_value or color[1]
        > MoveByGradientValidator.max_irrelevant_color_value else color[2],
    }

    #-------------------------------------------------
    @property
    def cyclic(self):
        """
        Whether the gradient is cyclic, i.e., allows moving between the darkest to the lightest color
        """
        return self._cyclic

    @cyclic.setter
    def cyclic(self, value):
        _u.validate_attr_type(self, "cyclic", value, bool)
        self._cyclic = value
        self._log_property_changed("cyclic")

    #======================================================================
    #   Validate
    #======================================================================

    #-----------------------------------------------------------------
    def reset(self, time0=None):
        """
        Reset the movement validation
        """

        self._log_func_enters("reset", [time0])

        self._last_color = None

    #-----------------------------------------------------------------
    def update_xyt(self, position, time_in_trial=None, time_in_session=None):
        """
        Validate the movement

        :param position: (x,y) coordinates
        :param time_in_trial: ignored
        :param time_in_session: ignored
        :return: None if all OK, ExperimentError if error
        """

        if not self._enabled:
            return None

        _u.update_xyt_validate_and_log(self, position)

        color = self._lcm.get_color_at(position[0], position[1])
        if color is None:  # color N/A -- can't validate
            self._last_color = None
            return None
        else:
            # casting "color" to int because get_color_at() may return uint8, and subtracting uint variables
            # would always yield a positive value
            color = int(color)

        if self._last_color is None:
            #-- Nothing to validate
            self._last_color = color
            return None

        expected_direction = 1 if self._rgb_should_ascend else -1
        rgb_delta = (color - self._last_color) * expected_direction
        if rgb_delta >= 0:
            #-- All is OK
            self._last_color = color
            return None

        if rgb_delta >= -self._max_valid_back_movement:
            #-- The movement was in the opposite color diredction, but only slightly:
            #-- Don't issue an error, but also don't update "last_color" - remember the previous one
            return None

        if self._cyclic and self._min_available_color is not None:
            color_range = self._max_available_color - self._min_available_color
            if np.abs(rgb_delta) >= self.cyclic_ratio * (color_range -
                                                         np.abs(rgb_delta)):
                # It's much more likely to interpret this movement as a "cyclic" movement - i.e., one that crossed
                # the boundary of lightest-to-darkest (or the other way around, depending on the ascend/descend direction)
                self._last_color = color
                return None

        if self._should_log(ttrk.log_debug):
            self._log_write(
                "InvalidDirection,last_color={:},curr_color={:}".format(
                    self._last_color, color), True)

        return trajtracker.validators.create_experiment_error(
            self, self.err_gradient, "You moved in an invalid direction")
예제 #16
0
class LocationsValidator(trajtracker.TTrkObject, EnabledDisabledObj):

    err_invalid_coordinates = "InvalidCoords"
    arg_color = 'color'  # ExperimentError argument: the color in the invalid location

    #------------------------------------------------------------
    def __init__(self,
                 image,
                 enabled=True,
                 position=None,
                 default_valid=False):
        """
        Constructor - invoked when you create a new object by writing LocationsValidator()

        :param image: Name of a BMP file, or the actual image (rectangular matrix of colors)
        :param enabled: See :attr:`~trajtracker.validators.LocationsValidator.enabled`
        :param position: See :attr:`~trajtracker.validators.LocationsValidator.position`
        :param default_valid: See :attr:`~trajtracker.validators.LocationsValidator.default_valid`
        """
        trajtracker.TTrkObject.__init__(self)
        EnabledDisabledObj.__init__(self, enabled=enabled)

        self._lcm = LocationColorMap(image,
                                     position=position,
                                     use_mapping=True,
                                     colormap="RGB")
        self.default_valid = default_valid
        self.valid_colors = set()
        self.invalid_colors = set()

    #======================================================================
    #   Properties
    #======================================================================

    #-------------------------------------------------
    @property
    def position(self):
        """
        The position of the image: (x,y) tuple/list, indicating the image center
        For even-sized images, use the Expyriment standard.
        The position is used to align the image's coordinate space with that of update_xyt()
        """
        return self._lcm.position

    @position.setter
    def position(self, value):
        self._lcm.position = value
        self._log_property_changed("position")

    #-------------------------------------------------
    @property
    def default_valid(self):
        """
        Indicates whether by default, all points are valid or not.
        If True: use :func:`~trajtracker.misc.LocationColorMap.invalid_colors` to indicate exceptions
        If False: use :func:`~trajtracker.misc.LocationColorMap.valid_colors` to indicate exceptions
        """
        return self._default_valid

    @default_valid.setter
    def default_valid(self, value):
        _u.validate_attr_type(self, "default_valid", value, bool)
        self._default_valid = value
        self._log_property_changed("default_valid")

    #-------------------------------------------------
    @property
    def valid_colors(self):
        return self._valid_colors

    @valid_colors.setter
    def valid_colors(self, value):
        self._valid_colors = self._get_colors_as_ints(value, "valid_colors")
        self._log_property_changed("valid_colors")

    #-------------------------------------------------
    @property
    def invalid_colors(self):
        return self._invalid_colors

    @invalid_colors.setter
    def invalid_colors(self, value):
        self._invalid_colors = self._get_colors_as_ints(value, "valid_colors")
        self._log_property_changed("invalid_colors")

    def _get_colors_as_ints(self, value, attr_name):
        if u.is_rgb(value):
            value = (value, )

        _u.validate_attr_is_collection(self, attr_name, value, allow_set=True)

        colors = set()
        for c in value:
            if not u.is_rgb(c):
                raise trajtracker.ValueError(
                    _u.ErrMsg.attr_invalid_type(type(self), attr_name, "color",
                                                value))
            colors.add(u.color_rgb_to_num(c))

        return colors

    #======================================================================
    #   Validate
    #======================================================================

    #----------------------------------------------------------
    def reset(self, time0=None):
        pass

    #----------------------------------------------------------
    def update_xyt(self, position, time_in_trial=None, time_in_session=None):
        """
        Check whether the given coordinate is a valid one

        :param time_in_trial: ignored
        :param time_in_session: ignored
        :return: None if all OK, ExperimentError if error
        """
        _u.update_xyt_validate_and_log(self, position)

        if not self._enabled:
            return None

        color = self._lcm.get_color_at(position[0], position[1])
        if self._default_valid:
            ok = color not in self._invalid_colors
        else:
            ok = color in self._valid_colors

        if ok:
            return None

        else:
            return trajtracker.validators.create_experiment_error(
                self, self.err_invalid_coordinates,
                "You moved to an invalid location", {self.arg_color: color})
class MoveByGradientValidator(_BaseValidator):
    """
    This validator gets an image, and allows mouse to move only according to it -
    from a light color to a darker color (or vice versa).
    """

    err_gradient = "gradient_violation"

    def __init__(self,
                 image,
                 position=(0, 0),
                 rgb_should_ascend=True,
                 max_valid_back_movement=0,
                 last_validated_rgb=None,
                 enabled=True):
        """
        Constructor

        :param image: Name of a BMP file, or the actual image (rectangular matrix of colors)
        :param position: See :attr:`~trajtracker.movement.MoveByGradientValidator.enabled`
        :param position: See :attr:`~trajtracker.movement.MoveByGradientValidator.position`
        :param rgb_should_ascend: See :attr:`~trajtracker.movement.MoveByGradientValidator.rgb_should_ascend`
        :param max_valid_back_movement: See :attr:`~trajtracker.movement.MoveByGradientValidator.max_valid_back_movement`
        :param last_validated_rgb: See :attr:`~trajtracker.movement.MoveByGradientValidator.last_validated_rgb`
        """
        super(MoveByGradientValidator, self).__init__(enabled=enabled)

        self._lcm = LocationColorMap(image,
                                     position=position,
                                     use_mapping=True,
                                     colormap="RGB")
        self.rgb_should_ascend = rgb_should_ascend
        self.max_valid_back_movement = max_valid_back_movement
        self.last_validated_rgb = last_validated_rgb
        self.reset()

    #======================================================================
    #   Properties
    #======================================================================

    #-------------------------------------------------
    @property
    def position(self):
        """
        The position of the image: (x,y) tuple/list, indicating the image center
        For even-sized images, use the Expyriment standard.
        The position is used to align the image's coordinate space with that of check_xy()
        """
        return self._lcm.position

    @position.setter
    def position(self, value):
        self._lcm.position = value

    #-------------------------------------------------
    @property
    def rgb_should_ascend(self):
        """
        Whether the valid movement is from lower RGB codes to higher RGB codes (True) or vice versa (False)
        """
        return self._rgb_should_ascend

    @rgb_should_ascend.setter
    def rgb_should_ascend(self, value):
        _u.validate_attr_type(self, "rgb_should_ascend", value, bool)
        self._rgb_should_ascend = value
        self._log_setter("rgb_should_ascend")

    #-------------------------------------------------
    @property
    def last_validated_rgb(self):
        """
        The last RGB color code to be validated (number between 0 and 65535; when assigning, you can also
        specify a list/tuple of 3 integers, each 0-255).
        If the last mouse position was on a color with RGB higher than this (or lower for rgb_should_ascend=False),
        validation is not done. This is intended to allow for "cyclic" movement, i.e., allow to "cross" the 0
        (e.g. from 0 to 65535 and then back to 0).
        Setting the value to None disables this feature
        """
        return self._last_validated_rgb

    @last_validated_rgb.setter
    def last_validated_rgb(self, value):
        if u.is_rgb(value):
            self._last_validated_rgb = u.color_rgb_to_num(value)
        else:
            if value is not None:
                _u.validate_attr_numeric(self, "last_validated_rgb", value)
                _u.validate_attr_not_negative(self, "last_validated_rgb",
                                              value)
            self._last_validated_rgb = value

        self._log_setter("last_validated_rgb")

    #-------------------------------------------------
    @property
    def max_valid_back_movement(self):
        """
        The maximal valid delta of color-change in the opposite direction that would still be allowed
        """
        return self._max_valid_back_movement

    @max_valid_back_movement.setter
    def max_valid_back_movement(self, value):
        _u.validate_attr_numeric(self, "max_valid_back_movement", value)
        _u.validate_attr_not_negative(self, "max_valid_back_movement", value)
        self._max_valid_back_movement = value
        self._log_setter("max_valid_back_movement")

    #======================================================================
    #   Validate
    #======================================================================

    #-----------------------------------------------------------------
    def reset(self, time0=None):
        """
        Reset the movement validation
        """
        self._last_color = None

    #-----------------------------------------------------------------
    def check_xyt(self, x_coord, y_coord, time=None):
        """
        Validate the movement

        :return: None if all OK, ValidationFailed if error
        """

        self._check_xyt_validate_and_log(x_coord, y_coord, time, False)

        if not self._enabled:
            return None

        color = self._lcm.get_color_at(x_coord, y_coord)
        if color is None:
            return None  # can't validate

        if self._last_color is None:
            #-- Nothing to validate
            self._last_color = color
            return None

        expected_direction = 1 if self._rgb_should_ascend else -1
        rgb_delta = (color - self._last_color) * expected_direction
        if rgb_delta >= 0:
            #-- All is OK
            self._last_color = color
            return None

        if rgb_delta >= -self._max_valid_back_movement:
            #-- The movement was in the opposite color diredction, but only slightly:
            #-- Don't issue an error, but also don't update "last_color" - remember the previous one
            return None

        #-- Invalid situation!

        if self._last_validated_rgb is not None and \
                ((self._rgb_should_ascend and self._last_color > self._last_validated_rgb) or
                 (not self._rgb_should_ascend and self._last_color < self._last_validated_rgb)):
            #-- Previous color is very close to 0 - avoid validating, in order to allow "crossing the 0 color"
            return None

        return self._create_validation_error(
            self.err_gradient, "You moved in an invalid direction")
    def test_get_even_size(self):
        img1 = [r[:-1] for r in testimage[:-1]]  # remove last row and column

        lcm = LocationColorMap(img1)
        self.assertEqual(lcm.get_color_at(-1, 0), (5,))
 def test_colormap_missing(self):
     lcm = LocationColorMap(testimage)
     self.assertRaises(ValueError, lambda: lcm.get_color_at(0, 0, use_mapping=True))
 def test_get_default_props(self):
     lcm = LocationColorMap(testimage)
     self.assertEqual(frozenset([(c,) for c in all_colors]), lcm.available_colors)
     self.assertIsNone(lcm.colormap)
 def test_colormap_good(self):
     lcm = LocationColorMap(testimage)
     lcm.colormap = self.get_good_colormap()