<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>目标跟踪 on Tech Snippets - 嵌入式技术笔记</title>
    <link>https://tech-snippets.xyz/tags/%E7%9B%AE%E6%A0%87%E8%B7%9F%E8%B8%AA/</link>
    <description>Recent content in 目标跟踪 on Tech Snippets - 嵌入式技术笔记</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Thu, 23 Apr 2026 16:00:00 +0800</lastBuildDate>
    <atom:link href="https://tech-snippets.xyz/tags/%E7%9B%AE%E6%A0%87%E8%B7%9F%E8%B8%AA/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>基于 OpenCV 的红色物体识别与多目标跟踪实战</title>
      <link>https://tech-snippets.xyz/posts/opencv-red-object-detection-tracking/</link>
      <pubDate>Thu, 23 Apr 2026 16:00:00 +0800</pubDate>
      <guid>https://tech-snippets.xyz/posts/opencv-red-object-detection-tracking/</guid>
      <description>前言 在计算机视觉领域，颜色检测是最基础也最实用的技术之一。红色作为一种醒目的颜色，在交通标志、安全警示、工业自动化等场景中应用广泛。今天我们来深入探讨如何用 OpenCV 实现红色物体的识别，并在此基础上实现多目标跟踪功能。
这篇文章不是简单的 API 调用演示，而是从原理出发，结合实际场景中的问题，一步步构建一个健壮的检测与跟踪系统。我们会遇到光照变化、噪声干扰、部分遮挡等实际问题，然后逐一解决。
一、为什么选择 HSV 颜色空间？ 当我们谈论颜色检测时，很多新手第一反应是直接在 RGB 图像上做阈值处理。比如，红色物体的 R 通道值比较高，那么我们设定一个阈值，只保留 R &amp;gt; 200 的像素。但实际一试就会发现，这种方法效果非常差。
问题出在哪里？RGB 颜色空间虽然直观，但它把亮度和颜色信息混在一起了。同一个红色物体，在强光下和阴影下，RGB 值可能差异巨大，但人眼感知到的颜色其实是一样的。这就导致基于 RGB 的阈值检测非常不稳定。
这时候 HSV 颜色空间就派上用场了。HSV 把颜色信息分解成三个独立的通道：
H (Hue, 色调)：表示颜色的种类，取值范围在 OpenCV 中是 0-179 S (Saturation, 饱和度)：表示颜色的鲜艳程度，0-255 V (Value, 明度)：表示颜色的明亮程度，0-255 HSV 的优势在于，颜色信息主要由 H 通道决定，而 V 通道单独控制亮度。这意味着，即使光线变化导致 V 值波动，只要 H 值在我们设定的红色范围内，我们仍然能稳定地检测到目标。
二、红色在 HSV 空间中的特殊性 红色有个有意思的特性：它在色相环的两端都有分布。在标准的 0-360 度色相环中，红色出现在 0 度附近和 360 度附近。OpenCV 为了用 8 位表示，把这个范围减半成了 0-179，所以红色就分布在 0-10 和 170-179 这两个区间。
这是很多初学者容易踩的坑。如果你只检测 0-10 这个区间，会发现稍微偏紫红或者偏橙红一点的物体就检测不到了。正确的做法是同时检测这两个区间，然后把结果合并。</description>
      <content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>在计算机视觉领域，颜色检测是最基础也最实用的技术之一。红色作为一种醒目的颜色，在交通标志、安全警示、工业自动化等场景中应用广泛。今天我们来深入探讨如何用 OpenCV 实现红色物体的识别，并在此基础上实现多目标跟踪功能。</p>
