from __future__ import print_function, division, absolute_import

import sys
# unittest only added in 3.4 self.subTest()
if sys.version_info[0] < 3 or sys.version_info[1] < 4:
    import unittest2 as unittest
else:
    import unittest
# unittest.mock is not available in 2.7 (though unittest2 might contain it?)
try:
    import unittest.mock as mock
except ImportError:
    import mock

import numpy as np

import imgaug as ia
from imgaug.testutils import reseed, wrap_shift_deprecation, assertWarns
from imgaug.augmentables.lines import LineString, LineStringsOnImage
from imgaug.augmentables.kps import Keypoint
from imgaug.augmentables.heatmaps import HeatmapsOnImage


class TestLineString_project_(unittest.TestCase):
    @property
    def _is_inplace(self):
        return True

    def _func(self, ls, from_shape, to_shape):
        return ls.project_(from_shape, to_shape)

    def test_project_to_2x_image_size(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        ls_proj = self._func(ls, (10, 10), (20, 20))
        assert np.allclose(ls_proj.coords, [(0, 0), (2, 0), (4, 2)])

    def test_project_to_2x_image_width(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        ls_proj = self._func(ls, (10, 10), (10, 20))
        assert np.allclose(ls_proj.coords, [(0, 0), (2, 0), (4, 1)])

    def test_project_to_2x_image_height(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        ls_proj = self._func(ls, (10, 10), (20, 10))
        assert np.allclose(ls_proj.coords, [(0, 0), (1, 0), (2, 2)])

    def test_inplaceness(self):
        ls = ia.LineString([(0, 0), (1, 0)])
        ls2 = self._func(ls, (10, 10), (10, 10))
        if self._is_inplace:
            assert ls is ls2
        else:
            assert ls is not ls2


class TestLineString_project(TestLineString_project_):
    @property
    def _is_inplace(self):
        return False

    def _func(self, ls, from_shape, to_shape):
        return ls.project(from_shape, to_shape)


class TestLineString_shift_(unittest.TestCase):
    @property
    def _is_inplace(self):
        return True

    def _func(self, ls, *args, **kwargs):
        def _func_impl():
            return ls.shift_(*args, **kwargs)

        return wrap_shift_deprecation(_func_impl, *args, **kwargs)

    def test_shift_along_xy(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        ls_shift = self._func(ls.deepcopy(), x=1, y=2)
        assert ls_shift.coords_almost_equals(
            [(0+1, 0+2), (1+1, 0+2), (2+1, 1+2)])

    def test_inplaceness(self):
        ls = ia.LineString([(0, 0), (1, 0)])
        ls2 = self._func(ls, y=0)
        if self._is_inplace:
            assert ls is ls2
        else:
            assert ls is not ls2


class TestLineString_shift(TestLineString_shift_):
    @property
    def _is_inplace(self):
        return False

    def _func(self, ls, *args, **kwargs):
        def _func_impl():
            return ls.shift(*args, **kwargs)

        return wrap_shift_deprecation(_func_impl, *args, **kwargs)

    def test_shift_by_positive_args(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        assert self._func(ls.deepcopy(), top=1).coords_almost_equals(
            [(0, 1), (1, 1), (2, 2)])
        assert self._func(ls.deepcopy(), right=1).coords_almost_equals(
            [(-1, 0), (0, 0), (1, 1)])
        assert self._func(ls.deepcopy(), bottom=1).coords_almost_equals(
            [(0, -1), (1, -1), (2, 0)])
        assert self._func(ls.deepcopy(), left=1).coords_almost_equals(
            [(1, 0), (2, 0), (3, 1)])

    def test_shift_by_negative_values(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        assert self._func(ls.deepcopy(), top=-1).coords_almost_equals(
            [(0, -1), (1, -1), (2, 0)])
        assert self._func(ls.deepcopy(), right=-1).coords_almost_equals(
            [(1, 0), (2, 0), (3, 1)])
        assert self._func(ls.deepcopy(), bottom=-1).coords_almost_equals(
            [(0, 1), (1, 1), (2, 2)])
        assert self._func(ls.deepcopy(), left=-1).coords_almost_equals(
            [(-1, 0), (0, 0), (1, 1)])

    def test_shift_by_positive_values_all_arguments_provided(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        assert self._func(
            ls.deepcopy(), top=1, right=2, bottom=3, left=4
        ).coords_almost_equals(
            [(0-2+4, 0+1-3), (1-2+4, 0+1-3), (2-2+4, 1+1-3)])

    def test_shift_of_empty_line_string(self):
        ls = LineString([])
        assert self._func(
            ls.deepcopy(), top=1, right=2, bottom=3, left=4
        ).coords_almost_equals([])


class TestLineString(unittest.TestCase):
    def setUp(self):
        reseed()

    def test___init___float32_array_as_coords(self):
        ls = LineString(np.float32([[0, 0], [1, 2]]))
        assert np.allclose(ls.coords, np.float32([[0, 0], [1, 2]]))
        assert ls.label is None

    def test___init___list_of_tuples_as_coords(self):
        ls = LineString([(0, 0), (1, 2)])
        assert np.allclose(ls.coords, np.float32([[0, 0], [1, 2]]))
        assert ls.label is None

    def test___init___empty_list_as_coords(self):
        ls = LineString([])
        assert ls.coords.shape == (0, 2)
        assert ls.label is None

    def test___init___label_set(self):
        ls = LineString([], label="test")
        assert ls.coords.shape == (0, 2)
        assert ls.label == "test"

    def test_length_with_triangle(self):
        ls = LineString(np.float32([[0, 0], [1, 0], [1, 1]]))
        assert np.isclose(ls.length, 2.0)

    def test_length_with_realworld_line_string(self):
        ls = LineString(np.float32([[0, 0], [1, 2], [4, 5]]))
        assert np.isclose(ls.length,
                          np.sqrt(1**2 + 2**2) + np.sqrt(3**2 + 3**2))

    def test_length_with_single_point(self):
        ls = LineString([(0, 0)])
        assert np.isclose(ls.length, 0.0)

    def test_length_with_zero_points(self):
        ls = LineString([])
        assert np.isclose(ls.length, 0.0)

    def test_xx_three_points(self):
        ls = LineString(np.float32([[0, 0], [1, 0], [2, 1]]))
        assert np.allclose(ls.xx, np.float32([0, 1, 2]))

    def test_xx_no_points(self):
        ls = LineString([])
        assert np.allclose(ls.xx, np.zeros((0,), dtype=np.float32))

    def test_yy_three_points(self):
        ls = LineString(np.float32([[0, 0], [0, 1], [0, 2]]))
        assert np.allclose(ls.yy, np.float32([0, 1, 2]))

    def test_yy_no_points(self):
        ls = LineString([])
        assert np.allclose(ls.yy, np.zeros((0,), dtype=np.float32))

    def test_xx_int_three_points(self):
        ls = LineString(np.float32([[0, 0], [1.4, 0], [2.6, 1]]))
        assert ls.xx_int.dtype.name == "int32"
        assert np.array_equal(ls.xx_int, np.int32([0, 1, 3]))

    def test_xx_int_no_points(self):
        ls = LineString([])
        assert ls.xx_int.dtype.name == "int32"
        assert np.array_equal(ls.xx_int, np.zeros((0,), dtype=np.int32))

    def test_yy_int_three_points(self):
        ls = LineString(np.float32([[0, 0], [0, 1.4], [1, 2.6]]))
        assert ls.yy_int.dtype.name == "int32"
        assert np.array_equal(ls.yy_int, np.int32([0, 1, 3]))

    def test_yy_int_no_points(self):
        ls = LineString([])
        assert ls.yy_int.dtype.name == "int32"
        assert np.array_equal(ls.yy_int, np.zeros((0,), dtype=np.int32))

    def test_height_three_points(self):
        ls = LineString(np.float32([[0, 0], [0, 1.4], [1, 2.6]]))
        assert np.isclose(ls.height, 2.6)

    def test_height_no_points(self):
        ls = LineString([])
        assert np.isclose(ls.height, 0.0)

    def test_width_three_points(self):
        ls = LineString(np.float32([[0, 0], [1.4, 0], [2.6, 1]]))
        assert np.isclose(ls.width, 2.6)

    def test_width_no_points(self):
        ls = LineString([])
        assert np.isclose(ls.width, 0.0)

    def test_get_pointwise_inside_image_mask_with_single_point_tuple(self):
        ls = LineString([(0, 0), (1.4, 0), (2.6, 1)])
        mask = ls.get_pointwise_inside_image_mask((2, 2))
        assert np.array_equal(mask, [True, True, False])

    def test_get_pointwise_inside_image_mask_with_array(self):
        ls = LineString([(0, 0), (1.4, 0), (2.6, 1)])
        mask = ls.get_pointwise_inside_image_mask(
            np.zeros((2, 2), dtype=np.uint8))
        assert np.array_equal(mask, [True, True, False])

    def test_get_pointwise_inside_image_mask_with_single_point_ls(self):
        ls = LineString([(0, 0)])
        mask = ls.get_pointwise_inside_image_mask((2, 2))
        assert np.array_equal(mask, [True])

    def test_get_pointwise_inside_image_mask_with_zero_points_ls(self):
        ls = LineString([])
        mask = ls.get_pointwise_inside_image_mask((2, 2))
        assert mask.shape == (0,)

    def test_compute_neighbour_distances_three_points(self):
        ls = LineString([(0, 0), (1.4, 0), (2.6, 1)])
        dists = ls.compute_neighbour_distances()
        assert np.allclose(dists, [np.sqrt(1.4**2), np.sqrt(1.2**2+1**2)])

    def test_compute_neighbour_distances_two_points(self):
        ls = LineString([(0, 0), (1.4, 0)])
        dists = ls.compute_neighbour_distances()
        assert np.allclose(dists, [np.sqrt(1.4**2)])

    def test_compute_neighbour_distances_single_point(self):
        ls = LineString([(0, 0)])
        dists = ls.compute_neighbour_distances()
        assert dists.shape == (0,)

    def test_compute_neighbour_distances_zero_points(self):
        ls = LineString([])
        dists = ls.compute_neighbour_distances()
        assert dists.shape == (0,)

    def test_compute_pointwise_distances_to_point_at_origin(self):
        ls = LineString([(0, 0), (5, 0), (5, 5)])
        dists = ls.compute_pointwise_distances((0, 0))
        assert np.allclose(dists, [0,
                                   5,
                                   np.sqrt(5**2 + 5**2)])

    def test_compute_pointwise_distances_to_point_at_x1_y1(self):
        ls = LineString([(0, 0), (5, 0), (5, 5)])
        dists = ls.compute_pointwise_distances((1, 1))
        assert np.allclose(dists, [np.sqrt(1**2 + 1**2),
                                   np.sqrt(4**2 + 1**2),
                                   np.sqrt(4**2 + 4**2)])

    def test_compute_pointwise_distances_from_empty_line_string(self):
        ls = LineString([])
        dists = ls.compute_pointwise_distances((1, 1))
        assert dists == []

    def test_compute_pointwise_distances_to_keypoint_at_origin(self):
        ls = LineString([(0, 0), (5, 0), (5, 5)])
        dists = ls.compute_pointwise_distances(Keypoint(x=0, y=0))
        assert np.allclose(dists, [0, 5, np.sqrt(5**2 + 5**2)])

    def test_compute_pointwise_distances_to_keypoint_at_x1_y1(self):
        ls = LineString([(0, 0), (5, 0), (5, 5)])
        dists = ls.compute_pointwise_distances(Keypoint(x=1, y=1))
        assert np.allclose(dists, [np.sqrt(1**2 + 1**2),
                                   np.sqrt(4**2 + 1**2),
                                   np.sqrt(4**2 + 4**2)])

    def test_compute_pointwise_distances_to_other_line_string_at_origin(self):
        # line string
        ls = LineString([(0, 0), (5, 0), (5, 5)])
        other = LineString([(0, 0)])
        dists = ls.compute_pointwise_distances(other)
        assert np.allclose(dists, [0,
                                   5,
                                   np.sqrt(5**2 + 5**2)])

    def test_compute_pointwise_distances_to_other_line_string_at_x1_y1(self):
        ls = LineString([(0, 0), (5, 0), (5, 5)])
        other = LineString([(1, 1)])
        dists = ls.compute_pointwise_distances(other)
        assert np.allclose(dists, [np.sqrt(1**2 + 1**2),
                                   np.sqrt(4**2 + 1**2),
                                   np.sqrt(4**2 + 4**2)])

    def test_compute_pointwise_distances_to_other_line_string_two_points(self):
        ls = LineString([(0, 0), (5, 0), (5, 5)])
        other = LineString([(0, -1), (5, -1)])
        dists = ls.compute_pointwise_distances(other)
        assert np.allclose(dists, [np.sqrt(0**2 + 1**2),
                                   np.sqrt(0**2 + 1**2),
                                   np.sqrt(0**2 + 6**2)])

    def test_compute_pointwise_distances_to_other_empty_line_string(self):
        ls = LineString([(0, 0), (5, 0), (5, 5)])
        other = LineString([])
        dists = ls.compute_pointwise_distances(other, default=False)
        assert dists is False

    def test_compute_distance_from_three_point_line_string(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        points = [(0, 0), (1, 0), (0, 1), (-0.5, -0.6)]
        expecteds = [0, 0, 1, np.sqrt(0.5**2 + 0.6**2)]

        for point, expected in zip(points, expecteds):
            with self.subTest(point=point):
                assert np.isclose(ls.compute_distance(point), expected)

    def test_compute_distance_from_single_point_line_string(self):
        ls = LineString([(0, 0)])
        points = [(0, 0), (-0.5, -0.6)]
        expecteds = [0, np.sqrt(0.5**2 + 0.6**2)]

        for point, expected in zip(points, expecteds):
            with self.subTest(point=point):
                assert np.isclose(ls.compute_distance(point), expected)

    def test_compute_distance_from_empty_line_string_no_default(self):
        ls = LineString([])
        assert ls.compute_distance((0, 0)) is None

    def test_compute_distance_from_empty_line_string_with_default(self):
        ls = LineString([])
        assert ls.compute_distance((0, 0), default=-1) == -1

    def test_compute_distance_to_keypoint(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        assert np.isclose(ls.compute_distance(ia.Keypoint(x=0, y=1)), 1)

    def test_compute_distance_to_line_string(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        points = [
            [(0, 0)],
            [(0, 1)],
            [(0, 0), (0, 1)],
            [(-1, -1), (-1, 1)]
        ]
        expecteds = [0, 1, 0, 1]

        for point, expected in zip(points, expecteds):
            with self.subTest(point=point):
                assert np.isclose(ls.compute_distance(LineString(point)),
                                  expected)

    def test_compute_distance_to_invalid_datatype(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        with self.assertRaises(ValueError):
            assert ls.compute_distance("foo")

    def test_contains_tuple(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        points = [(100, 0), (0, 0), (1, 0), (2, 1), (0+1e-8, 0), (0-1, 0)]
        expecteds = [False, True, True, True, True, False]

        for point, expected in zip(points, expecteds):
            with self.subTest(point=point):
                assert ls.contains(point) is expected

    def test_contains_tuple_max_distance(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        points = [(0+1e-8, 0), (0-1, 0)]
        max_distances = [0, 2]
        expecteds = [False, True]

        for point, max_distance, expected in zip(points, max_distances,
                                                 expecteds):
            with self.subTest(point=point, max_distance=max_distance):
                assert (
                    ls.contains(point, max_distance=max_distance)
                    is expected
                )

    def test_contains_keypoint(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        points = [(100, 0), (0, 0), (1, 0), (2, 1), (0+1e-8, 0), (0-1, 0)]
        expecteds = [False, True, True, True, True, False]

        for point, expected in zip(points, expecteds):
            with self.subTest(point=point):
                assert (
                    ls.contains(Keypoint(x=point[0], y=point[1]))
                    is expected
                )

    def test_contains_with_single_point_line_string(self):
        ls = LineString([(0, 0)])
        assert ls.contains((0, 0))
        assert not ls.contains((1, 0))

    def test_contains_with_empty_line_string(self):
        ls = LineString([])
        assert not ls.contains((0, 0))
        assert not ls.contains((1, 0))

    def test_project_empty_line_string_to_2x_image_size(self):
        ls = LineString([])
        ls_proj = ls.project((10, 10), (20, 20))
        assert ls_proj.coords.shape == (0, 2)

    def test_compute_out_of_image_fraction__no_points(self):
        ls = LineString([])
        image_shape = (100, 200, 3)
        factor = ls.compute_out_of_image_fraction(image_shape)
        assert np.isclose(factor, 0.0)

    def test_compute_out_of_image_fraction__one_point(self):
        ls = LineString([(1.0, 2.0)])
        image_shape = (100, 200, 3)
        factor = ls.compute_out_of_image_fraction(image_shape)
        assert np.isclose(factor, 0.0)

    def test_compute_out_of_image_fraction__one_point__ooi(self):
        ls = LineString([(-10.0, -20.0)])
        image_shape = (100, 200, 3)
        factor = ls.compute_out_of_image_fraction(image_shape)
        assert np.isclose(factor, 1.0)

    def test_compute_out_of_image_fraction__two_points(self):
        ls = LineString([(1.0, 2.0), (10.0, 20.0)])
        image_shape = (100, 200, 3)
        factor = ls.compute_out_of_image_fraction(image_shape)
        assert np.isclose(factor, 0.0)

    def test_compute_out_of_image_fraction__three_points_at_same_pos(self):
        ls = LineString([(10.0, 20.0), (10.0, 20.0), (10.0, 20.0)])
        image_shape = (100, 200, 3)
        factor = ls.compute_out_of_image_fraction(image_shape)
        assert len(ls.coords) == 3
        assert np.isclose(factor, 0.0)

    def test_compute_out_of_image_fraction__partially_ooi(self):
        ls = LineString([(9.0, 1.0), (11.0, 1.0)])
        image_shape = (10, 10, 3)
        factor = ls.compute_out_of_image_fraction(image_shape)
        assert np.isclose(factor, 0.5, atol=1e-3)

    def test_compute_out_of_image_fraction__leaves_image_multiple_times(self):
        ls = LineString([(9.0, 1.0), (11.0, 1.0), (11.0, 3.0),
                         (9.0, 3.0), (9.0, 5.0), (11.0, 5.0)])
        image_shape = (10, 10, 3)
        factor = ls.compute_out_of_image_fraction(image_shape)
        assert np.isclose(factor, 0.5, atol=1e-3)

    def test_compute_out_of_image_fraction__fully_ooi(self):
        ls = LineString([(15.0, 15.0), (20.0, 15.0)])
        image_shape = (10, 10, 3)
        factor = ls.compute_out_of_image_fraction(image_shape)
        assert np.isclose(factor, 1.0)

    def test_is_fully_within_image_with_simple_line_string(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        assert ls.is_fully_within_image((10, 10))
        assert ls.is_fully_within_image((2, 3))
        assert not ls.is_fully_within_image((2, 2))
        assert not ls.is_fully_within_image((1, 1))

    def test_is_fully_within_image_with_negative_coords_line_string(self):
        ls = LineString([(-1, 0), (1, 0), (2, 1)])
        assert not ls.is_fully_within_image((10, 10))
        assert not ls.is_fully_within_image((2, 3))
        assert not ls.is_fully_within_image((2, 2))
        assert not ls.is_fully_within_image((1, 1))

    def test_is_fully_within_image_with_single_point_line_string(self):
        ls = LineString([(0, 0)])
        assert ls.is_fully_within_image((10, 10))
        assert ls.is_fully_within_image((2, 3))
        assert ls.is_fully_within_image((2, 2))
        assert ls.is_fully_within_image((1, 1))

    def test_is_fully_within_image_with_empty_line_string(self):
        ls = LineString([])
        assert not ls.is_fully_within_image((10, 10))
        assert not ls.is_fully_within_image((2, 3))
        assert not ls.is_fully_within_image((2, 2))
        assert not ls.is_fully_within_image((1, 1))

    def test_is_fully_within_image_with_empty_line_string_default_set(self):
        ls = LineString([])
        assert ls.is_fully_within_image((10, 10), default=True)
        assert ls.is_fully_within_image((2, 3), default=True)
        assert ls.is_fully_within_image((2, 2), default=True)
        assert ls.is_fully_within_image((1, 1), default=True)
        assert ls.is_fully_within_image((10, 10), default=None) is None

    def test_is_partly_within_image_with_simple_line_string(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        assert ls.is_partly_within_image((10, 10))
        assert ls.is_partly_within_image((2, 3))
        assert ls.is_partly_within_image((2, 2))
        assert ls.is_partly_within_image((1, 1))

    def test_is_partly_within_image_with_simple_line_string2(self):
        ls = LineString([(1, 0), (2, 0), (3, 1)])
        assert ls.is_partly_within_image((10, 10))
        assert ls.is_partly_within_image((2, 3))
        assert ls.is_partly_within_image((2, 2))
        assert not ls.is_partly_within_image((1, 1))

    def test_is_partly_within_image_with_ls_cutting_through_image(self):
        # line string that cuts through the middle of the image,
        # with both points outside of a BB (0, 0), (10, 10)
        ls = LineString([(-1, 5), (11, 5)])
        assert ls.is_partly_within_image((100, 100))
        assert ls.is_partly_within_image((10, 12))
        assert ls.is_partly_within_image((10, 10))
        assert ls.is_partly_within_image((10, 1))
        assert not ls.is_partly_within_image((1, 1))

    def test_is_partly_within_image_with_line_string_around_rectangle(self):
        # line string around inner rectangle of (-1, -1), (11, 11)
        ls = LineString([(-1, -1), (11, -1), (11, 11), (-1, 11)])
        assert ls.is_partly_within_image((100, 100))
        assert ls.is_partly_within_image((12, 12))
        assert not ls.is_partly_within_image((10, 10))

    def test_is_partly_within_image_with_single_point_line_string(self):
        ls = LineString([(11, 11)])
        assert ls.is_partly_within_image((100, 100))
        assert ls.is_partly_within_image((12, 12))
        assert not ls.is_partly_within_image((10, 10))

    def test_is_partly_within_image_with_empty_line_string(self):
        ls = LineString([])
        assert not ls.is_partly_within_image((100, 100))
        assert not ls.is_partly_within_image((10, 10))
        assert ls.is_partly_within_image((100, 100), default=True)
        assert ls.is_partly_within_image((10, 10), default=True)
        assert ls.is_partly_within_image((100, 100), default=None) is None
        assert ls.is_partly_within_image((10, 10), default=None) is None

    def test_is_out_of_image_with_simple_line_string(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        assert not ls.is_out_of_image((10, 10))
        assert ls.is_out_of_image((1, 1), fully=False, partly=True)
        assert not ls.is_out_of_image((1, 1), fully=True, partly=False)
        assert ls.is_out_of_image((1, 1), fully=True, partly=True)
        assert not ls.is_out_of_image((1, 1), fully=False, partly=False)

    def test_is_out_of_image_with_empty_line_string(self):
        ls = LineString([])
        assert ls.is_out_of_image((10, 10))
        assert not ls.is_out_of_image((10, 10), default=False)
        assert ls.is_out_of_image((10, 10), default=None) is None

    def test_clip_out_of_image_with_simple_line_string(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], ls)

        lss_clipped = ls.clip_out_of_image((2, 2))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], ls)

        lss_clipped = ls.clip_out_of_image((2, 1))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], [(0, 0), (1, 0)])

    def test_clip_out_of_image_with_shifted_simple_line_string(self):
        # same as above, all coords at x+5, y+5
        ls = LineString([(5, 5), (6, 5), (7, 6)])

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], ls)

        lss_clipped = ls.clip_out_of_image((4, 4))
        assert len(lss_clipped) == 0

    def test_clip_out_of_image_with_ls_partially_outside_image(self):
        # line that leaves image plane and comes back
        ls = LineString([(0, 0), (1, 0), (3, 0),
                         (3, 2), (1, 2), (0, 2)])

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], ls)

        lss_clipped = ls.clip_out_of_image((10, 2))
        assert len(lss_clipped) == 2
        assert _coords_eq(lss_clipped[0], [(0, 0), (1, 0), (2, 0)])
        assert _coords_eq(lss_clipped[1], [(2, 2), (1, 2), (0, 2)])

        lss_clipped = ls.clip_out_of_image((10, 1))
        assert len(lss_clipped) == 2
        assert _coords_eq(lss_clipped[0], [(0, 0), (1, 0)])
        assert _coords_eq(lss_clipped[1], [(1, 2), (0, 2)])

    def test_clip_out_of_image_with_ls_partially_ooi_less_points(self):
        # same as above, but removing first and last point
        # so that only one point before and after out of image part remain
        ls = LineString([(1, 0), (3, 0),
                         (3, 2), (1, 2)])

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], ls)

        lss_clipped = ls.clip_out_of_image((10, 2))
        assert len(lss_clipped) == 2
        assert _coords_eq(lss_clipped[0], [(1, 0), (2, 0)])
        assert _coords_eq(lss_clipped[1], [(2, 2), (1, 2)])

        lss_clipped = ls.clip_out_of_image((10, 1))
        assert len(lss_clipped) == 0

    def test_clip_out_of_image_when_only_one_point_remains(self):
        # same as above, but only one point out of image remains
        ls = LineString([(1, 0), (3, 0),
                         (1, 2)])

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], ls)

        lss_clipped = ls.clip_out_of_image((10, 2))
        assert len(lss_clipped) == 2
        assert _coords_eq(lss_clipped[0], [(1, 0), (2, 0)])
        assert _coords_eq(lss_clipped[1], [(2, 1), (1, 2)])

        lss_clipped = ls.clip_out_of_image((10, 1))
        assert len(lss_clipped) == 0

    def test_clip_out_of_image_with_ls_leaving_image_multiple_times(self):
        # line string that leaves image, comes back, then leaves again, then
        # comes back again
        ls = LineString([(1, 0), (3, 0),  # leaves
                         (3, 1), (1, 1),  # comes back
                         (1, 2), (3, 2),  # leaves
                         (3, 3), (1, 3)])  # comes back

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], ls)

        lss_clipped = ls.clip_out_of_image((10, 2))
        assert len(lss_clipped) == 3  # from above: 1s line, 2nd+3rd, 4th
        assert _coords_eq(lss_clipped[0], [(1, 0), (2, 0)])
        assert _coords_eq(lss_clipped[1], [(2, 1), (1, 1), (1, 2), (2, 2)])
        assert _coords_eq(lss_clipped[2], [(2, 3), (1, 3)])

    def test_clip_out_of_image_with_ls_that_enters_image_from_outside(self):
        # line string that starts out of image and ends within the image plane
        for y in [1, 0]:
            with self.subTest(y=y):
                # one point inside image
                ls = LineString([(-10, y), (3, y)])

                lss_clipped = ls.clip_out_of_image((10, 10))
                assert len(lss_clipped) == 1
                assert _coords_eq(lss_clipped[0], [(0, y), (3, y)])

                lss_clipped = ls.clip_out_of_image((2, 1))
                assert len(lss_clipped) == 1
                assert _coords_eq(lss_clipped[0], [(0, y), (1, y)])

                lss_clipped = ls.clip_out_of_image((1, 1))
                if y == 1:
                    assert len(lss_clipped) == 0
                else:
                    assert len(lss_clipped) == 1
                    assert _coords_eq(lss_clipped[0], [(0, y), (1, y)])

                # two points inside image
                ls = LineString([(-10, y), (3, y), (5, y)])

                lss_clipped = ls.clip_out_of_image((10, 10))
                assert len(lss_clipped) == 1
                assert _coords_eq(lss_clipped[0], [(0, y), (3, y), (5, y)])

                lss_clipped = ls.clip_out_of_image((10, 4))
                assert len(lss_clipped) == 1
                assert _coords_eq(lss_clipped[0], [(0, y), (3, y), (4, y)])

                lss_clipped = ls.clip_out_of_image((2, 1))
                assert len(lss_clipped) == 1
                assert _coords_eq(lss_clipped[0], [(0, y), (1, y)])

                lss_clipped = ls.clip_out_of_image((1, 1))
                if y == 1:
                    assert len(lss_clipped) == 0
                else:
                    assert len(lss_clipped) == 1
                    assert _coords_eq(lss_clipped[0], [(0, y), (1, y)])

    def test_clip_out_of_image_with_ls_that_leaves_image_from_inside(self):
        # line string that starts within the image plane and ends outside
        for y in [1, 0]:
            with self.subTest(y=y):
                # one point inside image
                ls = LineString([(2, y), (5, y)])

                lss_clipped = ls.clip_out_of_image((10, 10))
                assert len(lss_clipped) == 1
                assert _coords_eq(lss_clipped[0], [(2, y), (5, y)])

                lss_clipped = ls.clip_out_of_image((10, 4))
                assert _coords_eq(lss_clipped[0], [(2, y), (4, y)])

                # two points inside image
                ls = LineString([(1, y), (2, y), (5, y)])

                lss_clipped = ls.clip_out_of_image((10, 10))
                assert len(lss_clipped) == 1
                assert _coords_eq(lss_clipped[0], [(1, y), (2, y), (5, y)])

                lss_clipped = ls.clip_out_of_image((10, 4))
                assert len(lss_clipped) == 1
                assert _coords_eq(lss_clipped[0], [(1, y), (2, y), (4, y)])

                lss_clipped = ls.clip_out_of_image((2, 1))
                assert len(lss_clipped) == 0

                # two points outside image
                ls = LineString([(2, y), (5, y), (6, y)])

                lss_clipped = ls.clip_out_of_image((10, 10))
                assert len(lss_clipped) == 1
                assert _coords_eq(lss_clipped[0], [(2, y), (5, y), (6, y)])

                lss_clipped = ls.clip_out_of_image((10, 4))
                assert len(lss_clipped) == 1
                assert _coords_eq(lss_clipped[0], [(2, y), (4, y)])

                lss_clipped = ls.clip_out_of_image((2, 1))
                assert len(lss_clipped) == 0

    def test_clip_out_of_image_with_ls_that_cuts_through_image(self):
        # line string that cuts through the image plane in the center
        for y in [1, 0]:
            ls = LineString([(-5, y), (5, y)])

            lss_clipped = ls.clip_out_of_image((10, 10))
            assert len(lss_clipped) == 1
            assert _coords_eq(lss_clipped[0], [(0, y), (5, y)])

            lss_clipped = ls.clip_out_of_image((4, 4))
            assert len(lss_clipped) == 1
            assert _coords_eq(lss_clipped[0], [(0, y), (4, y)])

    def test_clip_out_of_image_with_ls_that_runs_through_diagonal_corners(self):
        # line string that cuts through the image plane from the bottom left
        # corner to the top right corner
        ls = LineString([(-5, -5), (5, 5)])

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], [(0, 0), (5, 5)])

        lss_clipped = ls.clip_out_of_image((4, 4))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], [(0, 0), (4, 4)])

    def test_clip_out_of_image_with_ls_that_overlaps_with_image_edge(self):
        # line string that overlaps with the bottom edge
        ls = LineString([(1, 0), (4, 0)])

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], ls)

        lss_clipped = ls.clip_out_of_image((3, 3))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], [(1, 0), (3, 0)])

    def test_clip_out_of_image_with_ls_that_overlaps_with_image_edge2(self):
        # same as above, multiple points on line
        ls = LineString([(1, 0), (4, 0), (5, 0), (6, 0), (7, 0)])

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], ls)

        lss_clipped = ls.clip_out_of_image((5, 5))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], [(1, 0), (4, 0), (5, 0)])

        lss_clipped = ls.clip_out_of_image((5, 4))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], [(1, 0), (4, 0)])

        lss_clipped = ls.clip_out_of_image((5, 2))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], [(1, 0), (2, 0)])

    def test_clip_out_of_image_with_ls_that_overlaps_with_image_edge3(self):
        # line string that starts outside the image, intersects with the
        # bottom left corner and overlaps with the bottom border
        ls = LineString([(-5, 0), (5, 0)])

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], [(0, 0), (5, 0)])

        lss_clipped = ls.clip_out_of_image((10, 5))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], [(0, 0), (5, 0)])

        lss_clipped = ls.clip_out_of_image((10, 4))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], [(0, 0), (4, 0)])

    def test_clip_out_of_image_with_ls_that_contains_a_single_point(self):
        # line string that contains a single point
        ls = LineString([(2, 2)])

        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 1
        assert _coords_eq(lss_clipped[0], ls)

        lss_clipped = ls.clip_out_of_image((1, 1))
        assert len(lss_clipped) == 0

    def test_clip_out_of_image_with_ls_that_is_empty(self):
        # line string that is empty
        ls = LineString([])
        lss_clipped = ls.clip_out_of_image((10, 10))
        assert len(lss_clipped) == 0

    def test_clip_out_of_image_and_is_fully_within_image(self):
        # combine clip + is_fully_within_image
        sizes = [(200, 400), (400, 800), (800, 1600), (1600, 3200),
                 (3200, 6400)]
        sizes = sizes + [(w, h) for h, w in sizes]
        for h, w in sizes:
            ls = LineString([(0, 10), (w, 10), (w, h), (w-10, h-10)])
            lss_clipped = ls.clip_out_of_image((h, w))
            assert len(lss_clipped) == 2
            assert lss_clipped[0].is_fully_within_image((h, w))
            assert lss_clipped[1].is_fully_within_image((h, w))

            ls = LineString([(0, 10), (w+10, 10), (w+10, h-10), (w-10, h-10)])
            lss_clipped = ls.clip_out_of_image((h, w))
            assert len(lss_clipped) == 2
            assert lss_clipped[0].is_fully_within_image((h, w))
            assert lss_clipped[1].is_fully_within_image((h, w))

            ls = LineString([(-10, 10), (w+10, 10), (w-10, h-10)])
            lss_clipped = ls.clip_out_of_image((h, w))
            assert len(lss_clipped) == 2
            assert lss_clipped[0].is_fully_within_image((h, w))
            assert lss_clipped[1].is_fully_within_image((h, w))

    def test_draw_mask(self):
        ls = LineString([(0, 1), (5, 1), (5, 5)])

        arr = ls.draw_mask(
            (10, 10), size_lines=1, size_points=0, raise_if_out_of_image=False)

        assert np.all(arr[1, 0:5])
        assert np.all(arr[1:5, 5])
        assert not np.any(arr[0, :])
        assert not np.any(arr[2:, 0:5])

    def test_draw_mask_of_empty_line_string(self):
        ls = LineString([])

        arr = ls.draw_mask(
            (10, 10), size_lines=1, raise_if_out_of_image=False)

        assert not np.any(arr)

    def test_draw_line_heatmap_array(self):
        ls = LineString([(0, 1), (5, 1), (5, 5)])

        arr = ls.draw_lines_heatmap_array(
            (10, 10), alpha=0.5, size=1, raise_if_out_of_image=False)

        assert _drawing_allclose(arr[1, 0:5], 0.5)
        assert _drawing_allclose(arr[1:5, 5], 0.5)
        assert _drawing_allclose(arr[0, :], 0.0)
        assert _drawing_allclose(arr[2:, 0:5], 0.0)

    def test_draw_line_heatmap_array_with_empty_line_string(self):
        ls = LineString([])

        arr = ls.draw_lines_heatmap_array(
            (10, 10), alpha=0.5, size=1, raise_if_out_of_image=False)

        assert _drawing_allclose(arr, 0.0)

    def test_draw_points_heatmap_array(self):
        ls = LineString([(0, 1), (5, 1), (5, 5)])

        arr = ls.draw_points_heatmap_array(
            (10, 10), alpha=0.5, size=1, raise_if_out_of_image=False)

        assert _drawing_allclose(arr[1, 0], 0.5)
        assert _drawing_allclose(arr[1, 5], 0.5)
        assert _drawing_allclose(arr[5, 5], 0.5)
        assert _drawing_allclose(arr[0, :], 0.0)
        assert _drawing_allclose(arr[2:, 0:5], 0.0)

    def test_draw_points_heatmap_array_with_empty_line_string(self):
        ls = LineString([])

        arr = ls.draw_points_heatmap_array(
            (10, 10), alpha=0.5, size=1, raise_if_out_of_image=False)

        assert _drawing_allclose(arr, 0.0)

    def test_draw_heatmap_array_calls_other_drawing_functions(self):
        ls = LineString([(0, 1), (9, 1)])

        module_name = "imgaug.augmentables.lines."
        line_fname = "%sLineString.draw_lines_heatmap_array" % (module_name,)
        points_fname = "%sLineString.draw_points_heatmap_array" % (module_name,)
        with mock.patch(line_fname, return_value=1) as mock_line, \
                mock.patch(points_fname, return_value=2) as mock_points:
            _arr = ls.draw_heatmap_array(
                (10, 10),
                alpha_lines=0.9, alpha_points=0.8,
                size_lines=3, size_points=5,
                antialiased=True,
                raise_if_out_of_image=True)

            assert mock_line.call_count == 1
            assert mock_points.call_count == 1

            assert mock_line.call_args_list[0][0][0] == (10, 10)
            assert np.isclose(mock_line.call_args_list[0][1]["alpha"], 0.9)
            assert mock_line.call_args_list[0][1]["size"] == 3
            assert mock_line.call_args_list[0][1]["antialiased"] is True
            assert mock_line.call_args_list[0][1]["raise_if_out_of_image"] \
                is True

            assert mock_points.call_args_list[0][0][0] == (10, 10)
            assert np.isclose(mock_points.call_args_list[0][1]["alpha"], 0.8)
            assert mock_points.call_args_list[0][1]["size"] == 5
            assert mock_points.call_args_list[0][1]["raise_if_out_of_image"] \
                is True

    def test_draw_heatmap_array_without_mocking(self):
        ls = LineString([(0, 1), (5, 1), (5, 5)])

        arr = ls.draw_heatmap_array((10, 10),
                                    alpha_lines=0.9, alpha_points=0.5,
                                    size_lines=1, size_points=3,
                                    antialiased=False,
                                    raise_if_out_of_image=False)

        assert _drawing_allclose(arr[1, 0:5], 0.9)
        assert _drawing_allclose(arr[1, 0:5], 0.9)
        assert _drawing_allclose(arr[1, 0:5], 0.9)
        assert _drawing_allclose(arr[2:5, 5], 0.9)
        assert _drawing_allclose(arr[2:5, 5], 0.9)
        assert _drawing_allclose(arr[2:5, 5], 0.9)

        assert _drawing_allclose(arr[0, 0:2], 0.5)
        assert _drawing_allclose(arr[2, 0:2], 0.5)

        assert _drawing_allclose(arr[0, 4:6 + 1], 0.5)
        assert _drawing_allclose(arr[2, 4], 0.5)
        assert _drawing_allclose(arr[2, 6], 0.5)

        assert _drawing_allclose(arr[4, 4], 0.5)
        assert _drawing_allclose(arr[4, 6], 0.5)
        assert _drawing_allclose(arr[6, 4:6 + 1], 0.5)

        assert _drawing_allclose(arr[0, 3], 0.0)
        assert _drawing_allclose(arr[7:, :], 0.0)

    def test_draw_heatmap_array_with_empty_line_string(self):
        ls = LineString([])

        arr = ls.draw_heatmap_array((10, 10))

        assert arr.shape == (10, 10)
        assert np.sum(arr) == 0

    def test_draw_line_on_image_with_image_of_zeros(self):
        # image of 0s
        ls = LineString([(0, 1), (9, 1)])

        img = ls.draw_lines_on_image(
            np.zeros((3, 10, 3), dtype=np.uint8),
            color=(10, 200, 20),
            alpha=1.0, size=1,
            antialiased=False,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img[0, :, :] == 0)
        assert np.all(img[1, :, 0] == 10)
        assert np.all(img[1, :, 1] == 200)
        assert np.all(img[1, :, 2] == 20)
        assert np.all(img[2, :, :] == 0)

    def test_draw_line_on_image_with_2d_image_of_zeros(self):
        # image of 0s, 2D input image
        ls = LineString([(0, 1), (9, 1)])

        img = ls.draw_lines_on_image(
            np.zeros((3, 10), dtype=np.uint8),
            color=200,
            alpha=1.0, size=1,
            antialiased=False,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img[0, :] == 0)
        assert np.all(img[1, :] == 200)
        assert np.all(img[2, :] == 0)

    def test_draw_line_on_image_of_ones(self):
        # image of 1s
        ls = LineString([(0, 1), (9, 1)])

        img = ls.draw_lines_on_image(
            np.ones((3, 10, 3), dtype=np.uint8),
            color=(10, 200, 20),
            alpha=1.0, size=1,
            antialiased=False,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img[0, :, :] == 1)
        assert np.all(img[1, :, 0] == 10)
        assert np.all(img[1, :, 1] == 200)
        assert np.all(img[1, :, 2] == 20)
        assert np.all(img[2, :, :] == 1)

    def test_draw_line_on_image_alpha_at_50_percent(self):
        # alpha=0.5
        ls = LineString([(0, 1), (9, 1)])

        img = ls.draw_lines_on_image(
            np.zeros((3, 10, 3), dtype=np.uint8),
            color=(10, 200, 20),
            alpha=0.5, size=1,
            antialiased=False,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img[0, :, :] == 0)
        assert np.all(img[1, :, 0] == 5)
        assert np.all(img[1, :, 1] == 100)
        assert np.all(img[1, :, 2] == 10)
        assert np.all(img[2, :, :] == 0)

    def test_draw_line_on_image_alpha_at_50_percent_with_background(self):
        # alpha=0.5 with background
        ls = LineString([(0, 1), (9, 1)])

        img = ls.draw_lines_on_image(
            10 + np.zeros((3, 10, 3), dtype=np.uint8),
            color=(10, 200, 20),
            alpha=0.5, size=1,
            antialiased=False,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img[0, :, :] == 10)
        assert np.all(img[1, :, 0] == 5+5)
        assert np.all(img[1, :, 1] == 5+100)
        assert np.all(img[1, :, 2] == 5+10)
        assert np.all(img[2, :, :] == 10)

    def test_draw_line_on_image_with_size_3(self):
        # size=3
        ls = LineString([(0, 5), (9, 5)])

        img = ls.draw_lines_on_image(
            np.zeros((10, 10, 3), dtype=np.uint8),
            color=(10, 200, 20),
            alpha=1.0, size=3,
            antialiased=False,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img[5-1:5+1+1, :, 0] == 10)
        assert np.all(img[5-1:5+1+1, :, 1] == 200)
        assert np.all(img[5-1:5+1+1, :, 2] == 20)
        assert np.all(img[:5-1, :, :] == 0)
        assert np.all(img[5+1+1:, :, :] == 0)

    def test_draw_line_on_image_with_2d_image_and_size_3(self):
        # size=3, 2D input image
        ls = LineString([(0, 5), (9, 5)])

        img = ls.draw_lines_on_image(
            np.zeros((10, 10), dtype=np.uint8),
            color=200,
            alpha=1.0, size=3,
            antialiased=False,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img[5-1:5+1+1, :] == 200)
        assert np.all(img[:5-1, :] == 0)
        assert np.all(img[5+1+1:, :] == 0)

    def test_draw_line_on_image_with_size_3_and_antialiasing(self):
        # size=3, antialiasing
        ls = LineString([(0, 0), (9, 9)])

        img = ls.draw_lines_on_image(
            np.zeros((10, 10, 3), dtype=np.uint8),
            color=(100, 100, 100),
            alpha=1.0, size=3,
            antialiased=False,
            raise_if_out_of_image=False
        )
        img_aa = ls.draw_lines_on_image(
            np.zeros((10, 10, 3), dtype=np.uint8),
            color=(100, 100, 100),
            alpha=1.0, size=3,
            antialiased=True,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert img_aa.dtype.name == "uint8"
        assert np.sum(img) > 5 * 3 * 100
        assert np.sum(img_aa) > 5 * 3 * 100
        assert not np.array_equal(img, img_aa)
        assert np.all(img[:3, -3:, :] == 0)
        assert np.all(img_aa[:3, -3:, :] == 0)
        assert np.all(img[-3:, :3, :] == 0)
        assert np.all(img_aa[-3:, :3, :] == 0)

    def test_draw_line_on_image_with_line_partially_outside_image(self):
        # line partially outside if image
        ls = LineString([(-1, 1), (9, 1)])

        img = ls.draw_lines_on_image(
            np.zeros((3, 10, 3), dtype=np.uint8),
            color=(10, 200, 20),
            alpha=1.0, size=1,
            antialiased=False,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img[0, :, :] == 0)
        assert np.all(img[1, :, 0] == 10)
        assert np.all(img[1, :, 1] == 200)
        assert np.all(img[1, :, 2] == 20)
        assert np.all(img[2, :, :] == 0)

    def test_draw_line_on_image_with_line_fully_outside_image(self):
        # line fully outside if image
        ls = LineString([(-10, 1), (-9, 1)])

        img = ls.draw_lines_on_image(
            np.zeros((3, 10, 3), dtype=np.uint8),
            color=(10, 200, 20),
            alpha=1.0, size=1,
            antialiased=False,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img == 0)

    def test_draw_line_on_image_with_line_fully_ooi_and_raise_true(self):
        # raise_if_out_of_image=True
        ls = LineString([(0-5, 5), (-1, 5)])

        with self.assertRaises(Exception) as context:
            _img = ls.draw_lines_on_image(
                np.zeros((10, 10, 3), dtype=np.uint8),
                color=(100, 100, 100),
                alpha=1.0, size=3,
                antialiased=False,
                raise_if_out_of_image=True
            )

        assert "Cannot draw line string " in str(context.exception)

    def test_draw_line_on_image_with_line_part_inside_img_and_raise_true(self):
        # raise_if_out_of_image=True BUT line is partially inside image
        # (no point is inside image though)
        ls = LineString([(-1, 5), (11, 5)])

        _img = ls.draw_lines_on_image(
            np.zeros((10, 10, 3), dtype=np.uint8),
            color=(100, 100, 100),
            alpha=1.0, size=3,
            antialiased=False,
            raise_if_out_of_image=True
        )

    def test_draw_points_on_image_with_image_of_zeros(self):
        # iamge of 0s
        ls = LineString([(0, 1), (9, 1)])

        img = ls.draw_points_on_image(
            np.zeros((3, 10, 3), dtype=np.uint8),
            color=(10, 255, 20),
            alpha=1.0, size=3,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img[0:2, 0:2, 0] == 10)
        assert np.all(img[0:2, 0:2, 1] == 255)
        assert np.all(img[0:2, 0:2, 2] == 20)
        assert np.all(img[0:2, -2:, 0] == 10)
        assert np.all(img[0:2, -2:, 1] == 255)
        assert np.all(img[0:2, -2:, 2] == 20)
        assert np.all(img[:, 2:-2, :] == 0)

    def test_draw_points_on_image_with_image_of_ones(self):
        # image of 1s
        ls = LineString([(0, 1), (9, 1)])

        img = ls.draw_points_on_image(
            np.ones((3, 10, 3), dtype=np.uint8),
            color=(10, 200, 20),
            alpha=1.0, size=3,
            raise_if_out_of_image=False
        )

        assert img.dtype.name == "uint8"
        assert np.all(img[0:2, 0:2, 0] == 10)
        assert np.all(img[0:2, 0:2, 1] == 200)
        assert np.all(img[0:2, 0:2, 2] == 20)
        assert np.all(img[0:2, -2:, 0] == 10)
        assert np.all(img[0:2, -2:, 1] == 200)
        assert np.all(img[0:2, -2:, 2] == 20)
        assert np.all(img[:, 2:-2, :] == 1)

    def test_draw_points_on_image_with_alpha_50_percent(self):
        # alpha=0.5
        ls = LineString([(0, 1), (9, 1)])

        img = ls.draw_points_on_image(
            np.zeros((3, 10, 3), dtype=np.uint8),
            color=(10, 200, 20),
            alpha=0.5, size=3,
            raise_if_out_of_image=False
        )

        assert np.all(img[0:2, 0:2, 0] == 5)
        assert np.all(img[0:2, 0:2, 1] == 100)
        assert np.all(img[0:2, 0:2, 2] == 10)
        assert np.all(img[0:2, -2:, 0] == 5)
        assert np.all(img[0:2, -2:, 1] == 100)
        assert np.all(img[0:2, -2:, 2] == 10)
        assert np.all(img[:, 2:-2, :] == 0)

    def test_draw_points_on_image_with_size_one(self):
        # size=1
        ls = LineString([(0, 1), (9, 1)])

        img = ls.draw_points_on_image(
            np.zeros((3, 10, 3), dtype=np.uint8),
            color=(10, 200, 20),
            alpha=1.0, size=1,
            raise_if_out_of_image=False
        )

        assert np.all(img[0, :, :] == 0)
        assert np.all(img[2, :, :] == 0)

        assert np.all(img[1, 0, 0] == 10)
        assert np.all(img[1, 0, 1] == 200)
        assert np.all(img[1, 0, 2] == 20)

        assert np.all(img[1, -1, 0] == 10)
        assert np.all(img[1, -1, 1] == 200)
        assert np.all(img[1, -1, 2] == 20)

    def test_draw_points_on_image_with_ls_ooi_and_raise_true(self):
        with self.assertRaises(Exception) as context:
            ls = LineString([(0-5, 1), (9+5, 1)])
            _img = ls.draw_points_on_image(
                np.zeros((3, 10, 3), dtype=np.uint8),
                color=(10, 200, 20),
                alpha=0.5, size=1,
                raise_if_out_of_image=True
            )

        assert "Cannot draw keypoint " in str(context.exception)

    def test_draw_on_image_with_mocking(self):
        ls = LineString([(0, 1), (9, 1)])

        module_name = "imgaug.augmentables.lines."
        line_fname = "%sLineString.draw_lines_on_image" % (module_name,)
        points_fname = "%sLineString.draw_points_on_image" % (module_name,)
        with mock.patch(line_fname, return_value=1) as mock_line, \
                mock.patch(points_fname, return_value=2) as mock_points:
            _image = ls.draw_on_image(
                np.zeros((10, 10, 3), dtype=np.uint8),
                color=(1, 2, 3), color_lines=(4, 5, 6), color_points=(7, 8, 9),
                alpha=1.0, alpha_lines=0.9, alpha_points=0.8,
                size=1, size_lines=3, size_points=5,
                antialiased=False,
                raise_if_out_of_image=True)

        assert mock_line.call_count == 1
        assert mock_points.call_count == 1

        assert mock_line.call_args_list[0][0][0].shape == (10, 10, 3)
        assert mock_line.call_args_list[0][1]["color"][0] == 4
        assert mock_line.call_args_list[0][1]["color"][1] == 5
        assert mock_line.call_args_list[0][1]["color"][2] == 6
        assert np.isclose(mock_line.call_args_list[0][1]["alpha"], 0.9)
        assert mock_line.call_args_list[0][1]["size"] == 3
        assert mock_line.call_args_list[0][1]["antialiased"] is False
        assert mock_line.call_args_list[0][1]["raise_if_out_of_image"] \
            is True

        assert mock_points.call_args_list[0][0][0] == 1  # mock_line result
        assert mock_points.call_args_list[0][1]["color"][0] == 7
        assert mock_points.call_args_list[0][1]["color"][1] == 8
        assert mock_points.call_args_list[0][1]["color"][2] == 9
        assert np.isclose(mock_points.call_args_list[0][1]["alpha"], 0.8)
        assert mock_points.call_args_list[0][1]["size"] == 5
        assert mock_points.call_args_list[0][1]["raise_if_out_of_image"] \
            is True

    def test_draw_on_image_without_mocking(self):
        ls = LineString([(0, 1), (5, 1), (5, 5)])

        img = ls.draw_on_image(np.zeros((10, 10, 3), dtype=np.uint8),
                               color=(200, 120, 40), alpha=0.5, size=1)

        assert np.all(img[1, 0:5, 0] == 100)
        assert np.all(img[1, 0:5, 1] == 60)
        assert np.all(img[1, 0:5, 2] == 20)
        assert np.all(img[1:5, 5, 0] == 100)
        assert np.all(img[1:5, 5, 1] == 60)
        assert np.all(img[1:5, 5, 2] == 20)
        assert np.all(img[0:2+1, 0:2, 0] >= 50)  # color_points is 0.5*color
        assert np.all(img[0:2+1, 0:2, 1] >= 30)
        assert np.all(img[0:2+1, 0:2, 2] >= 10)
        assert np.all(img[0:2+1, 4:6+1, 0] >= 50)
        assert np.all(img[0:2+1, 4:6+1, 1] >= 30)
        assert np.all(img[0:2+1, 4:6+1, 2] >= 10)
        assert np.all(img[4:6+1, 4:6+1, 0] >= 50)
        assert np.all(img[4:6+1, 4:6+1, 1] >= 30)
        assert np.all(img[4:6+1, 4:6+1, 2] >= 10)
        assert np.all(img[0, 3, :] == 0)
        assert np.all(img[7:, :, :] == 0)

    def test_draw_on_image_with_empty_line_string(self):
        ls = LineString([])

        img = ls.draw_on_image(np.zeros((10, 10, 3), dtype=np.uint8))

        assert img.shape == (10, 10, 3)
        assert np.sum(img) == 0

    def test_extract_from_image_size_1_single_channel(self):
        ls = LineString([(0, 5), (9, 5)])
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)

        extract = ls.extract_from_image(img, antialiased=False)

        assert extract.shape == (1, 10, 1)
        assert np.array_equal(extract, img[5:6, 0:10, :])

    def test_extract_from_image_size_3_single_channel(self):
        ls = LineString([(1, 5), (8, 5)])
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)

        extract = ls.extract_from_image(img, size=3, antialiased=False)

        assert extract.shape == (3, 10, 1)
        assert np.array_equal(extract, img[4:6+1, 0:10, :])

    def test_extract_from_image_size_3_rgb(self):
        # size=3, RGB image
        ls = LineString([(1, 5), (8, 5)])
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)
        img_rgb = np.tile(img, (1, 1, 3))
        img_rgb[..., 1] += 10
        img_rgb[..., 2] += 20

        extract = ls.extract_from_image(img_rgb, size=3, antialiased=False)

        assert extract.shape == (3, 10, 3)
        assert np.array_equal(extract, img_rgb[4:6+1, 0:10, :])

    def test_extract_from_image_antialiased_true(self):
        # weak antialiased=True test
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)
        ls = LineString([(1, 1), (9, 9)])

        extract_aa = ls.extract_from_image(img, size=3, antialiased=True)
        extract = ls.extract_from_image(img, size=3, antialiased=False)

        assert extract_aa.shape == extract.shape
        assert np.sum(extract_aa) > np.sum(extract)

    def test_extract_from_image_pad_false(self):
        # pad=False
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)
        ls = LineString([(-5, 5), (-3, 5)])

        extract = ls.extract_from_image(img, size=1, antialiased=False,
                                        pad=False, prevent_zero_size=True)

        assert extract.shape == (1, 1, 1)
        assert np.sum(extract) == 0

    def test_extract_from_image_pad_false_and_prevent_zero_size_false(self):
        # pad=False, prevent_zero_size=False
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)
        ls = LineString([(-5, 5), (-3, 5)])

        extract = ls.extract_from_image(img, size=1, antialiased=False,
                                        pad=False, prevent_zero_size=False)

        assert extract.shape == (0, 0, 1)

    def test_extract_from_image_pad_max(self):
        # pad_max=1
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)
        ls = LineString([(-5, 5), (9, 5)])

        extract = ls.extract_from_image(img, antialiased=False, pad=True,
                                        pad_max=1)

        assert extract.shape == (1, 11, 1)
        assert np.array_equal(extract[:, 1:], img[5:6, 0:10, :])
        assert np.all(extract[0, 0, :] == 0)

    def test_extract_from_image_with_single_point_line_string(self):
        # 1 coord
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)
        ls = LineString([(1, 1)])

        extract = ls.extract_from_image(img)

        assert extract.shape == (1, 1, 1)
        assert np.sum(extract) == img[1:2, 1:2, :]

    def test_extract_from_image_with_single_point_ls_negative_coords(self):
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)
        ls = LineString([(-10, -10)])

        extract = ls.extract_from_image(img)

        assert extract.shape == (1, 1, 1)
        assert np.sum(extract) == 0

    def test_extract_from_image_with_1_point_neg_coords_prevent_zero_size(self):
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)
        ls = LineString([(-10, -10)])

        extract = ls.extract_from_image(img, prevent_zero_size=True)

        assert extract.shape == (1, 1, 1)
        assert np.sum(extract) == 0

    def test_extract_from_image_with_empty_line_string(self):
        # 0 coords
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)
        ls = LineString([])

        extract = ls.extract_from_image(img)

        assert extract.shape == (1, 1, 1)
        assert np.sum(extract) == 0

    def test_extract_from_image_with_empty_line_string_prevent_zero_size(self):
        img = np.arange(10*10).reshape((10, 10, 1)).astype(np.uint8)
        ls = LineString([])

        extract = ls.extract_from_image(img, prevent_zero_size=False)

        assert extract.shape == (0, 0, 1)
        assert np.sum(extract) == 0

    def test_concatenate_line_string_with_itself(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        ls_concat = ls.concatenate(ls)
        assert ls_concat.coords_almost_equals([
            (0, 0), (1, 0), (2, 1), (0, 0), (1, 0), (2, 1)
        ])

    def test_concatenate_empty_line_string_with_itself(self):
        ls = LineString([])
        ls_concat = ls.concatenate(ls)
        assert ls_concat.coords_almost_equals([])

    def test_concatenate_empty_line_string_with_single_point_line_string(self):
        ls = LineString([])
        ls_concat = ls.concatenate(LineString([(0, 0)]))
        assert ls_concat.coords_almost_equals([(0, 0)])

    def test_concatenate_single_point_line_string_with_empty_line_string(self):
        ls = LineString([(0, 0)])
        ls_concat = ls.concatenate(LineString([]))
        assert ls_concat.coords_almost_equals([(0, 0)])

    def test_concatenate_empty_line_string_with_list_of_tuples(self):
        ls = LineString([])
        ls_concat = ls.concatenate([(0, 0)])
        assert ls_concat.coords_almost_equals([(0, 0)])

    def test_to_keypoints(self):
        ls = LineString([(0, 0), (1, 0), (2, 1)])
        observed = ls.to_keypoints()
        assert all([isinstance(kp, ia.Keypoint) for kp in observed])
        assert np.isclose(observed[0].x, 0)
        assert np.isclose(observed[0].y, 0)
        assert np.isclose(observed[1].x, 1)
        assert np.isclose(observed[1].y, 0)
        assert np.isclose(observed[2].x, 2)
        assert np.isclose(observed[2].y, 1)

    def test_to_keypoints_with_empty_line_string(self):
        ls = LineString([])
        assert len(ls.to_keypoints()) == 0

    def test_to_bounding_box(self):
        ls = LineString([(0, 0), (1, 0), (1, 1)])
        observed = ls.to_bounding_box()
        assert isinstance(observed, ia.BoundingBox)
        assert np.isclose(observed.x1, 0)
        assert np.isclose(observed.y1, 0)
        assert np.isclose(observed.x2, 1)
        assert np.isclose(observed.y2, 1)

    def test_to_bounding_box_with_single_point_line_string(self):
        ls = LineString([(0, 0)])
        observed = ls.to_bounding_box()
        assert isinstance(observed, ia.BoundingBox)
        assert np.isclose(observed.x1, 0)
        assert np.isclose(observed.y1, 0)
        assert np.isclose(observed.x2, 0)
        assert np.isclose(observed.y2, 0)

    def test_to_bounding_box_with_empty_line_string(self):
        ls = LineString([])
        observed = ls.to_bounding_box()
        assert observed is None

    def test_to_polygon(self):
        ls = LineString([(0, 0), (1, 0), (1, 1)])
        observed = ls.to_polygon()
        assert isinstance(observed, ia.Polygon)
        assert np.allclose(observed.exterior, [(0, 0), (1, 0), (1, 1)])

    def test_to_polygon_with_single_point_line_string(self):
        ls = LineString([(0, 0)])
        observed = ls.to_polygon()
        assert isinstance(observed, ia.Polygon)
        assert np.allclose(observed.exterior, [(0, 0)])

    def test_to_polygon_with_empty_line_string(self):
        ls = LineString([])
        observed = ls.to_polygon()
        assert isinstance(observed, ia.Polygon)
        assert len(observed.exterior) == 0

    # TODO add antialiased=True test
    def test_to_heatmap(self):
        ls = LineString([(0, 5), (5, 5)])
        observed = ls.to_heatmap((10, 10), antialiased=False)
        assert isinstance(observed, HeatmapsOnImage)
        assert observed.shape == (10, 10)
        assert observed.arr_0to1.shape == (10, 10, 1)
        assert np.allclose(observed.arr_0to1[0:5, :, :], 0.0)
        assert np.allclose(observed.arr_0to1[5, 0:5, :], 1.0)
        assert np.allclose(observed.arr_0to1[6:, :, :], 0.0)

    def test_to_heatmap_with_empty_line_string(self):
        ls = LineString([])
        observed = ls.to_heatmap((5, 5), antialiased=False)
        assert observed.shape == (5, 5)
        assert observed.arr_0to1.shape == (5, 5, 1)
        assert np.allclose(observed.arr_0to1, 0.0)

    # TODO change this after the segmap PR was merged

    def test_segmentation_map(self):
        from imgaug.augmentables.segmaps import SegmentationMapsOnImage
        ls = LineString([(0, 5), (5, 5)])
        observed = ls.to_segmentation_map((10, 10))
        assert isinstance(observed, SegmentationMapsOnImage)
        assert observed.shape == (10, 10)
        assert observed.arr.shape == (10, 10, 1)
        assert np.all(observed.arr[0:5, :, :] == 0)
        assert np.all(observed.arr[5, 0:5, :] == 1)
        assert np.all(observed.arr[6:, :, :] == 0)

        ls = LineString([])
        observed = ls.to_segmentation_map((5, 5))
        assert observed.shape == (5, 5)
        assert observed.arr.shape == (5, 5, 1)
        assert np.all(observed.arr == 0)

    def test_coords_almost_equals_with_3_point_ls_90deg_angle(self):
        ls = LineString([(0, 0), (1, 0), (1, 1)])
        assert ls.coords_almost_equals(ls)
        assert ls.coords_almost_equals([(0, 0), (1, 0), (1, 1)])
        assert not ls.shift(y=1).coords_almost_equals(ls)
        assert ls.shift(y=1).coords_almost_equals(ls, max_distance=1.01)
        assert ls.coords_almost_equals([(0, 0), (0.5, 0), (1, 0), (1, 1)])

    def test_coords_almost_equals_with_4_point_ls_90deg_angle(self):
        ls = LineString([(0, 0), (0.5, 0), (1, 0), (1, 1)])
        assert ls.coords_almost_equals([(0, 0), (1, 0), (1, 1)])

    def test_coords_almost_equals_with_1_point_ls(self):
        ls = LineString([(0, 0)])
        assert ls.coords_almost_equals([(0, 0)])
        assert not ls.coords_almost_equals([(0+1, 0)])
        assert ls.coords_almost_equals([(0+1, 0)], max_distance=1.01)
        assert not ls.coords_almost_equals([])

    def test_coords_almost_equals_with_empty_line_string(self):
        ls = LineString([])
        assert ls.coords_almost_equals([])
        assert not ls.coords_almost_equals([(0, 0)])

    def test_coords_almost_equals_with_two_rectangular_line_strings(self):
        # two rectangles around height=10, width=10 image,
        # both LS closed, second one has more points around the left edge
        ls_a = LineString([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)])
        ls_b = LineString([(0, 0), (10, 0), (10, 10), (0, 10),
                           (0, 5.01), (0, 5.0), (0, 4.99), (0, 0)])
        assert ls_a.coords_almost_equals(ls_b)

    def test_coords_almost_equals_different_ls_but_overlapping_points(self):
        # almost the same as in above test
        # two rectangles around height=10, width=10 image,
        # both LS closed, second one has more points around the left edge
        # AND around left edge the second line string suddenly has a line
        # up to the right edge, which immediately returns back to the left
        # edge
        # All points overlap between the lines, i.e. this test fails if naively
        # checking for overlapping points.
        ls_a = LineString([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)])
        ls_b = LineString([(0, 0), (10, 0), (10, 10), (0, 10),
                           (0, 5.01), (10, 5.0), (0, 4.99), (0, 0)])
        assert not ls_a.coords_almost_equals(ls_b)

    def test_almost_equals_three_point_ls_without_label(self):
        ls = LineString([(0, 0), (1, 0), (1, 1)])
        ls_shifted = ls.shift(y=1)
        assert ls.almost_equals(ls)
        assert not ls.almost_equals(ls_shifted)
        assert ls.almost_equals(LineString([(0, 0), (1, 0), (1, 1)],
                                           label=None))
        assert not ls.almost_equals(LineString([(0, 0), (1, 0), (1, 1)],
                                               label="foo"))

    def test_almost_equals_three_point_ls_with_label(self):
        ls = LineString([(0, 0), (1, 0), (1, 1)], label="foo")
        assert not ls.almost_equals(LineString([(0, 0), (1, 0), (1, 1)],
                                               label=None))
        assert ls.almost_equals(LineString([(0, 0), (1, 0), (1, 1)],
                                           label="foo"))

    def test_almost_equals_empty_line_string(self):
        ls = LineString([])
        assert ls.almost_equals(LineString([]))
        assert not ls.almost_equals(LineString([], label="foo"))

    def test_almost_equals_empty_line_string_with_label(self):
        ls = LineString([], label="foo")
        assert not ls.almost_equals(LineString([]))
        assert ls.almost_equals(LineString([], label="foo"))

    def test_copy_with_various_line_strings(self):
        coords = [
            [(0, 0), (1, 0), (1, 1)],
            [(0, 0), (1.5, 0), (1.6, 1)],
            [(0, 0)],
            [],
            [(0, 0), (1, 0), (1, 1)]
        ]
        labels = [None, None, None, None, "foo"]

        for coords_i, label in zip(coords, labels):
            with self.subTest(coords=coords_i, label=label):
                ls = LineString(coords_i, label=label)
                observed = ls.copy()
                assert observed is not ls
                assert observed.coords is ls.coords
                assert observed.label is ls.label

    def test_copy_with_coords_arg_set(self):
        ls = LineString([(0, 0), (1, 0), (1, 1)])
        observed = ls.copy(coords=[(0, 0)])
        assert observed.coords_almost_equals([(0, 0)])
        assert observed.label is None

    def test_copy_with_label_arg_set(self):
        ls = LineString([(0, 0), (1, 0), (1, 1)])
        observed = ls.copy(label="bar")
        assert observed.coords is ls.coords
        assert observed.label == "bar"

    def test_deepcopy_with_various_line_strings(self):
        coords = [
            [(0, 0), (1, 0), (1, 1)],
            [(0, 0), (1.5, 0), (1.6, 1)],
            [(0, 0)],
            [],
            [(0, 0), (1, 0), (1, 1)]
        ]
        labels = [None, None, None, None, "foo"]

        for coords_i, label in zip(coords, labels):
            with self.subTest(coords=coords_i, label=label):
                ls = LineString(coords_i, label=label)
                observed = ls.deepcopy()
                assert observed is not ls
                assert observed.coords is not ls.coords
                assert observed.label == ls.label

    def test_deepcopy_with_coords_arg_set(self):
        ls = LineString([(0, 0), (1, 0), (1, 1)])
        observed = ls.deepcopy(coords=[(0, 0)])
        assert observed.coords_almost_equals([(0, 0)])
        assert observed.label is None

    def test_deepcopy_with_label_arg_set(self):
        ls = LineString([(0, 0), (1, 0), (1, 1)])
        observed = ls.deepcopy(label="bar")
        assert observed.coords is not ls.coords
        assert observed.label == "bar"

    def test_string_conversion(self):
        coords = [
            [(0, 0), (1, 0), (1, 1)],
            [(0, 0), (1.5, 0), (1.6, 1)],
            [(0, 0)],
            [],
            [(0, 0), (1, 0), (1, 1)]
        ]
        labels = [None, None, None, None, "foo"]

        for coords_i, label in zip(coords, labels):
            with self.subTest(coords=coords_i, label=label):
                ls = LineString(coords_i, label=label)
                # __str__() is tested more thoroughly in other tests
                assert ls.__repr__() == ls.__str__()

    def test___getitem__(self):
        cba = ia.LineString([(0, 1), (2, 3)])
        assert np.allclose(cba[0], (0, 1))
        assert np.allclose(cba[1], (2, 3))

    def test___getitem___slice(self):
        cba = ia.LineString([(0, 1), (2, 3), (4, 5)])
        assert np.allclose(cba[1:], [(2, 3), (4, 5)])

    def test___iter___two_points(self):
        cba = LineString([(1, 2), (3, 4)])
        for i, xy in enumerate(cba):
            assert i in [0, 1]
            if i == 0:
                assert np.allclose(xy, (1, 2))
            elif i == 1:
                assert np.allclose(xy, (3, 4))
        assert i == 1

    def test___iter___zero_points(self):
        cba = LineString([])
        i = 0
        for xy in cba:
            i += 1
        assert i == 0

    def test___str__(self):
        coords = [
            [(0, 0), (1, 0), (1, 1)],
            [(0, 0), (1.5, 0), (1.6, 1)],
            [(0, 0)],
            [],
            [(0, 0), (1, 0), (1, 1)]
        ]
        labels = [None, None, None, None, "foo"]
        expecteds = [
            "LineString([(0.00, 0.00), (1.00, 0.00), (1.00, 1.00)], "
            "label=None)",
            "LineString([(0.00, 0.00), (1.50, 0.00), (1.60, 1.00)], "
            "label=None)",
            "LineString([(0.00, 0.00)], label=None)",
            "LineString([], label=None)",
            "LineString([(0.00, 0.00), (1.00, 0.00), (1.00, 1.00)], "
            "label=foo)"
        ]

        for coords_i, label, expected in zip(coords, labels, expecteds):
            with self.subTest(coords=coords_i, label=label):
                ls = LineString(coords_i, label=label)
                observed = ls.__str__()
                assert observed == expected


