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)