balthou's picture
move decorators to main function
42e82ef
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)