Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global review #1

Closed
wants to merge 68 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
ad5cdc7
updating gitignore
rabah-khalek Nov 27, 2023
cd38eda
foundations
rabah-khalek Nov 27, 2023
59ea4af
updated readme
rabah-khalek Nov 27, 2023
1a01cae
Update README.md
rabah-khalek Nov 27, 2023
cbc2577
rm example1.ipynb
rabah-khalek Dec 3, 2023
7e84893
refactored base dataset
rabah-khalek Dec 3, 2023
77eb0a8
refactored dataset_300W and added landmark selection option
rabah-khalek Dec 3, 2023
8bdb471
refactored model
rabah-khalek Dec 3, 2023
e16737d
refactored drawing to handle nan
rabah-khalek Dec 3, 2023
7356bb6
introduced np einsum for the metrics
rabah-khalek Dec 3, 2023
7a22895
updated example1
rabah-khalek Dec 3, 2023
b3670c1
added new example to draw partial landmarks based on facial part
rabah-khalek Dec 3, 2023
4fea928
added example on options with numpy to calculate euc dists
rabah-khalek Dec 3, 2023
1c2c3eb
Merge branch 'main' of https://github.com/Giskard-AI/loreal-poc
rabah-khalek Dec 3, 2023
36b0458
better handling of nan in base dataset
rabah-khalek Dec 3, 2023
58a78d7
adde pre-commit hook
rabah-khalek Dec 3, 2023
bf74cde
updated dataset 300w
rabah-khalek Dec 3, 2023
f790de9
better handling of predict in face alignment wrapper
rabah-khalek Dec 3, 2023
95dbfd4
updated examples
rabah-khalek Dec 3, 2023
d5bdcec
added first draft of slicing in examples
rabah-khalek Dec 3, 2023
1a4d6e7
added filtering features in ex4, todo: integrate
rabah-khalek Dec 3, 2023
66c9e22
better separation between class/inst attr
rabah-khalek Dec 3, 2023
d25193b
updated py version limits in pyproject
rabah-khalek Dec 3, 2023
9fdda1c
fixed left_eye landmarks
rabah-khalek Dec 4, 2023
485ee1f
additional relevant facial parts added
rabah-khalek Dec 4, 2023
faf0870
added marks_for, better handling of paths, additional meta in base da…
rabah-khalek Dec 4, 2023
a1a7edc
added copy, slice to dataset_300W
rabah-khalek Dec 4, 2023
bc968aa
added nme_diff, slicing feature to tests
rabah-khalek Dec 4, 2023
e2aaef2
feature of prediction on facial_parts in model wrapper
rabah-khalek Dec 4, 2023
427b479
added slicing_functions
rabah-khalek Dec 4, 2023
1d51566
updated examples
rabah-khalek Dec 4, 2023
181c839
refactored slicing into transformation, some fixes
rabah-khalek Dec 4, 2023
b05d146
refactored slicing into transformation, some fixes
rabah-khalek Dec 4, 2023
c90289b
fixed typo
rabah-khalek Dec 4, 2023
d86c144
refactoring datasets
rabah-khalek Dec 6, 2023
8decdf5
refactoring models
rabah-khalek Dec 6, 2023
5349f09
added new tests, plus refactoring
rabah-khalek Dec 6, 2023
ce09972
refactoring transformation_functions
rabah-khalek Dec 6, 2023
8034311
updated examples
rabah-khalek Dec 6, 2023
5354689
docstrings added to ModelBase, and Image Types introduced
rabah-khalek Dec 6, 2023
aed6232
image types introduced
rabah-khalek Dec 6, 2023
6af7248
refactored transform method in dataset 300W
rabah-khalek Dec 6, 2023
534dbf7
introduced PredictionResult class
rabah-khalek Dec 6, 2023
8a9dfb7
improved TestResult class
rabah-khalek Dec 6, 2023
c3ad242
introduced fail rate in tests
rabah-khalek Dec 6, 2023
d95af6b
refactoring of transformation functions
rabah-khalek Dec 6, 2023
589b1e4
updated examples
rabah-khalek Dec 6, 2023
29de8ab
added time profiling, refactored tests
rabah-khalek Dec 6, 2023
5450ce1
updated examples
rabah-khalek Dec 6, 2023
4d4e5d5
added name attribute to FacialPart
rabah-khalek Dec 6, 2023
c215500
proper support of the two image types
rabah-khalek Dec 6, 2023
5effb4c
removed PIL support
rabah-khalek Dec 6, 2023
792af42
removed PIL support
rabah-khalek Dec 6, 2023
4b01a85
removed PIL support
rabah-khalek Dec 6, 2023
a462532
removed PIL support
rabah-khalek Dec 6, 2023
87cab70
added possibility to draw squares
rabah-khalek Dec 6, 2023
58dca0b
Added open-cv dependency
rabah-khalek Dec 6, 2023
d4a7ec1
updated examples
rabah-khalek Dec 6, 2023
25f05ef
Update README.md
rabah-khalek Dec 6, 2023
5718f55
fixed typos in datasets/base.py
rabah-khalek Dec 7, 2023
9221793
Add command and format config
Hartorn Dec 8, 2023
0127a6f
Add newly formatted files
Hartorn Dec 8, 2023
9f60a70
Fix the path for notebooks
Hartorn Dec 8, 2023
b7bab80
Merge pull request #2 from Giskard-AI/add-tools
rabah-khalek Dec 8, 2023
67d6687
Improved FacialPart ops
rabah-khalek Dec 9, 2023
1d3b397
improved load_marks_from_file
rabah-khalek Dec 9, 2023
926acef
changed logger name
rabah-khalek Dec 9, 2023
ff0427e
nitpick in models base
rabah-khalek Dec 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
.history
.pdm-python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
.idea
*.iml
300W

