VPI - 视觉编程接口

3.2 版本

KLT 特征跟踪器

概述

Kanade-Lucas-Tomasi (KLT) 特征跟踪器算法使用逆合成算法,估计图像模板在原始模板坐标和给定参考图像之间的 2D 平移和尺度变化。 更多信息,请参阅 [1]

输入是模板边界框数组、平移和尺度变化预测数组以及参考图像。 此外,模板图像输入用于更新模板补丁(详见下文)。

输出是从输入边界框坐标到参考图像坐标的平移和尺度变化估计数组,以及参考图像中的模板边界框坐标数组。

跟踪结果
注意
视频输出需要支持 HTML5 且支持 H.264 mp4 视频解码的浏览器。

实现

每个模板边界框定义一个模板图像补丁,该补丁与函数描述符一起在内部存储。 这些模板补丁基于预测的平移和尺度变化在参考图像中进行跟踪。 计算从原始边界框坐标到参考图像坐标的估计平移和尺度变化。 每个这样的估计都包括一个跟踪有效性标志(跟踪成功或失败)以及是否需要模板更新(基于用户定义的阈值参数)。

C API 函数

有关实现该算法的限制、约束和后端的列表,请查阅以下函数的参考文档。

函数描述
vpiInitKLTFeatureTrackerCreationParams 初始化 VPIKLTFeatureTrackerCreationParams 为默认值。
vpiCreateKLTFeatureTracker vpiSubmitKLTFeatureTracker 创建负载。
vpiSubmitKLTFeatureTracker 在两个帧上运行 KLT 特征跟踪器。

用法

