refactoring

This commit is contained in:
iperov 2021-10-26 12:25:58 +04:00
commit d90ec2d024
17 changed files with 202 additions and 41 deletions

View file

@ -0,0 +1,6 @@
from enum import IntEnum
class ELandmarks2D(IntEnum):
L5 = 0
L68 = 1
L468 = 2

4
xlib/face/EMaskType.py Normal file
View file

@ -0,0 +1,4 @@
from enum import IntEnum
class EMaskType(IntEnum):
UNDEFINED = 0

1317
xlib/face/FLandmarks2D.py Normal file

File diff suppressed because it is too large Load diff

40
xlib/face/FMask.py Normal file
View file

@ -0,0 +1,40 @@
import uuid
from typing import Union
from enum import IntEnum
class FMask:
class Type(IntEnum):
WHOLE_FACE = 0
def __init__(self, _from_pickled=False):
"""
"""
self._uuid : Union[bytes, None] = uuid.uuid4().bytes_le if not _from_pickled else None
self._mask_type : Union[FMask.Type, None] = None
self._FImage_uuid : Union[bytes, None] = None
def __getstate__(self):
return self.__dict__.copy()
def __setstate__(self, d):
self.__init__(_from_pickled=True)
self.__dict__.update(d)
def get_uuid(self) -> Union[bytes, None]: return self._uuid
def set_uuid(self, uuid : Union[bytes, None]):
if uuid is not None and not isinstance(uuid, bytes):
raise ValueError(f'uuid must be an instance of bytes or None')
self._uuid = uuid
def get_mask_type(self) -> Union['FMask.Type', None]: return self._mask_type
def set_mask_type(self, mask_type : Union['FMask.Type', None]):
if mask_type is not None and not isinstance(mask_type, 'FMask.Type'):
raise ValueError(f'mask_type must be an instance of FMask.Type or None')
self._mask_type = mask_type
def get_FImage_uuid(self) -> Union[bytes, None]: return self._FImage_uuid
def set_FImage_uuid(self, FImage_uuid : Union[bytes, None]):
if FImage_uuid is not None and not isinstance(FImage_uuid, bytes):
raise ValueError(f'FImage_uuid must be an instance of bytes or None')
self._FImage_uuid = FImage_uuid

50
xlib/face/FPose.py Normal file
View file

@ -0,0 +1,50 @@
from typing import Tuple
import numpy as np
from .. import math as lib_math
class FPose:
"""
Describes face pitch/yaw/roll
"""
def __init__(self):
self._pyr : np.ndarray = None
def __getstate__(self):
return self.__dict__.copy()
def __setstate__(self, d):
self.__init__()
self.__dict__.update(d)
def as_radians(self) -> Tuple[float, float, float]:
"""
returns pitch,yaw,roll in radians
"""
return self._pyr.copy()
def as_degress(self) -> Tuple[float, float, float]:
"""
returns pitch,yaw,roll in degrees
"""
return np.degrees(self._pyr)
@staticmethod
def from_radians(pitch, yaw, roll):
"""
"""
face_rect = FPose()
face_rect._pyr = np.array([pitch, yaw, roll], np.float32)
return face_rect
@staticmethod
def from_3D_468_landmarks(lmrks):
"""
"""
mat = np.empty((3,3))
mat[0,:] = (lmrks[454] - lmrks[234])/np.linalg.norm(lmrks[454] - lmrks[234])
mat[1,:] = (lmrks[152] - lmrks[6])/np.linalg.norm(lmrks[152] - lmrks[6])
mat[2,:] = np.cross(mat[0, :], mat[1, :])
pitch, yaw, roll = lib_math.rotation_matrix_to_euler(mat)
return FPose.from_radians(pitch, yaw, roll)

203
xlib/face/FRect.py Normal file
View file

