Spaces:
Sleeping
Sleeping
| from interactive_pipe import interactive_pipeline, interactive, Image | |
| from typing import Tuple | |
| import numpy as np | |
| # Helper functions | |
| # ---------------- | |
| def flip_image(img, flip=True, mirror=True): | |
| img = img[::-1] if flip else img | |
| img = img[:, ::-1] if mirror else img | |
| return img | |
| def get_crop(img, pos_x, pos_y, crop_size=0.1): | |
| c_size = int(crop_size * img.shape[0]) | |
| crop_x = (int(pos_x * img.shape[1]), int((pos_x) * img.shape[1]) + c_size) | |
| crop_y = (int(pos_y * img.shape[0]), int((pos_y) * img.shape[0]) + c_size) | |
| return crop_x, crop_y | |
| # Processing blocks | |
| # ----------------- | |
| def generate_feedback_ribbon() -> Tuple[np.ndarray, np.ndarray]: | |
| """Generate green and red ribbons for feedback""" | |
| flat_array = np.ones((800, 12, 3)) | |
| colors = [[0., 1., 0.], [1., 0., 0.]] | |
| ribbons = [flat_array*np.array(col)[None, None, :] for col in colors] | |
| return ribbons[0], ribbons[1] | |
| DIFFICULY = {"easy": 0.18, "medium": 0.1, "hard": 0.05} | |
| DIFFICULY_LEVELS = list(DIFFICULY.keys()) | |
| def generate_random_puzzle( | |
| seed: int = 43, | |
| difficulty: str = DIFFICULY_LEVELS[0], | |
| context: dict = {} | |
| ): | |
| """Generate random puzzle configuration and store in context. | |
| Configuration = 2D position and flip/mirror. | |
| Freeze seed for reproducibility. | |
| """ | |
| np.random.seed(seed) | |
| pos_x, pos_y = np.random.uniform(0.2, 0.8, 2) | |
| context["puzzle_pos"] = (pos_x, pos_y) | |
| context["puzzle_flip_mirror"] = np.random.choice([True, False], 2) | |
| context["puzzle_piece_size"] = DIFFICULY.get(difficulty, 0.18) | |
| def create_puzzle( | |
| img: np.ndarray, | |
| intensity: float = 0.4, | |
| context: dict = {} | |
| ) -> Tuple[np.ndarray, np.ndarray]: | |
| """Extract puzzle piece from image. Make a dark hole where the """ | |
| out = img.copy() | |
| x_gt, y_gt = context["puzzle_pos"] | |
| flip_gt, mirror_gt = context["puzzle_flip_mirror"] | |
| cs_x, cs_y = get_crop( | |
| img, x_gt, y_gt, crop_size=context["puzzle_piece_size"]) | |
| crop = img[cs_y[0]:cs_y[1], cs_x[0]:cs_x[1], ...] | |
| out[cs_y[0]:cs_y[1], cs_x[0]:cs_x[1]] = intensity*crop | |
| crop = flip_image(crop, flip=flip_gt, mirror=mirror_gt) | |
| return out, crop | |
| def flip_mirror_piece( | |
| piece: np.ndarray, | |
| flip: bool = False, | |
| mirror: bool = False, | |
| context: dict = {} | |
| ) -> np.ndarray: | |
| """Flip and/or mirror the puzzle piece.""" | |
| context["user_flip_mirror"] = (flip, mirror) | |
| return flip_image(piece.copy(), flip=flip, mirror=mirror) | |
| def place_puzzle( | |
| puzzle: np.ndarray, | |
| piece: np.ndarray, | |
| pos_x: float = 0.5, | |
| pos_y: float = 0.5, | |
| context: dict = {} | |
| ) -> np.ndarray: | |
| """Place the puzzle piece at the user-defined position.""" | |
| out = puzzle.copy() | |
| context["user_pos"] = (pos_x, pos_y) | |
| cp_x, cp_y = get_crop( | |
| img, pos_x, pos_y, crop_size=context["puzzle_piece_size"]) | |
| out[cp_y[0]:cp_y[1], cp_x[0]:cp_x[1]] = piece | |
| return out | |
| TOLERANCES = {"low": 0.01, "medium": 0.02, "high": 0.05} | |
| TOLERANCE_LEVELS = list(TOLERANCES.keys()) | |
| def check_puzzle(tolerance: str = "low", context: dict = {}) -> None: | |
| """Check if the user placed the puzzle piece correctly. | |
| Store the result in the context.""" | |
| x_gt, y_gt = context["puzzle_pos"] | |
| flip_gt, mirror_gt = context["puzzle_flip_mirror"] | |
| x, y = context["user_pos"] | |
| flip, mirror = context["user_flip_mirror"] | |
| check_pos = np.allclose([x_gt, y_gt], [x, y], | |
| atol=TOLERANCES.get(tolerance, 0.01)) | |
| check_flip_mirror = (flip_gt == flip) and (mirror_gt == mirror) | |
| success = check_pos and check_flip_mirror | |
| context["success"] = success | |
| def display_feedback( | |
| puzzle: np.ndarray, | |
| ok_ribbon: np.ndarray, | |
| nok_ribbon: np.ndarray, | |
| context: dict = {} | |
| ) -> np.ndarray: | |
| """Display green/red ribbon on the right side of the puzzle.""" | |
| success = context.get("success", False) | |
| ribbon = ok_ribbon if success else nok_ribbon | |
| out = np.hstack([puzzle, ribbon[:puzzle.shape[0], ...]]) | |
| return out | |
| # pipeline definition | |
| # ------------------- | |
| def captcha_pipe(inp): | |
| ok_ribbon, nok_ribbon = generate_feedback_ribbon() | |
| generate_random_puzzle() | |
| puzzle, puzzle_piece = create_puzzle(inp) | |
| puzzle_piece = flip_mirror_piece(puzzle_piece) | |
| puzzle = place_puzzle(puzzle, puzzle_piece) | |
| check_puzzle() | |
| puzzle = display_feedback(puzzle, ok_ribbon, nok_ribbon) | |
| return puzzle | |
| # add interactivity | |
| # ----------------- | |
| def main(img: np.ndarray, backend="gradio", debug: bool = False): | |
| # If debug mode, add interactive sliders to tune the puzzle generation | |
| # and help the "game master" design a feasible puzzle. | |
| if debug: | |
| interactive( | |
| tolerance=(TOLERANCE_LEVELS[0], TOLERANCE_LEVELS, "Tolerance") | |
| )(check_puzzle) | |
| interactive( | |
| seed=(43, [0, 100], "Puzzle seed"), | |
| difficulty=(DIFFICULY_LEVELS[0], DIFFICULY_LEVELS, "Difficulty") | |
| )(generate_random_puzzle) | |
| interactive( | |
| pos_x=(0.5, [0.1, 0.9, 0.005], "Position X", ["left", "right"]), | |
| pos_y=(0.5, [0.1, 0.9, 0.005], "Position Y", ["up", "down"]), | |
| )(place_puzzle) | |
| # left, right, up, down will only supported when using the Qt backend | |
| interactive( | |
| flip=(False, "Flip Image"), | |
| mirror=(False, "Mirror Image"), | |
| )(flip_mirror_piece) | |
| captcha_pipe_interactive = interactive_pipeline( | |
| gui=backend, | |
| cache=True, | |
| markdown_description=markdown_description | |
| )(captcha_pipe) | |
| captcha_pipe_interactive(img) | |
| if __name__ == "__main__": | |
| import argparse | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("-b", "--backend", default="gradio", | |
| choices=["gradio", "qt", "mpl"], type=str) | |
| parser.add_argument( | |
| "-d", "--debug", action="store_true", | |
| help="Debug mode (to tune difficulty and tolerance)" | |
| ) | |
| args = parser.parse_args() | |
| markdown_description = "# Code to build this app on gradio \n\n" | |
| markdown_description += "In local, try using `python app.py --backend qt --debug` to get the best experience with keyboard support aswell \n\n" | |
| markdown_description += "Please note that matplobi`--backend mpl` is also functional although it won't look as good\n\n" | |
| markdown_description += "```python\n"+open(__file__, 'r').read()+"```" | |
| img = Image.load_image("sample.jpg") | |
| main(img, backend=args.backend, debug=args.debug) | |