语言
  1. 导入 VPI 模块
    import vpi
  2. (可选)定义一个函数,根据上一次跟踪的输出边界框和估计值,更新下一次跟踪的输入边界框和预测值。 此自定义更新类似于默认更新,此处添加作为如何定义自定义更新的示例。
    # 此函数读取 KLT 跟踪器的结果(输出边界框和估计值),并更新下一次迭代的输入边界框和预测值。
    def customUpdate(inBoxes, inPreds, outBoxes, outEstim)
    with inBoxes.lock_cpu() as inBoxes_cpu, inPreds.lock_cpu() as inPreds_cpu, \
    outBoxes.rlock_cpu() as outBoxes_cpu, outEstim.rlock_cpu() as outEstim_cpu
    inBoxes_ = inBoxes_cpu.view(np.recarray)
    inPreds_ = inPreds_cpu.view(np.recarray)
    outBoxes_ = outBoxes_cpu.view(np.recarray)
    outEstim_ = outEstim_cpu.view(np.recarray)
    for i in range(outBoxes.size)
    # 如果边界框的跟踪状态丢失,则将其丢失状态分配给相应的输入边界框。
    if outBoxes_[i].tracking_status == vpi.KLTTrackStatus.LOST
    inBoxes_[i].tracking_status = vpi.KLTTrackStatus.LOST
    continue
    # 如果模板状态为需要更新,则使用相应的输出更新输入边界框,使其预测为单位矩阵(固定边界框)。
    if outBoxes_[i].template_status == vpi.KLTTemplateStatus.UPDATE_NEEDED
    inBoxes_[i] = outBoxes_[i]
    inPreds_[i] = np.eye(3)
    else:
    # 如果不需要更新,只需通过相应的输出估计值来更新输入预测值。
    inBoxes_[i].template_status = vpi.KLTTemplateStatus.UPDATE_NOT_NEEDED
    inPreds_[i] = outEstim_[i]
  3. 将来自 OpenCV 的输入帧转换为灰度 OpenCV 帧和包含图像数据的 VPI 图像。 两者都用于处理输入和输出视频帧。
    def convertFrameImage(inputFrame)
    if inputFrame.ndim == 3 and inputFrame.shape[2] == 3
    grayFrame = cv2.cvtColor(inputFrame, cv2.COLOR_BGR2GRAY)
    else:
    grayFrame = inputFrame
    grayImage = vpi.asimage(grayFrame.copy())
    return grayFrame, grayImage
  4. 将输入边界框定义为 vpi.Type.KLT_TRACKED_BOUNDING_BOX 类型的 VPI 数组。 其容量是在所有视频帧中要跟踪的边界框总数。 这样做是为了保证在跟踪整个视频中的所有边界框情况下的最大存储空间。
    inBoxes = vpi.Array(totalNumBoxes, vpi.Type.KLT_TRACKED_BOUNDING_BOX)
  5. 从输入视频中读取第一个输入帧,并将其转换为灰度 OpenCV 帧和 VPI 图像。
    validFrame, cvFrame = inVideo.read()
    if not validFrame
    print("Error reading first input frame", file=sys.stderr)
    exit(1)
    # 将 OpenCV 帧转换为灰度,同时返回 VPI 图像
    cvGray, imgTemplate = convertFrameImage(cvFrame)
  6. 创建 VPI KLTFeatureTracker 对象,该对象将包含算法所需的所有信息。 其中一个信息是输入预测,可以从对象中通过 in_predictions() 检索以进行其他处理。 构造函数接收图像模板、输入边界框和执行算法的后端。 假设所有输入帧都具有相同的大小,因此图像模板可以是第一帧。
    klt = vpi.KLTFeatureTracker(imgTemplate, inBoxes, backend=vpi.Backend.CUDA)
    inPreds = klt.in_predictions()
  7. 在每个有效帧中,首先检查当前帧是否在 allBoxes 中,allBoxes 是一个字典,将帧索引映射到要在该帧开始跟踪的输入边界框列表。 如果是,则将这些边界框添加到 klt 对象。
    while validFrame
    if curFrame in allBoxes
    klt.add_boxes(allBoxes[curFrame])
  8. 从输入视频中读取下一个输入帧,并将其转换为灰度 OpenCV 帧和 VPI 图像参考。
    curFrame += 1
    validFrame, cvFrame = inVideo.read()
    if not validFrame
    break
    cvGray, imgReference = convertFrameImage(cvFrame)
  9. 然后使用在 klt 创建中定义的 CUDA 后端,在输入图像帧上执行算法。 传递的更新函数是在开始时定义的函数。 当此参数不存在时,klt 调用运行默认更新。 输出边界框与跟踪信息一起返回。
    outBoxes = klt(imgReference, update=customUpdate)
  1. 初始化阶段
    1. 包含定义所需函数和结构的头文件。
      声明实现 KLT 特征跟踪器算法的函数。
    2. 定义输入帧和输入边界框。 有关如何根据轴对齐的边界框、参考帧、输入框和输入预测正确填充每个边界框的说明,请参阅 VPIBoundingBox 文档。
      int frame_count = /*... */;
      VPIImage *frames = /* ... */;
      int bbox_count = /* ... */;
      VPIBoundingBox *bboxes = /* ... */;
      struct VPIImageImpl * VPIImage
      图像的句柄。
      Definition: Types.h:256
      存储通用的 2D 边界框。
      Definition: Types.h:424
    3. 创建包含跟踪信息的边界框数组。 对于新的边界框,trackingStatus 必须为 0,表示边界框跟踪有效。 templateStatus 必须为 1,表示必须更新与此边界框对应的模板。
      VPIKLTTrackedBoundingBox tracked_bboxes[128];
      int b;
      for (b = 0; b < bbox_count; ++b)
      {
      tracked_bboxes[b].bbox = bboxes[b];
      tracked_bboxes[b].trackingStatus = 0; /* 有效跟踪 */
      tracked_bboxes[b].templateStatus = 1; /* 必须更新 */
      }
      int8_t templateStatus
      与此边界框相关的模板状态。
      Definition: Types.h:504
      int8_t trackingStatus
      此边界框的跟踪状态。
      Definition: Types.h:497
      VPIBoundingBox bbox
      正在跟踪的边界框。
      Definition: Types.h:490
      存储由 KLT 跟踪器跟踪的边界框。
      Definition: Types.h:488
    4. 将跟踪的边界框包装到 VPIArray 中。 数组类型必须是 VPI_ARRAY_TYPE_KLT_TRACKED_BOUNDING_BOX

      VPIArrayData data_bboxes;
      memset(&data_bboxes, 0, sizeof(data_bboxes));
      data_bboxes.buffer.aos.capacity = 128;
      data_bboxes.buffer.aos.sizePointer = &bbox_count;
      data_bboxes.buffer.aos.data = tracked_bboxes;
      VPIArray inputBoxList;
      vpiArrayCreateWrapper(&data_bboxes, 0, &inputBoxList);
      VPIArrayBufferType bufferType
      数组缓冲区类型。
      Definition: Array.h:172
      void * data
      指向数组的第一个元素。
      Definition: Array.h:135
      VPIArrayBuffer buffer
      存储数组内容。
      Definition: Array.h:175
      int32_t * sizePointer
      指向数组中元素的数量。
      Definition: Array.h:122
      VPIArrayBufferAOS aos
      以结构体数组布局存储的数组。
      Definition: Array.h:162
      int32_t capacity
      数组可以容纳的最大元素数。
      Definition: Array.h:126
      VPIArrayType type
      每个数组元素的类型。
      Definition: Array.h:118
      VPIStatus vpiArrayCreateWrapper(const VPIArrayData *data, uint64_t flags, VPIArray *array)
      通过包装现有的主机内存块来创建数组对象。
      struct VPIArrayImpl * VPIArray
      数组的句柄。
      Definition: Types.h:232
      @ VPI_ARRAY_TYPE_KLT_TRACKED_BOUNDING_BOX
      VPIKLTTrackedBoundingBox 元素。
      Definition: ArrayType.h:79
      @ VPI_ARRAY_BUFFER_HOST_AOS
      主机可访问的结构体数组。
      Definition: Array.h:146
      存储有关数组特性和内容的信息。
      Definition: Array.h:168
    5. 创建边界框变换预测数组,最初填充单位变换,因为模板与模板图像中的边界框内容完全匹配。
      int i;
      for (i = 0; i < bbox_count; ++i)
      {
      VPIHomographyTransform2D *xform = preds + i;
      /* 单位变换。 */
      memset(xform, 0, sizeof(*xform));
      xform->mat3[0][0] = 1;
      xform->mat3[1][1] = 1;
      xform->mat3[2][2] = 1;
      }
      float mat3[3][3]
      定义单应性的 3x3 齐次矩阵。
      Definition: Types.h:405
      存储通用的 2D 单应性变换。
      Definition: Types.h:404
    6. 将此数组包装到 VPIArray 中。 数组类型必须是 VPI_ARRAY_TYPE_HOMOGRAPHY_TRANSFORM_2D
      VPIArrayData data_preds;
      memset(&data_preds, 0, sizeof(data_preds));
      data_preds.buffer.aos.capacity = 128;
      int32_t data_preds_size = bbox_count;
      data_preds.buffer.aos.sizePointer = &data_preds_size;
      data_preds.buffer.aos.data = preds;
      VPIArray inputPredList;
      vpiArrayCreateWrapper(&data_preds, 0, &inputPredList);
      @ VPI_ARRAY_TYPE_HOMOGRAPHY_TRANSFORM_2D
      VPIHomographyTransform2D 元素。
      Definition: ArrayType.h:78
    7. 创建将包含处理所需的所有临时缓冲区的负载。 假设所有输入帧都具有相同的大小,因此使用第一帧的尺寸和类型来创建负载。
      VPIImageFormat imgFormat;
      vpiImageGetFormat(frames[0], &imgFormat);
      int width, height;
      vpiImageGetSize(frames[0], &width, &height);
      vpiCreateKLTFeatureTracker(VPI_BACKEND_CUDA, width, height, imgFormat, NULL, &klt);
      uint64_t VPIImageFormat
      预定义的图像格式。
      Definition: ImageFormat.h:94
      VPIStatus vpiImageGetFormat(VPIImage img, VPIImageFormat *format)
      获取图像格式。
      VPIStatus vpiImageGetSize(VPIImage img, int32_t *width, int32_t *height)
      以像素为单位获取图像尺寸。
      VPIStatus vpiCreateKLTFeatureTracker(uint64_t backends, int32_t imageWidth, int32_t imageHeight, VPIImageFormat imageFormat, const VPIKLTFeatureTrackerCreationParams *params, VPIPayload *payload)
      为 vpiSubmitKLTFeatureTracker 创建负载。
      struct VPIPayloadImpl * VPIPayload
      算法负载的句柄。
      Definition: Types.h:268
      @ VPI_BACKEND_CUDA
      CUDA 后端。
      Definition: Types.h:93
    8. 定义指导 KLT 跟踪过程的配置参数。
      VPIStatus vpiInitKLTFeatureTrackerParams(VPIKLTFeatureTrackerParams *params)
      使用默认值初始化 VPIKLTFeatureTrackerParams。
      定义 vpiCreateKLTFeatureTracker 参数的结构。
    9. 创建输出跟踪边界框数组。 它将包含基于前一帧和到目前为止收集的模板信息估计的当前帧的边界框。 它还包含边界框的当前跟踪状态。
      VPIArray outputBoxList;
      VPIStatus vpiArrayCreate(int32_t capacity, VPIArrayType type, uint64_t flags, VPIArray *array)
      创建空的数组实例。
    10. 创建输出估计变换。 它将包含使边界框模板与当前(参考)帧上的相应边界框匹配的变换。
      VPIArray outputEstimList;
    11. 创建将在其中提交算法以供执行的流。
      VPIStream stream;
      vpiStreamCreate(0, &stream);
      struct VPIStreamImpl * VPIStream
      流的句柄。
      Definition: Types.h:250
      VPIStatus vpiStreamCreate(uint64_t flags, VPIStream *stream)
      创建流实例。
  2. 处理阶段
    1. 从第二帧开始的处理循环。 前一帧是算法从中获取跟踪模板的帧,当前帧是这些模板与之匹配的帧。
      for (int idframe = 1; idframe < frame_count; ++idframe)
      {
      VPIImage imgTemplate = frames[idframe - 1];
      VPIImage imgReference = frames[idframe];
    2. 提交算法。 第一次运行时,它将遍历所有输入边界框,从模板帧中裁剪它们并将它们存储在负载中。 后续运行将为添加的新边界框重复裁剪和存储过程(在本示例中不会发生,但在 示例应用程序 中会发生),或者在参考帧上执行模板匹配。
      VPI_CHECK_STATUS(vpiSubmitKLTFeatureTracker(stream, VPI_BACKEND_CUDA, klt, imgTemplate, inputBoxList,
      inputPredList, imgReference, outputBoxList, outputEstimList,
      &params));
      VPIStatus vpiSubmitKLTFeatureTracker(VPIStream stream, uint64_t backend, VPIPayload payload, VPIImage templateImage, VPIArray inputBoxList, VPIArray inputPredictionList, VPIImage referenceImage, VPIArray outputBoxList, VPIArray outputEstimationList, const VPIKLTFeatureTrackerParams *params)
      在两个帧上运行 KLT 特征跟踪器。
    3. 等待处理完成。
      vpiStreamSync(stream);
      VPIStatus vpiStreamSync(VPIStream stream)
      阻止调用线程,直到此流队列中的所有提交命令完成(队列为空)。
    4. 锁定输出数组以检索更新的边界框和估计的变换。
      VPIArrayData updatedBBoxData;
      vpiArrayLockData(outputBoxList, VPI_LOCK_READ, VPI_ARRAY_BUFFER_HOST_AOS, &updatedBBoxData);
      VPIArrayData estimData;
      VPIKLTTrackedBoundingBox *updated_bbox = (VPIKLTTrackedBoundingBox *)updatedBBoxData.buffer.aos.data;
      VPIStatus vpiArrayLockData(VPIArray array, VPILockMode mode, VPIArrayBufferType bufType, VPIArrayData *data)
      获取数组对象的锁并返回数组内容。
      @ VPI_LOCK_READ
      仅锁定内存以进行读取。
      Definition: Types.h:617
    5. 锁定输入数组,以便可以更新下一次迭代的状态。 由于它们实际上是包装器,因此包装的数据将直接更新。 为了做到这一点,必须锁定相应的 VPI 数组以进行写入。
      VPIStatus vpiArrayLock(VPIArray array, VPILockMode mode)
      获取数组对象的锁。
      @ VPI_LOCK_READ_WRITE
      锁定内存以进行读取和写入。
      Definition: Types.h:631
    6. 循环遍历所有边界框。
      int b;
      for (b = 0; b < bbox_count; ++b)
      {
    7. 更新边界框状态。 如果跟踪丢失 (trackingStatus==1),则输入边界框也必须标记为丢失,以便后续 KLT 迭代忽略它。 如果需要更新模板 (templateStatus==1),则下一次迭代将执行更新,否则将执行模板匹配。
      tracked_bboxes[b].trackingStatus = updated_bbox[b].trackingStatus;
      tracked_bboxes[b].templateStatus = updated_bbox[b].templateStatus;
    8. 跳过未被跟踪的边界框。
      if (updated_bbox[b].trackingStatus)
      {
      continue;
      }
    9. 如果必须在下一次 KLT 迭代中更新此边界框的模板,则用户必须重新定义边界框。 有几种方法可以做到这一点。 可以使用诸如 Harris 关键点检测器 之类的特征检测器来帮助获取全新的边界框,使用 updated_bbox[b] 并通过其他方式对其进行细化,以避免累积跟踪误差,或者直接按原样使用它,这是一种不太稳健的方法,但仍然可以产生不错的结果。 本示例选择最后一种更简单的方法。
      if (updated_bbox[b].templateStatus)
      {
      tracked_bboxes[b] = updated_bbox[b];
    10. 还要重置相应的输入预测变换,将其设置为单位矩阵,因为现在假设输入边界框与正在跟踪的对象完全匹配。
      memset(&preds[b], 0, sizeof(preds[b]));
      preds[b].mat3[0][0] = 1;
      preds[b].mat3[1][1] = 1;
      preds[b].mat3[2][2] = 1;
      }
    11. 如果模板不需要更新,请将输入预测变换设置为此 KLT 迭代估计的变换。
      else
      {
      preds[b] = estim[b];
      }
      }
    12. 更新所有边界框后,解锁输入和输出数组,因为此迭代不再需要它们。
      vpiArrayUnlock(inputBoxList);
      vpiArrayUnlock(inputPredList);
      vpiArrayUnlock(outputBoxList);
      vpiArrayUnlock(outputEstimList);
      VPIStatus vpiArrayUnlock(VPIArray array)
      释放数组对象的锁。
    13. 本次迭代结束。
      }
  3. 清理阶段
    1. 释放流、负载以及输入和输出数组持有的资源。
      vpiArrayDestroy(inputBoxList);
      vpiArrayDestroy(inputPredList);
      vpiArrayDestroy(outputBoxList);
      vpiArrayDestroy(outputEstimList);
      void vpiArrayDestroy(VPIArray array)
      销毁数组实例。
      void vpiPayloadDestroy(VPIPayload payload)
      释放负载对象和所有关联的资源。
      void vpiStreamDestroy(VPIStream stream)
      销毁流实例并释放所有硬件资源。

有关更多信息,请参阅 VPI - 视觉编程接口 的“C API 参考”部分中的 KLT 特征跟踪器

参考

  1. Simon Baker, Iain Matthews, "Lucas-Kanade 20 Years On: A Unified Framework"。
    International Journal of Computer Vision, 2004 年 2 月,第 56 卷,第 3 期,第 221-255 页。