├── .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 | ![PyPI](https://img.shields.io/pypi/v/fastface) 4 | [![Documentation Status](https://readthedocs.org/projects/fastface/badge/?version=latest)](https://fastface.readthedocs.io/en/latest/?badge=latest) 5 | [![Downloads](https://pepy.tech/badge/fastface)](https://pepy.tech/project/fastface) 6 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fastface) 7 | ![PyPI - License](https://img.shields.io/pypi/l/fastface) 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 | ![alt text](resources/friends.jpg) 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 | ![alt text](../../resources/friends2.jpg) 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 | --------------------------------------------------------------------------------