diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 3a24117..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -*.jpg filter=lfs diff=lfs merge=lfs -text -*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/cpu-ci.yml b/.github/workflows/cpu-ci.yml index 672767e..b2c07fd 100644 --- a/.github/workflows/cpu-ci.yml +++ b/.github/workflows/cpu-ci.yml @@ -19,17 +19,17 @@ jobs: runs-on: ${{ matrix.os }} steps: - - name: Checkout Repository + - name: Checkout repository uses: actions/checkout@v4 with: lfs: true - - name: Install Prerequisites for Linux + - name: Install prerequisites for Linux if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' }} run: sudo apt update && sudo apt install -y libturbojpeg exiftool ffmpeg libheif-dev poppler-utils - - name: Install Prerequisites for MacOS + - name: Install prerequisites for MacOS if: ${{ matrix.os == 'macos-13' || matrix.os == 'macos-latest' }} run: brew install libjpeg exiftool ffmpeg libheif poppler @@ -40,18 +40,18 @@ jobs: python-version: ${{ matrix.python-version }} check-latest: true - - name: Install Dependencies + - name: Install dependencies run: | python -m pip install -U pip wheel "numpy>=2" "cython>=3.0.12" "setuptools>=69" python setup.py build_ext --inplace python -m pip install . - - name: Lint with Pylint + - name: Lint with pylint run: | python -m pip install pylint python -m pylint pyface --rcfile=.github/workflows/.pylintrc - - name: Run Tests with Pytest + - name: Run tests with pytest run: | mkdir -p tests/coverage python -m pip install pytest pytest-cov typeguard diff --git a/.github/workflows/cuda-ci.yml b/.github/workflows/cuda-ci.yml index c51d6c3..eb403fd 100644 --- a/.github/workflows/cuda-ci.yml +++ b/.github/workflows/cuda-ci.yml @@ -7,15 +7,55 @@ on: - "doc/**" - "**.md" +env: + LOCAL_REGISTRY: localhost:5000 + DOCKERFILE: docker/pr.dockerfile + jobs: + get_runner_and_uid: + name: Get Runner + runs-on: [self-hosted, unicorn] + steps: + - name: Get UID and GID + id: uid_gid + run: | + echo "uid_gid=$(id -u):$(id -g)" >> "$GITHUB_OUTPUT" + outputs: + runner: ${{ runner.name }} + uid: ${{ steps.uid_gid.outputs.uid_gid }} + + build_docker_image: + name: Build Docker Image + needs: [get_runner_and_uid] + runs-on: ${{ needs.get_runner_and_uid.outputs.runner }} + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Build Docker Image + id: docker_build + uses: docker/build-push-action@v3 + with: + file: ${{ env.DOCKERFILE }} + no-cache: false + push: true + tags: ${{ env.LOCAL_REGISTRY }}/pyface-container:ci + + outputs: + image: ${{ env.LOCAL_REGISTRY }}/pyface-container:ci + ci: name: CI + needs: [get_runner_and_uid, build_docker_image] + runs-on: ${{ needs.get_runner_and_uid.outputs.runner }} strategy: - fail-fast: false matrix: - python-version: ["3.10", "3.12"] - - runs-on: ["self-hosted", "unicorn"] + python-version: + - "3.10" + container: + image: ${{ needs.build_docker_image.outputs.image }} + options: --user ${{ needs.get_runner_and_uid.outputs.uid }} --gpus all steps: - name: Checkout repository diff --git a/demo/face_depth.py b/demo/face_depth.py index 791de6f..d2d4ca7 100644 --- a/demo/face_depth.py +++ b/demo/face_depth.py @@ -3,10 +3,10 @@ import pyface.components as pf -cur_folder = cb.get_curdir(__file__) +RESOURCE_DIR = cb.get_curdir(__file__).parent / "tests" / "resources" -def main(img_path: str = str(cur_folder / "data" / "EmmaWatson1.jpg")): +def main(img_path: str = str(RESOURCE_DIR / "EmmaWatson1.jpg")): face_detect = pf.build_face_detection() face_depth = pf.build_face_depth() @@ -16,7 +16,7 @@ def main(img_path: str = str(cur_folder / "data" / "EmmaWatson1.jpg")): results = face_depth([img], [face_box], return_depth=True) plotted = face_depth.draw_results(img, [face_box], results) - out_folder = cur_folder / "output" + out_folder = cb.get_curdir(__file__) / "output" out_folder.mkdir(exist_ok=True, parents=True) cb.imwrite(plotted, out_folder / "face_depth1.png") cb.imwrite(results[0]["depth_img"], out_folder / "face_depth2.png") diff --git a/demo/face_detection.py b/demo/face_detection.py index 33e55ea..48ad861 100644 --- a/demo/face_detection.py +++ b/demo/face_detection.py @@ -5,11 +5,11 @@ import pyface.components as pf -cur_folder = cb.get_curdir(__file__) +RESOURCE_DIR = cb.get_curdir(__file__).parent / "tests" / "resources" def main( - img_path: str = str(cur_folder / "data" / "EmmaWatson1.jpg"), + img_path: str = str(RESOURCE_DIR / "EmmaWatson1.jpg"), score_th: float = 0.5, inp_size: Tuple[int, int] = (480, 640), ): @@ -22,7 +22,7 @@ def main( proposals_list = face_detection([img] * 7) plotted = face_detection.draw_proposals(img, proposals_list[0]) - out_folder = cur_folder / "output" + out_folder = cb.get_curdir(__file__) / "output" out_folder.mkdir(exist_ok=True, parents=True) cb.imwrite(plotted, out_folder / "face_detection.png") diff --git a/demo/face_gender.py b/demo/face_gender.py index 11d88f4..0fb4ad6 100644 --- a/demo/face_gender.py +++ b/demo/face_gender.py @@ -3,10 +3,10 @@ import pyface.components as pf -cur_folder = cb.get_curdir(__file__) +RESOURCE_DIR = cb.get_curdir(__file__).parent / "tests" / "resources" -def main(img_path: str = str(cur_folder / "data" / "EmmaWatson1.jpg")): +def main(img_path: str = str(RESOURCE_DIR / "EmmaWatson1.jpg")): face_detect = pf.build_face_detection() face_gender = pf.build_gender_detection() @@ -16,7 +16,7 @@ def main(img_path: str = str(cur_folder / "data" / "EmmaWatson1.jpg")): results = face_gender([img], [face_box]) plotted = face_gender.draw_results(img, [face_box], results) - out_folder = cur_folder / "output" + out_folder = cb.get_curdir(__file__) / "output" out_folder.mkdir(exist_ok=True, parents=True) cb.imwrite(plotted, out_folder / "face_gender.png") diff --git a/demo/face_landmark.py b/demo/face_landmark.py index 2790a50..77ba853 100644 --- a/demo/face_landmark.py +++ b/demo/face_landmark.py @@ -5,11 +5,11 @@ import pyface as pf -cur_folder = cb.get_curdir(__file__) +RESOURCE_DIR = cb.get_curdir(__file__).parent / "tests" / "resources" def main( - img_path: str = str(cur_folder / "data" / "EmmaWatson1.jpg"), + img_path: str = str(RESOURCE_DIR / "EmmaWatson1.jpg"), score_th: float = 0.5, inp_size: Tuple[int, int] = (480, 640), ): @@ -25,7 +25,7 @@ def main( result = face_landmark([img], [box])[0] plotted = face_landmark.draw_result(img, box, result, plot_details=True) - out_folder = cur_folder / "output" + out_folder = cb.get_curdir(__file__) / "output" out_folder.mkdir(exist_ok=True, parents=True) cb.imwrite(plotted, out_folder / "face_landmark.jpg") diff --git a/demo/face_normalization.py b/demo/face_normalization.py index 78da1b6..5c2ede8 100644 --- a/demo/face_normalization.py +++ b/demo/face_normalization.py @@ -5,15 +5,15 @@ import pyface.components as pf -cur_folder = cb.get_curdir(__file__) +RESOURCE_DIR = cb.get_curdir(__file__).parent / "tests" / "resources" def main( - img_path: str = str(cur_folder / "data" / "EmmaWatson1.jpg"), + img_path: str = str(RESOURCE_DIR / "EmmaWatson1.jpg"), scale: float = 1, dst_size: Tuple[int, int] = (224, 224), ): - face_detection = pf.SCRFD() + face_detection = pf.build_face_detection() face_normaliation = pf.FaceNormalize( dst_size=dst_size, interpolation=cb.INTER.BILINEAR, @@ -22,13 +22,9 @@ def main( img = cb.imread(img_path) proposals = face_detection([img])[0] norm_img = face_normaliation([img], [cb.Keypoints(proposals["lmk5pts"][0])])[0] - norm_img = cb.draw_keypoints( - norm_img, - face_normaliation.destination_pts, - scale=1, - ) + norm_img = cb.draw_keypoints(norm_img, face_normaliation.destination_pts, scale=1) - out_folder = cur_folder / "output" + out_folder = cb.get_curdir(__file__) / "output" out_folder.mkdir(exist_ok=True, parents=True) cb.imwrite(norm_img, out_folder / f"face_normalize_s={scale}.png") diff --git a/demo/face_recognition.py b/demo/face_recognition.py index edf2ad3..64dd653 100644 --- a/demo/face_recognition.py +++ b/demo/face_recognition.py @@ -3,12 +3,12 @@ import pyface as pf -cur_folder = cb.get_curdir(__file__) +RESOURCE_DIR = cb.get_curdir(__file__).parent / "tests" / "resources" def main( - src_path: str = str(cur_folder / "data" / "EmmaWatson1.jpg"), - tgt_path: str = str(cur_folder / "data" / "face_bank" / "EmmaWatson.jpg"), + src_path: str = str(RESOURCE_DIR / "EmmaWatson1.jpg"), + tgt_path: str = str(RESOURCE_DIR / "face_bank" / "EmmaWatson.jpg"), ): face_detection = pf.build_face_detection() face_recognition = pf.build_face_recognition() @@ -17,7 +17,7 @@ def main( tgt_img = cb.imread(tgt_path) lmks = [p["lmk5pts"][0] for p in face_detection([src_img, tgt_img])] - out_folder = cur_folder / "output" + out_folder = cb.get_curdir(__file__) / "output" out_folder.mkdir(exist_ok=True, parents=True) results = face_recognition([src_img, tgt_img], lmks) src_emb = results[0]["embeddings"] diff --git a/demo/face_service.py b/demo/face_service.py index 01a9a72..0e83300 100644 --- a/demo/face_service.py +++ b/demo/face_service.py @@ -3,12 +3,12 @@ import pyface as pf -cur_folder = cb.get_curdir(__file__) +RESOURCE_DIR = cb.get_curdir(__file__).parent / "tests" / "resources" def main( - img_path: str = str(cur_folder / "data" / "EmmaWatson1.jpg"), - face_bank: str = str(cur_folder / "data" / "face_bank"), + img_path: str = str(RESOURCE_DIR / "EmmaWatson1.jpg"), + face_bank: str = str(RESOURCE_DIR / "face_bank"), ): face_service = pf.FaceService( enable_gender=True, @@ -19,7 +19,9 @@ def main( ) img = cb.imread(img_path) faces_on_img = face_service([img], do_1n=True)[0] - cb.imwrite(faces_on_img.gen_info_img(), str(cur_folder / "output" / "face_service.jpg")) + out_folder = cb.get_curdir(__file__) / "output" + out_folder.mkdir(exist_ok=True, parents=True) + cb.imwrite(faces_on_img.gen_info_img(), str(out_folder / "face_service.jpg")) if __name__ == "__main__": diff --git a/docker/Dockerfile b/docker/Dockerfile index 15c6a8a..72cc3a7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:experimental -FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04 as builder +FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu24.04 as builder ENV PYTHONDONTWRITEBYTECODE=1 \ DEBIAN_FRONTEND=noninteractive \ diff --git a/docker/pr.dockerfile b/docker/pr.dockerfile new file mode 100644 index 0000000..dc60eb1 --- /dev/null +++ b/docker/pr.dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:experimental +FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu24.04 as builder + +ENV PYTHONDONTWRITEBYTECODE=1 \ + DEBIAN_FRONTEND=noninteractive \ + TZ=Asia/Taipei + +RUN apt-get update -y && apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + tzdata wget git git-lfs libturbojpeg exiftool ffmpeg poppler-utils libpng-dev \ + libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev gcc \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + python3-pip libharfbuzz-dev libfribidi-dev libxcb1-dev libfftw3-dev gosu \ + libpq-dev python3-dev build-essential && \ + ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && \ + dpkg-reconfigure -f noninteractive tzdata && \ + apt-get clean && rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/pyface/components/face_depth/tddfav2.py b/pyface/components/face_depth/tddfav2.py index 34b6fa5..2dfbde6 100644 --- a/pyface/components/face_depth/tddfav2.py +++ b/pyface/components/face_depth/tddfav2.py @@ -7,7 +7,7 @@ from ..enums import FacePose from ..utils import append_to_batch, detach_from_batch, download_model_and_return_model_fpath -from .Sim3DR import sim3dr_cython +from .Sim3DR import sim3dr_cython # pylint: disable=E0611 def rasterize( @@ -168,9 +168,7 @@ def _prepare_bfm_npz( return u_base, w_shp_base, w_exp_base -def _parse_tffda_param( - param: np.ndarray, -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: +def _parse_tffda_param(param: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """ matrix pose form param: shape = (trans_dim + shape_dim + exp_dim) i.e., 62 = 12 + 40 + 10 @@ -261,16 +259,12 @@ def initialize(self) -> None: dummy_inputs = {k: np.zeros(input_dims[k], dtype=v["dtype"]) for k, v in self.bfm_engine.input_infos.items()} self.bfm_engine(**dummy_inputs) - def _transform( - self, - img: np.ndarray, - box: cb.Box, - ) -> Tuple[np.ndarray, Tuple[float, float], np.ndarray]: + def _transform(self, img: np.ndarray, box: cb.Box) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: inp_w, inp_h = self.engine.input_infos["input"]["shape"][2:] expanded_box = box.square().scale(fx=0.98, fy=0.98) + scale = np.array([expanded_box.width / inp_w, expanded_box.height / inp_h]) shift = expanded_box.left_top - scale = expanded_box.width / inp_w, expanded_box.height / inp_h blob = cb.imcropbox(img, expanded_box) blob = cb.imresize(blob, (inp_w, inp_h)) blob = blob.transpose(2, 0, 1)[None] @@ -280,7 +274,7 @@ def preprocess( self, imgs: List[np.ndarray], boxes: List[cb.Box], - ): + ) -> Tuple[List[np.ndarray], List[np.ndarray], List[np.ndarray]]: if len(imgs) != len(boxes): raise ValueError(f"imgs and boxes should have same length, but got {len(imgs)} and {len(boxes)}") @@ -294,11 +288,7 @@ def preprocess( shifts.append(shift) return blobs, scales, shifts - def _reconstruct_vertices( - self, - param: np.ndarray, - dense_flag: bool = False, - ) -> np.ndarray: + def _reconstruct_vertices(self, param: np.ndarray, dense_flag: bool = False) -> np.ndarray: def _similar_transform(pts3d, scales): x_s, y_s = scales[..., 1], scales[..., 0] z_s = (x_s + y_s) / 2 @@ -374,15 +364,22 @@ def __call__( blobs, scales, shifts = self.preprocess(imgs, boxes) preds = {k: [] for k in self.engine.output_infos.keys()} for batch in cb.make_batch(blobs, self.batch_size): + current_batch_size = len(batch) inputs = { name: np.concatenate(append_to_batch(batch, self.batch_size)).astype(v["dtype"]) # E1123 for name, v in self.engine.input_infos.items() } tmp_preds = self.engine(**inputs) for k, v in tmp_preds.items(): - preds[k].append(detach_from_batch(v, len(batch))) + preds[k].append(detach_from_batch(v, current_batch_size)) preds = {k: np.concatenate(v, 0) for k, v in preds.items()} - params = np.concatenate((preds["params"], scales, shifts), -1) + scales = np.stack(scales, 0) # n x 2 + shifts = np.stack(shifts, 0) # n x 2 + # Ensure shapes are compatible for concatenation + n = preds["params"].shape[0] + if scales.shape[0] != n or shifts.shape[0] != n: + raise ValueError(f"Shape mismatch: preds['params'] has {n} rows, scales has {scales.shape[0]}, shifts has {shifts.shape[0]}") + params = np.concatenate((preds["params"], scales, shifts), axis=-1) lmk3d68pts = self._gen_3d_landmarks(params) pose_degrees = self._get_pose_degrees(params) diff --git a/pyface/components/face_detection/scrfd.py b/pyface/components/face_detection/scrfd.py index 1eb51f7..ef24dfd 100644 --- a/pyface/components/face_detection/scrfd.py +++ b/pyface/components/face_detection/scrfd.py @@ -193,10 +193,11 @@ def __call__(self, imgs: List[np.ndarray]) -> List[dict]: preds = {k: [] for k in self.engine.output_infos.keys()} b = self.metadata["InputSize"][0] for batch in cb.make_batch(blobs, b): + current_batch_size = len(batch) inputs = {name: np.concatenate(append_to_batch(batch, b)) for name, _ in self.engine.input_infos.items()} tmp_preds = self.engine(**inputs) for k, v in tmp_preds.items(): - preds[k].append(detach_from_batch(v, len(batch))) + preds[k].append(detach_from_batch(v, current_batch_size)) preds = {k: np.concatenate(v, 0) for k, v in preds.items()} proposals_list = self.postprocess(preds, scales) return proposals_list diff --git a/pyface/components/face_gender.py b/pyface/components/face_gender.py index aae7d2b..703bcd8 100644 --- a/pyface/components/face_gender.py +++ b/pyface/components/face_gender.py @@ -113,10 +113,11 @@ def __call__(self, imgs: List[np.ndarray], boxes: List[cb.Box]) -> List[dict]: preds = {k: [] for k in self.engine.output_infos.keys()} b = self.metadata["InputSize"][0] for batch in cb.make_batch(blobs, b): + current_batch_size = len(batch) inputs = {name: np.concatenate(append_to_batch(batch, b)) for name, _ in self.engine.input_infos.items()} tmp_preds = self.engine(**inputs) for k, v in tmp_preds.items(): - preds[k].append(detach_from_batch(v, len(batch))) + preds[k].append(detach_from_batch(v, current_batch_size)) preds = {k: np.concatenate(v, 0) for k, v in preds.items()} preds = self.postprocess(preds) return preds diff --git a/pyface/components/face_recognition/arcface.py b/pyface/components/face_recognition/arcface.py index b500444..2b090f6 100644 --- a/pyface/components/face_recognition/arcface.py +++ b/pyface/components/face_recognition/arcface.py @@ -85,6 +85,7 @@ def __call__( preds = {k: [] for k in self.engine.output_infos.keys()} b = self.metadata["InputSize"][0] for batch in cb.make_batch(blobs, b): + current_batch_size = len(batch) inputs = {name: np.concatenate(append_to_batch(batch, b)) for name, _ in self.engine.input_infos.items()} tmp_preds = self.engine(**inputs) if self.enable_flip: @@ -93,7 +94,7 @@ def __call__( for k, v in tmp_preds.items(): tmp_preds[k] = np.concatenate([v, tmp_flip_preds[k]], 1) for k, v in tmp_preds.items(): - preds[k].append(detach_from_batch(v, len(batch))) + preds[k].append(detach_from_batch(v, current_batch_size)) preds = {k: np.concatenate(v, 0) for k, v in preds.items()} emgeddings = l2_norm(preds["encode"]) return [ diff --git a/pyface/utils.py b/pyface/utils.py index da1ac4b..3597f37 100644 --- a/pyface/utils.py +++ b/pyface/utils.py @@ -1,6 +1,3 @@ -import capybara as cb -import onnxruntime as ort - from .components.face_depth.tddfav2 import TDDFAV2 from .components.face_detection.scrfd import SCRFD from .components.face_gender import GenderDetector @@ -9,7 +6,6 @@ __all__ = [ "download_models", - "get_ort_backend", ] @@ -19,22 +15,3 @@ def download_models(): GenderDetector.download_models() CoordinateReg.download_models() ArcFace.download_models() - - -def cuda_avaliable(): - providers = ort.get_available_providers() - return "CUDAExecutionProvider" in providers - - -def coreml_avaliable(): - providers = ort.get_available_providers() - return "CoreMLExecutionProvider" in providers - - -def get_ort_backend(): - if cuda_avaliable(): - return cb.Backend.cuda - elif coreml_avaliable(): - return cb.Backend.coreml - else: - return cb.Backend.cpu diff --git a/tests/resources/EmmaWatson1.jpg b/tests/resources/EmmaWatson1.jpg new file mode 100644 index 0000000..40919d4 Binary files /dev/null and b/tests/resources/EmmaWatson1.jpg differ diff --git a/tests/resources/EmmaWatson1.png b/tests/resources/EmmaWatson1.png deleted file mode 100644 index 9022558..0000000 --- a/tests/resources/EmmaWatson1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7a06751436d341d1d4389b01db504d35ea477e7a10d80b38951b6d7d8194d650 -size 1257800 diff --git a/tests/resources/JohnnyDepp1.jpg b/tests/resources/JohnnyDepp1.jpg new file mode 100644 index 0000000..b3b823a Binary files /dev/null and b/tests/resources/JohnnyDepp1.jpg differ diff --git a/tests/resources/JohnnyDepp1.png b/tests/resources/JohnnyDepp1.png deleted file mode 100644 index eea2d5c..0000000 --- a/tests/resources/JohnnyDepp1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dd88f4714180e7ef110c2f06c043ac4e2cfd5bd1b4301d2820b114df9a8664b2 -size 1667229 diff --git a/tests/resources/answer/EmmaWatson1_depth.npz b/tests/resources/answer/EmmaWatson1_depth.npz new file mode 100644 index 0000000..516ca29 Binary files /dev/null and b/tests/resources/answer/EmmaWatson1_depth.npz differ diff --git a/tests/resources/answer/EmmaWatson1_detection.npz b/tests/resources/answer/EmmaWatson1_detection.npz new file mode 100644 index 0000000..c9fc45a Binary files /dev/null and b/tests/resources/answer/EmmaWatson1_detection.npz differ diff --git a/tests/resources/answer/JohnnyDepp1_depth.npz b/tests/resources/answer/JohnnyDepp1_depth.npz new file mode 100644 index 0000000..344d751 Binary files /dev/null and b/tests/resources/answer/JohnnyDepp1_depth.npz differ diff --git a/tests/resources/answer/JohnnyDepp1_detection.npz b/tests/resources/answer/JohnnyDepp1_detection.npz new file mode 100644 index 0000000..d43145e Binary files /dev/null and b/tests/resources/answer/JohnnyDepp1_detection.npz differ diff --git a/tests/resources/face_bank/EmmaWatson.jpg b/tests/resources/face_bank/EmmaWatson.jpg new file mode 100644 index 0000000..da4d54d Binary files /dev/null and b/tests/resources/face_bank/EmmaWatson.jpg differ diff --git a/tests/resources/face_bank/EmmaWatson.png b/tests/resources/face_bank/EmmaWatson.png deleted file mode 100644 index 9168717..0000000 --- a/tests/resources/face_bank/EmmaWatson.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1abd290e74139db3e22ac9dea2a942f27e626d076dd4375020d4849f5a98125b -size 1943539 diff --git a/tests/resources/face_bank/JohnnyDepp.jpg b/tests/resources/face_bank/JohnnyDepp.jpg new file mode 100644 index 0000000..e3a3fa0 Binary files /dev/null and b/tests/resources/face_bank/JohnnyDepp.jpg differ diff --git a/tests/resources/face_bank/JohnnyDepp.png b/tests/resources/face_bank/JohnnyDepp.png deleted file mode 100644 index dfc3ecd..0000000 --- a/tests/resources/face_bank/JohnnyDepp.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:898f56977f7a50f6e38e90b94cf67d37567b9b845dc36bf4141fe98ad0fbfe76 -size 1250454 diff --git a/tests/resources/xl_face.jpg b/tests/resources/xl_face.jpg new file mode 100644 index 0000000..6e394e2 Binary files /dev/null and b/tests/resources/xl_face.jpg differ diff --git a/tests/resources/xl_face.png b/tests/resources/xl_face.png deleted file mode 100644 index 7dfda05..0000000 --- a/tests/resources/xl_face.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:25c2c283659ec9175a924ced630bfe0d99d45fde735fe91079709d8c653a493c -size 450456 diff --git a/tests/resources/xs_faces.jpg b/tests/resources/xs_faces.jpg new file mode 100644 index 0000000..4eeee3c Binary files /dev/null and b/tests/resources/xs_faces.jpg differ diff --git a/tests/resources/xs_faces.png b/tests/resources/xs_faces.png deleted file mode 100644 index a669d64..0000000 --- a/tests/resources/xs_faces.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:97a93bfd39e8b7d1e8ee4906931a08a0b5b1ac6c4089313123983f66ff3069b0 -size 4645655 diff --git a/tests/test_face_depth.py b/tests/test_face_depth.py new file mode 100644 index 0000000..0df28dc --- /dev/null +++ b/tests/test_face_depth.py @@ -0,0 +1,72 @@ +import capybara as cb +import numpy as np +import pytest + +from pyface import build_face_depth +from tests.tools import assert_allclose + +RESOURCE_DIR = cb.get_curdir(__file__) / "resources" +ANSWER_DIR = RESOURCE_DIR / "answer" +ANSWER_DIR.mkdir(exist_ok=True, parents=True) + +TEST_DATA = [ + { + "img_fpath": RESOURCE_DIR / "EmmaWatson1.jpg", + "detect": ANSWER_DIR / "EmmaWatson1_detection.npz", + "expected": ANSWER_DIR / "EmmaWatson1_depth.npz", + }, + { + "img_fpath": RESOURCE_DIR / "JohnnyDepp1.jpg", + "detect": ANSWER_DIR / "JohnnyDepp1_detection.npz", + "expected": ANSWER_DIR / "JohnnyDepp1_depth.npz", + }, +] + + +@pytest.mark.parametrize("data", TEST_DATA) +def test_face_depth(data): + m = build_face_depth(backend="cpu") + img = cb.imread(data["img_fpath"]) + expected = np.load(data["expected"], allow_pickle=True) + boxes = np.load(data["detect"], allow_pickle=True)["boxes"] + results = m(imgs=[img], boxes=cb.Boxes(boxes), return_depth=True)[0] + + assert_allclose(results["param"], expected["param"]) + assert_allclose(results["lmk3d68pt"], expected["lmk3d68pt"]) + assert_allclose(results["pose_degree"], expected["pose_degree"]) + assert_allclose(results["depth_img"], expected["depth_img"]) + + +@pytest.mark.parametrize("data", TEST_DATA) +def test_face_depth_batch(data): + m = build_face_depth(batch_size=4) + img = cb.imread(data["img_fpath"]) + expected = np.load(data["expected"], allow_pickle=True) + boxes = np.load(data["detect"], allow_pickle=True)["boxes"] + results = m(imgs=[img] * 6, boxes=cb.Boxes(np.tile(boxes, (6, 1))), return_depth=True) + + for result in results: + assert_allclose(result["param"], expected["param"]) + assert_allclose(result["lmk3d68pt"], expected["lmk3d68pt"]) + assert_allclose(result["pose_degree"], expected["pose_degree"]) + assert_allclose(result["depth_img"], expected["depth_img"]) + + +def gen_target(): + m = build_face_depth(backend="cpu") + for data in TEST_DATA: + img = cb.imread(data["img_fpath"]) + boxes = np.load(data["detect"], allow_pickle=True)["boxes"] + results = m(imgs=[img], boxes=cb.Boxes(boxes), return_depth=True)[0] + np.savez_compressed( + data["expected"], + param=results["param"], + lmk3d68pt=results["lmk3d68pt"], + pose_degree=results["pose_degree"], + depth_img=results["depth_img"], + allow_pickle=False, + ) + + +if __name__ == "__main__": + gen_target() diff --git a/tests/test_face_detection.py b/tests/test_face_detection.py index 1b808e6..86c67a5 100644 --- a/tests/test_face_detection.py +++ b/tests/test_face_detection.py @@ -1,81 +1,87 @@ import capybara as cb -import cv2 import numpy as np import pytest import pyface as pf +from tests.tools import assert_allclose RESOURCE_DIR = cb.get_curdir(__file__) / "resources" +ANSWER_DIR = RESOURCE_DIR / "answer" +ANSWER_DIR.mkdir(exist_ok=True, parents=True) TEST_DATA = [ { - "img_fpath": RESOURCE_DIR / "EmmaWatson1.png", - "expected": { - "boxes": np.array([[412.52731323, 194.85551453, 751.04302979, 642.71121216]], dtype="float32"), - "lmk5pts": np.array( - [ - [ - [449.80097198, 366.0531826], - [581.79126072, 367.359972], - [465.10058594, 458.07265472], - [460.2443161, 527.72718811], - [570.59969902, 529.41928101], - ] - ], - dtype="float32", - ), - "scores": np.array([[0.80300564]], dtype="float32"), - }, + "img_fpath": RESOURCE_DIR / "EmmaWatson1.jpg", + "expected": ANSWER_DIR / "EmmaWatson1_detection.npz", }, { - "img_fpath": RESOURCE_DIR / "JohnnyDepp1.png", - "expected": { - "boxes": np.array([[434.57501197, 188.672789, 691.22249818, 507.53144979]], dtype="float32"), - "lmk5pts": np.array( - [ - [ - [478.99239435, 284.38753269], - [585.12645214, 287.86553004], - [500.60212723, 351.98840269], - [480.54181178, 422.01026003], - [553.02625371, 426.00003743], - ] - ], - dtype="float32", - ), - "scores": np.array([[0.7277902]], dtype="float32"), - }, + "img_fpath": RESOURCE_DIR / "JohnnyDepp1.jpg", + "expected": ANSWER_DIR / "JohnnyDepp1_detection.npz", }, ] @pytest.mark.parametrize("data", TEST_DATA) -def test_build_face_detection(data): - model = pf.build_face_detection( +def test_face_detection(data): + m = pf.build_face_detection( name="scrfd", model_name="scrfd_10g_gnkps_fp32", - backend=pf.get_ort_backend(), + backend=cb.get_recommended_backend(), ) - img = cv2.imread(data["img_fpath"]) - faces_on_img = model(imgs=[img])[0] + img = cb.imread(data["img_fpath"]) + expected = np.load(data["expected"]) + faces_on_img = m(imgs=[img])[0] assert faces_on_img["infos"]["num_proposals"] - np.testing.assert_allclose(faces_on_img["boxes"], data["expected"]["boxes"], rtol=1e-4) - np.testing.assert_allclose(faces_on_img["lmk5pts"], data["expected"]["lmk5pts"], rtol=1e-4) - np.testing.assert_allclose(faces_on_img["scores"], data["expected"]["scores"], rtol=1e-4) + assert_allclose(faces_on_img["boxes"], expected["boxes"]) + assert_allclose(faces_on_img["lmk5pts"], expected["lmk5pts"]) + assert_allclose(faces_on_img["scores"], expected["scores"]) @pytest.mark.parametrize("data", TEST_DATA) -def test_build_face_detection_cpu(data): - model = pf.build_face_detection( +def test_face_detection_cpu(data): + m = pf.build_face_detection(name="scrfd", model_name="scrfd_10g_gnkps_fp32", backend="cpu") + img = cb.imread(data["img_fpath"]) + expected = np.load(data["expected"], allow_pickle=True) + faces_on_img = m(imgs=[img] * 8)[0] + + assert faces_on_img["infos"]["num_proposals"] + assert_allclose(faces_on_img["boxes"], expected["boxes"]) + assert_allclose(faces_on_img["lmk5pts"], expected["lmk5pts"]) + assert_allclose(faces_on_img["scores"], expected["scores"]) + + +@pytest.mark.parametrize("data", TEST_DATA) +def test_face_detection_batch(data): + m = pf.build_face_detection( name="scrfd", model_name="scrfd_10g_gnkps_fp32", - backend="cpu", + batch_size=4, + backend=cb.get_recommended_backend(), ) - img = cv2.imread(data["img_fpath"]) - faces_on_img = model(imgs=[img])[0] + img = cb.imread(data["img_fpath"]) + expected = np.load(data["expected"], allow_pickle=True) + faces_list = m(imgs=[img] * 5) + for faces in faces_list: + assert faces["infos"]["num_proposals"] + assert_allclose(faces["boxes"], expected["boxes"]) + assert_allclose(faces["lmk5pts"], expected["lmk5pts"]) + assert_allclose(faces["scores"], expected["scores"]) - assert faces_on_img["infos"]["num_proposals"] - np.testing.assert_allclose(faces_on_img["boxes"], data["expected"]["boxes"], rtol=1e-4) - np.testing.assert_allclose(faces_on_img["lmk5pts"], data["expected"]["lmk5pts"], rtol=1e-4) - np.testing.assert_allclose(faces_on_img["scores"], data["expected"]["scores"], rtol=1e-4) + +def gen_target(): + m = pf.build_face_detection(name="scrfd", model_name="scrfd_10g_gnkps_fp32", backend="cpu") + for data in TEST_DATA: + img = cb.imread(data["img_fpath"]) + faces_on_img = m(imgs=[img] * 8)[0] + np.savez_compressed( + data["expected"], + boxes=faces_on_img["boxes"], + lmk5pts=faces_on_img["lmk5pts"], + scores=faces_on_img["scores"], + allow_pickle=False, + ) + + +if __name__ == "__main__": + gen_target() diff --git a/tests/tools.py b/tests/tools.py new file mode 100644 index 0000000..2e19f26 --- /dev/null +++ b/tests/tools.py @@ -0,0 +1,5 @@ +from functools import partial + +import numpy as np + +assert_allclose = partial(np.testing.assert_allclose, rtol=1e-05, atol=np.inf, equal_nan=False)