From a8e65029538ba1107a4e1ab3112268fe60155890 Mon Sep 17 00:00:00 2001 From: sqoshi Date: Mon, 9 Aug 2021 23:33:11 +0200 Subject: [PATCH 1/4] mask points by clicks --- run.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/run.py b/run.py index 9de619f..4f4d191 100644 --- a/run.py +++ b/run.py @@ -1,6 +1,7 @@ from argparse import ArgumentParser, Namespace import cv2 +import numpy as np from mask_imposer.colored_logger import get_configured_logger from mask_imposer.definitions import ImageFormat, Output, Improvements, MaskSet @@ -38,10 +39,28 @@ def main(): args = _parse_args() improvements = Improvements(args.show_samples, args.draw_landmarks) mask_set = MaskSet(args.mask_img, args.mask_coords) + + window = "Include Help" + img = cv2.imread(mask_set.img_path) + cv2.namedWindow(window) + + def capture_event(event, x, y, flags, params): + if event == cv2.EVENT_LBUTTONDBLCLK: + cv2.circle(img, (x, y), int(img.shape[0] / 40), (255, 0, 0), -1) + print(x, y) + + cv2.setMouseCallback(window, capture_event) + + while True: + cv2.imshow(window, img) + if cv2.waitKey(1) == 13: + break + cv2.destroyAllWindows() + # img = cv2.imread(args.mask_img) # cv2.imshow("example", img) # cv2.waitKey(0) - # exit() + exit() output = Output(args.output_dir, args.output_format) inspector = Inspector(logger) From f6bceb7e4c374475207ca3272ac04ebffb2c359c Mon Sep 17 00:00:00 2001 From: sqoshi Date: Sat, 28 Aug 2021 18:45:27 +0200 Subject: [PATCH 2/4] todo: check if everything works --- mask_imposer/collector/__init__.py | 1 + mask_imposer/collector/coords_collector.py | 80 ++++++++++++++++++++++ run.py | 50 +++++++------- 3 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 mask_imposer/collector/__init__.py create mode 100644 mask_imposer/collector/coords_collector.py diff --git a/mask_imposer/collector/__init__.py b/mask_imposer/collector/__init__.py new file mode 100644 index 0000000..3544844 --- /dev/null +++ b/mask_imposer/collector/__init__.py @@ -0,0 +1 @@ +from .coords_collector import CoordinatesCollector diff --git a/mask_imposer/collector/coords_collector.py b/mask_imposer/collector/coords_collector.py new file mode 100644 index 0000000..5ab5ae9 --- /dev/null +++ b/mask_imposer/collector/coords_collector.py @@ -0,0 +1,80 @@ +from logging import Logger +from typing import List, Dict, Optional, Any +from termcolor import colored + +import cv2 +from mask_imposer.imposer.mask_pointers import Pointer + + +class CoordinatesCollector: + """Class allow to input mask characteristic coordinates by clicking them on this image.""" + + def __init__(self, mask_img_path: str, logger: Logger) -> None: + self._logger = logger + self.window = "Manual collector." + self.mask_img_path = mask_img_path + self.img = cv2.imread(mask_img_path) + self.candidates: List[List[int]] = [] + self.mask_coords: Optional[Dict[int, List[int]]] = None + + def _capture_event( + self, event: cv2.cuda_Event, x: int, y: int, + flags: List[Any], params: List[Any] # pylint:disable=W0613 + ) -> None: + """Captures coordinates from left/ middle mouse click event inside image.""" + if event in {cv2.EVENT_LBUTTONDBLCLK, cv2.EVENT_MBUTTONDBLCLK} \ + and len(self.candidates) <= 4: + print(colored("Marked ", "yellow") + + colored(f"[{x}, {y}]", "green") + + colored(" point.", "yellow")) + cv2.circle(self.img, (x, y), int(self.img.shape[0] / 40), (255, 0, 0), -1) + self.candidates.append([x, y]) + + def _intercept_coords(self) -> None: + """Collects coordinates in loop until getting 4 coords pairs.""" + while True: + cv2.imshow(self.window, self.img) + cv2.waitKey(1) + if len(self.candidates) == 4: + cv2.imshow(self.window, self.img) + cv2.waitKey(1) + break + + def collect(self) -> Optional[Dict[int, List[int]]]: + """Creates json with collected from clickable input coordinates.""" + self._logger.info("Collecting mask coordinates interactively.") + cv2.namedWindow(self.window) + cv2.setMouseCallback(self.window, self._capture_event) + self._intercept_coords() + self._assign_mask_coords() + confirmed = self._confirm_coords() + cv2.destroyAllWindows() + if not confirmed: + return self.reset() + self._logger.info(f"Coordinates {self.mask_coords} accepted by user.") + return self.mask_coords + + def _assign_mask_coords(self) -> None: + """Assigns points from candidates to coordinates json.""" + self.mask_coords = { + 2: Pointer(*min(self.candidates, key=lambda x: x[0])), + 9: Pointer(*max(self.candidates, key=lambda x: x[1])), + 16: Pointer(*max(self.candidates, key=lambda x: x[0])), + 29: Pointer(*min(self.candidates, key=lambda x: x[1])) + } + + @staticmethod + def _confirm_coords() -> bool: + """Ask user to confirm inputted coords with empty string, y or yes.""" + response = input( + colored("Are points correctly assigned?: [Y/n]\n", "yellow") + ) + return response.lower() in {"y", "yes", ""} + + def reset(self) -> Optional[Dict[int, List[int]]]: + """Resets collector variables and collects coords once again.""" + self._logger.info("Mask coords collector has been reset.") + self.mask_coords = None + self.img = cv2.imread(self.mask_img_path) + self.candidates = [] + return self.collect() diff --git a/run.py b/run.py index 4f4d191..a60a097 100644 --- a/run.py +++ b/run.py @@ -1,15 +1,25 @@ from argparse import ArgumentParser, Namespace - -import cv2 -import numpy as np +from logging import Logger from mask_imposer.colored_logger import get_configured_logger from mask_imposer.definitions import ImageFormat, Output, Improvements, MaskSet +from mask_imposer.collector import CoordinatesCollector from mask_imposer.detector.landmark_detector import Detector from mask_imposer.imposer.mask_imposer import Imposer from mask_imposer.input_inspector import Inspector +def _create_mask_set(args, logger: Logger) -> MaskSet: + if args.mask_img: + if args.mask_coords: + return MaskSet(args.mask_img, args.mask_coords) + return MaskSet(args.mask_img, CoordinatesCollector(args.mask_img, logger).collect()) + return MaskSet( + f"mask_imposer/bundled/set_0{args.use_bundled_mask}/mask_image.png", + f"mask_imposer/bundled/set_0{args.use_bundled_mask}/mask_coords.json" + ) + + def _parse_args() -> Namespace: """Creates parser and parse command line arguments.""" parser = ArgumentParser() @@ -24,43 +34,31 @@ def _parse_args() -> Namespace: help="Draw circles on detected landmarks coords.") parser.add_argument("--detect-face-boxes", type=bool, default=False, help="Before landmark prediction detect face box.") - parser.add_argument("--mask-coords", type=str, default="mask_imposer/bundled/set_01/mask_coords.json", + + parser.add_argument("--mask-coords", type=str, default=None, + # "mask_imposer/bundled/set_01/mask_coords.json", help="Custom mask image path.") - parser.add_argument("--mask-img", type=str, default="mask_imposer/bundled/set_01/mask_image.png", + parser.add_argument("--mask-img", type=str, default=None, + # default="mask_imposer/bundled/set_01/mask_image.png", + help="Custom mask characteristic [2,9,16,29] landmarks coordinates json filepath.") + parser.add_argument("--use-bundled-mask", type=int, default=1, choices=[1, 2], + # "mask_imposer/bundled/set_01/mask_image.png", help="Custom mask characteristic [2,9,16,29] landmarks coordinates json filepath.") return parser.parse_args() def main(): logger = get_configured_logger() - logger.warning("test") # logger.propagate = False # logger.disabled = True args = _parse_args() improvements = Improvements(args.show_samples, args.draw_landmarks) - mask_set = MaskSet(args.mask_img, args.mask_coords) - - window = "Include Help" - img = cv2.imread(mask_set.img_path) - cv2.namedWindow(window) - - def capture_event(event, x, y, flags, params): - if event == cv2.EVENT_LBUTTONDBLCLK: - cv2.circle(img, (x, y), int(img.shape[0] / 40), (255, 0, 0), -1) - print(x, y) - cv2.setMouseCallback(window, capture_event) + mask_set = _create_mask_set(args, logger) - while True: - cv2.imshow(window, img) - if cv2.waitKey(1) == 13: - break - cv2.destroyAllWindows() + cc = CoordinatesCollector(args.mask_img, logger) + cc.collect() - # img = cv2.imread(args.mask_img) - # cv2.imshow("example", img) - # cv2.waitKey(0) - exit() output = Output(args.output_dir, args.output_format) inspector = Inspector(logger) From bbf1251123a02dc95386c6b1f34c08074b61043e Mon Sep 17 00:00:00 2001 From: sqoshi Date: Sat, 28 Aug 2021 18:47:35 +0200 Subject: [PATCH 3/4] mypy fix --- mask_imposer/collector/coords_collector.py | 6 +++--- run.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mask_imposer/collector/coords_collector.py b/mask_imposer/collector/coords_collector.py index 5ab5ae9..37cb645 100644 --- a/mask_imposer/collector/coords_collector.py +++ b/mask_imposer/collector/coords_collector.py @@ -15,7 +15,7 @@ def __init__(self, mask_img_path: str, logger: Logger) -> None: self.mask_img_path = mask_img_path self.img = cv2.imread(mask_img_path) self.candidates: List[List[int]] = [] - self.mask_coords: Optional[Dict[int, List[int]]] = None + self.mask_coords: Optional[Dict[int, Pointer]] = None def _capture_event( self, event: cv2.cuda_Event, x: int, y: int, @@ -40,7 +40,7 @@ def _intercept_coords(self) -> None: cv2.waitKey(1) break - def collect(self) -> Optional[Dict[int, List[int]]]: + def collect(self) -> Optional[Dict[int, Pointer]]: """Creates json with collected from clickable input coordinates.""" self._logger.info("Collecting mask coordinates interactively.") cv2.namedWindow(self.window) @@ -71,7 +71,7 @@ def _confirm_coords() -> bool: ) return response.lower() in {"y", "yes", ""} - def reset(self) -> Optional[Dict[int, List[int]]]: + def reset(self) -> Optional[Dict[int, Pointer]]: """Resets collector variables and collects coords once again.""" self._logger.info("Mask coords collector has been reset.") self.mask_coords = None diff --git a/run.py b/run.py index a60a097..0b34983 100644 --- a/run.py +++ b/run.py @@ -56,9 +56,6 @@ def main(): mask_set = _create_mask_set(args, logger) - cc = CoordinatesCollector(args.mask_img, logger) - cc.collect() - output = Output(args.output_dir, args.output_format) inspector = Inspector(logger) @@ -70,3 +67,5 @@ def main(): imposer = Imposer(detector.get_landmarks(), output, mask_set, improvements, logger) imposer.impose() + +# todo : check if mask inputted via clicks working and add readme From 26f36f6ff846800e530692532ba7fe44d1c18beb Mon Sep 17 00:00:00 2001 From: sqoshi Date: Mon, 30 Aug 2021 21:01:59 +0200 Subject: [PATCH 4/4] tests + version bump - input-able custom mask coords --- mask_imposer/collector/coords_collector.py | 2 +- readme.md | 59 ++++++++++++++-------- run.py | 9 +--- setup.py | 2 +- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/mask_imposer/collector/coords_collector.py b/mask_imposer/collector/coords_collector.py index 37cb645..563d4ae 100644 --- a/mask_imposer/collector/coords_collector.py +++ b/mask_imposer/collector/coords_collector.py @@ -51,7 +51,7 @@ def collect(self) -> Optional[Dict[int, Pointer]]: cv2.destroyAllWindows() if not confirmed: return self.reset() - self._logger.info(f"Coordinates {self.mask_coords} accepted by user.") + self._logger.info(f"Coordinates accepted by user.") return self.mask_coords def _assign_mask_coords(self) -> None: diff --git a/readme.md b/readme.md index 7233d93..e6937dc 100644 --- a/readme.md +++ b/readme.md @@ -24,8 +24,8 @@ Project is a part of series related with my Bachelor of Science Thesis research. Mask imposer is a tool for overlaying face mask over human central faces. -Application main purpose is to allow users to make fake datasets of people in face masks. Since there is no available -free dataset, application is useful in machine or deep learning in classification/ recognition problems. +Application main purpose is to make fake datasets of people in face masks. Since there is no available free dataset, +application is useful in machine or deep learning in classification/ recognition problems. ## General @@ -46,22 +46,20 @@ found [here]("http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2"). For now program requires only 4 of them: -- left - _2_ -- right - _16_ -- top - _29_ -- bottom - _9_ +- `left` - _2_ +- `right` - _16_ +- `top` - _29_ +- `bottom` - _9_ ### `Imposition` -As stayed in [previous paragraph](#important-landmarks) program use only few landmarks. - -After landmarks have been detected, program reading mask image with some hardcoded coordinates (X,Y) responding to all -indexes stated in [previous paragraph](#important-landmarks). +When landmarks have been detected, program is reading mask image with some hardcoded coordinates (X,Y) responding to all +indexes mentioned in [previous paragraph](#important-landmarks). ![mask.png](docs/.readme_media/points.png) -Red dots are points responding to left, right, top and bottom landmark defined -in [previous paragraph](#important-landmarks). +Red dots are points responding to *left*, *right*, *top* and *bottom* landmark defined +[here](#important-landmarks). In next state program computes distances between opposite dots in vertical and horizontal way. @@ -72,7 +70,7 @@ in target image. ![example_landmarks.png](docs/.readme_media/example_landmarks.png) -For now protruding part of the face mask is being cut and result is saved in output directory (default=`results`). +For now protruding part of the face mask is being cut and saved in output directory (default=`results`). ![example_res2.png](docs/.readme_media/example_res2.png) @@ -99,15 +97,20 @@ mim INPUT_DIR --option argument | --show-samples | ❌ | False | Show sample after detection. | | --draw-landmarks | ❌ | False | Draw circles on detected landmarks coords. | | --detect-face-boxes | ❌ | False | Before landmark prediction detect face box. | -| --mask-coords | ❌ | bundled_mask | Custom mask characteristic `[2,9,16,29]` landmarks coordinates json filepath. | -| --mask-img | ❌ | bundled_coords | Custom mask image filepath. | +| --use-bundled-mask | ❌ | 1 | Program offers some bundled (default) masks, choices (1,2) | +| --mask-coords | ❌ | None | Custom mask characteristic `[2,9,16,29]` landmarks coordinates json filepath. | +| --mask-img | ❌ | None | Custom mask image filepath. | ### Custom Mask -Mask maybe inputted via terminal by using two flags `--mask-img` and `--mask-coords`. +Mask maybe inputted via terminal by using flag `--mask-img`. Then mask image popup on the screen and program asks user +to interactively mark characteristic points on displayed image. + +If flag `--mask-img` would be combined with `--mask-coords` then characteristic points will be read from inputted +filepath. -Using custom mask requires inputting a path to mask image and a path to json -with [important landmarks](#important-landmarks). +Using custom mask requires inputting a _path to mask image_ or _path to mask image and a path to json +with [important landmarks](#important-landmarks)_. Image requirements: @@ -129,10 +132,22 @@ Example `mask_coords.json`: ```json { - "2": [ 15, 50], - "9": [185, 310], - "16": [365, 50], - "29": [185, 20] + "2": [ + 15, + 50 + ], + "9": [ + 185, + 310 + ], + "16": [ + 365, + 50 + ], + "29": [ + 185, + 20 + ] } ``` diff --git a/run.py b/run.py index 0b34983..0b0efa2 100644 --- a/run.py +++ b/run.py @@ -10,6 +10,7 @@ def _create_mask_set(args, logger: Logger) -> MaskSet: + """Determine importance of passed arguments in mask creation procedure.""" if args.mask_img: if args.mask_coords: return MaskSet(args.mask_img, args.mask_coords) @@ -34,7 +35,6 @@ def _parse_args() -> Namespace: help="Draw circles on detected landmarks coords.") parser.add_argument("--detect-face-boxes", type=bool, default=False, help="Before landmark prediction detect face box.") - parser.add_argument("--mask-coords", type=str, default=None, # "mask_imposer/bundled/set_01/mask_coords.json", help="Custom mask image path.") @@ -53,19 +53,12 @@ def main(): # logger.disabled = True args = _parse_args() improvements = Improvements(args.show_samples, args.draw_landmarks) - mask_set = _create_mask_set(args, logger) - output = Output(args.output_dir, args.output_format) - inspector = Inspector(logger) inspector.inspect(args.input_dir) - detector = Detector(inspector.get_images(), args.shape_predictor, args.detect_face_boxes, False, logger) detector.detect() # detector.save(args.output_dir, args.output_format) - imposer = Imposer(detector.get_landmarks(), output, mask_set, improvements, logger) imposer.impose() - -# todo : check if mask inputted via clicks working and add readme diff --git a/setup.py b/setup.py index 6f9ff45..cab4707 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="Mask Imposer", - version="1.1.0", + version="1.2.0", description="Tool to overlay fake face masks.", url="https://github.com/sqoshi/mask-imposer", author="Piotr Popis",