class TestLineStringsOnImage_items_setter(unittest.TestCase):
    def test_with_list_of_line_strings(self):
        ls = [ia.LineString([(0, 0), (1, 0)]),
              ia.LineString([(1, 1), (10, 0)])]
        lsoi = ia.LineStringsOnImage([], shape=(10, 20, 3))
        lsoi.items = ls
        assert np.all([
            (np.allclose(ls_i.coords, ls_j.coords))
            for ls_i, ls_j
            in zip(lsoi.line_strings, ls)
        ])


class TestLineStringsOnImage_on_(unittest.TestCase):
    @property
    def _is_inplace(self):
        return True

    def _func(self, lsoi, to_shape):
        return lsoi.on_(to_shape)

    def test_on_image_with_same_shape(self):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        lsoi_proj = self._func(lsoi, (100, 100, 3))

        assert np.all([ls_a.coords_almost_equals(ls_b)
                      for ls_a, ls_b
                      in zip(lsoi.line_strings, lsoi_proj.line_strings)])
        assert lsoi_proj.shape == (100, 100, 3)

    def test_on_image_with_2x_size(self):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        lsoi_proj = self._func(lsoi, (200, 200, 3))

        assert lsoi_proj.line_strings[0].coords_almost_equals(
            [(0, 0), (1*2, 0), (2*2, 1*2)]
        )
        assert lsoi_proj.line_strings[1].coords_almost_equals(
            [(10*2, 10*2)]
        )
        assert lsoi_proj.shape == (200, 200, 3)

    def test_on_image_with_2x_size_and_empty_list_of_line_strings(self):
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        lsoi_proj = self._func(lsoi, (200, 200, 3))

        assert len(lsoi_proj.line_strings) == 0
        assert lsoi_proj.shape == (200, 200, 3)

    def test_inplaceness(self):
        ls = ia.LineString([(0, 0), (1, 0)])
        lsoi = LineStringsOnImage([ls], shape=(10, 10, 3))
        lsoi2 = self._func(lsoi, (10, 10))
        if self._is_inplace:
            assert lsoi is lsoi2
        else:
            assert lsoi is not lsoi2