# C extensions
*.so
Expand Down
32 changes: 32 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.7
hooks:
- id: ruff
files: '^.*\.py$'
args:
- "--config"
- "pyproject.toml"
- "--fix"
- "--exit-non-zero-on-fix"

- repo: https://github.com/psf/black
rev: 23.11.0
hooks:
- id: black
files: '^.*\.py$'
args:
- "--config"
- "pyproject.toml"
- id: black-jupyter
files: '^.*\.ipynb$'
args:
- "--config"
- "pyproject.toml"


- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,35 @@
# loreal-poc

Assessing the quality of facial landmark models

## Setup

prod-env

```shell
pdm install --prod
source .venv/bin/activate
```

dev-env

```shell
pdm install -G :all
source .venv/bin/activate
pre-commit install
```

## Examples

setup dev-env and check out `examples`.

## Benchmark Datasets

From https://paperswithcode.com/task/facial-landmark-detection

- [x] 300W

## Metrics

- [x] ME (Mean euclidean distances)
- [x] NME (Normalised euclidean distances)
402 changes: 402 additions & 0 deletions examples/criteria1_partial_faces.ipynb

Large diffs are not rendered by default.

196 changes: 196 additions & 0 deletions examples/ex1_draw_landmarks.ipynb

Large diffs are not rendered by default.