@ -0,0 +1,203 @@
import operator
from collections import Iterable
from typing import List, Tuple
import cv2
import numpy as np
import numpy.linalg as npla
from .. import math as lib_math
from ..math import Affine2DMat, Affine2DUniMat
class FRect:
"""
Describes face rectangle in uniform float coordinates
"""
def __init__(self):
self._pts = None
def __getstate__(self):
return self.__dict__.copy()
def __setstate__(self, d):
self.__init__()
self.__dict__.update(d)
@staticmethod
def sort_by_area_size(rects : List['FRect']):
"""
sort list of FRect by largest area descend
"""
rects = [ (rect, rect.get_area()) for rect in rects ]
rects = sorted(rects, key=operator.itemgetter(1), reverse=True )
rects = [ x[0] for x in rects]
return rects
@staticmethod
def sort_by_dist_from_center(rects : List['FRect']):
"""
sort list of FRect by nearest distance from center to center of rects descent
"""
c = np.float32([0.5,0.5])
rects = [ (rect, npla.norm( rect.get_center_point()-c )) for rect in rects ]
rects = sorted(rects, key=operator.itemgetter(1) )
rects = [ x[0] for x in rects]
return rects
@staticmethod
def from_4pts(pts : Iterable):
"""
Construct FRect from 4 pts
0--3
| |
1--2
"""
if not isinstance(pts, Iterable):
raise ValueError('pts must be Iterable')
pts = np.array(pts, np.float32)
if pts.shape != (4,2):
raise ValueError('pts must have (4,2) shape')
face_rect = FRect()
face_rect._pts = pts
return face_rect
@staticmethod
def from_ltrb(ltrb : Iterable):
"""
Construct FRect from l,t,r,b list of float values
t
l-|-r
b
"""
if not isinstance(ltrb, Iterable):
raise ValueError('ltrb must be Iterable')
l,t,r,b = ltrb
return FRect.from_4pts([ [l,t], [l,b], [r,b], [r,t] ])
def get_area(self, w_h = None) -> float:
"""
get area of rectangle.
w_h(None) provide (w,h) to scale uniform rect to target size
"""
return lib_math.polygon_area(self.as_4pts(w_h))
def get_center_point(self, w_h = None) -> np.ndarray:
"""
w_h(None) provide (w,h) to scale uniform rect to target size
returns np.ndarray (2,)
"""
pts = self.as_4pts(w_h)
return np.mean(pts, 0)
def as_ltrb_bbox(self, w_h = None) -> np.ndarray:
"""
get bounding box of rect as left,top,right,bottom
w_h(None) provide (w,h) to scale uniform rect to target size
returns np.ndarray with l,t,r,b values
"""
pts = self.as_4pts( w_h=w_h)
return np.array( [np.min(pts[:,0]), np.max(pts[:,0]), np.min(pts[:,1]), np.max(pts[:,1])], np.float32 )
def as_4pts(self, w_h = None) -> np.ndarray:
"""
get rect as 4 pts
0--3
| |
1--2
w_h(None) provide (w,h) to scale uniform rect to target size
returns np.ndarray (4,2) 4 pts with w,h
"""
if w_h is not None:
return self._pts * w_h
return self._pts.copy()
def transform(self, mat, invert=False) -> 'FRect':
"""
Tranforms FRect using affine mat and returns new FRect()
mat : np.ndarray should be uniform affine mat
"""
if not isinstance(mat, np.ndarray):
raise ValueError('mat must be an instance of np.ndarray')
if invert:
mat = cv2.invertAffineTransform (mat)
pts = self._pts.copy()
pts = np.expand_dims(pts, axis=1)
pts = cv2.transform(pts, mat, pts.shape).squeeze()
return FRect.from_4pts(pts)
def cut(self, img : np.ndarray, coverage : float, output_size : int) -> Tuple[Affine2DMat, Affine2DUniMat]:
"""
Cut the face to square of output_size from img with given coverage using this rect
returns image,
uni_mat uniform matrix to transform uniform img space to uniform cutted space
"""
# Face rect is not a square, also rect can be rotated
h,w = img.shape[0:2]
# Get scaled rect pts to target img
pts = self.as_4pts( w_h=(w,h) )
# Estimate transform from global space to local aligned space with bounds [0..1]
mat = Affine2DMat.umeyama(pts, uni_rect, True)
# get corner points in global space
g_p = mat.invert().transform_points ( [(0,0),(0,1),(1,1),(1,0),(0.5,0.5)] )
g_c = g_p[4]
# calc diagonal vectors between corners in global space
tb_diag_vec = lib_math.segment_to_vector(g_p[0], g_p[2]).astype(np.float32)
bt_diag_vec = lib_math.segment_to_vector(g_p[1], g_p[3]).astype(np.float32)
mod = lib_math.segment_length(g_p[0],g_p[4])*coverage
l_t = np.array( [ g_c - tb_diag_vec*mod,
g_c + bt_diag_vec*mod,
g_c + tb_diag_vec*mod ], np.float32 )
mat = Affine2DMat.from_3_pairs ( l_t, np.float32(( (0,0),(output_size,0),(output_size,output_size) )))
uni_mat = Affine2DUniMat.from_3_pairs ( (l_t/(w,h)).astype(np.float32), np.float32(( (0,0),(1,0),(1,1) )) )
face_image = cv2.warpAffine(img, mat, (output_size, output_size), cv2.INTER_CUBIC )
return face_image, uni_mat
def draw(self, img : np.ndarray, color, thickness=1):
"""
draw rect on the img scaled by img.wh
color tuple of values should be the same as img color channels
"""
h,w = img.shape[0:2]
pts = self.as_4pts(w_h=(w,h)).astype(np.int32)
pts_len = len(pts)
for i in range (pts_len):
p0 = tuple( pts[i] )
p1 = tuple( pts[ (i+1) % pts_len] )
cv2.line (img, p0, p1, color, thickness=thickness, lineType=cv2.LINE_AA)
uni_rect = np.array([
[0.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[1.0, 0.0],
], dtype=np.float32)

143
xlib/face/FaceWarper.py Normal file
View file

@ -0,0 +1,143 @@
from typing import Iterable, Tuple, Union
import cv2
import numpy as np
from ..math import Affine2DMat, Affine2DUniMat
class FaceWarper:
def __init__(self,
img_to_face_uni_mat : Affine2DUniMat,
align_rot_deg : Union[None, float, Tuple[float, float] ] = [-15,15],
align_scale : Union[None, float, Tuple[float, float] ] = [-0.15, 0.15],
align_tx : Union[None, float, Tuple[float, float] ] = [-0.05, 0.05],
align_ty : Union[None, float, Tuple[float, float] ] = [-0.05, 0.05],
rw_grid_cell_count : Union[None, int, Tuple[int, int] ] = [3,7],
rw_grid_rot_deg : Union[None, float, Tuple[float, float] ] = [-180,180],
rw_grid_scale : Union[None, float, Tuple[float, float] ] = [-0.25, 0.25],
rw_grid_tx : Union[None, float, Tuple[float, float] ] = [-0.25, 0.25],
rw_grid_ty : Union[None, float, Tuple[float, float] ] = [-0.25, 0.25],
rnd_state : np.random.RandomState = None,
):
"""
Max quality one-pass face augmentation via geometric transformations with provided values.
img_to_face_uni_mat Affine2DUniMat
Affine2DUniMat given from FLandmarks2D.calc_cut
it is an uniform affineMat to transform original image to aligned face
align_* rw_grid_*
exact augmentation parameters or range for random generation.
"""
self._img_to_face_uni_mat = img_to_face_uni_mat
self._face_to_img_uni_mat = img_to_face_uni_mat.invert()
if rnd_state is None:
rnd_state = np.random
self._rnd_state_state = rnd_state.get_state()
self._align_rot_deg = rnd_state.uniform(*align_rot_deg) if isinstance(align_rot_deg, Iterable) else align_rot_deg
self._align_scale = rnd_state.uniform(*align_scale) if isinstance(align_scale, Iterable) else align_scale
self._align_tx = rnd_state.uniform(*align_tx) if isinstance(align_tx, Iterable) else align_tx
self._align_ty = rnd_state.uniform(*align_ty) if isinstance(align_ty, Iterable) else align_ty
self._rw_grid_cell_count = rnd_state.randint(*rw_grid_cell_count) if isinstance(rw_grid_cell_count, Iterable) else rw_grid_cell_count
self._rw_grid_rot_deg = rnd_state.uniform(*rw_grid_rot_deg) if isinstance(rw_grid_rot_deg, Iterable) else rw_grid_rot_deg
self._rw_grid_scale = rnd_state.uniform(*rw_grid_scale) if isinstance(rw_grid_scale, Iterable) else rw_grid_scale
self._rw_grid_tx = rnd_state.uniform(*rw_grid_tx) if isinstance(rw_grid_tx, Iterable) else rw_grid_tx
self._rw_grid_ty = rnd_state.uniform(*rw_grid_ty) if isinstance(rw_grid_ty, Iterable) else rw_grid_ty
self._cached = {}
def transform(self, img : np.ndarray, out_res : int, random_warp : bool = True) -> np.ndarray:
"""
transform an image. Subsequent calls will output the same result for any img shape and out_res.
img np.ndarray (HWC)
out_res int
random_warp(True) bool
"""
H,W = img.shape[:2]
key = (H,W,random_warp)
data = self._cached.get(key, None)
if data is None:
rnd_state = np.random.RandomState()
rnd_state.set_state( self._rnd_state_state )
image_grid, face_mask = self._cached[key] = self._gen(H,W, random_warp, out_res, rnd_state=rnd_state )
new_img = cv2.remap(img, image_grid, None, interpolation=cv2.INTER_LANCZOS4)
new_img *= face_mask
return new_img
def _gen(self, H, W, random_warp, out_res, rnd_state):
image_grid = np.stack(np.meshgrid(np.linspace(0., 1.0, H, dtype=np.float32),
np.linspace(0., 1.0, W, dtype=np.float32)), -1)
if random_warp:
# make a random face_warp_grid in the space of the face
face_warp_grid = FaceWarper._gen_random_warp_uni_grid_diff(out_res, self._rw_grid_cell_count, 0.12, rnd_state)
# make a randomly transformable mat to transform face_warp_grid from face to image
face_warp_grid_mat = (self._face_to_img_uni_mat *
Affine2DUniMat.from_transformation(0.5, 0.5, self._rw_grid_rot_deg, 1.0+self._rw_grid_scale, self._rw_grid_tx, self._rw_grid_ty)
)
# warp face_warp_grid to the space of image and merge with image_grid
image_grid += cv2.warpAffine(face_warp_grid, face_warp_grid_mat.to_exact_mat(out_res,out_res, W, H), (W,H), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT)
# scale uniform grid from to image size
image_grid *= (H-1, W-1)
# apply random transormations for align mat
img_to_face_rnd_mat = (self._face_to_img_uni_mat * Affine2DMat.from_transformation(0.5, 0.5, self._align_rot_deg, 1.0+self._align_scale, self._align_tx, self._align_ty)
).invert().to_exact_mat(W,H,out_res,out_res)
# warp image_grid to face space
image_grid = cv2.warpAffine(image_grid, img_to_face_rnd_mat, (out_res,out_res), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE )
# One-pass remap from original image to aligned face with all transformations
#new_img = cv2.remap(img, image_grid, None, interpolation=cv2.INTER_LANCZOS4)
# make mask to refine image-boundary visible in face space
face_mask = cv2.warpAffine( np.ones( (H,W), dtype=np.uint8), img_to_face_rnd_mat, (out_res,out_res), flags=cv2.INTER_NEAREST)[...,None]
return image_grid, face_mask
def _gen_random_warp_uni_grid_diff(size: int, cell_count, cell_mod, rnd_state) -> np.ndarray:
"""
generates square uniform random warp
grid of shape (size, size, 2) (x,y)
cell_count(3) 3+
cell_mod (0.12) [ 0 .. 0.24 ]
"""
cell_count = max(3, cell_count)
cell_mod = np.clip(cell_mod, 0, 0.24)
cell_size = 1.0 / (cell_count-1)
grid = np.zeros( (cell_count,cell_count, 2), dtype=np.float32 )
grid[1:-1,1:-1, 0:2] += rnd_state.uniform (low=-cell_size*cell_mod, high=cell_size*cell_mod, size=(cell_count-2, cell_count-2, 2) )
grid = cv2.resize(grid, (size, size), interpolation=cv2.INTER_CUBIC ).astype(np.float32)
# Linear dump border cells to zero
border_size = size // cell_count
dumper = np.linspace(0, 1, border_size, dtype=np.float32)
grid[:border_size, :,:] *= dumper[:,None,None]
grid[-border_size:,:,:] *= dumper[::-1,None,None]
grid[:,:border_size ,:] *= dumper[None,:,None]
grid[:,-border_size:,:] *= dumper[None,::-1,None]
return grid

283
xlib/face/Faceset.py Normal file
View file

@ -0,0 +1,283 @@
import pickle
import sqlite3
from pathlib import Path
from typing import Generator, List, Union, Iterable
import cv2
import numpy as np
from .FMask import FMask
from .UFaceMark import UFaceMark
from .UImage import UImage
from .UPerson import UPerson
class Faceset:
def __init__(self, path = None):
"""
Faceset is a class to store and manage face related data.
arguments:
path path to faceset .dfs file
Can be pickled.
"""
self._path = path = Path(path)
if path.suffix != '.dfs':
raise ValueError('Path must be a .dfs file')
self._conn = conn = sqlite3.connect(path, isolation_level=None)
self._cur = cur = conn.cursor()
cur = self._get_cursor()
cur.execute('BEGIN IMMEDIATE')
if not self._is_table_exists('FacesetInfo'):
self.recreate(shrink=False, _transaction=False)
cur.execute('COMMIT')
self.shrink()
else:
cur.execute('END')
def __del__(self):
self.close()
def __getstate__(self):
return {'_path' : self._path}
def __setstate__(self, d):
self.__init__( d['_path'] )
def __repr__(self): return self.__str__()
def __str__(self):
return f"Faceset. UImage:{self.get_UImage_count()} UFaceMark:{self.get_UFaceMark_count()}"
def _is_table_exists(self, name):
return self._cur.execute(f"SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", [name]).fetchone()[0] != 0
def _get_cursor(self) -> sqlite3.Cursor: return self._cur
def close(self):
if self._cur is not None:
self._cur.close()
self._cur = None
if self._conn is not None:
self._conn.close()
self._conn = None
def shrink(self):
self._cur.execute('VACUUM')
def recreate(self, shrink=True, _transaction=True):
"""
delete all data and recreate Faceset structure.
"""
cur = self._get_cursor()
if _transaction:
cur.execute('BEGIN IMMEDIATE')
for table_name, in cur.execute("SELECT name from sqlite_master where type = 'table';").fetchall():
cur.execute(f'DROP TABLE {table_name}')
(cur.execute('CREATE TABLE FacesetInfo (version INT)')
.execute('INSERT INTO FacesetInfo VALUES (1)')
.execute('CREATE TABLE UImage (uuid BLOB, name TEXT, format TEXT, data BLOB)')
.execute('CREATE TABLE UPerson (uuid BLOB, name TEXT, age NUMERIC)')
.execute('CREATE TABLE UFaceMark (uuid BLOB, UImage_uuid BLOB, UPerson_uuid BLOB, pickled_bytes BLOB)')
)
if _transaction:
cur.execute('COMMIT')
if shrink:
self.shrink()
###################
### UFaceMark
###################
def _UFaceMark_from_db_row(self, db_row) -> UFaceMark:
uuid, UImage_uuid, UPerson_uuid, pickled_bytes = db_row
return pickle.loads(pickled_bytes)
def add_UFaceMark(self, ufacemark_or_list : UFaceMark):
"""
add or update UFaceMark in DB
"""
if not isinstance(ufacemark_or_list, Iterable):
ufacemark_or_list = [ufacemark_or_list]
cur = self._cur
cur.execute('BEGIN IMMEDIATE')
for ufacemark in ufacemark_or_list:
pickled_bytes = pickle.dumps(ufacemark)
uuid = ufacemark.get_uuid()
UImage_uuid = ufacemark.get_UImage_uuid()
UPerson_uuid = ufacemark.get_UPerson_uuid()
if cur.execute('SELECT COUNT(*) from UFaceMark where uuid=?', [uuid] ).fetchone()[0] != 0:
cur.execute('UPDATE UFaceMark SET UImage_uuid=?, UPerson_uuid=?, pickled_bytes=? WHERE uuid=?',
[UImage_uuid, UPerson_uuid, pickled_bytes, uuid])
else:
cur.execute('INSERT INTO UFaceMark VALUES (?, ?, ?, ?)', [uuid, UImage_uuid, UPerson_uuid, pickled_bytes])
cur.execute('COMMIT')
def get_UFaceMark_count(self) -> int:
return self._cur.execute('SELECT COUNT(*) FROM UFaceMark').fetchone()[0]
def get_all_UFaceMark(self) -> List[UFaceMark]:
return [ pickle.loads(pickled_bytes) for pickled_bytes, in self._cur.execute('SELECT pickled_bytes FROM UFaceMark').fetchall() ]
def get_UFaceMark_by_uuid(self, uuid : bytes) -> Union[UFaceMark, None]:
c = self._cur.execute('SELECT * FROM UFaceMark WHERE uuid=?', [uuid])
db_row = c.fetchone()
if db_row is None:
return None
return self._UFaceMark_from_db_row(db_row)
def iter_UFaceMark(self) -> Generator[UFaceMark, None, None]:
"""
returns Generator of UFaceMark
"""
for db_row in self._cur.execute('SELECT * FROM UFaceMark').fetchall():
yield self._UFaceMark_from_db_row(db_row)
def delete_all_UFaceMark(self):
"""
deletes all UFaceMark from DB
"""
(self._cur.execute('BEGIN IMMEDIATE')
.execute('DELETE FROM UFaceMark')
.execute('COMMIT') )
###################
### UPerson
###################
def add_UPerson(self, uperson_or_list : UPerson):
"""
add or update UPerson in DB
"""
if not isinstance(uperson_or_list, Iterable):
uperson_or_list = [uperson_or_list]
cur = self._cur
cur.execute('BEGIN IMMEDIATE')
for uperson in uperson_or_list:
uuid = uperson.get_uuid()
name = uperson.get_name()
age = uperson.get_age()
if cur.execute('SELECT COUNT(*) from UPerson where uuid=?', [uuid]).fetchone()[0] != 0:
cur.execute('UPDATE UPerson SET name=?, age=? WHERE uuid=?', [name, age, uuid])
else:
cur.execute('INSERT INTO UPerson VALUES (?, ?, ?)', [uuid, name, age])
cur.execute('COMMIT')
def iter_UPerson(self) -> Generator[UPerson, None, None]:
"""
iterator of all UPerson's
"""
for uuid, name, age in self._cur.execute('SELECT * FROM UPerson').fetchall():
uperson = UPerson()
uperson.set_uuid(uuid)
uperson.set_name(name)
uperson.set_age(age)
yield uperson
def delete_all_UPerson(self):
"""
deletes all UPerson from DB
"""
(self._cur.execute('BEGIN IMMEDIATE')
.execute('DELETE FROM UPerson')
.execute('COMMIT') )
###################
### UImage
###################
def _UImage_from_db_row(self, db_row) -> UImage:
uuid, name, format, data_bytes = db_row
img = cv2.imdecode(np.frombuffer(data_bytes, dtype=np.uint8), flags=cv2.IMREAD_UNCHANGED)
uimg = UImage()
uimg.set_uuid(uuid)
uimg.set_name(name)
uimg.assign_image(img)
return uimg
def add_UImage(self, uimage_or_list : UImage, format : str = 'webp', quality : int = 100):
"""
add or update UImage in DB
uimage UImage object
format('png') webp ( does not support lossless on 100 quality ! )
png ( lossless )
jpg
jp2 ( jpeg2000 )
quality(100) 0-100 for formats jpg,jp2,webp
"""
if format not in ['webp','png', 'jpg', 'jp2']:
raise ValueError(f'format {format} is unsupported')
if format in ['jpg','jp2'] and quality < 0 or quality > 100:
raise ValueError('quality must be in range [0..100]')
if not isinstance(uimage_or_list, Iterable):
uimage_or_list = [uimage_or_list]
cur = self._cur
cur.execute('BEGIN IMMEDIATE')
for uimage in uimage_or_list:
# TODO optimize move encoding to out of transaction
img = uimage.get_image()
uuid = uimage.get_uuid()
if format == 'webp':
imencode_args = [int(cv2.IMWRITE_WEBP_QUALITY), quality]
elif format == 'jpg':
imencode_args = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
elif format == 'jp2':
imencode_args = [int(cv2.IMWRITE_JPEG2000_COMPRESSION_X1000), quality*10]
else:
imencode_args = []
ret, data_bytes = cv2.imencode( f'.{format}', img, imencode_args)
if not ret:
raise Exception(f'Unable to encode image format {format}')
if cur.execute('SELECT COUNT(*) from UImage where uuid=?', [uuid] ).fetchone()[0] != 0:
cur.execute('UPDATE UImage SET name=?, format=?, data=? WHERE uuid=?', [uimage.get_name(), format, data_bytes.data, uuid])
else:
cur.execute('INSERT INTO UImage VALUES (?, ?, ?, ?)', [uuid, uimage.get_name(), format, data_bytes.data])
cur.execute('COMMIT')
def get_UImage_count(self) -> int: return self._cur.execute('SELECT COUNT(*) FROM UImage').fetchone()[0]
def get_UImage_by_uuid(self, uuid : Union[bytes, None]) -> Union[UImage, None]:
"""
"""
if uuid is None:
return None
db_row = self._cur.execute('SELECT * FROM UImage where uuid=?', [uuid]).fetchone()
if db_row is None:
return None
return self._UImage_from_db_row(db_row)
def iter_UImage(self) -> Generator[UImage, None, None]:
"""
iterator of all UImage's
"""
for db_row in self._cur.execute('SELECT * FROM UImage').fetchall():
yield self._UImage_from_db_row(db_row)
def delete_all_UImage(self):
"""
deletes all UImage from DB
"""
(self._cur.execute('BEGIN IMMEDIATE')
.execute('DELETE FROM UImage')
.execute('COMMIT') )

99
xlib/face/UFaceMark.py Normal file
View file

@ -0,0 +1,99 @@
import uuid
from typing import List, Tuple, Union
from ..math import Affine2DMat
from .ELandmarks2D import ELandmarks2D
from .EMaskType import EMaskType
from .FLandmarks2D import FLandmarks2D
from .FPose import FPose
from .FRect import FRect
class UFaceMark:
def __init__(self, _from_pickled=False):
"""
Describes single face in the image.
"""
self._uuid : Union[bytes, None] = uuid.uuid4().bytes_le if not _from_pickled else None
self._UImage_uuid : Union[bytes, None] = None
self._UPerson_uuid : Union[bytes, None] = None
self._FRect : Union[FRect, None] = None
self._FLandmarks2D_list : List[FLandmarks2D] = []
self._FPose : Union[FPose, None] = None
self._mask_info_list : List = []
def __getstate__(self):
return self.__dict__.copy()
def __setstate__(self, d):
self.__init__(_from_pickled=True)
self.__dict__.update(d)
def __str__(self):
s = "Masks: "
return f"UFaceMark UUID:[...{self._uuid[-4:].hex()}]"
def __repr__(self): return self.__str__()
def get_uuid(self) -> Union[bytes, None]: return self._uuid
def set_uuid(self, uuid : Union[bytes, None]):
if uuid is not None and not isinstance(uuid, bytes):
raise ValueError(f'uuid must be an instance of bytes or None')
self._uuid = uuid
def get_UImage_uuid(self) -> Union[bytes, None]: return self._UImage_uuid
def set_UImage_uuid(self, UImage_uuid : Union[bytes, None]):
if UImage_uuid is not None and not isinstance(UImage_uuid, bytes):
raise ValueError(f'UImage_uuid must be an instance of bytes or None')
self._UImage_uuid = UImage_uuid
def get_UPerson_uuid(self) -> Union[bytes, None]: return self._UPerson_uuid
def set_UPerson_uuid(self, UPerson_uuid : Union[bytes, None]):
if UPerson_uuid is not None and not isinstance(UPerson_uuid, bytes):
raise ValueError(f'UPerson_uuid must be an instance of bytes or None')
self._UPerson_uuid = UPerson_uuid
def get_FRect(self) -> Union['FRect', None]: return self._FRect
def set_FRect(self, face_urect : Union['FRect', None]):
if face_urect is not None and not isinstance(face_urect, FRect):
raise ValueError(f'face_urect must be an instance of FRect or None')
self._FRect = face_urect
def get_all_FLandmarks2D(self) -> List[FLandmarks2D]: return self._FLandmarks2D_list
def get_FLandmarks2D_by_type(self, type : ELandmarks2D) -> Union[FLandmarks2D, None]:
"""get FLandmarks2D from list by type"""
if not isinstance(type, ELandmarks2D):
raise ValueError(f'type must be an instance of ELandmarks2D')
for ulmrks in self._FLandmarks2D_list:
if ulmrks.get_type() == type:
return ulmrks
return None
def add_FLandmarks2D(self, flmrks : FLandmarks2D):
if not isinstance(flmrks, FLandmarks2D):
raise ValueError('flmrks must be an instance of FLandmarks2D')
if self.get_FLandmarks2D_by_type(flmrks.get_type()) is not None:
raise Exception(f'_FLandmarks2D_list already contains type {flmrks.get_type()}.')
self._FLandmarks2D_list.append(flmrks)
def get_FPose(self) -> Union[FPose, None]: return self._FPose
def set_FPose(self, face_pose : FPose):
if not isinstance(face_pose, FPose):
raise ValueError('face_pose must be an instance of FPose')
self._FPose = face_pose
def get_mask_info_list(self) -> List[Tuple[EMaskType, bytes, Affine2DMat]]:
return self._mask_info_list
def add_mask_info(self, mask_type : EMaskType, UImage_uuid : bytes, mask_to_mark_uni_mat : Affine2DMat):
if not isinstance(mask_type, EMaskType):
raise ValueError('mask_type must be an instance of EMaskType')
if not isinstance(UImage_uuid, bytes):
raise ValueError('UImage_uuid must be an instance of bytes')
if not isinstance(mask_to_mark_uni_mat, Affine2DMat):
raise ValueError('mask_to_mark_uni_mat must be an instance of Affine2DMat')
self._mask_info_list.append( (mask_type, UImage_uuid, mask_to_mark_uni_mat) )

59
xlib/face/UImage.py Normal file
View file

@ -0,0 +1,59 @@
import uuid
from typing import Union
import numpy as np
class UImage:
def __init__(self):
"""
represents uncompressed image uint8 HWC ( 1/3/4 channels )
"""
self._uuid : Union[bytes, None] = uuid.uuid4().bytes_le
self._name : Union[str, None] = None
self._image : np.ndarray = None
def __getstate__(self):
return self.__dict__.copy()
def __setstate__(self, d):
self.__init__()
self.__dict__.update(d)
def __str__(self): return f"UImage UUID:[...{self._uuid[-4:].hex()}] name:[{self._name}] image:[{ (self._image.shape, self._image.dtype) if self._image is not None else None}]"
def __repr__(self): return self.__str__()
def get_uuid(self) -> Union[bytes, None]: return self._uuid
def set_uuid(self, uuid : Union[bytes, None]):
if uuid is not None and not isinstance(uuid, bytes):
raise ValueError(f'uuid must be an instance of bytes or None')
self._uuid = uuid
def get_name(self) -> Union[str, None]: return self._name
def set_name(self, name : Union[str, None]):
if name is not None and not isinstance(name, str):
raise ValueError(f'name must be an instance of str or None')
self._name = name
def get_image(self) -> Union[np.ndarray, None]: return self._image
def assign_image(self, image : Union[np.ndarray, None]):
"""
assign np.ndarray image , or remove(None)
It's mean you should not to change provided image nd.array after assigning, or do the copy before.
Image must be uint8 and HWC 1/3/4 channels.
"""
if image is not None:
if image.ndim == 2:
image = image[...,None]
if image.ndim != 3:
raise ValueError('image must have ndim == 3')
_,_,C = image.shape
if C not in [1,3,4]:
raise ValueError('image channels must be 1,3,4')
if image.dtype != np.uint8:
raise ValueError('image dtype must be uint8')
self._image = image

39
xlib/face/UPerson.py Normal file
View file

@ -0,0 +1,39 @@
import uuid
from typing import Union
class UPerson:
def __init__(self, _from_pickled=False):
"""
"""
self._uuid : Union[bytes, None] = uuid.uuid4().bytes_le if not _from_pickled else None
self._name : Union[str, None] = None
self._age : Union[int, None] = None
def __getstate__(self):
return self.__dict__.copy()
def __setstate__(self, d):
self.__init__(_from_pickled=True)
self.__dict__.update(d)
def __str__(self): return f"UPerson UUID:[...{self._uuid[-4:].hex()}] name:[{self._name}] age:[{self._age}]"
def __repr__(self): return self.__str__()
def get_uuid(self) -> Union[bytes, None]: return self._uuid
def set_uuid(self, uuid : Union[bytes, None]):
if uuid is not None and not isinstance(uuid, bytes):
raise ValueError(f'uuid must be an instance of bytes or None')
self._uuid = uuid
def get_name(self) -> Union[str, None]: return self._name
def set_name(self, name : Union[str, None]):
if name is not None and not isinstance(name, str):
raise ValueError(f'name must be an instance of str or None')
self._name = name
def get_age(self) -> Union[str, None]: return self._age
def set_age(self, age : Union[int, None]):
if age is not None and not isinstance(age, int):
raise ValueError(f'age must be an instance of int or None')
self._age = age

66
xlib/face/__init__.py Normal file
View file

@ -0,0 +1,66 @@
"""
Facelib.
Contains classes for effectively storing, manage, transfering and processing all face related data.
#####
Faceset
.List[UImage]
.List[UFaceMark]
.List[UPerson]
FaceWarper A class for face augmentation with geometric transformations.
##### META CLASSES
F* U* classes are picklable and expandable, have noneable members accessed via get/set. No properties.
E-classes are enums.
U-classes are unique, have uuid and can be saved in Faceset.
ELandmarks2D L5
L68
L468
EMaskType UNDEFINED, ..., ...
FRect rectangle of the face in uniform float coordinates
FLandmarks2D 2D landmarks of the face in uniform float coordinates
FPose pitch/yaw/roll values
UPerson - person info
.uuid
.name
.age
UImage - image
.uuid
.name
.data (H,W,C 1/3/4 ) of uint8[0..255]
UFaceMark - face mark info referencing UImage from which the face was detected
.uuid
.UImage_uuid - reference to FImage
.UPerson_uuid - reference to FPerson
.FRect
.List[FLandmarks2D]
.FPose
.List[ (EMaskType, FImage_uuid, uni_mat) ] - list of FMask and AffineMat to transform mask image space to UFaceMark image space
"""
from .ELandmarks2D import ELandmarks2D
from .EMaskType import EMaskType
from .Faceset import Faceset
from .FaceWarper import FaceWarper
from .FLandmarks2D import FLandmarks2D
from .FMask import FMask
from .FPose import FPose
from .FRect import FRect
from .UFaceMark import UFaceMark
from .UImage import UImage
from .UPerson import UPerson