├── .gitignore ├── 000-show-pi-camera └── app-show-picam.py ├── 001-pi-face-detection ├── altusi │ ├── Aller_Bd.ttf │ ├── REAME.md │ ├── __init__.py │ ├── config.py │ ├── facedetector.py │ ├── helper.py │ ├── imgproc.py │ ├── logger.py │ └── visualizer.py ├── app-face-detector.py └── openvino-models ├── 002-pi-facial-landmark-detection ├── altusi │ ├── Aller_Bd.ttf │ ├── REAME.md │ ├── __init__.py │ ├── config.py │ ├── facedetector.py │ ├── facelandmarker.py │ ├── helper.py │ ├── imgproc.py │ ├── logger.py │ └── visualizer.py ├── app-landmark-detector.py ├── demo.py └── openvino-models ├── 003-pi-face-alignment ├── altusi │ ├── .gitignore │ ├── Aller_Bd.ttf │ ├── REAME.md │ ├── __init__.py │ ├── config.py │ ├── facealigner.py │ ├── facedetector.py │ ├── facelandmarker.py │ ├── helper.py │ ├── imgproc.py │ ├── logger.py │ └── visualizer.py ├── app-face-alignment.py ├── exp-image-openface.py ├── exp-image-openvino.py ├── exp-video-openvino.py └── openvino-models ├── 004-pi-head-pose-estimation ├── altusi │ ├── Aller_Bd.ttf │ ├── __init__.py │ ├── config.py │ ├── facedetector.py │ ├── facelandmarker.py │ ├── headposer.py │ ├── helper.py │ ├── imgproc.py │ ├── inference.py │ ├── logger.py │ └── visualizer.py ├── app-headpose-image.py ├── app-headpose-vid.py └── headvisualizer.py ├── 005-pi-object-detection ├── altusi │ ├── Aller_Bd.ttf │ ├── REAME.md │ ├── __init__.py │ ├── config.py │ ├── helper.py │ ├── imgproc.py │ ├── logger.py │ ├── objectdetector.py │ └── visualizer.py ├── app-object-detector.py └── openvino-models ├── 006-pi-face-verification ├── altusi │ ├── Aller_Bd.ttf │ ├── REAME.md │ ├── __init__.py │ ├── config.py │ ├── facealigner.py │ ├── facedetector.py │ ├── faceembedder.py │ ├── facelandmarker.py │ ├── helper.py │ ├── imgproc.py │ ├── logger.py │ └── visualizer.py ├── app-face-verify.py └── openvino-models ├── 008-pi-emotion-recognition ├── altusi │ ├── Aller_Bd.ttf │ ├── __init__.py │ ├── config.py │ ├── emotioner.py │ ├── facealigner.py │ ├── facedetector.py │ ├── faceembedder.py │ ├── facelandmarker.py │ ├── helper.py │ ├── imgproc.py │ ├── logger.py │ └── visualizer.py ├── app-emotion-image.py ├── app-emotion-recog.py └── openvino-models ├── 012-tflite-object-detection ├── README.md ├── altusi │ ├── Aller_Bd.ttf │ ├── __init__.py │ ├── config.py │ ├── facelibs │ │ ├── __init__.py │ │ ├── facealigner.py │ │ ├── facedetector.py │ │ ├── faceembedder.py │ │ └── facelandmarker.py │ ├── helper.py │ ├── imgproc.py │ ├── logger.py │ └── visualizer.py ├── app-image.py ├── app-video.py └── tflite-models ├── 013-publish-docker └── README.md ├── README.md └── install.md /.gitignore: -------------------------------------------------------------------------------- 1 | # AI model 2 | *.dat 3 | *.h5 4 | *.hdf5 5 | *.pkl 6 | *.pth 7 | *.params 8 | 9 | 10 | # vscode folder 11 | .vscode/ 12 | 13 | # pycache file 14 | __pycache__/ 15 | 16 | # data folder 17 | data/ 18 | 19 | # .ipynb checkpoints 20 | .ipynb_checkpoints/ 21 | 22 | # vim swap file 23 | *.sw[onp] 24 | 25 | # media files 26 | *.avi 27 | *.mp4 28 | *.jpg 29 | *.png 30 | 31 | # log file 32 | *.log 33 | -------------------------------------------------------------------------------- /000-show-pi-camera/app-show-picam.py: -------------------------------------------------------------------------------- 1 | # import the necessary packages 2 | from picamera.array import PiRGBArray 3 | from picamera import PiCamera 4 | import time 5 | import cv2 as cv 6 | 7 | # initialize the camera and grab a reference to the raw camera capture 8 | camera = PiCamera() 9 | camera.resolution = (640, 480) 10 | camera.framerate = 32 11 | rawCapture = PiRGBArray(camera, size=(640, 480)) 12 | 13 | # allow the camera to warmup 14 | time.sleep(0.1) 15 | 16 | # capture frames from the camera 17 | for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True): 18 | # grab the raw NumPy array representing the image, then initialize the timestamp 19 | # and occupied/unoccupied text 20 | image = frame.array 21 | 22 | # show the frame 23 | cv.imshow("Frame", image) 24 | 25 | # clear the stream in preparation for the next frame 26 | rawCapture.truncate(0) 27 | 28 | key = cv.waitKey(1) 29 | if key in [27, ord('q')]: 30 | break 31 | 32 | -------------------------------------------------------------------------------- /001-pi-face-detection/altusi/Aller_Bd.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/001-pi-face-detection/altusi/Aller_Bd.ttf -------------------------------------------------------------------------------- /001-pi-face-detection/altusi/REAME.md: -------------------------------------------------------------------------------- 1 | # AltusI 2 | My library for Computer Vision and Artificial Intelligence 3 | 4 | # Update 5 | **2019, Sep 25:** 6 | - Add `config` for project's configuration 7 | - Add `helper` for support functions 8 | - Add `visualizer` module for visualization purpose 9 | - Develop `plotBBoxes` (with `classes`) functions to plot bboxes and classes with different colors 10 | 11 | **2019, Sep 24:** 12 | - Add `logger` for logging purpose 13 | -------------------------------------------------------------------------------- /001-pi-face-detection/altusi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/001-pi-face-detection/altusi/__init__.py -------------------------------------------------------------------------------- /001-pi-face-detection/altusi/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # ============================================================================= 5 | # PROJECT'S ORGANIZATION 6 | # ============================================================================= 7 | PROJECT_BASE = '.' 8 | 9 | #=============================================================================== 10 | # PROJECT'S PARAMETERS 11 | #=============================================================================== 12 | FONT = os.path.join(PROJECT_BASE, 'altusi', 'Aller_Bd.ttf') 13 | 14 | TIME_FM = '-%Y%m%d-%H%M%S' 15 | 16 | #=============================================================================== 17 | # PROJECT'S MODELS 18 | #=============================================================================== 19 | MODEL_DIR = 'openvino-models' 20 | FACE_DET_XML = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.xml') 21 | FACE_DET_BIN = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.bin') 22 | -------------------------------------------------------------------------------- /001-pi-face-detection/altusi/facedetector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Detector class 3 | =================== 4 | 5 | Module for Face Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceDetector: 16 | """Face Detector class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_DET_XML, 20 | bin_path=cfg.FACE_DET_BIN): 21 | """Initialize Face detector object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | 24 | # with NCS support 25 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 26 | 27 | 28 | def getFaces(self, image, def_score=0.5): 29 | """Detect faces in an input image with given threshold""" 30 | H, W = image.shape[:2] 31 | blob = cv.dnn.blobFromImage(image, size=(672, 384), ddepth=cv.CV_8U) 32 | self.__net.setInput(blob) 33 | out = self.__net.forward() 34 | 35 | bboxes = [] 36 | scores = [] 37 | for det in out.reshape(-1, 7): 38 | score = float(det[2]) 39 | if score < def_score: continue 40 | 41 | x1 = max(0, int(det[3] * W)) 42 | y1 = max(0, int(det[4] * H)) 43 | x2 = min(W, int(det[5] * W)) 44 | y2 = min(H, int(det[6] * H)) 45 | 46 | bboxes.append((x1, y1, x2, y2)) 47 | scores.append(score) 48 | 49 | return scores, bboxes 50 | -------------------------------------------------------------------------------- /001-pi-face-detection/altusi/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module 3 | ============ 4 | 5 | Support functions for file utilities 6 | """ 7 | 8 | import argparse 9 | import os 10 | 11 | 12 | def getFilename(file_path): 13 | path, filename = os.path.split(file_path) 14 | 15 | return path, filename 16 | 17 | 18 | def getFileNameExt(file_path): 19 | path, filename = getFilename(file_path) 20 | 21 | filename, ext = os.path.splitext(filename) 22 | 23 | return path, filename, ext 24 | 25 | def getArgs(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('--video', '-v', type=str, 28 | required=False, 29 | help='Video Streamming link or Path to video source') 30 | parser.add_argument('--name', '-n', type=str, 31 | required=False, default='camera', 32 | help='Name of video source') 33 | parser.add_argument('--show', '-s', 34 | default=False, action='store_true', 35 | help='Whether to show the output visualization') 36 | parser.add_argument('--flip_hor', '-fh', 37 | required=False, default=False, action='store_true', 38 | help='horizontally flip video frame') 39 | parser.add_argument('--flip_ver', '-fv', 40 | required=False, default=False, action='store_true', 41 | help='vertically flip video frame') 42 | args = parser.parse_args() 43 | 44 | return args 45 | -------------------------------------------------------------------------------- /001-pi-face-detection/altusi/imgproc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Imgproc library 3 | =============== 4 | 5 | Library to support Image processing functions 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Oct 03: 12 | - re-add to AltusI version 0.2 13 | """ 14 | 15 | import os 16 | import math 17 | import numpy as np 18 | import cv2 as cv 19 | 20 | 21 | def resizeByHeight(image, height=720): 22 | """Resize an image given the expected height and keep the original ratio 23 | 24 | Arguments: 25 | ---------- 26 | image : numpy.array 27 | input image to resize 28 | 29 | Keyword Arguments: 30 | ------------------ 31 | height : int (default: 720) 32 | expected width of output image 33 | 34 | Returns: 35 | -------- 36 | out_image : numpy.array 37 | output resized image 38 | """ 39 | H, W = image.shape[:2] 40 | width = int(1. * W * height / H + 0.5) 41 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 42 | 43 | return out_image 44 | 45 | 46 | def resizeByWidth(image, width=600): 47 | """Resize an image given the expected width and keep the original ratio 48 | 49 | Arguments: 50 | ---------- 51 | image : numpy.array 52 | input image to resize 53 | 54 | Keyword Arguments: 55 | ------------------ 56 | width : int (default: 600) 57 | expected width of output image 58 | 59 | Returns: 60 | -------- 61 | out_image : numpy.array 62 | output colored image after resized 63 | """ 64 | 65 | H, W = image.shape[:2] 66 | height = int(H * width / W) 67 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 68 | return out_image 69 | 70 | 71 | def cameraCalibrate(capturer, size=None, by_height=False): 72 | """Get camera's information like dimension and FPS 73 | 74 | Arguments: 75 | ---------- 76 | capturer : cv.VideoCapture 77 | OpenCV-Video capturer object 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | width : int (default: None) 82 | width value to resize by width 83 | 84 | Returns: 85 | -------- 86 | (W, H) : int, int 87 | dimension of video's frame 88 | FPS : float 89 | FPS of the video stream 90 | """ 91 | 92 | fps = capturer.get(cv.CAP_PROP_FPS) 93 | 94 | while True: 95 | _, frame = capturer.read() 96 | if _: 97 | if size: 98 | if by_height: 99 | frame = resizeByHeight(frame, size) 100 | else: 101 | frame = resizeByWidth(frame, size) 102 | H, W = frame.shape[:2] 103 | 104 | return (W, H), fps 105 | 106 | -------------------------------------------------------------------------------- /001-pi-face-detection/altusi/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logger class 3 | ============ 4 | 5 | Wrapper of built-in `logging` Python module 6 | that supports logging task to both console and files 7 | """ 8 | 9 | """ 10 | Revision 11 | -------- 12 | 2019, Sep 24: first version 13 | """ 14 | 15 | 16 | import os 17 | import logging 18 | 19 | # format for logging message 20 | LOG_FILE_FORMAT = '%(asctime)s %(name)12s [%(levelname)s] %(message)s' 21 | LOG_CONSOLE_FORMAT = '%(asctime)s [%(levelname)s] %(message)s' 22 | 23 | class Logger(): 24 | """Logger class for logging task""" 25 | 26 | def __init__(self, name, console=True): 27 | """Initialize logger 28 | 29 | Parameters 30 | ---------- 31 | name : str 32 | name of logger and file to log 33 | console : bool 34 | whether logging messages are emitted to console or not 35 | """ 36 | self.logger = logging.getLogger(name) 37 | 38 | self.__configLogger(name, console) 39 | 40 | 41 | def __configLogger(self, name, console): 42 | """Configure logger object 43 | 44 | Parameters 45 | ---------- 46 | name : str 47 | name of logging file 48 | console : bool 49 | whether logging messages are emitted to console or not 50 | """ 51 | self.logger.setLevel(logging.DEBUG) 52 | file_formatter = logging.Formatter(LOG_FILE_FORMAT) 53 | console_formatter = logging.Formatter(LOG_CONSOLE_FORMAT) 54 | 55 | # if `console` is set, setup Handler to process 56 | if console: 57 | console_log = logging.StreamHandler() 58 | console_log.setLevel(logging.DEBUG) 59 | console_log.setFormatter(console_formatter) 60 | 61 | file_log = logging.FileHandler(name + '.log', 62 | mode='w') 63 | file_log.setLevel(logging.DEBUG) 64 | file_log.setFormatter(file_formatter) 65 | 66 | if console: 67 | self.logger.addHandler(console_log) 68 | self.logger.addHandler(file_log) 69 | 70 | 71 | def critical(self, msg): 72 | """Emit CRITICAL message""" 73 | self.logger.critical(msg) 74 | 75 | 76 | def error(self, msg): 77 | """Emit ERROR message""" 78 | self.logger.error(msg) 79 | 80 | 81 | def warning(self, msg): 82 | """Emit WARNING message""" 83 | self.logger.warning(msg) 84 | 85 | 86 | def info(self, msg): 87 | """Emit INFO message""" 88 | self.logger.info(msg) 89 | 90 | 91 | def debug(self, msg): 92 | """Emit DEBUG message""" 93 | self.logger.debug(msg) 94 | -------------------------------------------------------------------------------- /001-pi-face-detection/altusi/visualizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Visualization module 3 | ==================== 4 | 5 | Process Image visualization, PIL package is in use 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Sep 25: first version 12 | - add plotBBoxes (with class ids) 13 | """ 14 | 15 | 16 | import random as rnd 17 | 18 | import numpy as np 19 | import cv2 as cv 20 | import PIL 21 | from PIL import Image, ImageFont, ImageDraw, ImageColor 22 | 23 | import altusi.config as cfg 24 | 25 | STANDARD_COLORS = [ 26 | 'AliceBlue', 'Chartreuse', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 27 | 'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue', 'AntiqueWhite', 28 | 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 29 | 'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange', 30 | 'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet', 31 | 'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite', 32 | 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', 33 | 'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki', 34 | 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 35 | 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', 36 | 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 37 | 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', 38 | 'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid', 39 | 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 40 | 'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin', 41 | 'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 42 | 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 43 | 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 44 | 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Green', 'SandyBrown', 45 | 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 46 | 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow', 47 | 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 48 | 'WhiteSmoke', 'Yellow', 'YellowGreen' 49 | ] 50 | 51 | COLOR_MAP = { 52 | 'face':'Crimson', 'bicycle':'BlueViolet', 'bus':'Gold', 'car':'DodgerBlue', 53 | 'motorbike':'OrangeRed', 'person':'Chartreuse' 54 | } 55 | 56 | def getRandomColor(): 57 | """Generate random color 58 | 59 | Returns: 60 | -------- 61 | color : tuple(int, int, int) 62 | generated random color 63 | """ 64 | 65 | color = tuple([int(255*rnd.random()) for _ in range(3)]) 66 | return color 67 | 68 | 69 | def plotBBoxes(image, bboxes, classes=None, scores=None, color='Chartreuse', linewidth=2, use_rgb=False): 70 | """Plot bounding boxes for given input objects 71 | 72 | Arguments: 73 | ---------- 74 | image : numpy.array 75 | input image for drawing 76 | bboxes : list((x1, y1, x2, y2)) 77 | input bounding boxes of objects to draw 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | classes : list(str) (default: None) 82 | list of classes for objects 83 | color : str (default: `Chartreuse`) 84 | color to plot 85 | linewidth : int (default: 2) 86 | how thick the shape is 87 | use_rgb : bool (default: False) 88 | whether using RGB image or not (apply for NDArray image) 89 | 90 | Returns: 91 | -------- 92 | image : numpy.array 93 | output image after drawing 94 | """ 95 | 96 | if len(bboxes) == 0: 97 | return image 98 | exit() 99 | 100 | if isinstance(image, np.ndarray): 101 | if not use_rgb: 102 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 103 | image = Image.fromarray(image) 104 | 105 | W, H = image.size 106 | draw = ImageDraw.Draw(image) 107 | 108 | if classes is not None: 109 | font = ImageFont.truetype(font=cfg.FONT, 110 | size=np.floor(3e-2*min(H, W) + 0.5).astype('int32') ) 111 | 112 | for i, ((x1, y1, x2, y2), cls) in enumerate(zip(bboxes, classes)): 113 | # draw bounding box 114 | draw.rectangle([x1, y1, x2, y2], 115 | outline=ImageColor.getrgb(COLOR_MAP[cls]), 116 | width=linewidth) 117 | # draw label 118 | text = cls 119 | if scores is not None: 120 | text = '{} {:.2f}'.format(cls, scores[i]) 121 | 122 | label_size = draw.textsize(text, font) 123 | text_coor = np.array([x1+linewidth, max(0, y1 - label_size[1] - 1)]) 124 | rec_coor = np.array([x1, text_coor[1]]) 125 | 126 | draw.rectangle([tuple(rec_coor), 127 | tuple(rec_coor + label_size + np.array([linewidth*2, 0])) ], 128 | fill=ImageColor.getrgb(COLOR_MAP[cls])) 129 | draw.text(text_coor, text, fill=ImageColor.getrgb('black'), font=font) 130 | else: 131 | for i, (x1, y1, x2, y2) in enumerate(bboxes): 132 | draw.rectangle([x1, y1, x2, y2], 133 | outline=ImageColor.getrgb(color), 134 | width=linewidth) 135 | 136 | del draw 137 | return image 138 | 139 | 140 | def plotInfo(image, info, color='Chartreuse', use_rgb=False): 141 | if isinstance(image, np.ndarray): 142 | if not use_rgb: 143 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 144 | image = Image.fromarray(image) 145 | 146 | W, H = image.size 147 | draw = ImageDraw.Draw(image) 148 | 149 | font = ImageFont.truetype(font=cfg.FONT, 150 | size=np.floor(3e-2*min(H, W) + 0.5).astype('int32') ) 151 | 152 | label_size = draw.textsize(info, font) 153 | text_coor = np.array([5, 0]) 154 | rec_coor = np.array([0, 0]) 155 | draw.rectangle([tuple(rec_coor), 156 | tuple(rec_coor + label_size + np.array([10, 0])) ], 157 | fill=ImageColor.getrgb(color)) 158 | draw.text(text_coor, info, fill=ImageColor.getrgb('black'), font=font) 159 | 160 | del draw 161 | return image 162 | -------------------------------------------------------------------------------- /001-pi-face-detection/app-face-detector.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import cv2 as cv 4 | 5 | import altusi.config as cfg 6 | import altusi.visualizer as vis 7 | from altusi import imgproc, helper 8 | from altusi.logger import Logger 9 | 10 | from altusi.facedetector import FaceDetector 11 | 12 | LOG = Logger('app-face-detector') 13 | 14 | def app(video_link, video_name, show, flip_hor, flip_ver): 15 | # initialize Face Detection net 16 | face_detector = FaceDetector() 17 | 18 | # initialize Video Capturer 19 | cap = cv.VideoCapture(video_link) 20 | (W, H), FPS = imgproc.cameraCalibrate(cap) 21 | LOG.info('Camera Info: ({}, {}) - {:.3f}'.format(W, H, FPS)) 22 | 23 | while cap.isOpened(): 24 | _, frm = cap.read() 25 | if not _: 26 | LOG.info('Reached the end of Video source') 27 | break 28 | 29 | if flip_ver: frm = cv.flip(frm, 0) 30 | if flip_hor: frm = cv.flip(frm, 1) 31 | 32 | _start_t = time.time() 33 | scores, bboxes = face_detector.getFaces(frm, def_score=0.5) 34 | _prx_t = time.time() - _start_t 35 | 36 | 37 | if len(bboxes): 38 | frm = vis.plotBBoxes(frm, bboxes, len(bboxes) * ['face'], scores) 39 | frm = vis.plotInfo(frm, 'Raspberry Pi - FPS: {:.3f}'.format(1/_prx_t)) 40 | frm = cv.cvtColor(np.asarray(frm), cv.COLOR_BGR2RGB) 41 | 42 | if show: 43 | cv.imshow(video_name, frm) 44 | key = cv.waitKey(1) 45 | if key in [27, ord('q')]: 46 | LOG.info('Interrupted by Users') 47 | break 48 | 49 | cap.release() 50 | cv.destroyAllWindows() 51 | 52 | 53 | def main(args): 54 | video_link = args.video if args.video else 0 55 | app(video_link, args.name, args.show, args.flip_hor, args.flip_ver) 56 | 57 | 58 | if __name__ == '__main__': 59 | LOG.info('Raspberry Pi: Face Detection') 60 | 61 | args = helper.getArgs() 62 | main(args) 63 | 64 | LOG.info('Process done') 65 | -------------------------------------------------------------------------------- /001-pi-face-detection/openvino-models: -------------------------------------------------------------------------------- 1 | /home/pi/workspace/openvino-models/ -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/altusi/Aller_Bd.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/002-pi-facial-landmark-detection/altusi/Aller_Bd.ttf -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/altusi/REAME.md: -------------------------------------------------------------------------------- 1 | # AltusI 2 | My library for Computer Vision and Artificial Intelligence 3 | 4 | # Update 5 | **2019, Sep 25:** 6 | - Add `config` for project's configuration 7 | - Add `helper` for support functions 8 | - Add `visualizer` module for visualization purpose 9 | - Develop `plotBBoxes` (with `classes`) functions to plot bboxes and classes with different colors 10 | 11 | **2019, Sep 24:** 12 | - Add `logger` for logging purpose 13 | -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/altusi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/002-pi-facial-landmark-detection/altusi/__init__.py -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/altusi/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # ============================================================================= 5 | # PROJECT'S ORGANIZATION 6 | # ============================================================================= 7 | PROJECT_BASE = '.' 8 | 9 | #=============================================================================== 10 | # PROJECT'S PARAMETERS 11 | #=============================================================================== 12 | FONT = os.path.join(PROJECT_BASE, 'altusi', 'Aller_Bd.ttf') 13 | 14 | TIME_FM = '-%Y%m%d-%H%M%S' 15 | 16 | #=============================================================================== 17 | # PROJECT'S MODELS 18 | #=============================================================================== 19 | MODEL_DIR = 'openvino-models' 20 | FACE_DET_XML = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.xml') 21 | FACE_DET_BIN = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.bin') 22 | 23 | FACE_LM_XML = os.path.join(MODEL_DIR, 'landmarks-regression-retail-0009-fp16.xml') 24 | FACE_LM_BIN = os.path.join(MODEL_DIR, 'landmarks-regression-retail-0009-fp16.bin') 25 | -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/altusi/facedetector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Detector class 3 | =================== 4 | 5 | Module for Face Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceDetector: 16 | """Face Detector class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_DET_XML, 20 | bin_path=cfg.FACE_DET_BIN): 21 | """Initialize Face detector object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | 24 | # with NCS support 25 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 26 | 27 | 28 | def getFaces(self, image, def_score=0.5): 29 | """Detect faces in an input image with given threshold""" 30 | H, W = image.shape[:2] 31 | blob = cv.dnn.blobFromImage(image, size=(672, 384), ddepth=cv.CV_8U) 32 | self.__net.setInput(blob) 33 | out = self.__net.forward() 34 | 35 | bboxes = [] 36 | scores = [] 37 | for det in out.reshape(-1, 7): 38 | score = float(det[2]) 39 | if score < def_score: continue 40 | 41 | x1 = max(0, int(det[3] * W)) 42 | y1 = max(0, int(det[4] * H)) 43 | x2 = min(W, int(det[5] * W)) 44 | y2 = min(H, int(det[6] * H)) 45 | 46 | bboxes.append((x1, y1, x2, y2)) 47 | scores.append(score) 48 | 49 | return scores, bboxes 50 | -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/altusi/facelandmarker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Facial Landmarker class 3 | ======================= 4 | 5 | Module for Facial Landmark Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceLandmarker: 16 | """Face Landmarker class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_LM_XML, 20 | bin_path=cfg.FACE_LM_BIN): 21 | """Initialize Face Landmarker object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 24 | 25 | 26 | def getLandmark(self, image): 27 | """Locate Facial landmark points in a face image""" 28 | H, W = image.shape[:2] 29 | 30 | blob = cv.dnn.blobFromImage(image, size=(48, 48), ddepth=cv.CV_8U) 31 | self.__net.setInput(blob) 32 | 33 | reg = self.__net.forward().reshape(-1, 10) 34 | points = np.zeros((5, 2), np.int) 35 | for i in range(0, 10, 2): 36 | points[i//2] = (reg[0][i:i+2] * np.array([W, H])).astype(np.int) 37 | 38 | return points 39 | 40 | -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/altusi/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module 3 | ============ 4 | 5 | Support functions for file utilities 6 | """ 7 | 8 | import argparse 9 | import os 10 | 11 | 12 | def getFilename(file_path): 13 | path, filename = os.path.split(file_path) 14 | 15 | return path, filename 16 | 17 | 18 | def getFileNameExt(file_path): 19 | path, filename = getFilename(file_path) 20 | 21 | filename, ext = os.path.splitext(filename) 22 | 23 | return path, filename, ext 24 | 25 | def getArgs(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('--video', '-v', type=str, 28 | required=False, 29 | help='Video Streamming link or Path to video source') 30 | parser.add_argument('--name', '-n', type=str, 31 | required=False, default='camera', 32 | help='Name of video source') 33 | parser.add_argument('--show', '-s', 34 | default=False, action='store_true', 35 | help='Whether to show the output visualization') 36 | parser.add_argument('--flip_hor', '-fh', 37 | required=False, default=False, action='store_true', 38 | help='horizontally flip video frame') 39 | parser.add_argument('--flip_ver', '-fv', 40 | required=False, default=False, action='store_true', 41 | help='vertically flip video frame') 42 | args = parser.parse_args() 43 | 44 | return args 45 | -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/altusi/imgproc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Imgproc library 3 | =============== 4 | 5 | Library to support Image processing functions 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Oct 03: 12 | - re-add to AltusI version 0.2 13 | """ 14 | 15 | import os 16 | import math 17 | import numpy as np 18 | import cv2 as cv 19 | 20 | 21 | def resizeByHeight(image, height=720): 22 | """Resize an image given the expected height and keep the original ratio 23 | 24 | Arguments: 25 | ---------- 26 | image : numpy.array 27 | input image to resize 28 | 29 | Keyword Arguments: 30 | ------------------ 31 | height : int (default: 720) 32 | expected width of output image 33 | 34 | Returns: 35 | -------- 36 | out_image : numpy.array 37 | output resized image 38 | """ 39 | H, W = image.shape[:2] 40 | width = int(1. * W * height / H + 0.5) 41 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 42 | 43 | return out_image 44 | 45 | 46 | def resizeByWidth(image, width=600): 47 | """Resize an image given the expected width and keep the original ratio 48 | 49 | Arguments: 50 | ---------- 51 | image : numpy.array 52 | input image to resize 53 | 54 | Keyword Arguments: 55 | ------------------ 56 | width : int (default: 600) 57 | expected width of output image 58 | 59 | Returns: 60 | -------- 61 | out_image : numpy.array 62 | output colored image after resized 63 | """ 64 | 65 | H, W = image.shape[:2] 66 | height = int(H * width / W) 67 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 68 | return out_image 69 | 70 | 71 | def cameraCalibrate(capturer, size=None, by_height=False): 72 | """Get camera's information like dimension and FPS 73 | 74 | Arguments: 75 | ---------- 76 | capturer : cv.VideoCapture 77 | OpenCV-Video capturer object 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | width : int (default: None) 82 | width value to resize by width 83 | 84 | Returns: 85 | -------- 86 | (W, H) : int, int 87 | dimension of video's frame 88 | FPS : float 89 | FPS of the video stream 90 | """ 91 | 92 | fps = capturer.get(cv.CAP_PROP_FPS) 93 | 94 | while True: 95 | _, frame = capturer.read() 96 | if _: 97 | if size: 98 | if by_height: 99 | frame = resizeByHeight(frame, size) 100 | else: 101 | frame = resizeByWidth(frame, size) 102 | H, W = frame.shape[:2] 103 | 104 | return (W, H), fps 105 | 106 | -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/altusi/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logger class 3 | ============ 4 | 5 | Wrapper of built-in `logging` Python module 6 | that supports logging task to both console and files 7 | """ 8 | 9 | """ 10 | Revision 11 | -------- 12 | 2019, Sep 24: first version 13 | """ 14 | 15 | 16 | import os 17 | import logging 18 | 19 | # format for logging message 20 | LOG_FILE_FORMAT = '%(asctime)s %(name)12s [%(levelname)s] %(message)s' 21 | LOG_CONSOLE_FORMAT = '%(asctime)s [%(levelname)s] %(message)s' 22 | 23 | class Logger(): 24 | """Logger class for logging task""" 25 | 26 | def __init__(self, name, console=True): 27 | """Initialize logger 28 | 29 | Parameters 30 | ---------- 31 | name : str 32 | name of logger and file to log 33 | console : bool 34 | whether logging messages are emitted to console or not 35 | """ 36 | self.logger = logging.getLogger(name) 37 | 38 | self.__configLogger(name, console) 39 | 40 | 41 | def __configLogger(self, name, console): 42 | """Configure logger object 43 | 44 | Parameters 45 | ---------- 46 | name : str 47 | name of logging file 48 | console : bool 49 | whether logging messages are emitted to console or not 50 | """ 51 | self.logger.setLevel(logging.DEBUG) 52 | file_formatter = logging.Formatter(LOG_FILE_FORMAT) 53 | console_formatter = logging.Formatter(LOG_CONSOLE_FORMAT) 54 | 55 | # if `console` is set, setup Handler to process 56 | if console: 57 | console_log = logging.StreamHandler() 58 | console_log.setLevel(logging.DEBUG) 59 | console_log.setFormatter(console_formatter) 60 | 61 | file_log = logging.FileHandler(name + '.log', 62 | mode='w') 63 | file_log.setLevel(logging.DEBUG) 64 | file_log.setFormatter(file_formatter) 65 | 66 | if console: 67 | self.logger.addHandler(console_log) 68 | self.logger.addHandler(file_log) 69 | 70 | 71 | def critical(self, msg): 72 | """Emit CRITICAL message""" 73 | self.logger.critical(msg) 74 | 75 | 76 | def error(self, msg): 77 | """Emit ERROR message""" 78 | self.logger.error(msg) 79 | 80 | 81 | def warning(self, msg): 82 | """Emit WARNING message""" 83 | self.logger.warning(msg) 84 | 85 | 86 | def info(self, msg): 87 | """Emit INFO message""" 88 | self.logger.info(msg) 89 | 90 | 91 | def debug(self, msg): 92 | """Emit DEBUG message""" 93 | self.logger.debug(msg) 94 | -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/altusi/visualizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Visualization module 3 | ==================== 4 | 5 | Process Image visualization, PIL package is in use 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Sep 25: first version 12 | - add plotBBoxes (with class ids) 13 | """ 14 | 15 | 16 | import random as rnd 17 | 18 | import numpy as np 19 | import cv2 as cv 20 | import PIL 21 | from PIL import Image, ImageFont, ImageDraw, ImageColor 22 | 23 | import altusi.config as cfg 24 | 25 | STANDARD_COLORS = [ 26 | 'AliceBlue', 'Chartreuse', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 27 | 'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue', 'AntiqueWhite', 28 | 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 29 | 'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange', 30 | 'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet', 31 | 'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite', 32 | 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', 33 | 'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki', 34 | 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 35 | 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', 36 | 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 37 | 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', 38 | 'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid', 39 | 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 40 | 'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin', 41 | 'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 42 | 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 43 | 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 44 | 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Green', 'SandyBrown', 45 | 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 46 | 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow', 47 | 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 48 | 'WhiteSmoke', 'Yellow', 'YellowGreen' 49 | ] 50 | 51 | COLOR_MAP = { 52 | 'face':'Crimson', 'bicycle':'BlueViolet', 'bus':'Gold', 'car':'DodgerBlue', 53 | 'motorbike':'OrangeRed', 'person':'Chartreuse' 54 | } 55 | 56 | def getRandomColor(): 57 | """Generate random color 58 | 59 | Returns: 60 | -------- 61 | color : tuple(int, int, int) 62 | generated random color 63 | """ 64 | 65 | color = tuple([int(255*rnd.random()) for _ in range(3)]) 66 | return color 67 | 68 | 69 | def plotBBoxes(image, bboxes, classes=None, scores=None, color='Chartreuse', linewidth=2, use_rgb=False): 70 | """Plot bounding boxes for given input objects 71 | 72 | Arguments: 73 | ---------- 74 | image : numpy.array 75 | input image for drawing 76 | bboxes : list((x1, y1, x2, y2)) 77 | input bounding boxes of objects to draw 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | classes : list(str) (default: None) 82 | list of classes for objects 83 | color : str (default: `Chartreuse`) 84 | color to plot 85 | linewidth : int (default: 2) 86 | how thick the shape is 87 | use_rgb : bool (default: False) 88 | whether using RGB image or not (apply for NDArray image) 89 | 90 | Returns: 91 | -------- 92 | image : numpy.array 93 | output image after drawing 94 | """ 95 | 96 | if len(bboxes) == 0: 97 | return image 98 | exit() 99 | 100 | if isinstance(image, np.ndarray): 101 | if not use_rgb: 102 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 103 | image = Image.fromarray(image) 104 | 105 | W, H = image.size 106 | draw = ImageDraw.Draw(image) 107 | 108 | if classes is not None: 109 | font = ImageFont.truetype(font=cfg.FONT, 110 | size=np.floor(3e-2*min(H, W) + 0.5).astype('int32') ) 111 | 112 | for i, ((x1, y1, x2, y2), cls) in enumerate(zip(bboxes, classes)): 113 | # draw bounding box 114 | draw.rectangle([x1, y1, x2, y2], 115 | outline=ImageColor.getrgb(COLOR_MAP[cls]), 116 | width=linewidth) 117 | # draw label 118 | text = cls 119 | if scores is not None: 120 | text = '{} {:.2f}'.format(cls, scores[i]) 121 | 122 | label_size = draw.textsize(text, font) 123 | text_coor = np.array([x1+linewidth, max(0, y1 - label_size[1] - 1)]) 124 | rec_coor = np.array([x1, text_coor[1]]) 125 | 126 | draw.rectangle([tuple(rec_coor), 127 | tuple(rec_coor + label_size + np.array([linewidth*2, 0])) ], 128 | fill=ImageColor.getrgb(COLOR_MAP[cls])) 129 | draw.text(text_coor, text, fill=ImageColor.getrgb('black'), font=font) 130 | else: 131 | for i, (x1, y1, x2, y2) in enumerate(bboxes): 132 | draw.rectangle([x1, y1, x2, y2], 133 | outline=ImageColor.getrgb(color), 134 | width=linewidth) 135 | 136 | del draw 137 | return image 138 | 139 | 140 | def plotInfo(image, info, color='Chartreuse', use_rgb=False): 141 | if isinstance(image, np.ndarray): 142 | if not use_rgb: 143 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 144 | image = Image.fromarray(image) 145 | 146 | W, H = image.size 147 | draw = ImageDraw.Draw(image) 148 | 149 | font = ImageFont.truetype(font=cfg.FONT, 150 | size=np.floor(5e-2*min(H, W) + 0.5).astype('int32')) 151 | 152 | label_size = draw.textsize(info, font) 153 | text_coor = np.array([5, 0]) 154 | rec_coor = np.array([0, 0]) 155 | draw.rectangle([tuple(rec_coor), 156 | tuple(rec_coor + label_size + np.array([10, 0])) ], 157 | fill=ImageColor.getrgb(color)) 158 | draw.text(text_coor, info, fill=ImageColor.getrgb('black'), font=font) 159 | 160 | del draw 161 | return image 162 | -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/app-landmark-detector.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import cv2 as cv 4 | 5 | import altusi.config as cfg 6 | import altusi.visualizer as vis 7 | from altusi import imgproc, helper 8 | from altusi.logger import Logger 9 | 10 | from altusi.facedetector import FaceDetector 11 | from altusi.facelandmarker import FaceLandmarker 12 | 13 | LOG = Logger('app-landmark-detector') 14 | 15 | 16 | def app(video_link, video_name, show, flip_hor, flip_ver): 17 | # initialize Face Detection net 18 | face_detector = FaceDetector() 19 | LOG.info('Face Detector initialization done') 20 | 21 | # initialize Face Landmark net 22 | face_landmarker = FaceLandmarker() 23 | LOG.info('Face Landmarker initialization done') 24 | 25 | # initialize Video Capturer 26 | cap = cv.VideoCapture(video_link) 27 | (W, H), FPS = imgproc.cameraCalibrate(cap, size=720, by_height=True) 28 | LOG.info('Camera Info: ({}, {}) - {:.3f}'.format(W, H, FPS)) 29 | 30 | while cap.isOpened(): 31 | _, frm = cap.read() 32 | if not _: 33 | LOG.info('Reached the end of Video source') 34 | break 35 | 36 | if flip_ver: frm = cv.flip(frm, 0) 37 | if flip_hor: frm = cv.flip(frm, 1) 38 | 39 | frm = imgproc.resizeByHeight(frm, 720) 40 | 41 | _start_t = time.time() 42 | scores, bboxes = face_detector.getFaces(frm, def_score=0.5) 43 | 44 | landmarks = [] 45 | for i, bbox in enumerate(bboxes): 46 | x1, y1, x2, y2 = bbox 47 | face_img = frm[y1:y2, x1:x2] 48 | landmark = face_landmarker.getLandmark(face_img) 49 | landmark[:, :] += np.array([x1, y1]) 50 | landmarks.append(landmark) 51 | _prx_t = time.time() - _start_t 52 | 53 | if len(bboxes): 54 | for i, landmark in enumerate(landmarks): 55 | for j, point in enumerate(landmark): 56 | cv.circle(frm, tuple(point), 3, (0, 255, 0), -1) 57 | 58 | frm = vis.plotBBoxes(frm, bboxes, len(bboxes) * ['face'], scores) 59 | 60 | frm = vis.plotInfo(frm, 'Raspberry Pi - FPS: {:.3f}'.format(1/_prx_t)) 61 | frm = cv.cvtColor(np.asarray(frm), cv.COLOR_BGR2RGB) 62 | 63 | if show: 64 | cv.imshow(video_name, frm) 65 | key = cv.waitKey(1) 66 | if key in [27, ord('q')]: 67 | LOG.info('Interrupted by Users') 68 | break 69 | 70 | cap.release() 71 | cv.destroyAllWindows() 72 | 73 | 74 | def main(args): 75 | video_link = args.video if args.video else 0 76 | app(video_link, args.name, args.show, args.flip_hor, args.flip_ver) 77 | 78 | 79 | if __name__ == '__main__': 80 | LOG.info('Raspberry Pi: Face Detection\n') 81 | 82 | args = helper.getArgs() 83 | main(args) 84 | 85 | LOG.info('Process done') 86 | -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/demo.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import cv2 as cv 4 | 5 | import altusi.config as cfg 6 | import altusi.visualizer as vis 7 | from altusi import imgproc, helper 8 | from altusi.logger import Logger 9 | 10 | from altusi.facedetector import FaceDetector 11 | from altusi.facelandmarker import FaceLandmarker 12 | 13 | LOG = Logger('app-landmark-detector') 14 | 15 | 16 | def processFrame(frm, face_detector, face_landmarker): 17 | _start_t = time.time() 18 | scores, bboxes = face_detector.getFaces(frm, def_score=0.5) 19 | 20 | landmarks = [] 21 | for i, bbox in enumerate(bboxes): 22 | x1, y1, x2, y2 = bbox 23 | face_img = frm[y1:y2, x1:x2] 24 | landmark = face_landmarker.getLandmark(face_img) 25 | landmark[:, :] += np.array([x1, y1]) 26 | landmarks.append(landmark) 27 | _prx_t = time.time() - _start_t 28 | 29 | if len(bboxes): 30 | for i, landmark in enumerate(landmarks): 31 | for j, point in enumerate(landmark): 32 | cv.circle(frm, tuple(point), 3, (0, 255, 0), -1) 33 | 34 | frm = vis.plotBBoxes(frm, bboxes, len(bboxes) * ['face'], scores) 35 | 36 | frm = vis.plotInfo(frm, 'Raspberry Pi - FPS: {:.3f}'.format(1/_prx_t)) 37 | frm = cv.cvtColor(np.asarray(frm), cv.COLOR_BGR2RGB) 38 | 39 | return frm 40 | 41 | 42 | def app(video_link, video_name, show, flip_hor, flip_ver): 43 | video_links = [ 44 | '/home/pi/Videos/crowd-6582.mp4', 45 | '/home/pi/Videos/india-444.mp4', 46 | '/home/pi/Videos/paris-2174.mp4', 47 | '/home/pi/Videos/scotland-21847.mp4', 48 | ] 49 | # initialize Face Detection net 50 | face_detector = FaceDetector() 51 | LOG.info('Face Detector initialization done') 52 | 53 | # initialize Face Landmark net 54 | face_landmarker = FaceLandmarker() 55 | LOG.info('Face Landmarker initialization done') 56 | 57 | # initialize Video Capturer 58 | cap0 = cv.VideoCapture(video_links[0]) 59 | cap1 = cv.VideoCapture(video_links[1]) 60 | cap2 = cv.VideoCapture(video_links[2]) 61 | cap3 = cv.VideoCapture(video_links[3]) 62 | # (W, H), FPS = imgproc.cameraCalibrate(cap, size=720, by_height=True) 63 | # LOG.info('Camera Info: ({}, {}) - {:.3f}'.format(W, H, FPS)) 64 | 65 | time_str = time.strftime(cfg.TIME_FM) 66 | saved_path = 'output.avi' 67 | writer = cv.VideoWriter(saved_path, cv.VideoWriter_fourcc(*'XVID'), 24, (1280, 720) ) 68 | 69 | cnt_frm = 0 70 | while cap0.isOpened() and cap1.isOpened() and cap2.isOpened() and cap3.isOpened(): 71 | _0, frm0 = cap0.read() 72 | _1, frm1 = cap1.read() 73 | _2, frm2 = cap2.read() 74 | _3, frm3 = cap3.read() 75 | 76 | if not _0 or not _1 or not _2 or not _3: 77 | LOG.info('Reached the end of Video source') 78 | break 79 | 80 | cnt_frm += 1 81 | frm0 = imgproc.resizeByHeight(frm0, 360) 82 | frm1 = imgproc.resizeByHeight(frm1, 360) 83 | frm2 = imgproc.resizeByHeight(frm2, 360) 84 | frm3 = imgproc.resizeByHeight(frm3, 360) 85 | 86 | frm0 = processFrame(frm0, face_detector, face_landmarker) 87 | frm1 = processFrame(frm1, face_detector, face_landmarker) 88 | frm2 = processFrame(frm2, face_detector, face_landmarker) 89 | frm3 = processFrame(frm3, face_detector, face_landmarker) 90 | 91 | frm = np.zeros((720, 1280, 3)) 92 | frm[:360, :640] = frm0 93 | frm[:360, 640:] = frm1 94 | frm[360:, :640] = frm2 95 | frm[360:, 640:] = frm3 96 | LOG.info('frm shape: {}'.format(frm.shape)) 97 | 98 | cv.imwrite(str(cnt_frm) + '.jpg', frm) 99 | writer.write(frm) 100 | LOG.info('Frames processed: {}'.format(cnt_frm)) 101 | 102 | if show: 103 | cv.imshow('output', frm) 104 | 105 | key = cv.waitKey(1) 106 | if key in [27, ord('q')]: 107 | LOG.info('Interrupted by Users') 108 | break 109 | 110 | writer.release() 111 | cap0.release() 112 | cap1.release() 113 | cap2.release() 114 | cap3.release() 115 | cv.destroyAllWindows() 116 | 117 | 118 | def main(args): 119 | video_link = args.video if args.video else 0 120 | app(video_link, args.name, args.show, args.flip_hor, args.flip_ver) 121 | 122 | 123 | if __name__ == '__main__': 124 | LOG.info('Raspberry Pi: Face Detection\n') 125 | 126 | args = helper.getArgs() 127 | main(args) 128 | 129 | LOG.info('Process done') 130 | -------------------------------------------------------------------------------- /002-pi-facial-landmark-detection/openvino-models: -------------------------------------------------------------------------------- 1 | /home/pi/workspace/openvino-models/ -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/.gitignore: -------------------------------------------------------------------------------- 1 | # AI model 2 | *.ai 3 | *.dat 4 | *.h5 5 | *.hdf5 6 | *.pkl 7 | *.pth 8 | *.params 9 | 10 | 11 | # vscode folder 12 | .vscode/ 13 | 14 | # pycache file 15 | __pycache__/ 16 | 17 | # data folder 18 | data/ 19 | 20 | # .ipynb checkpoints 21 | .ipynb_checkpoints/ 22 | 23 | # vim swap file 24 | *.sw[onp] 25 | 26 | # media files 27 | *.avi 28 | *.mp4 29 | *.jpg 30 | *.png 31 | -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/Aller_Bd.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/003-pi-face-alignment/altusi/Aller_Bd.ttf -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/REAME.md: -------------------------------------------------------------------------------- 1 | # AltusI 2 | My library for Computer Vision and Artificial Intelligence 3 | 4 | # Update 5 | **2019, Sep 25:** 6 | - Add `config` for project's configuration 7 | - Add `helper` for support functions 8 | - Add `visualizer` module for visualization purpose 9 | - Develop `plotBBoxes` (with `classes`) functions to plot bboxes and classes with different colors 10 | 11 | **2019, Sep 24:** 12 | - Add `logger` for logging purpose 13 | -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/003-pi-face-alignment/altusi/__init__.py -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # ============================================================================= 5 | # PROJECT'S ORGANIZATION 6 | # ============================================================================= 7 | PROJECT_BASE = '.' 8 | 9 | #=============================================================================== 10 | # PROJECT'S PARAMETERS 11 | #=============================================================================== 12 | FONT = os.path.join(PROJECT_BASE, 'altusi', 'Aller_Bd.ttf') 13 | 14 | TIME_FM = '-%Y%m%d-%H%M%S' 15 | 16 | #=============================================================================== 17 | # PROJECT'S MODELS 18 | #=============================================================================== 19 | MODEL_DIR = 'openvino-models' 20 | 21 | # face detection model 22 | FACE_DET_XML = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.xml') 23 | FACE_DET_BIN = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.bin') 24 | 25 | # facial landmark model 26 | FACE_LM_XML = os.path.join(MODEL_DIR, 'landmarks-regression-retail-0009-fp16.xml') 27 | FACE_LM_BIN = os.path.join(MODEL_DIR, 'landmarks-regression-retail-0009-fp16.bin') 28 | 29 | # face reidentification model 30 | FACE_EMB_XML = os.path.join(MODEL_DIR, 'face-reidentification-retail-0095-fp16.xml') 31 | FACE_EMB_BIN = os.path.join(MODEL_DIR, 'face-reidentification-retail-0095-fp16.bin') 32 | -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/facealigner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Aligner class 3 | ================== 4 | 5 | Align face to well form 6 | 7 | This work is inherited and converted from the official OpenVINO toolkit 8 | Original source code can be found here: 9 | /path/to/inference_engine_vpu_arm/deployment_tools/inference_engine/samples/smart_classroom_demo 10 | """ 11 | 12 | 13 | import numpy as np 14 | import cv2 as cv 15 | 16 | _H, _W = 112., 96. 17 | ref_lm_norm = [ 18 | 30.2946 / _W, 51.6963 / _H, 65.5318 / _W, 51.5014 / _H, 48.0252 / _W, 19 | 71.7366 / _H, 33.5493 / _W, 92.3655 / _H, 62.7299 / _W, 92.2041 / _H] 20 | 21 | def getTransform(src, dst): 22 | """get transforming matrix""" 23 | src = np.array(src, np.float) 24 | col_mean_src = np.mean(src, axis=0) 25 | src -= col_mean_src 26 | 27 | dst = np.array(dst, np.float) 28 | col_mean_dst = np.mean(dst, axis=0) 29 | dst -= col_mean_dst 30 | 31 | mean, dev_src = cv.meanStdDev(src) 32 | dev_src = max(dev_src[0], 1.192e-7) 33 | src /= dev_src[0] 34 | 35 | mean, dev_dst = cv.meanStdDev(dst) 36 | dev_dst = max(dev_dst[0], 1.192e-7) 37 | dst /= dev_dst[0] 38 | 39 | w, u, vt = cv.SVDecomp(np.matmul(src.T, dst)) 40 | r = np.matmul(u, vt) 41 | 42 | m = np.zeros((2, 3)) 43 | m[:, 0:2] = r * (dev_dst[0] / dev_src[0]) 44 | m[:, 2] = col_mean_dst.T - np.matmul(m[:, 0:2], col_mean_src.T) 45 | 46 | return m 47 | 48 | 49 | class FaceAligner: 50 | """Face Aligner class""" 51 | 52 | @staticmethod 53 | def align(face_image, landmark): 54 | """align an input image with given landmark points""" 55 | H, W = face_image.shape[:2] 56 | ref_lm = np.zeros((5, 2)) 57 | for i in range(5): 58 | ref_lm[i, 0] = ref_lm_norm[2*i] * W 59 | ref_lm[i, 1] = ref_lm_norm[2*i+1] * H 60 | 61 | m = getTransform(ref_lm, landmark) 62 | aligned_face = cv.warpAffine(face_image, m, (W, H), cv.WARP_INVERSE_MAP) 63 | 64 | return aligned_face 65 | -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/facedetector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Detector class 3 | =================== 4 | 5 | Module for Face Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceDetector: 16 | """Face Detector class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_DET_XML, 20 | bin_path=cfg.FACE_DET_BIN): 21 | """Initialize Face detector object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | 24 | # with NCS support 25 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 26 | 27 | 28 | def getFaces(self, image, def_score=0.5): 29 | """Detect faces in an input image with given threshold""" 30 | H, W = image.shape[:2] 31 | blob = cv.dnn.blobFromImage(image, size=(672, 384), ddepth=cv.CV_8U) 32 | self.__net.setInput(blob) 33 | out = self.__net.forward() 34 | 35 | bboxes = [] 36 | scores = [] 37 | for det in out.reshape(-1, 7): 38 | score = float(det[2]) 39 | if score < def_score: continue 40 | 41 | x1 = max(0, int(det[3] * W)) 42 | y1 = max(0, int(det[4] * H)) 43 | x2 = min(W, int(det[5] * W)) 44 | y2 = min(H, int(det[6] * H)) 45 | 46 | bboxes.append((x1, y1, x2, y2)) 47 | scores.append(score) 48 | 49 | return scores, bboxes 50 | -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/facelandmarker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Facial Landmarker class 3 | ======================= 4 | 5 | Module for Facial Landmark Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceLandmarker: 16 | """Face Landmarker class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_LM_XML, 20 | bin_path=cfg.FACE_LM_BIN): 21 | """Initialize Face Landmarker object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 24 | 25 | 26 | def getLandmark(self, image): 27 | """Locate Facial landmark points in a face image""" 28 | H, W = image.shape[:2] 29 | 30 | blob = cv.dnn.blobFromImage(image, size=(48, 48), ddepth=cv.CV_8U) 31 | self.__net.setInput(blob) 32 | 33 | reg = self.__net.forward().reshape(-1, 10) 34 | points = np.zeros((5, 2), np.int) 35 | for i in range(0, 10, 2): 36 | points[i//2] = (reg[0][i:i+2] * np.array([W, H])).astype(np.int) 37 | 38 | return points 39 | 40 | -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module 3 | ============ 4 | 5 | Support functions for file utilities 6 | """ 7 | 8 | import argparse 9 | import os 10 | 11 | 12 | def getFilename(file_path): 13 | path, filename = os.path.split(file_path) 14 | 15 | return path, filename 16 | 17 | 18 | def getFileNameExt(file_path): 19 | path, filename = getFilename(file_path) 20 | 21 | filename, ext = os.path.splitext(filename) 22 | 23 | return path, filename, ext 24 | 25 | def getArgs(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('--image', '-i', type=str, 28 | required=False, 29 | help='Path to image file') 30 | parser.add_argument('--video', '-v', type=str, 31 | required=False, 32 | help='Video Streamming link or Path to video source') 33 | parser.add_argument('--name', '-n', type=str, 34 | required=False, default='camera', 35 | help='Name of video source') 36 | parser.add_argument('--show', '-s', 37 | default=False, action='store_true', 38 | help='Whether to show the output visualization') 39 | parser.add_argument('--flip_hor', '-fh', 40 | required=False, default=False, action='store_true', 41 | help='horizontally flip video frame') 42 | parser.add_argument('--flip_ver', '-fv', 43 | required=False, default=False, action='store_true', 44 | help='vertically flip video frame') 45 | args = parser.parse_args() 46 | 47 | return args 48 | -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/imgproc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Imgproc library 3 | =============== 4 | 5 | Library to support Image processing functions 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Oct 03: 12 | - re-add to AltusI version 0.2 13 | """ 14 | 15 | import os 16 | import math 17 | import numpy as np 18 | import cv2 as cv 19 | 20 | 21 | def resizeByHeight(image, height=720): 22 | """Resize an image given the expected height and keep the original ratio 23 | 24 | Arguments: 25 | ---------- 26 | image : numpy.array 27 | input image to resize 28 | 29 | Keyword Arguments: 30 | ------------------ 31 | height : int (default: 720) 32 | expected width of output image 33 | 34 | Returns: 35 | -------- 36 | out_image : numpy.array 37 | output resized image 38 | """ 39 | H, W = image.shape[:2] 40 | width = int(1. * W * height / H + 0.5) 41 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 42 | 43 | return out_image 44 | 45 | 46 | def resizeByWidth(image, width=600): 47 | """Resize an image given the expected width and keep the original ratio 48 | 49 | Arguments: 50 | ---------- 51 | image : numpy.array 52 | input image to resize 53 | 54 | Keyword Arguments: 55 | ------------------ 56 | width : int (default: 600) 57 | expected width of output image 58 | 59 | Returns: 60 | -------- 61 | out_image : numpy.array 62 | output colored image after resized 63 | """ 64 | 65 | H, W = image.shape[:2] 66 | height = int(H * width / W) 67 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 68 | return out_image 69 | 70 | 71 | def cameraCalibrate(capturer, size=None, by_height=False): 72 | """Get camera's information like dimension and FPS 73 | 74 | Arguments: 75 | ---------- 76 | capturer : cv.VideoCapture 77 | OpenCV-Video capturer object 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | width : int (default: None) 82 | width value to resize by width 83 | 84 | Returns: 85 | -------- 86 | (W, H) : int, int 87 | dimension of video's frame 88 | FPS : float 89 | FPS of the video stream 90 | """ 91 | 92 | fps = capturer.get(cv.CAP_PROP_FPS) 93 | 94 | while True: 95 | _, frame = capturer.read() 96 | if _: 97 | if size: 98 | if by_height: 99 | frame = resizeByHeight(frame, size) 100 | else: 101 | frame = resizeByWidth(frame, size) 102 | H, W = frame.shape[:2] 103 | 104 | return (W, H), fps 105 | 106 | -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logger class 3 | ============ 4 | 5 | Wrapper of built-in `logging` Python module 6 | that supports logging task to both console and files 7 | """ 8 | 9 | """ 10 | Revision 11 | -------- 12 | 2019, Sep 24: first version 13 | """ 14 | 15 | 16 | import os 17 | import logging 18 | 19 | # format for logging message 20 | LOG_FILE_FORMAT = '%(asctime)s %(name)12s [%(levelname)s] %(message)s' 21 | LOG_CONSOLE_FORMAT = '%(asctime)s [%(levelname)s] %(message)s' 22 | 23 | class Logger(): 24 | """Logger class for logging task""" 25 | 26 | def __init__(self, name, console=True): 27 | """Initialize logger 28 | 29 | Parameters 30 | ---------- 31 | name : str 32 | name of logger and file to log 33 | console : bool 34 | whether logging messages are emitted to console or not 35 | """ 36 | self.logger = logging.getLogger(name) 37 | 38 | self.__configLogger(name, console) 39 | 40 | 41 | def __configLogger(self, name, console): 42 | """Configure logger object 43 | 44 | Parameters 45 | ---------- 46 | name : str 47 | name of logging file 48 | console : bool 49 | whether logging messages are emitted to console or not 50 | """ 51 | self.logger.setLevel(logging.DEBUG) 52 | file_formatter = logging.Formatter(LOG_FILE_FORMAT) 53 | console_formatter = logging.Formatter(LOG_CONSOLE_FORMAT) 54 | 55 | # if `console` is set, setup Handler to process 56 | if console: 57 | console_log = logging.StreamHandler() 58 | console_log.setLevel(logging.DEBUG) 59 | console_log.setFormatter(console_formatter) 60 | 61 | file_log = logging.FileHandler(name + '.log', 62 | mode='w') 63 | file_log.setLevel(logging.DEBUG) 64 | file_log.setFormatter(file_formatter) 65 | 66 | if console: 67 | self.logger.addHandler(console_log) 68 | self.logger.addHandler(file_log) 69 | 70 | 71 | def critical(self, msg): 72 | """Emit CRITICAL message""" 73 | self.logger.critical(msg) 74 | 75 | 76 | def error(self, msg): 77 | """Emit ERROR message""" 78 | self.logger.error(msg) 79 | 80 | 81 | def warning(self, msg): 82 | """Emit WARNING message""" 83 | self.logger.warning(msg) 84 | 85 | 86 | def info(self, msg): 87 | """Emit INFO message""" 88 | self.logger.info(msg) 89 | 90 | 91 | def debug(self, msg): 92 | """Emit DEBUG message""" 93 | self.logger.debug(msg) 94 | -------------------------------------------------------------------------------- /003-pi-face-alignment/altusi/visualizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Visualization module 3 | ==================== 4 | 5 | Process Image visualization, PIL package is in use 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Sep 25: first version 12 | - add plotBBoxes (with class ids) 13 | """ 14 | 15 | 16 | import random as rnd 17 | 18 | import numpy as np 19 | import cv2 as cv 20 | import PIL 21 | from PIL import Image, ImageFont, ImageDraw, ImageColor 22 | 23 | import altusi.config as cfg 24 | 25 | STANDARD_COLORS = [ 26 | 'AliceBlue', 'Chartreuse', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 27 | 'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue', 'AntiqueWhite', 28 | 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 29 | 'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange', 30 | 'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet', 31 | 'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite', 32 | 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', 33 | 'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki', 34 | 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 35 | 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', 36 | 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 37 | 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', 38 | 'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid', 39 | 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 40 | 'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin', 41 | 'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 42 | 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 43 | 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 44 | 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Green', 'SandyBrown', 45 | 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 46 | 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow', 47 | 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 48 | 'WhiteSmoke', 'Yellow', 'YellowGreen' 49 | ] 50 | 51 | COLOR_MAP = { 52 | 'face':'Crimson', 'bicycle':'BlueViolet', 'bus':'Gold', 'car':'DodgerBlue', 53 | 'motorbike':'OrangeRed', 'person':'Chartreuse' 54 | } 55 | 56 | def getRandomColor(): 57 | """Generate random color 58 | 59 | Returns: 60 | -------- 61 | color : tuple(int, int, int) 62 | generated random color 63 | """ 64 | 65 | color = tuple([int(255*rnd.random()) for _ in range(3)]) 66 | return color 67 | 68 | 69 | def plotBBoxes(image, bboxes, classes=None, scores=None, color='Chartreuse', linewidth=2, use_rgb=False): 70 | """Plot bounding boxes for given input objects 71 | 72 | Arguments: 73 | ---------- 74 | image : numpy.array 75 | input image for drawing 76 | bboxes : list((x1, y1, x2, y2)) 77 | input bounding boxes of objects to draw 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | classes : list(str) (default: None) 82 | list of classes for objects 83 | color : str (default: `Chartreuse`) 84 | color to plot 85 | linewidth : int (default: 2) 86 | how thick the shape is 87 | use_rgb : bool (default: False) 88 | whether using RGB image or not (apply for NDArray image) 89 | 90 | Returns: 91 | -------- 92 | image : numpy.array 93 | output image after drawing 94 | """ 95 | 96 | if len(bboxes) == 0: 97 | return image 98 | exit() 99 | 100 | if isinstance(image, np.ndarray): 101 | if not use_rgb: 102 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 103 | image = Image.fromarray(image) 104 | 105 | W, H = image.size 106 | draw = ImageDraw.Draw(image) 107 | 108 | if classes is not None: 109 | font = ImageFont.truetype(font=cfg.FONT, 110 | size=np.floor(3e-2*min(H, W) + 0.5).astype('int32') ) 111 | 112 | for i, ((x1, y1, x2, y2), cls) in enumerate(zip(bboxes, classes)): 113 | # draw bounding box 114 | draw.rectangle([x1, y1, x2, y2], 115 | outline=ImageColor.getrgb(COLOR_MAP[cls]), 116 | width=linewidth) 117 | # draw label 118 | text = cls 119 | if scores is not None: 120 | text = '{} {:.2f}'.format(cls, scores[i]) 121 | 122 | label_size = draw.textsize(text, font) 123 | text_coor = np.array([x1+linewidth, max(0, y1 - label_size[1] - 1)]) 124 | rec_coor = np.array([x1, text_coor[1]]) 125 | 126 | draw.rectangle([tuple(rec_coor), 127 | tuple(rec_coor + label_size + np.array([linewidth*2, 0])) ], 128 | fill=ImageColor.getrgb(COLOR_MAP[cls])) 129 | draw.text(text_coor, text, fill=ImageColor.getrgb('black'), font=font) 130 | else: 131 | for i, (x1, y1, x2, y2) in enumerate(bboxes): 132 | draw.rectangle([x1, y1, x2, y2], 133 | outline=ImageColor.getrgb(color), 134 | width=linewidth) 135 | 136 | del draw 137 | return image 138 | 139 | 140 | def plotInfo(image, info, color='Chartreuse', use_rgb=False): 141 | if isinstance(image, np.ndarray): 142 | if not use_rgb: 143 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 144 | image = Image.fromarray(image) 145 | 146 | W, H = image.size 147 | draw = ImageDraw.Draw(image) 148 | 149 | font = ImageFont.truetype(font=cfg.FONT, 150 | size=np.floor(5e-2*min(H, W) + 0.5).astype('int32')) 151 | 152 | label_size = draw.textsize(info, font) 153 | text_coor = np.array([5, 0]) 154 | rec_coor = np.array([0, 0]) 155 | draw.rectangle([tuple(rec_coor), 156 | tuple(rec_coor + label_size + np.array([10, 0])) ], 157 | fill=ImageColor.getrgb(color)) 158 | draw.text(text_coor, info, fill=ImageColor.getrgb('black'), font=font) 159 | 160 | del draw 161 | return image 162 | -------------------------------------------------------------------------------- /003-pi-face-alignment/app-face-alignment.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import cv2 as cv 4 | 5 | import altusi.config as cfg 6 | import altusi.visualizer as vis 7 | from altusi import imgproc, helper 8 | from altusi.logger import Logger 9 | 10 | from altusi.facedetector import FaceDetector 11 | from altusi.facelandmarker import FaceLandmarker 12 | from altusi.facealigner import FaceAligner 13 | 14 | LOG = Logger('app-face-alignment') 15 | 16 | 17 | def app(image_path): 18 | # initialize Face Detection net 19 | face_detector = FaceDetector() 20 | LOG.info('Face Detector initialization done') 21 | 22 | # initialize Face Landmark net 23 | face_landmarker = FaceLandmarker() 24 | LOG.info('Face Landmarker initialization done') 25 | 26 | # initialize Face Alignment class 27 | face_aligner = FaceAligner() 28 | LOG.info('Face Aligner initialization done') 29 | 30 | # initialize Video Capturer 31 | image = cv.imread(image_path) 32 | assert image is not None 33 | 34 | image = imgproc.resizeByHeight(image, 720) 35 | 36 | _start_t = time.time() 37 | scores, bboxes = face_detector.getFaces(image, def_score=0.5) 38 | 39 | landmarks = [] 40 | for i, bbox in enumerate(bboxes): 41 | x1, y1, x2, y2 = bbox 42 | face_img = image[y1:y2, x1:x2] 43 | landmark = face_landmarker.getLandmark(face_img) 44 | 45 | aligned_img = face_aligner.align(face_img, landmark) 46 | cv.imshow('aligned-faces' + str(i), aligned_img) 47 | 48 | landmark[:, :] += np.array([x1, y1]) 49 | landmarks.append(landmark) 50 | _prx_t = time.time() - _start_t 51 | 52 | if len(bboxes): 53 | for i, landmark in enumerate(landmarks): 54 | for j, point in enumerate(landmark): 55 | cv.circle(image, tuple(point), 3, (0, 255, 0), -1) 56 | 57 | image = vis.plotBBoxes(image, bboxes, len(bboxes) * ['face'], scores) 58 | 59 | image = vis.plotInfo(image, 'Raspberry Pi - FPS: {:.3f}'.format(1/_prx_t)) 60 | image = cv.cvtColor(np.asarray(image), cv.COLOR_BGR2RGB) 61 | 62 | cv.imshow(image_path, image) 63 | key = cv.waitKey(0) 64 | 65 | cv.destroyAllWindows() 66 | 67 | 68 | def main(args): 69 | app(args.image) 70 | 71 | 72 | if __name__ == '__main__': 73 | LOG.info('Raspberry Pi: Face Alignment\n') 74 | 75 | args = helper.getArgs() 76 | main(args) 77 | 78 | LOG.info('Process done') 79 | -------------------------------------------------------------------------------- /003-pi-face-alignment/exp-image-openface.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import numpy as np 5 | import cv2 as cv 6 | 7 | from altusi.logger import Logger 8 | from altusi import helper, imgproc 9 | import altusi.visualizer as vis 10 | 11 | from altusi.facedetector import FaceDetector 12 | from altusi.facelandmarker import FaceLandmarker 13 | 14 | 15 | LOG = Logger('test-func') 16 | 17 | TEMPLATE = np.float32([ 18 | (0.0792396913815, 0.339223741112), (0.0829219487236, 0.456955367943), 19 | (0.0967927109165, 0.575648016728), (0.122141515615, 0.691921601066), 20 | (0.168687863544, 0.800341263616), (0.239789390707, 0.895732504778), 21 | (0.325662452515, 0.977068762493), (0.422318282013, 1.04329000149), 22 | (0.531777802068, 1.06080371126), (0.641296298053, 1.03981924107), 23 | (0.738105872266, 0.972268833998), (0.824444363295, 0.889624082279), 24 | (0.894792677532, 0.792494155836), (0.939395486253, 0.681546643421), 25 | (0.96111933829, 0.562238253072), (0.970579841181, 0.441758925744), 26 | (0.971193274221, 0.322118743967), (0.163846223133, 0.249151738053), 27 | (0.21780354657, 0.204255863861), (0.291299351124, 0.192367318323), 28 | (0.367460241458, 0.203582210627), (0.4392945113, 0.233135599851), 29 | (0.586445962425, 0.228141644834), (0.660152671635, 0.195923841854), 30 | (0.737466449096, 0.182360984545), (0.813236546239, 0.192828009114), 31 | (0.8707571886, 0.235293377042), (0.51534533827, 0.31863546193), 32 | (0.516221448289, 0.396200446263), (0.517118861835, 0.473797687758), 33 | (0.51816430343, 0.553157797772), (0.433701156035, 0.604054457668), 34 | (0.475501237769, 0.62076344024), (0.520712933176, 0.634268222208), 35 | (0.565874114041, 0.618796581487), (0.607054002672, 0.60157671656), 36 | (0.252418718401, 0.331052263829), (0.298663015648, 0.302646354002), 37 | (0.355749724218, 0.303020650651), (0.403718978315, 0.33867711083), 38 | (0.352507175597, 0.349987615384), (0.296791759886, 0.350478978225), 39 | (0.631326076346, 0.334136672344), (0.679073381078, 0.29645404267), 40 | (0.73597236153, 0.294721285802), (0.782865376271, 0.321305281656), 41 | (0.740312274764, 0.341849376713), (0.68499850091, 0.343734332172), 42 | (0.353167761422, 0.746189164237), (0.414587777921, 0.719053835073), 43 | (0.477677654595, 0.706835892494), (0.522732900812, 0.717092275768), 44 | (0.569832064287, 0.705414478982), (0.635195811927, 0.71565572516), 45 | (0.69951672331, 0.739419187253), (0.639447159575, 0.805236879972), 46 | (0.576410514055, 0.835436670169), (0.525398405766, 0.841706377792), 47 | (0.47641545769, 0.837505914975), (0.41379548902, 0.810045601727), 48 | (0.380084785646, 0.749979603086), (0.477955996282, 0.74513234612), 49 | (0.523389793327, 0.748924302636), (0.571057789237, 0.74332894691), 50 | (0.672409137852, 0.744177032192), (0.572539621444, 0.776609286626), 51 | (0.5240106503, 0.783370783245), (0.477561227414, 0.778476346951)]) 52 | 53 | 54 | EYE_CENTER_LEFT = np.mean(TEMPLATE[np.array([36, 37, 38, 39, 40, 41])], axis=0) 55 | EYE_CENTER_RIGHT = np.mean(TEMPLATE[np.array([42, 43, 44, 45, 46, 47])], axis=0) 56 | 57 | TEMPLATE = np.vstack([TEMPLATE, EYE_CENTER_LEFT, EYE_CENTER_RIGHT]) 58 | 59 | TPL_MIN, TPL_MAX = np.min(TEMPLATE, axis=0), np.max(TEMPLATE, axis=0) 60 | MINMAX_TEMPLATE = (TEMPLATE - TPL_MIN) / (TPL_MAX - TPL_MIN) 61 | CENTER_EYES_AND_NOSE = np.array([-2, -1, 33]) 62 | 63 | 64 | def align(image, bbox, landmark): 65 | x1, y1, x2, y2 = bbox 66 | W, H = x2 - x1, y2 - y1 67 | template = MINMAX_TEMPLATE[CENTER_EYES_AND_NOSE] 68 | template[:, 0] *= 160 69 | template[:, 1] *= 160 70 | h = cv.getAffineTransform(np.float32(landmark[:3]), template) 71 | thumbnail = cv.warpAffine(image, h, (160, 160)) 72 | return thumbnail 73 | 74 | 75 | def app(image_path): 76 | image = cv.imread(image_path) 77 | assert image is not None 78 | 79 | face_detector = FaceDetector() 80 | face_landmarker = FaceLandmarker() 81 | 82 | scores, bboxes = face_detector.getFaces(image, def_score=0.5) 83 | landmarks = [] 84 | for i, bbox in enumerate(bboxes): 85 | x1, y1, x2, y2 = bbox 86 | face_img = image[y1:y2, x1:x2] 87 | landmark = face_landmarker.getLandmark(face_img) 88 | landmark[:, :] += np.array([x1, y1]) 89 | landmarks.append(landmark) 90 | 91 | for i, landmark in enumerate(landmarks): 92 | aligned_img = align(image, bboxes[i], landmark) 93 | cv.imshow(str(i), aligned_img) 94 | 95 | if len(bboxes): 96 | for i, landmark in enumerate(landmarks): 97 | for j, point in enumerate(landmark): 98 | cv.circle(image, tuple(point), 3, (0, 255, 0), -1) 99 | 100 | image = vis.plotBBoxes(image, bboxes) 101 | image = cv.cvtColor(np.asarray(image), cv.COLOR_BGR2RGB) 102 | 103 | cv.imshow(image_path, image) 104 | cv.waitKey(0) 105 | 106 | 107 | def main(args): 108 | image_path = args.image 109 | app(image_path) 110 | 111 | 112 | if __name__ == '__main__': 113 | LOG.info('Raspberry Pi: Face Alignment on Image\n') 114 | 115 | args = helper.getArgs() 116 | main(args) 117 | 118 | LOG.info('Process done') 119 | -------------------------------------------------------------------------------- /003-pi-face-alignment/exp-image-openvino.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import cv2 as cv 4 | 5 | import altusi.config as cfg 6 | import altusi.visualizer as vis 7 | from altusi import imgproc, helper 8 | from altusi.logger import Logger 9 | 10 | from altusi.facedetector import FaceDetector 11 | from altusi.facelandmarker import FaceLandmarker 12 | 13 | LOG = Logger('app-face-alignment') 14 | 15 | h, w = 112., 96. 16 | ref_lm_norm = [ 17 | 30.2946 / w, 51.6963 / h, 65.5318 / w, 51.5014 / h, 48.0252 / w, 18 | 71.7366 / h, 33.5493 / w, 92.3655 / h, 62.7299 / w, 92.2041 / h] 19 | 20 | def getTransform(src, dst): 21 | src = np.array(src, np.float) 22 | col_mean_src = np.mean(src, axis=0) 23 | src -= col_mean_src 24 | 25 | dst = np.array(dst, np.float) 26 | col_mean_dst = np.mean(dst, axis=0) 27 | dst -= col_mean_dst 28 | 29 | mean, dev_src = cv.meanStdDev(src) 30 | dev_src = max(dev_src[0], 1.192e-7) 31 | src /= dev_src[0] 32 | 33 | mean, dev_dst = cv.meanStdDev(dst) 34 | dev_dst = max(dev_dst[0], 1.192e-7) 35 | dst /= dev_dst[0] 36 | 37 | w, u, vt = cv.SVDecomp(np.matmul(src.T, dst)) 38 | r = np.matmul(u, vt) 39 | 40 | m = np.zeros((2, 3)) 41 | m[:, 0:2] = r * (dev_dst[0] / dev_src[0]) 42 | m[:, 2] = col_mean_dst.T - np.matmul(m[:, 0:2], col_mean_src.T) 43 | 44 | return m 45 | 46 | def alignFace(face_image, landmark): 47 | H, W = face_image.shape[:2] 48 | ref_lm = np.zeros((5, 2)) 49 | for i in range(5): 50 | ref_lm[i, 0] = ref_lm_norm[2*i] * W 51 | ref_lm[i, 1] = ref_lm_norm[2*i+1] * H 52 | 53 | m = getTransform(ref_lm, landmark) 54 | img = cv.warpAffine(face_image, m, (W, H), cv.WARP_INVERSE_MAP) 55 | 56 | return img 57 | 58 | 59 | def app(image_path): 60 | # initialize Face Detection net 61 | face_detector = FaceDetector() 62 | LOG.info('Face Detector initialization done') 63 | 64 | # initialize Face Landmark net 65 | face_landmarker = FaceLandmarker() 66 | LOG.info('Face Landmarker initialization done') 67 | 68 | # initialize Video Capturer 69 | image = cv.imread(image_path) 70 | assert image is not None 71 | 72 | image = imgproc.resizeByHeight(image, 720) 73 | 74 | _start_t = time.time() 75 | scores, bboxes = face_detector.getFaces(image, def_score=0.5) 76 | 77 | landmarks = [] 78 | for i, bbox in enumerate(bboxes): 79 | x1, y1, x2, y2 = bbox 80 | face_img = image[y1:y2, x1:x2] 81 | landmark = face_landmarker.getLandmark(face_img) 82 | 83 | aligned_img = alignFace(face_img, landmark) 84 | cv.imshow('aligned-faces' + str(i), aligned_img) 85 | 86 | landmark[:, :] += np.array([x1, y1]) 87 | landmarks.append(landmark) 88 | _prx_t = time.time() - _start_t 89 | 90 | if len(bboxes): 91 | for i, landmark in enumerate(landmarks): 92 | for j, point in enumerate(landmark): 93 | cv.circle(image, tuple(point), 3, (0, 255, 0), -1) 94 | 95 | image = vis.plotBBoxes(image, bboxes, len(bboxes) * ['face'], scores) 96 | 97 | image = vis.plotInfo(image, 'Raspberry Pi - FPS: {:.3f}'.format(1/_prx_t)) 98 | image = cv.cvtColor(np.asarray(image), cv.COLOR_BGR2RGB) 99 | 100 | cv.imshow(image_path, image) 101 | key = cv.waitKey(0) 102 | 103 | cv.destroyAllWindows() 104 | 105 | 106 | def main(args): 107 | app(args.image) 108 | 109 | 110 | if __name__ == '__main__': 111 | LOG.info('Raspberry Pi: Face Detection\n') 112 | 113 | args = helper.getArgs() 114 | main(args) 115 | 116 | LOG.info('Process done') 117 | -------------------------------------------------------------------------------- /003-pi-face-alignment/exp-video-openvino.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import cv2 as cv 4 | 5 | import altusi.config as cfg 6 | import altusi.visualizer as vis 7 | from altusi import imgproc, helper 8 | from altusi.logger import Logger 9 | 10 | from altusi.facedetector import FaceDetector 11 | from altusi.facelandmarker import FaceLandmarker 12 | 13 | LOG = Logger('app-face-alignment') 14 | 15 | h, w = 112., 96. 16 | ref_lm_norm = [ 17 | 30.2946 / w, 51.6963 / h, 65.5318 / w, 51.5014 / h, 48.0252 / w, 18 | 71.7366 / h, 33.5493 / w, 92.3655 / h, 62.7299 / w, 92.2041 / h] 19 | 20 | def getTransform(src, dst): 21 | src = np.array(src, np.float) 22 | col_mean_src = np.mean(src, axis=0) 23 | src -= col_mean_src 24 | 25 | dst = np.array(dst, np.float) 26 | col_mean_dst = np.mean(dst, axis=0) 27 | dst -= col_mean_dst 28 | 29 | mean, dev_src = cv.meanStdDev(src) 30 | dev_src = max(dev_src[0], 1.192e-7) 31 | src /= dev_src[0] 32 | 33 | mean, dev_dst = cv.meanStdDev(dst) 34 | dev_dst = max(dev_dst[0], 1.192e-7) 35 | dst /= dev_dst[0] 36 | 37 | w, u, vt = cv.SVDecomp(np.matmul(src.T, dst)) 38 | r = np.matmul(u, vt) 39 | 40 | m = np.zeros((2, 3)) 41 | m[:, 0:2] = r * (dev_dst[0] / dev_src[0]) 42 | m[:, 2] = col_mean_dst.T - np.matmul(m[:, 0:2], col_mean_src.T) 43 | 44 | return m 45 | 46 | def alignFace(face_image, landmark): 47 | H, W = face_image.shape[:2] 48 | ref_lm = np.zeros((5, 2)) 49 | for i in range(5): 50 | ref_lm[i, 0] = ref_lm_norm[2*i] * W 51 | ref_lm[i, 1] = ref_lm_norm[2*i+1] * H 52 | 53 | m = getTransform(ref_lm, landmark) 54 | img = cv.warpAffine(face_image, m, (W, H), cv.WARP_INVERSE_MAP) 55 | 56 | return img 57 | 58 | 59 | def app(image_path): 60 | # initialize Face Detection net 61 | face_detector = FaceDetector() 62 | LOG.info('Face Detector initialization done') 63 | 64 | # initialize Face Landmark net 65 | face_landmarker = FaceLandmarker() 66 | LOG.info('Face Landmarker initialization done') 67 | 68 | cap = cv.VideoCapture(0) 69 | while cap.isOpened(): 70 | _, image = cap.read() 71 | if not _: 72 | LOG.info('Reached the end of Video source') 73 | break 74 | 75 | image = imgproc.resizeByHeight(image, 720) 76 | 77 | _start_t = time.time() 78 | scores, bboxes = face_detector.getFaces(image, def_score=0.5) 79 | 80 | landmarks = [] 81 | for i, bbox in enumerate(bboxes): 82 | x1, y1, x2, y2 = bbox 83 | face_img = image[y1:y2, x1:x2] 84 | landmark = face_landmarker.getLandmark(face_img) 85 | 86 | aligned_img = alignFace(face_img, landmark) 87 | cv.imshow('aligned-faces' + str(i), aligned_img) 88 | 89 | landmark[:, :] += np.array([x1, y1]) 90 | landmarks.append(landmark) 91 | _prx_t = time.time() - _start_t 92 | 93 | if len(bboxes): 94 | for i, landmark in enumerate(landmarks): 95 | for j, point in enumerate(landmark): 96 | cv.circle(image, tuple(point), 3, (0, 255, 0), -1) 97 | 98 | image = vis.plotBBoxes(image, bboxes, len(bboxes) * ['face'], scores) 99 | 100 | image = vis.plotInfo(image, 'Raspberry Pi - FPS: {:.3f}'.format(1/_prx_t)) 101 | image = cv.cvtColor(np.asarray(image), cv.COLOR_BGR2RGB) 102 | 103 | cv.imshow(image_path, image) 104 | key = cv.waitKey(1) 105 | if key in [27, ord('q')]: 106 | LOG.info('Interrupted by Users') 107 | break 108 | 109 | cap.release() 110 | cv.destroyAllWindows() 111 | 112 | 113 | def main(args): 114 | app(args.image) 115 | 116 | 117 | if __name__ == '__main__': 118 | LOG.info('Raspberry Pi: Face Detection\n') 119 | 120 | args = helper.getArgs() 121 | main(args) 122 | 123 | LOG.info('Process done') 124 | -------------------------------------------------------------------------------- /003-pi-face-alignment/openvino-models: -------------------------------------------------------------------------------- 1 | /home/pi/workspace/openvino-models/ -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/altusi/Aller_Bd.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/004-pi-head-pose-estimation/altusi/Aller_Bd.ttf -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/altusi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/004-pi-head-pose-estimation/altusi/__init__.py -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/altusi/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # ============================================================================= 5 | # PROJECT'S ORGANIZATION 6 | # ============================================================================= 7 | PROJECT_BASE = '.' 8 | 9 | #=============================================================================== 10 | # PROJECT'S PARAMETERS 11 | #=============================================================================== 12 | FONT = os.path.join(PROJECT_BASE, 'altusi', 'Aller_Bd.ttf') 13 | 14 | TIME_FM = '-%Y%m%d-%H%M%S' 15 | 16 | pFPS = 5 17 | 18 | #=============================================================================== 19 | # PROJECT'S MODELS 20 | #=============================================================================== 21 | MODEL_DIR = 'openvino-models' 22 | 23 | # face detection model 24 | FACE_DET_XML = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.xml') 25 | FACE_DET_BIN = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.bin') 26 | 27 | # facial landmark model 28 | FACE_LM_XML = os.path.join(MODEL_DIR, 'landmarks-regression-retail-0009-fp16.xml') 29 | FACE_LM_BIN = os.path.join(MODEL_DIR, 'landmarks-regression-retail-0009-fp16.bin') 30 | 31 | # face reidentification model 32 | FACE_EMB_XML = os.path.join(MODEL_DIR, 'face-reidentification-retail-0095-fp16.xml') 33 | FACE_EMB_BIN = os.path.join(MODEL_DIR, 'face-reidentification-retail-0095-fp16.bin') 34 | 35 | # head pose estimation model 36 | HEAD_POSE_XML = os.path.join(MODEL_DIR, 'head-pose-estimation-adas-0001-fp16.xml') 37 | HEAD_POSE_BIN = os.path.join(MODEL_DIR, 'head-pose-estimation-adas-0001-fp16.bin') 38 | 39 | -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/altusi/facedetector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Detector class 3 | =================== 4 | 5 | Module for Face Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | from altusi.inference import Network 15 | 16 | 17 | class FaceDetector: 18 | """Face Detector class""" 19 | 20 | def __init__(self, 21 | xml_path=cfg.FACE_DET_XML, 22 | device='MYRIAD', 23 | inp_size=1, out_size=1, 24 | num_requests=2, plugin=None): 25 | """Initialize Face detector object""" 26 | self.__net = Network() 27 | self.plugin, (self.B, self.C, self.H, self.W) = self.__net.load_model( 28 | xml_path, device, inp_size, out_size, num_requests, plugin=plugin) 29 | 30 | 31 | def getFaces(self, image, def_score=0.5): 32 | """Detect faces in an input image with given threshold""" 33 | H, W = image.shape[:2] 34 | 35 | frm = cv.resize(image, (self.W, self.H)) 36 | frm = frm.transpose((2, 0, 1)) 37 | frm = frm.reshape((self.B, self.C, self.H, self.W)) 38 | 39 | self.__net.exec_net(0, frm) 40 | self.__net.wait(0) 41 | 42 | res = self.__net.get_output(0) 43 | 44 | scores, bboxes = [], [] 45 | for obj in res[0][0]: 46 | if obj[2] > def_score: 47 | score = obj[2] 48 | xmin = max(0, int(obj[3] * W)) 49 | ymin = max(0, int(obj[4] * H)) 50 | xmax = min(W, int(obj[5] * W)) 51 | ymax = min(H, int(obj[6] * H)) 52 | 53 | scores.append(score) 54 | bboxes.append([xmin, ymin, xmax, ymax]) 55 | 56 | return scores, bboxes 57 | 58 | 59 | def getPlugin(self): 60 | return self.plugin 61 | -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/altusi/facelandmarker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Facial Landmarker class 3 | ======================= 4 | 5 | Module for Facial Landmark Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceLandmarker: 16 | """Face Landmarker class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_LM_XML, 20 | bin_path=cfg.FACE_LM_BIN): 21 | """Initialize Face Landmarker object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 24 | 25 | 26 | def getLandmark(self, image): 27 | """Locate Facial landmark points in a face image""" 28 | H, W = image.shape[:2] 29 | 30 | blob = cv.dnn.blobFromImage(image, size=(48, 48), ddepth=cv.CV_8U) 31 | self.__net.setInput(blob) 32 | 33 | reg = self.__net.forward().reshape(-1, 10) 34 | points = np.zeros((5, 2), np.int) 35 | for i in range(0, 10, 2): 36 | points[i//2] = (reg[0][i:i+2] * np.array([W, H])).astype(np.int) 37 | 38 | return points 39 | 40 | -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/altusi/headposer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 as cv 3 | 4 | import altusi.config as cfg 5 | from altusi.inference import Network 6 | 7 | class HeadPoser: 8 | def __init__(self, 9 | xml_path=cfg.HEAD_POSE_XML, 10 | device='MYRIAD', 11 | inp_size=1, out_size=3, 12 | num_requests=2, plugin=None): 13 | self.__net = Network() 14 | self.plugin, (self.B, self.C, self.H, self.W) = self.__net.load_model( 15 | xml_path, device, inp_size, out_size, num_requests, plugin=plugin) 16 | 17 | 18 | def estimatePose(self, face_image): 19 | image = cv.resize(face_image, (self.W, self.H)) 20 | image = image.transpose((2, 0, 1)) 21 | image = image.reshape((self.B, self.C, self.H, self.W)) 22 | 23 | self.__net.exec_net(0, image) 24 | self.__net.wait(0) 25 | 26 | angle_y = self.__net.get_output(0, 'angle_y_fc')[0, 0] 27 | angle_p = self.__net.get_output(0, 'angle_p_fc')[0, 0] 28 | angle_r = self.__net.get_output(0, 'angle_r_fc')[0, 0] 29 | 30 | return angle_y, angle_p, angle_r 31 | 32 | def getPlugin(self): 33 | return self.plugin 34 | -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/altusi/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module 3 | ============ 4 | 5 | Support functions for file utilities 6 | """ 7 | 8 | import argparse 9 | import os 10 | 11 | 12 | def getFilename(file_path): 13 | path, filename = os.path.split(file_path) 14 | 15 | return path, filename 16 | 17 | 18 | def getFileNameExt(file_path): 19 | path, filename = getFilename(file_path) 20 | 21 | filename, ext = os.path.splitext(filename) 22 | 23 | return path, filename, ext 24 | 25 | def getArgs(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('--video', '-v', type=str, 28 | required=False, 29 | help='Video Streamming link or Path to video source') 30 | parser.add_argument('--name', '-n', type=str, 31 | required=False, default='camera', 32 | help='Name of video source') 33 | parser.add_argument('--show', '-s', 34 | default=False, action='store_true', 35 | help='Whether to show the output visualization') 36 | parser.add_argument('--record', '-r', 37 | default=False, action='store_true', 38 | help='Whether to save the output visualization') 39 | parser.add_argument('--flip_hor', '-fh', 40 | required=False, default=False, action='store_true', 41 | help='horizontally flip video frame') 42 | parser.add_argument('--flip_ver', '-fv', 43 | required=False, default=False, action='store_true', 44 | help='vertically flip video frame') 45 | args = parser.parse_args() 46 | 47 | return args 48 | -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/altusi/imgproc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Imgproc library 3 | =============== 4 | 5 | Library to support Image processing functions 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Oct 03: 12 | - re-add to AltusI version 0.2 13 | """ 14 | 15 | import os 16 | import math 17 | import numpy as np 18 | import cv2 as cv 19 | 20 | 21 | def resizeByHeight(image, height=720): 22 | """Resize an image given the expected height and keep the original ratio 23 | 24 | Arguments: 25 | ---------- 26 | image : numpy.array 27 | input image to resize 28 | 29 | Keyword Arguments: 30 | ------------------ 31 | height : int (default: 720) 32 | expected width of output image 33 | 34 | Returns: 35 | -------- 36 | out_image : numpy.array 37 | output resized image 38 | """ 39 | H, W = image.shape[:2] 40 | width = int(1. * W * height / H + 0.5) 41 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 42 | 43 | return out_image 44 | 45 | 46 | def resizeByWidth(image, width=600): 47 | """Resize an image given the expected width and keep the original ratio 48 | 49 | Arguments: 50 | ---------- 51 | image : numpy.array 52 | input image to resize 53 | 54 | Keyword Arguments: 55 | ------------------ 56 | width : int (default: 600) 57 | expected width of output image 58 | 59 | Returns: 60 | -------- 61 | out_image : numpy.array 62 | output colored image after resized 63 | """ 64 | 65 | H, W = image.shape[:2] 66 | height = int(H * width / W) 67 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 68 | return out_image 69 | 70 | 71 | def cameraCalibrate(capturer, size=None, by_height=False): 72 | """Get camera's information like dimension and FPS 73 | 74 | Arguments: 75 | ---------- 76 | capturer : cv.VideoCapture 77 | OpenCV-Video capturer object 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | width : int (default: None) 82 | width value to resize by width 83 | 84 | Returns: 85 | -------- 86 | (W, H) : int, int 87 | dimension of video's frame 88 | FPS : float 89 | FPS of the video stream 90 | """ 91 | 92 | fps = capturer.get(cv.CAP_PROP_FPS) 93 | 94 | while True: 95 | _, frame = capturer.read() 96 | if _: 97 | if size: 98 | if by_height: 99 | frame = resizeByHeight(frame, size) 100 | else: 101 | frame = resizeByWidth(frame, size) 102 | H, W = frame.shape[:2] 103 | 104 | return (W, H), fps 105 | 106 | -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/altusi/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logger class 3 | ============ 4 | 5 | Wrapper of built-in `logging` Python module 6 | that supports logging task to both console and files 7 | """ 8 | 9 | """ 10 | Revision 11 | -------- 12 | 2019, Sep 24: first version 13 | """ 14 | 15 | 16 | import os 17 | import logging 18 | 19 | # format for logging message 20 | LOG_FILE_FORMAT = '%(asctime)s %(name)12s [%(levelname)s] %(message)s' 21 | LOG_CONSOLE_FORMAT = '%(asctime)s [%(levelname)s] %(message)s' 22 | 23 | class Logger(): 24 | """Logger class for logging task""" 25 | 26 | def __init__(self, name, console=True): 27 | """Initialize logger 28 | 29 | Parameters 30 | ---------- 31 | name : str 32 | name of logger and file to log 33 | console : bool 34 | whether logging messages are emitted to console or not 35 | """ 36 | self.logger = logging.getLogger(name) 37 | 38 | self.__configLogger(name, console) 39 | 40 | 41 | def __configLogger(self, name, console): 42 | """Configure logger object 43 | 44 | Parameters 45 | ---------- 46 | name : str 47 | name of logging file 48 | console : bool 49 | whether logging messages are emitted to console or not 50 | """ 51 | self.logger.setLevel(logging.DEBUG) 52 | file_formatter = logging.Formatter(LOG_FILE_FORMAT) 53 | console_formatter = logging.Formatter(LOG_CONSOLE_FORMAT) 54 | 55 | # if `console` is set, setup Handler to process 56 | if console: 57 | console_log = logging.StreamHandler() 58 | console_log.setLevel(logging.DEBUG) 59 | console_log.setFormatter(console_formatter) 60 | 61 | file_log = logging.FileHandler(name + '.log', 62 | mode='w') 63 | file_log.setLevel(logging.DEBUG) 64 | file_log.setFormatter(file_formatter) 65 | 66 | if console: 67 | self.logger.addHandler(console_log) 68 | self.logger.addHandler(file_log) 69 | 70 | 71 | def critical(self, msg): 72 | """Emit CRITICAL message""" 73 | self.logger.critical(msg) 74 | 75 | 76 | def error(self, msg): 77 | """Emit ERROR message""" 78 | self.logger.error(msg) 79 | 80 | 81 | def warning(self, msg): 82 | """Emit WARNING message""" 83 | self.logger.warning(msg) 84 | 85 | 86 | def info(self, msg): 87 | """Emit INFO message""" 88 | self.logger.info(msg) 89 | 90 | 91 | def debug(self, msg): 92 | """Emit DEBUG message""" 93 | self.logger.debug(msg) 94 | -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/altusi/visualizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Visualization module 3 | ==================== 4 | 5 | Process Image visualization, PIL package is in use 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Sep 25: first version 12 | - add plotBBoxes (with class ids) 13 | """ 14 | 15 | 16 | import random as rnd 17 | 18 | import numpy as np 19 | import cv2 as cv 20 | import PIL 21 | from PIL import Image, ImageFont, ImageDraw, ImageColor 22 | 23 | import altusi.config as cfg 24 | 25 | STANDARD_COLORS = [ 26 | 'AliceBlue', 'Chartreuse', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 27 | 'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue', 'AntiqueWhite', 28 | 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 29 | 'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange', 30 | 'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet', 31 | 'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite', 32 | 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', 33 | 'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki', 34 | 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 35 | 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', 36 | 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 37 | 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', 38 | 'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid', 39 | 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 40 | 'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin', 41 | 'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 42 | 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 43 | 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 44 | 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Green', 'SandyBrown', 45 | 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 46 | 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow', 47 | 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 48 | 'WhiteSmoke', 'Yellow', 'YellowGreen' 49 | ] 50 | 51 | COLOR_MAP = { 52 | 'face':'Crimson', 'bicycle':'BlueViolet', 'bus':'Gold', 'car':'DodgerBlue', 53 | 'motorbike':'OrangeRed', 'person':'Chartreuse' 54 | } 55 | 56 | def getRandomColor(): 57 | """Generate random color 58 | 59 | Returns: 60 | -------- 61 | color : tuple(int, int, int) 62 | generated random color 63 | """ 64 | 65 | color = tuple([int(255*rnd.random()) for _ in range(3)]) 66 | return color 67 | 68 | 69 | def plotBBoxes(image, bboxes, classes=None, scores=None, color='Chartreuse', linewidth=2, use_rgb=False): 70 | """Plot bounding boxes for given input objects 71 | 72 | Arguments: 73 | ---------- 74 | image : numpy.array 75 | input image for drawing 76 | bboxes : list((x1, y1, x2, y2)) 77 | input bounding boxes of objects to draw 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | classes : list(str) (default: None) 82 | list of classes for objects 83 | color : str (default: `Chartreuse`) 84 | color to plot 85 | linewidth : int (default: 2) 86 | how thick the shape is 87 | use_rgb : bool (default: False) 88 | whether using RGB image or not (apply for NDArray image) 89 | 90 | Returns: 91 | -------- 92 | image : numpy.array 93 | output image after drawing 94 | """ 95 | 96 | if len(bboxes) == 0: 97 | return image 98 | exit() 99 | 100 | if isinstance(image, np.ndarray): 101 | if not use_rgb: 102 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 103 | image = Image.fromarray(image) 104 | 105 | W, H = image.size 106 | draw = ImageDraw.Draw(image) 107 | 108 | if classes is not None: 109 | font = ImageFont.truetype(font=cfg.FONT, 110 | size=np.floor(3e-2*min(H, W) + 0.5).astype('int32') ) 111 | 112 | for i, ((x1, y1, x2, y2), cls) in enumerate(zip(bboxes, classes)): 113 | # draw bounding box 114 | draw.rectangle([x1, y1, x2, y2], 115 | outline=ImageColor.getrgb(COLOR_MAP[cls]), 116 | width=linewidth) 117 | # draw label 118 | text = cls 119 | if scores is not None: 120 | text = '{} {:.2f}'.format(cls, scores[i]) 121 | 122 | label_size = draw.textsize(text, font) 123 | text_coor = np.array([x1+linewidth, max(0, y1 - label_size[1] - 1)]) 124 | rec_coor = np.array([x1, text_coor[1]]) 125 | 126 | draw.rectangle([tuple(rec_coor), 127 | tuple(rec_coor + label_size + np.array([linewidth*2, 0])) ], 128 | fill=ImageColor.getrgb(COLOR_MAP[cls])) 129 | draw.text(text_coor, text, fill=ImageColor.getrgb('black'), font=font) 130 | else: 131 | for i, (x1, y1, x2, y2) in enumerate(bboxes): 132 | draw.rectangle([x1, y1, x2, y2], 133 | outline=ImageColor.getrgb(color), 134 | width=linewidth) 135 | 136 | del draw 137 | return image 138 | 139 | 140 | def plotInfo(image, info, color='Chartreuse', use_rgb=False): 141 | if isinstance(image, np.ndarray): 142 | if not use_rgb: 143 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 144 | image = Image.fromarray(image) 145 | 146 | W, H = image.size 147 | draw = ImageDraw.Draw(image) 148 | 149 | font = ImageFont.truetype(font=cfg.FONT, 150 | size=np.floor(5e-2*min(H, W) + 0.5).astype('int32')) 151 | 152 | label_size = draw.textsize(info, font) 153 | text_coor = np.array([5, 0]) 154 | rec_coor = np.array([0, 0]) 155 | draw.rectangle([tuple(rec_coor), 156 | tuple(rec_coor + label_size + np.array([10, 0])) ], 157 | fill=ImageColor.getrgb(color)) 158 | draw.text(text_coor, info, fill=ImageColor.getrgb('black'), font=font) 159 | 160 | del draw 161 | return image 162 | -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/app-headpose-image.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import numpy as np 5 | import cv2 as cv 6 | 7 | import altusi.config as cfg 8 | from altusi import helper 9 | from altusi.logger import Logger 10 | import headvisualizer as hvis 11 | import altusi.visualizer as vis 12 | 13 | from altusi.facedetector import FaceDetector 14 | from altusi.headposer import HeadPoser 15 | 16 | 17 | LOG = Logger(__file__.split('.')[0]) 18 | 19 | 20 | def app(image_path): 21 | detector = FaceDetector() 22 | plugin = detector.getPlugin() 23 | poser = HeadPoser(plugin=plugin) 24 | image = cv.imread(image_path) 25 | 26 | scores, bboxes = detector.getFaces(image) 27 | 28 | if len(bboxes): 29 | for i, bbox in enumerate(bboxes): 30 | x1, y1, x2, y2 = bbox 31 | face_image = image[y1:y2, x1:x2] 32 | yaw, pitch, roll = poser.estimatePose(face_image) 33 | 34 | LOG.info('yaw: {:.4f} pitch: {:.4f} roll: {:.4f}'.format(yaw, pitch, roll)) 35 | 36 | cpoint = [(x1+x2)//2, (y1+y2)//2] 37 | image = hvis.draw(image, cpoint, (yaw, pitch, roll)) 38 | 39 | 40 | cv.imshow(image_path, image) 41 | cv.waitKey(0) 42 | 43 | 44 | def main(args): 45 | app(args.image) 46 | 47 | 48 | if __name__ == '__main__': 49 | LOG.info('Raspberry Pi: Head Pose\n') 50 | 51 | args = helper.getArgs() 52 | main(args) 53 | 54 | LOG.info('Process done') 55 | -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/app-headpose-vid.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import numpy as np 5 | import cv2 as cv 6 | 7 | import altusi.config as cfg 8 | from altusi import helper, imgproc 9 | from altusi.logger import Logger 10 | import headvisualizer as hvis 11 | import altusi.visualizer as vis 12 | 13 | from altusi.facedetector import FaceDetector 14 | from altusi.headposer import HeadPoser 15 | 16 | 17 | LOG = Logger(__file__.split('.')[0]) 18 | 19 | 20 | def app(video_path, video_name, show=False, record=False): 21 | detector = FaceDetector() 22 | plugin = detector.getPlugin() 23 | poser = HeadPoser(plugin=plugin) 24 | 25 | cap = cv.VideoCapture(video_path) 26 | (W, H), FPS = imgproc.cameraCalibrate(cap) 27 | _, frm = cap.read() 28 | frm = imgproc.resizeByHeight(frm) 29 | H, W = frm.shape[:2] 30 | FRM_MOD = int(1. * FPS / cfg.pFPS + 0.5) 31 | LOG.info('Camera Info: ({}, {}) - {:.3f}'.format(W, H, FPS)) 32 | 33 | if record: 34 | time_str = time.strftime(cfg.TIME_FM) 35 | writer = cv.VideoWriter(video_name+time_str+'.avi', 36 | cv.VideoWriter_fourcc(*'XVID'), FPS, (W, H)) 37 | 38 | cnt_frm = 0 39 | while cap.isOpened(): 40 | _, frm = cap.read() 41 | if not _: 42 | LOG.info('Reached the end of Video source') 43 | break 44 | 45 | cnt_frm += 1 46 | if cnt_frm % FRM_MOD: continue 47 | print(cnt_frm) 48 | frm = imgproc.resizeByHeight(frm) 49 | 50 | _start_t = time.time() 51 | scores, bboxes = detector.getFaces(frm) 52 | 53 | if len(bboxes): 54 | for i, bbox in enumerate(bboxes): 55 | x1, y1, x2, y2 = bbox 56 | face_image = frm[y1:y2, x1:x2] 57 | yaw, pitch, roll = poser.estimatePose(face_image) 58 | 59 | cpoint = [(x1+x2)//2, (y1+y2)//2] 60 | frm = hvis.draw(frm, cpoint, (yaw, pitch, roll)) 61 | _prx_t = time.time() - _start_t 62 | 63 | frm = vis.plotInfo(frm, 'Raspberry Pi - FPS: {:.3f}'.format(1/_prx_t)) 64 | frm = cv.cvtColor(np.asarray(frm), cv.COLOR_BGR2RGB) 65 | 66 | if record: 67 | writer.write(frm) 68 | 69 | if show: 70 | cv.imshow(video_name, frm) 71 | key = cv.waitKey(1) 72 | if key in [27, ord('q')]: 73 | LOG.info('Interrupted by Users') 74 | break 75 | 76 | if record: 77 | writer.release() 78 | cap.release() 79 | cv.destroyAllWindows() 80 | 81 | 82 | def main(args): 83 | video_link = args.video if args.video else 0 84 | app(video_link, args.name, args.show, args.record) 85 | 86 | 87 | if __name__ == '__main__': 88 | LOG.info('Raspberry Pi: Head Pose\n') 89 | 90 | args = helper.getArgs() 91 | main(args) 92 | 93 | LOG.info('Process done') 94 | -------------------------------------------------------------------------------- /004-pi-head-pose-estimation/headvisualizer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 as cv 3 | 4 | scale = 50 5 | 6 | def draw(image, cpoint, headpose): 7 | H, W = image.shape[:2] 8 | 9 | yaw, pitch, roll = headpose 10 | 11 | yaw *= np.pi / 180. 12 | pitch *= np.pi / 180. 13 | roll *= np.pi / 180. 14 | 15 | Rx = np.array([ 16 | [1, 0, 0], 17 | [0, np.cos(pitch), -np.sin(pitch)], 18 | [0, np.sin(pitch), np.cos(pitch)]]) 19 | 20 | Ry = np.array([ 21 | [np.cos(yaw), 0, -np.sin(yaw)], 22 | [0, 1, 0], 23 | [np.sin(yaw), 0, np.cos(yaw)]]) 24 | 25 | Rz = np.array([ 26 | [np.cos(roll), -np.sin(roll), 0], 27 | [np.sin(roll), np.cos(roll), 0], 28 | [0, 0, 1]]) 29 | 30 | r = np.matmul(Rx, np.matmul(Ry, Rz)) 31 | 32 | cameraMatrix = np.array([ 33 | [950., 0, W / 2], 34 | [0, 950., H / 2], 35 | [0, 0, 1]]) 36 | 37 | xAxis = np.array([1 * scale, 0, 0]).reshape((3, 1)) 38 | yAxis = np.array([0, -1 * scale, 0]).reshape((3, 1)) 39 | zAxis = np.array([0, 0, -1 * scale]).reshape((3, 1)) 40 | zAxis1 = np.array([0, 0, 1 * scale]).reshape((3, 1)) 41 | 42 | o = np.array([0, 0, 950.]).reshape((3, 1)) 43 | 44 | xAxis = np.matmul(r, xAxis) + o 45 | yAxis = np.matmul(r, yAxis) + o 46 | zAxis = np.matmul(r, zAxis) + o 47 | zAxis1 = np.matmul(r, zAxis1) + o 48 | 49 | p1, p2 = [0, 0], [0, 0] 50 | p2[0] = xAxis[0, 0] / xAxis[2, 0] * cameraMatrix[0, 0] + cpoint[0] 51 | p2[1] = xAxis[1, 0] / xAxis[2, 0] * cameraMatrix[1, 1] + cpoint[1] 52 | p2[0], p2[1] = int(p2[0]), int(p2[1]) 53 | cv.line(image, tuple(cpoint), tuple(p2), (0, 0, 255), 2) 54 | 55 | p2[0] = yAxis[0, 0] / yAxis[2, 0] * cameraMatrix[0, 0] + cpoint[0] 56 | p2[1] = yAxis[1, 0] / yAxis[2, 0] * cameraMatrix[1, 1] + cpoint[1] 57 | p2[0], p2[1] = int(p2[0]), int(p2[1]) 58 | cv.line(image, tuple(cpoint), tuple(p2), (0, 255, 0), 2) 59 | 60 | p1[0] = zAxis1[0, 0] / zAxis1[2, 0] * cameraMatrix[0, 0] + cpoint[0] 61 | p1[1] = zAxis1[1, 0] / zAxis1[2, 0] * cameraMatrix[1, 1] + cpoint[1] 62 | p1[0], p1[1] = int(p1[0]), int(p1[1]) 63 | 64 | p2[0] = zAxis[0, 0] / zAxis[2, 0] * cameraMatrix[0, 0] + cpoint[0] 65 | p2[1] = zAxis[1, 0] / zAxis[2, 0] * cameraMatrix[1, 1] + cpoint[1] 66 | p2[0], p2[1] = int(p2[0]), int(p2[1]) 67 | 68 | cv.line(image, tuple(p1), tuple(p2), (255, 0, 0), 2) 69 | cv.circle(image, tuple(p2), 3, (255, 0, 0), 2) 70 | 71 | return image 72 | -------------------------------------------------------------------------------- /005-pi-object-detection/altusi/Aller_Bd.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/005-pi-object-detection/altusi/Aller_Bd.ttf -------------------------------------------------------------------------------- /005-pi-object-detection/altusi/REAME.md: -------------------------------------------------------------------------------- 1 | # AltusI 2 | My library for Computer Vision and Artificial Intelligence 3 | 4 | # Update 5 | **2019, Sep 25:** 6 | - Add `config` for project's configuration 7 | - Add `helper` for support functions 8 | - Add `visualizer` module for visualization purpose 9 | - Develop `plotBBoxes` (with `classes`) functions to plot bboxes and classes with different colors 10 | 11 | **2019, Sep 24:** 12 | - Add `logger` for logging purpose 13 | -------------------------------------------------------------------------------- /005-pi-object-detection/altusi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/005-pi-object-detection/altusi/__init__.py -------------------------------------------------------------------------------- /005-pi-object-detection/altusi/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # ============================================================================= 5 | # PROJECT'S ORGANIZATION 6 | # ============================================================================= 7 | PROJECT_BASE = '.' 8 | 9 | #=============================================================================== 10 | # PROJECT'S PARAMETERS 11 | #=============================================================================== 12 | FONT = os.path.join(PROJECT_BASE, 'altusi', 'Aller_Bd.ttf') 13 | 14 | TIME_FM = '-%Y%m%d-%H%M%S' 15 | 16 | #=============================================================================== 17 | # PROJECT'S MODELS 18 | #=============================================================================== 19 | MODEL_DIR = 'openvino-models' 20 | FACE_DET_XML = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.xml') 21 | FACE_DET_BIN = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.bin') 22 | 23 | PERSON_DET_XML = os.path.join(MODEL_DIR, 'person-detection-retail-0002-fp16.xml') 24 | PERSON_DET_BIN = os.path.join(MODEL_DIR, 'person-detection-retail-0002-fp16.bin') 25 | 26 | PERSON_DET_XML = os.path.join(MODEL_DIR, 'person-detection-retail-0013-fp16.xml') 27 | PERSON_DET_BIN = os.path.join(MODEL_DIR, 'person-detection-retail-0013-fp16.bin') 28 | -------------------------------------------------------------------------------- /005-pi-object-detection/altusi/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module 3 | ============ 4 | 5 | Support functions for file utilities 6 | """ 7 | 8 | import argparse 9 | import os 10 | 11 | 12 | def getFilename(file_path): 13 | path, filename = os.path.split(file_path) 14 | 15 | return path, filename 16 | 17 | 18 | def getFileNameExt(file_path): 19 | path, filename = getFilename(file_path) 20 | 21 | filename, ext = os.path.splitext(filename) 22 | 23 | return path, filename, ext 24 | 25 | def getArgs(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('--video', '-v', type=str, 28 | required=False, 29 | help='Video Streamming link or Path to video source') 30 | parser.add_argument('--name', '-n', type=str, 31 | required=False, default='camera', 32 | help='Name of video source') 33 | parser.add_argument('--show', '-s', 34 | default=False, action='store_true', 35 | help='Whether to show the output visualization') 36 | parser.add_argument('--record', '-r', 37 | default=False, action='store_true', 38 | help='Whether to save the output visualization') 39 | parser.add_argument('--flip_hor', '-fh', 40 | required=False, default=False, action='store_true', 41 | help='horizontally flip video frame') 42 | parser.add_argument('--flip_ver', '-fv', 43 | required=False, default=False, action='store_true', 44 | help='vertically flip video frame') 45 | args = parser.parse_args() 46 | 47 | return args 48 | -------------------------------------------------------------------------------- /005-pi-object-detection/altusi/imgproc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Imgproc library 3 | =============== 4 | 5 | Library to support Image processing functions 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Oct 03: 12 | - re-add to AltusI version 0.2 13 | """ 14 | 15 | import os 16 | import math 17 | import numpy as np 18 | import cv2 as cv 19 | 20 | 21 | def resizeByHeight(image, height=720): 22 | """Resize an image given the expected height and keep the original ratio 23 | 24 | Arguments: 25 | ---------- 26 | image : numpy.array 27 | input image to resize 28 | 29 | Keyword Arguments: 30 | ------------------ 31 | height : int (default: 720) 32 | expected width of output image 33 | 34 | Returns: 35 | -------- 36 | out_image : numpy.array 37 | output resized image 38 | """ 39 | H, W = image.shape[:2] 40 | width = int(1. * W * height / H + 0.5) 41 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 42 | 43 | return out_image 44 | 45 | 46 | def resizeByWidth(image, width=600): 47 | """Resize an image given the expected width and keep the original ratio 48 | 49 | Arguments: 50 | ---------- 51 | image : numpy.array 52 | input image to resize 53 | 54 | Keyword Arguments: 55 | ------------------ 56 | width : int (default: 600) 57 | expected width of output image 58 | 59 | Returns: 60 | -------- 61 | out_image : numpy.array 62 | output colored image after resized 63 | """ 64 | 65 | H, W = image.shape[:2] 66 | height = int(H * width / W) 67 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 68 | return out_image 69 | 70 | 71 | def cameraCalibrate(capturer, size=None, by_height=False): 72 | """Get camera's information like dimension and FPS 73 | 74 | Arguments: 75 | ---------- 76 | capturer : cv.VideoCapture 77 | OpenCV-Video capturer object 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | width : int (default: None) 82 | width value to resize by width 83 | 84 | Returns: 85 | -------- 86 | (W, H) : int, int 87 | dimension of video's frame 88 | FPS : float 89 | FPS of the video stream 90 | """ 91 | 92 | fps = capturer.get(cv.CAP_PROP_FPS) 93 | 94 | while True: 95 | _, frame = capturer.read() 96 | if _: 97 | if size: 98 | if by_height: 99 | frame = resizeByHeight(frame, size) 100 | else: 101 | frame = resizeByWidth(frame, size) 102 | H, W = frame.shape[:2] 103 | 104 | return (W, H), fps 105 | 106 | -------------------------------------------------------------------------------- /005-pi-object-detection/altusi/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logger class 3 | ============ 4 | 5 | Wrapper of built-in `logging` Python module 6 | that supports logging task to both console and files 7 | """ 8 | 9 | """ 10 | Revision 11 | -------- 12 | 2019, Sep 24: first version 13 | """ 14 | 15 | 16 | import os 17 | import logging 18 | 19 | # format for logging message 20 | LOG_FILE_FORMAT = '%(asctime)s %(name)12s [%(levelname)s] %(message)s' 21 | LOG_CONSOLE_FORMAT = '%(asctime)s [%(levelname)s] %(message)s' 22 | 23 | class Logger(): 24 | """Logger class for logging task""" 25 | 26 | def __init__(self, name, console=True): 27 | """Initialize logger 28 | 29 | Parameters 30 | ---------- 31 | name : str 32 | name of logger and file to log 33 | console : bool 34 | whether logging messages are emitted to console or not 35 | """ 36 | self.logger = logging.getLogger(name) 37 | 38 | self.__configLogger(name, console) 39 | 40 | 41 | def __configLogger(self, name, console): 42 | """Configure logger object 43 | 44 | Parameters 45 | ---------- 46 | name : str 47 | name of logging file 48 | console : bool 49 | whether logging messages are emitted to console or not 50 | """ 51 | self.logger.setLevel(logging.DEBUG) 52 | file_formatter = logging.Formatter(LOG_FILE_FORMAT) 53 | console_formatter = logging.Formatter(LOG_CONSOLE_FORMAT) 54 | 55 | # if `console` is set, setup Handler to process 56 | if console: 57 | console_log = logging.StreamHandler() 58 | console_log.setLevel(logging.DEBUG) 59 | console_log.setFormatter(console_formatter) 60 | 61 | file_log = logging.FileHandler(name + '.log', 62 | mode='w') 63 | file_log.setLevel(logging.DEBUG) 64 | file_log.setFormatter(file_formatter) 65 | 66 | if console: 67 | self.logger.addHandler(console_log) 68 | self.logger.addHandler(file_log) 69 | 70 | 71 | def critical(self, msg): 72 | """Emit CRITICAL message""" 73 | self.logger.critical(msg) 74 | 75 | 76 | def error(self, msg): 77 | """Emit ERROR message""" 78 | self.logger.error(msg) 79 | 80 | 81 | def warning(self, msg): 82 | """Emit WARNING message""" 83 | self.logger.warning(msg) 84 | 85 | 86 | def info(self, msg): 87 | """Emit INFO message""" 88 | self.logger.info(msg) 89 | 90 | 91 | def debug(self, msg): 92 | """Emit DEBUG message""" 93 | self.logger.debug(msg) 94 | -------------------------------------------------------------------------------- /005-pi-object-detection/altusi/objectdetector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Object Detector class 3 | =================== 4 | 5 | Module for Object Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class ObjectDetector: 16 | """Object Detector class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.PERSON_DET_XML, 20 | bin_path=cfg.PERSON_DET_BIN): 21 | """Initialize Object detector object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | 24 | # with NCS support 25 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 26 | 27 | 28 | def getObjects(self, image, def_score=0.5): 29 | """Detect objects in an input image with given threshold""" 30 | H, W = image.shape[:2] 31 | blob = cv.dnn.blobFromImage(image, size=(544, 320), ddepth=cv.CV_8U) 32 | self.__net.setInput(blob) 33 | out = self.__net.forward() 34 | 35 | bboxes = [] 36 | scores = [] 37 | for det in out.reshape(-1, 7): 38 | score = float(det[2]) 39 | if score < def_score: continue 40 | 41 | x1 = max(0, int(det[3] * W)) 42 | y1 = max(0, int(det[4] * H)) 43 | x2 = min(W, int(det[5] * W)) 44 | y2 = min(H, int(det[6] * H)) 45 | 46 | bboxes.append((x1, y1, x2, y2)) 47 | scores.append(score) 48 | 49 | return scores, bboxes 50 | -------------------------------------------------------------------------------- /005-pi-object-detection/altusi/visualizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Visualization module 3 | ==================== 4 | 5 | Process Image visualization, PIL package is in use 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Sep 25: first version 12 | - add plotBBoxes (with class ids) 13 | """ 14 | 15 | 16 | import random as rnd 17 | 18 | import numpy as np 19 | import cv2 as cv 20 | import PIL 21 | from PIL import Image, ImageFont, ImageDraw, ImageColor 22 | 23 | import altusi.config as cfg 24 | 25 | STANDARD_COLORS = [ 26 | 'AliceBlue', 'Chartreuse', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 27 | 'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue', 'AntiqueWhite', 28 | 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 29 | 'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange', 30 | 'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet', 31 | 'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite', 32 | 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', 33 | 'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki', 34 | 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 35 | 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', 36 | 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 37 | 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', 38 | 'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid', 39 | 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 40 | 'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin', 41 | 'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 42 | 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 43 | 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 44 | 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Green', 'SandyBrown', 45 | 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 46 | 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow', 47 | 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 48 | 'WhiteSmoke', 'Yellow', 'YellowGreen' 49 | ] 50 | 51 | COLOR_MAP = { 52 | 'face':'Crimson', 'bicycle':'BlueViolet', 'bus':'Gold', 'car':'DodgerBlue', 53 | 'motorbike':'OrangeRed', 'person':'Chartreuse' 54 | } 55 | 56 | def getRandomColor(): 57 | """Generate random color 58 | 59 | Returns: 60 | -------- 61 | color : tuple(int, int, int) 62 | generated random color 63 | """ 64 | 65 | color = tuple([int(255*rnd.random()) for _ in range(3)]) 66 | return color 67 | 68 | 69 | def plotBBoxes(image, bboxes, classes=None, scores=None, color='Chartreuse', linewidth=2, use_rgb=False): 70 | """Plot bounding boxes for given input objects 71 | 72 | Arguments: 73 | ---------- 74 | image : numpy.array 75 | input image for drawing 76 | bboxes : list((x1, y1, x2, y2)) 77 | input bounding boxes of objects to draw 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | classes : list(str) (default: None) 82 | list of classes for objects 83 | color : str (default: `Chartreuse`) 84 | color to plot 85 | linewidth : int (default: 2) 86 | how thick the shape is 87 | use_rgb : bool (default: False) 88 | whether using RGB image or not (apply for NDArray image) 89 | 90 | Returns: 91 | -------- 92 | image : numpy.array 93 | output image after drawing 94 | """ 95 | 96 | if len(bboxes) == 0: 97 | return image 98 | exit() 99 | 100 | if isinstance(image, np.ndarray): 101 | if not use_rgb: 102 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 103 | image = Image.fromarray(image) 104 | 105 | W, H = image.size 106 | draw = ImageDraw.Draw(image) 107 | 108 | if classes is not None: 109 | font = ImageFont.truetype(font=cfg.FONT, 110 | size=np.floor(2e-2*min(H, W) + 0.5).astype('int32') ) 111 | 112 | for i, ((x1, y1, x2, y2), cls) in enumerate(zip(bboxes, classes)): 113 | # draw bounding box 114 | draw.rectangle([x1, y1, x2, y2], 115 | outline=ImageColor.getrgb(COLOR_MAP[cls]), 116 | width=linewidth) 117 | # draw label 118 | text = cls 119 | if scores is not None: 120 | text = '{} {:.2f}'.format(cls, scores[i]) 121 | 122 | label_size = draw.textsize(text, font) 123 | text_coor = np.array([x1+linewidth, max(0, y1 - label_size[1] - 1)]) 124 | rec_coor = np.array([x1, text_coor[1]]) 125 | 126 | draw.rectangle([tuple(rec_coor), 127 | tuple(rec_coor + label_size + np.array([linewidth*2, 0])) ], 128 | fill=ImageColor.getrgb(COLOR_MAP[cls])) 129 | draw.text(text_coor, text, fill=ImageColor.getrgb('black'), font=font) 130 | else: 131 | for i, (x1, y1, x2, y2) in enumerate(bboxes): 132 | draw.rectangle([x1, y1, x2, y2], 133 | outline=ImageColor.getrgb(color), 134 | width=linewidth) 135 | 136 | del draw 137 | return image 138 | 139 | 140 | def plotInfo(image, info, color='Chartreuse', use_rgb=False): 141 | if isinstance(image, np.ndarray): 142 | if not use_rgb: 143 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 144 | image = Image.fromarray(image) 145 | 146 | W, H = image.size 147 | draw = ImageDraw.Draw(image) 148 | 149 | font = ImageFont.truetype(font=cfg.FONT, 150 | size=np.floor(3e-2*min(H, W) + 0.5).astype('int32') ) 151 | 152 | label_size = draw.textsize(info, font) 153 | text_coor = np.array([5, 0]) 154 | rec_coor = np.array([0, 0]) 155 | draw.rectangle([tuple(rec_coor), 156 | tuple(rec_coor + label_size + np.array([10, 0])) ], 157 | fill=ImageColor.getrgb(color)) 158 | draw.text(text_coor, info, fill=ImageColor.getrgb('black'), font=font) 159 | 160 | del draw 161 | return image 162 | -------------------------------------------------------------------------------- /005-pi-object-detection/app-object-detector.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import cv2 as cv 4 | 5 | import altusi.config as cfg 6 | import altusi.visualizer as vis 7 | from altusi import imgproc, helper 8 | from altusi.logger import Logger 9 | 10 | from altusi.objectdetector import ObjectDetector 11 | 12 | LOG = Logger('app-face-detector') 13 | 14 | def app(video_link, video_name, show, record, flip_hor, flip_ver): 15 | # initialize Face Detection net 16 | object_detector = ObjectDetector() 17 | 18 | # initialize Video Capturer 19 | cap = cv.VideoCapture(video_link) 20 | (W, H), FPS = imgproc.cameraCalibrate(cap) 21 | LOG.info('Camera Info: ({}, {}) - {:.3f}'.format(W, H, FPS)) 22 | 23 | if record: 24 | time_str = time.strftime(cfg.TIME_FM) 25 | writer = cv.VideoWriter(video_name+time_str+'.avi', 26 | cv.VideoWriter_fourcc(*'XVID'), 20, (1280, 720)) 27 | 28 | cnt_frm = 0 29 | while cap.isOpened(): 30 | _, frm = cap.read() 31 | if not _: 32 | LOG.info('Reached the end of Video source') 33 | break 34 | cnt_frm += 1 35 | 36 | if flip_ver: frm = cv.flip(frm, 0) 37 | if flip_hor: frm = cv.flip(frm, 1) 38 | frm = imgproc.resizeByHeight(frm, 720) 39 | 40 | 41 | _start_t = time.time() 42 | scores, bboxes = object_detector.getObjects(frm, def_score=0.5) 43 | _prx_t = time.time() - _start_t 44 | 45 | 46 | if len(bboxes): 47 | frm = vis.plotBBoxes(frm, bboxes, len(bboxes) * ['person'], scores) 48 | frm = vis.plotInfo(frm, 'Raspberry Pi - FPS: {:.3f}'.format(1/_prx_t)) 49 | frm = cv.cvtColor(np.asarray(frm), cv.COLOR_BGR2RGB) 50 | 51 | if record: 52 | writer.write(frm) 53 | 54 | if show: 55 | cv.imshow(video_name, frm) 56 | key = cv.waitKey(1) 57 | if key in [27, ord('q')]: 58 | LOG.info('Interrupted by Users') 59 | break 60 | 61 | if record: 62 | writer.release() 63 | cap.release() 64 | cv.destroyAllWindows() 65 | 66 | 67 | def main(args): 68 | video_link = args.video if args.video else 0 69 | app(video_link, args.name, args.show, args.record, args.flip_hor, args.flip_ver) 70 | 71 | 72 | if __name__ == '__main__': 73 | LOG.info('Raspberry Pi: Object Detection') 74 | 75 | args = helper.getArgs() 76 | main(args) 77 | 78 | LOG.info('Process done') 79 | -------------------------------------------------------------------------------- /005-pi-object-detection/openvino-models: -------------------------------------------------------------------------------- 1 | /home/pi/workspace/openvino-models/ -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/Aller_Bd.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/006-pi-face-verification/altusi/Aller_Bd.ttf -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/REAME.md: -------------------------------------------------------------------------------- 1 | # AltusI 2 | My library for Computer Vision and Artificial Intelligence 3 | 4 | # Update 5 | **2019, Sep 25:** 6 | - Add `config` for project's configuration 7 | - Add `helper` for support functions 8 | - Add `visualizer` module for visualization purpose 9 | - Develop `plotBBoxes` (with `classes`) functions to plot bboxes and classes with different colors 10 | 11 | **2019, Sep 24:** 12 | - Add `logger` for logging purpose 13 | -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/006-pi-face-verification/altusi/__init__.py -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # ============================================================================= 5 | # PROJECT'S ORGANIZATION 6 | # ============================================================================= 7 | PROJECT_BASE = '.' 8 | 9 | #=============================================================================== 10 | # PROJECT'S PARAMETERS 11 | #=============================================================================== 12 | FONT = os.path.join(PROJECT_BASE, 'altusi', 'Aller_Bd.ttf') 13 | 14 | TIME_FM = '-%Y%m%d-%H%M%S' 15 | 16 | #=============================================================================== 17 | # PROJECT'S MODELS 18 | #=============================================================================== 19 | MODEL_DIR = 'openvino-models' 20 | 21 | # face detection model 22 | FACE_DET_XML = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.xml') 23 | FACE_DET_BIN = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.bin') 24 | 25 | # facial landmark model 26 | FACE_LM_XML = os.path.join(MODEL_DIR, 'landmarks-regression-retail-0009-fp16.xml') 27 | FACE_LM_BIN = os.path.join(MODEL_DIR, 'landmarks-regression-retail-0009-fp16.bin') 28 | 29 | # face reidentification model 30 | FACE_EMB_XML = os.path.join(MODEL_DIR, 'face-reidentification-retail-0095-fp16.xml') 31 | FACE_EMB_BIN = os.path.join(MODEL_DIR, 'face-reidentification-retail-0095-fp16.bin') 32 | -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/facealigner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Aligner class 3 | ================== 4 | 5 | Align face to well form 6 | 7 | This work is inherited and converted from the official OpenVINO toolkit 8 | Original source code can be found here: 9 | /path/to/inference_engine_vpu_arm/deployment_tools/inference_engine/samples/smart_classroom_demo 10 | """ 11 | 12 | 13 | import numpy as np 14 | import cv2 as cv 15 | 16 | _H, _W = 112., 96. 17 | ref_lm_norm = [ 18 | 30.2946 / _W, 51.6963 / _H, 65.5318 / _W, 51.5014 / _H, 48.0252 / _W, 19 | 71.7366 / _H, 33.5493 / _W, 92.3655 / _H, 62.7299 / _W, 92.2041 / _H] 20 | 21 | def getTransform(src, dst): 22 | """get transforming matrix""" 23 | src = np.array(src, np.float) 24 | col_mean_src = np.mean(src, axis=0) 25 | src -= col_mean_src 26 | 27 | dst = np.array(dst, np.float) 28 | col_mean_dst = np.mean(dst, axis=0) 29 | dst -= col_mean_dst 30 | 31 | mean, dev_src = cv.meanStdDev(src) 32 | dev_src = max(dev_src[0], 1.192e-7) 33 | src /= dev_src[0] 34 | 35 | mean, dev_dst = cv.meanStdDev(dst) 36 | dev_dst = max(dev_dst[0], 1.192e-7) 37 | dst /= dev_dst[0] 38 | 39 | w, u, vt = cv.SVDecomp(np.matmul(src.T, dst)) 40 | r = np.matmul(u, vt) 41 | 42 | m = np.zeros((2, 3)) 43 | m[:, 0:2] = r * (dev_dst[0] / dev_src[0]) 44 | m[:, 2] = col_mean_dst.T - np.matmul(m[:, 0:2], col_mean_src.T) 45 | 46 | return m 47 | 48 | 49 | class FaceAligner: 50 | """Face Aligner class""" 51 | 52 | @staticmethod 53 | def align(face_image, landmark): 54 | """align an input image with given landmark points""" 55 | H, W = face_image.shape[:2] 56 | ref_lm = np.zeros((5, 2)) 57 | for i in range(5): 58 | ref_lm[i, 0] = ref_lm_norm[2*i] * W 59 | ref_lm[i, 1] = ref_lm_norm[2*i+1] * H 60 | 61 | m = getTransform(ref_lm, landmark) 62 | aligned_face = cv.warpAffine(face_image, m, (W, H), cv.WARP_INVERSE_MAP) 63 | 64 | return aligned_face 65 | -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/facedetector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Detector class 3 | =================== 4 | 5 | Module for Face Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceDetector: 16 | """Face Detector class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_DET_XML, 20 | bin_path=cfg.FACE_DET_BIN): 21 | """Initialize Face detector object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | 24 | # with NCS support 25 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 26 | 27 | 28 | def getFaces(self, image, def_score=0.5): 29 | """Detect faces in an input image with given threshold""" 30 | H, W = image.shape[:2] 31 | blob = cv.dnn.blobFromImage(image, size=(672, 384), ddepth=cv.CV_8U) 32 | self.__net.setInput(blob) 33 | out = self.__net.forward() 34 | 35 | bboxes = [] 36 | scores = [] 37 | for det in out.reshape(-1, 7): 38 | score = float(det[2]) 39 | if score < def_score: continue 40 | 41 | x1 = max(0, int(det[3] * W)) 42 | y1 = max(0, int(det[4] * H)) 43 | x2 = min(W, int(det[5] * W)) 44 | y2 = min(H, int(det[6] * H)) 45 | 46 | bboxes.append((x1, y1, x2, y2)) 47 | scores.append(score) 48 | 49 | return scores, bboxes 50 | -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/faceembedder.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Embedder class 3 | =================== 4 | 5 | Face Embedding computation class 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceEmbedder: 16 | """Face embedder class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_EMB_XML, 20 | bin_path=cfg.FACE_EMB_BIN): 21 | """Initialize Face embedder object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 24 | 25 | 26 | def getEmb(self, face_image): 27 | """get embedding from a face image""" 28 | blob = cv.dnn.blobFromImage(face_image, size=(128, 128), ddepth=cv.CV_8U) 29 | self.__net.setInput(blob) 30 | 31 | emb = self.__net.forward().reshape(256) 32 | 33 | return emb 34 | -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/facelandmarker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Facial Landmarker class 3 | ======================= 4 | 5 | Module for Facial Landmark Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceLandmarker: 16 | """Face Landmarker class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_LM_XML, 20 | bin_path=cfg.FACE_LM_BIN): 21 | """Initialize Face Landmarker object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 24 | 25 | 26 | def getLandmark(self, image): 27 | """Locate Facial landmark points in a face image""" 28 | H, W = image.shape[:2] 29 | 30 | blob = cv.dnn.blobFromImage(image, size=(48, 48), ddepth=cv.CV_8U) 31 | self.__net.setInput(blob) 32 | 33 | reg = self.__net.forward().reshape(-1, 10) 34 | points = np.zeros((5, 2), np.int) 35 | for i in range(0, 10, 2): 36 | points[i//2] = (reg[0][i:i+2] * np.array([W, H])).astype(np.int) 37 | 38 | return points 39 | 40 | -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module 3 | ============ 4 | 5 | Support functions for file utilities 6 | """ 7 | 8 | import argparse 9 | import os 10 | 11 | 12 | def getFilename(file_path): 13 | path, filename = os.path.split(file_path) 14 | 15 | return path, filename 16 | 17 | 18 | def getFileNameExt(file_path): 19 | path, filename = getFilename(file_path) 20 | 21 | filename, ext = os.path.splitext(filename) 22 | 23 | return path, filename, ext 24 | 25 | def getArgs(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('--image1', '-i1', type=str, 28 | required=True, 29 | help='Path to first image') 30 | parser.add_argument('--image2', '-i2', type=str, 31 | required=True, 32 | help='Path to secod image') 33 | parser.add_argument('--name', '-n', type=str, 34 | required=False, default='camera', 35 | help='Name of video source') 36 | parser.add_argument('--show', '-s', 37 | default=False, action='store_true', 38 | help='Whether to show the output visualization') 39 | parser.add_argument('--record', '-r', 40 | default=False, action='store_true', 41 | help='Whether to save the output visualization') 42 | parser.add_argument('--flip_hor', '-fh', 43 | required=False, default=False, action='store_true', 44 | help='horizontally flip video frame') 45 | parser.add_argument('--flip_ver', '-fv', 46 | required=False, default=False, action='store_true', 47 | help='vertically flip video frame') 48 | args = parser.parse_args() 49 | 50 | return args 51 | -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/imgproc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Imgproc library 3 | =============== 4 | 5 | Library to support Image processing functions 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Oct 03: 12 | - re-add to AltusI version 0.2 13 | """ 14 | 15 | import os 16 | import math 17 | import numpy as np 18 | import cv2 as cv 19 | 20 | 21 | def resizeByHeight(image, height=720): 22 | """Resize an image given the expected height and keep the original ratio 23 | 24 | Arguments: 25 | ---------- 26 | image : numpy.array 27 | input image to resize 28 | 29 | Keyword Arguments: 30 | ------------------ 31 | height : int (default: 720) 32 | expected width of output image 33 | 34 | Returns: 35 | -------- 36 | out_image : numpy.array 37 | output resized image 38 | """ 39 | H, W = image.shape[:2] 40 | width = int(1. * W * height / H + 0.5) 41 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 42 | 43 | return out_image 44 | 45 | 46 | def resizeByWidth(image, width=600): 47 | """Resize an image given the expected width and keep the original ratio 48 | 49 | Arguments: 50 | ---------- 51 | image : numpy.array 52 | input image to resize 53 | 54 | Keyword Arguments: 55 | ------------------ 56 | width : int (default: 600) 57 | expected width of output image 58 | 59 | Returns: 60 | -------- 61 | out_image : numpy.array 62 | output colored image after resized 63 | """ 64 | 65 | H, W = image.shape[:2] 66 | height = int(H * width / W) 67 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 68 | return out_image 69 | 70 | 71 | def cameraCalibrate(capturer, size=None, by_height=False): 72 | """Get camera's information like dimension and FPS 73 | 74 | Arguments: 75 | ---------- 76 | capturer : cv.VideoCapture 77 | OpenCV-Video capturer object 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | width : int (default: None) 82 | width value to resize by width 83 | 84 | Returns: 85 | -------- 86 | (W, H) : int, int 87 | dimension of video's frame 88 | FPS : float 89 | FPS of the video stream 90 | """ 91 | 92 | fps = capturer.get(cv.CAP_PROP_FPS) 93 | 94 | while True: 95 | _, frame = capturer.read() 96 | if _: 97 | if size: 98 | if by_height: 99 | frame = resizeByHeight(frame, size) 100 | else: 101 | frame = resizeByWidth(frame, size) 102 | H, W = frame.shape[:2] 103 | 104 | return (W, H), fps 105 | 106 | -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logger class 3 | ============ 4 | 5 | Wrapper of built-in `logging` Python module 6 | that supports logging task to both console and files 7 | """ 8 | 9 | """ 10 | Revision 11 | -------- 12 | 2019, Sep 24: first version 13 | """ 14 | 15 | 16 | import os 17 | import logging 18 | 19 | # format for logging message 20 | LOG_FILE_FORMAT = '%(asctime)s %(name)12s [%(levelname)s] %(message)s' 21 | LOG_CONSOLE_FORMAT = '%(asctime)s [%(levelname)s] %(message)s' 22 | 23 | class Logger(): 24 | """Logger class for logging task""" 25 | 26 | def __init__(self, name, console=True): 27 | """Initialize logger 28 | 29 | Parameters 30 | ---------- 31 | name : str 32 | name of logger and file to log 33 | console : bool 34 | whether logging messages are emitted to console or not 35 | """ 36 | self.logger = logging.getLogger(name) 37 | 38 | self.__configLogger(name, console) 39 | 40 | 41 | def __configLogger(self, name, console): 42 | """Configure logger object 43 | 44 | Parameters 45 | ---------- 46 | name : str 47 | name of logging file 48 | console : bool 49 | whether logging messages are emitted to console or not 50 | """ 51 | self.logger.setLevel(logging.DEBUG) 52 | file_formatter = logging.Formatter(LOG_FILE_FORMAT) 53 | console_formatter = logging.Formatter(LOG_CONSOLE_FORMAT) 54 | 55 | # if `console` is set, setup Handler to process 56 | if console: 57 | console_log = logging.StreamHandler() 58 | console_log.setLevel(logging.DEBUG) 59 | console_log.setFormatter(console_formatter) 60 | 61 | file_log = logging.FileHandler(name + '.log', 62 | mode='w') 63 | file_log.setLevel(logging.DEBUG) 64 | file_log.setFormatter(file_formatter) 65 | 66 | if console: 67 | self.logger.addHandler(console_log) 68 | self.logger.addHandler(file_log) 69 | 70 | 71 | def critical(self, msg): 72 | """Emit CRITICAL message""" 73 | self.logger.critical(msg) 74 | 75 | 76 | def error(self, msg): 77 | """Emit ERROR message""" 78 | self.logger.error(msg) 79 | 80 | 81 | def warning(self, msg): 82 | """Emit WARNING message""" 83 | self.logger.warning(msg) 84 | 85 | 86 | def info(self, msg): 87 | """Emit INFO message""" 88 | self.logger.info(msg) 89 | 90 | 91 | def debug(self, msg): 92 | """Emit DEBUG message""" 93 | self.logger.debug(msg) 94 | -------------------------------------------------------------------------------- /006-pi-face-verification/altusi/visualizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Visualization module 3 | ==================== 4 | 5 | Process Image visualization, PIL package is in use 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Sep 25: first version 12 | - add plotBBoxes (with class ids) 13 | """ 14 | 15 | 16 | import random as rnd 17 | 18 | import numpy as np 19 | import cv2 as cv 20 | import PIL 21 | from PIL import Image, ImageFont, ImageDraw, ImageColor 22 | 23 | import altusi.config as cfg 24 | 25 | STANDARD_COLORS = [ 26 | 'AliceBlue', 'Chartreuse', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 27 | 'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue', 'AntiqueWhite', 28 | 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 29 | 'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange', 30 | 'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet', 31 | 'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite', 32 | 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', 33 | 'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki', 34 | 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 35 | 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', 36 | 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 37 | 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', 38 | 'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid', 39 | 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 40 | 'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin', 41 | 'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 42 | 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 43 | 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 44 | 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Green', 'SandyBrown', 45 | 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 46 | 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow', 47 | 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 48 | 'WhiteSmoke', 'Yellow', 'YellowGreen' 49 | ] 50 | 51 | COLOR_MAP = { 52 | 'face':'Crimson', 'bicycle':'BlueViolet', 'bus':'Gold', 'car':'DodgerBlue', 53 | 'motorbike':'OrangeRed', 'person':'Chartreuse' 54 | } 55 | 56 | def getRandomColor(): 57 | """Generate random color 58 | 59 | Returns: 60 | -------- 61 | color : tuple(int, int, int) 62 | generated random color 63 | """ 64 | 65 | color = tuple([int(255*rnd.random()) for _ in range(3)]) 66 | return color 67 | 68 | 69 | def plotBBoxes(image, bboxes, classes=None, scores=None, color='Chartreuse', linewidth=2, use_rgb=False): 70 | """Plot bounding boxes for given input objects 71 | 72 | Arguments: 73 | ---------- 74 | image : numpy.array 75 | input image for drawing 76 | bboxes : list((x1, y1, x2, y2)) 77 | input bounding boxes of objects to draw 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | classes : list(str) (default: None) 82 | list of classes for objects 83 | color : str (default: `Chartreuse`) 84 | color to plot 85 | linewidth : int (default: 2) 86 | how thick the shape is 87 | use_rgb : bool (default: False) 88 | whether using RGB image or not (apply for NDArray image) 89 | 90 | Returns: 91 | -------- 92 | image : numpy.array 93 | output image after drawing 94 | """ 95 | 96 | if len(bboxes) == 0: 97 | return image 98 | exit() 99 | 100 | if isinstance(image, np.ndarray): 101 | if not use_rgb: 102 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 103 | image = Image.fromarray(image) 104 | 105 | W, H = image.size 106 | draw = ImageDraw.Draw(image) 107 | 108 | if classes is not None: 109 | font = ImageFont.truetype(font=cfg.FONT, 110 | size=np.floor(3e-2*min(H, W) + 0.5).astype('int32') ) 111 | 112 | for i, ((x1, y1, x2, y2), cls) in enumerate(zip(bboxes, classes)): 113 | # draw bounding box 114 | draw.rectangle([x1, y1, x2, y2], 115 | outline=ImageColor.getrgb(COLOR_MAP[cls]), 116 | width=linewidth) 117 | # draw label 118 | text = cls 119 | if scores is not None: 120 | text = '{} {:.2f}'.format(cls, scores[i]) 121 | 122 | label_size = draw.textsize(text, font) 123 | text_coor = np.array([x1+linewidth, max(0, y1 - label_size[1] - 1)]) 124 | rec_coor = np.array([x1, text_coor[1]]) 125 | 126 | draw.rectangle([tuple(rec_coor), 127 | tuple(rec_coor + label_size + np.array([linewidth*2, 0])) ], 128 | fill=ImageColor.getrgb(COLOR_MAP[cls])) 129 | draw.text(text_coor, text, fill=ImageColor.getrgb('black'), font=font) 130 | else: 131 | for i, (x1, y1, x2, y2) in enumerate(bboxes): 132 | draw.rectangle([x1, y1, x2, y2], 133 | outline=ImageColor.getrgb(color), 134 | width=linewidth) 135 | 136 | del draw 137 | return image 138 | 139 | 140 | def plotInfo(image, info, color='Chartreuse', use_rgb=False): 141 | if isinstance(image, np.ndarray): 142 | if not use_rgb: 143 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 144 | image = Image.fromarray(image) 145 | 146 | W, H = image.size 147 | draw = ImageDraw.Draw(image) 148 | 149 | font = ImageFont.truetype(font=cfg.FONT, 150 | size=np.floor(5e-2*min(H, W) + 0.5).astype('int32')) 151 | 152 | label_size = draw.textsize(info, font) 153 | text_coor = np.array([5, 0]) 154 | rec_coor = np.array([0, 0]) 155 | draw.rectangle([tuple(rec_coor), 156 | tuple(rec_coor + label_size + np.array([10, 0])) ], 157 | fill=ImageColor.getrgb(color)) 158 | draw.text(text_coor, info, fill=ImageColor.getrgb('black'), font=font) 159 | 160 | del draw 161 | return image 162 | -------------------------------------------------------------------------------- /006-pi-face-verification/app-face-verify.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import numpy as np 5 | import cv2 as cv 6 | 7 | from altusi import helper, config as cfg 8 | from altusi.logger import Logger 9 | 10 | from altusi.facedetector import FaceDetector 11 | from altusi.facelandmarker import FaceLandmarker 12 | from altusi.facealigner import FaceAligner 13 | from altusi.faceembedder import FaceEmbedder 14 | 15 | 16 | LOG = Logger('app-face-id') 17 | 18 | 19 | def getDistance(u, v): 20 | uv = np.dot(u, v) 21 | uu = np.dot(u, u) 22 | vv = np.dot(v, v) 23 | norm = np.sqrt(uu * vv) + 1e-6 24 | 25 | return 1 - uv / norm 26 | 27 | 28 | def app(image1_path, image2_path): 29 | # initialize Face detector net 30 | detector = FaceDetector() 31 | 32 | # initialize Face Landmarker net 33 | landmarker = FaceLandmarker() 34 | 35 | # initialize Face Aligner 36 | aligner = FaceAligner() 37 | 38 | # intializa Face Embedder 39 | embedder = FaceEmbedder() 40 | # ================================================================ 41 | 42 | image1 = cv.imread(image1_path) 43 | image2 = cv.imread(image2_path) 44 | assert image1 is not None and image2 is not None 45 | # ================================================================ 46 | 47 | _, faces_1 = detector.getFaces(image1) 48 | _, faces_2 = detector.getFaces(image2) 49 | assert len(faces_1) and len(faces_2) 50 | # ================================================================ 51 | 52 | x1, y1, x2, y2 = faces_1[0] 53 | face_image1 = image1[y1:y2, x1:x2] 54 | lm1 = landmarker.getLandmark(face_image1) 55 | aligned_face1 = aligner.align(face_image1, lm1) 56 | 57 | x1, y1, x2, y2 = faces_2[0] 58 | face_image2 = image2[y1:y2, x1:x2] 59 | lm2 = landmarker.getLandmark(face_image2) 60 | aligned_face2 = aligner.align(face_image2, lm2) 61 | # ================================================================ 62 | 63 | emb1 = embedder.getEmb(face_image1) 64 | LOG.info('emb1 shape: {}'.format(emb1.shape)) 65 | 66 | emb2 = embedder.getEmb(face_image2) 67 | LOG.info('emb2 shape: {}'.format(emb2.shape)) 68 | 69 | dist = getDistance(emb1, emb2) 70 | LOG.info('distance: {:.4}'.format(dist)) 71 | # ================================================================ 72 | 73 | 74 | def main(args): 75 | image1_path = args.image1 76 | image2_path = args.image2 77 | 78 | app(image1_path, image2_path) 79 | 80 | 81 | if __name__ == '__main__': 82 | LOG.info('Raspberry Pi: Face Identification\n') 83 | 84 | args = helper.getArgs() 85 | main(args) 86 | 87 | LOG.info('Process done') 88 | -------------------------------------------------------------------------------- /006-pi-face-verification/openvino-models: -------------------------------------------------------------------------------- 1 | /home/pi/workspace/openvino-models/ -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/Aller_Bd.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/008-pi-emotion-recognition/altusi/Aller_Bd.ttf -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/008-pi-emotion-recognition/altusi/__init__.py -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # ============================================================================= 5 | # PROJECT'S ORGANIZATION 6 | # ============================================================================= 7 | PROJECT_BASE = '.' 8 | 9 | #=============================================================================== 10 | # PROJECT'S PARAMETERS 11 | #=============================================================================== 12 | FONT = os.path.join(PROJECT_BASE, 'altusi', 'Aller_Bd.ttf') 13 | 14 | TIME_FM = '-%Y%m%d-%H%M%S' 15 | 16 | #=============================================================================== 17 | # PROJECT'S MODELS 18 | #=============================================================================== 19 | MODEL_DIR = 'openvino-models' 20 | 21 | # face detection model 22 | FACE_DET_XML = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.xml') 23 | FACE_DET_BIN = os.path.join(MODEL_DIR, 'face-detection-adas-0001-fp16.bin') 24 | 25 | # facial landmark model 26 | FACE_LM_XML = os.path.join(MODEL_DIR, 'landmarks-regression-retail-0009-fp16.xml') 27 | FACE_LM_BIN = os.path.join(MODEL_DIR, 'landmarks-regression-retail-0009-fp16.bin') 28 | 29 | # face reidentification model 30 | FACE_EMB_XML = os.path.join(MODEL_DIR, 'face-reidentification-retail-0095-fp16.xml') 31 | FACE_EMB_BIN = os.path.join(MODEL_DIR, 'face-reidentification-retail-0095-fp16.bin') 32 | 33 | # emotion recognition 34 | EMOTION_XML = os.path.join(MODEL_DIR, 'emotions-recognition-retail-0003-fp16.xml') 35 | EMOTION_BIN = os.path.join(MODEL_DIR, 'emotions-recognition-retail-0003-fp16.bin') 36 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/emotioner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Emotion Recognition class 3 | =================== 4 | 5 | Module for Emotion Recognition 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class Emotioner: 16 | """Face Detector class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.EMOTION_XML, 20 | bin_path=cfg.EMOTION_BIN): 21 | """Initialize Face detector object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | 24 | # with NCS support 25 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 26 | 27 | 28 | def getEmotion(self, image): 29 | """Detect faces in an input image with given threshold""" 30 | H, W = image.shape[:2] 31 | blob = cv.dnn.blobFromImage(image, size=(64, 64), ddepth=cv.CV_8U) 32 | self.__net.setInput(blob) 33 | out = self.__net.forward().reshape(5) 34 | 35 | idx = np.argmax(out) 36 | 37 | return idx 38 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/facealigner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Aligner class 3 | ================== 4 | 5 | Align face to well form 6 | 7 | This work is inherited and converted from the official OpenVINO toolkit 8 | Original source code can be found here: 9 | /path/to/inference_engine_vpu_arm/deployment_tools/inference_engine/samples/smart_classroom_demo 10 | """ 11 | 12 | 13 | import numpy as np 14 | import cv2 as cv 15 | 16 | _H, _W = 112., 96. 17 | ref_lm_norm = [ 18 | 30.2946 / _W, 51.6963 / _H, 65.5318 / _W, 51.5014 / _H, 48.0252 / _W, 19 | 71.7366 / _H, 33.5493 / _W, 92.3655 / _H, 62.7299 / _W, 92.2041 / _H] 20 | 21 | def getTransform(src, dst): 22 | """get transforming matrix""" 23 | src = np.array(src, np.float) 24 | col_mean_src = np.mean(src, axis=0) 25 | src -= col_mean_src 26 | 27 | dst = np.array(dst, np.float) 28 | col_mean_dst = np.mean(dst, axis=0) 29 | dst -= col_mean_dst 30 | 31 | mean, dev_src = cv.meanStdDev(src) 32 | dev_src = max(dev_src[0], 1.192e-7) 33 | src /= dev_src[0] 34 | 35 | mean, dev_dst = cv.meanStdDev(dst) 36 | dev_dst = max(dev_dst[0], 1.192e-7) 37 | dst /= dev_dst[0] 38 | 39 | w, u, vt = cv.SVDecomp(np.matmul(src.T, dst)) 40 | r = np.matmul(u, vt) 41 | 42 | m = np.zeros((2, 3)) 43 | m[:, 0:2] = r * (dev_dst[0] / dev_src[0]) 44 | m[:, 2] = col_mean_dst.T - np.matmul(m[:, 0:2], col_mean_src.T) 45 | 46 | return m 47 | 48 | 49 | class FaceAligner: 50 | """Face Aligner class""" 51 | 52 | @staticmethod 53 | def align(face_image, landmark): 54 | """align an input image with given landmark points""" 55 | H, W = face_image.shape[:2] 56 | ref_lm = np.zeros((5, 2)) 57 | for i in range(5): 58 | ref_lm[i, 0] = ref_lm_norm[2*i] * W 59 | ref_lm[i, 1] = ref_lm_norm[2*i+1] * H 60 | 61 | m = getTransform(ref_lm, landmark) 62 | aligned_face = cv.warpAffine(face_image, m, (W, H), cv.WARP_INVERSE_MAP) 63 | 64 | return aligned_face 65 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/facedetector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Detector class 3 | =================== 4 | 5 | Module for Face Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceDetector: 16 | """Face Detector class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_DET_XML, 20 | bin_path=cfg.FACE_DET_BIN): 21 | """Initialize Face detector object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | 24 | # with NCS support 25 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 26 | 27 | 28 | def getFaces(self, image, def_score=0.5): 29 | """Detect faces in an input image with given threshold""" 30 | H, W = image.shape[:2] 31 | blob = cv.dnn.blobFromImage(image, size=(672, 384), ddepth=cv.CV_8U) 32 | self.__net.setInput(blob) 33 | out = self.__net.forward() 34 | 35 | bboxes = [] 36 | scores = [] 37 | for det in out.reshape(-1, 7): 38 | score = float(det[2]) 39 | if score < def_score: continue 40 | 41 | x1 = max(0, int(det[3] * W)) 42 | y1 = max(0, int(det[4] * H)) 43 | x2 = min(W, int(det[5] * W)) 44 | y2 = min(H, int(det[6] * H)) 45 | 46 | bboxes.append((x1, y1, x2, y2)) 47 | scores.append(score) 48 | 49 | return scores, bboxes 50 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/faceembedder.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Embedder class 3 | =================== 4 | 5 | Face Embedding computation class 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceEmbedder: 16 | """Face embedder class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_EMB_XML, 20 | bin_path=cfg.FACE_EMB_BIN): 21 | """Initialize Face embedder object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 24 | 25 | 26 | def getEmb(self, face_image): 27 | """get embedding from a face image""" 28 | blob = cv.dnn.blobFromImage(face_image, size=(128, 128), ddepth=cv.CV_8U) 29 | self.__net.setInput(blob) 30 | 31 | emb = self.__net.forward().reshape(256) 32 | 33 | return emb 34 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/facelandmarker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Facial Landmarker class 3 | ======================= 4 | 5 | Module for Facial Landmark Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceLandmarker: 16 | """Face Landmarker class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_LM_XML, 20 | bin_path=cfg.FACE_LM_BIN): 21 | """Initialize Face Landmarker object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 24 | 25 | 26 | def getLandmark(self, image): 27 | """Locate Facial landmark points in a face image""" 28 | H, W = image.shape[:2] 29 | 30 | blob = cv.dnn.blobFromImage(image, size=(48, 48), ddepth=cv.CV_8U) 31 | self.__net.setInput(blob) 32 | 33 | reg = self.__net.forward().reshape(-1, 10) 34 | points = np.zeros((5, 2), np.int) 35 | for i in range(0, 10, 2): 36 | points[i//2] = (reg[0][i:i+2] * np.array([W, H])).astype(np.int) 37 | 38 | return points 39 | 40 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module 3 | ============ 4 | 5 | Support functions for file utilities 6 | """ 7 | 8 | import argparse 9 | import os 10 | 11 | 12 | def getFilename(file_path): 13 | path, filename = os.path.split(file_path) 14 | 15 | return path, filename 16 | 17 | 18 | def getFileNameExt(file_path): 19 | path, filename = getFilename(file_path) 20 | 21 | filename, ext = os.path.splitext(filename) 22 | 23 | return path, filename, ext 24 | 25 | def getArgs(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('--image', '-i', type=str, 28 | required=False, 29 | help='Path to Image file') 30 | parser.add_argument('--video', '-v', type=str, 31 | required=False, 32 | help='Video Streamming link or Path to video source') 33 | parser.add_argument('--name', '-n', type=str, 34 | required=False, default='camera', 35 | help='Name of video source') 36 | parser.add_argument('--show', '-s', 37 | default=False, action='store_true', 38 | help='Whether to show the output visualization') 39 | parser.add_argument('--record', '-r', 40 | default=False, action='store_true', 41 | help='Whether to save the output visualization') 42 | parser.add_argument('--flip_hor', '-fh', 43 | required=False, default=False, action='store_true', 44 | help='horizontally flip video frame') 45 | parser.add_argument('--flip_ver', '-fv', 46 | required=False, default=False, action='store_true', 47 | help='vertically flip video frame') 48 | args = parser.parse_args() 49 | 50 | return args 51 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/imgproc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Imgproc library 3 | =============== 4 | 5 | Library to support Image processing functions 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Oct 03: 12 | - re-add to AltusI version 0.2 13 | """ 14 | 15 | import os 16 | import math 17 | import numpy as np 18 | import cv2 as cv 19 | 20 | 21 | def resizeByHeight(image, height=720): 22 | """Resize an image given the expected height and keep the original ratio 23 | 24 | Arguments: 25 | ---------- 26 | image : numpy.array 27 | input image to resize 28 | 29 | Keyword Arguments: 30 | ------------------ 31 | height : int (default: 720) 32 | expected width of output image 33 | 34 | Returns: 35 | -------- 36 | out_image : numpy.array 37 | output resized image 38 | """ 39 | H, W = image.shape[:2] 40 | width = int(1. * W * height / H + 0.5) 41 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 42 | 43 | return out_image 44 | 45 | 46 | def resizeByWidth(image, width=600): 47 | """Resize an image given the expected width and keep the original ratio 48 | 49 | Arguments: 50 | ---------- 51 | image : numpy.array 52 | input image to resize 53 | 54 | Keyword Arguments: 55 | ------------------ 56 | width : int (default: 600) 57 | expected width of output image 58 | 59 | Returns: 60 | -------- 61 | out_image : numpy.array 62 | output colored image after resized 63 | """ 64 | 65 | H, W = image.shape[:2] 66 | height = int(H * width / W) 67 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 68 | return out_image 69 | 70 | 71 | def cameraCalibrate(capturer, size=None, by_height=False): 72 | """Get camera's information like dimension and FPS 73 | 74 | Arguments: 75 | ---------- 76 | capturer : cv.VideoCapture 77 | OpenCV-Video capturer object 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | width : int (default: None) 82 | width value to resize by width 83 | 84 | Returns: 85 | -------- 86 | (W, H) : int, int 87 | dimension of video's frame 88 | FPS : float 89 | FPS of the video stream 90 | """ 91 | 92 | fps = capturer.get(cv.CAP_PROP_FPS) 93 | 94 | while True: 95 | _, frame = capturer.read() 96 | if _: 97 | if size: 98 | if by_height: 99 | frame = resizeByHeight(frame, size) 100 | else: 101 | frame = resizeByWidth(frame, size) 102 | H, W = frame.shape[:2] 103 | 104 | return (W, H), fps 105 | 106 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logger class 3 | ============ 4 | 5 | Wrapper of built-in `logging` Python module 6 | that supports logging task to both console and files 7 | """ 8 | 9 | """ 10 | Revision 11 | -------- 12 | 2019, Sep 24: first version 13 | """ 14 | 15 | 16 | import os 17 | import logging 18 | 19 | # format for logging message 20 | LOG_FILE_FORMAT = '%(asctime)s %(name)12s [%(levelname)s] %(message)s' 21 | LOG_CONSOLE_FORMAT = '%(asctime)s [%(levelname)s] %(message)s' 22 | 23 | class Logger(): 24 | """Logger class for logging task""" 25 | 26 | def __init__(self, name, console=True): 27 | """Initialize logger 28 | 29 | Parameters 30 | ---------- 31 | name : str 32 | name of logger and file to log 33 | console : bool 34 | whether logging messages are emitted to console or not 35 | """ 36 | self.logger = logging.getLogger(name) 37 | 38 | self.__configLogger(name, console) 39 | 40 | 41 | def __configLogger(self, name, console): 42 | """Configure logger object 43 | 44 | Parameters 45 | ---------- 46 | name : str 47 | name of logging file 48 | console : bool 49 | whether logging messages are emitted to console or not 50 | """ 51 | self.logger.setLevel(logging.DEBUG) 52 | file_formatter = logging.Formatter(LOG_FILE_FORMAT) 53 | console_formatter = logging.Formatter(LOG_CONSOLE_FORMAT) 54 | 55 | # if `console` is set, setup Handler to process 56 | if console: 57 | console_log = logging.StreamHandler() 58 | console_log.setLevel(logging.DEBUG) 59 | console_log.setFormatter(console_formatter) 60 | 61 | file_log = logging.FileHandler(name + '.log', 62 | mode='w') 63 | file_log.setLevel(logging.DEBUG) 64 | file_log.setFormatter(file_formatter) 65 | 66 | if console: 67 | self.logger.addHandler(console_log) 68 | self.logger.addHandler(file_log) 69 | 70 | 71 | def critical(self, msg): 72 | """Emit CRITICAL message""" 73 | self.logger.critical(msg) 74 | 75 | 76 | def error(self, msg): 77 | """Emit ERROR message""" 78 | self.logger.error(msg) 79 | 80 | 81 | def warning(self, msg): 82 | """Emit WARNING message""" 83 | self.logger.warning(msg) 84 | 85 | 86 | def info(self, msg): 87 | """Emit INFO message""" 88 | self.logger.info(msg) 89 | 90 | 91 | def debug(self, msg): 92 | """Emit DEBUG message""" 93 | self.logger.debug(msg) 94 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/altusi/visualizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Visualization module 3 | ==================== 4 | 5 | Process Image visualization, PIL package is in use 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Sep 25: first version 12 | - add plotBBoxes (with class ids) 13 | """ 14 | 15 | 16 | import random as rnd 17 | 18 | import numpy as np 19 | import cv2 as cv 20 | import PIL 21 | from PIL import Image, ImageFont, ImageDraw, ImageColor 22 | 23 | import altusi.config as cfg 24 | 25 | STANDARD_COLORS = [ 26 | 'AliceBlue', 'Chartreuse', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 27 | 'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue', 'AntiqueWhite', 28 | 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 29 | 'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange', 30 | 'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet', 31 | 'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite', 32 | 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', 33 | 'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki', 34 | 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 35 | 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', 36 | 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 37 | 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', 38 | 'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid', 39 | 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 40 | 'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin', 41 | 'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 42 | 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 43 | 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 44 | 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Green', 'SandyBrown', 45 | 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 46 | 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow', 47 | 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 48 | 'WhiteSmoke', 'Yellow', 'YellowGreen' 49 | ] 50 | 51 | COLOR_MAP = { 52 | 'face':'Crimson', 'bicycle':'BlueViolet', 'bus':'Gold', 'car':'DodgerBlue', 53 | 'motorbike':'OrangeRed', 'person':'Chartreuse' 54 | } 55 | 56 | def getRandomColor(): 57 | """Generate random color 58 | 59 | Returns: 60 | -------- 61 | color : tuple(int, int, int) 62 | generated random color 63 | """ 64 | 65 | color = tuple([int(255*rnd.random()) for _ in range(3)]) 66 | return color 67 | 68 | 69 | def plotBBoxes(image, bboxes, classes=None, scores=None, color='Chartreuse', linewidth=2, use_rgb=False): 70 | """Plot bounding boxes for given input objects 71 | 72 | Arguments: 73 | ---------- 74 | image : numpy.array 75 | input image for drawing 76 | bboxes : list((x1, y1, x2, y2)) 77 | input bounding boxes of objects to draw 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | classes : list(str) (default: None) 82 | list of classes for objects 83 | color : str (default: `Chartreuse`) 84 | color to plot 85 | linewidth : int (default: 2) 86 | how thick the shape is 87 | use_rgb : bool (default: False) 88 | whether using RGB image or not (apply for NDArray image) 89 | 90 | Returns: 91 | -------- 92 | image : numpy.array 93 | output image after drawing 94 | """ 95 | 96 | if len(bboxes) == 0: 97 | return image 98 | exit() 99 | 100 | if isinstance(image, np.ndarray): 101 | if not use_rgb: 102 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 103 | image = Image.fromarray(image) 104 | 105 | W, H = image.size 106 | draw = ImageDraw.Draw(image) 107 | 108 | if classes is not None: 109 | font = ImageFont.truetype(font=cfg.FONT, 110 | size=np.floor(3e-2*min(H, W) + 0.5).astype('int32') ) 111 | 112 | for i, ((x1, y1, x2, y2), cls) in enumerate(zip(bboxes, classes)): 113 | # draw bounding box 114 | if cls in COLOR_MAP: 115 | draw.rectangle([x1, y1, x2, y2], 116 | outline=ImageColor.getrgb(COLOR_MAP[cls]), 117 | width=linewidth) 118 | else: 119 | draw.rectangle([x1, y1, x2, y2], 120 | outline=ImageColor.getrgb('Chartreuse'), 121 | width=linewidth) 122 | 123 | # draw label 124 | text = cls 125 | if scores is not None: 126 | text = '{} {:.2f}'.format(cls, scores[i]) 127 | 128 | label_size = draw.textsize(text, font) 129 | text_coor = np.array([x1+linewidth, max(0, y1 - label_size[1] - 1)]) 130 | rec_coor = np.array([x1, text_coor[1]]) 131 | 132 | if cls in COLOR_MAP: 133 | draw.rectangle([tuple(rec_coor), 134 | tuple(rec_coor + label_size + np.array([linewidth*2, 0])) ], 135 | fill=ImageColor.getrgb(COLOR_MAP[cls])) 136 | else: 137 | draw.rectangle([tuple(rec_coor), 138 | tuple(rec_coor + label_size + np.array([linewidth*2, 0])) ], 139 | fill=ImageColor.getrgb('Chartreuse')) 140 | draw.text(text_coor, text, fill=ImageColor.getrgb('black'), font=font) 141 | else: 142 | for i, (x1, y1, x2, y2) in enumerate(bboxes): 143 | draw.rectangle([x1, y1, x2, y2], 144 | outline=ImageColor.getrgb(color), 145 | width=linewidth) 146 | 147 | del draw 148 | return image 149 | 150 | 151 | def plotInfo(image, info, color='Chartreuse', use_rgb=False): 152 | if isinstance(image, np.ndarray): 153 | if not use_rgb: 154 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 155 | image = Image.fromarray(image) 156 | 157 | W, H = image.size 158 | draw = ImageDraw.Draw(image) 159 | 160 | font = ImageFont.truetype(font=cfg.FONT, 161 | size=np.floor(5e-2*min(H, W) + 0.5).astype('int32')) 162 | 163 | label_size = draw.textsize(info, font) 164 | text_coor = np.array([5, 0]) 165 | rec_coor = np.array([0, 0]) 166 | draw.rectangle([tuple(rec_coor), 167 | tuple(rec_coor + label_size + np.array([10, 0])) ], 168 | fill=ImageColor.getrgb(color)) 169 | draw.text(text_coor, info, fill=ImageColor.getrgb('black'), font=font) 170 | 171 | del draw 172 | return image 173 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/app-emotion-image.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import cv2 as cv 4 | 5 | import altusi.config as cfg 6 | import altusi.visualizer as vis 7 | from altusi import imgproc, helper 8 | from altusi.logger import Logger 9 | 10 | from altusi.facedetector import FaceDetector 11 | from altusi.emotioner import Emotioner 12 | 13 | 14 | LOG = Logger(__file__.split(',')[0]) 15 | 16 | def app(image_path): 17 | # initialize Face Detection net 18 | face_detector = FaceDetector() 19 | 20 | # initialize Emotioner net 21 | emotioner = Emotioner() 22 | 23 | frm = cv.imread(image_path) 24 | _start_t = time.time() 25 | scores, bboxes = face_detector.getFaces(frm, def_score=0.5) 26 | for i, bbox in enumerate(bboxes): 27 | x1, y1, x2, y2 = bbox 28 | face_img = frm[y1:y2, x1:x2] 29 | emo_idx = emotioner.getEmotion(face_img) 30 | LOG.info('emotion: {}'.format(emo_idx)) 31 | _prx_t = time.time() - _start_t 32 | 33 | 34 | # if len(bboxes): 35 | # frm = vis.plotBBoxes(frm, bboxes, len(bboxes) * ['face'], scores) 36 | # frm = vis.plotInfo(frm, 'Raspberry Pi - FPS: {:.3f}'.format(1/_prx_t)) 37 | # frm = cv.cvtColor(np.asarray(frm), cv.COLOR_BGR2RGB) 38 | 39 | cv.destroyAllWindows() 40 | 41 | 42 | def main(args): 43 | app(args.image) 44 | 45 | 46 | if __name__ == '__main__': 47 | LOG.info('Raspberry Pi: Emotion Recognition\n') 48 | 49 | args = helper.getArgs() 50 | main(args) 51 | 52 | LOG.info('Process done') 53 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/app-emotion-recog.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import cv2 as cv 4 | 5 | import altusi.config as cfg 6 | import altusi.visualizer as vis 7 | from altusi import imgproc, helper 8 | from altusi.logger import Logger 9 | 10 | from altusi.facedetector import FaceDetector 11 | from altusi.emotioner import Emotioner 12 | 13 | LOG = Logger(__file__.split(',')[0]) 14 | EMOTION = ['neutral', 'happy', 'sad', 'surprise', 'anger'] 15 | 16 | 17 | def getPadding(image, bbox): 18 | H, W = image.shape[:2] 19 | x1, y1, x2, y2 = bbox 20 | 21 | w, h = x2-x1, y2-y1 22 | if h > w: 23 | d = (h - w)/2 24 | x1 = max(0, int(x1 - d)) 25 | x2 = min(W, int(x2 + d)) 26 | 27 | return x1, y1, x2, y2 28 | 29 | 30 | def app(video_link, video_name, show, record, flip_hor, flip_ver): 31 | # initialize Face Detection net 32 | face_detector = FaceDetector() 33 | 34 | # initialize Emotioner net 35 | emotioner = Emotioner() 36 | 37 | # initialize Video Capturer 38 | cap = cv.VideoCapture(video_link) 39 | (W, H), FPS = imgproc.cameraCalibrate(cap) 40 | LOG.info('Camera Info: ({}, {}) - {:.3f}'.format(W, H, FPS)) 41 | 42 | if record: 43 | time_str = time.strftime(cfg.TIME_FM) 44 | writer = cv.VideoWriter(video_name+time_str+'.avi', 45 | cv.VideoWriter_fourcc(*'XVID'), FPS, (W, H)) 46 | 47 | while cap.isOpened(): 48 | _, frm = cap.read() 49 | if not _: 50 | LOG.info('Reached the end of Video source') 51 | break 52 | 53 | if flip_ver: frm = cv.flip(frm, 0) 54 | if flip_hor: frm = cv.flip(frm, 1) 55 | 56 | _start_t = time.time() 57 | scores, bboxes = face_detector.getFaces(frm, def_score=0.5) 58 | emos = [] 59 | pbboxes = [] 60 | for i, bbox in enumerate(bboxes): 61 | x1, y1, x2, y2 = getPadding(frm, bbox) 62 | face_img = frm[y1:y2, x1:x2] 63 | emo_idx = emotioner.getEmotion(face_img) 64 | emos.append(EMOTION[emo_idx]) 65 | pbboxes.append((x1, y1, x2, y2)) 66 | _prx_t = time.time() - _start_t 67 | 68 | 69 | if len(bboxes): 70 | frm = vis.plotBBoxes(frm, pbboxes, emos) 71 | frm = vis.plotInfo(frm, 'Raspberry Pi - FPS: {:.3f}'.format(1/_prx_t)) 72 | frm = cv.cvtColor(np.asarray(frm), cv.COLOR_BGR2RGB) 73 | 74 | if record: 75 | writer.write(frm) 76 | 77 | if show: 78 | cv.imshow(video_name, frm) 79 | key = cv.waitKey(1) 80 | if key in [27, ord('q')]: 81 | LOG.info('Interrupted by Users') 82 | break 83 | 84 | if record: 85 | writer.release() 86 | cap.release() 87 | cv.destroyAllWindows() 88 | 89 | 90 | def main(args): 91 | video_link = args.video if args.video else 0 92 | app(video_link, args.name, args.show, args.record, args.flip_hor, args.flip_ver) 93 | 94 | 95 | if __name__ == '__main__': 96 | LOG.info('Raspberry Pi: Face Detection') 97 | 98 | args = helper.getArgs() 99 | main(args) 100 | 101 | LOG.info('Process done') 102 | -------------------------------------------------------------------------------- /008-pi-emotion-recognition/openvino-models: -------------------------------------------------------------------------------- 1 | /home/pi/workspace/openvino-models/ -------------------------------------------------------------------------------- /012-tflite-object-detection/README.md: -------------------------------------------------------------------------------- 1 | # 012-tflite-object-detection 2 | 3 | Model link: [[link]](https://drive.google.com/drive/folders/1J8o-D-M5WdbkvE8xwZrrwL02W5KmKDDU) 4 | 5 | Download files from the above link and put to `tflite-models` (e.g. `/home/pi/workspace`) 6 | Create a symbol link to that folder: `ln -s /path/to/tflite-models` 7 | 8 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/Aller_Bd.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhdoan/computer-vision-raspberrypi/f5e016ce9a72d66c47ab11de9aa35766a6a026b2/012-tflite-object-detection/altusi/Aller_Bd.ttf -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/__init__.py: -------------------------------------------------------------------------------- 1 | from .logger import Logger 2 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # ============================================================================= 5 | # PROJECT'S ORGANIZATION 6 | # ============================================================================= 7 | PROJECT_BASE = '.' 8 | 9 | #=============================================================================== 10 | # PROJECT'S PARAMETERS 11 | #=============================================================================== 12 | FONT = os.path.join(PROJECT_BASE, 'altusi', 'Aller_Bd.ttf') 13 | 14 | TIME_FM = '-%Y%m%d-%H%M%S' 15 | pFPS = 10 16 | 17 | #=============================================================================== 18 | # PROJECT'S MODELS 19 | #=============================================================================== 20 | MODEL_DIR = 'tflite-models' 21 | 22 | LABEL_PATH = os.path.join(MODEL_DIR, 'coco_labels.txt') 23 | DETECT_MODEL_PATH = os.path.join(MODEL_DIR, 'detect.tflite') 24 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/facelibs/__init__.py: -------------------------------------------------------------------------------- 1 | from facedetector import FaceDetector 2 | from facelandmarker import FaceLandmarker 3 | from facealigner import FaceAligner 4 | from faceembedder import FaceEmbedder 5 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/facelibs/facealigner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Aligner class 3 | ================== 4 | 5 | Align face to well form 6 | 7 | This work is inherited and converted from the official OpenVINO toolkit 8 | Original source code can be found here: 9 | /path/to/inference_engine_vpu_arm/deployment_tools/inference_engine/samples/smart_classroom_demo 10 | """ 11 | 12 | 13 | import numpy as np 14 | import cv2 as cv 15 | 16 | _H, _W = 112., 96. 17 | ref_lm_norm = [ 18 | 30.2946 / _W, 51.6963 / _H, 65.5318 / _W, 51.5014 / _H, 48.0252 / _W, 19 | 71.7366 / _H, 33.5493 / _W, 92.3655 / _H, 62.7299 / _W, 92.2041 / _H] 20 | 21 | def getTransform(src, dst): 22 | """get transforming matrix""" 23 | src = np.array(src, np.float) 24 | col_mean_src = np.mean(src, axis=0) 25 | src -= col_mean_src 26 | 27 | dst = np.array(dst, np.float) 28 | col_mean_dst = np.mean(dst, axis=0) 29 | dst -= col_mean_dst 30 | 31 | mean, dev_src = cv.meanStdDev(src) 32 | dev_src = max(dev_src[0], 1.192e-7) 33 | src /= dev_src[0] 34 | 35 | mean, dev_dst = cv.meanStdDev(dst) 36 | dev_dst = max(dev_dst[0], 1.192e-7) 37 | dst /= dev_dst[0] 38 | 39 | w, u, vt = cv.SVDecomp(np.matmul(src.T, dst)) 40 | r = np.matmul(u, vt) 41 | 42 | m = np.zeros((2, 3)) 43 | m[:, 0:2] = r * (dev_dst[0] / dev_src[0]) 44 | m[:, 2] = col_mean_dst.T - np.matmul(m[:, 0:2], col_mean_src.T) 45 | 46 | return m 47 | 48 | 49 | class FaceAligner: 50 | """Face Aligner class""" 51 | 52 | @staticmethod 53 | def align(face_image, landmark): 54 | """align an input image with given landmark points""" 55 | H, W = face_image.shape[:2] 56 | ref_lm = np.zeros((5, 2)) 57 | for i in range(5): 58 | ref_lm[i, 0] = ref_lm_norm[2*i] * W 59 | ref_lm[i, 1] = ref_lm_norm[2*i+1] * H 60 | 61 | m = getTransform(ref_lm, landmark) 62 | aligned_face = cv.warpAffine(face_image, m, (W, H), cv.WARP_INVERSE_MAP) 63 | 64 | return aligned_face 65 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/facelibs/facedetector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Detector class 3 | =================== 4 | 5 | Module for Face Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceDetector: 16 | """Face Detector class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_DET_XML, 20 | bin_path=cfg.FACE_DET_BIN): 21 | """Initialize Face detector object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | 24 | # with NCS support 25 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 26 | 27 | 28 | def getFaces(self, image, def_score=0.5): 29 | """Detect faces in an input image with given threshold""" 30 | H, W = image.shape[:2] 31 | blob = cv.dnn.blobFromImage(image, size=(672, 384), ddepth=cv.CV_8U) 32 | self.__net.setInput(blob) 33 | out = self.__net.forward() 34 | 35 | bboxes = [] 36 | scores = [] 37 | for det in out.reshape(-1, 7): 38 | score = float(det[2]) 39 | if score < def_score: continue 40 | 41 | x1 = max(0, int(det[3] * W)) 42 | y1 = max(0, int(det[4] * H)) 43 | x2 = min(W, int(det[5] * W)) 44 | y2 = min(H, int(det[6] * H)) 45 | 46 | bboxes.append((x1, y1, x2, y2)) 47 | scores.append(score) 48 | 49 | return scores, bboxes 50 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/facelibs/faceembedder.py: -------------------------------------------------------------------------------- 1 | """ 2 | Face Embedder class 3 | =================== 4 | 5 | Face Embedding computation class 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceEmbedder: 16 | """Face embedder class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_EMB_XML, 20 | bin_path=cfg.FACE_EMB_BIN): 21 | """Initialize Face embedder object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 24 | 25 | 26 | def getEmb(self, face_image): 27 | """get embedding from a face image""" 28 | blob = cv.dnn.blobFromImage(face_image, size=(128, 128), ddepth=cv.CV_8U) 29 | self.__net.setInput(blob) 30 | 31 | emb = self.__net.forward().reshape(256) 32 | 33 | return emb 34 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/facelibs/facelandmarker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Facial Landmarker class 3 | ======================= 4 | 5 | Module for Facial Landmark Detection 6 | """ 7 | 8 | 9 | import numpy as np 10 | import cv2 as cv 11 | 12 | import altusi.config as cfg 13 | 14 | 15 | class FaceLandmarker: 16 | """Face Landmarker class""" 17 | 18 | def __init__(self, 19 | xml_path=cfg.FACE_LM_XML, 20 | bin_path=cfg.FACE_LM_BIN): 21 | """Initialize Face Landmarker object""" 22 | self.__net = cv.dnn.readNet(xml_path, bin_path) 23 | self.__net.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD) 24 | 25 | 26 | def getLandmark(self, image): 27 | """Locate Facial landmark points in a face image""" 28 | H, W = image.shape[:2] 29 | 30 | blob = cv.dnn.blobFromImage(image, size=(48, 48), ddepth=cv.CV_8U) 31 | self.__net.setInput(blob) 32 | 33 | reg = self.__net.forward().reshape(-1, 10) 34 | points = np.zeros((5, 2), np.int) 35 | for i in range(0, 10, 2): 36 | points[i//2] = (reg[0][i:i+2] * np.array([W, H])).astype(np.int) 37 | 38 | return points 39 | 40 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module 3 | ============ 4 | 5 | Support functions for file utilities 6 | """ 7 | 8 | import argparse 9 | import os 10 | 11 | 12 | def getFilename(file_path): 13 | path, filename = os.path.split(file_path) 14 | 15 | return path, filename 16 | 17 | 18 | def getFileNameExt(file_path): 19 | path, filename = getFilename(file_path) 20 | 21 | filename, ext = os.path.splitext(filename) 22 | 23 | return path, filename, ext 24 | 25 | def getArgs(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('--image', '-i', type=str, 28 | required=False, 29 | help='Path to input image') 30 | parser.add_argument('--video', '-v', type=str, 31 | required=False, 32 | help='Video Streamming link or Path to video source') 33 | parser.add_argument('--name', '-n', type=str, 34 | required=False, default='camera', 35 | help='Name of video source') 36 | parser.add_argument('--show', '-s', 37 | default=False, action='store_true', 38 | help='Whether to show the output visualization') 39 | parser.add_argument('--record', '-r', 40 | default=False, action='store_true', 41 | help='Whether to save the output visualization') 42 | parser.add_argument('--flip_hor', '-fh', 43 | required=False, default=False, action='store_true', 44 | help='horizontally flip video frame') 45 | parser.add_argument('--flip_ver', '-fv', 46 | required=False, default=False, action='store_true', 47 | help='vertically flip video frame') 48 | args = parser.parse_args() 49 | 50 | return args 51 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/imgproc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Imgproc library 3 | =============== 4 | 5 | Library to support Image processing functions 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Oct 03: 12 | - re-add to AltusI version 0.2 13 | """ 14 | 15 | import os 16 | import time 17 | import math 18 | 19 | import numpy as np 20 | import cv2 as cv 21 | 22 | import altusi.config as cfg 23 | 24 | def resizeByHeight(image, height=720): 25 | """Resize an image given the expected height and keep the original ratio 26 | 27 | Arguments: 28 | ---------- 29 | image : numpy.array 30 | input image to resize 31 | 32 | Keyword Arguments: 33 | ------------------ 34 | height : int (default: 720) 35 | expected width of output image 36 | 37 | Returns: 38 | -------- 39 | out_image : numpy.array 40 | output resized image 41 | """ 42 | H, W = image.shape[:2] 43 | width = int(1. * W * height / H + 0.5) 44 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 45 | 46 | return out_image 47 | 48 | 49 | def resizeByWidth(image, width=600): 50 | """Resize an image given the expected width and keep the original ratio 51 | 52 | Arguments: 53 | ---------- 54 | image : numpy.array 55 | input image to resize 56 | 57 | Keyword Arguments: 58 | ------------------ 59 | width : int (default: 600) 60 | expected width of output image 61 | 62 | Returns: 63 | -------- 64 | out_image : numpy.array 65 | output colored image after resized 66 | """ 67 | 68 | H, W = image.shape[:2] 69 | height = int(H * width / W) 70 | out_image = cv.resize(image, (width, height), interpolation=cv.INTER_CUBIC) 71 | return out_image 72 | 73 | 74 | def cameraCalibrate(capturer, size=None, by_height=False): 75 | """Get camera's information like dimension and FPS 76 | 77 | Arguments: 78 | ---------- 79 | capturer : cv.VideoCapture 80 | OpenCV-Video capturer object 81 | 82 | Keyword Arguments: 83 | ------------------ 84 | width : int (default: None) 85 | width value to resize by width 86 | 87 | Returns: 88 | -------- 89 | (W, H) : int, int 90 | dimension of video's frame 91 | FPS : float 92 | FPS of the video stream 93 | """ 94 | 95 | fps = capturer.get(cv.CAP_PROP_FPS) 96 | 97 | while True: 98 | _, frame = capturer.read() 99 | if _: 100 | if size: 101 | if by_height: 102 | frame = resizeByHeight(frame, size) 103 | else: 104 | frame = resizeByWidth(frame, size) 105 | H, W = frame.shape[:2] 106 | 107 | return (W, H), fps 108 | 109 | 110 | def getVideoRecorder(video_name, frm_size, FPS): 111 | time_str = time.strftime(cfg.TIME_FM) 112 | writer = cv.VideoWriter(video_name + time_str + '.avi', 113 | cv.VideoWriter_fourcc(*'XVID'), FPS, frm_size) 114 | return writer 115 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logger class 3 | ============ 4 | 5 | Wrapper of built-in `logging` Python module 6 | that supports logging task to both console and files 7 | """ 8 | 9 | """ 10 | Revision 11 | -------- 12 | 2019, Sep 24: first version 13 | """ 14 | 15 | 16 | import os 17 | import logging 18 | 19 | # format for logging message 20 | LOG_FILE_FORMAT = '%(asctime)s %(name)12s [%(levelname)s] %(message)s' 21 | LOG_CONSOLE_FORMAT = '%(asctime)s [%(levelname)s] %(message)s' 22 | 23 | class Logger(): 24 | """Logger class for logging task""" 25 | 26 | def __init__(self, name, console=True): 27 | """Initialize logger 28 | 29 | Parameters 30 | ---------- 31 | name : str 32 | name of logger and file to log 33 | console : bool 34 | whether logging messages are emitted to console or not 35 | """ 36 | self.logger = logging.getLogger(name) 37 | 38 | self.__configLogger(name, console) 39 | 40 | 41 | def __configLogger(self, name, console): 42 | """Configure logger object 43 | 44 | Parameters 45 | ---------- 46 | name : str 47 | name of logging file 48 | console : bool 49 | whether logging messages are emitted to console or not 50 | """ 51 | self.logger.setLevel(logging.DEBUG) 52 | file_formatter = logging.Formatter(LOG_FILE_FORMAT) 53 | console_formatter = logging.Formatter(LOG_CONSOLE_FORMAT) 54 | 55 | # if `console` is set, setup Handler to process 56 | if console: 57 | console_log = logging.StreamHandler() 58 | console_log.setLevel(logging.DEBUG) 59 | console_log.setFormatter(console_formatter) 60 | 61 | file_log = logging.FileHandler(name + '.log', 62 | mode='w') 63 | file_log.setLevel(logging.DEBUG) 64 | file_log.setFormatter(file_formatter) 65 | 66 | if console: 67 | self.logger.addHandler(console_log) 68 | self.logger.addHandler(file_log) 69 | 70 | 71 | def critical(self, msg): 72 | """Emit CRITICAL message""" 73 | self.logger.critical(msg) 74 | 75 | 76 | def error(self, msg): 77 | """Emit ERROR message""" 78 | self.logger.error(msg) 79 | 80 | 81 | def warning(self, msg): 82 | """Emit WARNING message""" 83 | self.logger.warning(msg) 84 | 85 | 86 | def info(self, msg): 87 | """Emit INFO message""" 88 | self.logger.info(msg) 89 | 90 | 91 | def debug(self, msg): 92 | """Emit DEBUG message""" 93 | self.logger.debug(msg) 94 | -------------------------------------------------------------------------------- /012-tflite-object-detection/altusi/visualizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Visualization module 3 | ==================== 4 | 5 | Process Image visualization, PIL package is in use 6 | """ 7 | 8 | """ 9 | Revision 10 | -------- 11 | 2019, Sep 25: first version 12 | - add plotBBoxes (with class ids) 13 | """ 14 | 15 | 16 | import random as rnd 17 | 18 | import numpy as np 19 | import cv2 as cv 20 | import PIL 21 | from PIL import Image, ImageFont, ImageDraw, ImageColor 22 | 23 | import altusi.config as cfg 24 | 25 | STANDARD_COLORS = [ 26 | 'AliceBlue', 'Chartreuse', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 27 | 'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue', 'AntiqueWhite', 28 | 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 29 | 'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange', 30 | 'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet', 31 | 'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite', 32 | 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', 33 | 'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki', 34 | 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 35 | 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', 36 | 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 37 | 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', 38 | 'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid', 39 | 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 40 | 'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin', 41 | 'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 42 | 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 43 | 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 44 | 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Green', 'SandyBrown', 45 | 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 46 | 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow', 47 | 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 48 | 'WhiteSmoke', 'Yellow', 'YellowGreen' 49 | ] 50 | 51 | COLOR_MAP = { 52 | 'face':'Crimson', 'bicycle':'BlueViolet', 'bus':'Gold', 'car':'DodgerBlue', 53 | 'motorbike':'OrangeRed', 'person':'Chartreuse' 54 | } 55 | 56 | def getRandomColor(): 57 | """Generate random color 58 | 59 | Returns: 60 | -------- 61 | color : tuple(int, int, int) 62 | generated random color 63 | """ 64 | 65 | color = tuple([int(255*rnd.random()) for _ in range(3)]) 66 | return color 67 | 68 | 69 | def plotBBoxes(image, bboxes, classes=None, scores=None, color='Chartreuse', linewidth=2, use_rgb=False): 70 | """Plot bounding boxes for given input objects 71 | 72 | Arguments: 73 | ---------- 74 | image : numpy.array 75 | input image for drawing 76 | bboxes : list((x1, y1, x2, y2)) 77 | input bounding boxes of objects to draw 78 | 79 | Keyword Arguments: 80 | ------------------ 81 | classes : list(str) (default: None) 82 | list of classes for objects 83 | color : str (default: `Chartreuse`) 84 | color to plot 85 | linewidth : int (default: 2) 86 | how thick the shape is 87 | use_rgb : bool (default: False) 88 | whether using RGB image or not (apply for NDArray image) 89 | 90 | Returns: 91 | -------- 92 | image : numpy.array 93 | output image after drawing 94 | """ 95 | 96 | if len(bboxes) == 0: 97 | return image 98 | exit() 99 | 100 | if isinstance(image, np.ndarray): 101 | if not use_rgb: 102 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 103 | image = Image.fromarray(image) 104 | 105 | W, H = image.size 106 | draw = ImageDraw.Draw(image) 107 | 108 | if classes is not None: 109 | font = ImageFont.truetype(font=cfg.FONT, 110 | size=np.floor(3e-2*min(H, W) + 0.5).astype('int32') ) 111 | 112 | for i, ((x1, y1, x2, y2), cls) in enumerate(zip(bboxes, classes)): 113 | # draw bounding box 114 | if cls in COLOR_MAP: 115 | draw.rectangle([x1, y1, x2, y2], 116 | outline=ImageColor.getrgb(COLOR_MAP[cls]), 117 | width=linewidth) 118 | else: 119 | draw.rectangle([x1, y1, x2, y2], 120 | outline=ImageColor.getrgb('Chartreuse'), 121 | width=linewidth) 122 | 123 | # draw label 124 | text = cls 125 | if scores is not None: 126 | text = '{} {:.2f}'.format(cls, scores[i]) 127 | 128 | label_size = draw.textsize(text, font) 129 | text_coor = np.array([x1+linewidth, max(0, y1 - label_size[1] - 1)]) 130 | rec_coor = np.array([x1, text_coor[1]]) 131 | 132 | if cls in COLOR_MAP: 133 | draw.rectangle([tuple(rec_coor), 134 | tuple(rec_coor + label_size + np.array([linewidth*2, 0])) ], 135 | fill=ImageColor.getrgb(COLOR_MAP[cls])) 136 | else: 137 | draw.rectangle([tuple(rec_coor), 138 | tuple(rec_coor + label_size + np.array([linewidth*2, 0])) ], 139 | fill=ImageColor.getrgb('Chartreuse')) 140 | draw.text(text_coor, text, fill=ImageColor.getrgb('black'), font=font) 141 | else: 142 | for i, (x1, y1, x2, y2) in enumerate(bboxes): 143 | draw.rectangle([x1, y1, x2, y2], 144 | outline=ImageColor.getrgb(color), 145 | width=linewidth) 146 | 147 | del draw 148 | return image 149 | 150 | 151 | def plotInfo(image, info, color='Chartreuse', use_rgb=False): 152 | if isinstance(image, np.ndarray): 153 | if not use_rgb: 154 | image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 155 | image = Image.fromarray(image) 156 | 157 | W, H = image.size 158 | draw = ImageDraw.Draw(image) 159 | 160 | font = ImageFont.truetype(font=cfg.FONT, 161 | size=np.floor(5e-2*min(H, W) + 0.5).astype('int32')) 162 | 163 | label_size = draw.textsize(info, font) 164 | text_coor = np.array([5, 0]) 165 | rec_coor = np.array([0, 0]) 166 | draw.rectangle([tuple(rec_coor), 167 | tuple(rec_coor + label_size + np.array([10, 0])) ], 168 | fill=ImageColor.getrgb(color)) 169 | draw.text(text_coor, info, fill=ImageColor.getrgb('black'), font=font) 170 | 171 | del draw 172 | return image 173 | -------------------------------------------------------------------------------- /012-tflite-object-detection/app-image.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import re 5 | import numpy as np 6 | import cv2 as cv 7 | 8 | from tflite_runtime.interpreter import Interpreter 9 | 10 | from altusi import Logger, imgproc, helper 11 | from altusi import config as cfg, visualizer as vis 12 | 13 | LOG = Logger(__file__.split('.')[0]) 14 | 15 | 16 | def load_labels(path): 17 | labels = {} 18 | with open(path, 'r') as f: 19 | for line in f.readlines(): 20 | _ = line.index(' ') 21 | idx, cls = int(line[:_]), line[_:].strip() 22 | labels[idx] = cls 23 | 24 | return labels 25 | 26 | 27 | def set_input_tensor(interpreter, image): 28 | tensor_idx = interpreter.get_input_details()[0]['index'] 29 | input_tensor = interpreter.tensor(tensor_idx)()[0] 30 | input_tensor[:, :] = image 31 | 32 | 33 | def get_output_tensor(interpreter, idx): 34 | output_details = interpreter.get_output_details()[idx] 35 | tensor = np.squeeze(interpreter.get_tensor(output_details['index'])) 36 | return tensor 37 | 38 | 39 | def detect_objects(interpreter, image, frame_size, thresh=0.5): 40 | H, W = frame_size 41 | set_input_tensor(interpreter, image) 42 | interpreter.invoke() 43 | 44 | bboxes = get_output_tensor(interpreter, 0) 45 | class_ids = get_output_tensor(interpreter, 1) 46 | scores = get_output_tensor(interpreter, 2) 47 | idxes = np.where(scores >= thresh) 48 | 49 | for i, bbox in enumerate(bboxes): 50 | bboxes[i][0], bboxes[i][1] = bboxes[i][1], bboxes[i][0] 51 | bboxes[i][2], bboxes[i][3] = bboxes[i][3], bboxes[i][2] 52 | 53 | return scores[idxes], class_ids[idxes], bboxes[idxes] * np.array((W, H, W, H)) 54 | 55 | 56 | def app(image_path): 57 | 58 | labels = load_labels(cfg.LABEL_PATH) 59 | 60 | interpreter = Interpreter(cfg.DETECT_MODEL_PATH) 61 | interpreter.allocate_tensors() 62 | _, input_H, input_W, _ = interpreter.get_input_details()[0]['shape'] 63 | 64 | for i in range(5): 65 | image = cv.imread(image_path) 66 | assert image is not None, 'Invalid image path' 67 | 68 | _start_t = time.time() 69 | inp_image = cv.cvtColor(image, cv.COLOR_BGR2RGB) 70 | inp_image = cv.resize(inp_image, (input_W, input_H)) 71 | scores, class_ids, bboxes = detect_objects(interpreter, 72 | inp_image, image.shape[:2], thresh=0.6) 73 | _prx_t = time.time() - _start_t 74 | LOG.info('FPS: {:.3f}'.format(1/_prx_t)) 75 | 76 | image = vis.plotBBoxes(image, bboxes.astype('int'), 77 | classes=[labels[idx] for idx in class_ids], 78 | scores=scores) 79 | image = vis.plotInfo(image, 'Raspberry Pi - FPS: {:.2f}'.format(1/_prx_t)) 80 | image = cv.cvtColor(np.asarray(image), cv.COLOR_BGR2RGB) 81 | 82 | cv.imwrite('output.jpg', image) 83 | 84 | 85 | def main(args): 86 | app(args.image) 87 | 88 | 89 | if __name__ == '__main__': 90 | LOG.info('Raspberry Pi: Object Detection with TFLite\n') 91 | 92 | args = helper.getArgs() 93 | main(args) 94 | 95 | LOG.info('Process done') 96 | -------------------------------------------------------------------------------- /012-tflite-object-detection/app-video.py: -------------------------------------------------------------------------------- 1 | """ 2 | Object Detection with Tensorflow Lite 3 | ------------------------------------- 4 | 5 | Reference: https://github.com/tensorflow/examples/blob/master/lite/examples/object_detection/raspberry_pi/README.md 6 | """ 7 | 8 | import os 9 | import time 10 | 11 | import re 12 | import numpy as np 13 | import cv2 as cv 14 | 15 | from tflite_runtime.interpreter import Interpreter 16 | 17 | from altusi import Logger, imgproc, helper 18 | from altusi import config as cfg, visualizer as vis 19 | 20 | LOG = Logger(__file__.split('.')[0]) 21 | 22 | 23 | def load_labels(path): 24 | labels = {} 25 | with open(path, 'r') as f: 26 | for line in f.readlines(): 27 | _ = line.index(' ') 28 | idx, cls = int(line[:_]), line[_:].strip() 29 | labels[idx] = cls 30 | 31 | return labels 32 | 33 | 34 | def set_input_tensor(interpreter, image): 35 | tensor_idx = interpreter.get_input_details()[0]['index'] 36 | input_tensor = interpreter.tensor(tensor_idx)()[0] 37 | input_tensor[:, :] = image 38 | 39 | 40 | def get_output_tensor(interpreter, idx): 41 | output_details = interpreter.get_output_details()[idx] 42 | tensor = np.squeeze(interpreter.get_tensor(output_details['index'])) 43 | return tensor 44 | 45 | 46 | def detect_objects(interpreter, image, frame_size, thresh=0.5): 47 | H, W = frame_size 48 | set_input_tensor(interpreter, image) 49 | interpreter.invoke() 50 | 51 | bboxes = get_output_tensor(interpreter, 0) 52 | class_ids = get_output_tensor(interpreter, 1) 53 | scores = get_output_tensor(interpreter, 2) 54 | idxes = np.where(scores >= thresh) 55 | 56 | for i, bbox in enumerate(bboxes): 57 | bboxes[i][0], bboxes[i][1] = bboxes[i][1], bboxes[i][0] 58 | bboxes[i][2], bboxes[i][3] = bboxes[i][3], bboxes[i][2] 59 | 60 | return scores[idxes], class_ids[idxes], bboxes[idxes] * np.array((W, H, W, H)) 61 | 62 | 63 | def app(video_link, video_name, show=False, record=False): 64 | # initialize TFLite model 65 | labels = load_labels(cfg.LABEL_PATH) 66 | 67 | interpreter = Interpreter(cfg.DETECT_MODEL_PATH) 68 | interpreter.allocate_tensors() 69 | _, input_H, input_W, _ = interpreter.get_input_details()[0]['shape'] 70 | 71 | # initialize video capturer 72 | cap = cv.VideoCapture(video_link) 73 | (W, H), FPS = imgproc.cameraCalibrate(cap) 74 | FRM_MOD = int(FPS / cfg.pFPS + 0.5) 75 | LOG.info('Camera Info: ({}, {}) - {:.3f}'.format(W, H, FPS)) 76 | 77 | if record: 78 | writer = imgproc.getVideoRecorder(video_name, (W, H), FPS) 79 | 80 | frm_cnt = 0 81 | while cap.isOpened(): 82 | _, frm = cap.read() 83 | if not _: 84 | LOG.info('Reached the end of Video source') 85 | break 86 | 87 | frm_cnt += 1 88 | if frm_cnt % FRM_MOD: continue 89 | 90 | _start_t = time.time() 91 | inp_image = cv.cvtColor(frm, cv.COLOR_BGR2RGB) 92 | inp_image = cv.resize(inp_image, (input_W, input_H)) 93 | scores, class_ids, bboxes = detect_objects(interpreter, 94 | inp_image, frm.shape[:2], thresh=0.6) 95 | _prx_t = time.time() - _start_t 96 | LOG.info('FPS: {:.3f}'.format(1/_prx_t)) 97 | 98 | frm = vis.plotBBoxes(frm, bboxes.astype('int'), 99 | classes=[labels[idx] for idx in class_ids], 100 | scores=scores) 101 | frm = vis.plotInfo(frm, 'Raspberry Pi - FPS: {:.2f}'.format(1/_prx_t)) 102 | frm = cv.cvtColor(np.asarray(frm), cv.COLOR_BGR2RGB) 103 | 104 | if record: 105 | writer.write(frm) 106 | 107 | if show: 108 | cv.imshow(video_name, frm) 109 | key = cv.waitKey(1) 110 | if key in [27, ord('q')]: 111 | LOG.info('Interrupted by Users') 112 | break 113 | 114 | if record: 115 | writer.release() 116 | cap.release() 117 | cv.destroyAllWindows() 118 | 119 | 120 | def main(args): 121 | video_link = args.video if args.video else 0 122 | app(video_link, args.name, args.show, args.record) 123 | 124 | 125 | if __name__ == '__main__': 126 | LOG.info('Raspberry Pi: Object Detection with TFLite\n') 127 | 128 | args = helper.getArgs() 129 | main(args) 130 | 131 | LOG.info('Process done') 132 | -------------------------------------------------------------------------------- /012-tflite-object-detection/tflite-models: -------------------------------------------------------------------------------- 1 | /home/pi/workspace/tflite-models/ -------------------------------------------------------------------------------- /013-publish-docker/README.md: -------------------------------------------------------------------------------- 1 | # OpenCV Docker for Raspberry Pi 2 | 3 | OpenCV docker image for Raspberry Pi (arm32v7) can be pulled with: 4 | 5 | `docker pull danhdoan/opencv4-slim-buster:3.7.9` 6 | 7 | In this image, I built OpenCV 4.4.0 from source, and support Python 3 only (version 3.7.9) 8 | 9 | Image size: 907 MB (in later update, it will be smaller) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Computer Vision on Raspberry Pi 2 | 3 | by Danh Doan 4 | 5 | 6 | ## Introduction 7 | This is a series about developing common Computer Vision projects on Raspberry Pi board. Some of them requires the support of Movidius Neural Compute Stick to boost the performance. 8 | OpenVINO toolkit is mainly the development tool that helps optimize the hardware and models to work well with Raspi 9 | 10 | The main purpose of this work is to help developers from all levels to gain insights and resources to working with Raspberry Pi for Computer Vision projects. 11 | Every sample project is refactored and organized so that it is easily understandable and approachable. 12 | 13 | If you have any project ideas and issues with those projects, feel free to comment. 14 | It will help improve and enrich the contents. Thanks in advance with my sincere. 15 | 16 | **Other Computer Vision demos:** [[link]](https://www.youtube.com/watch?v=Suprnm2EiEE&list=PL9gpyuNNKEhJSAg8RxTrNj046GQJ1K9Q1) 17 | 18 | ## Updates 19 | 2021, Jan 04: 20 | * Publish Docker image for OpenCV [[link]](https://hub.docker.com/repository/docker/danhdoan/opencv4-slim-buster) 21 | 22 | **2019, Nov 27:** 23 | * Add 012-tflite-object-detection 24 | 25 | **2019, Nov 15:** 26 | * Add `openvino-models`: [[link]](https://drive.google.com/drive/folders/11G98FS2-klB4qGiz4YzEZfdnd0G2XNrL) 27 | 28 | **2019, Nov 05:** 29 | * Add 008-pi-emotion-recognition 30 | * Update installation guide to support IECore 31 | 32 | **2019, Oct 23:** 33 | * Add 004-pi-head-pose-estimation 34 | 35 | **2019, Oct 16:** 36 | * Add 006-pi-face-verification 37 | 38 | **2019, Oct 15:** 39 | * Add 003-pi-face-alignment 40 | 41 | **2019, Oct 12:** 42 | * Add 005-pi-object-detection 43 | * Add 002-pi-facial-landmark-detection 44 | * Add 001-pi-face-detection 45 | * Add 000-show-pi-camera 46 | 47 | ## Sample Projects 48 | * Test with Pi camera module: [[link]](https://github.com/danhdoan/computer-vision-raspberrypi/tree/master/000-show-pi-camera) 49 | * Play around with builtin Pi camera module 50 | * Face Detection with High Accuracy: [[link]](https://github.com/danhdoan/computer-vision-raspberrypi/tree/master/001-pi-face-detection) 51 | * Develop an accurate and robust Face detector with pretrained SSD model trained from WIDER dataset 52 | * Facial Keypoint Detection: [[link]](https://github.com/danhdoan/computer-vision-raspberrypi/tree/master/002-pi-facial-landmark-detection) [[demo]](https://www.youtube.com/watch?v=En_nsyF8kJM) [[demo]](https://www.youtube.com/watch?v=WzvgrhrDC1s) 53 | * Develop a simple Facial keypoints localization that detect 5 main keypoints of human faces (center eyes, nose tip, and mouth corner 54 | * Face Alignment: [[link]](https://github.com/danhdoan/computer-vision-raspberrypi/tree/master/003-pi-face-alignment) 55 | * Based on 5 keypoints, align human faces, to support other problem e.g. Face Identification, Face Verification, ... 56 | * Headpose Estimation: [[link]](https://github.com/danhdoan/computer-vision-raspberrypi/tree/master/004-pi-head-pose-estimation) [[demo]](https://www.youtube.com/watch?v=kN-QrA3h4oo) 57 | * Estimate Human head pose in Tait-Bryan angles (yaw, pitсh or roll) 58 | 59 | * Human Detection: [[link]](https://github.com/danhdoan/computer-vision-raspberrypi/tree/master/005-pi-object-detection) [[demo]](https://www.youtube.com/watch?v=Suprnm2EiEE) 60 | * Develop an Object detector especially with SSD model 61 | 62 | **Notice:** human is just an example of objects, any object detection model can be converted to work with this sample project 63 | 64 | * Face Verification: [[link]](https://github.com/danhdoan/computer-vision-raspberrypi/tree/master/006-pi-face-identification) 65 | 66 | Verify Face Identity by Face embeddings 67 | 68 | * Emotion Recognition: [[link]](https://github.com/danhdoan/computer-vision-raspberrypi/tree/master/008-pi-emotion-recognition) [[demo]](https://www.youtube.com/watch?v=RXCuG3I1Mkw) 69 | 70 | Recognize Emotional states of Human faces 71 | 72 | * Car and License Plate Detection: [ongoing] 73 | 74 | * TFLite Object Detection: [[link]](https://github.com/danhdoan/computer-vision-raspberrypi/tree/master/012-tflite-object-detection) [[demo]](https://www.youtube.com/watch?v=ncDyjjNTd5w) 75 | 76 | 77 | 78 | ## Installation 79 | 80 | Follow `install.md` instructions [[link]](https://github.com/danhdoan/computer-vision-raspberrypi/blob/master/install.md) to install essential packages and modules for working with Raspberry Pi. Installing OpenVINO as a toolkit to the development. 81 | 82 | ## Usage 83 | 1. Clone this repository: 84 | 85 | `cd ~` 86 | 87 | `mkdir workspace && cd workspace` 88 | 89 | `git clone https://github.com/danhdoan/computer-vision-raspberrypi` 90 | 91 | 2. Download OpenVINO pretrained-model 92 | 93 | `mkdir openvino-models tflite-models` 94 | 95 | You can notice a soft symbol link in any projects that maps to this directory. If you want to store it elsewhere, beware of re-map this symbol link. 96 | To download a model, just go to the official OpenVINO site from Intel: 97 | 98 | https://download.01.org/opencv/2019/open_model_zoo/R1/ 99 | 100 | In this project, models from R1 sub-dir are used. R3 is currently the latest, it also works well with sample code. 101 | Just download the FP16 models, they can be applied to Raspberry Pi. In my code, I usually add a postfix `-fp16` to clarify this issue. Thus, download a pretrained model and change its name correspondingly. 102 | 103 | 3. Run a sample project 104 | 105 | All sample projects have the same argument parser 106 | 107 | usage: .py [-h] [--video VIDEO] [--name NAME] [--show] 108 | [--record] [--flip_hor] [--flip_ver] 109 | optional arguments: 110 | -h, --help show this help message and exit 111 | --video VIDEO, -v VIDEO 112 | Video Streamming link or Path to video source 113 | --name NAME, -n NAME Name of video source 114 | --show, -s Whether to show the output visualization 115 | --record, -r Whether to save the output visualization 116 | --flip_hor, -fh horizontally flip video frame 117 | --flip_ver, -fv vertically flip video frame 118 | 119 | It is implemented in `altusi/helper.py`, you can customize as you wanted. To see argument list of a sample, e.g. `object-detection`, just type: 120 | 121 | python3 app-object-detector.py -h 122 | 123 | ## References 124 | * OpenVINO model zoo: https://download.01.org/opencv/2019/open_model_zoo/R3/ 125 | * Official Intel site: 126 | * [https://docs.openvinotoolkit.org/latest/_demos_README.html](https://docs.openvinotoolkit.org/latest/_demos_README.html) 127 | * https://docs.openvinotoolkit.org/latest/_models_intel_index.html 128 | * Raspberry Pi and OpenVINO installation: https://www.hackster.io/news/getting-started-with-the-intel-neural-compute-stick-2-and-the-raspberry-pi-6904ccfe963 129 | 130 | -------------------------------------------------------------------------------- /install.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Environment Setup Guide 2 | by Danh Doan 3 | 4 | 5 | ## Setup Raspbian OS 6 | 1. Download Raspbian image from official site [[link]](https://www.raspberrypi.org/downloads/raspbian/) 7 | 8 | Currently use Raspbian Stretch with Desktop - Release date: 2019-08-04 9 | 10 | 2. Write image to SD card using balenaEtcher [[link]](https://www.balena.io/etcher/) 11 | 12 | 3. Insert SD card to Raspberry Pi and power it on 13 | 14 | After booting, configure Timezone and Language, then update system 15 | 16 | 4. Enable Peripherals e.g. SSH, Picamera, Audio or Communication protocols: I2C, SPI 17 | 18 | `sudo raspi-config` 19 | 20 | Select `Interfacing Options` and `Advanced Options` to enable 21 | 22 | 5. Install basic packages 23 | 24 | `sudo apt clean && sudo apt autoremove` 25 | 26 | `sudo apt update && sudo apt upgrade` 27 | 28 | `sudo apt install git cmake vim-gtk gedit` 29 | 30 | ## Setup OpenVINO toolkit 31 | Follow the installation guide in this blog [[link]](https://blog.hackster.io/getting-started-with-the-intel-neural-compute-stick-2-and-the-raspberry-pi-6904ccfe963) 32 | 33 | 1. Download and Extract OpenVINO toolkit 34 | 35 | `cd ~` 36 | 37 | `mkdir openvino` 38 | 39 | `cd openvino` 40 | 41 | `wget https://download.01.org/opencv/2019/openvinotoolkit/R3/l_openvino_toolkit_runtime_raspbian_p_2019.3.334.tgz` 42 | 43 | `tar -xvf l_openvino_toolkit_runtime_raspbian_p_2019.3.334.tgz` 44 | 45 | `mv l_openvino_toolkit_runtime_raspbian_p_2019.3.334/* .` 46 | 47 | 2. Modify Installation Dir in setup script 48 | 49 | `sed -i "s||$(pwd)/|" bin/setupvars.sh` 50 | 51 | 3. Add to .bashrc 52 | 53 | `source /home/pi/openvino/bin/setupvars.sh` 54 | 55 | `source ~/.bashrc` 56 | 57 | 58 | 4. Update USB rule for Pi to recognize NCS 59 | 60 | `sudo usermod -a -G users "$(whoami)"` 61 | 62 | `sh ~/openvino/install_dependencies/install_NCS_udev_rules.sh` 63 | 64 | 65 | ## Install support packages for Computer Vision 66 | 67 | `sudo apt install python3-picamera` 68 | --------------------------------------------------------------------------------