104 changes: 104 additions & 0 deletions examples/ex2_draw_partial_landmarks.ipynb

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions examples/ex3_calculate_euc_dists.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"A = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])\n",
"B = np.asarray([[4.0, 0.0], [1.0, 1.0], [2.0, 2.0], [2.0, 3.0]])"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([4. , 1. , 2.23606798, 2.23606798])"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"a_min_b = A - B\n",
"np.sqrt(np.einsum(\"ij,ij->i\", a_min_b, a_min_b))"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([4. , 1. , 2.23606798, 2.23606798])"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"np.linalg.norm(A - B, axis=1)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([4. , 1. , 2.23606798, 2.23606798])"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"d_all = np.sqrt(np.einsum(\"ij->i\", (A - B) ** 2))\n",
"d_all"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.13"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Binary file added examples/imgs/NME.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/imgs/example1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions loreal_poc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .logger import logger

__all__ = ["logger"]
Empty file added loreal_poc/datasets/__init__.py
Empty file.
166 changes: 166 additions & 0 deletions loreal_poc/datasets/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import os
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import Union

import numpy as np
from numpy.lib.mixins import NDArrayOperatorsMixin


@dataclass(frozen=True)
class FacialPart(NDArrayOperatorsMixin):
part: np.ndarray
name: str = ""

def __add__(self, o):
return FacialPart(np.intersect1d((self.part, o.part)))

def __sub__(self, o):
return FacialPart(np.setxor1d((self.part, o.part)))

def __array__(self):
return self.part


# see https://ibug.doc.ic.ac.uk/resources/300-W/ for definitions
_entire = np.arange(0, 68)
_contour = np.arange(0, 17)
__left_contour = np.arange(0, 9)
__right_contour = np.arange(10, 18)
_left_eyebrow = np.arange(17, 22)
_right_eyebrow = np.arange(22, 27)
_nose = np.arange(27, 36)
__left_nose = np.array([31, 32])
__right_nose = np.array([34, 35])
_left_eye = np.arange(36, 42)
_right_eye = np.arange(42, 48)
_mouth = np.arange(48, 68)
__left_mouth = np.array([50, 49, 61, 48, 60, 67, 59, 58])
__right_mouth = np.array([52, 53, 63, 64, 54, 65, 55, 56])
_bottom_half = np.concatenate([np.arange(3, 15), _mouth])
_upper_half = np.setdiff1d(_entire, _bottom_half, assume_unique=True)
__center_axis = np.array([27, 28, 29, 30, 33, 51, 62, 66, 57, 8])
_left_half = np.concatenate([__left_contour, _left_eyebrow, _left_eye, __left_nose, __left_mouth, __center_axis])
_right_half = np.concatenate([np.setdiff1d(_entire, _left_half, assume_unique=True), __center_axis])


@dataclass(frozen=True)
class FacialParts:
entire: FacialPart = FacialPart(_entire, name="entire face")
contour: FacialPart = FacialPart(_contour, name="face contour")
left_eyebrow: FacialPart = FacialPart(_left_eyebrow, name="left eyebrow")
right_eyebrow: FacialPart = FacialPart(_right_eyebrow, name="right eyebrow")
nose: FacialPart = FacialPart(_nose, name="nose")
left_eye: FacialPart = FacialPart(_left_eye, name="left eye")
right_eye: FacialPart = FacialPart(_right_eye, name="right eye")
mouth: FacialPart = FacialPart(_mouth, name="mouth")
bottom_half: FacialPart = FacialPart(_bottom_half, name="bottom half")
upper_half: FacialPart = FacialPart(_upper_half, name="upper half")
left_half: FacialPart = FacialPart(_left_half, name="left half")
right_half: FacialPart = FacialPart(_right_half, name="right half")


class DatasetBase(ABC):
image_suffix: str
marks_suffix: str
n_landmarks: int
n_dimensions: int
image_type: np.ndarray

def __init__(
self,
images_dir_path: Union[str, Path],
landmarks_dir_path: Union[str, Path],
facial_part: FacialPart = FacialParts.entire,
) -> None:
images_dir_path = self._get_absolute_local_path(images_dir_path)
landmarks_dir_path = self._get_absolute_local_path(landmarks_dir_path)

self.image_paths = self._get_all_paths_based_on_suffix(images_dir_path, self.image_suffix)
self.marks_paths = self._get_all_paths_based_on_suffix(landmarks_dir_path, self.marks_suffix)
self._all_marks = None
self._all_images = None
self.facial_part = facial_part

if len(self.marks_paths) != len(self.image_paths):
raise ValueError(
f"{self.__class__.__name__}: Only {len(self.marks_paths)} found "
f"for {len(self.marks_paths)} of the images."
)

self.meta = dict()
self.meta.update(
{"num_samples": len(self), "images_dir_path": images_dir_path, "landmarks_dir_path": landmarks_dir_path}
)

def _get_absolute_local_path(self, local_path: Union[str, Path]):
cwd = os.getcwd()
local_path = Path(cwd) / local_path if cwd not in str(local_path) else local_path
if not os.path.exists(local_path):
raise ValueError(f"{self.__class__.__name__}: {local_path} does not exist")
return local_path

@classmethod
def _get_all_paths_based_on_suffix(cls, dir_path: Union[str, Path], suffix: str):
all_paths = os.listdir(dir_path)
all_paths_with_suffix = sorted([dir_path / x for x in all_paths if x.endswith(suffix)])
if len(all_paths_with_suffix) == 0:
raise ValueError(
f"{cls.__class__.__name__}: Landmarks with suffix {suffix}"
f" requested but no landmarks found in {dir_path}."
)
return all_paths_with_suffix

def __len__(self):
return len(self.image_paths)

@classmethod
@abstractmethod
def load_marks_from_file(cls, mark_file: Path):
...

@classmethod
@abstractmethod
def load_image_from_file(cls, image_file: Path):
...

@property
def all_marks(self):
if self._all_marks is None:
all_marks = np.empty((len(self), self.n_landmarks, self.n_dimensions))
all_marks[:, :, :] = np.nan
for i, marks_path in enumerate(self.marks_paths):
_marks = self.load_marks_from_file(marks_path)
if _marks.shape[0] != self.n_landmarks:
raise ValueError(f"{self.__class__} is only defined for {self.n_landmarks} landmarks.")
if _marks.shape[1] != self.n_dimensions:
raise ValueError(f"{self.__class__} is only defined for {self.n_dimensions} dimensions.")
all_marks[i, :, :] = _marks
self._all_marks = all_marks
return self._all_marks

@property
def all_images(self):
if self._all_images is None:
all_images = list()
for i, image_path in enumerate(self.image_paths):
_image = self.load_image_from_file(image_path)
all_images.append(_image)
self._all_images = all_images
return self._all_images

def all_marks_for(self, part: FacialPart, exclude=False):
idx = ~np.isin(FacialParts.entire, part) if not exclude else np.isin(FacialParts.entire, part)
part_landmarks = self.all_marks.copy()
part_landmarks[:, idx] = np.nan
return part_landmarks

def marks_for(self, part: FacialPart, mark_idx: int, exclude=False):
idx = ~np.isin(FacialParts.entire, part) if not exclude else np.isin(FacialParts.entire, part)
part_landmarks = self.all_marks[mark_idx].copy()
part_landmarks[idx] = np.nan
return part_landmarks


# %%
Loading