├── .gitignore
├── .pylintrc
├── .readthedocs.yaml
├── LICENSE
├── MANIFEST.in
├── README.md
├── TODO.md
├── config_zoo
├── lffd_original.yaml
└── lffd_slim.yaml
├── demo.py
├── doc_samples
├── export_to_onnx.py
├── export_to_torchscript.py
├── inference.py
├── testing.py
└── training.py
├── docs
├── Makefile
├── make.bat
└── source
│ ├── api
│ ├── base.rst
│ ├── dataset.rst
│ ├── index.rst
│ ├── loss.rst
│ ├── metric.rst
│ ├── module.rst
│ ├── transforms.rst
│ └── utils.rst
│ ├── concepts
│ ├── export.rst
│ ├── index.rst
│ ├── inference.rst
│ ├── testing.rst
│ └── training.rst
│ ├── conf.py
│ ├── index.rst
│ └── quickstart
│ ├── architectures.md
│ ├── index.rst
│ ├── installation.md
│ └── pretrained_models.md
├── fastface
├── __init__.py
├── adapter
│ ├── __init__.py
│ ├── extract_handler.py
│ ├── gdrive.py
│ └── http.py
├── api
│ └── __init__.py
├── arch
│ ├── __init__.py
│ └── lffd
│ │ ├── __init__.py
│ │ ├── blocks
│ │ ├── __init__.py
│ │ ├── anchor.py
│ │ ├── backbone_v1.py
│ │ ├── backbone_v2.py
│ │ ├── conv.py
│ │ ├── head.py
│ │ └── resblock.py
│ │ └── module.py
├── dataset
│ ├── __init__.py
│ ├── base.py
│ ├── fddb.py
│ └── widerface.py
├── loss
│ ├── __init__.py
│ ├── focal_loss.py
│ └── iou_loss.py
├── metric
│ ├── __init__.py
│ ├── ap.py
│ ├── ar.py
│ ├── functional
│ │ ├── __init__.py
│ │ └── ap.py
│ ├── utils.py
│ └── widerface_ap.py
├── module.py
├── registry.yaml
├── transforms
│ ├── __init__.py
│ ├── augmentation
│ │ ├── __init__.py
│ │ ├── blur.py
│ │ ├── color_jitter.py
│ │ ├── lffd_random_sample.py
│ │ ├── random_horizontal_flip.py
│ │ └── random_rotate.py
│ ├── compose.py
│ ├── discard.py
│ ├── functional
│ │ ├── __init__.py
│ │ ├── color_jitter.py
│ │ ├── interpolate.py
│ │ ├── pad.py
│ │ └── rotate.py
│ ├── interpolate.py
│ ├── normalize.py
│ ├── pad.py
│ └── rotate.py
├── utils
│ ├── __init__.py
│ ├── box.py
│ ├── cache.py
│ ├── cluster.py
│ ├── config.py
│ ├── data.py
│ ├── geo.py
│ ├── kernel.py
│ ├── preprocess.py
│ ├── random.py
│ └── vis.py
└── version.py
├── requirements.txt
├── resources
├── friends.jpg
├── friends2.jpg
├── tutorial_1_0.png
├── tutorial_1_1.png
└── tutorial_1_2.png
├── setup.cfg
├── setup.py
├── tests
├── __init__.py
├── data
│ ├── multi_face.jpeg
│ ├── no_face.jpg
│ └── single_face.jpg
├── test_base_apis.py
├── test_dataset_apis.py
├── test_loss_apis.py
├── test_metric_apis.py
├── test_module_apis.py
├── test_transforms_apis.py
├── test_utility_apis.py
└── utils.py
├── tox.ini
└── tutorials
├── bentoml_deployment
├── README.md
├── build.py
├── service.py
└── test.py
└── widerface_benchmark
├── README.md
└── test_widerface.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # dev
2 | .vscode
3 | .idea
4 | backup
5 |
6 | # cache
7 | *.pyc
8 | __pycache__
9 |
10 | # logs
11 | lightning_logs/
12 |
13 | # weights
14 | *.pt
15 | *.pth
16 | *.ckpt
17 |
18 | # package
19 | build/
20 | dist/
21 | fastface.egg-info/
22 |
23 | # docs
24 | docs/source/_build/
25 |
26 | # tests & reports
27 | .coverage
28 | .tox/
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [TYPECHECK]
2 |
3 | # List of members which are set dynamically and missed by Pylint inference
4 | # system, and so shouldn't trigger E1101 when accessed.
5 | generated-members=numpy.*, torch.*
6 |
7 | [FORMAT]
8 | indent-string=\t
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yaml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | # Build documentation in the docs/ directory with Sphinx
9 | sphinx:
10 | configuration: docs/source/conf.py
11 |
12 | # Optionally set the version of Python and requirements required to build your docs
13 | python:
14 | version: 3.8
15 | install:
16 | - requirements: requirements.txt
17 | - method: pip
18 | path: .
19 | extra_requirements:
20 | - docs
21 | system_packages: true
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Ömer BORHAN
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include fastface/registry.yaml
2 | include requirements.txt
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FastFace: Lightweight Face Detection Framework
2 |
3 | 
4 | [](https://fastface.readthedocs.io/en/latest/?badge=latest)
5 | [](https://pepy.tech/project/fastface)
6 | 
7 | 
8 |
9 | **Easy-to-use face detection framework, developed using [pytorch-lightning](https://www.pytorchlightning.ai/).**
10 | **Checkout [documentation](https://fastface.readthedocs.io/en/latest/) for more.**
11 |
12 | ## Key Features
13 |
14 | - :fire: **Use pretrained models for inference with just few lines of code**
15 | - :chart_with_upwards_trend: **Evaluate models on different datasets**
16 | - :hammer_and_wrench: **Train and prototype new models, using pre-defined architectures**
17 | - :rocket: **Export trained models with ease, to use in production**
18 |
19 | ## Contents
20 |
21 | - [Installation](#installation)
22 | - [Pretrained Models](#pretrained-models)
23 | - [Demo](#demo)
24 | - [Benchmarks](#benchmarks)
25 | - [Tutorials](#tutorials)
26 | - [References](#references)
27 | - [Citations](#citations)
28 |
29 | ## Installation
30 |
31 | From PyPI
32 |
33 | ```
34 | pip install fastface -U
35 | ```
36 |
37 | From source
38 |
39 | ```
40 | git clone https://github.com/borhanMorphy/fastface.git
41 | cd fastface
42 | pip install .
43 | ```
44 |
45 | ## Pretrained Models
46 |
47 | Pretrained models can be accessable via `fastface.FaceDetector.from_pretrained()`
48 |
49 | | Name | Architecture | Configuration | Parameters | Model Size | Link |
50 | | :---------------: | :----------: | :-----------: | :--------: | :--------: | :-------------------------------------------------------------------------------------------: |
51 | | **lffd_original** | lffd | original | 2.3M | 9mb | [weights](https://drive.google.com/file/d/1qFRuGhzoMWrW9WNlWw9jHXPY51MBssQD/view?usp=sharing) |
52 | | **lffd_slim** | lffd | slim | 1.5M | 6mb | [weights](https://drive.google.com/file/d/1UOHllYp5NY4mV7lHmq0c9xsryRIufpAQ/view?usp=sharing) |
53 |
54 | ## Demo
55 |
56 | Using package
57 |
58 | ```python
59 | import fastface as ff
60 | import imageio
61 | from pytorch_lightning.utilities.model_summary import ModelSummary
62 |
63 | # load image as RGB
64 | img = imageio.imread("")[:,:,:3]
65 |
66 | # build model with pretrained weights
67 | model = ff.FaceDetector.from_pretrained("lffd_original")
68 | # model: pl.LightningModule
69 |
70 | # get model summary
71 | ModelSummary(model, max_depth=1)
72 |
73 | # set model to eval mode
74 | model.eval()
75 |
76 | # [optional] move model to gpu
77 | model.to("cuda")
78 |
79 | # model inference
80 | preds, = model.predict(img, det_threshold=.8, iou_threshold=.4)
81 | # preds: {
82 | # 'boxes': [[xmin, ymin, xmax, ymax], ...],
83 | # 'scores':[, ...]
84 | # }
85 |
86 | ```
87 |
88 | Using [demo.py](/demo.py) script
89 |
90 | ```
91 | python demo.py --model lffd_original --device cuda --input
92 | ```
93 |
94 | sample output;
95 | 
96 |
97 | ## Benchmarks
98 |
99 | **Following results are obtained with this repository**
100 |
101 | #### WIDER FACE
102 |
103 | validation set results
104 |
105 | | Name | Easy | Medium | Hard |
106 | | :---------------: | :-------: | :-------: | :-------: |
107 | | **lffd_original** | **0.893** | **0.866** | **0.758** |
108 | | **lffd_slim** | **0.866** | **0.854** | **0.742** |
109 |
110 | ## Tutorials
111 |
112 | - [Widerface Benchmark](./tutorials/widerface_benchmark/README.md)
113 | - [BentoML Deployment](./tutorials/bentoml_deployment/README.md)
114 |
115 | ## References
116 |
117 | - [LFFD Paper](https://arxiv.org/pdf/1904.10633.pdf)
118 | - [Official LFFD Implementation](https://github.com/YonghaoHe/A-Light-and-Fast-Face-Detector-for-Edge-Devices)
119 |
120 | ## Citations
121 |
122 | ```bibtex
123 | @inproceedings{LFFD,
124 | title={LFFD: A Light and Fast Face Detector for Edge Devices},
125 | author={He, Yonghao and Xu, Dezhong and Wu, Lifang and Jian, Meng and Xiang, Shiming and Pan, Chunhong},
126 | booktitle={arXiv:1904.10633},
127 | year={2019}
128 | }
129 | ```
130 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | ## Bugs
2 | - [x] fix performance drop for tutorials/widerface_benchmark
3 | - [ ] average recall seems to be bugged, check it out
4 |
5 | ## Hot
6 | - [x] fill `Inference` section under the `CORE CONCEPTS`
7 | - [x] fill `Testing` section under the `CORE CONCEPTS`
8 | - [x] fill `Training` section under the `CORE CONCEPTS`
9 | - [x] fill `Export` section under the `CORE CONCEPTS`
10 | - [x] make sure tutorials works
11 | - [ ] add ci/cd for unittest, linting test and doc test
12 | - [ ] add coverage badge
13 | - [ ] add ci/cd pipeline badge
14 | - [ ] add doctest badge
15 | - [ ] test on windows 10
16 | - [x] add FDDB dataset
17 | - [ ] add UFDD dataset
18 |
19 | ## Near Future
20 | - [ ] add `ADVENCED GUIDE` to the docs
21 | - [ ] increase coverage of pydoc and doc-test for apis
22 | - [ ] add ONNX & torchscipt usecase deployment notebooks
23 |
24 | ## Future
25 | - [ ] extend Architecture docs
26 | - [ ] extend pytest.ini configuration
27 | - [ ] support CoreML
28 | - [ ] add CoreML deployment tutorial
29 |
30 | ## Maybe
31 | - [ ] support TFLite
32 |
--------------------------------------------------------------------------------
/config_zoo/lffd_original.yaml:
--------------------------------------------------------------------------------
1 | arch: "lffd"
2 | config: "slim"
3 |
4 | hparams:
5 | learning_rate: 0.1
6 | momentum: 0.9
7 | weight_decay: 0.00001
8 | milestones: [500000, 1000000, 1500000]
9 | gamma: 0.1
10 | ratio: 10
11 |
12 | preprocess:
13 | normalized_input: false
14 | mean: 127.5
15 | std: 127.5
--------------------------------------------------------------------------------
/config_zoo/lffd_slim.yaml:
--------------------------------------------------------------------------------
1 | arch: "lffd"
2 | config: "original"
3 |
4 | hparams:
5 | learning_rate: 0.1
6 | momentum: 0.9
7 | weight_decay: 0.00001
8 | milestones: [500000, 1000000, 1500000]
9 | gamma: 0.1
10 | ratio: 10
11 |
12 | preprocess:
13 | normalized_input: false
14 | mean: 127.5
15 | std: 127.5
--------------------------------------------------------------------------------
/demo.py:
--------------------------------------------------------------------------------
1 | import argparse
2 |
3 | import imageio
4 | import numpy as np
5 | import torch
6 | from pytorch_lightning.utilities.model_summary import ModelSummary
7 |
8 | import fastface as ff
9 |
10 |
11 | def get_arguments():
12 | ap = argparse.ArgumentParser()
13 |
14 | ap.add_argument(
15 | "--model",
16 | "-m",
17 | type=str,
18 | default="lffd_original",
19 | help=f"pretrained models {','.join(ff.list_pretrained_models())} or checkpoint path",
20 | )
21 |
22 | ap.add_argument(
23 | "--device",
24 | "-d",
25 | type=str,
26 | choices=["cpu", "cuda"],
27 | default="cuda" if torch.cuda.is_available() else "cpu",
28 | )
29 |
30 | ap.add_argument("--input", "-i", type=str, required=True, help="image file path")
31 |
32 | ap.add_argument(
33 | "--det-threshold",
34 | "-dt",
35 | type=float,
36 | default=0.7,
37 | help="detection score threshold",
38 | )
39 |
40 | ap.add_argument(
41 | "--iou-threshold", "-it", type=float, default=0.4, help="iou score threshold"
42 | )
43 |
44 | ap.add_argument(
45 | "--target-size",
46 | "-t",
47 | type=int,
48 | default=None,
49 | help="interpolates all inputs to given target size",
50 | )
51 |
52 | return ap.parse_args()
53 |
54 |
55 | def load_image(img_path: str) -> np.ndarray:
56 | """loads rgb image using given file path
57 |
58 | Args:
59 | img_path (str): image file path to load
60 |
61 | Returns:
62 | np.ndarray: rgb image as np.ndarray
63 | """
64 | img = imageio.imread(img_path)
65 | if not img.flags["C_CONTIGUOUS"]:
66 | # if img is not contiguous than fix it
67 | img = np.ascontiguousarray(img, dtype=img.dtype)
68 |
69 | if img.shape[2] == 4:
70 | # found RGBA
71 | img = img[:, :, :3]
72 |
73 | return img
74 |
75 |
76 | def main(
77 | model: str,
78 | device: str,
79 | img_path: str,
80 | det_threshold: float,
81 | iou_threshold: float,
82 | target_size: int,
83 | ):
84 | # load image
85 | img = load_image(img_path)
86 |
87 | # get pretrained model
88 | model = ff.FaceDetector.from_pretrained(model)
89 |
90 | # get model summary
91 | max_depth = 1 # 1: top-level summary, -1: print all levels
92 | print(ModelSummary(model, max_depth=max_depth))
93 |
94 | # set model to eval mode
95 | model.eval()
96 |
97 | # move model to given device
98 | model.to(device)
99 |
100 | # model feed forward
101 | (preds,) = model.predict(
102 | img,
103 | det_threshold=det_threshold,
104 | iou_threshold=iou_threshold,
105 | target_size=target_size,
106 | )
107 |
108 | # visualize predictions
109 | pretty_img = ff.utils.vis.render_predictions(img, preds)
110 |
111 | # show image
112 | pretty_img.show()
113 |
114 |
115 | if __name__ == "__main__":
116 | # python demo.py -m lffd_original -d cuda -t 640 -i
117 | args = get_arguments()
118 | main(
119 | args.model,
120 | args.device,
121 | args.input,
122 | args.det_threshold,
123 | args.iou_threshold,
124 | args.target_size,
125 | )
126 |
--------------------------------------------------------------------------------
/doc_samples/export_to_onnx.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 | import fastface as ff
4 |
5 | # checkout available pretrained models
6 | print(ff.list_pretrained_models())
7 | # ["lffd_slim", "lffd_original"]
8 |
9 | pretrained_model_name = "lffd_slim"
10 |
11 | # build pl.LightningModule using pretrained weights
12 | model = ff.FaceDetector.from_pretrained(pretrained_model_name)
13 |
14 | # onnx export configs
15 | opset_version = 11
16 |
17 | dynamic_axes = {
18 | "input_data": {0: "batch", 2: "height", 3: "width"}, # write axis names
19 | "preds": {0: "batch"},
20 | }
21 |
22 | input_names = ["input_data"]
23 |
24 | output_names = ["preds"]
25 |
26 | # define dummy sample
27 | input_sample = torch.rand(1, *model.arch.input_shape[1:])
28 |
29 | # export model as onnx
30 | model.to_onnx(
31 | "{}.onnx".format(pretrained_model_name),
32 | input_sample=input_sample,
33 | opset_version=opset_version,
34 | input_names=input_names,
35 | output_names=output_names,
36 | dynamic_axes=dynamic_axes,
37 | export_params=True,
38 | )
39 |
--------------------------------------------------------------------------------
/doc_samples/export_to_torchscript.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 | import fastface as ff
4 |
5 | # checkout available pretrained models
6 | print(ff.list_pretrained_models())
7 | # ["lffd_slim", "lffd_original"]
8 |
9 | pretrained_model_name = "lffd_slim"
10 |
11 | # build pl.LightningModule using pretrained weights
12 | model = ff.FaceDetector.from_pretrained(pretrained_model_name)
13 |
14 | model.eval()
15 |
16 | sc_model = model.to_torchscript()
17 |
18 | torch.jit.save(sc_model, "{}.ts".format(pretrained_model_name))
19 |
--------------------------------------------------------------------------------
/doc_samples/inference.py:
--------------------------------------------------------------------------------
1 | import imageio
2 |
3 | import fastface as ff
4 |
5 | # checkout available pretrained models
6 | print(ff.list_pretrained_models())
7 | # ["lffd_slim", "lffd_original"]
8 |
9 | # build pl.LightningModule using pretrained weights
10 | model = ff.FaceDetector.from_pretrained("lffd_slim")
11 |
12 | # set model to eval mode
13 | model.eval()
14 |
15 | # load image
16 | img = imageio.imread("")[:, :, :3]
17 |
18 | # find faces
19 | (preds,) = model.predict(img)
20 | """preds
21 | {
22 | 'boxes': [[xmin, ymin, xmax, ymax], ...],
23 | 'scores':[, ...]
24 | }
25 | """
26 |
27 | # visualize predictions
28 | pil_img = ff.utils.vis.render_predictions(img, preds)
29 | pil_img.show()
30 |
--------------------------------------------------------------------------------
/doc_samples/testing.py:
--------------------------------------------------------------------------------
1 | import pytorch_lightning as pl
2 | import torch
3 |
4 | import fastface as ff
5 |
6 | # checkout available pretrained models
7 | print(ff.list_pretrained_models())
8 | # ["lffd_slim", "lffd_original"]
9 |
10 | # build pl.LightningModule using pretrained weights
11 | model = ff.FaceDetector.from_pretrained("lffd_slim")
12 |
13 | # set model to eval mode
14 | model.eval()
15 |
16 | # build transforms
17 | transforms = ff.transforms.Compose(
18 | ff.transforms.Interpolate(target_size=480),
19 | ff.transforms.Padding(target_size=(480, 480)),
20 | )
21 |
22 | # build torch.utils.data.Dataset
23 | ds = ff.dataset.FDDBDataset(phase="test", transforms=transforms)
24 |
25 | # build torch.utils.data.DataLoader
26 | dl = ds.get_dataloader(batch_size=1, num_workers=0)
27 |
28 | # add average precision pl.metrics.Metric to the model
29 | model.add_metric("average_precision", ff.metric.AveragePrecision(iou_threshold=0.5))
30 |
31 | # define pl.Trainer for testing
32 | trainer = pl.Trainer(
33 | benchmark=True,
34 | logger=False,
35 | checkpoint_callback=False,
36 | gpus=1 if torch.cuda.is_available() else 0,
37 | precision=32,
38 | )
39 |
40 | # run test
41 | trainer.test(model, test_dataloaders=[dl])
42 | """
43 | DATALOADER:0 TEST RESULTS
44 | {'average_precision': 0.9459084272384644}
45 | """
46 |
--------------------------------------------------------------------------------
/doc_samples/training.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytorch_lightning as pl
4 | import torch
5 |
6 | import fastface as ff
7 |
8 | # set seed
9 | pl.seed_everything(41)
10 |
11 | # build training transforms
12 | train_transforms = ff.transforms.Compose(
13 | ff.transforms.Interpolate(target_size=480),
14 | ff.transforms.Padding(target_size=(480, 480)),
15 | ff.transforms.RandomHorizontalFlip(p=0.5),
16 | )
17 |
18 | # build val transforms
19 | val_transforms = ff.transforms.Compose(
20 | ff.transforms.Interpolate(target_size=480),
21 | ff.transforms.Padding(target_size=(480, 480)),
22 | )
23 |
24 | # build torch.utils.data.DataLoader for training
25 | train_dl = ff.dataset.FDDBDataset(
26 | phase="train", transforms=train_transforms
27 | ).get_dataloader(batch_size=8, shuffle=True, num_workers=8)
28 |
29 | # build torch.utils.data.DataLoader for validation
30 | val_dl = ff.dataset.FDDBDataset(phase="val", transforms=val_transforms).get_dataloader(
31 | batch_size=8, shuffle=False, num_workers=8
32 | )
33 |
34 | # define preprocess dict
35 | preprocess = {"mean": 127.5, "std": 127.5, "normalized_input": False}
36 |
37 | # define hyper parameter dict
38 | hparams = {
39 | "learning_rate": 0.1,
40 | "momentum": 0.9,
41 | "weight_decay": 0.00001,
42 | "milestones": [500000, 1000000, 1500000],
43 | "gamma": 0.1,
44 | "ratio": 10,
45 | }
46 |
47 | # checkout available architectures to train
48 | print(ff.list_archs())
49 | # ["lffd"]
50 | arch = "lffd"
51 |
52 | # checkout available configs for the architecture
53 | print(ff.list_arch_configs(arch))
54 | # ["original", "slim"]
55 | config = "slim"
56 |
57 | # build pl.LightningModule with random weights
58 | model = ff.FaceDetector.build(
59 | arch, config=config, preprocess=preprocess, hparams=hparams
60 | )
61 |
62 | # add average precision pl.metrics.Metric to the model
63 | model.add_metric("average_precision", ff.metric.AveragePrecision(iou_threshold=0.5))
64 |
65 | model_save_name = "{}_{}_{}_best".format(arch, config, "fddb")
66 | ckpt_save_path = "./checkpoints"
67 |
68 | # resume with checkpoint, if exists
69 | ckpt_resume_path = os.path.join(ckpt_save_path, model_save_name + ".ckpt")
70 | if not os.path.isfile(ckpt_resume_path):
71 | ckpt_resume_path = None
72 |
73 | # define checkpoint callback
74 | checkpoint_callback = pl.callbacks.ModelCheckpoint(
75 | dirpath=ckpt_save_path,
76 | verbose=True,
77 | filename=model_save_name,
78 | monitor="average_precision",
79 | save_top_k=1,
80 | mode="max", # only pick max of `average_precision`
81 | )
82 |
83 | # define pl.Trainer
84 | trainer = pl.Trainer(
85 | default_root_dir=".",
86 | accumulate_grad_batches=4, # update weights every 4 batches
87 | callbacks=[checkpoint_callback],
88 | gpus=1 if torch.cuda.is_available() else 0,
89 | precision=32,
90 | resume_from_checkpoint=ckpt_resume_path,
91 | max_epochs=100,
92 | check_val_every_n_epoch=2, # run validation every 2 epochs
93 | gradient_clip_val=10, # clip gradients
94 | )
95 |
96 | # start training
97 | trainer.fit(model, train_dataloader=train_dl, val_dataloaders=[val_dl])
98 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/source/api/base.rst:
--------------------------------------------------------------------------------
1 | fastface
2 | ++++++++
3 |
4 | .. autofunction:: fastface.list_pretrained_models
5 |
6 | .. autofunction:: fastface.download_pretrained_model
7 |
8 | .. autofunction:: fastface.list_archs
9 |
10 | .. autofunction:: fastface.list_arch_configs
11 |
12 | .. autofunction:: fastface.get_arch_config
--------------------------------------------------------------------------------
/docs/source/api/dataset.rst:
--------------------------------------------------------------------------------
1 | fastface.dataset
2 | ++++++++++++++++
3 |
4 | .. autoclass:: fastface.dataset.WiderFaceDataset
5 |
6 | .. autoclass:: fastface.dataset.FDDBDataset
7 |
8 | .. autoclass:: fastface.dataset.UFDDDataset
--------------------------------------------------------------------------------
/docs/source/api/index.rst:
--------------------------------------------------------------------------------
1 | .. toctree::
2 | :maxdepth: 2
3 |
4 | base.rst
5 | module.rst
6 | metric.rst
7 | loss.rst
8 | transforms.rst
9 | dataset.rst
10 | utils.rst
--------------------------------------------------------------------------------
/docs/source/api/loss.rst:
--------------------------------------------------------------------------------
1 | fastface.loss
2 | +++++++++++++
3 |
4 | .. autoclass:: fastface.loss.BinaryFocalLoss
5 |
6 | .. autoclass:: fastface.loss.DIoULoss
--------------------------------------------------------------------------------
/docs/source/api/metric.rst:
--------------------------------------------------------------------------------
1 | fastface.metric
2 | +++++++++++++++
3 |
4 | .. autoclass:: fastface.metric.WiderFaceAP
5 |
6 | .. autoclass:: fastface.metric.AveragePrecision
7 |
8 | .. autoclass:: fastface.metric.AverageRecall
--------------------------------------------------------------------------------
/docs/source/api/module.rst:
--------------------------------------------------------------------------------
1 | fastface.FaceDetector
2 | +++++++++++++++++++++
3 |
4 | .. automethod:: fastface.FaceDetector.build
5 |
6 | .. automethod:: fastface.FaceDetector.build_from_yaml
7 |
8 | .. automethod:: fastface.FaceDetector.from_checkpoint
9 |
10 | .. automethod:: fastface.FaceDetector.from_pretrained
11 |
12 | .. automethod:: fastface.FaceDetector.predict
13 |
14 | .. automethod:: fastface.FaceDetector.add_metric
15 |
16 | .. automethod:: fastface.FaceDetector.get_metrics
--------------------------------------------------------------------------------
/docs/source/api/transforms.rst:
--------------------------------------------------------------------------------
1 | fastface.transforms
2 | +++++++++++++++++++
3 |
4 | .. autoclass:: fastface.transforms.Interpolate
5 | .. autoclass:: fastface.transforms.ConditionalInterpolate
6 | .. autoclass:: fastface.transforms.Padding
7 | .. autoclass:: fastface.transforms.FaceDiscarder
8 | .. autoclass:: fastface.transforms.Rotate
9 | .. autoclass:: fastface.transforms.RandomGaussianBlur
10 | .. autoclass:: fastface.transforms.ColorJitter
11 | .. autoclass:: fastface.transforms.LFFDRandomSample
12 | .. autoclass:: fastface.transforms.RandomRotate
13 | .. autoclass:: fastface.transforms.RandomHorizontalFlip
14 | .. autoclass:: fastface.transforms.Compose
15 |
--------------------------------------------------------------------------------
/docs/source/api/utils.rst:
--------------------------------------------------------------------------------
1 | fastface.utils
2 | ++++++++++++++
3 |
4 | fastface.utils.box
5 | ##################
6 |
7 | .. autofunction:: fastface.utils.box.jaccard_vectorized
8 | .. autofunction:: fastface.utils.box.intersect
9 | .. autofunction:: fastface.utils.box.cxcywh2xyxy
10 | .. autofunction:: fastface.utils.box.xyxy2cxcywh
11 | .. autofunction:: fastface.utils.box.batched_nms
12 |
13 | fastface.utils.preprocess
14 | #########################
15 |
16 | .. autofunction:: fastface.utils.preprocess.prepare_batch
17 | .. autofunction:: fastface.utils.preprocess.adjust_results
18 |
19 | fastface.utils.vis
20 | ########################
21 |
22 | .. autofunction:: fastface.utils.vis.render_predictions
--------------------------------------------------------------------------------
/docs/source/concepts/export.rst:
--------------------------------------------------------------------------------
1 | Export
2 | ======
3 |
4 |
5 | To Onnx
6 | ++++++++++++++
7 |
8 | .. literalinclude:: ../../../doc_samples/export_to_onnx.py
9 | :language: python
10 |
11 | To TorchScript
12 | +++++++++++++++++++++
13 |
14 | .. literalinclude:: ../../../doc_samples/export_to_torchscript.py
15 | :language: python
--------------------------------------------------------------------------------
/docs/source/concepts/index.rst:
--------------------------------------------------------------------------------
1 | .. toctree::
2 | :maxdepth: 2
3 |
4 | inference.rst
5 | testing.rst
6 | training.rst
7 | export.rst
--------------------------------------------------------------------------------
/docs/source/concepts/inference.rst:
--------------------------------------------------------------------------------
1 | Inference
2 | =========
3 |
4 | .. literalinclude:: ../../../doc_samples/inference.py
5 | :language: python
6 |
--------------------------------------------------------------------------------
/docs/source/concepts/testing.rst:
--------------------------------------------------------------------------------
1 | Testing
2 | =======
3 |
4 | .. literalinclude:: ../../../doc_samples/testing.py
5 | :language: python
--------------------------------------------------------------------------------
/docs/source/concepts/training.rst:
--------------------------------------------------------------------------------
1 | Training
2 | ========
3 |
4 | .. literalinclude:: ../../../doc_samples/training.py
5 | :language: python
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | import os
14 | import sys
15 |
16 | sys.path.insert(0, os.path.abspath("../.."))
17 | from recommonmark.parser import CommonMarkParser
18 |
19 | import fastface
20 |
21 | # -- Project information -----------------------------------------------------
22 |
23 | project = "fastface"
24 | copyright = "2021, Ömer BORHAN"
25 | author = "Ömer BORHAN"
26 |
27 | # The full version, including alpha/beta/rc tags
28 | release = fastface.__version__
29 |
30 |
31 | # -- General configuration ---------------------------------------------------
32 |
33 | # Add any Sphinx extension module names here, as strings. They can be
34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
35 | # ones.
36 | extensions = [
37 | "sphinx.ext.autodoc",
38 | "sphinxcontrib.napoleon",
39 | "sphinxemoji.sphinxemoji",
40 | "recommonmark",
41 | "sphinx_markdown_tables",
42 | ]
43 |
44 | # Add any paths that contain templates here, relative to this directory.
45 | templates_path = ["_templates"]
46 |
47 | source_parsers = {
48 | ".md": "recommonmark.parser.CommonMarkParser",
49 | }
50 |
51 | # The suffix(es) of source filenames.
52 | # You can specify multiple suffix as a list of string:
53 | #
54 | source_suffix = {
55 | ".rst": "restructuredtext",
56 | ".txt": "markdown",
57 | ".md": "markdown",
58 | }
59 |
60 | # List of patterns, relative to source directory, that match files and
61 | # directories to ignore when looking for source files.
62 | # This pattern also affects html_static_path and html_extra_path.
63 | exclude_patterns = []
64 |
65 |
66 | # -- Options for HTML output -------------------------------------------------
67 |
68 | # The theme to use for HTML and HTML Help pages. See the documentation for
69 | # a list of builtin themes.
70 | #
71 | html_theme = "sphinx_rtd_theme"
72 |
73 | # Add any paths that contain custom static files (such as style sheets) here,
74 | # relative to this directory. They are copied after the builtin static files,
75 | # so a file named "default.css" will overwrite the builtin "default.css".
76 | html_static_path = ["_static"]
77 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. fastface documentation master file, created by
2 | sphinx-quickstart on Sat Feb 13 21:26:18 2021.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 |
7 | ====================================
8 | |:zap:| FastFace Documentation
9 | ====================================
10 | **Version**: |release|
11 |
12 | **Easy-to-use face detection framework, developed using** `pytorch-lightning `_
13 |
14 | * |:fire:| **Use pretrained models for inference with just few lines of code**
15 | * |:chart_with_upwards_trend:| **Evaluate models on different datasets**
16 | * |:hammer_and_wrench:| **Train and prototype new models, using pre-defined architectures**
17 | * |:rocket:| **Export trained models with ease, to use in production**
18 |
19 | .. toctree::
20 | :maxdepth: 2
21 | :caption: Getting Started
22 |
23 | quickstart/index.rst
24 |
25 | .. toctree::
26 | :maxdepth: 2
27 | :caption: Core Concepts
28 |
29 | concepts/index.rst
30 |
31 | .. toctree::
32 | :maxdepth: 2
33 | :caption: API Reference
34 |
35 | api/index.rst
--------------------------------------------------------------------------------
/docs/source/quickstart/architectures.md:
--------------------------------------------------------------------------------
1 | # Architectures
2 |
3 | ## LFFD: A Light and Fast Face Detector for Edge Devices
4 |
5 | **Paper: [link](https://arxiv.org/pdf/1904.10633.pdf)**
6 | **Year: 2019**
7 |
8 | Configuration|Max. Receptive Field|Parameters|Model Size|
9 | :------:|:------:|:--------:|:--------:
10 | **original**|560|2.3M|8.8mb
11 | **slim**|320|1.5M|5.9mb
--------------------------------------------------------------------------------
/docs/source/quickstart/index.rst:
--------------------------------------------------------------------------------
1 | .. toctree::
2 | :maxdepth: 2
3 |
4 | installation.md
5 | architectures.md
6 | pretrained_models.md
--------------------------------------------------------------------------------
/docs/source/quickstart/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | ## Latest Version
4 | **Install from PyPI**
5 | ```
6 | pip install fastface -U
7 | ```
8 |
9 | **Install from source**
10 | ```
11 | git clone https://github.com/borhanMorphy/light-face-detection.git
12 | cd light-face-detection
13 | pip install .
14 | ```
15 |
16 | ## Specific Version
17 | **Install from PyPI**
18 | ```
19 | pip install fastface==x.x.x
20 | ```
21 |
22 | **Install from source**
23 | ```
24 | git clone https://github.com/borhanMorphy/light-face-detection.git
25 | cd light-face-detection
26 | git checkout vx.x.x
27 | pip install .
28 | ```
--------------------------------------------------------------------------------
/docs/source/quickstart/pretrained_models.md:
--------------------------------------------------------------------------------
1 | # Pretrained Models
2 | **fastface** offers pretrained models and can be easly accessable **without manually downloading weights**.
3 |
4 | ## Model Zoo
5 |
6 | Name|Architecture|Configuration|Parameters|Model Size|Link
7 | :------:|:------:|:------:|:------:|:------:|:------:
8 | **lffd_original**|lffd|original|2.3M|9mb|[weights](https://drive.google.com/file/d/1qFRuGhzoMWrW9WNlWw9jHXPY51MBssQD/view?usp=sharing)
9 | **lffd_slim**|lffd|slim|1.5M|6mb|[weights](https://drive.google.com/file/d/1UOHllYp5NY4mV7lHmq0c9xsryRIufpAQ/view?usp=sharing)
10 |
11 | ## Usage
12 | To get any of pretrained models as `pl.LightningModule`
13 | ```python
14 | import fastface as ff
15 | model = ff.FaceDetector.from_pretrained("")
16 | ```
17 | If you don't have pretrained model weights, **fastface** will automatically download and put it under `$HOME/.cache/fastface//model/`
--------------------------------------------------------------------------------
/fastface/__init__.py:
--------------------------------------------------------------------------------
1 | from . import adapter, dataset, loss, metric, transforms, utils
2 | from .api import (
3 | download_pretrained_model,
4 | get_arch_config,
5 | list_arch_configs,
6 | list_archs,
7 | list_pretrained_models,
8 | )
9 | from .module import FaceDetector
10 | from .version import __version__
11 |
12 | __all__ = [
13 | "list_pretrained_models",
14 | "download_pretrained_model",
15 | "list_archs",
16 | "list_arch_configs",
17 | "get_arch_config",
18 | "adapter",
19 | "dataset",
20 | "loss",
21 | "metric",
22 | "transforms",
23 | "utils",
24 | "FaceDetector",
25 | "__version__",
26 | ]
27 |
--------------------------------------------------------------------------------
/fastface/adapter/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from .gdrive import GoogleDriveAdapter
4 | from .http import HttpAdapter
5 |
6 | logger = logging.getLogger("fastface.adapter")
7 |
8 | __adapters__ = {"gdrive": GoogleDriveAdapter, "http": HttpAdapter}
9 |
10 |
11 | def download_object(adapter: str, dest_path: str = None, **kwargs):
12 | assert adapter in __adapters__.keys(), "given adapter {} is not defined".format(
13 | adapter
14 | )
15 | logger.info("Downloading object to {} with {} adapter".format(dest_path, adapter))
16 | return __adapters__[adapter].download(dest_path, **kwargs)
17 |
18 |
19 | __all__ = ["download_object"]
20 |
--------------------------------------------------------------------------------
/fastface/adapter/extract_handler.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import tarfile
4 | import zipfile
5 |
6 | from tqdm import tqdm
7 |
8 | # add logging
9 |
10 | logger = logging.getLogger("fastface.adapter")
11 |
12 |
13 | class ExtractHandler:
14 | @staticmethod
15 | def extract(
16 | file_path: str, dest_path: str, *args, remove_after: bool = True, **kwargs
17 | ):
18 | logger.info("extracting {} to {}".format(file_path, dest_path))
19 | if file_path.endswith(".zip"):
20 | ExtractHandler._extract_zipfile(file_path, dest_path, *args, **kwargs)
21 | else:
22 | # expected .tar file
23 | ExtractHandler._extract_tarfile(file_path, dest_path, *args, **kwargs)
24 |
25 | # clear the file
26 | if remove_after:
27 | logger.warning("removing source {} file".format(file_path))
28 | os.remove(file_path)
29 |
30 | @staticmethod
31 | def _extract_tarfile(file_path: str, dest_path: str, set_attrs: bool = False):
32 | if file_path.endswith(".tar.gz") or file_path.endswith(".tgz"):
33 | mode = "r:gz"
34 | elif file_path.endswith(".tar.bz2") or file_path.endswith(".tbz"):
35 | mode = "r:bz2"
36 | else:
37 | raise AssertionError("tar file extension is not valid")
38 |
39 | with tarfile.open(file_path, mode=mode) as foo:
40 | members = foo.getmembers()
41 | for member in tqdm(
42 | members, desc="extracting tar.gz file to {}".format(dest_path)
43 | ):
44 | try:
45 | foo.extract(member, path=dest_path, set_attrs=set_attrs)
46 | except PermissionError:
47 | pass # ignore
48 | except Exception as e:
49 | logger.error(
50 | "extracing member: {} failed with\n{}".format(member, e)
51 | )
52 |
53 | @staticmethod
54 | def _extract_zipfile(file_path: str, dest_path: str):
55 | with zipfile.ZipFile(file_path, "r") as zip_ref:
56 | zip_ref.extractall(dest_path)
57 |
--------------------------------------------------------------------------------
/fastface/adapter/gdrive.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from google_drive_downloader import GoogleDriveDownloader as gdd
4 |
5 |
6 | class GoogleDriveAdapter:
7 | @staticmethod
8 | def download(
9 | dest_path: str,
10 | file_name: str = None,
11 | file_id: str = None,
12 | extract: bool = False,
13 | showsize: bool = True,
14 | **kwargs
15 | ):
16 | assert isinstance(file_id, str), "file id must be string but found:{}".format(
17 | type(file_id)
18 | )
19 | assert isinstance(
20 | file_name, str
21 | ), "file name must be string but found:{}".format(type(file_name))
22 | if not os.path.exists(dest_path):
23 | os.makedirs(dest_path, exist_ok=True)
24 | file_path = os.path.join(dest_path, file_name)
25 | gdd.download_file_from_google_drive(
26 | file_id, file_path, unzip=extract, showsize=showsize, **kwargs
27 | )
28 |
--------------------------------------------------------------------------------
/fastface/adapter/http.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import requests
4 |
5 | from .extract_handler import ExtractHandler
6 |
7 |
8 | class HttpAdapter:
9 | @staticmethod
10 | def download(
11 | dest_path: str,
12 | file_name: str = None,
13 | url: str = None,
14 | extract: bool = False,
15 | **kwargs
16 | ):
17 | # TODO check if file name format is matched with downloaded file
18 |
19 | assert isinstance(url, str), "url must be string but found:{}".format(type(url))
20 |
21 | file_name = url.split("/")[-1] if file_name is None else file_name
22 | res = requests.get(url)
23 |
24 | assert (
25 | res.status_code == 200
26 | ), "wrong status code \
27 | recived:{} with response:{}".format(
28 | res.status_code, res.content
29 | )
30 |
31 | file_path = os.path.join(dest_path, file_name)
32 |
33 | if not os.path.exists(dest_path):
34 | os.makedirs(dest_path, exist_ok=True)
35 |
36 | with open(file_path, "wb") as foo:
37 | foo.write(res.content)
38 |
39 | if not extract:
40 | return
41 |
42 | ExtractHandler.extract(file_path, dest_path, **kwargs)
43 |
--------------------------------------------------------------------------------
/fastface/api/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Dict, List
3 |
4 | from ..adapter import download_object
5 | from ..utils.cache import get_model_cache_dir
6 | from ..utils.config import discover_archs, get_arch_cls, get_registry
7 |
8 |
9 | def list_pretrained_models() -> List[str]:
10 | """Returns available pretrained model names
11 |
12 | Returns:
13 | List[str]: list of pretrained model names
14 |
15 | >>> import fastface as ff
16 | >>> ff.list_pretrained_models()
17 | ['lffd_original', 'lffd_slim']
18 | """
19 | return list(get_registry().keys())
20 |
21 |
22 | def download_pretrained_model(model: str, target_path: str = None) -> str:
23 | """Downloads pretrained model to given target path,
24 | if target path is None, it will use model cache path.
25 | If model already exists in the given target path than it will do notting.
26 |
27 | Args:
28 | model (str): pretrained model name to download
29 | target_path (str, optional): target directory to download model. Defaults to None.
30 |
31 | Returns:
32 | str: file path of the model
33 | """
34 | if target_path is None:
35 | target_path = get_model_cache_dir()
36 | registry = get_registry()
37 | assert model in registry, f"given model: {model} is not in the registry"
38 | assert os.path.exists(
39 | target_path
40 | ), f"given target path: {target_path} does not exists"
41 | assert os.path.isdir(target_path), "given target path must be directory not a file"
42 |
43 | adapter = registry[model]["adapter"]
44 | file_name = registry[model]["adapter"]["kwargs"]["file_name"]
45 | model_path = os.path.join(target_path, file_name)
46 |
47 | if not os.path.isfile(model_path):
48 | # download if model not exists
49 | download_object(adapter["type"], dest_path=target_path, **adapter["kwargs"])
50 | return model_path
51 |
52 |
53 | def list_archs() -> List[str]:
54 | """Returns available architecture names
55 |
56 | Returns:
57 | List[str]: list of arch names
58 |
59 | >>> import fastface as ff
60 | >>> ff.list_archs()
61 | ['lffd']
62 |
63 | """
64 | return [arch for arch, _ in discover_archs()]
65 |
66 |
67 | def list_arch_configs(arch: str) -> List[str]:
68 | """Returns available architecture configurations as list
69 |
70 | Args:
71 | arch (str): architecture name
72 |
73 | Returns:
74 | List[str]: list of arch config names
75 |
76 | >>> import fastface as ff
77 | >>> ff.list_arch_configs('lffd')
78 | ['original', 'slim']
79 |
80 | """
81 | return list(get_arch_cls(arch).__CONFIGS__.keys())
82 |
83 |
84 | def get_arch_config(arch: str, config: str) -> Dict:
85 | """Returns configuration dictionary for given arch and config names
86 |
87 | Args:
88 | arch (str): architecture name
89 | config (str): configuration name
90 |
91 | Returns:
92 | Dict: configuration details as dictionary
93 |
94 | >>> import fastface as ff
95 | >>> ff.get_arch_config('lffd', 'slim')
96 | {'input_shape': (-1, 3, 480, 480), 'backbone_name': 'lffd-v2', 'head_infeatures': [64, 64, 64, 128, 128], 'head_outfeatures': [128, 128, 128, 128, 128], 'rf_sizes': [20, 40, 80, 160, 320], 'rf_start_offsets': [3, 7, 15, 31, 63], 'rf_strides': [4, 8, 16, 32, 64], 'scales': [(10, 20), (20, 40), (40, 80), (80, 160), (160, 320)]}
97 |
98 | """
99 | arch_cls = get_arch_cls(arch)
100 | return arch_cls.__CONFIGS__[config].copy()
101 |
--------------------------------------------------------------------------------
/fastface/arch/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borhanMorphy/fastface/4db5e4bb2c378d973c5d7da60e78be36c2422f45/fastface/arch/__init__.py
--------------------------------------------------------------------------------
/fastface/arch/lffd/__init__.py:
--------------------------------------------------------------------------------
1 | from .module import LFFD
2 |
3 | arch_cls = LFFD
4 |
--------------------------------------------------------------------------------
/fastface/arch/lffd/blocks/__init__.py:
--------------------------------------------------------------------------------
1 | from .anchor import Anchor
2 | from .backbone_v1 import LFFDBackboneV1
3 | from .backbone_v2 import LFFDBackboneV2
4 | from .conv import conv3x3
5 | from .head import LFFDHead
6 | from .resblock import ResBlock
7 |
8 | __all__ = [
9 | "Anchor",
10 | "LFFDBackboneV1",
11 | "LFFDBackboneV2",
12 | "conv3x3",
13 | "LFFDHead",
14 | "ResBlock",
15 | ]
16 |
--------------------------------------------------------------------------------
/fastface/arch/lffd/blocks/anchor.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn as nn
3 |
4 | from ....utils.box import generate_grids
5 |
6 |
7 | class Anchor(nn.Module):
8 | def __init__(self, rf_stride: int, rf_start_offset: int, rf_size: int):
9 | super().__init__()
10 | self.rf_stride = rf_stride
11 | self.rf_start_offset = rf_start_offset
12 | self.rf_size = rf_size
13 | grids = generate_grids(1500 // rf_stride, 1500 // rf_stride)
14 | rfs = (grids * rf_stride + rf_start_offset).repeat(
15 | 1, 1, 2
16 | ) # fh x fw x 2 => fh x fw x 4
17 | rfs[:, :, :2] = rfs[:, :, :2] - rf_size / 2
18 | rfs[:, :, 2:] = rfs[:, :, 2:] + rf_size / 2
19 | rf_centers = (rfs[:, :, :2] + rfs[:, :, 2:]) / 2
20 |
21 | # pylint: disable=not-callable
22 | self.register_buffer(
23 | "rf_normalizer", torch.tensor(rf_size / 2), persistent=False
24 | )
25 | self.register_buffer("rfs", rfs, persistent=False)
26 | self.register_buffer("rf_centers", rf_centers.repeat(1, 1, 2), persistent=False)
27 | # rfs: fh x fw x 4 as x1,y1,x2,y2
28 |
29 | def estimated_forward(self, imgh: int, imgw: int) -> torch.Tensor:
30 | """Estimates anchors using image dimensions
31 |
32 | Args:
33 | imgh (int): image height
34 | imgw (int): image width
35 |
36 | Returns:
37 | torch.Tensor: anchors with shape (fh x fw x 4) as xmin, ymin, xmax, ymax
38 | """
39 | fh = imgh // self.rf_stride - 1
40 | fw = imgw // self.rf_stride - 1
41 | return self.forward(fh, fw)
42 |
43 | def forward(self, fh: int, fw: int) -> torch.Tensor:
44 | """Generates anchors using featuremap dimensions
45 |
46 | Args:
47 | fh (int): featuremap hight
48 | fw (int): featuremap width
49 |
50 | Returns:
51 | torch.Tensor: anchors with shape (fh x fw x 4) as xmin, ymin, xmax, ymax
52 | """
53 | return self.rfs[:fh, :fw, :]
54 |
--------------------------------------------------------------------------------
/fastface/arch/lffd/blocks/backbone_v1.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from .conv import conv3x3
7 | from .resblock import ResBlock
8 |
9 |
10 | class LFFDBackboneV1(nn.Module):
11 | def __init__(self, in_channels: int):
12 | super().__init__()
13 | # *tiny part
14 | self.conv1_dw = conv3x3(in_channels, 64, stride=2, padding=0)
15 | self.relu1 = nn.ReLU()
16 |
17 | self.conv2_dw = conv3x3(64, 64, stride=2, padding=0)
18 | self.relu2 = nn.ReLU()
19 |
20 | self.res_block1 = ResBlock(64)
21 | self.res_block2 = ResBlock(64)
22 | self.res_block3 = ResBlock(64)
23 | self.res_block4 = ResBlock(64)
24 |
25 | # *small part
26 | self.conv3_dw = conv3x3(64, 64, stride=2, padding=0)
27 | self.relu3 = nn.ReLU()
28 |
29 | self.res_block5 = ResBlock(64)
30 | self.res_block6 = ResBlock(64)
31 |
32 | # *medium part
33 | self.conv4_dw = conv3x3(64, 128, stride=2, padding=0)
34 | self.relu4 = nn.ReLU()
35 | self.res_block7 = ResBlock(128)
36 |
37 | # *large part
38 | self.conv5_dw = conv3x3(128, 128, stride=2, padding=0)
39 | self.relu5 = nn.ReLU()
40 | self.res_block8 = ResBlock(128)
41 | self.res_block9 = ResBlock(128)
42 | self.res_block10 = ResBlock(128)
43 |
44 | def forward(self, x: torch.Tensor) -> List[torch.Tensor]:
45 | # *tiny part
46 | c1 = self.conv1_dw(x) # 3 => 64
47 | r1 = self.relu1(c1)
48 |
49 | c2 = self.conv2_dw(r1) # 64 => 64
50 | r2 = self.relu2(c2)
51 |
52 | r4, c4 = self.res_block1(r2, c2) # 64 => 64
53 | r6, c6 = self.res_block2(r4, c4) # 64 => 64
54 | r8, c8 = self.res_block3(r6, c6) # 64 => 64
55 | r10, _ = self.res_block4(r8, c8) # 64 => 64
56 |
57 | # *small part
58 | c11 = self.conv3_dw(r10) # 64 => 64
59 | r11 = self.relu3(c11)
60 |
61 | r13, c13 = self.res_block5(r11, c11) # 64 => 64
62 | r15, _ = self.res_block6(r13, c13) # 64 => 64
63 |
64 | # *medium part
65 | c16 = self.conv4_dw(r15) # 64 => 128
66 | r16 = self.relu4(c16)
67 |
68 | r18, _ = self.res_block7(r16, c16) # 128 => 128
69 |
70 | # *large part
71 | c19 = self.conv5_dw(r18) # 128 => 128
72 | r19 = self.relu5(c19)
73 |
74 | r21, c21 = self.res_block8(r19, c19) # 128 => 128
75 | r23, c23 = self.res_block9(r21, c21) # 128 => 128
76 | r25, _ = self.res_block10(r23, c23) # 128 => 128
77 |
78 | return [r8, r10, r13, r15, r18, r21, r23, r25]
79 |
--------------------------------------------------------------------------------
/fastface/arch/lffd/blocks/backbone_v2.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from .conv import conv3x3
7 | from .resblock import ResBlock
8 |
9 |
10 | class LFFDBackboneV2(nn.Module):
11 | def __init__(self, in_channels: int):
12 | super().__init__()
13 | # *tiny part
14 | self.conv1_dw = conv3x3(in_channels, 64, stride=2, padding=0)
15 | self.relu1 = nn.ReLU()
16 |
17 | self.conv2_dw = conv3x3(64, 64, stride=2, padding=0)
18 | self.relu2 = nn.ReLU()
19 |
20 | self.res_block1 = ResBlock(64)
21 | self.res_block2 = ResBlock(64)
22 | self.res_block3 = ResBlock(64)
23 |
24 | # *small part
25 | self.conv3_dw = conv3x3(64, 64, stride=2, padding=0)
26 | self.relu3 = nn.ReLU()
27 |
28 | self.res_block4 = ResBlock(64)
29 |
30 | # *medium part
31 | self.conv4_dw = conv3x3(64, 64, stride=2, padding=0)
32 | self.relu4 = nn.ReLU()
33 | self.res_block5 = ResBlock(64)
34 |
35 | # *large part
36 | self.conv5_dw = conv3x3(64, 128, stride=2, padding=0)
37 | self.relu5 = nn.ReLU()
38 | self.res_block6 = ResBlock(128)
39 |
40 | # *large part
41 | self.conv6_dw = conv3x3(128, 128, stride=2, padding=0)
42 | self.relu6 = nn.ReLU()
43 |
44 | self.res_block7 = ResBlock(128)
45 |
46 | def forward(self, x: torch.Tensor) -> List[torch.Tensor]:
47 | # *tiny part
48 | c1 = self.conv1_dw(x) # 3 => 64
49 | r1 = self.relu1(c1)
50 |
51 | c2 = self.conv2_dw(r1) # 64 => 64
52 | r2 = self.relu2(c2)
53 |
54 | r4, c4 = self.res_block1(r2, c2) # 64 => 64
55 | r6, c6 = self.res_block2(r4, c4) # 64 => 64
56 | r8, _ = self.res_block3(r6, c6) # 64 => 64
57 |
58 | # *small part
59 | c9 = self.conv3_dw(r8) # 64 => 64
60 | r9 = self.relu3(c9)
61 |
62 | r11, _ = self.res_block4(r9, c9) # 64 => 64
63 |
64 | # *medium part
65 | c12 = self.conv4_dw(r11) # 64 => 64
66 | r12 = self.relu4(c12)
67 |
68 | r14, _ = self.res_block5(r12, c12) # 64 => 64
69 |
70 | # *large part
71 | c15 = self.conv5_dw(r14) # 64 => 128
72 | r15 = self.relu5(c15)
73 | r17, _ = self.res_block6(r15, c15) # 128 => 128
74 |
75 | # *large part
76 | c18 = self.conv6_dw(r17) # 128 => 128
77 | r18 = self.relu6(c18)
78 | r20, _ = self.res_block7(r18, c18) # 128 => 128
79 |
80 | return [r8, r11, r14, r17, r20]
81 |
--------------------------------------------------------------------------------
/fastface/arch/lffd/blocks/conv.py:
--------------------------------------------------------------------------------
1 | import torch.nn as nn
2 |
3 |
4 | def conv3x3(
5 | in_channels: int, out_channels: int, stride: int = 1, padding: int = 1
6 | ) -> nn.Module:
7 |
8 | conv = nn.Conv2d(
9 | in_channels,
10 | out_channels,
11 | kernel_size=3,
12 | stride=stride,
13 | padding=padding,
14 | bias=True,
15 | )
16 |
17 | nn.init.xavier_normal_(conv.weight)
18 |
19 | if conv.bias is not None:
20 | conv.bias.data.fill_(0)
21 |
22 | return conv
23 |
24 |
25 | def conv1x1(in_channels: int, out_channels: int) -> nn.Module:
26 |
27 | conv = nn.Conv2d(
28 | in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=True
29 | )
30 |
31 | nn.init.xavier_normal_(conv.weight)
32 |
33 | if conv.bias is not None:
34 | conv.bias.data.fill_(0)
35 |
36 | return conv
37 |
--------------------------------------------------------------------------------
/fastface/arch/lffd/blocks/head.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from .anchor import Anchor
7 | from .conv import conv1x1
8 |
9 |
10 | class LFFDHead(nn.Module):
11 | def __init__(
12 | self,
13 | head_idx: int,
14 | infeatures: int,
15 | features: int,
16 | rf_size: int,
17 | rf_start_offset: int,
18 | rf_stride: int,
19 | num_classes: int = 1,
20 | ):
21 | super().__init__()
22 | self.head_idx = head_idx
23 | self.num_classes = num_classes
24 | self.anchor = Anchor(rf_stride, rf_start_offset, rf_size)
25 |
26 | self.det_conv = nn.Sequential(conv1x1(infeatures, features), nn.ReLU())
27 |
28 | self.cls_head = nn.Sequential(
29 | conv1x1(features, features), nn.ReLU(), conv1x1(features, self.num_classes)
30 | )
31 |
32 | self.reg_head = nn.Sequential(
33 | conv1x1(features, features), nn.ReLU(), conv1x1(features, 4)
34 | )
35 |
36 | def conv_xavier_init(m):
37 | if type(m) == nn.Conv2d:
38 | nn.init.xavier_normal_(m.weight)
39 |
40 | if m.bias is not None:
41 | m.bias.data.fill_(0)
42 |
43 | self.apply(conv_xavier_init)
44 |
45 | def forward(self, features: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
46 | data = self.det_conv(features)
47 |
48 | cls_logits = self.cls_head(data)
49 | # (b,1,h,w)
50 | reg_logits = self.reg_head(data)
51 | # (b,c,h,w)
52 | return reg_logits, cls_logits
53 |
54 | def logits_to_boxes(self, reg_logits: torch.Tensor) -> torch.Tensor:
55 | """Applies bounding box regression using regression logits
56 | Args:
57 | reg_logits (torch.Tensor): bs,fh,fw,4
58 |
59 | Returns:
60 | pred_boxes (torch.Tensor): bs,fh,fw,4 as xmin,ymin,xmax,ymax
61 | """
62 | _, fh, fw, _ = reg_logits.shape
63 |
64 | rf_centers = self.anchor.rf_centers[:fh, :fw]
65 | # rf_centers: fh,fw,4 cx1,cy1,cx1,cy1
66 |
67 | # reg_logits[:, :, :, 0] = torch.clamp(reg_logits[:, :, :, 0], 0, fw*self.rf_stride)
68 | # reg_logits[:, :, :, 1] = torch.clamp(reg_logits[:, :, :, 1], 0, fh*self.rf_stride)
69 | # reg_logits[:, :, :, 2] = torch.clamp(reg_logits[:, :, :, 2], 0, fw*self.rf_stride)
70 | # reg_logits[:, :, :, 3] = torch.clamp(reg_logits[:, :, :, 3], 0, fh*self.rf_stride)
71 |
72 | return rf_centers - reg_logits * self.anchor.rf_normalizer
73 |
--------------------------------------------------------------------------------
/fastface/arch/lffd/blocks/resblock.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple
2 |
3 | import torch
4 | import torch.nn as nn
5 |
6 | from .conv import conv3x3
7 |
8 |
9 | class ResBlock(nn.Module):
10 | def __init__(self, features: int):
11 | super(ResBlock, self).__init__()
12 | self.conv1 = conv3x3(features, features)
13 | self.relu1 = nn.ReLU()
14 | self.conv2 = conv3x3(features, features)
15 | self.relu2 = nn.ReLU()
16 |
17 | def forward(
18 | self, activated_input: torch.Tensor, residual_input: torch.Tensor
19 | ) -> Tuple[torch.Tensor, torch.Tensor]:
20 | x = self.conv1(activated_input) # c(i) => c(i+1)
21 | x = self.relu1(x)
22 | x = self.conv2(x) # c(i+1) => c(i+2)
23 | residual_output = x + residual_input # residual
24 | activated_output = self.relu2(residual_output)
25 | return activated_output, residual_output
26 |
--------------------------------------------------------------------------------
/fastface/arch/lffd/module.py:
--------------------------------------------------------------------------------
1 | import math
2 | from typing import Dict, List, Tuple
3 |
4 | import torch
5 | import torch.nn as nn
6 |
7 | from .blocks import LFFDBackboneV1, LFFDBackboneV2, LFFDHead
8 |
9 |
10 | class LFFD(nn.Module):
11 |
12 | __CONFIGS__ = {
13 | "original": {
14 | "input_shape": (-1, 3, 640, 640),
15 | "backbone_name": "lffd-v1",
16 | "head_infeatures": [64, 64, 64, 64, 128, 128, 128, 128],
17 | "head_outfeatures": [128, 128, 128, 128, 128, 128, 128, 128],
18 | "rf_sizes": [15, 20, 40, 70, 110, 250, 400, 560],
19 | "rf_start_offsets": [3, 3, 7, 7, 15, 31, 31, 31],
20 | "rf_strides": [4, 4, 8, 8, 16, 32, 32, 32],
21 | "scales": [
22 | (10, 15),
23 | (15, 20),
24 | (20, 40),
25 | (40, 70),
26 | (70, 110),
27 | (110, 250),
28 | (250, 400),
29 | (400, 560),
30 | ],
31 | },
32 | "slim": {
33 | "input_shape": (-1, 3, 480, 480),
34 | "backbone_name": "lffd-v2",
35 | "head_infeatures": [64, 64, 64, 128, 128],
36 | "head_outfeatures": [128, 128, 128, 128, 128],
37 | "rf_sizes": [20, 40, 80, 160, 320],
38 | "rf_start_offsets": [3, 7, 15, 31, 63],
39 | "rf_strides": [4, 8, 16, 32, 64],
40 | "scales": [(10, 20), (20, 40), (40, 80), (80, 160), (160, 320)],
41 | },
42 | }
43 |
44 | def __init__(self, config: Dict, **kwargs):
45 | super().__init__()
46 |
47 | assert "input_shape" in config, "`input_shape` must be defined in the config"
48 | assert (
49 | "backbone_name" in config
50 | ), "`backbone_name` must be defined in the config"
51 | assert (
52 | "head_infeatures" in config
53 | ), "`head_infeatures` must be defined in the config"
54 | assert (
55 | "head_outfeatures" in config
56 | ), "`head_outfeatures` must be defined in the config"
57 | assert "rf_sizes" in config, "`rf_sizes` must be defined in the config"
58 | assert (
59 | "rf_start_offsets" in config
60 | ), "`rf_start_offsets` must be defined in the config"
61 | assert "rf_strides" in config, "`rf_strides` must be defined in the config"
62 | assert "scales" in config, "`scales` must be defined in the config"
63 |
64 | backbone_name = config.get("backbone_name")
65 | head_infeatures = config.get("head_infeatures")
66 | head_outfeatures = config.get("head_outfeatures")
67 | rf_sizes = config.get("rf_sizes")
68 | rf_start_offsets = config.get("rf_start_offsets")
69 | rf_strides = config.get("rf_strides")
70 |
71 | self.face_scales = config["scales"]
72 |
73 | self.input_shape = config.get("input_shape")
74 |
75 | # TODO check if list lenghts are matched
76 | if backbone_name == "lffd-v1":
77 | self.backbone = LFFDBackboneV1(3)
78 | elif backbone_name == "lffd-v2":
79 | self.backbone = LFFDBackboneV2(3)
80 | else:
81 | raise ValueError(f"given backbone name: {backbone_name} is not valid")
82 |
83 | self.heads = nn.ModuleList(
84 | [
85 | LFFDHead(
86 | idx + 1,
87 | infeatures,
88 | outfeatures,
89 | rf_size,
90 | rf_start_offset,
91 | rf_stride,
92 | num_classes=1,
93 | )
94 | for idx, (
95 | infeatures,
96 | outfeatures,
97 | rf_size,
98 | rf_start_offset,
99 | rf_stride,
100 | ) in enumerate(
101 | zip(
102 | head_infeatures,
103 | head_outfeatures,
104 | rf_sizes,
105 | rf_start_offsets,
106 | rf_strides,
107 | )
108 | )
109 | ]
110 | )
111 |
112 | self.cls_loss_fn = nn.BCEWithLogitsLoss(reduction="none")
113 | self.reg_loss_fn = nn.MSELoss(reduction="none")
114 |
115 | def forward(self, batch: torch.Tensor) -> List[torch.Tensor]:
116 | """preprocessed image batch
117 |
118 | Args:
119 | batch (torch.Tensor): B x C x H x W
120 |
121 | Returns:
122 | List[torch.Tensor]: list of logits as B x FH x FW x 5
123 | (0:4) reg logits
124 | (4:5) cls logits
125 | """
126 | features = self.backbone(batch)
127 |
128 | logits: List[torch.Tensor] = []
129 |
130 | for head_idx, head in enumerate(self.heads):
131 | reg_logits, cls_logits = head(features[head_idx])
132 | # reg_logits : B x 4 x fh x fw
133 | # cls_logits : B x 1 x fh x fw
134 | reg_logits = reg_logits.permute(0, 2, 3, 1)
135 | cls_logits = cls_logits.permute(0, 2, 3, 1)
136 |
137 | logits.append(
138 | # concat channel wise
139 | # B x fh x fw x 4 (+) B x fh x fw x 1
140 | torch.cat([reg_logits, cls_logits], dim=3)
141 | )
142 |
143 | return logits
144 |
145 | def logits_to_preds(self, logits: List[torch.Tensor]) -> torch.Tensor:
146 | """Applies postprocess to given logits
147 |
148 | Args:
149 | logits (List[torch.Tensor]): list of logits as B x FH x FW x 5
150 | (0:4) reg logits
151 | (4:5) cls logits
152 | Returns:
153 | torch.Tensor: as preds with shape of B x N x 5 where x1,y1,x2,y2,score
154 | """
155 | preds: List[torch.Tensor] = []
156 |
157 | for head_idx, head in enumerate(self.heads):
158 | batch_size, fh, fw, _ = logits[head_idx].shape
159 |
160 | scores = torch.sigmoid(logits[head_idx][:, :, :, [4]])
161 | boxes = head.logits_to_boxes(logits[head_idx][:, :, :, :4])
162 |
163 | preds.append(
164 | # B x n x 5 as x1,y1,x2,y2,score
165 | torch.cat([boxes, scores], dim=3)
166 | .flatten(start_dim=1, end_dim=2)
167 | .contiguous()
168 | )
169 |
170 | # concat channelwise: B x N x 5
171 | return torch.cat(preds, dim=1).contiguous()
172 |
173 | def compute_loss(
174 | self, logits: List[torch.Tensor], raw_targets: List[Dict], hparams: Dict = {}
175 | ) -> Dict[str, torch.Tensor]:
176 | """Computes loss using given logits and raw targets
177 |
178 | Args:
179 | logits (List[torch.Tensor]): list of torch.Tensor(B, fh, fw, 5) where;
180 | (0:4) reg logits
181 | (4:5) cls logits
182 | raw_targets (List[Dict]): list of dicts as;
183 | "target_boxes": torch.Tensor(N, 4)
184 | hparams (Dict, optional): model hyperparameter dict. Defaults to {}.
185 |
186 | Returns:
187 | Dict[str, torch.Tensor]: loss values as key value pairs
188 |
189 | """
190 | batch_size = len(raw_targets)
191 | neg_select_ratio = hparams.get("ratio", 10)
192 |
193 | fmap_shapes = [head_logits.shape[1:3] for head_logits in logits]
194 |
195 | logits = torch.cat(
196 | [head_logits.view(batch_size, -1, 5) for head_logits in logits], dim=1
197 | )
198 | # logits: b, n, 5
199 |
200 | reg_logits = logits[:, :, :4]
201 | # reg_logits: b, n, 4
202 |
203 | cls_logits = logits[:, :, 4]
204 | # cls_logits: b, n
205 |
206 | targets = self.build_targets(
207 | fmap_shapes, raw_targets, logits.dtype, logits.device
208 | )
209 | # targets: b, n, 5
210 |
211 | reg_targets = targets[:, :, :4]
212 | # reg_targets: b, n, 4
213 |
214 | cls_targets = targets[:, :, 4]
215 | # cls_targets: b, n
216 |
217 | pos_mask = cls_targets == 1
218 | neg_mask = cls_targets == 0
219 | num_of_positives = pos_mask.sum()
220 |
221 | pos_cls_loss = self.cls_loss_fn(cls_logits[pos_mask], cls_targets[pos_mask])
222 | neg_cls_loss = self.cls_loss_fn(cls_logits[neg_mask], cls_targets[neg_mask])
223 | order = neg_cls_loss.argsort(descending=True)
224 | keep_cls = max(num_of_positives * neg_select_ratio, 100)
225 |
226 | cls_loss = torch.cat([pos_cls_loss, neg_cls_loss[order][:keep_cls]]).mean()
227 |
228 | if pos_mask.sum() > 0:
229 | reg_loss = self.reg_loss_fn(
230 | reg_logits[pos_mask], reg_targets[pos_mask]
231 | ).mean()
232 | else:
233 | reg_loss = torch.tensor(
234 | 0, dtype=logits.dtype, device=logits.device, requires_grad=True
235 | ) # pylint: disable=not-callable
236 |
237 | loss = cls_loss + reg_loss
238 |
239 | return {"loss": loss, "cls_loss": cls_loss, "reg_loss": reg_loss}
240 |
241 | def build_targets(
242 | self,
243 | fmap_shapes: List[Tuple[int, int]],
244 | raw_targets: List[Dict],
245 | dtype=torch.float32,
246 | device="cpu",
247 | ) -> torch.Tensor:
248 | """build model targets using given logits and raw targets
249 |
250 | Args:
251 | fmap_shapes (List[Tuple[int, int]]): feature map shapes for each head, [(fh,fw), ...]
252 | raw_targets (List[Dict]): list of dicts as;
253 | "target_boxes": torch.Tensor(N, 4)
254 | dtype : dtype of the tensor. Defaults to torch.float32.
255 | device (str): device of the tensor. Defaults to 'cpu'.
256 |
257 | Returns:
258 | torch.Tensor: targets as B x N x 5 where (0:4) reg targets (4:5) cls targets
259 | """
260 |
261 | batch_target_boxes = []
262 | batch_target_face_scales = []
263 |
264 | batch_size = len(raw_targets)
265 |
266 | for target in raw_targets:
267 | t_boxes = target["target_boxes"]
268 | batch_target_boxes.append(t_boxes)
269 |
270 | # select max face dim as `face scale` (defined in the paper)
271 | batch_target_face_scales.append(
272 | 0
273 | if t_boxes.size(0) == 0
274 | else (t_boxes[:, [2, 3]] - t_boxes[:, [0, 1]]).max(dim=1)[0]
275 | )
276 |
277 | targets = []
278 |
279 | for head_idx, (head, (fh, fw)) in enumerate(zip(self.heads, fmap_shapes)):
280 | min_face_scale, max_face_scale = self.face_scales[head_idx]
281 | min_gray_face_scale = math.floor(min_face_scale * 0.9)
282 | max_gray_face_scale = math.ceil(max_face_scale * 1.1)
283 |
284 | rfs = head.anchor.forward(fh, fw).to(device)
285 | # rfs: fh x fw x 4 as xmin, ymin, xmax, ymax
286 |
287 | # calculate rf normalizer for the head
288 | rf_normalizer = head.anchor.rf_size / 2
289 |
290 | # get rf centers
291 | rf_centers = (rfs[..., [2, 3]] + rfs[..., [0, 1]]) / 2
292 |
293 | # rf_centers: fh x fw x 2 as center_x, center_y
294 |
295 | rfs = rfs.repeat(batch_size, 1, 1, 1)
296 | # rfs fh x fw x 4 => bs x fh x fw x 4
297 |
298 | head_cls_targets = torch.zeros(
299 | *(batch_size, fh, fw), dtype=dtype, device=device
300 | ) # 0: bg, 1: fg, -1: ignore
301 | head_reg_targets = torch.zeros(
302 | *(batch_size, fh, fw, 4), dtype=dtype, device=device
303 | )
304 |
305 | for batch_idx, (target_boxes, target_face_scales) in enumerate(
306 | zip(batch_target_boxes, batch_target_face_scales)
307 | ):
308 | # for each image in the batch
309 | if target_boxes.size(0) == 0:
310 | continue
311 |
312 | # selected accepted boxes
313 | (head_accept_box_ids,) = torch.where(
314 | (target_face_scales > min_face_scale)
315 | & (target_face_scales < max_face_scale)
316 | )
317 |
318 | # find ignore boxes
319 | (head_ignore_box_ids,) = torch.where(
320 | (
321 | (target_face_scales >= min_gray_face_scale)
322 | & (target_face_scales <= min_face_scale)
323 | )
324 | | (
325 | (target_face_scales <= max_gray_face_scale)
326 | & (target_face_scales >= max_face_scale)
327 | )
328 | )
329 |
330 | for gt_idx, (x1, y1, x2, y2) in enumerate(target_boxes):
331 |
332 | match_mask = (
333 | (x1 < rf_centers[:, :, 0]) & (x2 > rf_centers[:, :, 0])
334 | ) & ((y1 < rf_centers[:, :, 1]) & (y2 > rf_centers[:, :, 1]))
335 |
336 | # match_mask: fh, fw
337 | if match_mask.sum() <= 0:
338 | continue
339 |
340 | if gt_idx in head_ignore_box_ids:
341 | # if gt is in gray scale, all matches sets as ignore
342 | match_fh, match_fw = torch.where(match_mask)
343 |
344 | # set matches as fg
345 | head_cls_targets[batch_idx, match_fh, match_fw] = -1
346 | continue
347 | elif gt_idx not in head_accept_box_ids:
348 | # if gt not in gray scale and not in accepted ids, than skip it
349 | continue
350 |
351 | match_fh, match_fw = torch.where(
352 | match_mask & (head_cls_targets[batch_idx, :, :] != -1)
353 | )
354 | double_match_fh, double_match_fw = torch.where(
355 | (head_cls_targets[batch_idx, :, :] == 1) & match_mask
356 | )
357 |
358 | # set matches as fg
359 | head_cls_targets[batch_idx, match_fh, match_fw] = 1
360 |
361 | head_reg_targets[batch_idx, match_fh, match_fw, 0] = (
362 | rf_centers[match_fh, match_fw, 0] - x1
363 | ) / rf_normalizer
364 | head_reg_targets[batch_idx, match_fh, match_fw, 1] = (
365 | rf_centers[match_fh, match_fw, 1] - y1
366 | ) / rf_normalizer
367 | head_reg_targets[batch_idx, match_fh, match_fw, 2] = (
368 | rf_centers[match_fh, match_fw, 0] - x2
369 | ) / rf_normalizer
370 | head_reg_targets[batch_idx, match_fh, match_fw, 3] = (
371 | rf_centers[match_fh, match_fw, 1] - y2
372 | ) / rf_normalizer
373 |
374 | # set multi-matches as ignore
375 | head_cls_targets[batch_idx, double_match_fh, double_match_fw] = -1
376 |
377 | targets.append(
378 | torch.cat(
379 | [
380 | head_reg_targets.view(batch_size, -1, 4),
381 | head_cls_targets.view(batch_size, -1, 1),
382 | ],
383 | dim=2,
384 | )
385 | )
386 |
387 | # concat n wise
388 | return torch.cat(targets, dim=1)
389 |
390 | def configure_optimizers(self, hparams: Dict = {}):
391 | optimizer = torch.optim.SGD(
392 | self.parameters(),
393 | lr=hparams.get("learning_rate", 1e-1),
394 | momentum=hparams.get("momentum", 0.9),
395 | weight_decay=hparams.get("weight_decay", 1e-5),
396 | )
397 |
398 | lr_scheduler = torch.optim.lr_scheduler.MultiStepLR(
399 | optimizer,
400 | milestones=hparams.get("milestones", [600000, 1000000, 1200000, 1400000]),
401 | gamma=hparams.get("gamma", 0.1),
402 | )
403 |
404 | return [optimizer], [lr_scheduler]
405 |
--------------------------------------------------------------------------------
/fastface/dataset/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import BaseDataset
2 | from .fddb import FDDBDataset
3 | from .widerface import WiderFaceDataset
4 |
5 | __all__ = [
6 | "BaseDataset",
7 | "FDDBDataset",
8 | "WiderFaceDataset",
9 | ]
10 |
--------------------------------------------------------------------------------
/fastface/dataset/base.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import logging
3 | import os
4 | from typing import Dict, List, Tuple
5 |
6 | import checksumdir
7 | import imageio
8 | import numpy as np
9 | import torch
10 | from torch.utils.data import DataLoader, Dataset
11 | from tqdm import tqdm
12 |
13 | from ..adapter import download_object
14 |
15 | logger = logging.getLogger("fastface.dataset")
16 |
17 |
18 | class _IdentitiyTransforms:
19 | """Dummy tranforms"""
20 |
21 | def __call__(self, img: np.ndarray, targets: Dict) -> Tuple:
22 | return img, targets
23 |
24 |
25 | def default_collate_fn(batch):
26 | batch, targets = zip(*batch)
27 | batch = np.stack(batch, axis=0).astype(np.float32)
28 | batch = torch.from_numpy(batch).permute(0, 3, 1, 2).contiguous()
29 | for i, target in enumerate(targets):
30 | for k, v in target.items():
31 | if isinstance(v, np.ndarray):
32 | targets[i][k] = torch.from_numpy(v)
33 |
34 | return batch, targets
35 |
36 |
37 | class BaseDataset(Dataset):
38 | def __init__(self, ids: List[str], targets: List[Dict], transforms=None, **kwargs):
39 | super().__init__()
40 | assert isinstance(ids, list), "given `ids` must be list"
41 | assert isinstance(targets, list), "given `targets must be list"
42 | assert len(ids) == len(targets), "lenght of both lists must be equal"
43 |
44 | self.ids = ids
45 | self.targets = targets
46 | self.transforms = _IdentitiyTransforms() if transforms is None else transforms
47 |
48 | # set given kwargs to the dataset
49 | for key, value in kwargs.items():
50 | if hasattr(self, key):
51 | # log warning
52 | continue
53 | setattr(self, key, value)
54 |
55 | def __getitem__(self, idx: int) -> Tuple:
56 | img = self._load_image(self.ids[idx])
57 | targets = copy.deepcopy(self.targets[idx])
58 |
59 | # apply transforms
60 | img, targets = self.transforms(img, targets)
61 |
62 | # clip boxes
63 | targets["target_boxes"] = self._clip_boxes(
64 | targets["target_boxes"], img.shape[:2]
65 | )
66 |
67 | # discard zero sized boxes
68 | targets["target_boxes"] = self._discard_zero_size_boxes(targets["target_boxes"])
69 |
70 | return (img, targets)
71 |
72 | def __len__(self) -> int:
73 | return len(self.ids)
74 |
75 | @staticmethod
76 | def _clip_boxes(boxes: np.ndarray, shape: Tuple[int, int]) -> np.ndarray:
77 | # TODO pydoc
78 | height, width = shape
79 | boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(min=0, max=width - 1)
80 | boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(min=0, max=height - 1)
81 |
82 | return boxes
83 |
84 | @staticmethod
85 | def _discard_zero_size_boxes(boxes: np.ndarray) -> np.ndarray:
86 | # TODO pydoc
87 | scale = (boxes[:, [2, 3]] - boxes[:, [0, 1]]).min(axis=1)
88 | return boxes[scale > 0]
89 |
90 | @staticmethod
91 | def _load_image(img_file_path: str):
92 | """loads rgb image using given file path
93 |
94 | Args:
95 | img_path (str): image file path to load
96 |
97 | Returns:
98 | np.ndarray: rgb image as np.ndarray
99 | """
100 | img = imageio.imread(img_file_path)
101 | if not img.flags["C_CONTIGUOUS"]:
102 | # if img is not contiguous than fix it
103 | img = np.ascontiguousarray(img, dtype=img.dtype)
104 |
105 | if len(img.shape) == 4:
106 | # found RGBA, converting to => RGB
107 | img = img[:, :, :3]
108 | elif len(img.shape) == 2:
109 | # found GRAYSCALE, converting to => RGB
110 | img = np.stack([img, img, img], axis=-1)
111 |
112 | return np.array(img, dtype=np.uint8)
113 |
114 | def get_dataloader(
115 | self,
116 | batch_size: int = 1,
117 | shuffle: bool = False,
118 | num_workers: int = 0,
119 | collate_fn=default_collate_fn,
120 | pin_memory: bool = False,
121 | **kwargs
122 | ):
123 |
124 | return DataLoader(
125 | self,
126 | batch_size=batch_size,
127 | shuffle=shuffle,
128 | num_workers=num_workers,
129 | collate_fn=collate_fn,
130 | pin_memory=pin_memory,
131 | **kwargs
132 | )
133 |
134 | def get_mean_std(self) -> Dict:
135 | # TODO pydoc
136 | mean_sum, mean_sq_sum = np.zeros(3), np.zeros(3)
137 | for img, _ in tqdm(
138 | self, total=len(self), desc="calculating mean and std for the dataset"
139 | ):
140 | d = img.astype(np.float32) / 255
141 |
142 | mean_sum[0] += np.mean(d[:, :, 0])
143 | mean_sum[1] += np.mean(d[:, :, 1])
144 | mean_sum[2] += np.mean(d[:, :, 2])
145 |
146 | mean_sq_sum[0] += np.mean(d[:, :, 0] ** 2)
147 | mean_sq_sum[1] += np.mean(d[:, :, 1] ** 2)
148 | mean_sq_sum[2] += np.mean(d[:, :, 2] ** 2)
149 |
150 | mean = mean_sum / len(self)
151 | std = (mean_sq_sum / len(self) - mean ** 2) ** 0.5
152 |
153 | return {"mean": mean.tolist(), "std": std.tolist()}
154 |
155 | def get_normalized_boxes(self) -> np.ndarray:
156 | # TODO pydoc
157 | normalized_boxes = []
158 | for img, targets in tqdm(
159 | self, total=len(self), desc="computing normalized target boxes"
160 | ):
161 | if targets["target_boxes"].shape[0] == 0:
162 | continue
163 | max_size = max(img.shape)
164 | normalized_boxes.append(targets["target_boxes"] / max_size)
165 |
166 | return np.concatenate(normalized_boxes, axis=0)
167 |
168 | def get_box_scale_histogram(self) -> Tuple[np.ndarray, np.ndarray]:
169 | bins = map(lambda x: 2 ** x, range(10))
170 | total_boxes = []
171 | for _, targets in tqdm(self, total=len(self), desc="getting box sizes"):
172 | if targets["target_boxes"].shape[0] == 0:
173 | continue
174 | total_boxes.append(targets["target_boxes"])
175 |
176 | total_boxes = np.concatenate(total_boxes, axis=0)
177 | areas = (total_boxes[:, 2] - total_boxes[:, 0]) * (
178 | total_boxes[:, 3] - total_boxes[:, 1]
179 | )
180 |
181 | return np.histogram(np.sqrt(areas), bins=list(bins))
182 |
183 | def download(self, urls: List, target_dir: str):
184 | for k, v in urls.items():
185 |
186 | keys = list(v["check"].items())
187 | checked_keys = []
188 |
189 | for key, md5hash in keys:
190 | target_sub_dir = os.path.join(target_dir, key)
191 | if not os.path.exists(target_sub_dir):
192 | checked_keys.append(False)
193 | else:
194 | checked_keys.append(
195 | checksumdir.dirhash(target_sub_dir, hashfunc="md5") == md5hash
196 | )
197 |
198 | if sum(checked_keys) == len(keys):
199 | logger.debug("found {} at {}".format(k, target_dir))
200 | continue
201 |
202 | # download
203 | adapter = v.get("adapter")
204 | kwargs = v.get("kwargs", {})
205 | logger.warning(
206 | "{} not found in the {}, downloading...".format(k, target_dir)
207 | )
208 | download_object(adapter, dest_path=target_dir, **kwargs)
209 |
--------------------------------------------------------------------------------
/fastface/dataset/fddb.py:
--------------------------------------------------------------------------------
1 | import math
2 | import os
3 | from typing import List
4 |
5 | import numpy as np
6 |
7 | from ..utils.cache import get_data_cache_dir
8 | from .base import BaseDataset
9 |
10 |
11 | def _ellipse2box(major_r, minor_r, angle, center_x, center_y):
12 | tan_t = -(minor_r / major_r) * math.tan(angle)
13 | t = math.atan(tan_t)
14 | x1 = center_x + (
15 | major_r * math.cos(t) * math.cos(angle)
16 | - minor_r * math.sin(t) * math.sin(angle)
17 | )
18 | x2 = center_x + (
19 | major_r * math.cos(t + math.pi) * math.cos(angle)
20 | - minor_r * math.sin(t + math.pi) * math.sin(angle)
21 | )
22 | x_max = max(x1, x2)
23 | x_min = min(x1, x2)
24 |
25 | if math.tan(angle) != 0:
26 | tan_t = (minor_r / major_r) * (1 / math.tan(angle))
27 | else:
28 | tan_t = (minor_r / major_r) * (1 / (math.tan(angle) + 0.0001))
29 | t = math.atan(tan_t)
30 | y1 = center_y + (
31 | minor_r * math.sin(t) * math.cos(angle)
32 | + major_r * math.cos(t) * math.sin(angle)
33 | )
34 | y2 = center_y + (
35 | minor_r * math.sin(t + math.pi) * math.cos(angle)
36 | + major_r * math.cos(t + math.pi) * math.sin(angle)
37 | )
38 | y_max = max(y1, y2)
39 | y_min = min(y1, y2)
40 |
41 | return x_min, y_min, x_max, y_max
42 |
43 |
44 | def _load_single_annotation_fold(source_path: str, fold_idx: int):
45 | # source_path/FDDB-fold-{:02d}-ellipseList.txt
46 | # TODO check fold idx range
47 |
48 | fold_file_name = "FDDB-fold-{:02d}-ellipseList.txt".format(fold_idx)
49 | fold_prefix = "FDDB-folds"
50 |
51 | img_file_path = os.path.join(source_path, "{}.jpg")
52 |
53 | fold_file_path = os.path.join(source_path, fold_prefix, fold_file_name)
54 | ids = []
55 | targets = []
56 | boxes = []
57 |
58 | with open(fold_file_path, "r") as foo:
59 | for line in foo.read().split("\n"):
60 | if os.path.isfile(img_file_path.format(line)):
61 | # indicates img file path
62 | if len(boxes) > 0:
63 | boxes = np.array(boxes)
64 | targets.append(boxes)
65 |
66 | ids.append(img_file_path.format(line))
67 | boxes = []
68 | elif line.isnumeric():
69 | # indicates number of face line
70 | pass
71 | elif line != "":
72 | # indicates box
73 | # 123.583300 85.549500 1.265839 269.693400 161.781200 1
74 | major_r, minor_r, angle, cx, cy, _ = [
75 | float(point) for point in line.split(" ") if point != ""
76 | ]
77 | box = _ellipse2box(major_r, minor_r, angle, cx, cy)
78 | boxes.append(box)
79 | if len(boxes) > 0:
80 | boxes = np.array(boxes)
81 | targets.append(boxes)
82 |
83 | return ids, targets
84 |
85 |
86 | class FDDBDataset(BaseDataset):
87 | """FDDB fastface.dataset.BaseDataset Instance
88 |
89 | paper: http://vis-www.cs.umass.edu/fddb/fddb.pdf
90 | specs:
91 | - total number of images: 2845
92 | - total number of faces: 5171
93 |
94 | """
95 |
96 | __URLS__ = {
97 | "fddb-images": {
98 | "adapter": "http",
99 | "check": {
100 | "2002": "ffd8ac86d9f407ac415cfe4dd2421407",
101 | "2003": "6356cbce76b26a92fc9788b221f5e5bb",
102 | },
103 | "kwargs": {
104 | "url": "http://vis-www.cs.umass.edu/fddb/originalPics.tar.gz",
105 | "extract": True,
106 | },
107 | },
108 | "fddb-annotations": {
109 | "adapter": "http",
110 | "check": {"FDDB-folds": "694de7a9144611e2353b7055819026e3"},
111 | "kwargs": {
112 | "url": "http://vis-www.cs.umass.edu/fddb/FDDB-folds.tgz",
113 | "extract": True,
114 | },
115 | },
116 | }
117 |
118 | __phases__ = ("train", "val", "test")
119 | __folds__ = tuple((i + 1 for i in range(10)))
120 | __splits__ = ((1, 2, 4, 5, 7, 9, 10), (3, 6, 8), (1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
121 |
122 | def __init__(
123 | self,
124 | source_dir: str = None,
125 | phase: str = None,
126 | folds: List[int] = None,
127 | transforms=None,
128 | **kwargs
129 | ):
130 |
131 | source_dir = (
132 | get_data_cache_dir(suffix="fddb") if source_dir is None else source_dir
133 | )
134 |
135 | # check if download
136 | self.download(self.__URLS__, source_dir)
137 |
138 | assert os.path.exists(
139 | source_dir
140 | ), "given source directory for fddb is not exist at {}".format(source_dir)
141 | assert (
142 | phase is None or phase in FDDBDataset.__phases__
143 | ), "given phase {} is \
144 | not valid, must be one of: {}".format(
145 | phase, self.__phases__
146 | )
147 |
148 | if phase is None:
149 | folds = list(self.__folds__) if folds is None else folds
150 | else:
151 | # TODO log here if `phase` is not None, folds argument will be ignored
152 | folds = self.__splits__[self.__phases__.index(phase)]
153 |
154 | ids = []
155 | targets = []
156 | for fold_idx in folds:
157 | assert (
158 | fold_idx in self.__folds__
159 | ), "given fold {} is not in the fold list".format(fold_idx)
160 | raw_ids, raw_targets = _load_single_annotation_fold(source_dir, fold_idx)
161 | ids += raw_ids
162 | # TODO each targets must be dict
163 | for target in raw_targets:
164 | targets.append({"target_boxes": target.astype(np.float32)})
165 | del raw_targets
166 | super().__init__(ids, targets, transforms=transforms, **kwargs)
167 |
--------------------------------------------------------------------------------
/fastface/dataset/widerface.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import List, Tuple
3 |
4 | import numpy as np
5 | from scipy.io import loadmat
6 |
7 | from ..utils.cache import get_data_cache_dir
8 | from .base import BaseDataset
9 |
10 |
11 | def _parse_annotation_file(lines: List, ranges: List) -> Tuple[List, List]:
12 | idx = 0
13 | length = len(lines)
14 |
15 | def parse_box(box):
16 | x, y, w, h = [int(b) for b in box.split(" ")[:4]]
17 | return x, y, x + w, y + h
18 |
19 | ids = []
20 | targets = []
21 | while idx < length - 1:
22 | img_file_name = lines[idx]
23 | img_idx = int(img_file_name.split("-")[0])
24 |
25 | bbox_count = int(lines[idx + 1])
26 |
27 | if bbox_count == 0:
28 | idx += 3
29 |
30 | if img_idx in ranges:
31 | ids.append(img_file_name)
32 | targets.append([])
33 | continue
34 |
35 | boxes = lines[idx + 2 : idx + 2 + bbox_count]
36 |
37 | boxes = list(map(parse_box, boxes))
38 |
39 | if img_idx in ranges:
40 | ids.append(img_file_name)
41 | targets.append(boxes)
42 | idx = idx + len(boxes) + 2
43 |
44 | return ids, targets
45 |
46 |
47 | def _get_validation_set(root_path: str, partition: str):
48 | val_mat = loadmat(
49 | os.path.join(root_path, f"eval_tools/ground_truth/wider_{partition}_val.mat")
50 | )
51 | source_image_dir = os.path.join(root_path, "WIDER_val/images")
52 | ids = []
53 | targets = []
54 | total = val_mat["file_list"].shape[0]
55 | for i in range(total):
56 | event_name = str(val_mat["event_list"][i][0][0])
57 | rows = val_mat["face_bbx_list"][i][0].shape[0]
58 | for j in range(rows):
59 | file_name = str(val_mat["file_list"][i][0][j][0][0])
60 | gt_select_ids = np.squeeze(val_mat["gt_list"][i][0][j][0])
61 | gt_boxes = val_mat["face_bbx_list"][i][0][j][0]
62 | ignore = np.ones((gt_boxes.shape[0], 1), dtype=gt_boxes.dtype)
63 |
64 | ignore[gt_select_ids - 1] = 0
65 | gt_boxes[:, [2, 3]] = gt_boxes[:, [2, 3]] + gt_boxes[:, [0, 1]]
66 | ids.append(os.path.join(source_image_dir, event_name, file_name + ".jpg"))
67 | gt_boxes = np.concatenate([gt_boxes, ignore], axis=1)
68 |
69 | mask = np.bitwise_or(
70 | gt_boxes[:, 0] >= gt_boxes[:, 2], gt_boxes[:, 1] >= gt_boxes[:, 3]
71 | )
72 | gt_boxes = gt_boxes[~mask, :]
73 |
74 | targets.append(gt_boxes)
75 |
76 | return ids, targets
77 |
78 |
79 | class WiderFaceDataset(BaseDataset):
80 | """Widerface fastface.dataset.BaseDataset Instance"""
81 |
82 | __URLS__ = {
83 | "widerface-train": {
84 | "adapter": "gdrive",
85 | "check": {
86 | "WIDER_train/images/0--Parade": "312740df0cd71f60a46867d703edd7d6"
87 | },
88 | "kwargs": {
89 | "file_id": "0B6eKvaijfFUDQUUwd21EckhUbWs",
90 | "file_name": "WIDER_train.zip",
91 | "extract": True,
92 | },
93 | },
94 | "widerface-val": {
95 | "adapter": "gdrive",
96 | "check": {"WIDER_val": "31c304a9e3b85d384f25447de1159f85"},
97 | "kwargs": {
98 | "file_id": "0B6eKvaijfFUDd3dIRmpvSk8tLUk",
99 | "file_name": "WIDER_val.zip",
100 | "extract": True,
101 | },
102 | },
103 | "widerface-annotations": {
104 | "adapter": "http",
105 | "check": {"wider_face_split": "46114d331b8081101ebd620fbfdafa7a"},
106 | "kwargs": {
107 | "url": "http://mmlab.ie.cuhk.edu.hk/projects/WIDERFace/support/bbx_annotation/wider_face_split.zip",
108 | "extract": True,
109 | },
110 | },
111 | "widerface-eval-code": {
112 | "adapter": "http",
113 | "check": {"eval_tools": "2831a12876417f414fd6017ef1e531ec"},
114 | "kwargs": {
115 | "url": "http://shuoyang1213.me/WIDERFACE/support/eval_script/eval_tools.zip",
116 | "extract": True,
117 | },
118 | },
119 | }
120 |
121 | __phases__ = ("train", "val", "test")
122 | __partitions__ = ("hard", "medium", "easy")
123 | __partition_ranges__ = (
124 | tuple(range(21)),
125 | tuple(range(21, 41)),
126 | tuple(range(41, 62)),
127 | )
128 |
129 | def __init__(
130 | self,
131 | source_dir: str = None,
132 | phase: str = None,
133 | partitions: List = None,
134 | transforms=None,
135 | **kwargs,
136 | ):
137 |
138 | source_dir = (
139 | get_data_cache_dir(suffix="widerface") if source_dir is None else source_dir
140 | )
141 |
142 | # check if download
143 | self.download(self.__URLS__, source_dir)
144 |
145 | assert os.path.exists(
146 | source_dir
147 | ), "given source directory for fddb is not exist at {}".format(source_dir)
148 | assert (
149 | phase is None or phase in WiderFaceDataset.__phases__
150 | ), "given phase {} is not \
151 | valid, must be one of: {}".format(
152 | phase, WiderFaceDataset.__phases__
153 | )
154 |
155 | if not partitions:
156 | partitions = WiderFaceDataset.__partitions__
157 |
158 | for partition in partitions:
159 | assert (
160 | partition in WiderFaceDataset.__partitions__
161 | ), "given partition {} is \
162 | not in the defined list: {}".format(
163 | partition, self.__partitions__
164 | )
165 |
166 | # TODO handle phase
167 |
168 | if phase == "train":
169 | ranges = []
170 | for partition in partitions:
171 | ranges += WiderFaceDataset.__partition_ranges__[
172 | WiderFaceDataset.__partitions__.index(partition)
173 | ]
174 | source_image_dir = os.path.join(
175 | source_dir, f"WIDER_{phase}/images"
176 | ) # TODO add assertion
177 | annotation_path = os.path.join(
178 | source_dir, f"wider_face_split/wider_face_{phase}_bbx_gt.txt"
179 | )
180 | with open(annotation_path, "r") as foo:
181 | annotations = foo.read().split("\n")
182 | raw_ids, raw_targets = _parse_annotation_file(annotations, ranges)
183 | del annotations
184 | ids = []
185 | targets = []
186 | for idx, target in zip(raw_ids, raw_targets):
187 | if len(target) == 0:
188 | continue
189 | target = np.array(target, dtype=np.float32)
190 | mask = np.bitwise_or(
191 | target[:, 0] >= target[:, 2], target[:, 1] >= target[:, 3]
192 | )
193 | target = target[~mask, :]
194 | if len(target) == 0:
195 | continue
196 | targets.append({"target_boxes": target.astype(np.float32)})
197 | ids.append(os.path.join(source_image_dir, idx))
198 | else:
199 | # TODO each targets must be dict and handle hard parameter
200 | ids, raw_targets = _get_validation_set(source_dir, partitions[0])
201 | targets = []
202 | for target in raw_targets:
203 | targets.append(
204 | {
205 | "target_boxes": target[:, :4].astype(np.float32),
206 | "ignore_flags": target[:, 4].astype(np.int32),
207 | }
208 | )
209 | del raw_targets
210 |
211 | super().__init__(ids, targets, transforms=transforms, **kwargs)
212 |
--------------------------------------------------------------------------------
/fastface/loss/__init__.py:
--------------------------------------------------------------------------------
1 | # classification losses
2 | from .focal_loss import BinaryFocalLoss
3 |
4 | # regression losses
5 | from .iou_loss import DIoULoss
6 |
7 | __all__ = ["BinaryFocalLoss", "DIoULoss"]
8 |
--------------------------------------------------------------------------------
/fastface/loss/focal_loss.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 |
4 | class BinaryFocalLoss(torch.nn.Module):
5 | """Binary Focal Loss"""
6 |
7 | def __init__(self, gamma: float = 2, alpha: float = 1, **kwargs):
8 | super().__init__()
9 | self.gamma = gamma
10 | self.alpha = alpha
11 |
12 | def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
13 | # TODO pydoc
14 | # TODO converge test
15 | # input: torch.Tensor(N,)
16 | # target: torch.Tensor(N,)
17 |
18 | probs = torch.sigmoid(input)
19 | pos_mask = target == 1
20 |
21 | # -alpha * (1 - p_t)**gamma * log(p_t)
22 | pos_loss = (
23 | -self.alpha
24 | * torch.pow(1 - probs[pos_mask], self.gamma)
25 | * torch.log(probs[pos_mask] + 1e-16)
26 | )
27 | neg_loss = (
28 | -self.alpha
29 | * torch.pow(probs[~pos_mask], self.gamma)
30 | * torch.log(1 - probs[~pos_mask] + 1e-16)
31 | )
32 |
33 | loss = torch.cat([pos_loss, neg_loss])
34 |
35 | return loss
36 |
--------------------------------------------------------------------------------
/fastface/loss/iou_loss.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 |
4 | class DIoULoss(torch.nn.Module):
5 | """DIoU loss"""
6 |
7 | def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
8 | """calculates distance IoU loss
9 |
10 | Args:
11 | input (torch.Tensor): N,4 as xmin, ymin, xmax, ymax
12 | target (torch.Tensor): N,4 as xmin, ymin, xmax, ymax
13 |
14 | Returns:
15 | torch.Tensor: loss
16 | """
17 | # TODO do not get mean
18 | dtype = input.dtype
19 | device = input.device
20 | if input.size(0) == 0:
21 | # pylint: disable=not-callable
22 | return torch.tensor(0, dtype=dtype, device=device, requires_grad=True)
23 |
24 | eps = 1e-16
25 |
26 | # intersection area
27 | intersection = (
28 | # min_x2 - max_x1
29 | (
30 | torch.min(input[:, 2], target[:, 2])
31 | - torch.max(input[:, 0], target[:, 0])
32 | ).clamp(0)
33 | *
34 | # min_y2 - max_y1
35 | (
36 | torch.min(input[:, 3], target[:, 3])
37 | - torch.max(input[:, 1], target[:, 1])
38 | ).clamp(0)
39 | )
40 |
41 | # union area
42 | input_wh = input[:, [2, 3]] - input[:, [0, 1]]
43 | target_wh = target[:, [2, 3]] - target[:, [0, 1]]
44 | union = (
45 | (input_wh[:, 0] * input_wh[:, 1])
46 | + (target_wh[:, 0] * target_wh[:, 1])
47 | - intersection
48 | )
49 |
50 | IoU = intersection / (union + eps)
51 |
52 | enclosing_box_w = torch.max(input[:, 2], target[:, 2]) - torch.min(
53 | input[:, 0], target[:, 0]
54 | )
55 | enclosing_box_h = torch.max(input[:, 3], target[:, 3]) - torch.min(
56 | input[:, 1], target[:, 1]
57 | )
58 |
59 | # convex diagonal squared
60 | c_square = enclosing_box_w ** 2 + enclosing_box_h ** 2
61 |
62 | # squared euclidian distance between box centers
63 | input_cx = (input[:, 0] + input[:, 2]) / 2
64 | input_cy = (input[:, 1] + input[:, 3]) / 2
65 | target_cx = (target[:, 0] + target[:, 2]) / 2
66 | target_cy = (target[:, 1] + target[:, 3]) / 2
67 |
68 | p_square = (input_cx - target_cx) ** 2 + (input_cy - target_cy) ** 2
69 |
70 | penalty = p_square / (c_square + eps)
71 |
72 | # DIoU loss
73 | return 1 - IoU + penalty
74 |
--------------------------------------------------------------------------------
/fastface/metric/__init__.py:
--------------------------------------------------------------------------------
1 | from .ap import AveragePrecision
2 | from .ar import AverageRecall
3 | from .widerface_ap import WiderFaceAP
4 |
5 | __all__ = ["WiderFaceAP", "AveragePrecision", "AverageRecall"]
6 |
--------------------------------------------------------------------------------
/fastface/metric/ap.py:
--------------------------------------------------------------------------------
1 | import math
2 | from typing import List, Tuple, Union
3 |
4 | import torch
5 | from torchmetrics import Metric
6 |
7 | from .functional import average_precision
8 | from .utils import generate_prediction_table
9 |
10 |
11 | class AveragePrecision(Metric):
12 | """torchmetrics.Metric instance to calculate binary average precision
13 |
14 | Args:
15 | iou_threshold (Union[List, float]): iou threshold or list of iou thresholds
16 | area (str): small or medium or large or None
17 |
18 | Returns:
19 | [type]: [description]
20 | """
21 |
22 | __areas__ = {
23 | "small": (0 ** 2, 32 ** 2),
24 | "medium": (32 ** 2, 96 ** 2),
25 | "large": (96 ** 2, math.inf),
26 | }
27 |
28 | def __init__(self, iou_threshold: Union[List, float] = 0.5, area: str = None):
29 | super().__init__(dist_sync_on_step=False)
30 | if area is None:
31 | area_range = (0, math.inf)
32 | area_name = ""
33 | else:
34 | assert area in self.__areas__, "given area is not defined"
35 | area_name = ""
36 | area_range = self.__areas__[area]
37 |
38 | self.area_name = area_name
39 | self.area_range = area_range
40 | self.iou_threshold = (
41 | iou_threshold if isinstance(iou_threshold, List) else [iou_threshold]
42 | )
43 | # [N,5 dimensional as xmin,ymin,xmax,ymax,conf]
44 | self.add_state("pred_boxes", default=[], dist_reduce_fx=None)
45 | # [M,4 dimensional as xmin,ymin,xmax,ymax]
46 | self.add_state("target_boxes", default=[], dist_reduce_fx=None)
47 |
48 | # pylint: disable=method-hidden
49 | def update(self, preds: List[torch.Tensor], targets: List[torch.Tensor], **kwargs):
50 | """
51 | Args:
52 | preds (List[torch.Tensor]): [N,5 dimensional as xmin,ymin,xmax,ymax,conf]
53 | targets (List[torch.Tensor]): [M,4 dimensional as xmin,ymin,xmax,ymax]
54 | """
55 | # pylint: disable=no-member
56 | if isinstance(preds, List):
57 | self.pred_boxes += preds
58 | else:
59 | self.pred_boxes.append(preds)
60 |
61 | if isinstance(targets, List):
62 | self.target_boxes += targets
63 | else:
64 | self.target_boxes.append(targets)
65 |
66 | # pylint: disable=method-hidden
67 | def compute(self):
68 | """Calculates average precision"""
69 | # pylint: disable=no-member
70 |
71 | return average_precision(
72 | self.pred_boxes,
73 | self.target_boxes,
74 | iou_thresholds=self.iou_threshold,
75 | area_range=self.area_range,
76 | )
77 |
78 | def get_precision_recall_curve(self) -> Tuple[torch.Tensor, torch.Tensor]:
79 | table = generate_prediction_table(self.preds_boxes, self.targets_boxes)
80 | # table: List[torch.Tensor] as IoU | Conf | Area | Best | Target Idx
81 | table = torch.cat(table, dim=0)
82 | # table: torch.Tensor(N, 5) as IoU | Conf | Area | Best | Target Idx
83 |
84 | if table.size(0) == 0:
85 | # pylint: disable=not-callable
86 | return torch.tensor([0], dtype=torch.float32)
87 |
88 | # sort table by confidance scores
89 | table = table[table[:, 1].argsort(descending=True), :]
90 |
91 | N = table.size(0)
92 | M = sum([target.size(0) for target in self.targets_boxes])
93 |
94 | # set as fp if lower than iou threshold
95 | # ! mean value will be used for iou threshold
96 | iou_threshold = sum(self.iou_threshold) / len(self.iou_threshold)
97 | table[table[:, 0] < iou_threshold, 3] = 0.0
98 |
99 | accumulated_tp = torch.cumsum(table[:, 3], dim=0)
100 |
101 | precision = accumulated_tp / torch.arange(1, N + 1, dtype=torch.float32)
102 | recall = accumulated_tp / (M + 1e-16)
103 |
104 | return precision, recall
105 |
--------------------------------------------------------------------------------
/fastface/metric/ar.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | import torch
4 | from torchmetrics import Metric
5 | from scipy.optimize import linear_sum_assignment
6 |
7 | from ..utils.box import jaccard_vectorized
8 |
9 |
10 | class AverageRecall(Metric):
11 | r"""torchmetrics.Metric instance to calculate average recall
12 |
13 | .. math::
14 | AR = 2 \times \int_\text{iou_threshold_min}^\text{iou_threshold_max} recall(o)do
15 |
16 | Args:
17 | iou_threshold_min (float, optional): minimum threshold for IoU. Defaults to 0.5.
18 | iou_threshold_max (float, optional): maximum threshold for IoU. Defaults to 1.0.
19 |
20 | Refs:
21 | https://arxiv.org/pdf/1502.05082.pdf
22 | """
23 |
24 | def __init__(self, iou_threshold_min: float = 0.5, iou_threshold_max: float = 1.0):
25 | super().__init__(dist_sync_on_step=False)
26 |
27 | assert (
28 | iou_threshold_max >= iou_threshold_min
29 | ), "max value must be greater or equal than min value"
30 |
31 | if iou_threshold_max == iou_threshold_min:
32 | # single threshold
33 | self.thresholds = torch.tensor(
34 | [iou_threshold_max]
35 | ) # pylint: disable=not-callable
36 | else:
37 | # multi thresholds
38 | self.thresholds = torch.arange(iou_threshold_min, iou_threshold_max, 0.01)
39 |
40 | self.iou_threshold_min = iou_threshold_min
41 | self.iou_threshold_max = iou_threshold_max
42 | self.add_state("ious", default=[], dist_reduce_fx=None)
43 |
44 | def update(self, preds: List[torch.Tensor], targets: List[torch.Tensor], **kwargs):
45 | """
46 | Arguments:
47 | preds [List]: [N,5 dimensional as xmin,ymin,xmax,ymax,score]
48 | targets [List]: [M,4 dimensional as xmin,ymin,xmax,ymax]
49 | """
50 | # TODO this might speed down the pipeline
51 |
52 | if not isinstance(preds, List):
53 | preds = [preds]
54 |
55 | if not isinstance(targets, List):
56 | targets = [targets]
57 |
58 | ious = []
59 | for pred, gt in zip(preds, targets):
60 | # pred: N,5 as xmin, ymin, xmax, ymax, score
61 | # gt: M,4 as xmin, ymin, xmax, ymax
62 |
63 | N = pred.size(0)
64 | M = gt.size(0)
65 |
66 | if M == 0:
67 | continue
68 |
69 | if N == 0:
70 | [ious.append(0.0) for _ in range(M)]
71 | continue
72 |
73 | # N,M
74 | iou = jaccard_vectorized(pred[:, :4], gt[:, :4])
75 | select_i, select_j = linear_sum_assignment(-1 * iou.numpy())
76 | select_i = torch.tensor(select_i) # pylint: disable=not-callable
77 | select_j = torch.tensor(select_j) # pylint: disable=not-callable
78 | ious += iou[select_i, select_j].tolist()
79 |
80 | # pylint: disable=no-member
81 | self.ious += ious
82 |
83 | def compute(self):
84 | """Calculates average recall"""
85 |
86 | # pylint: disable=no-member
87 | # pylint: disable=not-callable
88 | ious = torch.tensor(self.ious, dtype=torch.float32)
89 |
90 | recalls = []
91 |
92 | for th in self.thresholds:
93 | mask = ious >= th
94 | recalls.append(ious[mask].size(0) / (ious.size(0) + 1e-16))
95 |
96 | recalls = torch.tensor(recalls)
97 |
98 | if self.iou_threshold_min == self.iou_threshold_max:
99 | return recalls.mean()
100 |
101 | average_recall = torch.trapz(recalls, self.thresholds) / (
102 | self.iou_threshold_max - self.iou_threshold_min
103 | )
104 |
105 | return average_recall
106 |
--------------------------------------------------------------------------------
/fastface/metric/functional/__init__.py:
--------------------------------------------------------------------------------
1 | from .ap import average_precision
2 |
3 | __all__ = ["average_precision"]
4 |
--------------------------------------------------------------------------------
/fastface/metric/functional/ap.py:
--------------------------------------------------------------------------------
1 | import math
2 | from typing import List, Tuple
3 |
4 | import torch
5 |
6 | from ..utils import generate_prediction_table
7 |
8 |
9 | def average_precision(
10 | predictions: List,
11 | targets: List,
12 | iou_thresholds: List[float] = [0.5],
13 | area_range: Tuple[int, int] = (0, math.inf),
14 | ) -> torch.Tensor:
15 | """Calculates average precision for given inputs
16 |
17 | Args:
18 | predictions (List): [N,5 dimensional as xmin,ymin,xmax,ymax,conf]
19 | targets (List): [M,4 dimensional as xmin,ymin,xmax,ymax]
20 | iou_thresholds (List[float], optional): list of iou thresholds. Defaults to [0.5].
21 | area_range (Tuple[int, int], optional): min box area and max box area. Defaults to (0, math.inf).
22 |
23 | Returns:
24 | torch.Tensor: average precision
25 | """
26 | assert len(predictions) == len(
27 | targets
28 | ), "prediction and ground truths must be equal in lenght"
29 | assert len(predictions) > 0, "given input list lenght must be greater than 0"
30 |
31 | table = generate_prediction_table(predictions, targets)
32 | # table: List[torch.Tensor] as IoU | Conf | Area | Best | Target Idx
33 | table = torch.cat(table, dim=0)
34 | # table: torch.Tensor(N, 5) as IoU | Conf | Area | Best | Target Idx
35 |
36 | if table.size(0) == 0:
37 | # pylint: disable=not-callable
38 | return torch.tensor([0], dtype=torch.float32)
39 |
40 | # sort table by confidance scores
41 | table = table[table[:, 1].argsort(descending=True), :]
42 |
43 | # filter by area
44 | # TODO handle if area is 0
45 | mask = (table[:, 2] > area_range[0]) & (table[:, 2] < area_range[1])
46 | table = table[mask, :]
47 |
48 | N = table.size(0)
49 |
50 | if N == 0:
51 | # pylint: disable=not-callable
52 | return torch.tensor([0], dtype=torch.float32)
53 |
54 | # TODO make it better
55 | all_targets = torch.cat(targets, dim=0)
56 | areas = (all_targets[:, 2] - all_targets[:, 0]) * (
57 | all_targets[:, 3] - all_targets[:, 1]
58 | )
59 | mask = (areas > area_range[0]) & (areas < area_range[1])
60 | M = areas[mask].size(0)
61 |
62 | aps = []
63 |
64 | # for each iou threshold
65 | for iou_threshold in iou_thresholds:
66 | ntable = table.clone()
67 | # set as fp if lower than iou threshold
68 | ntable[ntable[:, 0] < iou_threshold, 3] = 0.0
69 |
70 | accumulated_tp = torch.cumsum(ntable[:, 3], dim=0)
71 |
72 | precision = accumulated_tp / torch.arange(1, N + 1, dtype=torch.float32)
73 | recall = accumulated_tp / (M + 1e-16)
74 |
75 | unique_recalls = recall.unique_consecutive()
76 | auc = torch.empty(unique_recalls.size(0), dtype=torch.float32)
77 | # pylint: disable=not-callable
78 | last_value = torch.tensor(0, dtype=torch.float32)
79 |
80 | for i, recall_value in enumerate(unique_recalls):
81 | mask = recall == recall_value # N,
82 | p_mul = precision[mask].max() # get max p
83 | auc[i] = p_mul * (recall_value - last_value)
84 | last_value = recall_value
85 | aps.append(auc.sum())
86 | return sum(aps) / len(aps)
87 |
--------------------------------------------------------------------------------
/fastface/metric/utils.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | import torch
4 |
5 | from ..utils.box import jaccard_vectorized
6 |
7 |
8 | def generate_prediction_table(
9 | preds: List[torch.Tensor], targets: List[torch.Tensor]
10 | ) -> List[torch.Tensor]:
11 | """Generates prediction table
12 |
13 | Args:
14 | preds (List[torch.Tensor]): list of predictions as [torch.Tensor(N,5), ...] xmin,ymin,xmax,ymax,score
15 | targets (List[torch.Tensor]): list of targets as [torch.Tensor(M,4), ...] xmin,ymin,xmax,ymax
16 |
17 | Returns:
18 | List[torch.Tensor]: list of table as [torch.Tensor(N,4), ...] IoU, Conf, Area, Best, Target Idx
19 | """
20 | assert len(preds) == len(targets), "length of predictions and targets must be same"
21 |
22 | table = []
23 | # IoU | Conf | Area | Best
24 | for pred, gt in zip(preds, targets):
25 | N = pred.size(0)
26 | M = gt.size(0)
27 |
28 | if M == 0:
29 | single_table = torch.zeros(N, 5)
30 | single_table[:, 4] = -1
31 | # IoU | Conf | Area | Best | GT IDX
32 | single_table[:, 1] = pred[:, 4]
33 | table.append(single_table)
34 | continue
35 | elif N == 0:
36 | continue
37 |
38 | ious = jaccard_vectorized(pred[:, :4], gt)
39 | # ious: N x M
40 | iou_vals, match_ids = torch.max(ious, dim=1)
41 | # iou_vals: N,
42 | # match_ids: N,
43 |
44 | best_matches = torch.zeros(N, dtype=torch.long)
45 | # best_matches: N,
46 |
47 | areas = (gt[:, 2] - gt[:, 0]) * (gt[:, 3] - gt[:, 1])
48 |
49 | single_table = torch.stack(
50 | [iou_vals, pred[:, 4], areas[match_ids], best_matches, match_ids.float()],
51 | dim=1,
52 | )
53 |
54 | single_table = single_table[single_table[:, 0].argsort(dim=0, descending=True)]
55 | for gt_idx in range(M):
56 | (match_ids,) = torch.where(single_table[:, 4] == gt_idx)
57 | if match_ids.size(0) == 0:
58 | continue
59 | # set best
60 | single_table[match_ids[0], 3] = 1
61 |
62 | table.append(single_table)
63 |
64 | return table
65 |
--------------------------------------------------------------------------------
/fastface/metric/widerface_ap.py:
--------------------------------------------------------------------------------
1 | from typing import List, Tuple
2 |
3 | import numpy as np
4 | import torch
5 | import torchvision.ops.boxes as box_ops
6 | from torchmetrics import Metric
7 |
8 |
9 | class WiderFaceAP(Metric):
10 | """torchmetrics.Metric instance to calculate widerface average precision
11 |
12 | Args:
13 | iou_threshold (float): widerface AP score IoU threshold, default is 0.5
14 |
15 | """
16 |
17 | # this implementation heavily inspired by: https://github.com/wondervictor/WiderFace-Evaluation
18 |
19 | def __init__(self, iou_threshold: float = 0.5):
20 | super().__init__(dist_sync_on_step=False)
21 |
22 | self.iou_threshold = iou_threshold
23 | self.threshold_steps = 1000
24 | self.add_state("pred_boxes", default=[], dist_reduce_fx=None)
25 | self.add_state("gt_boxes", default=[], dist_reduce_fx=None)
26 | self.add_state("ignore_flags", default=[], dist_reduce_fx=None)
27 |
28 | def update(
29 | self,
30 | preds: List[torch.Tensor],
31 | targets: List[torch.Tensor],
32 | ignore_flags: List[torch.Tensor] = None,
33 | **kwargs
34 | ):
35 | """
36 | Arguments:
37 | preds [List]: [Ni,5 dimensional as xmin,ymin,xmax,ymax,conf]
38 | targets [List]: [Ni,5 dimensional as xmin,ymin,xmax,ymax]
39 | ignore_flags [List]: [Ni, dimensional]
40 | """
41 | # pylint: disable=no-member
42 | if isinstance(preds, List):
43 | self.pred_boxes += preds
44 | else:
45 | self.pred_boxes.append(preds)
46 |
47 | if isinstance(ignore_flags, List):
48 | self.ignore_flags += ignore_flags
49 | else:
50 | self.ignore_flags.append(ignore_flags)
51 |
52 | if isinstance(targets, List):
53 | self.gt_boxes += targets
54 | else:
55 | self.gt_boxes.append(targets)
56 |
57 | def compute(self) -> float:
58 | # pylint: disable=no-member
59 | curve = np.zeros((self.threshold_steps, 2), dtype=np.float32)
60 |
61 | normalized_preds = self.normalize_scores(
62 | [pred.float().cpu().numpy() for pred in self.pred_boxes]
63 | )
64 |
65 | gt_boxes = [gt_boxes.cpu().float().numpy() for gt_boxes in self.gt_boxes]
66 |
67 | ignore_flags = [ignore_flag.cpu().numpy() for ignore_flag in self.ignore_flags]
68 |
69 | total_faces = 0
70 |
71 | for preds, gts, i_flags in zip(normalized_preds, gt_boxes, ignore_flags):
72 | # skip if no gts
73 | if gts.shape[0] == 0:
74 | continue
75 |
76 | # count keeped gts
77 | total_faces += (i_flags == 0).sum()
78 |
79 | if preds.shape[0] == 0:
80 | continue
81 | # gts: M,4 as x1,y1,x2,y2
82 | # preds: N,5 as x1,y1,x2,y2,norm_score
83 |
84 | # sort preds
85 | preds = preds[(-preds[:, -1]).argsort(), :]
86 |
87 | # evaluate single image
88 | match_counts, ignore_pred_mask = self.evaluate_single_image(
89 | preds, gts, i_flags
90 | )
91 | # match_counts: N,
92 | # ignore_pred_mask: N,
93 |
94 | # calculate image pr
95 | curve += self.calculate_image_pr(preds, ignore_pred_mask, match_counts)
96 |
97 | for i in range(self.threshold_steps):
98 | curve[i, 0] = curve[i, 1] / curve[i, 0]
99 | curve[i, 1] = curve[i, 1] / total_faces
100 |
101 | propose = curve[:, 0]
102 | recall = curve[:, 1]
103 |
104 | # add sentinel values at the end
105 | # [0] + propose + [0]
106 | propose = np.concatenate([[0.0], propose, [0.0]])
107 |
108 | # [0] + propose + [1]
109 | recall = np.concatenate([[0.0], recall, [1.0]])
110 |
111 | # compute the precision envelope
112 | for i in range(propose.shape[0] - 1, 0, -1):
113 | propose[i - 1] = max(propose[i - 1], propose[i])
114 |
115 | # to calculate area under PR curve, look for points
116 | # where X axis (recall) changes value
117 | (points,) = np.where(recall[1:] != recall[:-1])
118 |
119 | # and sum (\Delta recall) * prec
120 | ap = ((recall[points + 1] - recall[points]) * propose[points + 1]).sum()
121 |
122 | return ap
123 |
124 | @staticmethod
125 | def normalize_scores(batch_preds: List[np.ndarray]) -> List[np.ndarray]:
126 | """[summary]
127 |
128 | Args:
129 | preds (List[np.ndarray]): [description]
130 |
131 | Returns:
132 | List[np.ndarray]: [description]
133 | """
134 | norm_preds = []
135 | max_score = 0
136 | min_score = 1
137 | for preds in batch_preds:
138 | if preds.shape[0] == 0:
139 | continue
140 |
141 | min_score = min(preds[:, -1].min(), min_score)
142 | max_score = max(preds[:, -1].max(), max_score)
143 |
144 | d = max_score - min_score
145 |
146 | for preds in batch_preds:
147 | n_preds = preds.copy()
148 | if preds.shape[0] == 0:
149 | norm_preds.append(n_preds)
150 | continue
151 | n_preds[:, -1] = (n_preds[:, -1] - min_score) / d
152 | norm_preds.append(n_preds)
153 | return norm_preds
154 |
155 | def evaluate_single_image(
156 | self, preds: np.ndarray, gts: np.ndarray, ignore: np.ndarray
157 | ) -> Tuple[np.ndarray, np.ndarray]:
158 |
159 | N = preds.shape[0]
160 | M = gts.shape[0]
161 |
162 | ious = box_ops.box_iou(
163 | # pylint: disable=not-callable
164 | torch.tensor(preds[:, :4], dtype=torch.float32),
165 | # pylint: disable=not-callable
166 | torch.tensor(gts, dtype=torch.float32),
167 | ).numpy()
168 | # ious: N,M
169 |
170 | ignore_pred_mask = np.zeros((N,), dtype=np.float32)
171 | gt_match_mask = np.zeros((M,), dtype=np.float32)
172 | match_counts = np.zeros((N,), dtype=np.float32)
173 |
174 | for i in range(N):
175 | max_iou, max_iou_idx = ious[i, :].max(), ious[i, :].argmax()
176 |
177 | if max_iou >= self.iou_threshold:
178 | if ignore[max_iou_idx] == 1: # if matched gt is ignored
179 | ignore_pred_mask[i] = 1 # set prediction to be ignored later
180 | gt_match_mask[max_iou_idx] = -1 # set gt match as ignored
181 | elif gt_match_mask[max_iou_idx] == 0: # if matched gt is not ignored
182 | gt_match_mask[max_iou_idx] = 1 # set match as positive
183 |
184 | # count each positive match
185 | match_counts[i] = (gt_match_mask == 1).sum()
186 |
187 | return match_counts, ignore_pred_mask
188 |
189 | def calculate_image_pr(
190 | self, preds: np.ndarray, ignore_pred_mask: np.ndarray, match_counts: np.ndarray
191 | ) -> np.ndarray:
192 |
193 | pr = np.zeros((self.threshold_steps, 2), dtype=np.float32)
194 | thresholds = np.arange(0, self.threshold_steps, dtype=np.float32)
195 | thresholds = 1 - (thresholds + 1) / self.threshold_steps
196 |
197 | for i, threshold in enumerate(thresholds):
198 |
199 | (pos_ids,) = np.where(preds[:, 4] >= threshold)
200 | if len(pos_ids) == 0:
201 | pr[i, 0] = 0
202 | pr[i, 1] = 0
203 | else:
204 | pos_ids = pos_ids[-1]
205 | (p_index,) = np.where(ignore_pred_mask[: pos_ids + 1] == 0)
206 | pr[i, 0] = len(p_index)
207 | pr[i, 1] = match_counts[pos_ids]
208 | return pr
209 |
--------------------------------------------------------------------------------
/fastface/module.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Dict, List, Union
3 |
4 | import numpy as np
5 | import pytorch_lightning as pl
6 | import torchmetrics
7 | import torch
8 | import torch.nn as nn
9 | import yaml
10 |
11 | from . import api, utils
12 |
13 |
14 | class FaceDetector(pl.LightningModule):
15 | """Generic pl.LightningModule definition for face detection"""
16 |
17 | def __init__(
18 | self,
19 | arch: nn.Module = None,
20 | normalized_input: bool = False,
21 | mean: Union[float, List] = 0.0,
22 | std: Union[float, List] = 1.0,
23 | hparams: Dict = None,
24 | ):
25 | super().__init__()
26 | self.save_hyperparameters(hparams)
27 | self.arch = arch
28 | self.__metrics = {}
29 |
30 | self.init_preprocess(mean=mean, std=std, normalized_input=normalized_input)
31 |
32 | def init_preprocess(
33 | self,
34 | normalized_input: bool = False,
35 | mean: Union[float, List] = 0.0,
36 | std: Union[float, List] = 1.0,
37 | ):
38 |
39 | # preprocess
40 | if isinstance(mean, list):
41 | assert len(mean) == 3, "mean dimension must be 3 not {}".format(len(mean))
42 | mean = [float(m) for m in mean]
43 | else:
44 | mean = [float(mean) for _ in range(3)]
45 |
46 | if isinstance(std, list):
47 | assert len(std) == 3, "std dimension must be 3 not {}".format(len(std))
48 | std = [float(m) for m in std]
49 | else:
50 | std = [float(std) for _ in range(3)]
51 |
52 | self.register_buffer(
53 | "normalizer",
54 | torch.tensor(255.0)
55 | if normalized_input
56 | else torch.tensor(1.0), # pylint: disable=not-callable
57 | persistent=False,
58 | )
59 |
60 | self.register_buffer(
61 | "mean",
62 | torch.tensor(mean)
63 | .view(-1, 1, 1)
64 | .contiguous(), # pylint: disable=not-callable
65 | persistent=False,
66 | )
67 |
68 | self.register_buffer(
69 | "std",
70 | torch.tensor(std)
71 | .view(-1, 1, 1)
72 | .contiguous(), # pylint: disable=not-callable
73 | persistent=False,
74 | )
75 |
76 | def add_metric(self, name: str, metric: torchmetrics.Metric):
77 | """Adds given metric with name key
78 |
79 | Args:
80 | name (str): name of the metric
81 | metric (torchmetrics.Metric): Metric object
82 | """
83 | # TODO add warnings if override happens
84 | self.__metrics[name] = metric
85 |
86 | def get_metrics(self) -> Dict[str, torchmetrics.Metric]:
87 | """Return metrics defined in the `FaceDetector` instance
88 |
89 | Returns:
90 | Dict[str, torchmetrics.Metric]: defined model metrics with names
91 | """
92 | return {k: v for k, v in self.__metrics.items()}
93 |
94 | @torch.jit.unused
95 | def predict(
96 | self,
97 | data: Union[np.ndarray, List],
98 | target_size: int = None,
99 | det_threshold: float = 0.4,
100 | iou_threshold: float = 0.4,
101 | keep_n: int = 200,
102 | ):
103 | """Performs face detection using given image or images
104 |
105 | Args:
106 | data (Union[np.ndarray, List]): numpy RGB image or list of RGB images
107 | target_size (int): if given than images will be up or down sampled using `target_size`, Default: None
108 | det_threshold (float): detection score threshold, Default: 0.4
109 | iou_threshold (float): iou value threshold for nms, Default: 0.4
110 | keep_n (int): describes how many prediction will be selected for each batch, Default: 200
111 |
112 | Returns:
113 | List: prediction result as list of dictionaries.
114 | [
115 | # single image results
116 | {
117 | "boxes": , # List[List[xmin, ymin, xmax, ymax]]
118 | "scores": # List[float]
119 | },
120 | ...
121 | ]
122 | >>> import fastface as ff
123 | >>> import imageio
124 | >>> model = ff.FaceDetector.from_pretrained('lffd_original').eval()
125 | >>> img = imageio.imread('resources/friends.jpg')[:,:,:3]
126 | >>> model.predict(img, target_size=640)
127 | [{'boxes': [[1057, 181, 1186, 352], [558, 220, 703, 400], [141, 218, 270, 382], [866, 271, 976, 403], [327, 252, 442, 392]], 'scores': [0.9992017149925232, 0.9973644614219666, 0.9416095018386841, 0.8408345580101013, 0.7937759160995483]}]
128 | """
129 |
130 | batch = self.to_tensor(data)
131 | # batch: list of tensors
132 |
133 | batch_size = len(batch)
134 |
135 | batch, scales, paddings = utils.preprocess.prepare_batch(
136 | batch, target_size=target_size, adaptive_batch=target_size is None
137 | )
138 | # batch: torch.Tensor(B,C,T,T)
139 | # scales: torch.Tensor(B,)
140 | # paddings: torch.Tensor(B,4) as pad (left, top, right, bottom)
141 |
142 | preds = self.forward(
143 | batch,
144 | det_threshold=det_threshold,
145 | iou_threshold=iou_threshold,
146 | keep_n=keep_n,
147 | )
148 | # preds: torch.Tensor(B, N, 6) as x1,y1,x2,y2,score,batch_idx
149 |
150 | preds = [preds[preds[:, 5] == batch_idx, :5] for batch_idx in range(batch_size)]
151 | # preds: list of torch.Tensor(N, 5) as x1,y1,x2,y2,score
152 |
153 | preds = utils.preprocess.adjust_results(preds, scales, paddings)
154 | # preds: list of torch.Tensor(N, 5) as x1,y1,x2,y2,score
155 |
156 | preds = self.to_json(preds)
157 | # preds: list of {"boxes": [(x1,y1,x2,y2), ...], "score": [, ...]}
158 |
159 | return preds
160 |
161 | # ! use forward only for inference not training
162 | def forward(
163 | self,
164 | batch: torch.Tensor,
165 | det_threshold: float = 0.3,
166 | iou_threshold: float = 0.4,
167 | keep_n: int = 200,
168 | ) -> torch.Tensor:
169 | """batch of images with float and B x C x H x W shape
170 |
171 | Args:
172 | batch (torch.Tensor): torch.FloatTensor(B x C x H x W)
173 |
174 | Returns:
175 | torch.Tensor: preds with shape (N, 6);
176 | (1:4) xmin, ymin, xmax, ymax
177 | (4:5) score
178 | (5:6) batch idx
179 | """
180 |
181 | # apply preprocess
182 | batch = ((batch / self.normalizer) - self.mean) / self.std
183 |
184 | # get logits
185 | with torch.no_grad():
186 | logits = self.arch.forward(batch)
187 | # logits, any
188 |
189 | preds = self.arch.logits_to_preds(logits)
190 | # preds: torch.Tensor(B, N, 5)
191 |
192 | batch_preds = self._postprocess(
193 | preds,
194 | det_threshold=det_threshold,
195 | iou_threshold=iou_threshold,
196 | keep_n=keep_n,
197 | )
198 |
199 | return batch_preds
200 |
201 | def _postprocess(
202 | self,
203 | preds: torch.Tensor,
204 | det_threshold: float = 0.3,
205 | iou_threshold: float = 0.4,
206 | keep_n: int = 200,
207 | ) -> torch.Tensor:
208 |
209 | # TODO pydoc
210 | batch_size = preds.size(0)
211 |
212 | # filter out some predictions using score
213 | pick_b, pick_n = torch.where(preds[:, :, 4] >= det_threshold)
214 |
215 | # add batch_idx dim to preds
216 | preds = torch.cat(
217 | [preds[pick_b, pick_n, :], pick_b.to(preds.dtype).unsqueeze(1)], dim=1
218 | )
219 | # preds: N x 6
220 |
221 | batch_preds: List[torch.Tensor] = []
222 | for batch_idx in range(batch_size):
223 | (pick_n,) = torch.where(batch_idx == preds[:, 5])
224 | order = preds[pick_n, 4].sort(descending=True)[1]
225 |
226 | batch_preds.append(
227 | # preds: n, 6
228 | preds[pick_n, :][order][:keep_n, :]
229 | )
230 |
231 | batch_preds = torch.cat(batch_preds, dim=0)
232 | # batch_preds: N x 6
233 |
234 | # filter with nms
235 | pick = utils.box.batched_nms(
236 | batch_preds[:, :4], # boxes as x1,y1,x2,y2
237 | batch_preds[:, 4], # det score between [0, 1]
238 | batch_preds[:, 5], # id of the batch that prediction belongs to
239 | iou_threshold=iou_threshold,
240 | )
241 |
242 | # select picked preds
243 | batch_preds = batch_preds[pick, :]
244 | # batch_preds: N x 6
245 | return batch_preds
246 |
247 | def training_step(self, batch, batch_idx):
248 | batch, targets = batch
249 |
250 | # apply preprocess
251 | batch = ((batch / self.normalizer) - self.mean) / self.std
252 |
253 | # compute logits
254 | logits = self.arch.forward(batch)
255 |
256 | # compute loss
257 | loss = self.arch.compute_loss(logits, targets, hparams=self.hparams["hparams"])
258 | # loss: dict of losses or loss
259 |
260 | return loss
261 |
262 | def training_epoch_end(self, outputs):
263 | losses = {}
264 | for output in outputs:
265 | if isinstance(output, dict):
266 | for k, v in output.items():
267 | if k not in losses:
268 | losses[k] = []
269 | losses[k].append(v)
270 | else:
271 | # only contains `loss`
272 | if "loss" not in losses:
273 | losses["loss"] = []
274 | losses["loss"].append(output)
275 |
276 | for name, loss in losses.items():
277 | self.log("{}/training".format(name), sum(loss) / len(loss))
278 |
279 | def on_validation_epoch_start(self):
280 | for metric in self.__metrics.values():
281 | metric.reset()
282 |
283 | def validation_step(self, batch, batch_idx, *args):
284 | batch, targets = batch
285 | batch_size = batch.size(0)
286 |
287 | # apply preprocess
288 | batch = ((batch / self.normalizer) - self.mean) / self.std
289 |
290 | with torch.no_grad():
291 | # compute logits
292 | logits = self.arch.forward(batch)
293 |
294 | # compute loss
295 | loss = self.arch.compute_loss(
296 | logits, targets, hparams=self.hparams["hparams"]
297 | )
298 | # loss: dict of losses or loss
299 |
300 | # logits to preds
301 | preds = self.arch.logits_to_preds(logits)
302 | # preds: torch.Tensor(B, N, 5)
303 |
304 | # postprocess predictions
305 | preds = self._postprocess(
306 | preds, det_threshold=0.1, iou_threshold=0.4, keep_n=500
307 | )
308 | # preds: N,6 as x1,y1,x2,y2,score,batch_idx
309 |
310 | batch_preds = [
311 | preds[preds[:, 5] == batch_idx][:, :5].cpu()
312 | for batch_idx in range(batch_size)
313 | ]
314 |
315 | kwargs = {}
316 | batch_gt_boxes = []
317 | for target in targets:
318 | for k, v in target.items():
319 | if isinstance(v, torch.Tensor):
320 | v = v.cpu()
321 | if k == "target_boxes":
322 | batch_gt_boxes.append(v.cpu())
323 | else:
324 | if k not in kwargs:
325 | kwargs[k] = []
326 | kwargs[k].append(v)
327 |
328 | # TODO handle here
329 | # self.debug_step(batch, batch_preds, batch_gt_boxes)
330 |
331 | for metric in self.__metrics.values():
332 | metric.update(batch_preds, batch_gt_boxes, **kwargs)
333 |
334 | return loss
335 |
336 | def debug_step(self, batch, batch_preds, batch_gt_boxes):
337 | from PIL import Image
338 |
339 | for img, preds, gt_boxes in zip(batch, batch_preds, batch_gt_boxes):
340 | img = (img.permute(1, 2, 0).cpu() * 255).numpy().astype(np.uint8)
341 | preds = preds.cpu().long().numpy()
342 | gt_boxes = gt_boxes.cpu().long().numpy()
343 | img = utils.vis.draw_rects(img, preds[:, :4], color=(255, 0, 0))
344 | img = utils.vis.draw_rects(img, gt_boxes[:, :4], color=(0, 255, 0))
345 | pil_img = Image.fromarray(img)
346 | pil_img.show()
347 | if input("press `q` to exit") == "q":
348 | exit(0)
349 |
350 | def validation_epoch_end(self, outputs):
351 | losses = {}
352 | for output in outputs:
353 | if isinstance(output, dict):
354 | for k, v in output.items():
355 | if k not in losses:
356 | losses[k] = []
357 | losses[k].append(v)
358 | else:
359 | # only contains `loss`
360 | if "loss" not in losses:
361 | losses["loss"] = []
362 | losses["loss"].append(output)
363 |
364 | for name, loss in losses.items():
365 | self.log("{}/validation".format(name), sum(loss) / len(loss))
366 |
367 | for name, metric in self.__metrics.items():
368 | self.log("metrics/{}".format(name), metric.compute())
369 |
370 | def on_test_epoch_start(self):
371 | for metric in self.__metrics.values():
372 | metric.reset()
373 |
374 | def test_step(self, batch, batch_idx):
375 | batch, targets = batch
376 | batch_size = batch.size(0)
377 |
378 | # compute preds
379 | preds = self.forward(batch, det_threshold=0.1, iou_threshold=0.4, keep_n=10000)
380 | # preds: N,6 as x1,y1,x2,y2,score,batch_idx
381 |
382 | batch_preds = [
383 | preds[preds[:, 5] == batch_idx][:, :5].cpu()
384 | for batch_idx in range(batch_size)
385 | ]
386 | kwargs = {}
387 | batch_gt_boxes = []
388 | for target in targets:
389 | for k, v in target.items():
390 | if isinstance(v, torch.Tensor):
391 | v = v.cpu()
392 | if k == "target_boxes":
393 | batch_gt_boxes.append(v.cpu())
394 | else:
395 | if k not in kwargs:
396 | kwargs[k] = []
397 | kwargs[k].append(v)
398 |
399 | for metric in self.__metrics.values():
400 | metric.update(batch_preds, batch_gt_boxes, **kwargs)
401 |
402 | def test_epoch_end(self, _):
403 | metric_results = {}
404 | for name, metric in self.__metrics.items():
405 | metric_results[name] = metric.compute()
406 | return metric_results
407 |
408 | def configure_optimizers(self):
409 | return self.arch.configure_optimizers(hparams=self.hparams["hparams"])
410 |
411 | @classmethod
412 | def build_from_yaml(cls, yaml_file_path: str) -> pl.LightningModule:
413 | """Classmethod for creating `fastface.FaceDetector` instance from scratch using yaml file
414 |
415 | Args:
416 | yaml_file_path (str): yaml file path
417 |
418 | Returns:
419 | pl.LightningModule: fastface.FaceDetector instance with random weights initialization
420 | """
421 |
422 | assert os.path.isfile(
423 | yaml_file_path
424 | ), "could not find the yaml file given {}".format(yaml_file_path)
425 | with open(yaml_file_path, "r") as foo:
426 | yaml_config = yaml.load(foo, Loader=yaml.FullLoader)
427 |
428 | assert "arch" in yaml_config, "yaml file must contain `arch` key"
429 | assert "config" in yaml_config, "yaml file must contain `config` key"
430 |
431 | arch = yaml_config["arch"]
432 | config = yaml_config["config"]
433 | preprocess = yaml_config.get(
434 | "preprocess", {"mean": 0.0, "std": 1.0, "normalized_input": True}
435 | )
436 | hparams = yaml_config.get("hparams", {})
437 |
438 | return cls.build(arch, config, **preprocess, hparams=hparams)
439 |
440 | @classmethod
441 | def build(
442 | cls,
443 | arch: str,
444 | config: Union[str, Dict],
445 | preprocess: Dict = {"mean": 0.0, "std": 1.0, "normalized_input": True},
446 | hparams: Dict = {},
447 | **kwargs,
448 | ) -> pl.LightningModule:
449 | """Classmethod for creating `fastface.FaceDetector` instance from scratch
450 |
451 | Args:
452 | arch (str): architecture name
453 | config (Union[str, Dict]): configuration name or configuration dictionary
454 | preprocess (Dict, optional): preprocess arguments of the module. Defaults to {"mean": 0, "std": 1, "normalized_input": True}.
455 | hparams (Dict, optional): hyper parameters for the model. Defaults to {}.
456 |
457 | Returns:
458 | pl.LightningModule: fastface.FaceDetector instance with random weights initialization
459 | """
460 | assert isinstance(preprocess, dict), "preprocess must be dict, not {}".format(
461 | type(preprocess)
462 | )
463 |
464 | # get architecture nn.Module class
465 | arch_cls = utils.config.get_arch_cls(arch)
466 |
467 | # check config
468 | if isinstance(config, str):
469 | config = api.get_arch_config(arch, config)
470 |
471 | # build nn.Module with given configuration
472 | arch_module = arch_cls(config=config, **kwargs)
473 |
474 | module_params = {}
475 |
476 | # add hparams
477 | module_params.update({"hparams": hparams})
478 |
479 | # add preprocess to the hparams
480 | module_params.update({"preprocess": preprocess})
481 |
482 | # add config and arch information to the hparams
483 | module_params.update({"config": config, "arch": arch})
484 |
485 | # add kwargs to the hparams
486 | module_params.update({"kwargs": kwargs})
487 |
488 | # build pl.LightninModule with given architecture
489 | return cls(arch=arch_module, **preprocess, hparams=module_params)
490 |
491 | @classmethod
492 | def from_checkpoint(cls, ckpt_path: str, **kwargs) -> pl.LightningModule:
493 | """Classmethod for creating `fastface.FaceDetector` instance, using checkpoint file path
494 |
495 | Args:
496 | ckpt_path (str): file path of the checkpoint
497 |
498 | Returns:
499 | pl.LightningModule: fastface.FaceDetector instance with checkpoint weights
500 | """
501 | return cls.load_from_checkpoint(ckpt_path, map_location="cpu", **kwargs)
502 |
503 | @classmethod
504 | def from_pretrained(
505 | cls, model: str, target_path: str = None, **kwargs
506 | ) -> pl.LightningModule:
507 | """Classmethod for creating `fastface.FaceDetector` instance, using model name
508 |
509 | Args:
510 | model (str): pretrained model name.
511 | target_path (str, optional): path to check for model weights, if not given it will use cache path. Defaults to None.
512 |
513 | Returns:
514 | pl.LightningModule: fastface.FaceDetector instance with pretrained weights
515 | """
516 | if model in api.list_pretrained_models():
517 | model = api.download_pretrained_model(model, target_path=target_path)
518 | assert os.path.isfile(model), f"given {model} not found in the disk"
519 | return cls.from_checkpoint(model, **kwargs)
520 |
521 | def on_load_checkpoint(self, checkpoint: Dict):
522 | arch = checkpoint["hyper_parameters"]["arch"]
523 | config = checkpoint["hyper_parameters"]["config"]
524 | preprocess = checkpoint["hyper_parameters"]["preprocess"]
525 |
526 | kwargs = checkpoint["hyper_parameters"]["kwargs"]
527 |
528 | # get architecture nn.Module class
529 | arch_cls = utils.config.get_arch_cls(arch)
530 |
531 | # build nn.Module with given configuration
532 | self.arch = arch_cls(config=config, **kwargs)
533 |
534 | # initialize preprocess with given arguments
535 | self.init_preprocess(**preprocess)
536 |
537 | def to_tensor(self, images: Union[np.ndarray, List]) -> List[torch.Tensor]:
538 | """Converts given image or list of images to list of tensors
539 | Args:
540 | images (Union[np.ndarray, List]): RGB image or list of RGB images
541 | Returns:
542 | List[torch.Tensor]: list of torch.Tensor(C x H x W)
543 | """
544 | assert isinstance(
545 | images, (list, np.ndarray)
546 | ), "give images must be eather list of numpy arrays or numpy array"
547 |
548 | if isinstance(images, np.ndarray):
549 | images = [images]
550 |
551 | batch: List[torch.Tensor] = []
552 |
553 | for img in images:
554 | assert (
555 | len(img.shape) == 3
556 | ), "image shape must be channel, height\
557 | , with length of 3 but found {}".format(
558 | len(img.shape)
559 | )
560 | assert (
561 | img.shape[2] == 3
562 | ), "channel size of the image must be 3 but found {}".format(img.shape[2])
563 |
564 | batch.append(
565 | # h,w,c => c,h,w
566 | # pylint: disable=not-callable
567 | torch.tensor(img, dtype=self.dtype, device=self.device).permute(2, 0, 1)
568 | )
569 |
570 | return batch
571 |
572 | def to_json(self, preds: List[torch.Tensor]) -> List[Dict]:
573 | """Converts given list of tensor predictions to json serializable format
574 | Args:
575 | preds (List[torch.Tensor]): list of torch.Tensor(N,5) as xmin, ymin, xmax, ymax, score
576 | Returns:
577 | List[Dict]: [
578 | # single image results
579 | {
580 | "boxes": , # List[List[xmin, ymin, xmax, ymax]]
581 | "scores": # List[float]
582 | },
583 | ...
584 | ]
585 | """
586 | results: List[Dict] = []
587 |
588 | for pred in preds:
589 | if pred.size(0) != 0:
590 | pred = pred.cpu().numpy()
591 | boxes = pred[:, :4].astype(np.int32).tolist()
592 | scores = pred[:, 4].tolist()
593 | else:
594 | boxes = []
595 | scores = []
596 |
597 | results.append({"boxes": boxes, "scores": scores})
598 |
599 | return results
600 |
--------------------------------------------------------------------------------
/fastface/registry.yaml:
--------------------------------------------------------------------------------
1 | lffd_original:
2 | adapter:
3 | type: "gdrive"
4 | kwargs:
5 | file_name: "lffd_original.ckpt"
6 | file_id: "1qFRuGhzoMWrW9WNlWw9jHXPY51MBssQD"
7 | overwrite: false
8 | extract: false
9 | showsize: true
10 | lffd_slim:
11 | adapter:
12 | type: "gdrive"
13 | kwargs:
14 | file_name: "lffd_slim.ckpt"
15 | file_id: "1UOHllYp5NY4mV7lHmq0c9xsryRIufpAQ"
16 | overwrite: false
17 | extract: false
18 | showsize: true
--------------------------------------------------------------------------------
/fastface/transforms/__init__.py:
--------------------------------------------------------------------------------
1 | from .augmentation import *
2 | from .compose import Compose
3 | from .discard import FaceDiscarder
4 | from .interpolate import ConditionalInterpolate, Interpolate
5 | from .pad import Padding
6 | from .rotate import Rotate
7 |
8 | __all__ = [
9 | "Compose",
10 | "FaceDiscarder",
11 | "ConditionalInterpolate",
12 | "Interpolate",
13 | "Padding",
14 | "Rotate",
15 | ]
16 |
--------------------------------------------------------------------------------
/fastface/transforms/augmentation/__init__.py:
--------------------------------------------------------------------------------
1 | from .blur import RandomGaussianBlur
2 | from .color_jitter import ColorJitter
3 | from .lffd_random_sample import LFFDRandomSample
4 | from .random_horizontal_flip import RandomHorizontalFlip
5 | from .random_rotate import RandomRotate
6 |
7 | __all__ = [
8 | "RandomGaussianBlur",
9 | "ColorJitter",
10 | "LFFDRandomSample",
11 | "RandomHorizontalFlip",
12 | "RandomRotate",
13 | ]
--------------------------------------------------------------------------------
/fastface/transforms/augmentation/blur.py:
--------------------------------------------------------------------------------
1 | import random
2 | from typing import Dict, Tuple
3 |
4 | import numpy as np
5 |
6 | from ...utils import kernel
7 |
8 |
9 | class RandomGaussianBlur:
10 | """Applies gaussian blur to the image with a probability of `p`
11 | """
12 |
13 | def __init__(self, p: float = 0.5, kernel_size: int = 15, sigma: float = 5):
14 | super().__init__()
15 | self.p = p
16 | self.kernel = kernel.get_gaussian_kernel(kernel_size, sigma=sigma)
17 |
18 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
19 | assert len(img.shape) == 3, "image shape expected 3 but found: {}".format(
20 | len(img.shape)
21 | )
22 | targets = dict() if targets is None else targets
23 |
24 | if random.random() > self.p:
25 | return kernel.apply_conv2d(img, self.kernel), targets
26 |
27 | return (img, targets)
28 |
--------------------------------------------------------------------------------
/fastface/transforms/augmentation/color_jitter.py:
--------------------------------------------------------------------------------
1 | import random
2 | from typing import Dict, Tuple
3 |
4 | import numpy as np
5 |
6 | from .. import functional as F
7 |
8 |
9 | class ColorJitter:
10 | """Jitters the color of the image with randomly selected values"""
11 |
12 | def __init__(
13 | self,
14 | p: float = 0.5,
15 | brightness: float = 0,
16 | contrast: float = 0,
17 | saturation: float = 0,
18 | ):
19 | super().__init__()
20 | self.p = p
21 | self.brightness_range = (-brightness, brightness)
22 | self.contrast_range = (-contrast, contrast)
23 | self.saturation_range = (-saturation, saturation)
24 |
25 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
26 | assert len(img.shape) == 3, "image shape expected 3 but found: {}".format(
27 | len(img.shape)
28 | )
29 |
30 | targets = dict() if targets is None else targets
31 |
32 | if random.random() > self.p:
33 | value = np.random.uniform(*self.brightness_range)
34 | img = F.adjust_brightness(img, factor=value)
35 |
36 | if random.random() > self.p:
37 | value = np.random.uniform(*self.contrast_range)
38 | img = F.adjust_contrast(img, factor=value)
39 |
40 | if random.random() > self.p:
41 | value = np.random.uniform(*self.saturation_range)
42 | img = F.adjust_saturation(img, factor=value)
43 |
44 | return (img, targets)
45 |
--------------------------------------------------------------------------------
/fastface/transforms/augmentation/lffd_random_sample.py:
--------------------------------------------------------------------------------
1 | import random
2 | from typing import Dict, List, Tuple
3 |
4 | import numpy as np
5 | from PIL import Image
6 |
7 | from ..interpolate import Interpolate
8 | from ..pad import Padding
9 |
10 |
11 | class LFFDRandomSample:
12 | """Applies augmantation defined in the LFFD paper"""
13 |
14 | def __init__(
15 | self,
16 | scales: List[Tuple[int, int]],
17 | target_size: Tuple[int, int] = (640, 640),
18 | p: float = 0.5,
19 | ):
20 | assert (
21 | p >= 0 and p <= 1.0
22 | ), "given `p` is not valid, must be between 0 and 1 but found: {}".format(p)
23 | self.scales = scales
24 | self.target_size = target_size # W,H
25 | self.padding = Padding(target_size=target_size, pad_value=0)
26 | self.interpolate = Interpolate(target_size=target_size[0])
27 | self.p = p
28 |
29 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
30 | """Randomly samples faces using given scales. All scales represents branches and
31 | for each branch selection probability is same.
32 |
33 | Args:
34 | img (np.ndarray): H,W,C
35 | targets (Dict, Optional): contains targets
36 |
37 | Returns:
38 | Tuple[np.ndarray, Dict]: transformed image and transformed targets
39 | """
40 |
41 | targets = dict() if targets is None else targets
42 |
43 | target_boxes = targets.get("target_boxes")
44 |
45 | if (
46 | (target_boxes is None)
47 | or (target_boxes.shape[0] == 0)
48 | or (random.random() > self.p)
49 | ):
50 | img, targets = self.interpolate(img, targets=targets)
51 | img, targets = self.padding(img, targets=targets)
52 | return (img, targets)
53 |
54 | num_faces = target_boxes.shape[0]
55 |
56 | # select one face
57 | selected_face_idx = random.randint(0, num_faces - 1)
58 |
59 | selected_face_scale_idx = random.choice(list(range(len(self.scales))))
60 | min_scale, max_scale = self.scales[selected_face_scale_idx]
61 |
62 | scale_size = random.uniform(min_scale, max_scale)
63 |
64 | x1, y1, x2, y2 = target_boxes[selected_face_idx].astype(np.int32)
65 |
66 | face_scale = max(y2 - y1, x2 - x1)
67 | h, w = img.shape[:2]
68 |
69 | sf = scale_size / face_scale
70 |
71 | aboxes = target_boxes * sf
72 | sx1, sy1, sx2, sy2 = aboxes[selected_face_idx].astype(np.int32)
73 |
74 | offset_w_1 = (self.target_size[0] - (sx2 - sx1)) // 2
75 | offset_w_2 = offset_w_1 + (self.target_size[0] - (sx2 - sx1)) % 2
76 |
77 | offset_w_1 //= sf
78 | offset_w_2 //= sf
79 |
80 | offset_h_1 = (self.target_size[1] - (sy2 - sy1)) // 2
81 | offset_h_2 = offset_h_1 + (self.target_size[1] - (sy2 - sy1)) % 2
82 |
83 | offset_h_1 //= sf
84 | offset_h_2 //= sf
85 |
86 | offset_w_1 = int(min(x1, offset_w_1))
87 | offset_w_2 = int(min(w - x2, offset_w_2))
88 |
89 | offset_h_1 = int(min(y1, offset_h_1))
90 | offset_h_2 = int(min(h - y2, offset_h_2))
91 |
92 | # select faces that center's lie between cropped region
93 | low_h, high_h = y1 - offset_h_1, y2 + offset_h_2
94 | low_w, high_w = x1 - offset_w_1, x2 + offset_w_2
95 | cboxes_x = (target_boxes[:, 0] + target_boxes[:, 2]) // 2
96 | cboxes_y = (target_boxes[:, 1] + target_boxes[:, 3]) // 2
97 |
98 | # TODO handle here
99 | center_mask = np.bitwise_and(
100 | np.bitwise_and(cboxes_x > low_w, cboxes_x < high_w),
101 | np.bitwise_and(cboxes_y > low_h, cboxes_y < high_h),
102 | )
103 |
104 | aimg = img[y1 - offset_h_1 : y2 + offset_h_2, x1 - offset_w_1 : x2 + offset_w_2]
105 |
106 | # TODO control this line
107 | aimg = np.array(
108 | Image.fromarray(aimg).resize(
109 | (int(aimg.shape[1] * sf), int(aimg.shape[0] * sf))
110 | )
111 | )
112 |
113 | aimg = aimg[: self.target_size[1], : self.target_size[0]]
114 |
115 | target_boxes[:, [0, 2]] = target_boxes[:, [0, 2]] - (x1 - offset_w_1)
116 | target_boxes[:, [1, 3]] = target_boxes[:, [1, 3]] - (y1 - offset_h_1)
117 | target_boxes *= sf
118 |
119 | x1, y1, x2, y2 = target_boxes[selected_face_idx].astype(np.int32)
120 |
121 | cx = (x1 + x2) // 2
122 | cy = (y1 + y2) // 2
123 |
124 | img = np.zeros((self.target_size[1], self.target_size[0], 3), dtype=np.uint8)
125 | tcx = img.shape[1] // 2
126 | tcy = img.shape[0] // 2
127 |
128 | offset_x = int(tcx - cx)
129 | offset_y = int(tcy - cy)
130 |
131 | if offset_x >= 0:
132 | # pad left
133 | left_index_x = offset_x
134 | right_index_x = offset_x + aimg.shape[1]
135 | else:
136 | # pad_right
137 | left_index_x = 0
138 | right_index_x = aimg.shape[1]
139 | if offset_y >= 0:
140 | # pad up
141 | up_index_y = offset_y
142 | down_index_y = offset_y + aimg.shape[0]
143 | else:
144 | # pad down
145 | up_index_y = 0
146 | down_index_y = aimg.shape[0]
147 |
148 | target_h, target_w = img[
149 | up_index_y:down_index_y, left_index_x:right_index_x
150 | ].shape[:2]
151 | source_h, source_w = aimg.shape[:2]
152 |
153 | up_index_y = up_index_y + target_h - source_h
154 | down_index_y = down_index_y + target_h - source_h
155 | left_index_x = left_index_x + target_w - source_w
156 | right_index_x = right_index_x + target_w - source_w
157 |
158 | img[up_index_y:down_index_y, left_index_x:right_index_x] = aimg
159 |
160 | target_boxes[:, [0, 2]] += left_index_x
161 | target_boxes[:, [1, 3]] += up_index_y
162 |
163 | target_boxes[:, 0] = target_boxes[:, 0].clip(0, self.target_size[0])
164 | target_boxes[:, 1] = target_boxes[:, 1].clip(0, self.target_size[1])
165 | target_boxes[:, 2] = target_boxes[:, 2].clip(0, self.target_size[0])
166 | target_boxes[:, 3] = target_boxes[:, 3].clip(0, self.target_size[1])
167 | targets["target_boxes"] = target_boxes[center_mask, :]
168 |
169 | return (img, targets)
170 |
--------------------------------------------------------------------------------
/fastface/transforms/augmentation/random_horizontal_flip.py:
--------------------------------------------------------------------------------
1 | import random
2 | from typing import Dict, Tuple
3 |
4 | import numpy as np
5 |
6 |
7 | class RandomHorizontalFlip:
8 | """Applies random horizontal flip for the image and updated boxes"""
9 |
10 | def __init__(self, p: float = 0.5):
11 | assert (
12 | p >= 0 and p <= 1.0
13 | ), "given `p` is not valid, must be between 0 and 1 but found: {}".format(p)
14 | self.p = p
15 |
16 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
17 | targets = dict() if targets is None else targets
18 | if random.random() > self.p:
19 | return (img, targets)
20 |
21 | if len(img.shape) == 3:
22 | nimg = img[:, ::-1, :]
23 | elif len(img.shape) == 2:
24 | nimg = img[:, ::-1]
25 | else:
26 | raise AssertionError("image has wrong dimensions")
27 |
28 | target_boxes = targets.get("target_boxes")
29 | if (target_boxes is None) or (target_boxes.shape[0] == 0):
30 | return (nimg, targets)
31 |
32 | # x1,y1,x2,y2
33 | w = nimg.shape[1]
34 | cboxes = target_boxes.copy()
35 | target_boxes[:, 0] = w - cboxes[:, 2]
36 | target_boxes[:, 2] = w - cboxes[:, 0]
37 |
38 | targets["target_boxes"] = target_boxes
39 |
40 | return (nimg, targets)
41 |
--------------------------------------------------------------------------------
/fastface/transforms/augmentation/random_rotate.py:
--------------------------------------------------------------------------------
1 | import random
2 | from typing import Dict, Tuple
3 |
4 | import numpy as np
5 |
6 | from .. import functional as F
7 |
8 |
9 | class RandomRotate:
10 | """Rotates the image and boxes clockwise with randomly selected value"""
11 |
12 | def __init__(self, p: float = 0.5, degree_range: float = 0):
13 | super().__init__()
14 | self.p = p
15 | self.degree_range = degree_range
16 |
17 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
18 | assert len(img.shape) == 3, "image shape expected 3 but found: {}".format(
19 | len(img.shape)
20 | )
21 |
22 | targets = dict() if targets is None else targets
23 |
24 | if random.random() > self.p:
25 | return (img, targets)
26 |
27 | degree = np.random.uniform(low=-self.degree_range, high=self.degree_range)
28 |
29 | nimg, targets = F.rotate(img, degree, targets=targets)
30 |
31 | return (nimg, targets)
32 |
--------------------------------------------------------------------------------
/fastface/transforms/compose.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Tuple
2 |
3 | import numpy as np
4 |
5 |
6 | class Compose:
7 | """Compose given transforms"""
8 |
9 | def __init__(self, *transforms):
10 | self.transforms = transforms
11 |
12 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
13 | targets = dict() if targets is None else targets
14 | # TODO add logger
15 | for transform in self.transforms:
16 | img, targets = transform(img, targets=targets)
17 |
18 | return (img, targets)
19 |
--------------------------------------------------------------------------------
/fastface/transforms/discard.py:
--------------------------------------------------------------------------------
1 | import math
2 | from typing import Dict, Tuple
3 |
4 | import numpy as np
5 |
6 |
7 | class FaceDiscarder:
8 | """Discard face boxes using min and max scale"""
9 |
10 | def __init__(self, min_face_size: int = 0, max_face_size: int = math.inf):
11 | self.min_face_size = min_face_size
12 | self.max_face_size = max_face_size
13 |
14 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
15 | targets = dict() if targets is None else targets
16 |
17 | if "target_boxes" in targets:
18 | target_boxes = targets["target_boxes"]
19 | face_scales = (target_boxes[:, [2, 3]] - target_boxes[:, [0, 1]]).max(
20 | axis=1
21 | )
22 | mask = (face_scales >= self.min_face_size) & (
23 | face_scales <= self.max_face_size
24 | )
25 | targets["target_boxes"] = target_boxes[mask]
26 |
27 | if "ignore_flags" in targets:
28 | targets["ignore_flags"] = targets["ignore_flags"][mask]
29 |
30 | return (img, targets)
31 |
--------------------------------------------------------------------------------
/fastface/transforms/functional/__init__.py:
--------------------------------------------------------------------------------
1 | from .color_jitter import adjust_brightness, adjust_contrast, adjust_saturation
2 | from .interpolate import interpolate
3 | from .pad import pad
4 | from .rotate import rotate
5 |
6 | __all__ = [
7 | "adjust_brightness",
8 | "adjust_contrast",
9 | "adjust_saturation",
10 | "interpolate",
11 | "pad",
12 | "rotate",
13 | ]
14 |
--------------------------------------------------------------------------------
/fastface/transforms/functional/color_jitter.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from PIL import Image, ImageEnhance
3 |
4 |
5 | def adjust_brightness(img: np.ndarray, factor: float = 0.0) -> np.ndarray:
6 | # between [-1, 1] 0 is same image -1 is darken, 1 is brighten
7 | factor = min(factor, 1)
8 | factor = max(factor, -1)
9 |
10 | pimg = ImageEnhance.Brightness(Image.fromarray(img)).enhance(factor + 1)
11 | return np.array(pimg)
12 |
13 |
14 | def adjust_contrast(img: np.ndarray, factor: float = 0.0) -> np.ndarray:
15 | # between [-1, 1] 0 is same image -1 is lower, 1 is higher
16 | factor = min(factor, 1)
17 | factor = max(factor, -1)
18 |
19 | pimg = ImageEnhance.Contrast(Image.fromarray(img)).enhance(factor + 1)
20 | return np.array(pimg)
21 |
22 |
23 | def adjust_saturation(img: np.ndarray, factor: float = 0.0) -> np.ndarray:
24 | # between [-1, 1] 0 is same image -1 is lower, 1 is higher
25 | factor = min(factor, 1)
26 | factor = max(factor, -1)
27 |
28 | pimg = ImageEnhance.Color(Image.fromarray(img)).enhance(factor + 1)
29 | return np.array(pimg)
30 |
--------------------------------------------------------------------------------
/fastface/transforms/functional/interpolate.py:
--------------------------------------------------------------------------------
1 | from typing import Dict
2 |
3 | import numpy as np
4 | from PIL import Image
5 |
6 |
7 | def interpolate(img: np.ndarray, target_size: int, targets: Dict = None):
8 | targets = dict() if targets is None else targets
9 | h, w = img.shape[:2]
10 |
11 | sf = target_size / max(h, w)
12 |
13 | nh = int(sf * h)
14 | nw = int(sf * w)
15 |
16 | nimg = np.array(Image.fromarray(img).resize((nw, nh)), dtype=img.dtype)
17 |
18 | if "target_boxes" in targets:
19 | targets["target_boxes"] *= sf
20 |
21 | return nimg, targets
22 |
--------------------------------------------------------------------------------
/fastface/transforms/functional/pad.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Tuple
2 |
3 | import numpy as np
4 |
5 |
6 | def pad(
7 | img: np.ndarray, target_size: Tuple, pad_value: float = 0.0, targets: Dict = None
8 | ):
9 | targets = dict() if targets is None else targets
10 | h, w, c = img.shape
11 | tw, th = target_size
12 |
13 | pad_left = int((tw - w) // 2) + (tw - w) % 2
14 | pad_right = int((tw - w) // 2)
15 | if w > tw:
16 | pad_left, pad_right = 0, 0
17 |
18 | pad_up = int((th - h) // 2) + (th - h) % 2
19 | pad_down = int((th - h) // 2)
20 | if h > th:
21 | pad_up, pad_down = 0, 0
22 |
23 | nimg = np.ones((th, tw, c), dtype=img.dtype) * pad_value
24 | nimg[pad_up : th - pad_down, pad_left : tw - pad_right] = img
25 |
26 | if "target_boxes" in targets:
27 | target_boxes = targets["target_boxes"]
28 |
29 | if len(target_boxes.shape) == 2 and target_boxes.shape[0] > 0:
30 | target_boxes[:, [0, 2]] = target_boxes[:, [0, 2]] + pad_left
31 | target_boxes[:, [1, 3]] = target_boxes[:, [1, 3]] + pad_up
32 |
33 | targets["target_boxes"] = target_boxes
34 |
35 | return nimg, targets
36 |
--------------------------------------------------------------------------------
/fastface/transforms/functional/rotate.py:
--------------------------------------------------------------------------------
1 | from typing import Dict
2 |
3 | import numpy as np
4 | from PIL import Image
5 |
6 | from ...utils.geo import get_rotation_matrix
7 |
8 |
9 | def rotate(img: np.ndarray, degree: int, targets: Dict = None) -> np.ndarray:
10 | # clockwise rotation
11 |
12 | targets = dict() if targets is None else targets
13 |
14 | h, w = img.shape[:2]
15 | cx = w // 2
16 | cy = h // 2
17 |
18 | nimg = np.array(Image.fromarray(img).rotate((360 - degree), center=(cx, cy)))
19 |
20 | if "target_boxes" in targets:
21 | r = get_rotation_matrix(degree)
22 | N = targets["target_boxes"].shape[0]
23 | if N == 0:
24 | return nimg, targets
25 |
26 | coords = np.empty((N, 4, 2), dtype=targets["target_boxes"].dtype)
27 |
28 | # x1,y1
29 | coords[:, 0, :] = targets["target_boxes"][:, [0, 1]]
30 | # x1,y2
31 | coords[:, 1, :] = targets["target_boxes"][:, [0, 3]]
32 | # x2,y1
33 | coords[:, 2, :] = targets["target_boxes"][:, [2, 1]]
34 | # x2,y2
35 | coords[:, 3, :] = targets["target_boxes"][:, [2, 3]]
36 |
37 | # convert to regular coodinate space
38 | coords[..., [0]] = w - coords[..., [0]]
39 | coords[..., [1]] = h - coords[..., [1]]
40 |
41 | # centerize the coordinates
42 | coords[..., [0]] -= cx
43 | coords[..., [1]] -= cy
44 |
45 | # apply clockwise rotation
46 | coords = np.matmul(coords, r)
47 |
48 | # revert centerization
49 | coords[..., [0]] += cx
50 | coords[..., [1]] += cy
51 |
52 | # re-convert to image coordinate space
53 | coords[..., [0]] = w - coords[..., [0]]
54 | coords[..., [1]] = h - coords[..., [1]]
55 |
56 | # create the box
57 | x1 = coords[..., 0].min(axis=1)
58 | y1 = coords[..., 1].min(axis=1)
59 | x2 = coords[..., 0].max(axis=1)
60 | y2 = coords[..., 1].max(axis=1)
61 |
62 | # stack
63 | n_boxes = np.stack([x1, y1, x2, y2], axis=1)
64 |
65 | # clip
66 | n_boxes[:, [0, 2]] = n_boxes[:, [0, 2]].clip(min=0, max=w - 1)
67 | n_boxes[:, [1, 3]] = n_boxes[:, [1, 3]].clip(min=0, max=h - 1)
68 |
69 | targets["target_boxes"] = n_boxes
70 |
71 | return nimg, targets
72 |
--------------------------------------------------------------------------------
/fastface/transforms/interpolate.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Tuple
2 |
3 | import numpy as np
4 |
5 | from . import functional as F
6 |
7 |
8 | class Interpolate:
9 | """Interpolates the image and boxes using target size"""
10 |
11 | def __init__(self, target_size: int = 640):
12 | super(Interpolate, self).__init__()
13 | self.target_size = target_size
14 |
15 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
16 | assert len(img.shape) == 3, "image shape expected 3 but found: {}".format(
17 | len(img.shape)
18 | )
19 | targets = dict() if targets is None else targets
20 |
21 | nimg, targets = F.interpolate(img, self.target_size, targets=targets)
22 |
23 | return (nimg, targets)
24 |
25 |
26 | class ConditionalInterpolate:
27 | """Interpolates the image and boxes if image height or width exceed given maximum size"""
28 |
29 | # TODO add to pytest
30 |
31 | def __init__(self, max_size: int = 640):
32 | super(ConditionalInterpolate, self).__init__()
33 | self.max_size = max_size
34 |
35 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
36 | assert len(img.shape) == 3, "image shape expected 3 but found: {}".format(
37 | len(img.shape)
38 | )
39 | targets = dict() if targets is None else targets
40 |
41 | if max(img.shape) <= self.max_size:
42 | return (img, targets)
43 |
44 | nimg, targets = F.interpolate(img, self.max_size, targets=targets)
45 |
46 | return (nimg, targets)
47 |
--------------------------------------------------------------------------------
/fastface/transforms/normalize.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List, Tuple, Union
2 |
3 | import numpy as np
4 |
5 |
6 | class Normalize:
7 | """Normalizes the image with given `mean` and `std`. (img - mean) / std"""
8 |
9 | def __init__(
10 | self, mean: Union[List, Tuple, float] = 0, std: Union[List, Tuple, float] = 1
11 | ):
12 | self.mean = mean
13 | self.std = std
14 |
15 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
16 | targets = dict() if targets is None else targets
17 |
18 | img = img.astype(np.float32)
19 | img = img - self.mean
20 | img = img / self.std
21 |
22 | return (img, targets)
23 |
--------------------------------------------------------------------------------
/fastface/transforms/pad.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Tuple
2 |
3 | import numpy as np
4 |
5 | from . import functional as F
6 |
7 |
8 | class Padding:
9 | """Applies padding to image and target boxes"""
10 |
11 | def __init__(self, target_size: Tuple[int, int] = (640, 640), pad_value: int = 0):
12 | super(Padding, self).__init__()
13 | self.pad_value = pad_value
14 | self.target_size = target_size # w,h
15 |
16 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
17 | # TODO check image shape
18 | targets = dict() if targets is None else targets
19 |
20 | return F.pad(
21 | img, target_size=self.target_size, pad_value=self.pad_value, targets=targets
22 | )
23 |
--------------------------------------------------------------------------------
/fastface/transforms/rotate.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Tuple
2 |
3 | import numpy as np
4 |
5 | from . import functional as F
6 |
7 |
8 | class Rotate:
9 | """Rotates the image and boxes clockwise using given degree"""
10 |
11 | def __init__(self, degree: float = 0):
12 | super().__init__()
13 | self.degree = degree
14 |
15 | def __call__(self, img: np.ndarray, targets: Dict = None) -> Tuple[np.ndarray, Dict]:
16 | assert len(img.shape) == 3, "image shape expected 3 but found: {}".format(
17 | len(img.shape)
18 | )
19 |
20 | targets = dict() if targets is None else targets
21 |
22 | nimg, targets = F.rotate(img, self.degree, targets=targets)
23 |
24 | return (nimg, targets)
25 |
--------------------------------------------------------------------------------
/fastface/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from . import box, cache, cluster, config, geo, kernel, preprocess, random, vis
2 |
3 | __all__ = [
4 | "box",
5 | "cache",
6 | "cluster",
7 | "config",
8 | "geo",
9 | "kernel",
10 | "preprocess",
11 | "random",
12 | "vis",
13 | ]
14 |
--------------------------------------------------------------------------------
/fastface/utils/box.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from torchvision.ops import nms
3 |
4 |
5 | def generate_grids(fh: int, fw: int) -> torch.Tensor:
6 | """generates grids using given feature map dimension
7 |
8 | Args:
9 | fh (int): height of the feature map
10 | fw (int): width of the feature map
11 |
12 | Returns:
13 | torch.Tensor: fh x fw x 2 as x1,y1
14 | """
15 | # x: fh x fw
16 | # y: fh x fw
17 | x, y = torch.meshgrid(torch.arange(fh), torch.arange(fw), indexing="xy")
18 |
19 | # grids: fh x fw x 2
20 | return torch.stack([x, y], dim=2).float()
21 |
22 |
23 | def jaccard_vectorized(box_a: torch.Tensor, box_b: torch.Tensor) -> torch.Tensor:
24 | """Calculates jaccard index with a vectorized fashion
25 |
26 | Args:
27 | box_a (torch.Tensor): torch.Tensor(A,4) as xmin,ymin,xmax,ymax
28 | box_b (torch.Tensor): torch.Tensor(B,4) as xmin,ymin,xmax,ymax
29 |
30 | Returns:
31 | torch.Tensor: IoUs as torch.Tensor(A,B)
32 | """
33 | inter = intersect(box_a, box_b)
34 | area_a = (
35 | ((box_a[:, 2] - box_a[:, 0]) * (box_a[:, 3] - box_a[:, 1]))
36 | .unsqueeze(1)
37 | .expand_as(inter)
38 | ) # [A,B]
39 | area_b = (
40 | ((box_b[:, 2] - box_b[:, 0]) * (box_b[:, 3] - box_b[:, 1]))
41 | .unsqueeze(0)
42 | .expand_as(inter)
43 | ) # [A,B]
44 |
45 | union = area_a + area_b - inter
46 | return inter / union
47 |
48 |
49 | def jaccard_centered(wh_a: torch.Tensor, wh_b: torch.Tensor) -> torch.Tensor:
50 | """Calculates jaccard index of same centered boxes
51 | Args:
52 | wh_a (torch.Tensor): torch.Tensor(A,2) as width,height
53 | wh_b (torch.Tensor): torch.Tensor(B,2) as width,height
54 | Returns:
55 | torch.Tensor: torch.Tensor(A,B)
56 | """
57 | inter = intersect_centered(wh_a, wh_b)
58 | area_a = (wh_a[:, 0] * wh_a[:, 1]).unsqueeze(1).expand_as(inter) # [A,B]
59 | area_b = (wh_b[:, 0] * wh_b[:, 1]).unsqueeze(0).expand_as(inter) # [A,B]
60 | union = area_a + area_b - inter
61 | return inter / union
62 |
63 |
64 | def intersect(box_a: torch.Tensor, box_b: torch.Tensor) -> torch.Tensor:
65 | """Calculates intersection area of boxes given
66 | Args:
67 | box_a (torch.Tensor): torch.Tensor(A,4) as xmin,ymin,xmax,ymax
68 | box_b (torch.Tensor): torch.Tensor(B,4) as xmin,ymin,xmax,ymax
69 | Returns:
70 | torch.Tensor: torch.Tensor(A,B)
71 | """
72 |
73 | A = box_a.size(0)
74 | B = box_b.size(0)
75 | max_xy = torch.min(
76 | box_a[:, 2:].unsqueeze(1).expand(A, B, 2),
77 | box_b[:, 2:].unsqueeze(0).expand(A, B, 2),
78 | )
79 |
80 | min_xy = torch.max(
81 | box_a[:, :2].unsqueeze(1).expand(A, B, 2),
82 | box_b[:, :2].unsqueeze(0).expand(A, B, 2),
83 | )
84 |
85 | inter = torch.clamp((max_xy - min_xy), min=0)
86 | return inter[:, :, 0] * inter[:, :, 1]
87 |
88 |
89 | def intersect_centered(wh_a: torch.Tensor, wh_b: torch.Tensor) -> torch.Tensor:
90 | """Calculates intersection of same centered boxes
91 |
92 | Args:
93 | wh_a (torch.Tensor): torch.Tensor(A,2) as width,height
94 | wh_b (torch.Tensor): torch.Tensor(B,2) as width,height
95 |
96 | Returns:
97 | torch.Tensor: torch.Tensor(A,B)
98 | """
99 |
100 | A = wh_a.size(0)
101 | B = wh_b.size(0)
102 | min_w = torch.min(
103 | wh_a[:, [0]].unsqueeze(1).expand(A, B, 2),
104 | wh_b[:, [0]].unsqueeze(0).expand(A, B, 2),
105 | )
106 |
107 | # [A,2] -> [A,1,2] -> [A,B,2]
108 |
109 | min_h = torch.min(
110 | wh_a[:, [1]].unsqueeze(1).expand(A, B, 2),
111 | wh_b[:, [1]].unsqueeze(0).expand(A, B, 2),
112 | )
113 | # [B,2] -> [1,B,2] -> [A,B,2]
114 |
115 | return min_w[:, :, 0] * min_h[:, :, 0]
116 |
117 |
118 | def cxcywh2xyxy(boxes: torch.Tensor) -> torch.Tensor:
119 | """Convert box coordiates, centerx centery width height to xmin ymin xmax ymax
120 |
121 | Args:
122 | boxes (torch.Tensor): torch.Tensor(N,4) as centerx centery width height
123 |
124 | Returns:
125 | torch.Tensor: torch.Tensor(N,4) as xmin ymin xmax ymax
126 | """
127 |
128 | wh_half = boxes[:, 2:] / 2
129 |
130 | x1y1 = boxes[:, :2] - wh_half
131 | x2y2 = boxes[:, :2] + wh_half
132 |
133 | return torch.cat([x1y1, x2y2], dim=1)
134 |
135 |
136 | def xyxy2cxcywh(boxes: torch.Tensor) -> torch.Tensor:
137 | """Convert box coordiates, xmin ymin xmax ymax to centerx centery width height
138 |
139 | Args:
140 | boxes (torch.Tensor): torch.Tensor(N,4) as xmin ymin xmax ymax
141 |
142 | Returns:
143 | torch.Tensor: torch.Tensor(N,4) as centerx centery width height
144 | """
145 | wh = boxes[:, 2:] - boxes[:, :2]
146 | cxcy = (boxes[:, 2:] + boxes[:, :2]) / 2
147 |
148 | return torch.cat([cxcy, wh], dim=1)
149 |
150 |
151 | @torch.jit.script
152 | def batched_nms(
153 | boxes: torch.Tensor,
154 | scores: torch.Tensor,
155 | batch_ids: torch.Tensor,
156 | iou_threshold: float = 0.4,
157 | ) -> torch.Tensor:
158 | """Applies batched non max suppression to given boxes
159 |
160 | Args:
161 | boxes (torch.Tensor): torch.Tensor(N,4) as xmin ymin xmax ymax
162 | scores (torch.Tensor): torch.Tensor(N,) as score
163 | batch_ids (torch.Tensor): torch.LongTensor(N,) as batch idx
164 | iou_threshold (float, optional): nms threshold. Defaults to 0.4.
165 |
166 | Returns:
167 | torch.Tensor: keep mask
168 | """
169 | if boxes.size(0) == 0:
170 | return torch.empty((0,), dtype=torch.long)
171 |
172 | max_val = torch.max(boxes)
173 |
174 | cboxes = boxes / max_val
175 |
176 | offsets = batch_ids.to(boxes.dtype) # N,
177 |
178 | cboxes += offsets.unsqueeze(1).repeat(1, 4)
179 |
180 | return nms(cboxes, scores, iou_threshold)
181 |
--------------------------------------------------------------------------------
/fastface/utils/cache.py:
--------------------------------------------------------------------------------
1 | import os
2 | from functools import wraps
3 |
4 | from ..version import __version__
5 |
6 |
7 | def ensure_path(fun):
8 | @wraps(fun)
9 | def more_fun(*args, **kwargs):
10 | p = fun(*args, **kwargs)
11 | if os.path.isfile(p):
12 | return p
13 | if not os.path.exists(p):
14 | os.makedirs(p, exist_ok=True)
15 | return p
16 |
17 | return more_fun
18 |
19 |
20 | @ensure_path
21 | def get_cache_dir() -> str:
22 | return os.path.join(os.path.expanduser("~"), ".cache", "fastface", __version__)
23 |
24 |
25 | @ensure_path
26 | def get_model_cache_dir(suffix: str = "") -> str:
27 | return os.path.join(get_cache_dir(), "model", suffix)
28 |
29 |
30 | @ensure_path
31 | def get_data_cache_dir(suffix: str = "") -> str:
32 | return os.path.join(get_cache_dir(), "data", suffix)
33 |
34 |
35 | @ensure_path
36 | def get_checkpoint_cache_dir(suffix: str = "") -> str:
37 | return os.path.join(get_cache_dir(), "checkpoints", suffix)
38 |
--------------------------------------------------------------------------------
/fastface/utils/cluster.py:
--------------------------------------------------------------------------------
1 | import math
2 | import random
3 |
4 | import torch
5 |
6 |
7 | class KMeans:
8 | """Test"""
9 |
10 | def __init__(self, k: int, distance_fn=None, dim_size: int = 2, nstart: int = 2):
11 | # TODO use nstart
12 | assert distance_fn is not None, "please provide a distance function"
13 |
14 | self._params = torch.empty(k, dim_size, dtype=torch.float32)
15 | self._distance_fn = distance_fn
16 | self._best_distance_score = math.inf
17 |
18 | def fit(self, points: torch.Tensor):
19 | assert (
20 | len(points.shape) == 2
21 | ), "shape length of the points \
22 | must be 2 but found {}".format(
23 | len(points.shape)
24 | )
25 | assert isinstance(
26 | points, torch.Tensor
27 | ), "points must be torch.tensor but found {}".format(type(points))
28 | sample_size = points.size(0)
29 | k = self._params.size(0)
30 |
31 | self._params = points[random.sample(range(sample_size), k=k), :]
32 | # self._params: torch.Tensor(k, dim_size)
33 |
34 | latest_cluster = torch.zeros(sample_size, dtype=torch.long)
35 | # latest_cluster: torch.Tensor(sample_size)
36 |
37 | while 1:
38 | # points: torch.Tensor(sample_size, dim_size)
39 | # self._params: torch.Tensor(k, dim_size)
40 | dists = self._distance_fn(points, self._params)
41 | # dists: torch.Tensor(sample_size, k)
42 |
43 | assigned_clusters = torch.argmin(dists, dim=1)
44 | # assigned_clusters: torch.Tensor(sample_size)
45 |
46 | if (latest_cluster == assigned_clusters).all():
47 | # break if converged
48 | break
49 |
50 | for i in range(k):
51 | self._params[i] = points[assigned_clusters == i, :].median(dim=0)[0]
52 |
53 | latest_cluster = assigned_clusters
54 |
--------------------------------------------------------------------------------
/fastface/utils/config.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | import os
3 | from typing import Dict, Generator
4 |
5 | import yaml
6 |
7 | __all__ = [
8 | "get_pkg_root_path",
9 | "get_pkg_arch_path",
10 | "get_registry_path",
11 | "get_registry",
12 | "discover_archs",
13 | "get_arch_pkg",
14 | "get_arch_cls",
15 | ]
16 |
17 | __ROOT_PATH__ = os.path.sep.join(os.path.realpath(__file__).split(os.path.sep)[:-2])
18 |
19 |
20 | def get_pkg_root_path() -> str:
21 | global __ROOT_PATH__
22 | return __ROOT_PATH__
23 |
24 |
25 | def get_pkg_arch_path() -> str:
26 | root_path = get_pkg_root_path()
27 | return os.path.join(root_path, "arch")
28 |
29 |
30 | def get_registry_path() -> str:
31 | root_path = get_pkg_root_path()
32 | return os.path.join(root_path, "registry.yaml")
33 |
34 |
35 | def get_registry() -> Dict:
36 | registry_path = get_registry_path()
37 | with open(registry_path, "r") as foo:
38 | registry = yaml.load(foo, Loader=yaml.FullLoader)
39 | return registry
40 |
41 |
42 | def discover_archs() -> Generator:
43 | """yields tuple as architecture name and full path of the module
44 |
45 | Yields:
46 | Generator: (architecture name, full path of the module)
47 | """
48 |
49 | arch_path = get_pkg_arch_path()
50 | for candidate in os.listdir(arch_path):
51 | file_path = os.path.join(arch_path, candidate)
52 | if os.path.isfile(file_path) or candidate == "__pycache__":
53 | continue
54 | module_path = os.path.join(file_path, "module.py")
55 | assert os.path.isfile(
56 | module_path
57 | ), "cannot find: {}. {} must contain module.py".format(module_path, candidate)
58 | yield (candidate, module_path)
59 |
60 |
61 | def get_arch_pkg(arch: str):
62 | arch_path = get_pkg_arch_path()
63 | for arch_name, module_path in discover_archs():
64 | if arch_name != arch:
65 | continue
66 | abs_m_p = module_path.replace(arch_path, "", 1).replace("/module.py", "", -1)
67 | abs_m_p = (
68 | abs_m_p.replace(os.path.sep, "", 1)
69 | if abs_m_p.startswith(os.path.sep)
70 | else abs_m_p
71 | )
72 | abs_m_p = abs_m_p.replace(os.path.sep, ".")
73 | return importlib.import_module("fastface.arch.{}".format(abs_m_p))
74 |
75 | raise AssertionError("given {} is not found".format(arch))
76 |
77 |
78 | def get_arch_cls(arch: str):
79 | arch_path = get_pkg_arch_path()
80 | for arch_name, module_path in discover_archs():
81 | if arch_name != arch:
82 | continue
83 | abs_m_p = module_path.replace(arch_path, "", 1).replace(".py", "", -1)
84 | abs_m_p = (
85 | abs_m_p.replace(os.path.sep, "", 1)
86 | if abs_m_p.startswith(os.path.sep)
87 | else abs_m_p
88 | )
89 | abs_m_p = abs_m_p.replace(os.path.sep, ".")
90 | api = importlib.import_module("fastface.arch.{}".format(abs_m_p))
91 |
92 | for d in dir(api):
93 | if d.lower() != arch:
94 | continue
95 | return getattr(api, d)
96 |
97 | raise AssertionError(
98 | "given {} nn.Module is not found. Hint: arch folder may contain broken architecture implementation.".format(
99 | arch
100 | )
101 | )
102 |
--------------------------------------------------------------------------------
/fastface/utils/data.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import torch
3 |
4 |
5 | def default_collate_fn(batch):
6 | batch, targets = zip(*batch)
7 | batch = np.stack(batch, axis=0).astype(np.float32)
8 | batch = torch.from_numpy(batch).permute(0, 3, 1, 2).contiguous()
9 | for i, target in enumerate(targets):
10 | for k, v in target.items():
11 | if isinstance(v, np.ndarray):
12 | targets[i][k] = torch.from_numpy(v)
13 |
14 | return batch, targets
15 |
--------------------------------------------------------------------------------
/fastface/utils/geo.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | import numpy as np
4 |
5 |
6 | def get_rotation_matrix(degree: float) -> np.ndarray:
7 | rad = math.radians(degree)
8 | sinq = math.sin(rad)
9 | cosq = math.cos(rad)
10 | return np.array([[cosq, sinq], [-1 * sinq, cosq]], dtype=np.float32)
11 |
--------------------------------------------------------------------------------
/fastface/utils/kernel.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple
2 | import numpy as np
3 |
4 | from ..transforms import functional as F
5 |
6 |
7 | def apply_conv2d(img: np.ndarray, kernel: np.ndarray) -> np.ndarray:
8 | assert len(kernel.shape) == 2, "kernel shape must be 2D but found {}".format(
9 | len(kernel.shape)
10 | )
11 | img_h, img_w = img.shape[:2]
12 | nimg = []
13 | s = kernel.shape + tuple(np.subtract(img.shape[:2], kernel.shape) + 1)
14 | if len(img.shape) == 2:
15 | img = img[..., np.newaxis]
16 |
17 | for ch in range(img.shape[2]):
18 | subM = np.lib.stride_tricks.as_strided(
19 | img[..., ch], shape=s, strides=img[..., ch].strides * 2
20 | )
21 | nimg.append(np.einsum("ij,ijkl->kl", kernel, subM))
22 |
23 | nimg = np.stack(nimg, axis=2).astype(np.uint8)
24 |
25 | nimg, _ = F.pad(nimg, (img_w, img_h), pad_value=0)
26 |
27 | return nimg
28 |
29 |
30 | def get_gaussian_kernel(kernel_size: int, sigma: float = 1.0,
31 | center_point: Tuple[float, float] = None, normalize: bool = True) -> np.ndarray:
32 | """Generates gaussian kernel using 2D gaussian distribution
33 |
34 | Args:
35 | kernel_size (int): kernel size
36 | sigma (float, optional): sigma value of the gaussian kernel. Defaults to 1.0.
37 | center_point (Tuple[float, float], optional): mean data point of the distribution as x,y order. Defaults to None.
38 | normalize (bool, optional): whether to normalize kernel or not. Defaults to True.
39 |
40 | Returns:
41 | np.ndarray: 2D kernel with shape kernel_size x kernel_size
42 | """
43 | ax = np.arange(kernel_size)
44 | xx, yy = np.meshgrid(ax, ax)
45 |
46 | if center_point is None:
47 | center_point = ((kernel_size - 1) / 2, (kernel_size - 1) / 2)
48 | center_point_x, center_point_y = center_point
49 | kernel = np.exp(-0.5 * (np.square(xx - center_point_x) + np.square(yy - center_point_y)) / np.square(sigma))
50 | if normalize:
51 | kernel = kernel / (np.pi * 2 * sigma ** 2)
52 | kernel /= kernel.sum()
53 | return kernel
54 |
--------------------------------------------------------------------------------
/fastface/utils/preprocess.py:
--------------------------------------------------------------------------------
1 | from typing import List, Tuple
2 |
3 | import torch
4 | import torch.nn.functional as F
5 |
6 |
7 | def prepare_batch(
8 | batch: List[torch.Tensor], target_size: int, adaptive_batch: bool = False
9 | ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
10 | """Convert list of tensors to tensors
11 |
12 | Args:
13 | batch (List[torch.Tensor]): list of tensors(float) as (C x H x W)
14 | target_size (int): maximum dimension size to fit
15 | adaptive_batch (bool, optional): if true than batching will be adaptive,
16 | using max dimension of the batch, otherwise it will use `target_size`, Default: True
17 |
18 | Returns:
19 | Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
20 | (0) : batched inputs as B x C x target_size x target_size
21 | (1) : applied scale factors for each image as torch.FloatTensor(B,)
22 | (2) : applied padding for each image as torch.LongTensor(B,4) pad left, top, right, bottom
23 | """
24 | if adaptive_batch:
25 | # select max dimension in inputs
26 | target_size: int = max([max(img.size(1), img.size(2)) for img in batch])
27 |
28 | scales: List = []
29 | paddings: List = []
30 |
31 | for i, img in enumerate(batch):
32 | # apply interpolation
33 | img_h: int = img.size(-2)
34 | img_w: int = img.size(-1)
35 |
36 | scale_factor: float = min(target_size / img_h, target_size / img_w)
37 |
38 | img = F.interpolate(
39 | img.unsqueeze(0),
40 | scale_factor=scale_factor,
41 | mode="bilinear",
42 | recompute_scale_factor=False,
43 | align_corners=False,
44 | )
45 |
46 | new_h: int = img.size(-2)
47 | new_w: int = img.size(-1)
48 |
49 | # apply padding
50 | pad_left = (target_size - new_w) // 2
51 | pad_right = pad_left + (target_size - new_w) % 2
52 |
53 | pad_top = (target_size - new_h) // 2
54 | pad_bottom = pad_top + (target_size - new_h) % 2
55 |
56 | img = F.pad(img, (pad_left, pad_right, pad_top, pad_bottom), value=0)
57 |
58 | paddings.append([pad_left, pad_top, pad_right, pad_bottom])
59 | scales.append(scale_factor)
60 | batch[i] = img
61 |
62 | batch = torch.cat(batch, dim=0).contiguous()
63 | # pylint: disable=not-callable
64 | scales = torch.tensor(scales, dtype=batch.dtype, device=batch.device)
65 | # pylint: disable=not-callable
66 | paddings = torch.tensor(paddings, dtype=batch.dtype, device=batch.device)
67 |
68 | return batch, scales, paddings
69 |
70 |
71 | def adjust_results(
72 | preds: List[torch.Tensor], scales: torch.Tensor, paddings: torch.Tensor
73 | ) -> torch.Tensor:
74 | """Re-adjust predictions using scales and paddings
75 |
76 | Args:
77 | preds (List[torch.Tensor]): list of torch.Tensor(N, 5) as xmin, ymin, xmax, ymax, score
78 | scales (torch.Tensor): torch.Tensor(B,)
79 | paddings (torch.Tensor): torch.Tensor(B,4) as pad_left, pad_top, pad_right, pad_bottom
80 |
81 | Returns:
82 | torch.Tensor: torch.Tensor(B, N, 5) as xmin, ymin, xmax, ymax, score
83 | """
84 | for i, pred in enumerate(preds):
85 | if pred.size(0) == 0:
86 | continue
87 |
88 | preds[i][:, :4] = pred[:, :4] - paddings[i, :2].repeat(1, 2)
89 | preds[i][:, :4] = pred[:, :4] / scales[i]
90 |
91 | return preds
92 |
--------------------------------------------------------------------------------
/fastface/utils/random.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple
2 |
3 | import numpy as np
4 | import torch
5 |
6 | from .box import cxcywh2xyxy
7 |
8 |
9 | def generate_uniform_boxes(
10 | center_range: Tuple[float, float] = (0.1, 0.9),
11 | wh_range: Tuple[float, float] = (0.2, 0.8),
12 | n: int = 100,
13 | ):
14 |
15 | # TODO pydoc
16 |
17 | cxcy = np.random.uniform(low=center_range[0], high=center_range[1], size=(n, 2))
18 | wh = np.random.uniform(low=wh_range[0], high=wh_range[1], size=(n, 2))
19 |
20 | boxes = np.concatenate([cxcy, wh], axis=1).astype(np.float32)
21 |
22 | boxes = cxcywh2xyxy(torch.from_numpy(boxes))
23 |
24 | return boxes.clamp(min=0, max=1)
25 |
--------------------------------------------------------------------------------
/fastface/utils/vis.py:
--------------------------------------------------------------------------------
1 | import random
2 | from typing import Dict, Tuple
3 |
4 | import numpy as np
5 | from PIL import Image, ImageColor, ImageDraw
6 |
7 |
8 | def render_predictions(
9 | img: np.ndarray, preds: Dict, color: Tuple[int, int, int] = None
10 | ) -> Image:
11 | """Returns Rendered PIL Image using given predictions
12 | Args:
13 | img (np.ndarray): 3 channeled image
14 | preds (Dict): predictions as {'boxes':[[x1,y1,x2,y2], ...], 'scores':[, ..]}
15 | color (Tuple[int,int,int], optional): color of the boundaries. if None that it will be random color.
16 |
17 | Returns:
18 | Image: 3 channeled pil image
19 | """
20 | if color is None:
21 | color = random.choice(list(ImageColor.colormap.keys()))
22 | pil_img = Image.fromarray(img)
23 |
24 | # TODO use score
25 | for (x1, y1, x2, y2), score in zip(preds["boxes"], preds["scores"]):
26 | ImageDraw.Draw(pil_img).rectangle([(x1, y1), (x2, y2)], outline=color, width=3)
27 | return pil_img
28 |
29 |
30 | def render_targets(
31 | img: np.ndarray, targets: Dict, color: Tuple[int, int, int] = None
32 | ) -> Image:
33 | """Returns Rendered PIL Image using given targets
34 | Args:
35 | img (np.ndarray): 3 channeled image
36 | targets (Dict): {'target_boxes':[[x1,y1,x2,y2], ...], ...}
37 | color (Tuple[int,int,int], optional): color of the boundaries. if None that it will be random color.
38 |
39 | Returns:
40 | Image: 3 channeled pil image
41 | """
42 | if color is None:
43 | color = random.choice(list(ImageColor.colormap.keys()))
44 | pil_img = Image.fromarray(img)
45 | for x1, y1, x2, y2 in targets["target_boxes"].astype(np.int32):
46 | ImageDraw.Draw(pil_img).rectangle([(x1, y1), (x2, y2)], outline=color, width=3)
47 | return pil_img
48 |
49 |
50 | def draw_rects(
51 | img: np.ndarray, boxes: np.ndarray, color: Tuple[int, int, int] = None
52 | ) -> np.ndarray:
53 | """Returns Rendered numpy image using given boxes
54 | Args:
55 | img (np.ndarray): 3 channeled image
56 | boxes (np.ndarray): with shape N,4 as xmin,ymin,xmax,ymax
57 | color (Tuple[int,int,int], optional): color of the boundaries. if None that it will be random color.
58 |
59 | Returns:
60 | Image: 3 channeled pil image
61 | """
62 | if color is None:
63 | color = random.choice(list(ImageColor.colormap.keys()))
64 | pil_img = Image.fromarray(img)
65 | for x1, y1, x2, y2 in boxes.astype(np.int32):
66 | ImageDraw.Draw(pil_img).rectangle([(x1, y1), (x2, y2)], outline=color, width=3)
67 | return np.array(pil_img)
68 |
--------------------------------------------------------------------------------
/fastface/version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.1.4"
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pytorch-lightning==1.8.*
2 | torch>=1.8.1
3 | torchvision>=0.9.0
4 | torchmetrics==1.*
5 | imageio~=2.9.0
6 | scipy==1.10.1
7 | googledrivedownloader==0.4
8 | requests~=2.25.0
9 | PyYAML~=5.4.1
10 | checksumdir==1.2.*
--------------------------------------------------------------------------------
/resources/friends.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borhanMorphy/fastface/4db5e4bb2c378d973c5d7da60e78be36c2422f45/resources/friends.jpg
--------------------------------------------------------------------------------
/resources/friends2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borhanMorphy/fastface/4db5e4bb2c378d973c5d7da60e78be36c2422f45/resources/friends2.jpg
--------------------------------------------------------------------------------
/resources/tutorial_1_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borhanMorphy/fastface/4db5e4bb2c378d973c5d7da60e78be36c2422f45/resources/tutorial_1_0.png
--------------------------------------------------------------------------------
/resources/tutorial_1_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borhanMorphy/fastface/4db5e4bb2c378d973c5d7da60e78be36c2422f45/resources/tutorial_1_1.png
--------------------------------------------------------------------------------
/resources/tutorial_1_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borhanMorphy/fastface/4db5e4bb2c378d973c5d7da60e78be36c2422f45/resources/tutorial_1_2.png
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [tool:pytest]
2 | minversion = 6.0
3 | addopts = --cache-clear --doctest-modules --pylint --pylint-error-types=EF --cov=fastface -v
4 | testpaths =
5 | tests
6 | fastface
7 |
8 | [flake8]
9 | max-line-length = 120
10 | exclude =
11 | checkpoints,
12 | *.egg
13 | .git
14 | build
15 | backup
16 | docs
17 |
18 | select = E,W,F
19 | doctests = True
20 | verbose = 2
21 | format = pylint
22 | ignore =
23 | W503 # line break before binary operator
24 | W504 # line break after binary operator
25 | E203 # whitespace before ':'
26 | E731 # do not assign a lambda expression, use a def
27 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import find_packages, setup
2 |
3 |
4 | def get_version() -> str:
5 | with open("fastface/version.py", "r") as foo:
6 | version = foo.read().split("=")[-1].replace("'", "").replace('"', "").strip()
7 | return version
8 |
9 |
10 | __author__ = {"name": "Ömer BORHAN", "email": "borhano.f.42@gmail.com"}
11 |
12 | # load long description
13 | with open("README.md", "r") as foo:
14 | long_description = foo.read()
15 |
16 | # load requirements
17 | with open("requirements.txt", "r") as foo:
18 | requirements = foo.read().split("\n")
19 |
20 | docs_require = [
21 | "sphinxemoji",
22 | "sphinx_rtd_theme",
23 | "recommonmark",
24 | "sphinx_markdown_tables",
25 | "sphinxcontrib-napoleon",
26 | ]
27 |
28 | test_require = [
29 | "pytest",
30 | "pytest-pylint",
31 | "pytest-cov",
32 | ]
33 |
34 | dev_require = (
35 | [
36 | "isort",
37 | "black",
38 | "flake8",
39 | "tox",
40 | "tox-conda",
41 | ]
42 | + test_require
43 | + docs_require
44 | )
45 |
46 | extras_require = {
47 | "test": test_require,
48 | "docs": docs_require,
49 | "dev": dev_require,
50 | "all": dev_require,
51 | }
52 |
53 | setup(
54 | # package name `pip install fastface`
55 | name="fastface",
56 | # package version `major.minor.patch`
57 | version=get_version(),
58 | # small description
59 | description="A face detection framework for edge devices using pytorch lightning",
60 | # long description
61 | long_description=long_description,
62 | # content type of long description
63 | long_description_content_type="text/markdown",
64 | # source code url for this package
65 | url="https://github.com/borhanMorphy/light-face-detection",
66 | # project urls
67 | project_urls={
68 | "Documentation": "https://fastface.readthedocs.io/en/latest/",
69 | },
70 | # author of the repository
71 | author=__author__["name"],
72 | # author's email adress
73 | author_email=__author__["email"],
74 | # package license
75 | license="MIT",
76 | # package root directory
77 | packages=find_packages(),
78 | # requirements
79 | install_requires=requirements,
80 | # extra requirements
81 | extras_require=extras_require,
82 | include_package_data=True,
83 | # keywords that resemble this package
84 | keywords=["pytorch_lightning", "face detection", "edge AI", "LFFD"],
85 | zip_safe=False,
86 | # classifiers for the package
87 | classifiers=[
88 | "Environment :: Console",
89 | "Natural Language :: English",
90 | "Intended Audience :: Developers",
91 | "Intended Audience :: Science/Research",
92 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
93 | "Programming Language :: Python :: 3",
94 | "Programming Language :: Python :: 3.8",
95 | ],
96 | )
97 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borhanMorphy/fastface/4db5e4bb2c378d973c5d7da60e78be36c2422f45/tests/__init__.py
--------------------------------------------------------------------------------
/tests/data/multi_face.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borhanMorphy/fastface/4db5e4bb2c378d973c5d7da60e78be36c2422f45/tests/data/multi_face.jpeg
--------------------------------------------------------------------------------
/tests/data/no_face.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borhanMorphy/fastface/4db5e4bb2c378d973c5d7da60e78be36c2422f45/tests/data/no_face.jpg
--------------------------------------------------------------------------------
/tests/data/single_face.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borhanMorphy/fastface/4db5e4bb2c378d973c5d7da60e78be36c2422f45/tests/data/single_face.jpg
--------------------------------------------------------------------------------
/tests/test_base_apis.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 | import fastface as ff
6 |
7 |
8 | @pytest.mark.parametrize(
9 | "api",
10 | [
11 | "list_pretrained_models",
12 | "download_pretrained_model",
13 | "list_archs",
14 | "list_arch_configs",
15 | "get_arch_config",
16 | ],
17 | )
18 | def test_api_exists(api):
19 | assert api in dir(ff), f"{api} not found in the fastface"
20 |
21 |
22 | def test_list_pretrained_models():
23 | models = ff.list_pretrained_models()
24 | assert isinstance(
25 | models, list
26 | ), f"returned value must be list but found:{type(models)}"
27 | for model in models:
28 | assert isinstance(
29 | model, str
30 | ), f"pretrained model must contain name as string but found:{type(model)}"
31 |
32 |
33 | def test_list_archs():
34 | archs = ff.list_archs()
35 | assert isinstance(
36 | archs, list
37 | ), f"returned value must be list but found:{type(archs)}"
38 | for arch in archs:
39 | assert isinstance(
40 | arch, str
41 | ), f"architecture must contain name as string but found:{type(arch)}"
42 |
43 |
44 | @pytest.mark.parametrize("arch", ff.list_archs())
45 | def test_list_arch_configs(arch: str):
46 | arch_configs = ff.list_arch_configs(arch)
47 | assert isinstance(
48 | arch_configs, list
49 | ), f"returned value must be list but found:{type(arch_configs)}"
50 | for arch_config in arch_configs:
51 | assert isinstance(
52 | arch_config, str
53 | ), f"architecture config must contain string but found:{type(arch_config)}"
54 |
55 |
56 | @pytest.mark.parametrize("arch", ff.list_archs())
57 | def test_get_arch_config(arch: str):
58 | arch_configs = ff.list_arch_configs(arch)
59 | for arch_config in arch_configs:
60 | config = ff.get_arch_config(arch, arch_config)
61 | assert isinstance(
62 | config, dict
63 | ), f"{arch}.{arch_config} must be dictionary but found: {type(config)}"
64 |
65 |
66 | @pytest.mark.parametrize("model_name", ff.list_pretrained_models())
67 | def test_download(model_name: str):
68 | model_file_path = ff.download_pretrained_model(
69 | model_name, target_path=ff.utils.cache.get_model_cache_dir()
70 | )
71 | assert os.path.exists(model_file_path), "model file is not exists in {}".format(
72 | model_file_path
73 | )
74 |
--------------------------------------------------------------------------------
/tests/test_dataset_apis.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import fastface as ff
4 |
5 | # TODO expand this
6 |
7 |
8 | @pytest.mark.parametrize("dataset_name", ["FDDBDataset", "WiderFaceDataset"])
9 | def test_api_exists(dataset_name: str):
10 | assert hasattr(
11 | ff.dataset, dataset_name
12 | ), "{} not found in the fastface.dataset".format(dataset_name)
13 |
--------------------------------------------------------------------------------
/tests/test_loss_apis.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from torchmetrics import Metric
3 |
4 | import fastface as ff
5 |
6 | # TODO expand this
7 |
8 |
9 | @pytest.mark.parametrize(
10 | "metric_name", ["AveragePrecision", "AverageRecall", "WiderFaceAP"]
11 | )
12 | def test_api_exists(metric_name: str):
13 | assert hasattr(
14 | ff.metric, metric_name
15 | ), "{} not found in the fastface.metric".format(metric_name)
16 |
17 |
18 | @pytest.mark.parametrize(
19 | "metric_name", ["AveragePrecision", "AverageRecall", "WiderFaceAP"]
20 | )
21 | def test_get_available_metrics(metric_name: str):
22 | metric_cls = getattr(ff.metric, metric_name)
23 | metric = metric_cls()
24 | assert isinstance(
25 | metric, Metric
26 | ), "returned value must be `torchmetrics.Metric` but found:{}".format(
27 | type(metric)
28 | )
29 |
--------------------------------------------------------------------------------
/tests/test_metric_apis.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from torchmetrics import Metric
3 |
4 | import fastface as ff
5 |
6 | # TODO expand this
7 |
8 |
9 | @pytest.mark.parametrize(
10 | "metric_name", ["AveragePrecision", "AverageRecall", "WiderFaceAP"]
11 | )
12 | def test_api_exists(metric_name: str):
13 | assert hasattr(
14 | ff.metric, metric_name
15 | ), "{} not found in the fastface.metric".format(metric_name)
16 |
17 |
18 | @pytest.mark.parametrize(
19 | "metric_name", ["AveragePrecision", "AverageRecall", "WiderFaceAP"]
20 | )
21 | def test_get_available_metrics(metric_name: str):
22 | metric_cls = getattr(ff.metric, metric_name)
23 | metric = metric_cls()
24 | assert isinstance(
25 | metric, Metric
26 | ), "returned value must be `torchmetrics.Metric` but found:{}".format(
27 | type(metric)
28 | )
29 |
--------------------------------------------------------------------------------
/tests/test_module_apis.py:
--------------------------------------------------------------------------------
1 | import os
2 | import tempfile
3 |
4 | import numpy as np
5 | import pytest
6 | import pytorch_lightning as pl
7 | import torch
8 |
9 | import fastface as ff
10 |
11 | from . import utils
12 |
13 |
14 | @pytest.mark.parametrize(
15 | "api", ["build", "build_from_yaml", "from_checkpoint", "from_pretrained"]
16 | )
17 | def test_api_exists(api):
18 | assert api in dir(
19 | ff.FaceDetector
20 | ), "{} not found in the fastface.FaceDetector".format(api)
21 |
22 |
23 | @pytest.mark.parametrize("arch,config", list(utils.build_module_args()))
24 | def test_module_build(arch: str, config: str):
25 | module = ff.FaceDetector.build(arch, config)
26 | assert isinstance(
27 | module, pl.LightningModule
28 | ), "module must be instance of pl.LightningModule but found:{}".format(type(module))
29 | config = ff.get_arch_config(arch, config)
30 | module = ff.FaceDetector.build(arch, config)
31 | assert isinstance(
32 | module, pl.LightningModule
33 | ), "module must be instance of pl.LightningModule but found:{}".format(type(module))
34 |
35 |
36 | @pytest.mark.parametrize("model_name", ff.list_pretrained_models())
37 | def test_module_from_pretrained(model_name: str):
38 | module = ff.FaceDetector.from_pretrained(model_name)
39 | assert isinstance(
40 | module, pl.LightningModule
41 | ), "module must be instance of pl.LightningModule but found:{}".format(type(module))
42 |
43 |
44 | @pytest.mark.parametrize("model_name", ff.list_pretrained_models())
45 | def test_module_from_checkpoint(model_name: str):
46 | cache_path = ff.utils.cache.get_model_cache_dir()
47 |
48 | model_path = os.path.join(cache_path, model_name)
49 |
50 | if not os.path.exists(model_path):
51 | # download the model
52 | model_path = ff.download_pretrained_model(model_name, target_path=cache_path)
53 |
54 | module = ff.FaceDetector.from_checkpoint(model_path)
55 | assert isinstance(
56 | module, pl.LightningModule
57 | ), "module must be instance of pl.LightningModule but found:{}".format(type(module))
58 |
59 |
60 | @pytest.mark.parametrize(
61 | "yaml_file_path", ["config_zoo/lffd_original.yaml", "config_zoo/lffd_slim.yaml"]
62 | )
63 | def test_module_build_from_yaml(yaml_file_path: str):
64 | module = ff.FaceDetector.build_from_yaml(yaml_file_path)
65 | assert isinstance(
66 | module, pl.LightningModule
67 | ), "module must be instance of pl.LightningModule but found:{}".format(type(module))
68 |
69 |
70 | @pytest.mark.parametrize(
71 | "model_name, img_file_path",
72 | utils.mixup_arguments(ff.list_pretrained_models(), utils.get_img_paths()),
73 | )
74 | def test_module_predict(model_name: str, img_file_path: str):
75 | module = ff.FaceDetector.from_pretrained(model_name)
76 | module.eval()
77 | img = utils.load_image(img_file_path)
78 | preds = module.predict(img)
79 | assert isinstance(
80 | preds, list
81 | ), "prediction result must be list but found {}".format(type(preds))
82 | assert len(preds) == 1, "lenght of predictions must be 1 but found {}".format(
83 | len(preds)
84 | )
85 |
86 |
87 | @pytest.mark.parametrize(
88 | "model_name, img_file_path",
89 | utils.mixup_arguments(ff.list_pretrained_models(), utils.get_img_paths()),
90 | )
91 | def test_module_forward(model_name: str, img_file_path: str):
92 | module = ff.FaceDetector.from_pretrained(model_name)
93 | module.eval()
94 | data = utils.load_image_as_tensor(img_file_path)
95 | preds = module.forward(data)
96 | # preds: N,6
97 | assert isinstance(
98 | preds, torch.Tensor
99 | ), "predictions must be tensor but found {}".format(type(preds))
100 | assert (
101 | len(preds.shape) == 2
102 | ), "prediction shape length must be 2 but found {}".format(len(preds.shape))
103 | assert (
104 | preds.shape[1] == 6
105 | ), "prediction shape index 1 must be 6 but found {}".format(preds.shape[1])
106 | assert (
107 | (preds[:, 4] >= 0) & (preds[:, 4] <= 1)
108 | ).all(), "predicton scores must be between 0 and 1"
109 | assert (preds[:, 5] == 0).all(), "batch dimension of all predictions must be 0"
110 | assert (preds[:, :4] >= 0).all(), "box dimensions of predictions must be positive"
111 | assert (
112 | (preds[:, [2, 3]] - preds[:, [0, 1]]) >= 0
113 | ).all(), "predicted box height and width must be greater than 0"
114 |
115 |
116 | @pytest.mark.parametrize("arch,config", list(utils.build_module_args()))
117 | def test_module_export_to_torchscript(arch: str, config: str):
118 | module = ff.FaceDetector.build(arch, config)
119 | module.eval()
120 |
121 | sc_module = module.to_torchscript()
122 | assert isinstance(
123 | sc_module, torch.jit.ScriptModule
124 | ), "build failed \
125 | for {} with config {}".format(
126 | arch, config
127 | )
128 |
129 | dummy_input = torch.rand(2, 3, 480, 360)
130 |
131 | output = module.forward(dummy_input)
132 |
133 | sc_output = sc_module.forward(dummy_input)
134 |
135 | assert (
136 | output == sc_output
137 | ).all(), "module output and exported module output \
138 | does not match for {} with config {}".format(
139 | arch, config
140 | )
141 |
142 |
143 | @pytest.mark.parametrize("arch,config", list(utils.build_module_args()))
144 | def test_module_export_to_onnx(arch: str, config: str):
145 | try:
146 | import onnxruntime as ort
147 | except ImportError:
148 | pytest.skip("skipping test, onnxruntime is not installed")
149 |
150 | module = ff.FaceDetector.build(arch, config)
151 | module.eval()
152 |
153 | opset_version = 11
154 |
155 | dynamic_axes = {
156 | "input_data": {0: "batch", 2: "height", 3: "width"}, # write axis names
157 | "preds": {0: "batch"},
158 | }
159 |
160 | input_names = ["input_data"]
161 |
162 | output_names = ["preds"]
163 |
164 | input_sample = torch.rand(1, *module.arch.input_shape[1:])
165 |
166 | with tempfile.NamedTemporaryFile(suffix=".onnx", delete=True) as tmpfile:
167 |
168 | module.to_onnx(
169 | tmpfile.name,
170 | input_sample=input_sample,
171 | opset_version=opset_version,
172 | input_names=input_names,
173 | output_names=output_names,
174 | dynamic_axes=dynamic_axes,
175 | export_params=True,
176 | )
177 |
178 | sess = ort.InferenceSession(tmpfile.name)
179 |
180 | del module
181 |
182 | dummy_input = np.random.rand(2, 3, 200, 200).astype(np.float32)
183 | input_name = sess.get_inputs()[0].name
184 | (ort_output,) = sess.run(None, {input_name: dummy_input})
185 |
186 | assert (
187 | len(ort_output.shape) == 2
188 | ), "shape of the output must be length of 2 but found {}".format(
189 | len(ort_output.shape)
190 | )
191 | assert (
192 | ort_output.shape[1] == 6
193 | ), "shape of output must be N,6 but found N,{}".format(ort_output.shape[1])
194 |
--------------------------------------------------------------------------------
/tests/test_transforms_apis.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import fastface as ff
4 |
5 | from . import utils
6 |
7 |
8 | @pytest.mark.parametrize(
9 | "transform_name",
10 | [
11 | # regular
12 | "Compose",
13 | "FaceDiscarder",
14 | "ConditionalInterpolate",
15 | "Interpolate",
16 | "Padding",
17 | "Rotate",
18 | # augmentations
19 | "RandomGaussianBlur",
20 | "ColorJitter",
21 | "LFFDRandomSample",
22 | "RandomHorizontalFlip",
23 | "RandomRotate",
24 | ],
25 | )
26 | def test_api_exists(transform_name: str):
27 | assert hasattr(
28 | ff.transforms, transform_name
29 | ), "{} not found in the fastface.transforms".format(transform_name)
30 |
31 |
32 | @pytest.mark.parametrize("img_file_path", utils.get_img_paths())
33 | def test_interpolate_call(img_file_path: str):
34 | img = utils.load_image(img_file_path)
35 | target_size = 480
36 | interpolate = ff.transforms.Interpolate(target_size=target_size)
37 | result_img, _ = interpolate(img)
38 | assert (
39 | max(result_img.shape) == target_size
40 | ), "expected max dim to be {} but found {}".format(
41 | target_size, max(result_img.shape)
42 | )
43 |
44 |
45 | @pytest.mark.parametrize("img_file_path", utils.get_img_paths())
46 | def test_padding_call(img_file_path: str):
47 | img = utils.load_image(img_file_path)
48 | target_size = max(img.shape)
49 | target_size = (target_size, target_size)
50 | padding = ff.transforms.Padding(target_size=target_size)
51 | result_img, _ = padding(img)
52 | assert (
53 | result_img.shape[:2] == target_size
54 | ), "expected image shape to \
55 | be {} but found {}".format(
56 | target_size, max(result_img.shape[:2])
57 | )
58 |
--------------------------------------------------------------------------------
/tests/test_utility_apis.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import fastface as ff
4 |
5 | # TODO expand here
6 |
7 |
8 | @pytest.mark.parametrize(
9 | "func",
10 | [
11 | "get_cache_dir",
12 | "get_model_cache_dir",
13 | "get_data_cache_dir",
14 | "get_checkpoint_cache_dir",
15 | ],
16 | )
17 | def test_cache_func_exists(func: str):
18 | assert func in dir(
19 | ff.utils.cache
20 | ), "{} not found in the fastface.utils.cache".format(func)
21 |
22 |
23 | @pytest.mark.parametrize(
24 | "func",
25 | [
26 | "get_pkg_root_path",
27 | "get_pkg_arch_path",
28 | "get_registry_path",
29 | "get_registry",
30 | "discover_archs",
31 | "get_arch_pkg",
32 | "get_arch_cls",
33 | ],
34 | )
35 | def test_config_func_exists(func: str):
36 | assert func in dir(
37 | ff.utils.config
38 | ), "{} not found in the fastface.utils.config".format(func)
39 |
40 |
41 | @pytest.mark.parametrize("func", ["render_predictions", "render_targets", "draw_rects"])
42 | def test_visualize_func_exists(func: str):
43 | assert func in dir(ff.utils.vis), "{} not found in the fastface.utils.vis".format(
44 | func
45 | )
46 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | import os
3 | from typing import List, Tuple
4 |
5 | import imageio
6 | import numpy as np
7 | import torch
8 |
9 | import fastface as ff
10 |
11 | __VALID_IMG_EXTS__ = (".jpeg", ".jpg", ".png")
12 |
13 | extract_ext = lambda file_name: os.path.splitext(file_name.lower())[1]
14 |
15 |
16 | def build_module_args() -> Tuple:
17 | for arch in ff.list_archs():
18 | for config in ff.list_arch_configs(arch):
19 | yield (arch, config)
20 |
21 |
22 | def mixup_arguments(*args) -> List:
23 | """mixups given arguments
24 | [argument_1_1, argument_1_2], [argument_2_1] =>
25 | [(argument_1_1, argument_2_1), (argument_1_2, argument_2_1)]
26 |
27 | Returns:
28 | List: [(arg1, arg2), ...]
29 | """
30 | return list(itertools.product(*args))
31 |
32 |
33 | def get_img_paths() -> List:
34 | return [
35 | os.path.join("tests/data/", file_name)
36 | for file_name in os.listdir("tests/data/")
37 | if extract_ext(file_name) in __VALID_IMG_EXTS__
38 | ]
39 |
40 |
41 | def load_image(img_file_path: str) -> np.ndarray:
42 | return imageio.imread(img_file_path)[:, :, :3]
43 |
44 |
45 | def load_image_as_tensor(img_file_path: str) -> torch.Tensor:
46 | img = load_image(img_file_path)
47 | return torch.from_numpy(img).float().permute(2, 0, 1).unsqueeze(0).contiguous()
48 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py{39,38}-{lin,mac,win}
3 |
4 | [testenv]
5 | setenv =
6 | PYTHONPATH = .
7 | deps =
8 | -r{toxinidir}/requirements.txt
9 | commands =
10 | pip install -U ".[test]"
11 | pytest
--------------------------------------------------------------------------------
/tutorials/bentoml_deployment/README.md:
--------------------------------------------------------------------------------
1 | # FastFace BentoML Deployment
2 |
3 | **[BentoML](https://www.bentoml.ai/) is a model serving framework, enabling to deliver prediction services in a fast, repeatable and scalable way.
**
4 |
5 | **This tutorial will explain how to export [fastface](<(https://github.com/borhanMorphy/light-face-detection)>) models as [ONNX](https://onnx.ai/) and deploy to bentoml prediction service.**
6 |
7 | ## Installation
8 |
9 | **install latest fastface and bentoml via pip**
10 |
11 | ```
12 | pip install fastface==0.1.4 BentoML==0.12.1 -U
13 | ```
14 |
15 | ## BentoService Definition
16 |
17 | define BentoService as [service.py](./service.py)
18 |
19 | ```python
20 | from bentoml import env, artifacts, api, BentoService
21 | from bentoml.adapters import ImageInput
22 | from bentoml.frameworks.onnx import OnnxModelArtifact
23 |
24 | import numpy as np
25 | from typing import List, Dict
26 |
27 | @env(infer_pip_packages=True)
28 | @artifacts([
29 | OnnxModelArtifact('model', backend="onnxruntime")
30 | ])
31 | class FaceDetectionService(BentoService):
32 |
33 | def prepare_input(self, img: np.ndarray) -> np.ndarray:
34 | img = np.transpose(img[:, :, :3], (2, 0, 1))
35 | return np.expand_dims(img, axis=0).astype(np.float32)
36 |
37 | def to_json(self, results: np.ndarray) -> Dict:
38 | # results: (N, 6) as x1,y1,x2,y2,score,batch_idx
39 | return {
40 | "boxes": results[:, :4].astype(np.int32).tolist(),
41 | "scores": results[:, 4].astype(np.float32).tolist()
42 | }
43 |
44 | @api(input=ImageInput(), batch=True, mb_max_batch_size=8, mb_max_latency=1000)
45 | def detect(self, imgs: List[np.ndarray]):
46 | input_name = self.artifacts.model.get_inputs()[0].name
47 | preds = []
48 | for img in imgs:
49 | results = self.artifacts.model.run(None, {input_name: self.prepare_input(img) })[0]
50 | preds.append(
51 | self.to_json(results)
52 | )
53 | return preds
54 | ```
55 |
56 | ## Build And Pack BentoService
57 |
58 | define operations as [build.py](./build.py)
59 |
60 | ```python
61 | # import fastface package to get pretrained model
62 | import fastface as ff
63 | import torch
64 | import tempfile
65 |
66 | # pretrained model
67 | pretrained_model_name = "lffd_original"
68 |
69 | # get pretrained model
70 | model = ff.FaceDetector.from_pretrained(pretrained_model_name)
71 |
72 | # export as onnx
73 | opset_version = 11
74 |
75 | dynamic_axes = {
76 | "input_data": {0: "batch", 2: "height", 3: "width"}, # write axis names
77 | "preds": {0: "batch"}
78 | }
79 |
80 | input_names = [
81 | "input_data"
82 | ]
83 |
84 | output_names = [
85 | "preds"
86 | ]
87 |
88 | # define dummy sample
89 | input_sample = torch.rand(1, *model.arch.input_shape[1:])
90 |
91 | # export model as onnx
92 | with tempfile.NamedTemporaryFile(suffix='.onnx', delete=True) as tmpfile:
93 | model.to_onnx(tmpfile.name,
94 | input_sample=input_sample,
95 | opset_version=opset_version,
96 | input_names=input_names,
97 | output_names=output_names,
98 | dynamic_axes=dynamic_axes,
99 | export_params=True
100 | )
101 |
102 | # get FaceDetectionService
103 | from service import FaceDetectionService
104 |
105 | # create FaceDetectionService instance
106 | face_detection_service = FaceDetectionService()
107 |
108 | # Pack the model artifact
109 | face_detection_service.pack('model', tmpfile.name)
110 |
111 | # Save the service to disk for model serving
112 | saved_path = face_detection_service.save(version="v{}".format(ff.__version__))
113 |
114 | print("saved path: {}".format(saved_path))
115 | ```
116 |
117 | run `build.py` with the following
118 |
119 | ```
120 | python build.py
121 | ```
122 |
123 | ## Serving The Model In Production Mode
124 |
125 | To serve model in production mode run the following (model will be served from http://0.0.0.0:5000).
126 |
127 | ```
128 | bentoml serve-gunicorn FaceDetectionService:latest -w 1
129 | ```
130 |
131 | ## Test Rest API
132 |
133 | test rest api with [test.py](./test.py)
134 |
135 | ```python
136 | import requests
137 | from fastface.utils.vis import render_predictions
138 | import imageio
139 |
140 | url = "http://localhost:5000/detect"
141 |
142 | payload={}
143 | files=[
144 | ('image',('friends2.jpg',open('../../resources/friends2.jpg','rb'),'image/jpeg'))
145 | ]
146 | headers = {}
147 |
148 | response = requests.request("POST", url, headers=headers, data=payload, files=files)
149 |
150 | print(response.json())
151 |
152 | pretty_img = render_predictions(imageio.imread('../../resources/friends2.jpg'), response.json())
153 |
154 | # show image
155 | pretty_img.show()
156 | ```
157 |
158 | Output should look like this
159 |
160 | 
161 |
162 | ## Build And Deploy Using Docker
163 |
164 | BentoML also provides docker support for distributing services.
165 |
166 | Run following to build docker image
167 |
168 | ```
169 | docker build --tag face-detection-service $HOME/bentoml/repository/FaceDetectionService/v0.1.4/
170 | ```
171 |
172 | After docker image build is done, run docker container with the following
173 |
174 | ```
175 | docker run -p 5000:5000 -e BENTOML__APISERVER__DEFAULT_GUNICORN_WORKER_COUNTS=1 --name face_detection_service face-detection-service
176 | ```
177 |
--------------------------------------------------------------------------------
/tutorials/bentoml_deployment/build.py:
--------------------------------------------------------------------------------
1 | # import fastface package to get pretrained model
2 | import tempfile
3 |
4 | import torch
5 |
6 | import fastface as ff
7 |
8 | # pretrained model
9 | pretrained_model_name = "lffd_original"
10 |
11 | # get pretrained model
12 | model = ff.FaceDetector.from_pretrained(pretrained_model_name)
13 |
14 | # export as onnx
15 | opset_version = 11
16 |
17 | dynamic_axes = {
18 | "input_data": {0: "batch", 2: "height", 3: "width"}, # write axis names
19 | "preds": {0: "batch"},
20 | }
21 |
22 | input_names = ["input_data"]
23 |
24 | output_names = ["preds"]
25 |
26 | # define dummy sample
27 | input_sample = torch.rand(1, *model.arch.input_shape[1:])
28 |
29 | # export model as onnx
30 | with tempfile.NamedTemporaryFile(suffix=".onnx", delete=True) as tmpfile:
31 | model.to_onnx(
32 | tmpfile.name,
33 | input_sample=input_sample,
34 | opset_version=opset_version,
35 | input_names=input_names,
36 | output_names=output_names,
37 | dynamic_axes=dynamic_axes,
38 | export_params=True,
39 | )
40 |
41 | # get FaceDetectionService
42 | from service import FaceDetectionService
43 |
44 | # create FaceDetectionService instance
45 | face_detection_service = FaceDetectionService()
46 |
47 | # Pack the model artifact
48 | face_detection_service.pack("model", tmpfile.name)
49 |
50 | # Save the service to disk for model serving
51 | saved_path = face_detection_service.save(version="v{}".format(ff.__version__))
52 |
53 | print("saved path: {}".format(saved_path))
54 |
--------------------------------------------------------------------------------
/tutorials/bentoml_deployment/service.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List
2 |
3 | import numpy as np
4 | from bentoml import BentoService, api, artifacts, env
5 | from bentoml.adapters import ImageInput
6 | from bentoml.frameworks.onnx import OnnxModelArtifact
7 |
8 |
9 | @env(infer_pip_packages=True)
10 | @artifacts([OnnxModelArtifact("model", backend="onnxruntime")])
11 | class FaceDetectionService(BentoService):
12 | def prepare_input(self, img: np.ndarray) -> np.ndarray:
13 | img = np.transpose(img[:, :, :3], (2, 0, 1))
14 | return np.expand_dims(img, axis=0).astype(np.float32)
15 |
16 | def to_json(self, results: np.ndarray) -> Dict:
17 | # results: (N, 6) as x1,y1,x2,y2,score,batch_idx
18 | return {
19 | "boxes": results[:, :4].astype(np.int32).tolist(),
20 | "scores": results[:, 4].astype(np.float32).tolist(),
21 | }
22 |
23 | @api(input=ImageInput(), batch=True, mb_max_batch_size=8, mb_max_latency=1000)
24 | def detect(self, imgs: List[np.ndarray]):
25 | input_name = self.artifacts.model.get_inputs()[0].name
26 | preds = []
27 | for img in imgs:
28 | results = self.artifacts.model.run(
29 | None, {input_name: self.prepare_input(img)}
30 | )[0]
31 | preds.append(self.to_json(results))
32 | return preds
33 |
--------------------------------------------------------------------------------
/tutorials/bentoml_deployment/test.py:
--------------------------------------------------------------------------------
1 | import imageio
2 | import requests
3 |
4 | from fastface.utils.vis import render_predictions
5 |
6 | url = "http://localhost:5000/detect"
7 |
8 | payload = {}
9 | files = [
10 | (
11 | "image",
12 | ("friends2.jpg", open("../../resources/friends2.jpg", "rb"), "image/jpeg"),
13 | )
14 | ]
15 | headers = {}
16 |
17 | response = requests.request("POST", url, headers=headers, data=payload, files=files)
18 |
19 | print(response.json())
20 |
21 | pretty_img = render_predictions(
22 | imageio.imread("../../resources/friends2.jpg"), response.json()
23 | )
24 |
25 | # show image
26 | pretty_img.show()
27 |
--------------------------------------------------------------------------------
/tutorials/widerface_benchmark/README.md:
--------------------------------------------------------------------------------
1 | # Widerface Benchmark Tutorial
2 |
3 | ## Setup
4 | Install latest version of `fastface` with
5 | ```
6 | pip install fastface -U
7 | ```
8 |
9 | ## Discovery
10 | `fastface` is packed with varius pretrained models, to see full list run the following
11 | ```
12 | python -c "import fastface as ff;print(ff.list_pretrained_models())"
13 | ```
14 | Output will be look like
15 | ```
16 | ['lffd_original', 'lffd_slim']
17 | ```
18 |
19 | ## Start
20 |
21 | Lets import required packages
22 | ```python
23 | import fastface as ff
24 | import pytorch_lightning as pl
25 | import torch
26 | ```
27 |
28 | Build pretrained model. For this tutorial `lffd_original` is selected but you can also select another model
29 | ```python
30 | model = ff.FaceDetector.from_pretrained("lffd_original")
31 | # model: pl.LightningModule
32 | ```
33 |
34 | **If you don't have pretrained model weights on your PC, `fastface` will automatically download and put it under `$HOME/.cache/fastface//model/`**
35 |
36 |
37 | Add widerface average precision(defined in the widerface competition) metric to the model
38 | ```python
39 | metric = ff.metric.WiderFaceAP(iou_threshold=0.5)
40 | # metric: pl.metrics.Metric
41 |
42 | # add metric to the model
43 | model.add_metric("widerface_ap", metric)
44 | ```
45 |
46 | Define widerface dataset. For this tutorial `easy` partition is selected but `medium` or `hard` partitions are also available
47 | **`Warning!` Do not use `batch_size` > 1**, because tensors can not be stacked due to different size of images. Also using fixed image size drops metric performance.
48 | ```python
49 | ds = ff.dataset.WiderFaceDataset(
50 | phase="test",
51 | partitions=["easy"],
52 | transforms= ff.transforms.Compose(
53 | ff.transforms.ConditionalInterpolate(max_size=1500),
54 | )
55 | )
56 | # ds: torch.utils.data.Dataset
57 |
58 | # get dataloader
59 | dl = ds.get_dataloader(batch_size=1, num_workers=1)
60 | # dl: torch.utils.data.DataLoader
61 | ```
62 |
63 | **If you don't have widerface validation dataset on your PC, `fastface` will automatically download and put it under `$HOME/.cache/fastface//data/widerface/`**
64 |
65 | Define `pytorch_lightning.Trainer`
66 | ```python
67 | trainer = pl.Trainer(
68 | benchmark=True,
69 | logger=False,
70 | checkpoint_callback=False,
71 | gpus=1 if torch.cuda.is_available() else 0,
72 | precision=32)
73 | ```
74 |
75 | Run test
76 | ```python
77 | trainer.test(model, test_dataloaders=dl)
78 | ```
79 |
80 | You should get output like this after test is done
81 |
82 | ```script
83 | --------------------------------------------------------------------------------
84 | DATALOADER:0 TEST RESULTS
85 | {'widerface_ap': 0.8929094818903156}
86 | --------------------------------------------------------------------------------
87 | ```
88 |
89 | Checkout [test_widerface.py](./test_widerface.py) script to see full code
90 |
--------------------------------------------------------------------------------
/tutorials/widerface_benchmark/test_widerface.py:
--------------------------------------------------------------------------------
1 | import pytorch_lightning as pl
2 | import torch
3 |
4 | import fastface as ff
5 |
6 | # model device, select gpu if exists
7 | device = "cuda" if torch.cuda.is_available() else "cpu"
8 |
9 | # widerface dataset partition
10 | partition = "easy" # also `medium` or `hard` can be selectable
11 |
12 | # select and build pretrained model to test on widerface
13 | # for this tutorial `lffd_original` is selected
14 | # for selectable models checkout ff.list_pretrained_models()
15 | model = ff.FaceDetector.from_pretrained("lffd_original")
16 | # model: pl.LightningModule
17 |
18 | # get widerface average precision metric, defined in the competition
19 | metric = ff.metric.WiderFaceAP(iou_threshold=0.5)
20 | # metric: pl.metrics.Metric
21 |
22 | # add metric to the model
23 | model.add_metric("widerface_ap", metric)
24 |
25 | # get widerface dataset
26 | ds = ff.dataset.WiderFaceDataset(
27 | phase="test",
28 | partitions=[partition],
29 | transforms=ff.transforms.Compose(
30 | ff.transforms.ConditionalInterpolate(max_size=1500)
31 | ),
32 | )
33 | # ds: torch.utils.data.Dataset
34 |
35 | # get dataloader
36 | dl = ds.get_dataloader(batch_size=1, num_workers=1)
37 | # dl: torch.utils.data.DataLoader
38 |
39 | # define trainer
40 | trainer = pl.Trainer(
41 | logger=False,
42 | checkpoint_callback=False,
43 | gpus=1 if device == "cuda" else 0,
44 | precision=32,
45 | )
46 |
47 | # run test
48 | trainer.test(model, test_dataloaders=dl)
49 |
--------------------------------------------------------------------------------