class TestLineStringsOnImage_on(TestLineStringsOnImage_on_):
    @property
    def _is_inplace(self):
        return False

    def _func(self, lsoi, to_shape):
        return lsoi.on(to_shape)


class TestLineStringsOnImage_remove_out_of_image_(unittest.TestCase):
    @property
    def _is_inplace(self):
        return True

    def _func(self, lsoi, fully=True, partly=False):
        return lsoi.remove_out_of_image_(fully=fully, partly=partly)

    def test_remove_out_of_image_with_two_line_strings(self):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        observed = self._func(lsoi)

        assert len(observed.line_strings) == 2
        assert observed.line_strings[0] is ls1
        assert observed.line_strings[1] is ls2

    def test_remove_out_of_image_with_empty_list_of_line_strings(self):
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = self._func(lsoi)

        assert len(observed.line_strings) == 0
        assert observed.shape == (100, 100, 3)

    def test_remove_out_of_image_with_one_empty_line_string(self):
        ls1 = LineString([])
        lsoi = LineStringsOnImage([ls1], shape=(100, 100, 3))

        observed = self._func(lsoi)

        assert len(observed.line_strings) == 0
        assert observed.shape == (100, 100, 3)

    def test_remove_out_of_image_with_one_line_string(self):
        ls1 = LineString([(-10, -10), (5, 5)])
        lsoi = LineStringsOnImage([ls1], shape=(100, 100, 3))

        observed = self._func(lsoi)

        assert len(observed.line_strings) == 1
        assert observed.line_strings[0] is ls1
        assert observed.shape == (100, 100, 3)

    def test_remove_out_of_image_remove_even_partial_oois(self):
        ls1 = LineString([(-10, -10), (5, 5)])
        lsoi = LineStringsOnImage([ls1], shape=(100, 100, 3))

        observed = self._func(lsoi, partly=True, fully=True)

        assert len(observed.line_strings) == 0
        assert observed.shape == (100, 100, 3)

    def test_remove_out_of_image_with_one_single_point_line_strings(self):
        ls1 = LineString([(-10, -10)])
        lsoi = LineStringsOnImage([ls1], shape=(100, 100, 3))

        observed = self._func(lsoi)

        assert len(observed.line_strings) == 0
        assert observed.shape == (100, 100, 3)

    def test_remove_out_of_image_partly_false_and_fully_false(self):
        ls1 = LineString([(-10, -10)])
        lsoi = LineStringsOnImage([ls1], shape=(100, 100, 3))

        observed = self._func(lsoi, partly=False, fully=False)

        assert len(observed.line_strings) == 1
        assert observed.line_strings[0] is ls1
        assert observed.shape == (100, 100, 3)

    def test_inplaceness(self):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        lsoi2 = self._func(lsoi)

        if self._is_inplace:
            assert lsoi is lsoi2
        else:
            assert lsoi is not lsoi2


