def approximate_polygon(args, dir_path, image_name, image):
    """Traces the polygon of the shape in the image, then approximates it with
    thick-edge approximation. The two polygons are overlaid on the original
    image and saved to file. Stats the polygons are returned."""

    # Find the polygons in the image, and verify there is only one.
    gray_image = cv2.cvtColor(image, code=cv2.COLOR_BGR2GRAY)
    (contours, _) = cv2.findContours(gray_image, cv2.RETR_EXTERNAL,
            cv2.CHAIN_APPROX_SIMPLE)
    if len(contours) != 1:
        return None
    (num_points, _, num_dim) = contours[0].shape
    polygon = numpy.reshape(contours[0], [num_points, num_dim])

    # Calculate the thickness parameter and approximate the polygon.
    (height, width) = gray_image.shape
    thickness = min(height, width) * args.thickness
    approx_polygon = thick_polygonal_approximate(polygon, thickness)

    # Create the subdirectory in the output directory if it doesn't exist
    subdir_path = path.relpath(dir_path, args.data_dir)
    output_subdir_path = path.join(args.output_dir, subdir_path)
    if not path.exists(output_subdir_path):
        makedirs(output_subdir_path)

    # Compare the polygons represented by the two polygons
    (vertices, _) = polygon.shape
    (approx_vertices, _) = approx_polygon.shape
    (area, approx_area, vertex_diff, area_diff) = compare_polygons(polygon,
            approx_polygon)

    # Report the statistics to the user
    image_path = path.join(subdir_path, image_name)
    print("\nImage {} Statistics:".format(image_path))
    print("\tPolygon Vertices:              {:<10}".format(vertices))
    print("\tPolygon Area:                  {:<10.3f}".format(area))
    print("\tApproximated Polygon Vertices: {:<10} ({:0.3f}%)".format(
            approx_vertices, vertex_diff * 100))
    print("\tApproximated Polygon Area:     {:<10.3f} ({:0.3f}%)".format(
            approx_area, area_diff * 100))

    # Create figures for and save the image with the original and approximated
    # polygons overlaid.
    polygon_figure = overlay_polygon(image, polygon, thickness, image_name,
            "polygon", args.output_dir)
    approx_figure = overlay_polygon(image, approx_polygon, thickness,
            image_name, "approx", args.output_dir)

    # If specified by user, show each plot, then close the plots
    if args.show_images:
        pyplot.show()
    pyplot.close(polygon_figure)
    pyplot.close(approx_figure)

    return (image_path, vertices, approx_vertices, area, approx_area,
            vertex_diff * 100, area_diff * 100)
    def test_large_point_set(self):
        """Tests the function with a larger, complete polygon point set, in this
        case, a circle."""

        # Generate the x points for a circle of radius 10, centered at (40, 100)
        radius = 10
        centerpoint = numpy.array([40, 100])
        circle_upper_xs = numpy.linspace(-radius, radius, num=10000)
        circle_lower_xs = numpy.flip(circle_upper_xs[1:-1], axis=0)

        # Generate the y points for the circle, and combine the x and y points.
        circle_upper_ys = numpy.sqrt(radius ** 2 - circle_upper_xs ** 2)
        circle_lower_ys = -numpy.sqrt(radius ** 2 - circle_lower_xs ** 2)
        circle_ys = numpy.concatenate([circle_upper_ys, circle_lower_ys])
        circle_xs = numpy.concatenate([circle_upper_xs, circle_lower_xs])
        points = numpy.column_stack([circle_xs, circle_ys]) + centerpoint

        # The thickness used, and the polygonal function call
        thickness = 0.01 * radius
        dominant_points = thick_polygonal_approximate(points, thickness)

        # Verify the format of the output
        self.assertIsInstance(dominant_points, numpy.ndarray)
        self.assertEqual(dominant_points.shape[1], 2)

        # Compare the original area to the new area, and the reduction in the
        # number of vertices.
        (vertices, _) = points.shape
        (dominant_vertices, _) = dominant_points.shape
        (area, dominant_area, vertex_diff, area_diff) = compare_polygons(points,
                dominant_points)

        # Report the results to the user
        print("\nLarge Point Set Test Results:")
        print("\tPolygon Vertices:              {:<10}".format(vertices))
        print("\tPolygon Area:                  {:<10.3f}".format(area))
        print("\tApproximated Polygon Vertices: {:<10} ({:0.3f}%)".format(
                dominant_vertices, vertex_diff * 100))
        print("\tApproximated Polygon Area:     {:<10.3f} ({:0.3f}%)".format(
                dominant_area, area_diff * 100))

        # If specified on the command line, show the plot
        if BasicUnitTests.LARGE_POINT_SET_SHOW_PLOT:
            pyplot.plot(points[:, 0], points[:, 1], 'r', color='r',
                    label='Original Polygon', linewidth=5)
            pyplot.plot(dominant_points[:, 0], dominant_points[:, 1], 'r',
                    color='b', label='Approximated Polygon', linewidth=3)
            pyplot.legend()
            pyplot.show()
    def test_no_minimum(self):
        """Tests that the function can handle when there is no minimum point,
        but there is a maximum."""

        # The parameters for the test, and the function call
        thickness = 40
        points = numpy.array([
            [20, 100],
            [30, 170],
            [40, 105],
        ])
        dominant_points = thick_polygonal_approximate(points, thickness)

        # Verify the output
        self.assertIsInstance(dominant_points, numpy.ndarray)
        self.assertEqual(dominant_points.shape, points.shape)
        self.assertTrue(numpy.array_equal(dominant_points, points))
    def test_horizontal_line(self):
        """Tests that the function can handle a horizontal regression line that
        passes through the endpoints. This is a thick line test."""

        # The parameters for the test, and the function call
        thickness = 40
        points = numpy.array([
            [20, 100],
            [30, 30],
            [40, 170],
            [50, 100],
        ])
        dominant_points = thick_polygonal_approximate(points, thickness)

        # Verify the output
        self.assertIsInstance(dominant_points, numpy.ndarray)
        self.assertEqual(dominant_points.shape, points.shape)
        self.assertTrue(numpy.array_equal(dominant_points, points))
    def test_thick_curve(self):
        """Tests the function with a simple 4-point polyline with a min and max
        that are thick enough, so all points are dominant."""

        # The parameters for the test, and the function call
        thickness = 40
        points = numpy.array([
            [20, 100],
            [30, 30],
            [40, 170],
            [50, 105],
        ])
        dominant_points = thick_polygonal_approximate(points, thickness)

        # Verify the output
        self.assertIsInstance(dominant_points, numpy.ndarray)
        self.assertEqual(dominant_points.shape, points.shape)
        self.assertTrue(numpy.array_equal(dominant_points, points))
    def test_reverse_order(self):
        """Tests that the function can also handle points in reverse order. This
        is the same test as the thick_curve_test, with the ordering of the
        points reversed."""

        # The parameters for the test, and the function call
        thickness = 40
        points = numpy.array([
            [50, 105],
            [40, 170],
            [30, 30],
            [20, 100],
        ])
        dominant_points = thick_polygonal_approximate(points, thickness)

        # Verify the output
        self.assertIsInstance(dominant_points, numpy.ndarray)
        self.assertEqual(dominant_points.shape, points.shape)
        self.assertTrue(numpy.array_equal(dominant_points, points))
    def test_thin_curve(self):
        """Tests the function with a simple 4-point polyline with a min and max
        that are not thick enough to be considered dominant points."""

        # The parameters for the test, and the function call
        thickness = 40
        points = numpy.array([
            [20, 20],
            [30, 15],
            [40, 25],
            [50, 23],
        ])
        dominant_points = thick_polygonal_approximate(points, thickness)

        # Verify the output
        self.assertIsInstance(dominant_points, numpy.ndarray)
        self.assertEqual(dominant_points.shape, (2, 2))
        self.assertTrue(numpy.array_equal(dominant_points,
                [[20, 20], [50, 23]]))
