点追踪
视频讲解如下:
当前系列所有demo下载地址:
https://github.com/GaoRenBao/OpenCv4-Demo
不同编程语言对应的OpenCv版本以及开发环境信息如下:
语言 | OpenCv版本 | IDE |
C# | OpenCvSharp4.4.8.0.20230708 | Visual Studio 2022 |
C++ | OpenCv-4.5.5-vc14_vc15 | Visual Studio 2022 |
Python | OpenCv-Python (4.6.0.66) | PyCharm Community Edition 2022.1.3 |
点追踪主要实现了两种功能演示,一种是自动获取角点进行追踪,另一种是通过鼠标点击,进行指定点的追踪。
C#版本
C#版本需要安装“OpenCvSharp4”、“OpenCvSharp4.runtime.win”两个库才行。不然会报错。
如果需要使用“ BitmapConverter.ToBitmap”操作,则需要追加安装“OpenCvSharp4.Extensions”库。
OpenCv3版本的SetMouseCallback操作请参考:/Course?id=4743192000049
这里就不重复操作了。
效果演示如下:

代码如下:
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Linq;
namespace demo
{
internal class Program
{
/// <summary>
/// 视频操作
/// </summary>
public static VideoCapture Cap = new VideoCapture();
public static Point2f point;
public static bool addRemovePt = false;
static void Main(string[] args)
{
// 打开ID为0的摄像头
Cap.Open(0);
// 判断摄像头是否成功打开
if (!Cap.IsOpened())
{
Console.WriteLine("摄像头打开失败.");
return;
}
// 设置采集的图像尺寸为:640*480
//Cap.Set(VideoCaptureProperties.FrameWidth, 640);
//Cap.Set(VideoCaptureProperties.FrameHeight, 480);
//Cap.Set(VideoCaptureProperties.Exposure, -3); // 曝光
Mat frame = new Mat();
Mat image = new Mat();
Mat gray = new Mat();
Mat prevGray = new Mat();
bool needToInit = true;
bool nightMode = false;
Point2f[] points1 = null;
Point2f[] points2 = null;
const int MAX_COUNT = 500;
TermCriteria criteria = new TermCriteria(CriteriaTypes.MaxIter | CriteriaTypes.Eps, 20, 0.03);
Cv2.NamedWindow("CamShift Demo");
Cv2.SetMouseCallback("CamShift Demo", new MouseCallback(onMouse));
var window = new Window("CamShift Demo");
while (true)
{
if (Cap.Read(frame))
{
frame.CopyTo(image);
Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
//if (nightMode)
// image = Scalar::all(0);
if (needToInit)
{
// 自动初始化
//points2 = Cv2.GoodFeaturesToTrack(gray, 500, 0.01, 10, new Mat(), 3, false, 0.04);
// 像素级检测特征点
Point2f[] po = Cv2.GoodFeaturesToTrack(gray, MAX_COUNT, 0.01, 10, new Mat(), 3, false, 0.04);
// 亚像素级检测
points2 = Cv2.CornerSubPix(gray, po, new Size(10, 10), new Size(-1, -1), criteria);
addRemovePt = false;
}
else if (points1 != null && points1.Length > 0)
{
byte[] status;
float[] err;
if (prevGray.Empty())
gray.CopyTo(prevGray);
//光流金字塔,输出图二的特征点
points2 = new Point2f[points1.Length];
Cv2.CalcOpticalFlowPyrLK(prevGray, gray, points1, ref points2, out status, out err);
int i, k;
for (i = k = 0; i < points2.Length; i++)
{
if (addRemovePt)
{
// C# 里没有计算范数的函数Norm,我这里直接按距离算了
Point2f p = point;
Point2f p2 = points2[i];
double a = Math.Sqrt(Math.Abs(p.X - p2.X) * Math.Abs(p.X - p2.X) + Math.Abs(p.Y - p2.Y) * Math.Abs(p.Y - p2.Y));
if (a <= 5)
{
addRemovePt = false;
continue;
}
}
if (status[i] == 0x00)
continue;
points2[k++] = points2[i];
Cv2.Circle(image, (Point)points2[i], 3, new Scalar(0, 255, 0), 8);
}
// C++ 的resize功能
points2 = points2.ToList().Take(k).ToList().ToArray();
}
if (addRemovePt && (points2 == null || points2.Length < MAX_COUNT))
{
Point2f[] tmp = new Point2f[] { point };
Point2f[] tmp2 = Cv2.CornerSubPix(gray, tmp, new Size(10, 10), new Size(-1, -1), criteria);
// C++ 的push_back功能
List<Point2f> a = points2.ToList();
a.Add(tmp2[0]);
points2 = a.ToArray();
addRemovePt = false;
}
needToInit = false;
// 在Window窗口中播放视频(方法1)
window.ShowImage(image);
// 在Window窗口中播放视频(方法2)
//Cv2.ImShow("CamShift Demo", image);
// 在pictureBox1中显示效果图
//pictureBox1.Image = BitmapConverter.ToBitmap(image);
char c = (char)Cv2.WaitKey(10);
if (c == 27) break;
switch (c)
{
case 'r':
needToInit = true;
break;
case 'c':
points1 = new Point2f[] { };
points2 = new Point2f[] { };
break;
case 'n':
nightMode = !nightMode;
break;
}
Swap(ref points2, ref points1);
Swap(ref prevGray, ref gray);
}
}
}
public static void Swap<T>(ref T a, ref T b)
{
T t = a;
a = b;
b = t;
}
public static void onMouse(MouseEventTypes @event, int x, int y, MouseEventFlags flags, IntPtr userData)
{
if (@event == MouseEventTypes.LButtonDown)
{
point = new Point2f((float)x, (float)y);
addRemovePt = true;
}
}
}
}
C++版本
C++版本是毛星云的版本简化来的。
效果演示如下:

