diff options
| author | adamhrv <adam@ahprojects.com> | 2019-01-06 17:16:18 +0100 |
|---|---|---|
| committer | adamhrv <adam@ahprojects.com> | 2019-01-06 17:16:18 +0100 |
| commit | 4bcb82c0f295d79d3d247252e7e98b2d986ae821 (patch) | |
| tree | a51105698c46ecfcb0a09c5ba294f9d9ffa43e7a /megapixels/app | |
| parent | 2efde746810a0264ad2cf09dc9b003bfcd17a4d5 (diff) | |
externalize drawing, cleanup
Diffstat (limited to 'megapixels/app')
| -rw-r--r-- | megapixels/app/models/bbox.py | 20 | ||||
| -rw-r--r-- | megapixels/app/processors/face_detector.py | 27 | ||||
| -rw-r--r-- | megapixels/app/processors/face_landmarks.py | 194 | ||||
| -rw-r--r-- | megapixels/app/processors/face_landmarks_2d.py | 87 | ||||
| -rw-r--r-- | megapixels/app/processors/face_landmarks_3d.py | 38 | ||||
| -rw-r--r-- | megapixels/app/processors/face_pose.py | 15 | ||||
| -rw-r--r-- | megapixels/app/settings/app_cfg.py | 8 | ||||
| -rw-r--r-- | megapixels/app/settings/types.py | 27 | ||||
| -rw-r--r-- | megapixels/app/utils/display_utils.py | 16 | ||||
| -rw-r--r-- | megapixels/app/utils/draw_utils.py | 65 |
10 files changed, 337 insertions, 160 deletions
diff --git a/megapixels/app/models/bbox.py b/megapixels/app/models/bbox.py index 55a92512..04ee4a70 100644 --- a/megapixels/app/models/bbox.py +++ b/megapixels/app/models/bbox.py @@ -29,6 +29,7 @@ class BBox: def __init__(self, x1, y1, x2, y2): """Represents a bounding box and provides methods for accessing and modifying + All values are normalized unless otherwise specified :param x1: normalized left coord :param y1: normalized top coord :param x2: normalized right coord @@ -40,8 +41,8 @@ class BBox: self._y2 = y2 self._width = x2 - x1 self._height = y2 - y1 - self._cx = x1 + (self._width // 2) - self._cy = y1 + (self._height // 2) + self._cx = x1 + (self._width / 2) + self._cy = y1 + (self._height / 2) self._tl = (x1, y1) self._br = (x2, y2) self._rect = (self._x1, self._y1, self._x2, self._y2) @@ -111,7 +112,14 @@ class BBox: # # ----------------------------------------------------------------- # # Utils - # def constrain(self, dim): + def contains(self, pt_norm): + '''Returns Checks if this BBox contains the normalized point + :param pt: (int|float, int|float) x, y + :returns (bool) + ''' + x, y = pt_norm + return (x > self._x1 and x < self._x2 and y > self._y1 and y < self._y2) + def distance(self, b): a = self dcx = self._cx - b.cx @@ -168,6 +176,12 @@ class BBox: # ----------------------------------------------------------------- # Convert to + def to_square(self, bounds): + '''Forces bbox to square dimensions + :param bounds: (int, int) w, h of the image + :returns (BBox) in square ratio + ''' + def to_dim(self, dim): """scale is (w, h) is tuple of dimensions""" w, h = dim diff --git a/megapixels/app/processors/face_detector.py b/megapixels/app/processors/face_detector.py index a805a474..6bf27576 100644 --- a/megapixels/app/processors/face_detector.py +++ b/megapixels/app/processors/face_detector.py @@ -65,8 +65,6 @@ class DetectorHaar: class DetectorDLIBCNN: - - dnn_size = (300, 300) pyramids = 0 conf_thresh = 0.85 @@ -79,13 +77,10 @@ class DetectorDLIBCNN: self.detector = dlib.cnn_face_detection_model_v1(cfg.DIR_MODELS_DLIB_CNN) os.environ['CUDA_VISIBLE_DEVICES'] = cuda_visible_devices # reset - def detect(self, im, size=None, conf_thresh=None, pyramids=None, largest=False, zone=None): + def detect(self, im, conf_thresh=None, pyramids=None, largest=False, zone=None): bboxes = [] conf_thresh = self.conf_thresh if conf_thresh is None else conf_thresh pyramids = self.pyramids if pyramids is None else pyramids - dnn_size = self.dnn_size if size is None else size - # resize image - im = im_utils.resize(im, width=dnn_size[0], height=dnn_size[1]) dim = im.shape[:2][::-1] im = im_utils.bgr2rgb(im) # convert to RGB for dlib # run detector @@ -110,7 +105,6 @@ class DetectorDLIBCNN: class DetectorDLIBHOG: - size = (320, 240) pyramids = 0 conf_thresh = 0.85 @@ -119,12 +113,9 @@ class DetectorDLIBHOG: self.log = logger_utils.Logger.getLogger() self.detector = dlib.get_frontal_face_detector() - def detect(self, im, size=None, conf_thresh=None, pyramids=0, largest=False, zone=False): + def detect(self, im, conf_thresh=None, pyramids=0, largest=False, zone=False): conf_thresh = self.conf_thresh if conf_thresh is None else conf_thresh - dnn_size = self.size if size is None else size pyramids = self.pyramids if pyramids is None else pyramids - - im = im_utils.resize(im, width=dnn_size[0], height=dnn_size[1]) dim = im.shape[:2][::-1] im = im_utils.bgr2rgb(im) # ? hog_results = self.detector.run(im, pyramids) @@ -153,23 +144,23 @@ class DetectorCVDNN: dnn_scale = 1.0 # fixed dnn_mean = (104.0, 177.0, 123.0) # fixed dnn_crop = False # crop or force resize - size = (300, 300) - conf_thresh = 0.85 + blob_size = (300, 300) + conf_thresh = 0.95 def __init__(self): - import dlib + self.log = logger_utils.Logger.getLogger() fp_prototxt = join(cfg.DIR_MODELS_CAFFE, 'face_detect', 'opencv_face_detector.prototxt') fp_model = join(cfg.DIR_MODELS_CAFFE, 'face_detect', 'opencv_face_detector.caffemodel') self.net = cv.dnn.readNet(fp_prototxt, fp_model) self.net.setPreferableBackend(cv.dnn.DNN_BACKEND_OPENCV) self.net.setPreferableTarget(cv.dnn.DNN_TARGET_CPU) - def detect(self, im, size=None, conf_thresh=None, largest=False, pyramids=None, zone=False): + def detect(self, im, conf_thresh=None, largest=False, pyramids=None, zone=False): """Detects faces and returns (list) of (BBox)""" conf_thresh = self.conf_thresh if conf_thresh is None else conf_thresh - dnn_size = self.size if size is None else size - im = cv.resize(im, dnn_size) - blob = cv.dnn.blobFromImage(im, self.dnn_scale, dnn_size, self.dnn_mean) + im = cv.resize(im, self.blob_size) + dim = im.shape[:2][::-1] + blob = cv.dnn.blobFromImage(im, self.dnn_scale, dim, self.dnn_mean) self.net.setInput(blob) net_outputs = self.net.forward() diff --git a/megapixels/app/processors/face_landmarks.py b/megapixels/app/processors/face_landmarks.py new file mode 100644 index 00000000..8086ba1e --- /dev/null +++ b/megapixels/app/processors/face_landmarks.py @@ -0,0 +1,194 @@ +from os.path import join +from pathlib import Path + +import cv2 as cv +import numpy as np +import imutils + +from app.utils import im_utils, logger_utils +from app.models.bbox import BBox +from app.settings import app_cfg as cfg +from app.settings import types +from app.models.bbox import BBox + + +# ---------------------------------------------------------------------- +# +# 2D landmarks: 5pt and 68pt +# +# ---------------------------------------------------------------------- + +class Landmarks2D: + + # Abstract class + + def __init__(self): + self.log = logger_utils.Logger.getLogger() + + def landmarks(self, im, bbox): + # override + self.log.warn('Define landmarks() function') + pass + + def flatten(self, points): + '''Converts list of point-tupes into a flattened list for CSV + :param points: (list) of x,y points + :returns dict item for each point (eg {'x1':100, 'y1':200}) + ''' + points_formatted = {} + for idx, pt in enumerate(points, 1): + for j, d in enumerate('xy'): + points_formatted[f'{d}{idx}'] = pt[j] + return points_formatted + + def normalize(self, points, dim): + return [np.array(p)/dim for p in points] # divides each point by w,h dim + + + +import face_alignment + +class FaceAlignment2D_68(Landmarks2D): + + # https://github.com/1adrianb/face-alignment + # Estimates 2D facial landmarks + + def __init__(self, gpu=0, flip_input=False): + t = face_alignment.LandmarksType._2D + device = f'cuda:{gpu}' if gpu > -1 else 'cpu' + self.fa = face_alignment.FaceAlignment(t, device=device, flip_input=flip_input) + super().__init__() + self.log.debug(f'{device}') + self.log.debug(f'{t}') + + def landmarks(self, im): + '''Calculates the 2D facial landmarks + :param im: (numpy.ndarray) BGR image + :returns (list) of 68 (int) (tuples) as (x,y) + ''' + # predict landmarks + points = self.fa.get_landmarks(im) # returns array of arrays of 68 2D pts/face + # convert to data type + points = [list(map(int, p)) for p in points[0]] + return points + + +class Dlib2D(Landmarks2D): + + def __init__(self, model): + super().__init__() + # init dlib + import dlib + self.predictor = dlib.shape_predictor(model) + self.log.info(f'loaded predictor model: {model}') + + def landmarks(self, im, bbox): + # Draw high-confidence faces + dim_wh = im.shape[:2][::-1] + bbox = bbox.to_dlib() + im_gray = cv.cvtColor(im, cv.COLOR_BGR2GRAY) + points = [[p.x, p.y] for p in self.predictor(im_gray, bbox).parts()] + return points + + +class Dlib2D_68(Dlib2D): + + def __init__(self): + # Get 68-point landmarks using DLIB + super().__init__(cfg.DIR_MODELS_DLIB_68PT) + + +class Dlib2D_5(Dlib2D): + + def __init__(self): + # Get 5-point landmarks using DLIB + super().__init__(cfg.DIR_MODELS_DLIB_5PT) + + +class MTCNN2D_5(Landmarks2D): + + # Get 5-point landmarks using MTCNN + # https://github.com/ipazc/mtcnn + # pip install mtcnn + + def __init__(self): + super().__init__() + self.log.warn('NB: MTCNN runs both face detector and landmark predictor together.') + self.log.warn(' this will use face with most similar ROI') + from mtcnn.mtcnn import MTCNN + self.detector = MTCNN() + + def landmarks(self, im, bbox): + '''Detects face using MTCNN and returns (list) of BBox + :param im: (numpy.ndarray) image + :returns list of BBox + ''' + results = [] + dim_wh = im.shape[:2][::-1] # (w, h) + + # run MTCNN to get bbox and landmarks + dets = self.detector.detect_faces(im) + keypoints = [] + bboxes = [] + #iterate detections and convert to BBox + for det in dets: + #rect = det['box'] + points = det['keypoints'] + # convert to normalized for contain-comparison + points_norm = [np.array(pt)/dim_wh for pname, pt in points.items()] + contains = False not in [bbox.contains(pn) for pn in points_norm] + if contains: + results.append(points) # append original points + + return results + + +# ---------------------------------------------------------------------- +# +# 3D landmarks +# +# ---------------------------------------------------------------------- + +class Landmarks3D: + + def __init__(self): + self.log = logger_utils.Logger.getLogger() + + def landmarks(self, im, bbox): + pass + + def flatten(self, points): + '''Converts list of point-tupes into a flattened list for CSV + :param points: (list) of x,y points + :returns dict item for each point (eg {'x1':100, 'y1':200}) + ''' + points_formatted = {} + for idx, pt in enumerate(points, 1): + for j, d in enumerate('xyz'): + points_formatted[f'{d}{idx}'] = pt[j] + return points_formatted + + def normalize(self, points, dim): + return [np.array(p)/dim for p in points] # divides each point by w,h dim + + +class FaceAlignment3D_68(Landmarks3D): + + # Estimates 3D facial landmarks + import face_alignment + + def __init__(self, gpu=0, flip_input=False): + super().__init__() + device = f'cuda:{gpu}' if gpu > -1 else 'cpu' + self.fa = face_alignment.FaceAlignment(face_alignment.LandmarksType._3D, device=device, flip_input=flip_input) + + def landmarks(self, im, as_type=str): + '''Calculates the 3D facial landmarks + :param im: (numpy.ndarray) BGR image + :returns (list) of 68 (int) (tuples) as (x,y, z) + ''' + # predict landmarks + points = self.fa.get_landmarks(im) # returns array of arrays of 68 3D pts/face + # convert to data type + points = [list(map(int, p)) for p in points[0]] + return points
\ No newline at end of file diff --git a/megapixels/app/processors/face_landmarks_2d.py b/megapixels/app/processors/face_landmarks_2d.py deleted file mode 100644 index e8ce93c1..00000000 --- a/megapixels/app/processors/face_landmarks_2d.py +++ /dev/null @@ -1,87 +0,0 @@ -import os -from os.path import join -from pathlib import Path - -import cv2 as cv -import numpy as np -import imutils -from app.utils import im_utils, logger_utils -from app.models.bbox import BBox -from app.settings import app_cfg as cfg -from app.settings import types -from app.models.bbox import BBox - -class LandmarksFaceAlignment: - - # Estimates 2D facial landmarks - import face_alignment - - def __init__(self, gpu=0): - self.log = logger_utils.Logger.getLogger() - device = f'cuda:{gpu}' if gpu > -1 else 'cpu' - self.fa = face_alignment.FaceAlignment(face_alignment.LandmarksType._2D, device=device, flip_input=True) - - def landmarks(self, im, as_type=str): - '''Calculates the 3D facial landmarks - :param im: (numpy.ndarray) image - :param as_type: (str) or (list) type to return data - ''' - preds = self.fa.get_landmarks(im) - # convert to comma separated ints - # storing data as "[1,2], [3,4]" is larger file size than storing as "1,2,3,4" - # storing a list object in Pandas seems to result in 30% larger CSV files - # TODO optimize this - preds_int = [list(map(int, x)) for x in preds[0]] # list of ints - if as_type is str: - return ','.join([','.join(list(map(str,[x,y]))) for x,y in preds_int]) - else: - return preds_int - - -class LandmarksDLIB: - - def __init__(self): - # init dlib - import dlib - self.log = logger_utils.Logger.getLogger() - self.predictor = dlib.shape_predictor(cfg.DIR_MODELS_DLIB_68PT) - - def landmarks(self, im, bbox): - # Draw high-confidence faces - dim = im.shape[:2][::-1] - bbox = bbox.to_dlib() - im_gray = cv.cvtColor(im, cv.COLOR_BGR2GRAY) - landmarks = [[p.x, p.y] for p in self.predictor(im_gray, bbox).parts()] - return landmarks - - -class LandmarksMTCNN: - - # https://github.com/ipazc/mtcnn - # pip install mtcnn - - dnn_size = (400, 400) - - def __init__(self, size=(400,400)): - from mtcnn.mtcnn import MTCNN - self.detector = MTCNN() - - def landmarks(self, im, opt_size=None, opt_conf_thresh=None, opt_pyramids=None): - '''Detects face using MTCNN and returns (list) of BBox - :param im: (numpy.ndarray) image - :returns list of BBox - ''' - rois = [] - dnn_size = self.dnn_size if opt_size is None else opt_size - im = im_utils.resize(im, width=dnn_size[0], height=dnn_size[1]) - dim = im.shape[:2][::-1] - - # run MTCNN - dets = self.detector.detect_faces(im) - - for det in dets: - rect = det['box'] - keypoints = det['keypoints'] # not using here. see 'face_landmarks.py' - bbox = BBox.from_xywh_dim(*rect, dim) - rois.append(bbox) - return rois
\ No newline at end of file diff --git a/megapixels/app/processors/face_landmarks_3d.py b/megapixels/app/processors/face_landmarks_3d.py index 3663364c..470d263c 100644 --- a/megapixels/app/processors/face_landmarks_3d.py +++ b/megapixels/app/processors/face_landmarks_3d.py @@ -12,43 +12,24 @@ from app.models.bbox import BBox from app.settings import app_cfg as cfg from app.settings import types +class Landmarks3D: -class FaceLandmarks2D: - - # Estimates 2D facial landmarks - import face_alignment - - def __init__(self, gpu=0): + def __init__(self): self.log = logger_utils.Logger.getLogger() - device = f'cuda:{gpu}' if gpu > -1 else 'cpu' - self.fa = face_alignment.FaceAlignment(face_alignment.LandmarksType._2D, device=device, flip_input=True) - - def landmarks(self, im, as_type=str): - '''Calculates the 3D facial landmarks - :param im: (numpy.ndarray) image - :param as_type: (str) or (list) type to return data - ''' - preds = self.fa.get_landmarks(im) - # convert to comma separated ints - # storing data as "[1,2], [3,4]" is larger file size than storing as "1,2,3,4" - # storing a list object in Pandas seems to result in 30% larger CSV files - # TODO optimize this - preds_int = [list(map(int, x)) for x in preds[0]] # list of ints - if as_type is str: - return ','.join([','.join(list(map(str,[x,y]))) for x,y in preds_int]) - else - return preds_int + def landmarks(self, im, bbox): + pass -class FaceLandmarks3D: + +class FaceAlignment3D(Landmarks3D): # Estimates 3D facial landmarks import face_alignment - def __init__(self, gpu=0): - self.log = logger_utils.Logger.getLogger() + def __init__(self, gpu=0, flip_input=False): + super().__init__() device = f'cuda:{gpu}' if gpu > -1 else 'cpu' - self.fa = face_alignment.FaceAlignment(face_alignment.LandmarksType._3D, device=device, flip_input=False) + self.fa = face_alignment.FaceAlignment(face_alignment.LandmarksType._3D, device=device, flip_input=flip_input) def landmarks(self, im, as_type=str): '''Calculates the 3D facial landmarks @@ -66,6 +47,7 @@ class FaceLandmarks3D: else return preds_int + def draw(self, im): '''draws landmarks in 3d scene''' diff --git a/megapixels/app/processors/face_pose.py b/megapixels/app/processors/face_pose.py index 96281637..8bc95f8d 100644 --- a/megapixels/app/processors/face_pose.py +++ b/megapixels/app/processors/face_pose.py @@ -95,18 +95,3 @@ class FacePoseDLIB: result['yaw'] = yaw return result - - - def draw_pose(self, im, pt_nose, image_pts): - cv.line(im, pt_nose, tuple(image_pts['pitch'].ravel()), self.pose_types['pitch'], 3) - cv.line(im, pt_nose, tuple(image_pts['yaw'].ravel()), self.pose_types['yaw'], 3) - cv.line(im, pt_nose, tuple(image_pts['roll'].ravel()), self.pose_types['roll'], 3) - - - def draw_degrees(self, im, pose_data, color=(0,255,0)): - for i, pose_type in enumerate(self.pose_types.items()): - k, clr = pose_type - v = pose_data[k] - t = '{}: {:.2f}'.format(k, v) - origin = (10, 30 + (25 * i)) - cv.putText(im, t, origin, cv.FONT_HERSHEY_SIMPLEX, 0.5, clr, thickness=2, lineType=2)
\ No newline at end of file diff --git a/megapixels/app/settings/app_cfg.py b/megapixels/app/settings/app_cfg.py index 55fed166..b13ff8ec 100644 --- a/megapixels/app/settings/app_cfg.py +++ b/megapixels/app/settings/app_cfg.py @@ -14,12 +14,16 @@ codecs.register(lambda name: codecs.lookup('utf8') if name == 'utf8mb4' else Non # Enun lists used for custom Click Params # ----------------------------------------------------------------------------- -FaceDetectNetVar = click_utils.ParamVar(types.FaceDetectNet) -HaarCascadeVar = click_utils.ParamVar(types.HaarCascade) LogLevelVar = click_utils.ParamVar(types.LogLevel) MetadataVar = click_utils.ParamVar(types.Metadata) DatasetVar = click_utils.ParamVar(types.Dataset) DataStoreVar = click_utils.ParamVar(types.DataStore) +# Face analysis +HaarCascadeVar = click_utils.ParamVar(types.HaarCascade) +FaceDetectNetVar = click_utils.ParamVar(types.FaceDetectNet) +FaceLandmark2D_5Var = click_utils.ParamVar(types.FaceLandmark2D_5) +FaceLandmark2D_68Var = click_utils.ParamVar(types.FaceLandmark2D_68) +FaceLandmark3D_68Var = click_utils.ParamVar(types.FaceLandmark3D_68) # # data_store DATA_STORE = '/data_store_hdd/' diff --git a/megapixels/app/settings/types.py b/megapixels/app/settings/types.py index c2e2caf7..50e395e0 100644 --- a/megapixels/app/settings/types.py +++ b/megapixels/app/settings/types.py @@ -6,10 +6,7 @@ def find_type(name, enum_type): return enum_opt return None - -class FaceDetectNet(Enum): - """Scene text detector networks""" - HAAR, DLIB_CNN, DLIB_HOG, CVDNN, MTCNN = range(5) + class CVBackend(Enum): """OpenCV 3.4.2+ DNN target type""" @@ -45,16 +42,32 @@ class LogLevel(Enum): # -------------------------------------------------------------------- class Metadata(Enum): - IDENTITY, FILE_RECORD, FACE_VECTOR, FACE_POSE, FACE_ROI, FACE_LANDMARKS_2D_68, \ - FACE_LANDMARKS_3D_68 = range(7) + IDENTITY, FILE_RECORD, FACE_VECTOR, FACE_POSE, \ + FACE_ROI, FACE_LANDMARK_2D_68, FACE_LANDMARK_2D_5,FACE_LANDMARK_3D_68 = range(8) class Dataset(Enum): - LFW, VGG_FACE2, MSCELEB, UCCS, UMD_FACES = range(5) + LFW, VGG_FACE2, MSCELEB, UCCS, UMD_FACES, SCUT_FBP, SELFIE_DATASET = range(7) # --------------------------------------------------------------------- # Face analysis types # -------------------------------------------------------------------- +class FaceDetectNet(Enum): + """Scene text detector networks""" + HAAR, DLIB_CNN, DLIB_HOG, CVDNN, MTCNN = range(5) + +class FaceLandmark2D_5(Enum): + DLIB, MTCNN = range(2) + +class FaceLandmark2D_68(Enum): + DLIB, FACE_ALIGNMENT = range(2) + +class FaceLandmark3D_68(Enum): + FACE_ALIGNMENT = range(1) + +class FaceLandmark3D(Enum): + FACE_ALIGNMENT = range(1) + class FaceEmotion(Enum): # Map these to text strings for web display NEUTRAL, HAPPY, SAD, ANGRY, FRUSTURATED = range(5) diff --git a/megapixels/app/utils/display_utils.py b/megapixels/app/utils/display_utils.py new file mode 100644 index 00000000..58e2feec --- /dev/null +++ b/megapixels/app/utils/display_utils.py @@ -0,0 +1,16 @@ +import sys + +import cv2 as cv + + +def handle_keyboard(): + '''Used with cv.imshow('title', image) to wait for keyboard press + ''' + while True: + k = cv.waitKey(1) & 0xFF + if k == 27 or k == ord('q'): # ESC + cv.destroyAllWindows() + sys.exit() + elif k != 255: + # any key to continue + break
\ No newline at end of file diff --git a/megapixels/app/utils/draw_utils.py b/megapixels/app/utils/draw_utils.py new file mode 100644 index 00000000..f6d53609 --- /dev/null +++ b/megapixels/app/utils/draw_utils.py @@ -0,0 +1,65 @@ +import sys + +import cv2 as cv + + +# --------------------------------------------------------------------------- +# +# OpenCV drawing functions +# +# --------------------------------------------------------------------------- + +pose_types = {'pitch': (0,0,255), 'roll': (255,0,0), 'yaw': (0,255,0)} + + +def draw_landmarks2D(im, points, radius=3, color=(0,255,0), stroke_weight=2): + '''Draws facial landmarks, either 5pt or 68pt + ''' + for x,y in points: + cv.circle(im, (x,y), radius, color, -1, cv.LINE_AA) + + +def draw_landmarks3D(im, points, radius=3, color=(0,255,0), stroke_weight=2): + '''Draws 3D facial landmarks + ''' + for x,y,z in points: + cv.circle(im, (x,y), radius, color, -1, cv.LINE_AA) + + +def draw_bbox(im, bbox, color=(0,255,0), stroke_weight=2): + '''Draws a dimensioned (not-normalized) BBox onto cv2 image + ''' + cv.rectangle(im, bbox.pt_tl, bbox.pt_br, color, stroke_weight) + + +def draw_pose(im, pt_nose, image_pts): + '''Draws 3-axis pose over image + ''' + cv.line(im, pt_nose, tuple(image_pts['pitch'].ravel()), pose_types['pitch'], 3) + cv.line(im, pt_nose, tuple(image_pts['yaw'].ravel()), pose_types['yaw'], 3) + cv.line(im, pt_nose, tuple(image_pts['roll'].ravel()), pose_types['roll'], 3) + + +def draw_degrees(im, pose_data, color=(0,255,0)): + '''Draws degrees as text over image + ''' + for i, pose_type in enumerate(pose_types.items()): + k, clr = pose_type + v = pose_data[k] + t = '{}: {:.2f}'.format(k, v) + origin = (10, 30 + (25 * i)) + cv.putText(im, t, origin, cv.FONT_HERSHEY_SIMPLEX, 0.5, clr, thickness=2, lineType=2) + + +# --------------------------------------------------------------------------- +# +# Matplotlib drawing functions +# +# --------------------------------------------------------------------------- + +def plot_landmarks3D(im, points, radius=3, color=(0,255,0), stroke_weight=2): + '''Draws facial landmarks, either 5pt or 68pt + ''' + for pt in points: + cv.circle(im, tuple(pt), radius, color, -1, cv.LINE_AA) + |
