import copy import math import gradio as gr import cv2 import numpy as np from perspective_transform import compute_target_coords, direct_linear_transformation roi_coords = [] input_image = None def get_warped_image(): """ Computes the transformation matrix given the source and target points :return: """ global roi_coords, input_image if len(roi_coords) == 4: pts = np.array(roi_coords, np.int32) target_coords = compute_target_coords(pts) homography = direct_linear_transformation(roi_coords, target_coords) coordinates = { "input": { "top-left": "({}, {})".format(roi_coords[0][0], roi_coords[0][1]), "top-right": "({}, {})".format(roi_coords[1][0], roi_coords[1][1]), "bottom-right": "({}, {})".format(roi_coords[2][0], roi_coords[2][1]), "bottom-left": "({}, {})".format(roi_coords[3][0], roi_coords[3][1]) }, "projected": { "top-left": "({}, {})".format(int(target_coords[0].tolist()[0]), int(target_coords[0].tolist()[1])), "top-right": "({}, {})".format(int(target_coords[1].tolist()[0]), int(target_coords[1].tolist()[1])), "bottom-right": "({}, {})".format(int(target_coords[2].tolist()[0]), int(target_coords[2].tolist()[1])), "bottom-left": "({}, {})".format(int(target_coords[3].tolist()[0]), int(target_coords[3].tolist()[1])) } } mask = np.zeros((input_image.shape[0], input_image.shape[1])) cv2.fillConvexPoly(mask, pts, 1) mask = mask.astype(bool) cropped = np.zeros_like(input_image) cropped[mask] = input_image[mask] warped = cv2.warpPerspective(cropped, homography, (cropped.shape[:2][1], cropped.shape[:2][0])) # Crop the warped image to be just the contents within target_coords contours, hierachy = cv2.findContours(cv2.cvtColor(warped, cv2.COLOR_RGB2GRAY), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) x, y, w, h = cv2.boundingRect(contours[0]) contour_crop = warped[y:y + h, x:x + w] return coordinates, contour_crop, np.around(homography, 2), *contour_crop.shape else: return None, None, None, None, None def click_callback(img_path, evt: gr.SelectData): """ This callback is triggered when the user clicks on the image. Whenever the user clicks on the image, add a new coordinate, or adjust the location of an existing coordinate. If there are four coordinates, we automatically return the warped image. :param img_path: (str) The path to the temporary image Gradio saves :param evt: (gr.SelectData) If we specify the type hint, the type is automatically determined :return: (tuple) The image with overlays, the expanded outputs of get_warped_image() """ global roi_coords, input_image img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) # Save off a copy of the first input image if len(roi_coords) == 0: input_image = copy.copy(img) # Either create a new coordinate, or adjust the position of an existing coordinate if len(roi_coords) < 4: roi_coords.append(evt.index) else: distances = [math.dist(evt.index, coord) for coord in roi_coords] roi_coords[np.argmin(distances)] = evt.index if len(roi_coords) == 4: display_image = copy.copy(input_image) # Overlay the corners of the ROI pts = np.array(roi_coords, np.int32).reshape((-1, 1, 2)) cv2.polylines(display_image, [pts], True, (255, 255, 255), 2) # Always overlay the location of the coordinates for coord in roi_coords: cv2.circle(display_image, coord, radius=5, color=(255, 0, 0), thickness=-1) return display_image, *get_warped_image() else: return input_image, *get_warped_image() def clear_variables(*kwargs): """ Clears the defined coordinates and the input image. :param kwargs: (tuple) Depending on who calls this function, there may be unecessary input arguments :return: (None) Clears the image component in the Gradio app """ global roi_coords, input_image roi_coords = [] input_image = None return input_image def resize_image(img_path, width=None, height=None): """ Resizes the input image to the given width/height while maintaining the original remaining dimension. :param img_path: (str) The path to the temporary image Gradio saves :param width: (int) The desired image width :param height: (int) The desired image height :return: (np.ndarray) The resized image """ if img_path is not None: img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) img_height, img_width = img.shape[:2] img = cv2.resize(img, (width, img_height)) if width is not None else cv2.resize(img, (img_width, height)) return img else: return None with gr.Blocks() as demo: gr.Markdown("

Points-4-Perspective

") gr.Markdown("

Click on the top-left, top-right, bottom-right, and bottom-left corners of the ROI

") button_clear = gr.Button("Clear Inputs") with gr.Row(): with gr.Column(): image_input = gr.Image(label="Input Image", type="filepath", value="warp_test_images/test.jpg", height=900) gallery = gr.Examples( fn=clear_variables, run_on_click=True, examples=[ "warp_test_images/test.jpg", "warp_test_images/test2.jpg", "warp_test_images/test4.png", "warp_test_images/billboard.jpg", "warp_test_images/billboard2.jpg", "warp_test_images/venice.jpg", "warp_test_images/palacio_vergara.jpg" ], inputs=image_input ) with gr.Column(): image_output = gr.Image(label="Cropped Warp", type="filepath", tool="editor", interactive=True) slider_image_width = gr.Slider(label="Width", minimum=10, maximum=900, step=1) slider_image_height = gr.Slider(label="Height", minimum=10, maximum=900, step=1) numpy_t_matrix = gr.Numpy(label="Transformation Matrix", row_count=3, col_count=3, headers=['', '', ''], interactive=False) json_coordinates = gr.JSON(label="Coordinates") image_input.select(click_callback, image_input, [image_input, json_coordinates, image_output, numpy_t_matrix, slider_image_width, slider_image_height]) image_input.clear(clear_variables) button_clear.click(clear_variables, None, image_input) slider_image_width.release(resize_image, [image_output, slider_image_width, gr.State(None)], image_output) slider_image_height.release(resize_image, [image_output, gr.State(None), slider_image_height], image_output) demo.queue(concurrency_count=10, max_size=20) # demo.launch(inbrowser=True) demo.launch()