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()