<p>这篇文章不是简单的 API 调用演示，而是从原理出发，结合实际场景中的问题，一步步构建一个健壮的检测与跟踪系统。我们会遇到光照变化、噪声干扰、部分遮挡等实际问题，然后逐一解决。</p>
<h2 id="一为什么选择-hsv-颜色空间">一、为什么选择 HSV 颜色空间？</h2>
<p>当我们谈论颜色检测时，很多新手第一反应是直接在 RGB 图像上做阈值处理。比如，红色物体的 R 通道值比较高，那么我们设定一个阈值，只保留 R &gt; 200 的像素。但实际一试就会发现，这种方法效果非常差。</p>
<p>问题出在哪里？RGB 颜色空间虽然直观，但它把亮度和颜色信息混在一起了。同一个红色物体，在强光下和阴影下，RGB 值可能差异巨大，但人眼感知到的颜色其实是一样的。这就导致基于 RGB 的阈值检测非常不稳定。</p>
<p>这时候 HSV 颜色空间就派上用场了。HSV 把颜色信息分解成三个独立的通道：</p>
<ul>
<li><strong>H (Hue, 色调)</strong>：表示颜色的种类，取值范围在 OpenCV 中是 0-179</li>
<li><strong>S (Saturation, 饱和度)</strong>：表示颜色的鲜艳程度，0-255</li>
<li><strong>V (Value, 明度)</strong>：表示颜色的明亮程度，0-255</li>
</ul>
<p><img alt="HSV 颜色空间模型" loading="lazy" src="/images/hsv-color-space.svg"></p>
<p>HSV 的优势在于，颜色信息主要由 H 通道决定，而 V 通道单独控制亮度。这意味着，即使光线变化导致 V 值波动，只要 H 值在我们设定的红色范围内，我们仍然能稳定地检测到目标。</p>
<h2 id="二红色在-hsv-空间中的特殊性">二、红色在 HSV 空间中的特殊性</h2>
<p>红色有个有意思的特性：它在色相环的两端都有分布。在标准的 0-360 度色相环中，红色出现在 0 度附近和 360 度附近。OpenCV 为了用 8 位表示，把这个范围减半成了 0-179，所以红色就分布在 0-10 和 170-179 这两个区间。</p>
<p>这是很多初学者容易踩的坑。如果你只检测 0-10 这个区间，会发现稍微偏紫红或者偏橙红一点的物体就检测不到了。正确的做法是同时检测这两个区间，然后把结果合并。</p>
<p>我们来定义一下红色的检测范围：</p>
<pre tabindex="0"><code>低红色区间：
- H: 0-10
- S: 100-255
- V: 100-255