class TestLineStringsOnImage_remove_out_of_image(
        TestLineStringsOnImage_remove_out_of_image_):
    @property
    def _is_inplace(self):
        return False

    def _func(self, lsoi, fully=True, partly=False):
        return lsoi.remove_out_of_image(fully=fully, partly=partly)


class TestLineStringsOnImage_clip_out_of_image_(unittest.TestCase):
    @property
    def _is_inplace(self):
        return True

    def _func(self, lsoi):
        return lsoi.clip_out_of_image_()

    def test_clip_out_of_image_with_two_simple_line_strings(self):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        observed = self._func(lsoi)

        expected = []
        expected.extend(ls1.clip_out_of_image((100, 100, 3)))
        expected.extend(ls2.clip_out_of_image((100, 100, 3)))
        assert len(lsoi.line_strings) == len(expected)
        for ls_obs, ls_exp in zip(observed.line_strings, expected):
            assert ls_obs.coords_almost_equals(ls_exp)
        assert observed.shape == (100, 100, 3)

    def test_clip_out_of_image_with_empty_list_of_line_strings(self):
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = self._func(lsoi)

        assert len(observed.line_strings) == 0
        assert observed.shape == (100, 100, 3)

    def test_clip_out_of_image_with_one_empty_line_string(self):
        ls1 = LineString([])
        lsoi = LineStringsOnImage([ls1], shape=(100, 100, 3))

        observed = self._func(lsoi)

        assert len(observed.line_strings) == 0
        assert observed.shape == (100, 100, 3)

    def test_clip_out_of_image_with_single_point_ls_and_negative_coords(self):
        ls1 = LineString([(-10, -10)])
        lsoi = LineStringsOnImage([ls1], shape=(100, 100, 3))

        observed = self._func(lsoi)

        assert len(observed.line_strings) == 0
        assert observed.shape == (100, 100, 3)

    def test_inplaceness(self):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        lsoi2 = self._func(lsoi)

        if self._is_inplace:
            assert lsoi is lsoi2
        else:
            assert lsoi is not lsoi2


