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

module standardization #32

Merged
merged 16 commits into from
Aug 9, 2024
4 changes: 3 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+-rc'
- '[0-9]+.[0-9]+.[0-9]+-rc[0-9]+'

jobs:
publish:
Expand Down
Empty file added .github/workflows/test.yml
Empty file.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ src/__pycache__
src/data
.vscode
venv
benchmark/
test_*
benchm
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
module.tar.gz:
tar czf $@ --exclude='.DS_Store' *.sh .env src/*.py src/models/*.py src/models/checkpoints/*.pt src/models/checkpoints/*.onnx requirements.txt

test:
python3 -m pytest tests/*

lint:
python3 -m pylint src --disable=W0719,C0114,W0201,W1203,E1101,W1203,E0611,R0902,R0913,E0611,E0001,R0914,R0903
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C0114: Missing module docstring
W0201: Attribute defined outside init
W1203: Use % formatting in logging functions and pass the % parameters as arguments
E1101: Instance of 'x' has no 'y' member
E0611: No name 'x' in module 'y'
E0001: Syntax error
R0801: Similar lines in 'x' files

Remove these from the disable string, and if you do need to disable them, put the ignore right next to the code where you need it

5 changes: 2 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env bash
# setup.sh -- environment bootstrapper for python virtualenv
#!/bin/bash

set -e

Expand Down Expand Up @@ -30,5 +29,5 @@ python3 -m venv $VIRTUAL_ENV
echo installing dependencies from requirements.txt
$VIRTUAL_ENV/bin/pip install -r requirements.txt
source .venv/bin/activate
python3 -m PyInstaller --onefile --hidden-import="googleapiclient" src/main.py
$PYTHON -m PyInstaller --onefile --hidden-import="googleapiclient" --add-data "./src/models/checkpoints:checkpoints" src/main.py
tar -czvf dist/archive.tar.gz dist/main
3 changes: 1 addition & 2 deletions meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
"arch": [
"linux/amd64",
"linux/arm64",
"darwin/arm64",
"darwin/amd64"
"darwin/arm64"
]
},
"entrypoint": "dist/main"
Expand Down
19 changes: 17 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
viam-sdk>=0.21
onnx2torch

numpy
torch

torch; sys_platform == 'darwin'
torch==2.2.2+cpu; sys_platform == 'linux'
-f https://download.pytorch.org/whl/cpu/torch_stable.html

torchvision == 0.18; sys_platform == 'darwin'
torchvision==0.17.2+cpu;sys_platform == 'linux'
-f https://download.pytorch.org/whl/cpu/torch_stable.html

pyinstaller
torchvision

facenet-pytorch
opencv-python
pillow


##for testing
pytest
pytest-asyncio
pylint
Empty file added src/__init__.py
Empty file.
57 changes: 44 additions & 13 deletions src/distance.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,51 @@
from torch.nn import CosineSimilarity
import torch.nn.functional as F
from torch import Tensor
import numpy as np
import torch.nn.functional as F
from torch.nn import CosineSimilarity


def euclidean_distance(t1, t2):
"""
Computes the Euclidean distance between two tensors using the L1 norm.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Euclidean usually means the L2 norm, not L1 -- the L1 norm is colloquially called the "manhattan" or "taxicab" norm


Args:
t1 (torch.Tensor): The first tensor.
t2 (torch.Tensor): The second tensor.

Returns:
float: The Euclidean distance between the two tensors.
"""
return F.pairwise_distance(t1, t2, p=1).item() # pylint: disable=not-callable

def euclidean_distance(t1,t2):
return F.pairwise_distance(t1,t2, p=1).item()

def euclidean_l2_distance(t1, t2):
if isinstance(t1, np.ndarray) and isinstance(t2, np.ndarray):
euclidean_distance = t2 - t1
euclidean_distance = np.sum(np.multiply(euclidean_distance, euclidean_distance))
euclidean_distance = np.sqrt(euclidean_distance)
return euclidean_distance
else:
return F.pairwise_distance(t1,t2).item()
"""
Computes the Euclidean distance between two tensors or numpy arrays using the L2 norm.

Args:
t1 (torch.Tensor or numpy.ndarray): The first tensor or numpy array.
t2 (torch.Tensor or numpy.ndarray): The second tensor or numpy array.

Returns:
float: The Euclidean distance between the two inputs.
"""
if isinstance(t1, np.ndarray) and isinstance(t2, np.ndarray):
euc_distance = t2 - t1
euc_distance = np.sum(np.multiply(euc_distance, euc_distance))
euc_distance = np.sqrt(euc_distance)
return euc_distance
return F.pairwise_distance(t1, t2).item() # pylint: disable=not-callable


def cosine_distance(t1, t2):
"""
Computes the cosine distance between two tensors.

Args:
t1 (torch.Tensor): The first tensor.
t2 (torch.Tensor): The second tensor.

Returns:
float: The cosine distance between the two tensors.
"""
cos_sim = CosineSimilarity(dim=0, eps=1e-6)
return 1-cos_sim(t1, t2).item()
return 1 - cos_sim(t1, t2).item()
32 changes: 30 additions & 2 deletions src/encoder.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
from src.models import utils
from torchvision.utils import save_image
import numpy.random as rd
from torchvision.utils import save_image

from src.models import utils


class Encoder:
"""
A class used to compute embeddings of a face image using a specified model.

Attributes:
model_name (str): The name of the face recognition model.
transform (callable): The transformation function to preprocess the face image.
translator (callable): The function to translate infrared images to three channels.
face_recognizer (callable): The face recognition model to compute embeddings.
align (tuple): A tuple containing alignment settings.
normalization (bool): If True, normalize the embeddings.
debug (bool): If True, save intermediate images for debugging purposes.

Methods:
encode(face, is_ir):
Computes the embedding of the given face image.
"""

def __init__(self, model_name, align, normalization, debug) -> None:
self.model_name = model_name
self.transform, self.translator, self.face_recognizer = utils.get_all(
Expand All @@ -14,6 +32,16 @@ def __init__(self, model_name, align, normalization, debug) -> None:
self.debug = debug

def encode(self, face, is_ir):
"""
Computes the embedding of the given face image.

Args:
face (numpy.ndarray): The input face image.
is_ir (bool): Flag indicating if the input image is infrared.

Returns:
numpy.ndarray: The computed embedding of the face image.
"""
img = self.transform(face)
if self.debug:
_id = rd.randint(0, 10000)
Expand Down
67 changes: 51 additions & 16 deletions src/extractor.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from viam.logging import getLogger
import math
import os
import sys

# from mediapipe.python.solutions.face_detection import FaceDetection
from src.distance import euclidean_l2_distance
import cv2 as cv
import numpy as np
import math
import torchvision.transforms.functional as F
from torchvision.utils import save_image
from numpy import random as rd
import os
from torchvision.utils import save_image
from viam.logging import getLogger

from src.distance import euclidean_l2_distance
from src.models.yunet import YuNet
import cv2 as cv
import sys

LOGGER = getLogger(__name__)
backend_target_pairs = [
Expand All @@ -27,6 +27,23 @@


class Extractor:
"""
Initializes the Extractor with the specified parameters.

Args:
target_size (tuple): The target size of the extracted face images.
extraction_threshold (float): The confidence threshold for face extraction.
extracting_model (str): The name of the face extraction model to be used.
margin (float, optional): Margin added around detected faces for extraction. Defaults to 0.1. # pylint: disable=line-too-long
grayscale (bool, optional): If True, convert images to grayscale before processing. Defaults to False.
enforce_detection (bool, optional): If True, enforce face detection. Defaults to False.
align (bool, optional): If True, align faces based on eye coordinates. Defaults to True.
debug (bool, optional): If True, save intermediate images for debugging purposes. Defaults to False.

Raises:
ValueError: If an unknown extracting model is specified.
"""

def __init__(
self,
target_size: (int, int), # type: ignore
Expand All @@ -46,15 +63,13 @@ def __init__(
self.align = align
self.margin = margin

# if extracting_model.startswith("mediapipe:"):
# self.detector = FaceDetection(min_detection_confidence=.2, model_selection=int(extracting_model[-1]))
def resource_path(relative_path):
"""Get absolute path to resource, works for dev and for PyInstaller"""
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
base_path = sys._MEIPASS # pylint: disable=E1101,W0212
except Exception: # pylint: disable=broad-exception-caught
base_path = os.path.abspath(os.path.join("src", "models"))

return os.path.join(base_path, relative_path)

Expand All @@ -73,11 +88,21 @@ def resource_path(relative_path):
targetId=target_id,
)
else:
raise Exception(f"unknown extracting model: {extracting_model}")
raise ValueError(f"unknown extracting model: {extracting_model}")

self.debug = debug

def extract_faces(self, img, is_ir):
def extract_faces(self, img):
"""
Extracts and aligns faces from the input image.

Args:
img (numpy.ndarray): The input image from which faces are to be extracted.
is_ir (bool): Flag indicating if the input image is infrared.

Returns:
list: A list of tuples containing the extracted face image, the region object, and a confidence score. # pylint: disable=line-too-long
"""
face_objs = []
img_h, img_w = img.shape[0], img.shape[1]

Expand Down Expand Up @@ -113,7 +138,17 @@ def extract_faces(self, img, is_ir):
return face_objs

def align_face(self, face, left_eye, right_eye):
# this function aligns given face in img based on left and right eye coordinates
"""
Aligns the given face image based on the coordinates of the left and right eyes.

Args:
face (numpy.ndarray): The face image to be aligned.
left_eye (tuple): The (x, y) coordinates of the left eye.
right_eye (tuple): The (x, y) coordinates of the right eye.

Returns:
torch.Tensor: The aligned face image as a tensor.
"""

left_eye_x, left_eye_y = left_eye
right_eye_x, right_eye_y = right_eye
Expand Down
Loading