def _by_points(self,
                   case_name,
                   n,
                   *,
                   angle_min=0,
                   angle_max=359,
                   noise_error=0.0):
        w, h = 300, 150
        ellipse = shapes.Ellipse(w // 2, h // 2, w // 3, h // 3, 20)

        img_contour = np.zeros((h, w), np.uint8)
        ellipse.draw(img_contour, 255)
        self.dump_debug_img(Path(case_name) / '0_ellipse.png', img_contour)

        np.random.seed(239)
        angles = np.random.randint(angle_min, angle_max, (n, ))
        xys = np.float32(list(map(ellipse.calculate_point, angles)))
        xys += np.random.random_sample((len(xys), 2)) * noise_error

        img_points = img_contour // 3
        draw_pixels(img_points, xys, 255)
        self.dump_debug_img(Path(case_name) / '1_points.png', img_points)

        estimation = fit_ellipse(xys)
        estimation.draw(img_points, 255)
        self.dump_debug_img(Path(case_name) / '2_estimation.png', img_points)

        self.assertTrue(
            np.all(np.abs(np.array(ellipse) - np.array(estimation)) < 3.0))
    def _by_points(self, case_name, n, *,
                   angle_min=0, angle_max=359, noise_error=0.0):
        w, h = 300, 150
        ellipse = shapes.Ellipse(w // 2, h // 2, w // 3, h // 3, 20)

        img_contour = np.zeros((h, w), np.uint8)
        ellipse.draw(img_contour, 255)
        self.dump_debug_img(Path(case_name) / '0_ellipse.png', img_contour)

        np.random.seed(239)
        angles = np.random.randint(angle_min, angle_max, (n,))
        xys = np.float32(list(map(ellipse.calculate_point, angles)))
        xys += np.random.random_sample((len(xys), 2)) * noise_error

        img_points = img_contour // 3
        draw_pixels(img_points, xys, 255)
        self.dump_debug_img(Path(case_name) / '1_points.png', img_points)

        estimation = fit_ellipse(xys)
        estimation.draw(img_points, 255)
        self.dump_debug_img(Path(case_name) / '2_estimation.png', img_points)

        self.assertTrue(np.all(np.abs(np.array(ellipse) - np.array(estimation)) < 3.0))
    def process_contour(self, contour):
        points = contour.reshape(-1, 2)
        found_ellipses = []
        n = len(points)
        mask = np.ones(n, np.bool)

        if n < self._min_pixels:
            return found_ellipses

        for ellipse in self.ellipses:
            err = self._distance_to_ellipse_as_circle(points, ellipse)
            mask[err < self._err_threshold] = False

        for from_i in range(0, len(points), self._min_pixels // 4):
            number_to_sample = 3 * len(points) // 5
            sub_points = points[from_i:][:number_to_sample]
            sub_points = sub_points[mask[from_i:][:number_to_sample]]
            if len(sub_points) < self._min_pixels // 2:
                continue

            max_inliers = 0
            best_ellipse = None
            for i in range(20):
                indices = np.arange(len(sub_points))
                np.random.shuffle(indices)
                sample_points = sub_points[indices[:self._min_pixels // 2]]
                assert len(sample_points) >= 5

                ellipse = fit_ellipse(sample_points)
                if not self._is_ok(ellipse):
                    continue
                err = self._distance_to_ellipse_as_circle(points[mask], ellipse)
                inliers_mask = err < self._err_threshold
                inliers = inliers_mask.sum()

                if inliers < self._min_pixels:
                    continue

                if inliers > max_inliers:
                    max_inliers = inliers
                    best_ellipse = ellipse

            if best_ellipse is not None:
                err = self._distance_to_ellipse_as_circle(points[mask], best_ellipse)
                inliers_mask = err < self._err_threshold

                for i in range(3):
                    if inliers_mask.sum() < 5:
                        break
                    ellipse = fit_ellipse(points[mask][inliers_mask])
                    if not self._is_ok(ellipse):
                        break
                    err = self._distance_to_ellipse_as_circle(points[mask], ellipse)
                    inliers_mask = err < self._err_threshold

                if inliers_mask.sum() < 5 or not self._is_ok(ellipse):
                    break
                sectors_number = self._min_pixels // 2
                covered_sectors_number = len(self._calculate_sectors(points[mask][inliers_mask], ellipse, sectors_number))

                if covered_sectors_number / sectors_number < self._min_sectors_fraction:
                    continue

                err = self._distance_to_ellipse_as_circle(points, ellipse)
                mask[err < self._err_threshold] = False
                self.ellipses.append(ellipse)
                found_ellipses.append(ellipse)
        return found_ellipses