<?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/%E5%AE%9E%E6%97%B6%E6%A3%80%E6%B5%8B/</link><description>Recent content in 实时检测 on Tech Snippets - 嵌入式技术笔记</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Wed, 15 Apr 2026 03:00:00 +0800</lastBuildDate><atom:link href="https://tech-snippets.xyz/tags/%E5%AE%9E%E6%97%B6%E6%A3%80%E6%B5%8B/index.xml" rel="self" type="application/rss+xml"/><item><title>实时检测指定颜色和形状的物体：算法方案对比与实现</title><link>https://tech-snippets.xyz/posts/real-time-color-shape-detection/</link><pubDate>Wed, 15 Apr 2026 03:00:00 +0800</pubDate><guid>https://tech-snippets.xyz/posts/real-time-color-shape-detection/</guid><description>详细对比传统图像处理与深度学习方案，提供完整可运行代码，适合嵌入式设备上的实时颜色形状检测应用</description><content:encoded><![CDATA[<h2 id="引言">引言</h2>
<p>在工业检测、机器人视觉、智能分拣等应用场景中，我们经常需要<strong>实时检测特定颜色和形状的物体</strong>。例如：</p>
<ul>
<li>冰壶比赛自动计分系统：检测冰面上的圆形冰壶</li>
<li>工业零件分拣：检测红色圆形螺丝、蓝色方形螺母</li>
<li>自动驾驶交通标志识别：检测圆形红圈禁令标志</li>
<li>AGV 小车导航：识别地面彩色圆形二维码</li>
</ul>
<p>本文将从简单到复杂，介绍几种常见的实现方案，对比它们的性能，并提供完整的开源参考代码，帮助你根据实际场景选择最合适的方案。</p>
<h2 id="方案对比总览">方案对比总览</h2>
<p>我们主要对比四种主流方案：</p>
<table>
<thead>
<tr>
<th>方案</th>
<th>原理</th>
<th>计算量</th>
<th>准确率</th>
<th>适合场景</th>
<th>MCU 能否运行</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>颜色分割 + 轮廓检测</strong></td>
<td>阈值分割 + 形状分析</td>
<td>极低</td>
<td>对颜色形状变化敏感</td>
<td>背景简单、光照稳定</td>
<td>✅ Cortex-M7 可以</td>
</tr>
<tr>
<td><strong>颜色空间转换 + Hough 变换</strong></td>
<td>Hough 圆/直线检测</td>
<td>低</td>
<td>圆形检测较好</td>
<td>固定形状检测</td>
<td>✅ Cortex-M4 可以</td>
</tr>
<tr>
<td><strong>Blob 分析 + 特征匹配</strong></td>
<td>连通域分析 + 形状分类</td>
<td>中</td>
<td>中等</td>
<td>多目标批量处理</td>
<td>✅ Cortex-M7 可以</td>
</tr>
<tr>
<td><strong>深度学习目标检测</strong></td>
<td>YOLO/SSD 直接检测</td>
<td>高</td>
<td>鲁棒性强</td>
<td>复杂背景、光照变化</td>
<td>❌ 需要 MCU+NPU 或 Linux</td>
</tr>
</tbody>
</table>
<p>下面详细介绍每种方案的实现。</p>
<h2 id="方案一颜色分割--轮廓检测">方案一：颜色分割 + 轮廓检测</h2>
<h3 id="11-算法流程">1.1 算法流程</h3>
<figure>
<svg viewBox="0 0 700 280" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <style>
      .box { fill: #1a1a2e; stroke: #4a9eff; stroke-width: 2; rx: 6; }
      .text { fill: #e0e0e0; font-family: Arial, sans-serif; font-size: 13px; text-anchor: middle; }
      .label { fill: #4a9eff; font-family: Arial, sans-serif; font-size: 14px; font-weight: bold; }
      .arrow { stroke: #4a9eff; stroke-width: 2; fill: none; marker-end: url(#arrow); }
    </style>
    <marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#4a9eff"/>
    </marker>
  </defs>
  <rect class="box" x="50" y="20" width="140" height="60"/>
  <text class="label" x="120" y="45">原始图像</text>
  <text class="text" x="120" y="62">RGB/BGR</text>
  <path class="arrow" d="M190 50 L240 50"/>
  <rect class="box" x="240" y="20" width="140" height="60"/>
  <text class="label" x="310" y="45">颜色空间转换</text>
  <text class="text" x="310" y="62">RGB → HSV</text>
  <path class="arrow" d="M380 50 L430 50"/>
  <rect class="box" x="430" y="20" width="140" height="60"/>
  <text class="label" x="500" y="45">颜色阈值分割</text>
  <text class="text" x="500" y="62">二值掩码</text>
  <path class="arrow" d="M500 80 L500 120"/>
  <rect class="box" x="430" y="120" width="140" height="60"/>
  <text class="label" x="500" y="145">形态学处理</text>
  <text class="text" x="500" y="162">腐蚀 + 膨胀</text>
  <path class="arrow" d="M430 150 L380 150"/>
  <rect class="box" x="240" y="120" width="140" height="60"/>
  <text class="label" x="310" y="145">查找轮廓</text>
  <text class="text" x="310" y="162">cv2.findContours</text>
  <path class="arrow" d="M240 150 L190 150"/>
  <rect class="box" x="50" y="120" width="140" height="60"/>
  <text class="label" x="120" y="145">形状特征计算</text>
  <text class="text" x="120" y="162">面积、周长、圆形度</text>
  <path class="arrow" d="M120 180 L120 220"/>
  <rect class="box" x="50" y="220" width="600" height="40"/>
  <text class="label" x="350" y="245">输出：符合颜色和形状要求的目标</text>
</svg>
<figcaption style="text-align:center;color:#888;font-size:12px;margin-top:8px;">颜色分割 + 轮廓检测流程图</figcaption>
</figure>
<h3 id="12-核心原理">1.2 核心原理</h3>
<ol>
<li><strong>颜色空间转换</strong>：从 RGB 转到 HSV 颜色空间，更容易按颜色分割</li>
<li><strong>阈值分割</strong>：对 H/S/V 三个通道设置范围，得到二值掩码</li>
<li><strong>形态学处理</strong>：腐蚀 + 膨胀去除噪声</li>
<li><strong>轮廓查找</strong>：找到所有连通区域</li>
<li><strong>形状特征计算</strong>：计算面积、周长、圆形度、矩形度等特征</li>
<li><strong>特征匹配</strong>：筛选符合指定形状的目标</li>
</ol>
<h3 id="13-完整-python-实现">1.3 完整 Python 实现</h3>
<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></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">detect_color_shape</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">image</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">    <span class="n">color_lower</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">120</span><span class="p">,</span> <span class="mi">70</span><span class="p">]),</span> 
</span></span><span class="line"><span class="cl">    <span class="n">color_upper</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">shape_type</span><span class="o">=</span><span class="s2">&#34;circle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">min_area</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_area</span><span class="o">=</span><span class="mi">10000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">circularity_threshold</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">aspect_ratio_range</span><span class="o">=</span><span class="p">(</span><span class="mf">0.9</span><span class="p">,</span> <span class="mf">1.1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    检测指定颜色和形状的物体
</span></span></span><span class="line"><span class="cl"><span class="s2">    
</span></span></span><span class="line"><span class="cl"><span class="s2">    参数:
</span></span></span><span class="line"><span class="cl"><span class="s2">        image: 输入 RGB/BGR 图像
</span></span></span><span class="line"><span class="cl"><span class="s2">        color_lower: HSV 颜色下限
</span></span></span><span class="line"><span class="cl"><span class="s2">        color_upper: HSV 颜色上限
</span></span></span><span class="line"><span class="cl"><span class="s2">        shape_type: &#34;circle&#34; / &#34;square&#34; / &#34;rectangle&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">        min_area: 最小面积（像素）
</span></span></span><span class="line"><span class="cl"><span class="s2">        max_area: 最大面积（像素）
</span></span></span><span class="line"><span class="cl"><span class="s2">        circularity_threshold: 圆形度阈值（0~1，越大越圆）
</span></span></span><span class="line"><span class="cl"><span class="s2">        aspect_ratio_range: 宽高比范围（方形接近 1）
</span></span></span><span class="line"><span class="cl"><span class="s2">    
</span></span></span><span class="line"><span class="cl"><span class="s2">    返回:
</span></span></span><span class="line"><span class="cl"><span class="s2">        detections: 检测结果列表 [(x, y, w, h, contour), ...]
</span></span></span><span class="line"><span class="cl"><span class="s2">        mask: 颜色分割掩码（用于调试）
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 1. 颜色空间转换：BGR → 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">image</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"># 2. 颜色阈值分割</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">inRange</span><span class="p">(</span><span class="n">hsv</span><span class="p">,</span> <span class="n">color_lower</span><span class="p">,</span> <span class="n">color_upper</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># 3. 形态学处理去除噪声</span>
</span></span><span class="line"><span class="cl">    <span class="n">kernel</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getStructuringElement</span><span class="p">(</span><span class="n">cv2</span><span class="o">.</span><span class="n">MORPH_ELLIPSE</span><span class="p">,</span> <span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">5</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></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_CLOSE</span><span class="p">,</span> <span class="n">kernel</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># 4. 查找轮廓</span>
</span></span><span class="line"><span class="cl">    <span class="n">contours</span><span class="p">,</span> <span class="n">hierarchy</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">findContours</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">mask</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></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">detections</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">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="c1"># 计算轮廓面积，过滤太小/太大的</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="n">min_area</span> <span class="ow">or</span> <span class="n">area</span> <span class="o">&gt;</span> <span class="n">max_area</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">perimeter</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">arcLength</span><span class="p">(</span><span class="n">contour</span><span class="p">,</span> <span class="kc">True</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">approx</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">approxPolyDP</span><span class="p">(</span><span class="n">contour</span><span class="p">,</span> <span class="mf">0.04</span> <span class="o">*</span> <span class="n">perimeter</span><span class="p">,</span> <span class="kc">True</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">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">w</span><span class="p">,</span> <span class="n">h</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">boundingRect</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">matched</span> <span class="o">=</span> <span class="kc">False</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">shape_type</span> <span class="o">==</span> <span class="s2">&#34;circle&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 圆形度 = 4π * 面积 / (周长^2)</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 完美圆形 = 1，越不规则值越小</span>
</span></span><span class="line"><span class="cl">            <span class="n">circularity</span> <span class="o">=</span> <span class="mi">4</span> <span class="o">*</span> <span class="n">np</span><span class="o">.</span><span class="n">pi</span> <span class="o">*</span> <span class="n">area</span> <span class="o">/</span> <span class="p">(</span><span class="n">perimeter</span> <span class="o">*</span> <span class="n">perimeter</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">circularity</span> <span class="o">&gt;=</span> <span class="n">circularity_threshold</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">matched</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">shape_type</span> <span class="o">==</span> <span class="s2">&#34;square&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 宽高比接近 1，且顶点数约为 4</span>
</span></span><span class="line"><span class="cl">            <span class="n">aspect_ratio</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="o">/</span> <span class="n">h</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">approx</span><span class="p">)</span> <span class="o">==</span> <span class="mi">4</span> <span class="ow">and</span> 
</span></span><span class="line"><span class="cl">                <span class="n">aspect_ratio</span> <span class="o">&gt;=</span> <span class="n">aspect_ratio_range</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">and</span> 
</span></span><span class="line"><span class="cl">                <span class="n">aspect_ratio</span> <span class="o">&lt;=</span> <span class="n">aspect_ratio_range</span><span class="p">[</span><span class="mi">1</span><span class="p">]):</span>
</span></span><span class="line"><span class="cl">                <span class="n">matched</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">shape_type</span> <span class="o">==</span> <span class="s2">&#34;rectangle&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># 顶点数约为 4 即可</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">approx</span><span class="p">)</span> <span class="o">==</span> <span class="mi">4</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">matched</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">shape_type</span> <span class="o">==</span> <span class="s2">&#34;triangle&#34;</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">approx</span><span class="p">)</span> <span class="o">==</span> <span class="mi">3</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">matched</span> <span class="o">=</span> <span class="kc">True</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">matched</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">center_x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="n">w</span> <span class="o">//</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">            <span class="n">center_y</span> <span class="o">=</span> <span class="n">y</span> <span class="o">+</span> <span class="n">h</span> <span class="o">//</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">            <span class="n">detections</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">center_x</span><span class="p">,</span> <span class="n">center_y</span><span class="p">,</span> <span class="n">w</span><span class="p">,</span> <span class="n">h</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="k">return</span> <span class="n">detections</span><span class="p">,</span> <span class="n">mask</span>
</span></span></code></pre></div><h3 id="14-使用示例">1.4 使用示例</h3>
<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="c1"># HSV 红色范围（两种分段，因为红色在 H 通道首尾）</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">120</span><span class="p">,</span> <span class="mi">70</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">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">120</span><span class="p">,</span> <span class="mi">70</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">180</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></span><span class="line"><span class="cl"><span class="c1"># 读取图像</span>
</span></span><span class="line"><span class="cl"><span class="n">image</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">imread</span><span class="p">(</span><span class="s2">&#34;test_image.jpg&#34;</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">detections1</span><span class="p">,</span> <span class="n">mask1</span> <span class="o">=</span> <span class="n">detect_color_shape</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">image</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 class="n">shape_type</span><span class="o">=</span><span class="s2">&#34;circle&#34;</span><span class="p">,</span> <span class="n">min_area</span><span class="o">=</span><span class="mi">500</span>
</span></span><span class="line"><span class="cl"><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">detections2</span><span class="p">,</span> <span class="n">mask2</span> <span class="o">=</span> <span class="n">detect_color_shape</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">image</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 class="n">shape_type</span><span class="o">=</span><span class="s2">&#34;circle&#34;</span><span class="p">,</span> <span class="n">min_area</span><span class="o">=</span><span class="mi">500</span>
</span></span><span class="line"><span class="cl"><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">all_detections</span> <span class="o">=</span> <span class="n">detections1</span> <span class="o">+</span> <span class="n">detections2</span>
</span></span><span class="line"><span class="cl"><span class="n">combined_mask</span> <span class="o">=</span> <span class="n">mask1</span> <span class="o">|</span> <span class="n">mask2</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="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">w</span><span class="p">,</span> <span class="n">h</span><span class="p">,</span> <span class="n">contour</span><span class="p">)</span> <span class="ow">in</span> <span class="n">all_detections</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">drawContours</span><span class="p">(</span><span class="n">image</span><span class="p">,</span> <span class="p">[</span><span class="n">contour</span><span class="p">],</span> <span class="o">-</span><span class="mi">1</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">image</span><span class="p">,</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span> <span class="mi">3</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 class="n">cv2</span><span class="o">.</span><span class="n">putText</span><span class="p">(</span><span class="n">image</span><span class="p">,</span> <span class="s2">&#34;Red Circle&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">x</span><span class="o">-</span><span class="mi">20</span><span class="p">,</span> <span class="n">y</span><span class="o">-</span><span class="n">h</span><span class="o">-</span><span class="mi">10</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.5</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></span><span class="line"><span class="cl"><span class="n">cv2</span><span class="o">.</span><span class="n">imwrite</span><span class="p">(</span><span class="s2">&#34;result.jpg&#34;</span><span class="p">,</span> <span class="n">image</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;检测到 </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">all_detections</span><span class="p">)</span><span class="si">}</span><span class="s2"> 个红色圆形物体&#34;</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="15-常见颜色-hsv-范围参考">1.5 常见颜色 HSV 范围参考</h3>
<table>
<thead>
<tr>
<th>颜色</th>
<th>H 下限</th>
<th>H 上限</th>
<th>S 下限</th>
<th>S 上限</th>
<th>V 下限</th>
<th>V 上限</th>
</tr>
</thead>
<tbody>
<tr>
<td>红色</td>
<td>0/170</td>
<td>10/180</td>
<td>120</td>
<td>255</td>
<td>70</td>
<td>255</td>
</tr>
<tr>
<td>橙色</td>
<td>11</td>
<td>25</td>
<td>120</td>
<td>255</td>
<td>70</td>
<td>255</td>
</tr>
<tr>
<td>黄色</td>
<td>26</td>
<td>35</td>
<td>120</td>
<td>255</td>
<td>70</td>
<td>255</td>
</tr>
<tr>
<td>绿色</td>
<td>36</td>
<td>70</td>
<td>120</td>
<td>255</td>
<td>70</td>
<td>255</td>
</tr>
<tr>
<td>青色</td>
<td>71</td>
<td>99</td>
<td>120</td>
<td>255</td>
<td>70</td>
<td>255</td>
</tr>
<tr>
<td>蓝色</td>
<td>100</td>
<td>124</td>
<td>120</td>
<td>255</td>
<td>70</td>
<td>255</td>
</tr>
<tr>
<td>紫色</td>
<td>125</td>
<td>155</td>
<td>120</td>
<td>255</td>
<td>70</td>
<td>255</td>
</tr>
<tr>
<td>粉色</td>
<td>156</td>
<td>169</td>
<td>120</td>
<td>255</td>
<td>70</td>
<td>255</td>
</tr>
<tr>
<td>黑色</td>
<td>0</td>
<td>180</td>
<td>0</td>
<td>255</td>
<td>0</td>
<td>46</td>
</tr>
<tr>
<td>灰色</td>
<td>0</td>
<td>180</td>
<td>0</td>
<td>43</td>
<td>47</td>
<td>221</td>
</tr>
<tr>
<td>白色</td>
<td>0</td>
<td>180</td>
<td>0</td>
<td>30</td>
<td>222</td>
<td>255</td>
</tr>
</tbody>
</table>
<blockquote>
<p>注意：不同相机的白平衡和亮度设置不同，实际使用时需要根据你的图像微调范围。建议用 OpenCV 窗口滑动条调参：</p>
</blockquote>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_hsv_trackbars</span><span class="p">(</span><span class="n">window_name</span><span class="o">=</span><span class="s2">&#34;HSV Tuner&#34;</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">namedWindow</span><span class="p">(</span><span class="n">window_name</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">createTrackbar</span><span class="p">(</span><span class="s2">&#34;H_min&#34;</span><span class="p">,</span> <span class="n">window_name</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">180</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="kc">None</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">createTrackbar</span><span class="p">(</span><span class="s2">&#34;H_max&#34;</span><span class="p">,</span> <span class="n">window_name</span><span class="p">,</span> <span class="mi">180</span><span class="p">,</span> <span class="mi">180</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="kc">None</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">createTrackbar</span><span class="p">(</span><span class="s2">&#34;S_min&#34;</span><span class="p">,</span> <span class="n">window_name</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="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="kc">None</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">createTrackbar</span><span class="p">(</span><span class="s2">&#34;S_max&#34;</span><span class="p">,</span> <span class="n">window_name</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="kc">None</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">createTrackbar</span><span class="p">(</span><span class="s2">&#34;V_min&#34;</span><span class="p">,</span> <span class="n">window_name</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="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="kc">None</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">createTrackbar</span><span class="p">(</span><span class="s2">&#34;V_max&#34;</span><span class="p">,</span> <span class="n">window_name</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="kc">None</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">get_hsv_from_trackbars</span><span class="p">(</span><span class="n">window_name</span><span class="o">=</span><span class="s2">&#34;HSV Tuner&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">h_min</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getTrackbarPos</span><span class="p">(</span><span class="s2">&#34;H_min&#34;</span><span class="p">,</span> <span class="n">window_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">h_max</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getTrackbarPos</span><span class="p">(</span><span class="s2">&#34;H_max&#34;</span><span class="p">,</span> <span class="n">window_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">s_min</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getTrackbarPos</span><span class="p">(</span><span class="s2">&#34;S_min&#34;</span><span class="p">,</span> <span class="n">window_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">s_max</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getTrackbarPos</span><span class="p">(</span><span class="s2">&#34;S_max&#34;</span><span class="p">,</span> <span class="n">window_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">v_min</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getTrackbarPos</span><span class="p">(</span><span class="s2">&#34;V_min&#34;</span><span class="p">,</span> <span class="n">window_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">v_max</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getTrackbarPos</span><span class="p">(</span><span class="s2">&#34;V_max&#34;</span><span class="p">,</span> <span class="n">window_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="n">h_min</span><span class="p">,</span> <span class="n">s_min</span><span class="p">,</span> <span class="n">v_min</span><span class="p">]),</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="n">h_max</span><span class="p">,</span> <span class="n">s_max</span><span class="p">,</span> <span class="n">v_max</span><span class="p">])</span>
</span></span></code></pre></div><h3 id="16-优缺点分析">1.6 优缺点分析</h3>
<p>✅ <strong>优点</strong>：</p>
<ul>
<li>计算量极小，<strong>100MHz Cortex-M 可以实时处理 QVGA 图像</strong></li>
<li>代码简单，容易理解和调试</li>
<li>参数可调，适合固定场景</li>
</ul>
<p>❌ <strong>缺点</strong>：</p>
<ul>
<li>对光照变化敏感，光照一变颜色就不准了</li>
<li>对遮挡敏感，部分遮挡会改变轮廓形状</li>
<li>只能检测大致形状，无法区分语义</li>
</ul>
<h2 id="方案二hough-变换检测圆形直线">方案二：Hough 变换检测圆形/直线</h2>
<h3 id="21-原理简介">2.1 原理简介</h3>
<p>Hough 变换是一种经典的形状检测算法，它通过投票机制在参数空间中找到最可能的形状参数。对于圆形，参数空间是 <code>(x, y, r)</code> 三个维度：圆心坐标 + 半径。</p>
<h3 id="22-opencv-实现">2.2 OpenCV 实现</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">detect_circles_hough</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">image</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">    <span class="n">dp</span><span class="o">=</span><span class="mf">1.2</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">    <span class="n">min_dist</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">    <span class="n">param1</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">    <span class="n">param2</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">min_radius</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">    <span class="n">max_radius</span><span class="o">=</span><span class="mi">100</span>
</span></span><span class="line"><span class="cl"><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    使用 Hough 变换检测圆形
</span></span></span><span class="line"><span class="cl"><span class="s2">    
</span></span></span><span class="line"><span class="cl"><span class="s2">    参数说明:
</span></span></span><span class="line"><span class="cl"><span class="s2">        dp: 累加器分辨率反比，1 = 原图分辨率
</span></span></span><span class="line"><span class="cl"><span class="s2">        min_dist: 两个圆心之间的最小距离（避免重复检测）
</span></span></span><span class="line"><span class="cl"><span class="s2">        param1: Canny 边缘检测高阈值
</span></span></span><span class="line"><span class="cl"><span class="s2">        param2: 累加器阈值，越大检测越少但越准
</span></span></span><span class="line"><span class="cl"><span class="s2">        min_radius/max_radius: 半径范围
</span></span></span><span class="line"><span class="cl"><span class="s2">    
</span></span></span><span class="line"><span class="cl"><span class="s2">    返回:
</span></span></span><span class="line"><span class="cl"><span class="s2">        circles: [(x, y, r), ...]
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 转灰度</span>
</span></span><span class="line"><span class="cl">    <span class="n">gray</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">image</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_BGR2GRAY</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">gray</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">gray</span><span class="p">,</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="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"># Hough 变换检测圆</span>
</span></span><span class="line"><span class="cl">    <span class="n">circles</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">HoughCircles</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">gray</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">HOUGH_GRADIENT</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="n">dp</span><span class="o">=</span><span class="n">dp</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="n">minDist</span><span class="o">=</span><span class="n">min_dist</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">param1</span><span class="o">=</span><span class="n">param1</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="n">param2</span><span class="o">=</span><span class="n">param2</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="n">minRadius</span><span class="o">=</span><span class="n">min_radius</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="n">maxRadius</span><span class="o">=</span><span class="n">max_radius</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">circles</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">circles</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">uint16</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">around</span><span class="p">(</span><span class="n">circles</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">circles</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="p">:]:</span>
</span></span><span class="line"><span class="cl">            <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">r</span> <span class="o">=</span> <span class="n">i</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">i</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">i</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">result</span><span class="o">.</span><span class="n">append</span><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">r</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">result</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">image</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">imread</span><span class="p">(</span><span class="s2">&#34;curling.jpg&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">circles</span> <span class="o">=</span> <span class="n">detect_circles_hough</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">image</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">    <span class="n">min_radius</span><span class="o">=</span><span class="mi">80</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">    <span class="n">max_radius</span><span class="o">=</span><span class="mi">120</span>  <span class="c1"># 冰壶直径约 100 像素</span>
</span></span><span class="line"><span class="cl"><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="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">r</span><span class="p">)</span> <span class="ow">in</span> <span class="n">circles</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">image</span><span class="p">,</span> <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">r</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 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">image</span><span class="p">,</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span> <span class="mi">3</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 class="c1"># 圆心</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;检测到 </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">circles</span><span class="p">)</span><span class="si">}</span><span class="s2"> 个冰壶&#34;</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="23-hough-圆检测结果示意">2.3 Hough 圆检测结果示意</h3>
<figure>
<svg viewBox="0 0 500 350" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <style>
      .ice { fill: #e6f3ff; stroke: #88aadd; stroke-width: 2; rx: 8; }
      .stone { fill: #2d2d2d; stroke: #fff; stroke-width: 2; }
      .detected { fill: none; stroke: #00ff00; stroke-width: 3; }
      .text { fill: #333; font-family: Arial, sans-serif; font-size: 12px; }
    </style>
  </defs>
  <!-- 冰面 -->
  <rect class="ice" x="50" y="20" width="400" height="300"/>
  <text class="text" x="250" y="40" text-anchor="middle">冰面</text>
  <!-- 冰壶 -->
  <circle class="stone" cx="120" cy="150" r="40"/>
  <circle class="stone" cx="200" cy="180" r="40"/>
  <circle class="stone" cx="280" cy="120" r="40"/>
  <circle class="stone" cx="350" cy="200" r="40"/>
  <!-- 检测框 -->
  <circle class="detected" cx="120" cy="150" r="40"/>
  <circle class="detected" cx="200" cy="180" r="40"/>
  <circle class="detected" cx="280" cy="120" r="40"/>
  <circle class="detected" cx="350" cy="200" r="40"/>
<p><text class="text" x="250" y="320" text-anchor="middle">Hough 变换可以准确检测已知半径的圆形冰壶</text>
</svg></p>
<figcaption style="text-align:center;color:#888;font-size:12px;margin-top:8px;">Hough 圆检测冰壶效果示意图</figcaption>
</figure>
<h3 id="24-优缺点分析">2.4 优缺点分析</h3>
<p>✅ <strong>优点</strong>：</p>
<ul>
<li>计算量比轮廓检测略大，但仍然很小</li>
<li>专门针对圆形检测，效果比轮廓好</li>
<li>可以直接得到半径和圆心坐标，定位准确</li>
</ul>
<p>❌ <strong>缺点</strong>：</p>
<ul>
<li>需要知道大致半径范围，半径范围设置不好就漏检</li>
<li>重叠圆容易误检</li>
<li>对椭圆形变形处理不好</li>
<li>只能检测圆，不能检测任意形状</li>
</ul>
<h2 id="方案三blob-分析连通域检测">方案三：Blob 分析（连通域检测）</h2>
<h3 id="31-原理简介">3.1 原理简介</h3>
<p>Blob（斑点/连通域）分析是指先找出图像中所有颜色相似的连通区域，然后根据面积、圆形度、惯性比等特征筛选目标。OpenCV 提供了 <code>SimpleBlobDetector</code> 方便使用。</p>
<h3 id="32-opencv-实现">3.2 OpenCV 实现</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">detect_blobs_by_color_and_shape</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">image</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">color_lower</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">color_upper</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">min_area</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_area</span><span class="o">=</span><span class="mi">10000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">min_circularity</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_circularity</span><span class="o">=</span><span class="mf">1.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">min_inertia_ratio</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">min_convexity</span><span class="o">=</span><span class="mf">0.5</span>
</span></span><span class="line"><span class="cl"><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    基于 Blob 分析检测指定颜色和形状的物体
</span></span></span><span class="line"><span class="cl"><span class="s2">    
</span></span></span><span class="line"><span class="cl"><span class="s2">    参数:
</span></span></span><span class="line"><span class="cl"><span class="s2">        min_inertia_ratio: 惯性比，圆 ≈ 1，长条 ≈ 0
</span></span></span><span class="line"><span class="cl"><span class="s2">        min_convexity: 凸包面积比，越凸越接近 1
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 颜色分割得到掩码</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">image</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 class="n">mask</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">color_lower</span><span class="p">,</span> <span class="n">color_upper</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># 设置 Blob 检测器参数</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">SimpleBlobDetector_Params</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">params</span><span class="o">.</span><span class="n">filterByArea</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span><span class="o">.</span><span class="n">minArea</span> <span class="o">=</span> <span class="n">min_area</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span><span class="o">.</span><span class="n">maxArea</span> <span class="o">=</span> <span class="n">max_area</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">params</span><span class="o">.</span><span class="n">filterByCircularity</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span><span class="o">.</span><span class="n">minCircularity</span> <span class="o">=</span> <span class="n">min_circularity</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span><span class="o">.</span><span class="n">maxCircularity</span> <span class="o">=</span> <span class="n">max_circularity</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">params</span><span class="o">.</span><span class="n">filterByInertia</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span><span class="o">.</span><span class="n">minInertiaRatio</span> <span class="o">=</span> <span class="n">min_inertia_ratio</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">params</span><span class="o">.</span><span class="n">filterByConvexity</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span><span class="o">.</span><span class="n">minConvexity</span> <span class="o">=</span> <span class="n">min_convexity</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">detector</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">SimpleBlobDetector_create</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">keypoints</span> <span class="o">=</span> <span class="n">detector</span><span class="o">.</span><span class="n">detect</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="c1"># 提取结果</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</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">kp</span> <span class="ow">in</span> <span class="n">keypoints</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">x</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">kp</span><span class="o">.</span><span class="n">pt</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">        <span class="n">y</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">kp</span><span class="o">.</span><span class="n">pt</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">        <span class="n">size</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">kp</span><span class="o">.</span><span class="n">size</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">r</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">size</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span><span class="o">.</span><span class="n">append</span><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">r</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">result</span><span class="p">,</span> <span class="n">mask</span><span class="p">,</span> <span class="n">keypoints</span>
</span></span></code></pre></div><h3 id="33-blob-特征参数说明">3.3 Blob 特征参数说明</h3>
<table>
<thead>
<tr>
<th>参数</th>
<th>取值范围</th>
<th>完美圆形值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>面积</strong></td>
<td>像素数</td>
<td>-</td>
<td>过滤太小太大的目标</td>
</tr>
<tr>
<td><strong>圆形度</strong></td>
<td>0~1</td>
<td>≈ 1.0</td>
<td>越接近 1 越圆</td>
</tr>
<tr>
<td><strong>凸性</strong></td>
<td>0~1</td>
<td>≈ 1.0</td>
<td>凸包面积 / 轮廓面积</td>
</tr>
<tr>
<td><strong>惯性比</strong></td>
<td>0~1</td>
<td>≈ 1.0</td>
<td>对圆形的度量</td>
</tr>
</tbody>
</table>
<p><strong>圆形度 vs 惯性比</strong>：</p>
<ul>
<li>圆形度对轮廓噪声更敏感，因为它完全基于周长和面积</li>
<li>惯性比基于二阶矩，对噪声鲁棒性更好</li>
</ul>
<h3 id="34-优缺点分析">3.4 优缺点分析</h3>
<p>✅ <strong>优点</strong>：</p>
<ul>
<li>内置参数丰富，可以灵活过滤各种形状</li>
<li>一次检测多个 Blob，批量处理高效</li>
<li>参数直观，容易调整</li>
<li>计算量中等，适合嵌入式</li>
</ul>
<p>❌ <strong>缺点</strong>：</p>
<ul>
<li>需要先做颜色分割，对光照变化敏感</li>
<li>重叠 Blob 难以分开</li>
<li>对于非凸形状处理不好</li>
</ul>
<h2 id="方案四深度学习目标检测yolo--ssd">方案四：深度学习目标检测（YOLO / SSD）</h2>
<h3 id="41-方案架构">4.1 方案架构</h3>
<p>当背景复杂、光照变化大时，传统方法效果不好，可以使用深度学习目标检测直接训练一个检测器：</p>
<figure>
<svg viewBox="0 0 650 320" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <style>
      .box { fill: #1a1a2e; stroke: #4a9eff; stroke-width: 2; rx: 6; }
      .text { fill: #e0e0e0; font-family: Arial, sans-serif; font-size: 13px; text-anchor: middle; }
      .label { fill: #4a9eff; font-family: Arial, sans-serif; font-size: 14px; font-weight: bold; }
      .arrow { stroke: #4a9eff; stroke-width: 2; fill: none; marker-end: url(#arrow); }
    </style>
    <marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#4a9eff"/>
    </marker>
  </defs>
  <rect class="box" x="50" y="20" width="180" height="60"/>
  <text class="label" x="140" y="45">输入图像</text>
  <text class="text" x="140" y="62">任意尺寸，RGB</text>
  <path class="arrow" d="M230 50 L280 50"/>
  <rect class="box" x="280" y="20" width="240" height="60"/>
  <text class="label" x="400" y="45">YOLO / SSD 前向传播</text>
  <text class="text" x="400" y="62">输出边框 + 类别 + 置信度</text>
  <path class="arrow" d="M400 80 L400 120"/>
  <rect class="box" x="280" y="120" width="240" height="60"/>
  <text class="label" x="400" y="145">NMS 非极大值抑制</text>
  <text class="text" x="400" y="162">去除重叠框，保留最佳检测</text>
  <path class="arrow" d="M400 180 L400 220"/>
  <rect class="box" x="100" y="220" width="450" height="60"/>
  <text class="label" x="325" y="245">输出：所有检测到的目标 (x, y, w, h, class_id, conf)</text>
  <rect class="box" x="500" y="20" x="80" y="160" rx="5"/>
  <text class="label" x="540" y="90" transform="rotate(90, 540, 90)">深度学习</text>
</svg>
<figcaption style="text-align:center;color:#888;font-size:12px;margin-top:8px;">深度学习目标检测流程</figcaption>
</figure>
<h3 id="42-使用-yolov8-检测示例">4.2 使用 YOLOv8 检测示例</h3>
<p>需要先安装 ultralytics：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install ultralytics
</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="kn">from</span> <span class="nn">ultralytics</span> <span class="kn">import</span> <span class="n">YOLO</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">model</span> <span class="o">=</span> <span class="n">YOLO</span><span class="p">(</span><span class="s2">&#34;yolov8n.pt&#34;</span><span class="p">)</span>  <span class="c1"># 检测自定义类需要重新训练</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 假设我们训练了一个检测 &#34;red_circle&#34;, &#34;blue_square&#34; 的模型</span>
</span></span><span class="line"><span class="cl"><span class="c1"># model = YOLO(&#34;best.pt&#34;)  # 你的训练结果</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">detect_objects_yolo</span><span class="p">(</span><span class="n">image</span><span class="p">,</span> <span class="n">conf_threshold</span><span class="o">=</span><span class="mf">0.5</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;YOLOv8 目标检测&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">results</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="n">image</span><span class="p">,</span> <span class="n">conf</span><span class="o">=</span><span class="n">conf_threshold</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="n">detections</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">box</span> <span class="ow">in</span> <span class="n">results</span><span class="o">.</span><span class="n">boxes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">,</span> <span class="n">x2</span><span class="p">,</span> <span class="n">y2</span> <span class="o">=</span> <span class="n">box</span><span class="o">.</span><span class="n">xyxy</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">tolist</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">conf</span> <span class="o">=</span> <span class="n">box</span><span class="o">.</span><span class="n">conf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">item</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">class_id</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">box</span><span class="o">.</span><span class="n">cls</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">item</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">class_name</span> <span class="o">=</span> <span class="n">model</span><span class="o">.</span><span class="n">names</span><span class="p">[</span><span class="n">class_id</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">x</span> <span class="o">=</span> <span class="nb">int</span><span class="p">((</span><span class="n">x1</span> <span class="o">+</span> <span class="n">x2</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">y</span> <span class="o">=</span> <span class="nb">int</span><span class="p">((</span><span class="n">y1</span> <span class="o">+</span> <span class="n">y2</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">w</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">x2</span> <span class="o">-</span> <span class="n">x1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">h</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">y2</span> <span class="o">-</span> <span class="n">y1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">detections</span><span class="o">.</span><span class="n">append</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;bbox&#34;</span><span class="p">:</span> <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">w</span><span class="p">,</span> <span class="n">h</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;class_name&#34;</span><span class="p">:</span> <span class="n">class_name</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;confidence&#34;</span><span class="p">:</span> <span class="n">conf</span>
</span></span><span class="line"><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">detections</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">image</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">imread</span><span class="p">(</span><span class="s2">&#34;test.jpg&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">detections</span> <span class="o">=</span> <span class="n">detect_objects_yolo</span><span class="p">(</span><span class="n">image</span><span class="p">,</span> <span class="n">conf_threshold</span><span class="o">=</span><span class="mf">0.5</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">det</span> <span class="ow">in</span> <span class="n">detections</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">w</span><span class="p">,</span> <span class="n">h</span> <span class="o">=</span> <span class="n">det</span><span class="p">[</span><span class="s2">&#34;bbox&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span> <span class="o">=</span> <span class="n">det</span><span class="p">[</span><span class="s2">&#34;class_name&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">conf</span> <span class="o">=</span> <span class="n">det</span><span class="p">[</span><span class="s2">&#34;confidence&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">x1</span> <span class="o">=</span> <span class="n">x</span> <span class="o">-</span> <span class="n">w</span> <span class="o">//</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">    <span class="n">y1</span> <span class="o">=</span> <span class="n">y</span> <span class="o">-</span> <span class="n">h</span> <span class="o">//</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">    <span class="n">x2</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="n">w</span> <span class="o">//</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">    <span class="n">y2</span> <span class="o">=</span> <span class="n">y</span> <span class="o">+</span> <span class="n">h</span> <span class="o">//</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">cv2</span><span class="o">.</span><span class="n">rectangle</span><span class="p">(</span><span class="n">image</span><span class="p">,</span> <span class="p">(</span><span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">),</span> <span class="p">(</span><span class="n">x2</span><span class="p">,</span> <span class="n">y2</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">putText</span><span class="p">(</span><span class="n">image</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">conf</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="o">-</span><span class="mi">10</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.5</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></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;检测到 </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">detections</span><span class="p">)</span><span class="si">}</span><span class="s2"> 个目标&#34;</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="43-数据收集与训练">4.3 数据收集与训练</h3>
<p>如果要检测特定颜色和形状的组合，你需要收集自己的数据集：</p>
<ol>
<li><strong>收集数据</strong>：拍摄至少 100-200 张包含目标的不同角度、光照下的图片</li>
<li><strong>标注数据</strong>：使用 LabelImg 或 Roboflow 标注边框和类别
<ul>
<li>类别示例：<code>red_circle</code>, <code>red_square</code>, <code>blue_circle</code>, <code>blue_square</code></li>
</ul>
</li>
<li><strong>划分数据集</strong>：训练集 80%，验证集 10%，测试集 10%</li>
<li><strong>训练模型</strong>：使用 YOLOv8 训练
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">yolo detect train <span class="nv">data</span><span class="o">=</span>data.yaml <span class="nv">model</span><span class="o">=</span>yolov8n.pt <span class="nv">epochs</span><span class="o">=</span><span class="m">50</span> <span class="nv">imgsz</span><span class="o">=</span><span class="m">640</span>
</span></span></code></pre></div></li>
<li><strong>量化转换</strong>：如果部署到嵌入式设备，需要转换成 INT8 量化模型</li>
</ol>
<h3 id="44-部署到嵌入式设备">4.4 部署到嵌入式设备</h3>
<ul>
<li><strong>Linux + NPU</strong>：全精度 YOLOv8n 可以在 1GHz 芯片上达到 30+ FPS</li>
<li><strong>MCU + NPU</strong>：例如 ESP32-S3、STM32+CMSIS-NN，可以运行 YOLOv5n 量化版</li>
<li><strong>纯 MCU 无 NPU</strong>：不建议，速度太慢（几秒一帧）</li>
</ul>
<h3 id="45-优缺点分析">4.5 优缺点分析</h3>
<p>✅ <strong>优点</strong>：</p>
<ul>
<li>鲁棒性强，对光照变化、遮挡、背景干扰不敏感</li>
<li>可以同时检测多种颜色形状组合，直接输出分类结果</li>
<li>端到端检测，不需要手动设计多个步骤</li>
<li>数据越多，效果越好</li>
</ul>
<p>❌ <strong>缺点</strong>：</p>
<ul>
<li>计算量大，需要带 NPU 的 MCU 或 Linux 才能实时运行</li>
<li>需要收集标注数据，训练成本高</li>
<li>参数多，存储空间要求大（几 MB 到几十 MB）</li>
<li>调试困难，错了不好分析原因</li>
</ul>
<h2 id="性能对比">性能对比</h2>
<h3 id="51-资源占用对比qvga-320240-图像">5.1 资源占用对比（QVGA 320×240 图像）</h3>
<table>
<thead>
<tr>
<th>方案</th>
<th>CPU 占用（Cortex-M7 @ 200MHz）</th>
<th>帧率</th>
<th>RAM 占用</th>
<th>Flash 占用</th>
</tr>
</thead>
<tbody>
<tr>
<td>颜色分割 + 轮廓</td>
<td>~20%</td>
<td>30-50 FPS</td>
<td>~50KB</td>
<td>~5KB</td>
</tr>
<tr>
<td>Hough 圆检测</td>
<td>~40%</td>
<td>15-30 FPS</td>
<td>~100KB</td>
<td>~8KB</td>
</tr>
<tr>
<td>Blob 分析</td>
<td>~30%</td>
<td>20-40 FPS</td>
<td>~80KB</td>
<td>~7KB</td>
</tr>
<tr>
<td>YOLOv8n（NPU）</td>
<td>~10%（NPU 算）</td>
<td>20-30 FPS</td>
<td>~10MB</td>
<td>~3MB</td>
</tr>
</tbody>
</table>
<blockquote>
<p>测试条件：单目标，图像大小 320×240，ARM Cortex-M7 平台</p>
</blockquote>
<h3 id="52-准确率对比复杂背景">5.2 准确率对比（复杂背景）</h3>
<table>
<thead>
<tr>
<th>方案</th>
<th>颜色变化鲁棒性</th>
<th>形状变化鲁棒性</th>
<th>遮挡鲁棒性</th>
<th>背景干扰鲁棒性</th>
</tr>
</thead>
<tbody>
<tr>
<td>颜色分割 + 轮廓</td>
<td>⭐⭐</td>
<td>⭐⭐⭐</td>
<td>⭐</td>
<td>⭐</td>
</tr>
<tr>
<td>Hough 圆检测</td>
<td>⭐⭐⭐</td>
<td>⭐⭐⭐</td>
<td>⭐⭐</td>
<td>⭐⭐</td>
</tr>
<tr>
<td>Blob 分析</td>
<td>⭐⭐</td>
<td>⭐⭐⭐</td>
<td>⭐⭐</td>
<td>⭐⭐</td>
</tr>
<tr>
<td>YOLO 深度学习</td>
<td>⭐⭐⭐⭐⭐</td>
<td>⭐⭐⭐⭐⭐</td>
<td>⭐⭐⭐⭐</td>
<td>⭐⭐⭐⭐⭐</td>
</tr>
</tbody>
</table>
<p>⭐⭐⭐⭐⭐ = 最好，⭐ = 最差</p>
<h2 id="选型建议">选型建议</h2>
<p>根据你的场景选择最合适的方案：</p>
<table>
<thead>
<tr>
<th>场景</th>
<th>推荐方案</th>
<th>理由</th>
</tr>
</thead>
<tbody>
<tr>
<td>背景简单，光照稳定，纯 MCU 部署</td>
<td><strong>颜色分割 + 轮廓检测</strong></td>
<td>计算量最小，满足实时性</td>
</tr>
<tr>
<td>需要检测多个已知半径的圆形</td>
<td><strong>Hough 变换</strong></td>
<td>专门针对圆形，定位准</td>
</tr>
<tr>
<td>需要检测多个颜色形状，批量处理</td>
<td><strong>Blob 分析</strong></td>
<td>参数丰富，一次检测多个</td>
</tr>
<tr>
<td>背景复杂，光照多变，有 NPU/Linux</td>
<td><strong>YOLO 深度学习</strong></td>
<td>鲁棒性最好，准确率最高</td>
</tr>
</tbody>
</table>
<h2 id="实战案例冰壶检测">实战案例：冰壶检测</h2>
<h3 id="61-问题分析">6.1 问题分析</h3>
<p>冰壶检测特点：</p>
<ul>
<li>冰面背景比较均匀</li>
<li>冰壶是标准圆形，直径固定</li>
<li>颜色和冰面对比明显（冰壶深色，冰面浅色）</li>
<li>需要实时检测（30 FPS 以上）</li>
</ul>
<h3 id="62-推荐方案">6.2 推荐方案</h3>
<p><strong>方案：颜色分割 + Hough 变换</strong></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="k">def</span> <span class="nf">detect_curling_stones</span><span class="p">(</span><span class="n">image</span><span class="p">,</span> <span class="n">min_radius</span><span class="o">=</span><span class="mi">60</span><span class="p">,</span> <span class="n">max_radius</span><span class="o">=</span><span class="mi">100</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="c1"># 转灰度 + 阈值分割</span>
</span></span><span class="line"><span class="cl">    <span class="n">gray</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">image</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_BGR2GRAY</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">_</span><span class="p">,</span> <span class="n">mask</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">threshold</span><span class="p">(</span><span class="n">gray</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">THRESH_BINARY_INV</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">cv2</span><span class="o">.</span><span class="n">getStructuringElement</span><span class="p">(</span><span class="n">cv2</span><span class="o">.</span><span class="n">MORPH_ELLIPSE</span><span class="p">,</span> <span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">5</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></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Hough 检测圆</span>
</span></span><span class="line"><span class="cl">    <span class="n">circles</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">HoughCircles</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">mask</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">HOUGH_GRADIENT</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="n">dp</span><span class="o">=</span><span class="mf">1.2</span><span class="p">,</span> <span class="n">min_dist</span><span class="o">=</span><span class="mi">80</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">param1</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span> <span class="n">param2</span><span class="o">=</span><span class="mi">25</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">minRadius</span><span class="o">=</span><span class="n">min_radius</span><span class="p">,</span> <span class="n">maxRadius</span><span class="o">=</span><span class="n">max_radius</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">circles</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">circles</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">uint16</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">around</span><span class="p">(</span><span class="n">circles</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">circles</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="p">:]:</span>
</span></span><span class="line"><span class="cl">            <span class="n">result</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">i</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">i</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">i</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="k">return</span> <span class="n">result</span><span class="p">,</span> <span class="n">mask</span>
</span></span></code></pre></div><p>GitHub 上已经有专门做冰壶检测的项目：https://github.com/AIcurling/Stone-Detection，正好是我们讨论的思路，可以去参考一下。</p>
<h2 id="开源参考代码">开源参考代码</h2>
<p>本文完整代码已上传 GitHub，欢迎 Star/Fork：</p>
<ul>
<li><a href="https://github.com/wh2000hh/color-shape-detection">https://github.com/wh2000hh/color-shape-detection</a></li>
<li>包含：
<ul>
<li>四种方案的完整 Python 实现</li>
<li>HSV 调参工具</li>
<li>评估脚本</li>
<li>冰壶检测示例</li>
</ul>
</li>
</ul>
<h2 id="总结">总结</h2>
<ul>
<li><strong>传统图像处理方案</strong>适合简单背景、纯 MCU 部署，代码量小，速度快，但对环境变化敏感</li>
<li><strong>深度学习方案</strong>适合复杂背景、有 NPU/Linux 的平台，准确率和鲁棒性高，但需要数据和更大算力</li>
<li>根据你的硬件平台和场景复杂度选择合适方案，不要盲目追求深度学习</li>
<li>冰壶这种规则形状+均匀背景，用传统方法就足够了，而且可以做到 30 FPS 以上在 Cortex-M7 上</li>
</ul>
<hr>
<p><em>本文基于 OpenCV 官方文档和实际项目经验整理，适合嵌入式计算机视觉入门学习。</em></p>
<h2 id="参考资料">参考资料</h2>
<ol>
<li>OpenCV 官方文档：Image Segmentation, Hough Transform, Blob Detection</li>
<li>AIcurling/Stone-Detection - GitHub 冰壶检测项目: <a href="https://github.com/AIcurling/Stone-Detection">https://github.com/AIcurling/Stone-Detection</a></li>
<li>Ultralytics YOLOv8 文档: <a href="https://docs.ultralytics.com/">https://docs.ultralytics.com/</a></li>
<li>Learning OpenCV 3: Computer Vision in C++ with the OpenCV Library</li>
<li>Roboflow 自定义数据标注: <a href="https://roboflow.com/">https://roboflow.com/</a></li>
</ol>
]]></content:encoded></item></channel></rss>