中国 · 南京 · 栖霞区紫东路2号紫东国际创意园B3-2幢5F
+86-18994094214 (仅工作日:8:30~17:30)
contact@oakchina.cn

OpenCV CEO教你用OAK(四):创建复杂的管道

OpenCV CEO教你用OAK(四):创建复杂的管道

1.介绍

这是此系列的第四篇文章,前三篇文章内容如下:

在这里,我们将仔细看看DepthAI管道(pipeline),它的不同节点(nodes),以及这些单独的节点如何组合在一起创建一个工作系统。

在之前的文章中,我们已经看到了一些起作用的节点。在这里,我们将更多地关注那些我们到目前为止还没有尝试过的方法。

2.管道

来自DepthAI的任何动作,无论是神经推理还是彩色相机输出,都需要定义一个管道,包括对应我们需求的节点和连接。

在管道中,不同的节点相互连接,数据以定义的顺序从一个节点流向另一个节点,同时在每个节点对数据执行不同的操作。

这里有一个简单的管道来获得OAK-D上的黑白相机模块的输出。

首先在主机上创建管道,定义节点之间的连接和数据流。然后,在初始化期间,定义的管道被发送到OAK设备,并使用来自摄像机和其他模块的所需输入来运行,输出被发送回主机。

让我们看看可供我们使用的不同节点,以及我们如何使用它们。

3.节点

到目前为止,我们知道节点是DepthAI管道的构建块,但节点本身是什么?

你可以将每个节点视为一个函数,它对一组给定的输入应用一些操作,并产生一组输出。

现在,这些节点可以链接在一起,这样一个节点的输出就可以作为管道中另一个节点的输入。

我们已经在以前的文章中看到了一些节点,如StereoDepth、MonoCamera和其他NN节点。让我们看看更多可供我们使用的节点,以及每个节点的用途。

3.1 XLinkIn/XLinkOut

XLinkIn和XLinkOut是两个最基本的节点,它们使主机能够与OAK设备进行交互。

XLinkIn

XLinkIn节点用于从主机向设备发送数据。

输入

任何输入都可以从主机端发送到设备。

输出

节点的输出是来自主计算机的可以在管道中使用的未改变的输入。

语法

pipeline = dai.Pipeline()
xlinkIn = pipeline.create(dai.node.XLinkIn)

XLinkOut

XLinkOut节点用于将数据从设备发送到主机。

输入

任何输入都可以从设备发送到主机端。

输出

节点的输出是主机可以使用的来自设备计算机的未改变的输入。

语法

pipeline = dai.Pipeline()
xlinkOut = pipeline.create(dai.node.XLinkOut)

3.2 彩色相机

ColorCamera节点是从RGB相机模块捕获的图像帧的来源。

可以使用InputControl和InputConfig在运行时控制和操作来自节点的输出图像帧,这可以用于更改不同的参数,如白平衡、曝光、聚焦等。

Camera节点本身内部有不同的组件:

1.图像传感器

正是物理图像传感器捕捉光线并产生原始图像帧。

2.ISP(图像信号处理器)

ISP与图像传感器通信,用于Bayer变换、去马赛克、降噪和其他图像增强。它还根据3A算法(自动对焦、自动曝光和自动白平衡)处理图像传感器调整,如曝光时间、感光度(ISO)和镜头位置。

3.后处理器Postprocessor

它将来自ISP的平面帧转换为视频/预览/静止帧。

输入

  1. inputConfig – ImageManipConfig
  2. inputControl – CameraControl

输出

  1. raw–img frame–raw 10 Bayer数据。
  2. ISP–img frame–YUV 420 planar(与YU12/IYUV/I420相同)
  3. still–img frame–NV12,适合较大尺寸的帧。当一个捕获事件被发送到ColorCamera时,这个图像就被创建了,所以它就像照一张照片。
  4. preview–img frame–RGB(或BGR平面/交错,如果已配置),最适合小尺寸预览并将图像输入到神经网络。
  5. video–img frame–NV12,适合较大尺寸的帧。

语法

