Skip to content

Commit

Permalink
Merge pull request #11 from sqoshi/coords-image-clicker
Browse files Browse the repository at this point in the history
mask points by clicks
  • Loading branch information
sqoshi authored Aug 30, 2021
2 parents 4043fae + 26f36f6 commit 38d9baf
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 36 deletions.
1 change: 1 addition & 0 deletions mask_imposer/collector/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .coords_collector import CoordinatesCollector
80 changes: 80 additions & 0 deletions mask_imposer/collector/coords_collector.py
Original file line number Diff line number Diff line change
@@ -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, Pointer]] = 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, Pointer]]:
"""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 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, Pointer]]:
"""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()
59 changes: 37 additions & 22 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand All @@ -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)

Expand All @@ -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:

Expand All @@ -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
]
}
```

Expand Down
35 changes: 22 additions & 13 deletions run.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
from argparse import ArgumentParser, Namespace

import cv2
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:
"""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)
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()
Expand All @@ -23,33 +35,30 @@ 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)
# img = cv2.imread(args.mask_img)
# cv2.imshow("example", img)
# cv2.waitKey(0)
# exit()
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()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 38d9baf

Please sign in to comment.