class TestLineStringsOnImage_clip_out_of_image(
        TestLineStringsOnImage_clip_out_of_image_):
    @property
    def _is_inplace(self):
        return False

    def _func(self, lsoi):
        return lsoi.clip_out_of_image()


class TestLineStringsOnImage_shift_(unittest.TestCase):
    @property
    def _is_inplace(self):
        return True

    def _func(self, lsoi, *args, **kwargs):
        def _func_impl():
            return lsoi.shift_(*args, **kwargs)

        if len(lsoi.line_strings) == 0:
            return _func_impl()
        return wrap_shift_deprecation(_func_impl, *args, **kwargs)

    def test_shift_two_simple_line_strings_along_xy(self):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        observed = self._func(lsoi.deepcopy(), x=1, y=2)

        assert observed.line_strings[0].coords_almost_equals(
            ls1.shift(x=1, y=2)
        )
        assert observed.line_strings[1].coords_almost_equals(
            ls2.shift(x=1, y=2)
        )
        assert observed.shape == (100, 100, 3)

    def test_inplaceness(self):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        lsoi2 = self._func(lsoi, y=1)

        if self._is_inplace:
            assert lsoi is lsoi2
        else:
            assert lsoi is not lsoi2


class TestLineStringsOnImage_shift(TestLineStringsOnImage_shift_):
    @property
    def _is_inplace(self):
        return False

    def _func(self, lsoi, *args, **kwargs):
        def _func_impl():
            return lsoi.shift(*args, **kwargs)

        return wrap_shift_deprecation(_func_impl, *args, **kwargs)

    def test_shift_with_two_simple_line_strings(self):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        observed = self._func(lsoi.deepcopy(), top=1, right=2, bottom=3, left=4)

        assert observed.line_strings[0].coords_almost_equals([
            (0 - 2 + 4, 0 + 1 - 3),
            (1 - 2 + 4, 0 + 1 - 3),
            (2 - 2 + 4, 1 + 1 - 3)
        ])
        assert observed.line_strings[1].coords_almost_equals([
            (10 - 2 + 4, 10 + 1 - 3)
        ])
        assert observed.shape == (100, 100, 3)

    def test_shift_with_empty_list_of_line_strings(self):
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = self._func(lsoi, top=1, right=2, bottom=3, left=4)

        assert len(observed.line_strings) == 0
        assert observed.shape == (100, 100, 3)