pipeline = dai.Pipeline()
cam = pipeline.create(dai.node.ColorCamera)

演示

ColorCamera output

3.3 边缘检测器EdgeDetector

边缘检测器节点使用Sobel滤波器来强调和查找图像帧中的边缘。

输入

  1. inputImage – ImgFrame
  2. inputConfig – EdgeDetectorConfig

输出

  1. output image–输出带有强调边缘的图像帧

语法

pipeline = dai.Pipeline()
edgeDetector = pipeline.create(dai.node.EdgeDetector)

演示

3.4 特征跟踪器FeatureTracker

FeatureTracker检测一帧上的关键点(特征),并在下一帧跟踪它们。

有效特征从Harris score或Shi-Tomasi中获得。目标要素的默认数量为320,最大默认数量为480。

它目前支持的分辨率:720p和480p。

输入

  1. inputConfig – FeatureTrackerConfig
  2. inputImage – ImgFrame

输出:

  1. outputFeatures – TrackedFeatures
  2. passthroughInputImage–img frame

语法

pipeline = dai.Pipeline()
featureTracker = pipeline.create(dai.node.FeatureTracker)

演示

3.5 ImageManip

ImageManip节点可用于裁剪、旋转矩形区域或在图像帧上执行各种图像变换,如旋转、镜像、翻转、透视变换。

输入

  1. inputImage – ImgFrame
  2. inputConfig – ImageManipConfig

输出

  1. Modified Image Frame

语法

pipeline = dai.Pipeline()manip = pipeline.create(dai.node.ImageManip)

演示

旋转90度的图像帧

3.6 IMU

IMU(惯性测量单元)节点可用于接收来自器件上IMU芯片的数据。