def main():
    """The main function for the script."""

    # Parse the arguments, and run a basic sanity check on them.
    args = parse_arguments()
    sanity_check(args)

    # Either open up the video file or the camera specified by the user.
    if args.video_file is not None:
        video_stream = cv2.VideoCapture(args.video_file)
        error_msg = "Error: {}: Unable to open video file.".format(
            args.video_file)
    else:
        video_stream = cv2.VideoCapture(args.camera_id)
        error_msg = "Error: Unable to open camera with id {}.".format(
            args.camera_id)
    if not video_stream.isOpened():
        print(error_msg)
        exit(1)

    # Extract the name of the video. For video files, this is the path without
    # the extension. On Linux systems, we name the cameras as /dev/video[x].
    if args.video_file is not None:
        video_file = args.video_file
        (video_name, _) = path.splitext(args.video_file)
    else:
        video_file = "/dev/video{}".format(args.camera_id)
        video_name = "camera{}".format(args.camera_id)

    # If a camera is being used, set it to its maximum resolution, or the one
    # specified by the user.
    if args.video_file is None:
        video_stream.set(cv2.cv.CV_CAP_PROP_FRAME_WIDTH, args.camera_width)
        video_stream.set(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT, args.camera_height)

    # If the thickness was specified as a fraction compute the thickness as the
    # fraction of the smaller image dimension.
    frame_width = int(round(video_stream.get(cv2.cv.CV_CAP_PROP_FRAME_WIDTH)))
    frame_height = int(round(video_stream.get(
        cv2.cv.CV_CAP_PROP_FRAME_HEIGHT)))
    if args.thickness < 1.0:
        thickness = args.thickness * min(frame_width, frame_height)
    else:
        thickness = args.thickness

    # Inform the user of the camera resolution and the controls
    print("\nThick Polygonal Approximation Application:")
    print("\tVideo Stream Resolution: {:d}x{:d}".format(
        frame_width, frame_height))
    print("\tPress 's' to save the current frame, 'q' to quit.")
    print("\tPress 'r' to reset the background subtraction algorithm's "
          "state.\n")

    try:
        # Initialize the BGS, grab the polygons from the first frame.
        bgs_algorithm = init_bgs()
        (frame,
         polygons) = process_frame(bgs_algorithm,
                                   video_stream,
                                   show_intermediate=args.show_intermediate)

        # While frames remain, iterate over each and overlay the polygons.
        frame_num = 0
        while frame is not None:
            # Approximate the polygons for the outlines, and overlay them.
            thick_polygons = [
                thick_polygonal_approximate(polygon, thickness)
                for polygon in polygons
            ]
            cv2.polylines(frame,
                          thick_polygons,
                          isClosed=True,
                          color=(180, 40, 100),
                          thickness=int(thickness / 2))

            # Show the frame with the polygons overlaid, and process user keys.
            cv2.imshow(video_file, frame)
            key_pressed = chr(cv2.waitKey(50) & 0xFF)

            # Take the appropriate action based on the key that was pressed.
            if key_pressed == 'r':
                print("Resetting background subtraction algorithm state...")
                bgs_algorithm = init_bgs()
            elif key_pressed == 's':
                frame_path = "{}_{}.png".format(video_name, frame_num)
                print("Saving frame to '{}'...".format(frame_path))
                cv2.imwrite(frame_path, frame)
            elif key_pressed == 'q':
                print("Quitting...")
                break

            # Read the next frame and extract its polygons
            (frame, polygons) = process_frame(
                bgs_algorithm,
                video_stream,
                show_intermediate=args.show_intermediate)
            frame_num += 1

    except KeyboardInterrupt:
        print("\nKeyboard interrupt received. Quitting...")

    # Destroy all the open windows
    cv2.destroyAllWindows()