# TODO test to_keypoints_on_image()
#      test invert_to_keypoints_on_image()
#      test to_xy_array()
#      test fill_from_xy_array_()
class TestLineStringsOnImage(unittest.TestCase):
    def setUp(self):
        reseed()

    def test___init__(self):
        lsoi = LineStringsOnImage([
            LineString([]),
            LineString([(0, 0), (5, 0)])
        ], shape=(10, 10, 3))
        assert len(lsoi.line_strings) == 2
        assert lsoi.shape == (10, 10, 3)

    def test___init___shape_is_array(self):
        image = np.zeros((10, 10, 3), dtype=np.uint8)
        with assertWarns(self, ia.DeprecationWarning):
            lsoi = LineStringsOnImage([
                LineString([]),
                LineString([(0, 0), (5, 0)])
            ], shape=image)
        assert len(lsoi.line_strings) == 2
        assert lsoi.shape == (10, 10, 3)

    def test___init___with_empty_list(self):
        lsoi = LineStringsOnImage([], shape=(10, 10))
        assert lsoi.line_strings == []
        assert lsoi.shape == (10, 10)

    def test_items(self):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        items = lsoi.items

        assert items == [ls1, ls2]

    def test_items_empty(self):
        kpsoi = LineStringsOnImage([], shape=(40, 50, 3))

        items = kpsoi.items

        assert items == []

    def test_empty_with_empty_list(self):
        lsoi = LineStringsOnImage([], shape=(10, 10, 3))
        assert lsoi.empty

    def test_empty_with_list_of_empty_line_string(self):
        lsoi = LineStringsOnImage([LineString([])], shape=(10, 10, 3))
        assert not lsoi.empty

    def test_empty_with_list_of_single_point_line_string(self):
        lsoi = LineStringsOnImage([LineString([(0, 0)])], shape=(10, 10, 3))
        assert not lsoi.empty

    def test_from_xy_arrays_single_input_array_with_two_line_strings(self):
        arrs = np.float32([
            [(0, 0), (10, 10), (5, 10)],
            [(5, 5), (15, 15), (10, 15)]
        ])

        lsoi = LineStringsOnImage.from_xy_arrays(arrs, shape=(100, 100, 3))

        assert len(lsoi.line_strings) == 2
        assert lsoi.line_strings[0].coords_almost_equals(arrs[0])
        assert lsoi.line_strings[1].coords_almost_equals(arrs[1])

    def test_from_xy_arrays_with_list_of_two_line_string_arrays(self):
        arrs = [
            np.float32([(0, 0), (10, 10), (5, 10)]),
            np.float32([(5, 5), (15, 15), (10, 15), (25, 25)])
        ]

        lsoi = LineStringsOnImage.from_xy_arrays(arrs, shape=(100, 100, 3))

        assert len(lsoi.line_strings) == 2
        assert lsoi.line_strings[0].coords_almost_equals(arrs[0])
        assert lsoi.line_strings[1].coords_almost_equals(arrs[1])

    def test_from_xy_arrays_with_empty_3d_array_and_0_points_per_ls(self):
        arrs = np.zeros((0, 0, 2), dtype=np.float32)

        lsoi = LineStringsOnImage.from_xy_arrays(arrs, shape=(100, 100, 3))

        assert len(lsoi.line_strings) == 0

    def test_from_xy_arrays_with_empty_3d_array_and_5_points_per_ls(self):
        arrs = np.zeros((0, 5, 2), dtype=np.float32)

        lsoi = LineStringsOnImage.from_xy_arrays(arrs, shape=(100, 100, 3))

        assert len(lsoi.line_strings) == 0

    def test_to_xy_arrays_with_two_line_strings_of_same_length(self):
        ls1 = LineString([(0, 0), (10, 10), (5, 10)])
        ls2 = LineString([(5, 5), (15, 15), (10, 15)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        arrs = lsoi.to_xy_arrays()

        assert isinstance(arrs, list)
        assert len(arrs) == 2
        assert arrs[0].dtype.name == "float32"
        assert arrs[1].dtype.name == "float32"
        assert np.allclose(arrs, [
            [(0, 0), (10, 10), (5, 10)],
            [(5, 5), (15, 15), (10, 15)]
        ])

    def test_to_xy_arrays_with_two_line_strings_of_different_lengths(self):
        ls1 = LineString([(0, 0), (10, 10), (5, 10)])
        ls2 = LineString([(5, 5), (15, 15), (10, 15), (25, 25)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        arrs = lsoi.to_xy_arrays()

        assert isinstance(arrs, list)
        assert len(arrs) == 2
        assert arrs[0].dtype.name == "float32"
        assert arrs[1].dtype.name == "float32"
        assert np.allclose(arrs[0], [(0, 0), (10, 10), (5, 10)])
        assert np.allclose(arrs[1], [(5, 5), (15, 15), (10, 15), (25, 25)])

    def test_to_xy_arrays_with_two_line_strings_one_of_them_empty(self):
        ls1 = LineString([])
        ls2 = LineString([(5, 5), (15, 15), (10, 15), (25, 25)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        arrs = lsoi.to_xy_arrays()

        assert isinstance(arrs, list)
        assert len(arrs) == 2
        assert arrs[0].dtype.name == "float32"
        assert arrs[1].dtype.name == "float32"
        assert arrs[0].shape == (0, 2)
        assert np.allclose(arrs[1], [(5, 5), (15, 15), (10, 15), (25, 25)])

    def test_to_xy_arrays_with_two_empty_list_of_line_strings(self):
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        arrs = lsoi.to_xy_arrays()

        assert isinstance(arrs, list)
        assert len(arrs) == 0

    def test_draw_on_image_with_default_settings(self):
        ls1 = LineString([(0, 0), (10, 10), (5, 10)])
        ls2 = LineString([(5, 5), (15, 15), (10, 15), (25, 25)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))
        img = np.zeros((100, 100, 3), dtype=np.uint8) + 1

        observed = lsoi.draw_on_image(img)

        expected = np.copy(img)
        for ls in [ls1, ls2]:
            expected = ls.draw_on_image(expected)
        assert np.array_equal(observed, expected)

    def test_draw_on_image_with_custom_settings(self):
        ls1 = LineString([(0, 0), (10, 10), (5, 10)])
        ls2 = LineString([(5, 5), (15, 15), (10, 15), (25, 25)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))
        img = np.zeros((100, 100, 3), dtype=np.uint8) + 1

        observed = lsoi.draw_on_image(img,
                                      color_lines=(0, 0, 255),
                                      color_points=(255, 0, 0),
                                      alpha_lines=0.5,
                                      alpha_points=0.6,
                                      antialiased=False)

        expected = np.copy(img)
        for ls in [ls1, ls2]:
            expected = ls.draw_on_image(expected,
                                        color_lines=(0, 0, 255),
                                        color_points=(255, 0, 0),
                                        alpha_lines=0.5,
                                        alpha_points=0.6,
                                        antialiased=False)
        assert np.array_equal(observed, expected)

    def test_draw_on_image_with_default_settings_one_ls_empty(self):
        ls1 = LineString([])
        ls2 = LineString([(5, 5), (15, 15), (10, 15), (25, 25)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))
        img = np.zeros((100, 100, 3), dtype=np.uint8) + 1

        observed = lsoi.draw_on_image(img)

        expected = np.copy(img)
        for ls in [ls1, ls2]:
            expected = ls.draw_on_image(expected)
        assert np.array_equal(observed, expected)

    def test_draw_on_image_with_default_settings_and_empty_list_of_ls(self):
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))
        img = np.zeros((100, 100, 3), dtype=np.uint8) + 1

        observed = lsoi.draw_on_image(img)

        expected = np.copy(img)
        assert np.array_equal(observed, expected)

    def test_remove_out_of_image_fraction_(self):
        item1 = ia.LineString([(5, 1), (9, 1)])
        item2 = ia.LineString([(5, 1), (15, 1)])
        item3 = ia.LineString([(15, 1), (25, 1)])
        cbaoi = ia.LineStringsOnImage([item1, item2, item3],
                                      shape=(10, 10, 3))

        cbaoi_reduced = cbaoi.remove_out_of_image_fraction_(0.6)

        assert len(cbaoi_reduced.items) == 2
        assert cbaoi_reduced.items == [item1, item2]
        assert cbaoi_reduced is cbaoi

    def test_remove_out_of_image_fraction(self):
        item1 = ia.LineString([(5, 1), (9, 1)])
        item2 = ia.LineString([(5, 1), (15, 1)])
        item3 = ia.LineString([(15, 1), (25, 1)])
        cbaoi = ia.LineStringsOnImage([item1, item2, item3],
                                      shape=(10, 10, 3))

        cbaoi_reduced = cbaoi.remove_out_of_image_fraction(0.6)

        assert len(cbaoi_reduced.items) == 2
        assert cbaoi_reduced.items == [item1, item2]
        assert cbaoi_reduced is not cbaoi

    def test_to_xy_array(self):
        lsoi = ia.LineStringsOnImage(
            [ia.LineString([(0, 0), (1, 2)]),
             ia.LineString([(10, 20), (30, 40)])],
            shape=(1, 2, 3))

        xy_out = lsoi.to_xy_array()

        expected = np.float32([
            [0.0, 0.0],
            [1.0, 2.0],
            [10.0, 20.0],
            [30.0, 40.0]
        ])
        assert xy_out.shape == (4, 2)
        assert np.allclose(xy_out, expected)
        assert xy_out.dtype.name == "float32"

    def test_to_xy_array__empty_object(self):
        lsoi = ia.LineStringsOnImage(
            [],
            shape=(1, 2, 3))

        xy_out = lsoi.to_xy_array()

        assert xy_out.shape == (0, 2)
        assert xy_out.dtype.name == "float32"

    def test_fill_from_xy_array___empty_array(self):
        xy = np.zeros((0, 2), dtype=np.float32)
        lsoi = ia.LineStringsOnImage([], shape=(2, 2, 3))

        lsoi = lsoi.fill_from_xy_array_(xy)

        assert len(lsoi.line_strings) == 0

    def test_fill_from_xy_array___empty_list(self):
        xy = []
        lsoi = ia.LineStringsOnImage([], shape=(2, 2, 3))

        lsoi = lsoi.fill_from_xy_array_(xy)

        assert len(lsoi.line_strings) == 0

    def test_fill_from_xy_array___array_with_two_coords(self):
        xy = np.array(
            [(100, 101),
             (102, 103),
             (200, 201),
             (202, 203)], dtype=np.float32)
        lsoi = ia.LineStringsOnImage(
            [ia.LineString([(0, 0), (1, 2)]),
             ia.LineString([(10, 20), (30, 40)])],
            shape=(2, 2, 3))

        lsoi = lsoi.fill_from_xy_array_(xy)

        assert len(lsoi.line_strings) == 2
        assert np.allclose(
            lsoi.line_strings[0].coords,
            [(100, 101), (102, 103)])
        assert np.allclose(
            lsoi.line_strings[1].coords,
            [(200, 201), (202, 203)])

    def test_fill_from_xy_array___list_with_two_coords(self):
        xy = [(100, 101),
              (102, 103),
              (200, 201),
              (202, 203)]
        lsoi = ia.LineStringsOnImage(
            [ia.LineString([(0, 0), (1, 2)]),
             ia.LineString([(10, 20), (30, 40)])],
            shape=(2, 2, 3))

        lsoi = lsoi.fill_from_xy_array_(xy)

        assert len(lsoi.line_strings) == 2
        assert np.allclose(
            lsoi.line_strings[0].coords,
            [(100, 101), (102, 103)])
        assert np.allclose(
            lsoi.line_strings[1].coords,
            [(200, 201), (202, 203)])

    def test_to_keypoints_on_image(self):
        lsoi = ia.LineStringsOnImage(
            [ia.LineString([(0, 0), (1, 2)]),
             ia.LineString([(10, 20), (30, 40)])],
            shape=(1, 2, 3))

        kpsoi = lsoi.to_keypoints_on_image()

        assert len(kpsoi.keypoints) == 2*2
        assert kpsoi.keypoints[0].x == 0
        assert kpsoi.keypoints[0].y == 0
        assert kpsoi.keypoints[1].x == 1
        assert kpsoi.keypoints[1].y == 2
        assert kpsoi.keypoints[2].x == 10
        assert kpsoi.keypoints[2].y == 20
        assert kpsoi.keypoints[3].x == 30
        assert kpsoi.keypoints[3].y == 40

    def test_to_keypoints_on_image__empty_instance(self):
        lsoi = ia.LineStringsOnImage([], shape=(1, 2, 3))

        kpsoi = lsoi.to_keypoints_on_image()

        assert len(kpsoi.keypoints) == 0

    def test_invert_to_keypoints_on_image_(self):
        lsoi = ia.LineStringsOnImage(
            [ia.LineString([(0, 0), (1, 2)]),
             ia.LineString([(10, 20), (30, 40)])],
            shape=(1, 2, 3))
        kpsoi = ia.KeypointsOnImage(
            [ia.Keypoint(100, 101), ia.Keypoint(102, 103),
             ia.Keypoint(110, 120), ia.Keypoint(130, 140)],
            shape=(10, 20, 30))

        lsoi_inv = lsoi.invert_to_keypoints_on_image_(kpsoi)

        assert len(lsoi_inv.line_strings) == 2
        assert lsoi_inv.shape == (10, 20, 30)
        assert np.allclose(
            lsoi.line_strings[0].coords,
            [(100, 101), (102, 103)])
        assert np.allclose(
            lsoi.line_strings[1].coords,
            [(110, 120), (130, 140)])

    def test_invert_to_keypoints_on_image___empty_instance(self):
        lsoi = ia.LineStringsOnImage([], shape=(1, 2, 3))
        kpsoi = ia.KeypointsOnImage([], shape=(10, 20, 30))

        lsoi_inv = lsoi.invert_to_keypoints_on_image_(kpsoi)

        assert len(lsoi_inv.line_strings) == 0
        assert lsoi_inv.shape == (10, 20, 30)

    def test_copy_with_two_line_strings(self):
        # basic test, without labels
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        observed = lsoi.copy()

        assert observed.line_strings[0] is ls1
        assert observed.line_strings[1] is ls2
        assert observed.shape == (100, 100, 3)

    def test_copy_with_two_line_strings_and_labels(self):
        # basic test, with labels
        ls1 = LineString([(0, 0), (1, 0), (2, 1)], label="foo")
        ls2 = LineString([(10, 10)], label="bar")
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        observed = lsoi.copy()

        assert observed.line_strings[0] is ls1
        assert observed.line_strings[1] is ls2
        assert observed.line_strings[0].label == "foo"
        assert observed.line_strings[1].label == "bar"
        assert observed.shape == (100, 100, 3)

    def test_copy_with_empty_list_of_line_strings(self):
        # LSOI is empty
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = lsoi.copy()

        assert observed.line_strings == []
        assert observed.shape == (100, 100, 3)

    def test_copy_and_replace_line_strings_attribute(self):
        # provide line_strings
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = lsoi.copy(line_strings=[ls1, ls2], shape=(200, 201, 3))

        assert observed.line_strings[0] is ls1
        assert observed.line_strings[1] is ls2
        assert observed.shape == (200, 201, 3)

    def test_copy_and_replace_line_strings_attribute_with_labeled_ls(self):
        # provide line_strings, with labels
        ls1 = LineString([(0, 0), (1, 0), (2, 1)], label="foo")
        ls2 = LineString([(10, 10)], label="bar")
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = lsoi.copy(line_strings=[ls1, ls2], shape=(200, 201, 3))

        assert observed.line_strings[0] is ls1
        assert observed.line_strings[1] is ls2
        assert observed.line_strings[0].label == "foo"
        assert observed.line_strings[1].label == "bar"
        assert observed.shape == (200, 201, 3)

    def test_copy_and_replace_line_strings_attribute_with_empty_list(self):
        # provide empty list of line_strings
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = lsoi.copy(line_strings=[], shape=(200, 201, 3))

        assert observed.line_strings == []
        assert observed.shape == (200, 201, 3)

    def test_deepcopy_with_two_line_strings(self):
        # basic test, without labels
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        observed = lsoi.deepcopy()

        assert observed.line_strings[0] is not ls1
        assert observed.line_strings[1] is not ls2
        assert observed.line_strings[0].coords_almost_equals(ls1)
        assert observed.line_strings[1].coords_almost_equals(ls2)
        assert observed.shape == (100, 100, 3)

    def test_deepcopy_with_two_line_strings_and_labels(self):
        # basic test, with labels
        ls1 = LineString([(0, 0), (1, 0), (2, 1)], label="foo")
        ls2 = LineString([(10, 10)], label="bar")
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        observed = lsoi.deepcopy()

        assert observed.line_strings[0] is not ls1
        assert observed.line_strings[1] is not ls2
        assert observed.line_strings[0].coords_almost_equals(ls1)
        assert observed.line_strings[1].coords_almost_equals(ls2)
        assert observed.line_strings[0].label == "foo"
        assert observed.line_strings[1].label == "bar"
        assert observed.shape == (100, 100, 3)

    def test_deepcopy_with_empty_list_of_line_strings(self):
        # LSOI is empty
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = lsoi.deepcopy()

        assert observed.line_strings == []
        assert observed.shape == (100, 100, 3)

    def test_deepcopy_and_replace_line_strings_attribute(self):
        # provide line_strings
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = lsoi.deepcopy(line_strings=[ls1, ls2], shape=(200, 201, 3))

        # line strings provided via line_strings are also deepcopied
        assert observed.line_strings[0].coords_almost_equals(ls1)
        assert observed.line_strings[1].coords_almost_equals(ls2)
        assert observed.shape == (200, 201, 3)

    def test_deepcopy_and_replace_line_strings_attribute_with_labeled_ls(self):
        # provide line_strings, with labels
        ls1 = LineString([(0, 0), (1, 0), (2, 1)], label="foo")
        ls2 = LineString([(10, 10)], label="bar")
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = lsoi.deepcopy(line_strings=[ls1, ls2], shape=(200, 201, 3))

        # line strings provided via line_strings are also deepcopied
        assert observed.line_strings[0].coords_almost_equals(ls1)
        assert observed.line_strings[1].coords_almost_equals(ls2)
        assert observed.line_strings[0].label == "foo"
        assert observed.line_strings[1].label == "bar"
        assert observed.shape == (200, 201, 3)

    def test_deepcopy_and_replace_line_strings_attribute_with_empty_list(self):
        # provide empty list of line_strings
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = lsoi.deepcopy(line_strings=[], shape=(200, 201, 3))

        assert observed.line_strings == []
        assert observed.shape == (200, 201, 3)

    def test___getitem__(self):
        cbas = [
            ia.LineString([(0, 0), (1, 0), (1, 1)]),
            ia.LineString([(1, 1), (2, 1), (2, 2)])
        ]
        cbasoi = ia.LineStringsOnImage(cbas, shape=(3, 4, 3))

        assert cbasoi[0] is cbas[0]
        assert cbasoi[1] is cbas[1]
        assert cbasoi[0:2] == cbas

    def test___iter__(self):
        cbas = [ia.LineString([(0, 0), (1, 1)]),
                ia.LineString([(1, 2), (3, 4)])]
        cbasoi = ia.LineStringsOnImage(cbas, shape=(40, 50, 3))

        for i, cba in enumerate(cbasoi):
            assert cba is cbas[i]

    def test___iter___empty(self):
        cbasoi = ia.LineStringsOnImage([], shape=(40, 50, 3))
        i = 0
        for _cba in cbasoi:
            i += 1
        assert i == 0

    def test___len__(self):
        cbas = [ia.LineString([(0, 0), (1, 1)]),
                ia.LineString([(1, 2), (3, 4)])]
        cbasoi = ia.LineStringsOnImage(cbas, shape=(40, 50, 3))
        assert len(cbasoi) == 2

    def test___repr__(self):
        def _func(obj):
            return obj.__repr__()

        self._test_str_repr(_func)

    def test___str__(self):
        def _func(obj):
            return obj.__str__()

        self._test_str_repr(_func)

    def test___repr___empty_list_of_line_strings(self):
        def _func(obj):
            return obj.__repr__()

        self._test_str_repr_empty_list_of_line_strings(_func)

    def test___str___empty_list_of_line_strings(self):
        def _func(obj):
            return obj.__str__()

        self._test_str_repr_empty_list_of_line_strings(_func)

    @classmethod
    def _test_str_repr(cls, func):
        ls1 = LineString([(0, 0), (1, 0), (2, 1)])
        ls2 = LineString([(10, 10)])
        lsoi = LineStringsOnImage([ls1, ls2], shape=(100, 100, 3))

        observed = func(lsoi)

        expected = "LineStringsOnImage([%s, %s], shape=(100, 100, 3))" % (
            func(ls1), func(ls2)
        )
        assert observed == expected

    @classmethod
    def _test_str_repr_empty_list_of_line_strings(cls, func):
        lsoi = LineStringsOnImage([], shape=(100, 100, 3))

        observed = func(lsoi)

        expected = "LineStringsOnImage([], shape=(100, 100, 3))"
        assert observed == expected


def _coords_eq(ls, other):
    return ls.coords_almost_equals(other, max_distance=1e-2)


def _drawing_allclose(arr, v):
    # draw_points_heatmaps_array() and draw_line_heatmap_array() are
    # currently limited to 1/255 accuracy due to drawing in uint8
    return np.allclose(arr, v, atol=(1.01/255), rtol=0)