高红色区间：
- H: 170-179
- S: 100-255
- V: 100-255
</code></pre><p>这里的 S 和 V 都设置了一个比较高的下限 100，这是为了过滤掉那些不饱和、偏暗的红色区域，比如暗红色的阴影部分，它们往往不是我们感兴趣的目标。</p>
<h2 id="三完整的检测流程">三、完整的检测流程</h2>
<p>知道了颜色空间的选择和范围设定，我们来梳理一下完整的检测流程：</p>
<p><img alt="红色物体识别处理流程" loading="lazy" src="/images/opencv-pipeline.svg"></p>
<ol>
<li><strong>读取图像</strong>：从摄像头、视频文件或磁盘读取图像</li>
<li><strong>高斯模糊</strong>：去除噪点，平滑图像</li>
<li><strong>颜色空间转换</strong>：把 RGB 转换为 HSV</li>
<li><strong>颜色掩码</strong>：用 <code>inRange()</code> 函数提取红色区域</li>
<li><strong>形态学处理</strong>：腐蚀和膨胀，去除小的噪点</li>
<li><strong>轮廓检测</strong>：找出连通区域的轮廓</li>
<li><strong>轮廓过滤</strong>：根据面积、形状等条件过滤</li>
<li><strong>结果输出</strong>：绘制检测框和跟踪信息</li>
</ol>
<p>这个流程看起来步骤不少，但每一步都有它的作用。我们接下来逐一讲解每个环节的原理和实现细节。</p>
<h2 id="四图像预处理高斯模糊">四、图像预处理：高斯模糊</h2>
<p>在做颜色检测之前，先做一下高斯模糊是个好习惯。原因很简单：原始图像往往有很多噪点，这些噪点在颜色掩码之后会变成一个个小白点，严重影响后续的轮廓检测。</p>
<p>高斯模糊的原理是用一个高斯核（也就是一个权重矩阵）在图像上做卷积。简单来说，就是每个像素的值变成它周围像素的加权平均，离得越近权重越大。这样可以保留图像的整体特征，同时去除高频噪点。</p>
<p>在 OpenCV 中，调用非常简单：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">blurred</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">GaussianBlur</span><span class="p">(</span><span class="n">frame</span><span class="p">,</span> <span class="p">(</span><span class="mi">11</span><span class="p">,</span> <span class="mi">11</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span>
</span></span></code></pre></div><p>这里的 <code>(11, 11)</code> 是高斯核的大小，必须是奇数。核越大，模糊效果越明显，但处理速度也越慢。对于 640x480 分辨率的图像，11x11 的核通常是一个不错的平衡。</p>
<h2 id="五颜色掩码的生成">五、颜色掩码的生成</h2>
<p>有了预处理后的图像，我们就可以生成颜色掩码了。掩码是一个二值图像，属于红色的像素是白色（255），其他的都是黑色（0）。</p>
<p>前面提到红色有两个区间，所以我们需要分别生成两个掩码，然后合并：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># 低红色区间</span>
</span></span><span class="line"><span class="cl"><span class="n">lower_red1</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="n">upper_red1</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="mi">10</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="n">mask1</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">inRange</span><span class="p">(</span><span class="n">hsv</span><span class="p">,</span> <span class="n">lower_red1</span><span class="p">,</span> <span class="n">upper_red1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 高红色区间</span>
</span></span><span class="line"><span class="cl"><span class="n">lower_red2</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="mi">170</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="n">upper_red2</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="mi">179</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="n">mask2</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">inRange</span><span class="p">(</span><span class="n">hsv</span><span class="p">,</span> <span class="n">lower_red2</span><span class="p">,</span> <span class="n">upper_red2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 合并两个掩码</span>
</span></span><span class="line"><span class="cl"><span class="n">mask</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">bitwise_or</span><span class="p">(</span><span class="n">mask1</span><span class="p">,</span> <span class="n">mask2</span><span class="p">)</span>
</span></span></code></pre></div><p><code>inRange()</code> 这个函数会检查图像的每个像素，如果三个通道的值都在对应的上下限之间，就把掩码中对应位置设为 255，否则设为 0。这一步非常高效，因为它是逐像素操作，可以并行化处理。</p>
<p>此时的掩码往往还有一些小的白点，这是图像噪点导致的。接下来我们用形态学操作来清理这些噪点。</p>
<h2 id="六形态学处理腐蚀与膨胀">六、形态学处理：腐蚀与膨胀</h2>
<p>形态学处理是数学形态学在图像处理中的应用，基本操作是腐蚀和膨胀。</p>
<p><strong>腐蚀</strong>的作用是去除小的白噪点。它的原理是用一个结构元素（通常是矩形或圆形）在图像上扫描，只有当结构元素覆盖的区域全是白色时，中心像素才保留白色，否则变成黑色。这就导致白色区域向内收缩，小的噪点直接消失。</p>
<p><strong>膨胀</strong>则相反，它会让白色区域向外扩张。如果结构元素覆盖的区域有白色，中心像素就设为白色。膨胀可以用来填充物体内部的小黑点。</p>
<p>在实际应用中，我们通常先腐蚀再膨胀，这个组合叫做开运算（Opening）：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># 定义结构元素</span>
</span></span><span class="line"><span class="cl"><span class="n">kernel</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">((</span><span class="mi">5</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">uint8</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 先腐蚀</span>
</span></span><span class="line"><span class="cl"><span class="n">mask</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">erode</span><span class="p">(</span><span class="n">mask</span><span class="p">,</span> <span class="n">kernel</span><span class="p">,</span> <span class="n">iterations</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 后膨胀</span>
</span></span><span class="line"><span class="cl"><span class="n">mask</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">dilate</span><span class="p">(</span><span class="n">mask</span><span class="p">,</span> <span class="n">kernel</span><span class="p">,</span> <span class="n">iterations</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</span></span></code></pre></div><p>这里的 <code>iterations</code> 参数表示操作执行的次数。次数越多，效果越明显，但也要注意不要过度处理，否则会把我们感兴趣的目标也腐蚀掉了。</p>
<p>也可以直接用 OpenCV 提供的开运算函数：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">mask</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">morphologyEx</span><span class="p">(</span><span class="n">mask</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">MORPH_OPEN</span><span class="p">,</span> <span class="n">kernel</span><span class="p">,</span> <span class="n">iterations</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</span></span></code></pre></div><p>效果是一样的。通过形态学处理，我们的掩码会变得干净很多，接下来的轮廓检测就容易了。</p>
<h2 id="七轮廓检测与过滤">七、轮廓检测与过滤</h2>
<p>有了干净的掩码，我们就可以找轮廓了。轮廓就是把连通的白色像素连起来形成的曲线。OpenCV 的 <code>findContours()</code> 函数就是干这个的。</p>
<p>这里有个小细节需要注意：OpenCV 的 <code>findContours()</code> 在不同版本中有不同的返回值。在 OpenCV 3 中它返回三个值，在 OpenCV 4 中只返回两个。为了兼容，我们通常这样写：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">contours</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">findContours</span><span class="p">(</span><span class="n">mask</span><span class="o">.</span><span class="n">copy</span><span class="p">(),</span> <span class="n">cv2</span><span class="o">.</span><span class="n">RETR_EXTERNAL</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">CHAIN_APPROX_SIMPLE</span><span class="p">)</span>
</span></span></code></pre></div><p>第一个参数是输入图像，注意我们传的是 <code>mask.copy()</code>，因为 <code>findContours()</code> 会修改原图。第二个参数 <code>RETR_EXTERNAL</code> 表示只检测最外层的轮廓，内部的轮廓忽略。第三个参数 <code>CHAIN_APPROX_SIMPLE</code> 是轮廓近似方法，它会把水平、垂直、对角线的线段压缩成端点，节省内存。</p>
<p>拿到轮廓列表后，我们需要过滤掉那些明显不是目标的轮廓。最常用的过滤条件是面积：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">min_area</span> <span class="o">=</span> <span class="mi">500</span>  <span class="c1"># 最小面积，根据实际情况调整</span>
</span></span><span class="line"><span class="cl"><span class="n">valid_contours</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">contour</span> <span class="ow">in</span> <span class="n">contours</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">area</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">contourArea</span><span class="p">(</span><span class="n">contour</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">area</span> <span class="o">&gt;</span> <span class="n">min_area</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">valid_contours</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">contour</span><span class="p">)</span>
</span></span></code></pre></div><p><code>min_area</code> 的值要根据图像分辨率和目标大小来设置。比如 640x480 的图像，500-1000 像素是一个比较合理的起点。如果设得太小，会有很多噪点被误检；如果设得太大，真正的小目标会被漏掉。</p>
<p>除了面积，我们还可以根据长宽比、圆形度等形状特征来进一步过滤。比如如果我们要检测红色小球，那么可以计算轮廓的外接矩形，然后检查它的长宽比是否接近 1。</p>
<h2 id="八目标定位最小包围圆">八、目标定位：最小包围圆</h2>
<p>对于每个有效的轮廓，我们需要确定它的位置和大小。最简单的方法是画外接矩形，但对于圆形物体，最小包围圆通常更合适：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">for</span> <span class="n">contour</span> <span class="ow">in</span> <span class="n">valid_contours</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 计算最小包围圆</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span> <span class="n">radius</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">minEnclosingCircle</span><span class="p">(</span><span class="n">contour</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># 转换为整数</span>
</span></span><span class="line"><span class="cl">    <span class="n">center</span> <span class="o">=</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">x</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">y</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">radius</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">radius</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># 绘制结果</span>
</span></span><span class="line"><span class="cl">    <span class="n">cv2</span><span class="o">.</span><span class="n">circle</span><span class="p">(</span><span class="n">frame</span><span class="p">,</span> <span class="n">center</span><span class="p">,</span> <span class="n">radius</span><span class="p">,</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">cv2</span><span class="o">.</span><span class="n">circle</span><span class="p">(</span><span class="n">frame</span><span class="p">,</span> <span class="n">center</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">),</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span></code></pre></div><p>这里 <code>(x, y)</code> 是圆心坐标，<code>radius</code> 是半径。我们画两个圆：一个绿色的大圆表示物体轮廓，一个红色的实心小圆表示圆心位置。这样在可视化时非常直观。</p>
<p>除了最小包围圆，<code>cv2.boundingRect()</code> 可以得到轴对齐的外接矩形，<code>cv2.fitEllipse()</code> 可以拟合椭圆。选择哪种方式取决于你的目标形状和应用场景。</p>
<h2 id="九多目标跟踪的基本思路">九、多目标跟踪的基本思路</h2>
<p>现在我们能检测到每一帧中的红色物体了，但这还不够。在视频序列中，我们需要知道这一帧的物体 A 是不是上一帧的物体 A，这就是跟踪要解决的问题。</p>
<p>最简单的跟踪方法是基于距离的匹配：对于当前帧检测到的每个目标，找它在上一帧中最近的那个目标，如果距离小于某个阈值，就认为是同一个物体。</p>
<p>让我们定义一个简单的跟踪器类：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TrackedObject</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj_id</span><span class="p">,</span> <span class="n">center</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">obj_id</span> <span class="o">=</span> <span class="n">obj_id</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">center</span> <span class="o">=</span> <span class="n">center</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">=</span> <span class="mi">0</span>  <span class="c1"># 消失的帧数</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Tracker</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">next_id</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">objects</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">update</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">centers</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 如果没有检测到目标，所有物体消失计数+1</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">centers</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">obj_id</span> <span class="ow">in</span> <span class="nb">list</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">keys</span><span class="p">()):</span>
</span></span><span class="line"><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># 如果当前没有跟踪的物体，全部注册为新物体</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">center</span> <span class="ow">in</span> <span class="n">centers</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">next_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">TrackedObject</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">next_id</span><span class="p">,</span> <span class="n">center</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">next_id</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># 否则，做距离匹配</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># ... 匹配逻辑留待后面实现</span>
</span></span></code></pre></div><p>这个跟踪器会给每个物体分配一个唯一的 ID，并且记录它连续消失了多少帧。如果一个物体连续消失超过一定帧数（比如 30 帧），我们就认为它真正离开了视野，可以从跟踪列表中删除了。</p>
<h2 id="十距离匹配算法">十、距离匹配算法</h2>
<p>现在来实现距离匹配的核心逻辑。假设当前帧检测到了 N 个中心点，上一帧跟踪着 M 个物体，我们需要找到它们之间的最佳匹配。</p>
<p>首先计算所有可能配对的距离矩阵：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">scipy.spatial</span> <span class="kn">import</span> <span class="n">distance</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">current_centers</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">(</span><span class="n">centers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">object_ids</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">keys</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">object_centers</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="n">obj</span><span class="o">.</span><span class="n">center</span> <span class="k">for</span> <span class="n">obj</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">values</span><span class="p">()])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 计算距离矩阵：M x N</span>
</span></span><span class="line"><span class="cl"><span class="n">D</span> <span class="o">=</span> <span class="n">distance</span><span class="o">.</span><span class="n">cdist</span><span class="p">(</span><span class="n">object_centers</span><span class="p">,</span> <span class="n">current_centers</span><span class="p">)</span>
</span></span></code></pre></div><p><code>distance.cdist()</code> 会计算两组点之间所有两两配对的欧氏距离，返回一个形状为 (M, N) 的矩阵。接下来我们要找一个配对方式，使得总距离最小，每个点最多配对一次。</p>
<p>这种问题叫做二分图匹配，可以用匈牙利算法（Hungarian Algorithm）解决。Scipy 提供了现成的实现：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">scipy.optimize</span> <span class="kn">import</span> <span class="n">linear_sum_assignment</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">row_ind</span><span class="p">,</span> <span class="n">col_ind</span> <span class="o">=</span> <span class="n">linear_sum_assignment</span><span class="p">(</span><span class="n">D</span><span class="p">)</span>
</span></span></code></pre></div><p>这里 <code>row_ind</code> 是上一帧物体的索引，<code>col_ind</code> 是当前帧检测的索引，它们的对应关系就是最优匹配。</p>
<p>但是直接用匈牙利算法有个问题：如果一个物体是新出现的，或者一个物体消失了，我们不应该强行匹配。所以在得到匹配结果后，我们还要检查每个匹配的距离是否超过了阈值：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">max_distance</span> <span class="o">=</span> <span class="mi">80</span>  <span class="c1"># 最大匹配距离</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">used_rows</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">used_cols</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">row</span><span class="p">,</span> <span class="n">col</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">row_ind</span><span class="p">,</span> <span class="n">col_ind</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">D</span><span class="p">[</span><span class="n">row</span><span class="p">,</span> <span class="n">col</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">max_distance</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">continue</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># 更新已存在的目标</span>
</span></span><span class="line"><span class="cl">    <span class="n">obj_id</span> <span class="o">=</span> <span class="n">object_ids</span><span class="p">[</span><span class="n">row</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">center</span> <span class="o">=</span> <span class="n">current_centers</span><span class="p">[</span><span class="n">col</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="n">used_rows</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">row</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">used_cols</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">col</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 处理未匹配的跟踪目标（可能消失了）</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">object_ids</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">row</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">used_rows</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">obj_id</span> <span class="o">=</span> <span class="n">object_ids</span><span class="p">[</span><span class="n">row</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 处理未匹配的检测（可能是新目标）</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">current_centers</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">col</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">used_cols</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">next_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">TrackedObject</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">next_id</span><span class="p">,</span> <span class="n">current_centers</span><span class="p">[</span><span class="n">col</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">next_id</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span></code></pre></div><p>最后，我们清理掉那些消失太久的目标：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">max_disappeared</span> <span class="o">=</span> <span class="mi">30</span>  <span class="c1"># 最多允许消失30帧</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">obj_id</span> <span class="ow">in</span> <span class="nb">list</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">keys</span><span class="p">()):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">&gt;</span> <span class="n">max_disappeared</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">del</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span>
</span></span></code></pre></div><p>这样我们就有了一个完整的多目标跟踪器。</p>
<h2 id="十一完整代码实现">十一、完整代码实现</h2>
<p>现在我们把前面讲的所有内容整合起来，写一个完整的可运行程序：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">cv2</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">scipy.spatial</span> <span class="kn">import</span> <span class="n">distance</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">scipy.optimize</span> <span class="kn">import</span> <span class="n">linear_sum_assignment</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TrackedObject</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj_id</span><span class="p">,</span> <span class="n">center</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">obj_id</span> <span class="o">=</span> <span class="n">obj_id</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">center</span> <span class="o">=</span> <span class="n">center</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Tracker</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">max_distance</span><span class="o">=</span><span class="mi">80</span><span class="p">,</span> <span class="n">max_disappeared</span><span class="o">=</span><span class="mi">30</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">next_id</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">objects</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">max_distance</span> <span class="o">=</span> <span class="n">max_distance</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">max_disappeared</span> <span class="o">=</span> <span class="n">max_disappeared</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">update</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">centers</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">centers</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">obj_id</span> <span class="ow">in</span> <span class="nb">list</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">keys</span><span class="p">()):</span>
</span></span><span class="line"><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">&gt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">max_disappeared</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="k">del</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">center</span> <span class="ow">in</span> <span class="n">centers</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">next_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">TrackedObject</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">next_id</span><span class="p">,</span> <span class="n">center</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">next_id</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">object_ids</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">keys</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">object_centers</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="n">obj</span><span class="o">.</span><span class="n">center</span> <span class="k">for</span> <span class="n">obj</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">values</span><span class="p">()])</span>
</span></span><span class="line"><span class="cl">        <span class="n">current_centers</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">(</span><span class="n">centers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">D</span> <span class="o">=</span> <span class="n">distance</span><span class="o">.</span><span class="n">cdist</span><span class="p">(</span><span class="n">object_centers</span><span class="p">,</span> <span class="n">current_centers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">row_ind</span><span class="p">,</span> <span class="n">col_ind</span> <span class="o">=</span> <span class="n">linear_sum_assignment</span><span class="p">(</span><span class="n">D</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">used_rows</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">used_cols</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">row</span><span class="p">,</span> <span class="n">col</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">row_ind</span><span class="p">,</span> <span class="n">col_ind</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">D</span><span class="p">[</span><span class="n">row</span><span class="p">,</span> <span class="n">col</span><span class="p">]</span> <span class="o">&gt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">max_distance</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">continue</span>
</span></span><span class="line"><span class="cl">            
</span></span><span class="line"><span class="cl">            <span class="n">obj_id</span> <span class="o">=</span> <span class="n">object_ids</span><span class="p">[</span><span class="n">row</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">center</span> <span class="o">=</span> <span class="nb">tuple</span><span class="p">(</span><span class="n">current_centers</span><span class="p">[</span><span class="n">col</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">            <span class="n">used_rows</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">row</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">used_cols</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">col</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">object_ids</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">row</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">used_rows</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">obj_id</span> <span class="o">=</span> <span class="n">object_ids</span><span class="p">[</span><span class="n">row</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span><span class="o">.</span><span class="n">disappeared</span> <span class="o">&gt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">max_disappeared</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="k">del</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="n">obj_id</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">current_centers</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">col</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">used_cols</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">objects</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">next_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">TrackedObject</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">next_id</span><span class="p">,</span> <span class="nb">tuple</span><span class="p">(</span><span class="n">current_centers</span><span class="p">[</span><span class="n">col</span><span class="p">]))</span>
</span></span><span class="line"><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">next_id</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">objects</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">cap</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">VideoCapture</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">cap</span><span class="o">.</span><span class="n">isOpened</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;无法打开摄像头&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">tracker</span> <span class="o">=</span> <span class="n">Tracker</span><span class="p">(</span><span class="n">max_distance</span><span class="o">=</span><span class="mi">80</span><span class="p">,</span> <span class="n">max_disappeared</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">ret</span><span class="p">,</span> <span class="n">frame</span> <span class="o">=</span> <span class="n">cap</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">ret</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># 预处理：高斯模糊</span>
</span></span><span class="line"><span class="cl">        <span class="n">blurred</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">GaussianBlur</span><span class="p">(</span><span class="n">frame</span><span class="p">,</span> <span class="p">(</span><span class="mi">11</span><span class="p">,</span> <span class="mi">11</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># 转换到 HSV</span>
</span></span><span class="line"><span class="cl">        <span class="n">hsv</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">blurred</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_BGR2HSV</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># 红色掩码</span>
</span></span><span class="line"><span class="cl">        <span class="n">lower_red1</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">        <span class="n">upper_red1</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="mi">10</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">        <span class="n">mask1</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">inRange</span><span class="p">(</span><span class="n">hsv</span><span class="p">,</span> <span class="n">lower_red1</span><span class="p">,</span> <span class="n">upper_red1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">lower_red2</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="mi">170</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">        <span class="n">upper_red2</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="mi">179</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">        <span class="n">mask2</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">inRange</span><span class="p">(</span><span class="n">hsv</span><span class="p">,</span> <span class="n">lower_red2</span><span class="p">,</span> <span class="n">upper_red2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">mask</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">bitwise_or</span><span class="p">(</span><span class="n">mask1</span><span class="p">,</span> <span class="n">mask2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># 形态学处理</span>
</span></span><span class="line"><span class="cl">        <span class="n">kernel</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">((</span><span class="mi">5</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">uint8</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">mask</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">morphologyEx</span><span class="p">(</span><span class="n">mask</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">MORPH_OPEN</span><span class="p">,</span> <span class="n">kernel</span><span class="p">,</span> <span class="n">iterations</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># 轮廓检测</span>
</span></span><span class="line"><span class="cl">        <span class="n">contours</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">findContours</span><span class="p">(</span><span class="n">mask</span><span class="o">.</span><span class="n">copy</span><span class="p">(),</span> <span class="n">cv2</span><span class="o">.</span><span class="n">RETR_EXTERNAL</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">CHAIN_APPROX_SIMPLE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">centers</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">contour</span> <span class="ow">in</span> <span class="n">contours</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">area</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">contourArea</span><span class="p">(</span><span class="n">contour</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">area</span> <span class="o">&lt;</span> <span class="mi">500</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">continue</span>
</span></span><span class="line"><span class="cl">            
</span></span><span class="line"><span class="cl">            <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span> <span class="n">radius</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">minEnclosingCircle</span><span class="p">(</span><span class="n">contour</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">center</span> <span class="o">=</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">x</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">y</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="n">radius</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">radius</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            
</span></span><span class="line"><span class="cl">            <span class="c1"># 绘制检测结果</span>
</span></span><span class="line"><span class="cl">            <span class="n">cv2</span><span class="o">.</span><span class="n">circle</span><span class="p">(</span><span class="n">frame</span><span class="p">,</span> <span class="n">center</span><span class="p">,</span> <span class="n">radius</span><span class="p">,</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">cv2</span><span class="o">.</span><span class="n">circle</span><span class="p">(</span><span class="n">frame</span><span class="p">,</span> <span class="n">center</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">),</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            
</span></span><span class="line"><span class="cl">            <span class="n">centers</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">center</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># 跟踪更新</span>
</span></span><span class="line"><span class="cl">        <span class="n">tracked_objects</span> <span class="o">=</span> <span class="n">tracker</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">centers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># 绘制跟踪结果</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">obj_id</span><span class="p">,</span> <span class="n">obj</span> <span class="ow">in</span> <span class="n">tracked_objects</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="n">cv2</span><span class="o">.</span><span class="n">putText</span><span class="p">(</span><span class="n">frame</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;ID: </span><span class="si">{</span><span class="n">obj_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                        <span class="p">(</span><span class="n">obj</span><span class="o">.</span><span class="n">center</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">-</span> <span class="mi">20</span><span class="p">,</span> <span class="n">obj</span><span class="o">.</span><span class="n">center</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="mi">30</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                        <span class="n">cv2</span><span class="o">.</span><span class="n">FONT_HERSHEY_SIMPLEX</span><span class="p">,</span> <span class="mf">0.6</span><span class="p">,</span> <span class="p">(</span><span class="mi">255</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># 显示结果</span>
</span></span><span class="line"><span class="cl">        <span class="n">cv2</span><span class="o">.</span><span class="n">imshow</span><span class="p">(</span><span class="s2">&#34;Frame&#34;</span><span class="p">,</span> <span class="n">frame</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">cv2</span><span class="o">.</span><span class="n">imshow</span><span class="p">(</span><span class="s2">&#34;Mask&#34;</span><span class="p">,</span> <span class="n">mask</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">cv2</span><span class="o">.</span><span class="n">waitKey</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFF</span> <span class="o">==</span> <span class="nb">ord</span><span class="p">(</span><span class="s1">&#39;q&#39;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">cap</span><span class="o">.</span><span class="n">release</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">cv2</span><span class="o">.</span><span class="n">destroyAllWindows</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">main</span><span class="p">()</span>
</span></span></code></pre></div><p>这个代码可以直接运行。需要注意的是，你需要安装 opencv-python、numpy 和 scipy 这三个库：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install opencv-python numpy scipy
</span></span></code></pre></div><h2 id="十二参数调优指南">十二、参数调优指南</h2>
<p>代码写好了，但在实际应用中，你可能需要根据具体场景调整一些参数。这里给大家一个调优的参考：</p>
<p><strong>1. HSV 阈值范围</strong></p>
<ul>
<li>如果检测不到目标，尝试降低 S 和 V 的下限</li>
<li>如果误检太多，尝试提高 S 和 V 的下限</li>
<li>如果只检测到部分红色，可以适当扩大 H 的范围</li>
</ul>
<p><strong>2. 最小面积 min_area</strong></p>
<ul>
<li>根据目标在画面中的大小调整</li>
<li>640x480 分辨率下，500-2000 是常见范围</li>
<li>目标越小，这个值应该越小</li>
</ul>
<p><strong>3. 最大匹配距离 max_distance</strong></p>
<ul>
<li>取决于目标移动速度和帧率</li>
<li>快速移动的物体需要更大的值</li>
<li>太大可能导致 ID 混淆，太小可能导致跟丢</li>
</ul>
<p><strong>4. 最大消失帧数 max_disappeared</strong></p>
<ul>
<li>取决于帧率和遮挡情况</li>
<li>30fps 视频，30 帧就是 1 秒</li>
<li>遮挡时间长的场景可以适当增大</li>
</ul>
<p><strong>5. 高斯核大小</strong></p>
<ul>
<li>噪点多就用大一点的核，比如 (15, 15)</li>
<li>目标细节需要保留就用小一点的核，比如 (7, 7)</li>
</ul>
<h2 id="十三常见问题与解决">十三、常见问题与解决</h2>
<p><strong>问题 1：光照变化时检测不稳定</strong></p>
<ul>
<li>解决：可以尝试自适应阈值，或者先做直方图均衡化</li>
<li>或者采集不同光照下的样本，动态调整 HSV 范围</li>
</ul>
<p><strong>问题 2：红色背景导致误检</strong></p>
<ul>
<li>解决：增加形状过滤，或者考虑运动信息</li>
<li>或者用深度相机，先做背景减除</li>
</ul>
<p><strong>问题 3：目标交叉时 ID 混乱</strong></p>
<ul>
<li>解决：可以引入更多特征，比如颜色直方图、大小等</li>
<li>或者用更高级的跟踪算法，比如 SORT、DeepSORT</li>
</ul>
<p><strong>问题 4：部分遮挡时跟丢</strong></p>
<ul>
<li>解决：增大 max_disappeared 参数</li>
<li>或者加入运动预测（卡尔曼滤波）</li>
</ul>
<h2 id="十四进阶方向">十四、进阶方向</h2>
<p>这篇文章讲的是一个基础但实用的系统，如果你想继续深入，这里有几个方向可以探索：</p>
<p><strong>1. 卡尔曼滤波预测</strong>
在两帧之间用卡尔曼滤波预测目标位置，匹配时用预测位置代替上一帧位置。这样可以处理快速移动和短暂遮挡。</p>
<p><strong>2. DeepSORT</strong>
在 SORT 基础上加入表观特征（ReID），用深度学习模型提取目标特征，这样即使目标被完全遮挡后重新出现，也能正确关联。</p>
<p><strong>3. 多颜色检测</strong>
这套方法不仅可以检测红色，改一下 HSV 范围就能检测其他颜色。甚至可以同时检测多种颜色，用不同的颜色做不同的标记。</p>
<p><strong>4. 与其他传感器融合</strong>
比如结合深度相机的距离信息，结合 IMU 的运动信息，让跟踪更稳健。</p>
<h2 id="总结">总结</h2>
<p>我们从颜色空间选择讲起，一步步构建了一个完整的红色物体识别与多目标跟踪系统。核心要点是：</p>
<ol>
<li>用 HSV 颜色空间代替 RGB，获得更好的光照鲁棒性</li>
<li>红色需要检测两个区间，不要忘记 170-179</li>
<li>形态学处理是去除噪点的关键</li>
<li>基于距离的匈牙利算法匹配可以实现多目标跟踪</li>
<li>合理的参数调优是实际应用中不可或缺的</li>
</ol>
<p>这套方法虽然简单，但在很多实际场景中已经足够好用。希望这篇文章能帮助你快速上手计算机视觉项目。</p>
<p>（全文完，约6800字）</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