代码如下:
//---------------------------------【头文件、命名空间包含部分】----------------------------
// 描述:包含程序所使用的头文件和命名空间
//-------------------------------------------------------------------------------------------------
#include "opencv2/video/tracking.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <ctype.h>
using namespace cv;
using namespace std;
static void help()
{
cout << "\n\t操作说明: \n"
"\t\t通过点击在图像中添加/删除特征点\n"
"\t\tESC - 退出程序\n"
"\t\tr -自动进行追踪\n"
"\t\tc - 删除所有点\n"
"\t\tn - 开/光-夜晚模式\n" << endl;
}
Point2f point;
bool addRemovePt = false;
//--------------------------------【onMouse( )回调函数】------------------------------------
// 描述:鼠标操作回调
//-------------------------------------------------------------------------------------------------
static void onMouse(int event, int x, int y, int /*flags*/, void* /*param*/)
{
//此句代码的OpenCV2版为:
//if (event == CV_EVENT_LBUTTONDOWN)
//此句代码的OpenCV3版为:
if (event == EVENT_LBUTTONDOWN)
{
point = Point2f((float)x, (float)y);
addRemovePt = true;
}
}
//-----------------------------------【main( )函数】--------------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始
//-------------------------------------------------------------------------------------------------
int main(int argc, char** argv)
{
help();
VideoCapture cap;
//此句代码的OpenCV2版为:
//TermCriteria termcrit(CV_TERMCRIT_ITER | CV_TERMCRIT_EPS, 20, 0.03);
//此句代码的OpenCV3版为:
TermCriteria termcrit(TermCriteria::MAX_ITER | TermCriteria::EPS, 20, 0.03);
Size subPixWinSize(10, 10), winSize(31, 31);
const int MAX_COUNT = 500;
bool needToInit = false;
bool nightMode = false;
cap.open(0);
if (!cap.isOpened())
{
cout << "Could not initialize capturing...\n";
return 0;
}
namedWindow("LK Demo", 1);
setMouseCallback("LK Demo", onMouse, 0);
Mat gray, prevGray, image;
vector<Point2f> points[2];
for (;;)
{
Mat frame;
cap >> frame;
if (frame.empty())
break;
frame.copyTo(image);
cvtColor(image, gray, COLOR_BGR2GRAY);
if (nightMode)
image = Scalar::all(0);
if (needToInit)
{
// 自动初始化
goodFeaturesToTrack(gray, points[1], MAX_COUNT, 0.01, 10, Mat(), 3, false, 0.04);
cornerSubPix(gray, points[1], subPixWinSize, Size(-1, -1), termcrit);
addRemovePt = false;
}
else if (!points[0].empty())
{
vector<uchar> status;
vector<float> err;
if (prevGray.empty())
gray.copyTo(prevGray);
calcOpticalFlowPyrLK(prevGray, gray, points[0], points[1], status, err, winSize,
3, termcrit, 0, 0.001);
size_t i, k;
for (i = k = 0; i < points[1].size(); i++)
{
if (addRemovePt)
{
if (norm(point - points[1][i]) <= 5)
{
addRemovePt = false;
continue;
}
}
if (!status[i])
continue;
points[1][k++] = points[1][i];
circle(image, points[1][i], 3, Scalar(0, 255, 0), -1, 8);
}
points[1].resize(k);
}
if (addRemovePt && points[1].size() < (size_t)MAX_COUNT)
{
vector<Point2f> tmp;
tmp.push_back(point);
//此句代码的OpenCV2版为:
//cornerSubPix(gray, tmp, winSize, cvSize(-1, -1), termcrit);
//此句代码的OpenCV3版为:
cornerSubPix(gray, tmp, winSize, Size(-1, -1), termcrit);
points[1].push_back(tmp[0]);
addRemovePt = false;
}
needToInit = false;
imshow("LK Demo", image);
char c = (char)waitKey(10);
if (c == 27)
break;
switch (c)
{
case 'r':
needToInit = true;
break;
case 'c':
points[0].clear();
points[1].clear();
break;
case 'n':
nightMode = !nightMode;
break;
}
std::swap(points[1], points[0]);
cv::swap(prevGray, gray);
}
return 0;
}
Python版本
Python没找到CornerSubPix函数的操作方法,没办法,只能用goodFeaturesToTrack代替了,效果看起来像是点追踪,实际上是追踪一个长宽都为5的正方形roi区域。
效果演示如下:

代码如下:
import cv2
import numpy as np
xo,yo=0,0
addRemovePt = False
# ShiTomasi corner detection的参数
feature_params = dict(maxCorners=500,
qualityLevel=0.01,
minDistance=10,
blockSize=3)
# 光流法参数
lk_params = dict(winSize=(10, 10),
maxLevel=2,
criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_MAX_ITER, 20, 0.03))
#创建回调函数
def onMouse(event,x,y,flags,param):
global xo,yo,addRemovePt
if event == cv2.EVENT_LBUTTONDOWN:
xo,yo = x, y
addRemovePt = True
# 打开摄像头
Cap = cv2.VideoCapture(0)
# 判断视频是否打开
if (Cap.isOpened() == False):
print('Open Camera Error.')
else:
Cap.set(cv2.CAP_PROP_FRAME_WIDTH,640) # 设置图像宽
Cap.set(cv2.CAP_PROP_FRAME_HEIGHT,480) # 设置图像高
Cap.set(cv2.CAP_PROP_EXPOSURE, -3) # 设置曝光值
# 读取设置的参数
size = (int(Cap.get(cv2.CAP_PROP_FRAME_WIDTH)),int(Cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
baog = int(Cap.get(cv2.CAP_PROP_EXPOSURE))
# 输出参数
print('摄像头设置,尺寸:' + str(size))
print('摄像头设置,曝光:' + str(baog))
cv2.namedWindow("CamShift Demo", 1)
cv2.setMouseCallback("CamShift Demo", onMouse)
grabbed, frame = Cap.read() # 取出视频的第一帧
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 灰度化
points1 = cv2.goodFeaturesToTrack(gray, mask=None, **feature_params)
prevGray = None
needToInit = True
# 读取图像
while True:
grabbed, frame = Cap.read()
if frame is None:
continue
image=frame.copy()
# 将原图像转换为灰度图像
gray=cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
if needToInit == True:
points2 = cv2.goodFeaturesToTrack(gray, mask=None, **feature_params)
addRemovePt = False
elif points1 is not None and points1.size != 0 :
if prevGray is None:
prevGray = gray.copy()
# 计算光流以获取点的新位置
points2, st, err = cv2.calcOpticalFlowPyrLK(prevGray, gray, points1, None, **lk_params)
# 绘制跟踪点
for i, (new) in enumerate(zip(points2[st == 1])):
frame = cv2.circle(frame, ((int)(new[0][0]),(int)(new[0][1])), 8, (0, 255, 0), -1)
if addRemovePt == True and points2 is None :
maskroi=np.zeros_like(gray)
maskroi[yo:yo+5, xo:xo+5]=255
# 效果看起来像是点追踪,实际上是追踪一个长宽都为5的正方形roi区域
# CornerSubPix函数的操作方法没找到...
points2 = cv2.goodFeaturesToTrack(gray,mask=maskroi, **feature_params)
addRemovePt = False;
needToInit = False
cv2.imshow('CamShift Demo', frame)
k = cv2.waitKey(30) # & 0xff
# r 等于114
if k == 114:
needToInit = True
# c 等于99
if k == 99:
points1 = None
points2 = None
points1=points2
prevGray = gray.copy()
cv2.destroyAllWindows()