DepthAI设备使用BNO085 9轴传感器,支持(IMU(芯片本身的传感器融合。IMU芯片具有多个板载传感器,即加速度计、陀螺仪和磁力计。

注:OAK-D 配有IMU传感器,OAK-D-Lite 没有。如果你想使用IMU传感器,建议你在购买前检查设备规格。

输入

这个节点不需要任何输入。

输出

  1. IMU数据

语法

pipeline = dai.Pipeline()imu = pipeline.create(dai.node.IMU)

演示

3.7 对象跟踪器ObjectTracker

对象跟踪器节点从检测输入中跟踪检测到的对象。它使用卡尔曼滤波器和匈牙利算法进行跟踪。

输入

  1. inputDetectionFrame–img frame
  2. inputTrackerFrame–img frame
  3. inputDetections – ImgDetections

输出

  1. out – Tracklets
  2. passthroughDetectionFrame – ImgFrame
  3. passthroughTrackerFrame – ImgFrame
  4. passthroughDetections – ImgDetections

语法:

pipeline = dai.Pipeline()objectTracker = pipeline.create(dai.node.ObjectTracker)

演示

3.8 脚本Script

脚本节点允许用户在设备上运行自定义Python脚本。由于计算资源的限制,脚本节点不应该用于繁重的计算(例如,图像处理/CV),而是用于管理管道的流动。

示例用例包括控制ImageManip、ColorCamera、SpatialLocationCalculator等节点、解码NeuralNetwork结果或与GPIOs接口。

输入/输出

用户可以根据需要定义任意多的输入和输出,输入和输出可以是DepthAI支持的任何Message类型。

语法

pipeline = dai.Pipeline()script = pipeline.create(dai.node.Script)

3.9 空间位置计算器SpatialLocationCalculator

SpatialLocationCalculator可以基于来自inputDepth的深度图和由inputConfig提供的ROI(region-of-interest)来计算深度。

该节点将对ROI中的深度值进行平均,并移除超出范围的深度值。

输入

  1. inputConfig – SpatialLocationCalculatorConfig
  2. inputDepth – ImgFrame

输出

  1. out – SpatialLocationCalculatorData
  2. passthroughDepth – ImgFrame

语法

pipeline = dai.Pipeline()spatialCalc = pipeline.SpatialLocationCalculator()

演示

3.10 视频编码器VideoEncoder

VideoEncoder节点用于将图像帧编码成H264/H265/JPEG,这可用于保存设备的视频输出。

输入

  1. input – ImgFrame

输出:

  1. bitstream – ImgFrame

语法:

pipeline = dai.Pipeline()encoder = pipeline.create(dai.node.VideoEncoder)

3.11 系统记录器SystemLogger

SystemLogger节点用于获取设备的系统信息。

该节点提供设备上所有资源(如内存和存储)的使用信息,它还提供处理器利用率和芯片温度等统计数据。

输出

  1. out – SystemInformation

语法:

pipeline = dai.Pipeline()logger = pipeline.create(dai.node.SystemLogger)

演示

4.通过链接节点实现复杂管道

现在我们已经了解了不同的DepthAI节点以及如何创建一个简单的管道。让我们更进一步,利用这些知识创建一个稍微复杂的管道。

我们将创建一个管道来进行人脸检测,此外,在检测人脸区域进行地标检测。所有这些都将在设备上完成,我们将只获得管道的最终输出,即检测到的面部边界框和检测到的界标。

在查看代码之前,让我们先了解一下管道的概况,以及管道中使用的不同节点是如何连接的。

4.1 代码(面部检测+面部标志检测)

导入所需模块

import cv2
import depthai as dai
import time
import blobconverter

定义帧大小

FRAME_SIZE = (640, 400)

定义检测神经网络模型名称和输入大小

我们将使用来自Depthai model zoo的“face-detection-retail-0004”模型。

它是一个人脸检测模型,给出图像帧中检测到的人脸的边界框坐标。

# If you define the blob make sure the FACE_MODEL_NAME and ZOO_TYPE are None
DET_INPUT_SIZE = (300, 300)
FACE_MODEL_NAME = "face-detection-retail-0004"
ZOO_TYPE = "depthai"
blob_path = None

定义地标神经网络模型名称和输入大小

我们将使用Open Model Zoo中的”landmarks-regression-retail-0009”模型。

这是一个面部标志检测模型,提供了面部的五个标志:左眼,右眼,鼻子,嘴唇的左角和嘴唇的右角。

# If you define the blob make sure the LANDMARKS_MODEL_NAME and ZOO_TYPE are None
LANDMARKS_INPUT_SIZE = (48, 48)
LANDMARKS_MODEL_NAME = "landmarks-regression-retail-0009"
LANDMARKS_ZOO_TYPE = "intel"
landmarks_blob_path = None

开始定义管道

pipeline = dai.Pipeline()

定义源-RGB相机

cam = pipeline.createColorCamera()
cam.setPreviewSize(FRAME_SIZE[0], FRAME_SIZE[1])
cam.setInterleaved(False)
cam.setResolution(dai.ColorCameraProperties.SensorResolution.THE_1080_P)
cam.setBoardSocket(dai.CameraBoardSocket.RGB)

定义用于人脸检测的神经网络节点

# Convert model from OMZ to blob
if FACE_MODEL_NAME is not None:
    blob_path = blobconverter.from_zoo(
        name=FACE_MODEL_NAME,
        shaves=6,
        zoo_type=ZOO_TYPE
    )

# Define face detection NN node
faceDetNn = pipeline.createMobileNetDetectionNetwork()
faceDetNn.setConfidenceThreshold(0.75)
faceDetNn.setBlobPath(blob_path)

为地标检测模型定义神经网络节点

# Convert model from OMZ to blob
if LANDMARKS_MODEL_NAME is not None:
    landmarks_blob_path = blobconverter.from_zoo(
        name=LANDMARKS_MODEL_NAME,
        shaves=6,
        zoo_type=LANDMARKS_ZOO_TYPE
    )

# Define landmarks detection NN node
landmarksDetNn = pipeline.createNeuralNetwork()
landmarksDetNn.setBlobPath(landmarks_blob_path)

定义用于人脸和地标检测的ImageManip节点

# Define face detection input config
faceDetManip = pipeline.createImageManip()
faceDetManip.initialConfig.setResize(DET_INPUT_SIZE[0], DET_INPUT_SIZE[1])
faceDetManip.initialConfig.setKeepAspectRatio(False)

# Define landmark detection input config
lndmrksDetManip = pipeline.createImageManip()

链接相机、人脸图像映射和人脸检测神经网络节点

# Linking
cam.preview.link(faceDetManip.inputImage)
faceDetManip.out.link(faceDetNn.input)

定义脚本节点

脚本节点将把面部检测神经网络的输出作为输入,并为界标神经网络设置ImageManipConfig。

script = pipeline.create(dai.node.Script)
script.setProcessor(dai.ProcessorType.LEON_CSS)
script.setScriptPath("script.py")

script.py

import time


# Correct the bounding box
def correct_bb(bb):
    bb.xmin = max(0, bb.xmin)
    bb.ymin = max(0, bb.ymin)
    bb.xmax = min(bb.xmax, 1)
    bb.ymax = min(bb.ymax, 1)
    return bb


# Main loop
while True:
    time.sleep(0.001)

    # Get image frame
    img = node.io['frame'].get()

    # Get detection output
    face_dets = node.io['face_det_in'].tryGet()
    if face_dets and img is not None:

        # Loop over all detections
        for det in face_dets.detections:

            # Correct bounding box
            correct_bb(det)
            node.warn(f"New detection {det.xmin}, {det.ymin}, {det.xmax}, {det.ymax}")

            # Set config parameters
            cfg = ImageManipConfig()
            cfg.setCropRect(det.xmin, det.ymin, det.xmax, det.ymax)
            cfg.setResize(48, 48)
            cfg.setKeepAspectRatio(False)

            # Output image and config
            node.io['manip_cfg'].send(cfg)
            node.io['manip_img'].send(img)

链接到脚本输入

cam.preview.link(script.inputs['frame'])
faceDetNn.out.link(script.inputs['face_det_in'])

将脚本输出链接到地标ImageManipconfig

script.outputs['manip_cfg'].link(lndmrksDetManip.inputConfig)
script.outputs['manip_img'].link(lndmrksDetManip.inputImage)

将ImageManip输出链接到Landmark NN

lndmrksDetManip.out.link(landmarksDetNn.input)

创建输出流

# Create preview output
xOutPreview = pipeline.createXLinkOut()
xOutPreview.setStreamName("preview")
cam.preview.link(xOutPreview.input)

# Create face detection output
xOutDet = pipeline.createXLinkOut()
xOutDet.setStreamName('det_out')
faceDetNn.out.link(xOutDet.input)

# Create cropped face output
xOutCropped = pipeline.createXLinkOut()
xOutCropped.setStreamName('face_out')
lndmrksDetManip.out.link(xOutCropped.input)

# Create landmarks detection output
xOutLndmrks = pipeline.createXLinkOut()
xOutLndmrks.setStreamName('lndmrks_out')
landmarksDetNn.out.link(xOutLndmrks.input)

在图像框上显示信息

def display_info(frame, bbox, landmarks, status, status_color, fps):
    # Display bounding box
    cv2.rectangle(frame, bbox, status_color[status], 2)

    # Display landmarks
    if landmarks is not None:
        for landmark in landmarks:
            cv2.circle(frame, landmark, 2, (0, 255, 255), -1)

    # Create background for showing details
    cv2.rectangle(frame, (5, 5, 175, 100), (50, 0, 0), -1)

    # Display authentication status on the frame
    cv2.putText(frame, status, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.5, status_color[status])

    # Display instructions on the frame
    cv2.putText(frame, f'FPS: {fps:.2f}', (20, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255))

定义一些我们将在主循环中使用的变量

# Frame count
frame_count = 0

# Placeholder fps value
fps = 0

# Used to record the time when we processed last frames
prev_frame_time = 0

# Used to record the time at which we processed current frames
new_frame_time = 0

# Set status colors
status_color = {
    'Face Detected': (0, 255, 0),
    'No Face Detected': (0, 0, 255)
}

主循环

我们启动管道,从“预览”队列中获取视频帧。我们还分别从“det_out”和“lndmrks_out”队列获得面部和标志检测输出。

一旦我们有了边界框和界标输出,我们就在图像帧上显示它们。

# Start pipeline
with dai.Device(pipeline) as device:

    # Output queue will be used to get the right camera frames from the outputs defined above
    qCam = device.getOutputQueue(name="preview", maxSize=1, blocking=False)

    # Output queue will be used to get nn detection data from the video frames.
    qDet = device.getOutputQueue(name="det_out", maxSize=1, blocking=False)

    # Output queue will be used to get cropped face region.
    qFace = device.getOutputQueue(name="face_out", maxSize=1, blocking=False)

    # Output queue will be used to get landmarks from the face region.
    qLndmrks = device.getOutputQueue(name="lndmrks_out", maxSize=1, blocking=False)

    while True:
        # Get camera frame
        inCam = qCam.get()
        frame = inCam.getCvFrame()

        bbox = None

        inDet = qDet.tryGet()

        if inDet is not None:
            detections = inDet.detections

            # if face detected
            if len(detections) is not 0:
                detection = detections[0]

                # Correct bounding box
                xmin = max(0, detection.xmin)
                ymin = max(0, detection.ymin)
                xmax = min(detection.xmax, 1)
                ymax = min(detection.ymax, 1)

                # Calculate coordinates
                x = int(xmin*FRAME_SIZE[0])
                y = int(ymin*FRAME_SIZE[1])
                w = int(xmax*FRAME_SIZE[0]-xmin*FRAME_SIZE[0])
                h = int(ymax*FRAME_SIZE[1]-ymin*FRAME_SIZE[1])

                bbox = (x, y, w, h)

        # Show cropped face region
        inFace = qFace.tryGet()
        if inFace is not None:
            face = inFace.getCvFrame()
            cv2.imshow("face", face)

        landmarks = None

        # Get landmarks NN output
        inLndmrks = qLndmrks.tryGet()
        if inLndmrks is not None:
            # Get NN layer names
            # print(f"Layer names: {inLndmrks.getAllLayerNames()}")

            # Retrieve landmarks from NN output layer
            landmarks = inLndmrks.getLayerFp16("95")

            x_landmarks = []
            y_landmarks = []

            # Landmarks in following format [x1,y1,x2,y2,..]
            # Extract all x coordinates [x1,x2,..]
            for x_point in landmarks[::2]:
                # Get x coordinate on original frame
                x_point = int((x_point * w) + x)
                x_landmarks.append(x_point)

            # Extract all y coordinates [y1,y2,..]
            for y_point in landmarks[1::2]:
                # Get y coordinate on original frame
                y_point = int((y_point * h) + y)
                y_landmarks.append(y_point)

            # Zip x & y coordinates to get a list of points [(x1,y1),(x2,y2),..]
            landmarks = list(zip(x_landmarks, y_landmarks))

        # Check if a face was detected in the frame
        if bbox:
            # Face detected
            status = 'Face Detected'
        else:
            # No face detected
            status = 'No Face Detected'

        # Display info on frame
        display_info(frame, bbox, landmarks, status, status_color, fps)

        # Calculate average fps
        if frame_count % 10 == 0:
            # Time when we finish processing last 100 frames
            new_frame_time = time.time()

            # Fps will be number of frame processed in one second
            fps = 1 / ((new_frame_time - prev_frame_time)/10)
            prev_frame_time = new_frame_time

        # Capture the key pressed
        key_pressed = cv2.waitKey(1) & 0xff

        # Stop the program if Esc key was pressed
        if key_pressed == 27:
            break

        # Display the final frame
        cv2.imshow("Face Cam", frame)

        # Increment frame count
        frame_count += 1

# Close all output windows
cv2.destroyAllWindows

5.效果演示

Landmarks output

6.结论

在这篇文章中,我们概述了DepthAI管道的不同部分,以及如何使用它们在节点之间创建连接来完成复杂的任务。

在下一篇文章中,我们将使用目前为止所学的一切,创建一个有趣的应用程序。

文章来源:learnopencv.com

Tags:

索引