<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/feed.xml" rel="self" type="application/atom+xml" /><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/" rel="alternate" type="text/html" hreflang="zh-CN" /><updated>2026-04-14T17:53:09+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/feed.xml</id><title type="html">工业级系统十讲</title><subtitle>一个关于曲面 Pattern 缺陷检测的系列：从问题本体、模板组织、配准与测量，到配置路由、多线程、插件机制与工程落地。</subtitle><entry><title type="html">第十讲：为什么一套接近产品化的工业算法系统，最后仍差了一步——从算法可行到组织最后一公里</title><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/04/why-an-almost-productized-industrial-algorithm-system-still-fell-one-step-short.html" rel="alternate" type="text/html" title="第十讲：为什么一套接近产品化的工业算法系统，最后仍差了一步——从算法可行到组织最后一公里" /><published>2026-04-04T12:00:00+08:00</published><updated>2026-04-04T12:00:00+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/04/why-an-almost-productized-industrial-algorithm-system-still-fell-one-step-short</id><content type="html" xml:base="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/04/why-an-almost-productized-industrial-algorithm-system-still-fell-one-step-short.html"><![CDATA[<h1 id="why-an-almost-productized-industrial-algorithm-system-still-fell-one-step-short-from-algorithmic-feasibility-to-the-last-mile-of-organization">Why an Almost Productized Industrial Algorithm System Still Fell One Step Short: From Algorithmic Feasibility to the Last Mile of Organization</h1>

<blockquote>
  <p><strong>English Abstract</strong><br />
This article reflects on why an industrial inspection system that had already become highly mature at the algorithmic level still failed to complete the final step toward true productization. The core detection chain was already in place: template organization, two-layer registration, point-wise defect measurement, configuration routing, multithreading, plugin support, and AI-assisted review had all grown into a coherent technical system, and the algorithm had been validated in real production environments. Yet large-scale and lasting productization requires more than a working algorithm. It also depends on a reliable implementation chain, platform architecture, field equipment stability, cross-team coordination, and sufficient organizational authority to align these elements around the technical core. In this sense, what ultimately constrained the system was not the last mile of technology, but the last mile of organization.</p>
</blockquote>

<p>在前面九讲里，我已经尽可能把这套曲面 Pattern 缺陷检测系统讲清楚了：它面对的是什么问题，模板如何被组织，配准如何建立可比较关系，缺陷如何沿测量集被逐点测出来，系统为什么能在真实产线上跑起来，又为什么必须长出模板生产链、ROI 擦除、插件机制和 AI 模块这样的能力。</p>

<p>如果只看这些内容，读者很容易产生一个自然判断：</p>

<blockquote>
  <p><strong>既然主检测链已经成立，速度也不差，客户评价也不低，那么它为什么没有真正变成一个被大规模推广、长期复制的成熟产品？</strong></p>
</blockquote>

<p>这一讲，我就想专门回答这个问题。</p>

<p>因为在我看来，这个项目最后“差了一步”，并不是因为算法路线不成立，也不是因为核心检测链没有跑通。恰恰相反，它已经非常接近产品化了：在客户现场，算法这一环长期得到最高评价；在普通 <code class="language-plaintext highlighter-rouge">i5 + 4G</code> 内存的上位机上，四十多个字符的镭雕检测只需要约 <code class="language-plaintext highlighter-rouge">400ms</code>；模板、配准、测量、配置、插件、AI 这些层也都已经长出来了。也就是说，它并不是一个停留在纸面上的方案，而是一套已经在真实工业条件下证明过自己可行性的系统。</p>

<p>但工业项目真正难的地方，并不止于把算法做出来。算法仍然往往是最难、最核心的一环；而且算法之所以能够真正成立、真正成熟，本身也依赖一条可靠的产品化链条去支撑它的持续迭代与优化。也正因为如此，当算法已经成立之后，是否还有一整条足够可靠的实施链、平台链和组织协同链把它真正接住，往往决定了系统最终能不能被做成。</p>

<p>这条链条包括实施工程师团队、上位机平台、现场设备、跨部门配合、节拍调度，以及最关键的一点：核心技术负责人是否同时拥有足够的组织权力，去推动这些环节围绕系统主线一起收敛。</p>

<p>而这恰恰是这个项目最后没有真正走完的地方。</p>

<p>所以，这一讲真正要讲清楚的不是“项目哪里没做好”，而是四件更本质的事情：</p>

<ol>
  <li>这套系统实际上已经接近产品化到了什么程度；</li>
  <li>为什么最后真正卡住它的，并不是算法本身；</li>
  <li>为什么实施链、平台链和现场链反而成了更大的瓶颈；</li>
  <li>为什么一个工业算法系统要想真正推广，最难跨过去的，往往不是技术最后一公里，而是组织最后一公里。</li>
</ol>

<h2 id="一这套系统其实已经接近产品化到了什么程度">一、这套系统其实已经接近产品化到了什么程度</h2>

<p>如果今天回头看这个项目，我首先想强调的一点是：它并不是一个“思路不错但落不了地”的系统。恰恰相反，它已经具备了很多真正接近产品化的特征。</p>

<p>首先，它不是一个单点算法，而是一条比较完整的工业技术链。模板组织、两层配准、注册集与测量集分离、局部缺陷测量、<code class="language-plaintext highlighter-rouge">XML/INI</code> 配置链、模板创建工具、<code class="language-plaintext highlighter-rouge">ROI</code> 擦除、插件机制、AI 复核模块，这些东西不是零散拼起来的，而是已经围绕同一条检测主线生长出来了。</p>

<p>其次，它不是停留在实验室里的 <code class="language-plaintext highlighter-rouge">demo</code>，而是真正进入过客户现场，并且在实际产线上跑过。更重要的是，算法这一环在客户那边长期得到的反馈一直是最好的。机械、电子、上位机、上下料这些环节不断出问题时，客户对算法这一环的评价反而始终更高。对我来说，这一点很重要，因为它说明这套系统最核心的技术部分并没有虚高，而是经受过真实现场的检验。</p>

<p>再者，从运行效率上说，它也并不差。四十多个字符的镭雕检测只需要约 <code class="language-plaintext highlighter-rouge">400ms</code>，而运行平台只是普通的 <code class="language-plaintext highlighter-rouge">i5、4G</code> 内存上位机。这说明系统并不存在那种“理论上能跑、实际上太慢”的根本缺陷。至少在算法核这一层，它已经证明了自己的速度和可行性。</p>

<p>也就是说，如果只看算法系统本身，它其实离“能做成一个产品”已经很近了。</p>

<p>所以，我今天更愿意把这个项目理解成：</p>

<blockquote>
  <p><strong>一个已经非常接近产品化的工业算法系统，最后却被产品化链条本身卡住了。</strong></p>
</blockquote>

<h2 id="二为什么最核心的问题不在算法而在产品化链条">二、为什么最核心的问题不在算法，而在产品化链条</h2>

<p>工业研发里很容易出现一种误判：一旦项目没有真正推广起来，大家第一反应就会觉得，问题大概还是出在算法不够成熟，或者算法还没有完全准备好。</p>

<p>但这个项目恰恰让我越来越清楚地意识到：很多时候，算法成立与否，只是产品化的前提，而不是产品化本身。</p>

<p>这套系统真正遇到的问题，不是“主检测链有没有建立起来”，而是主检测链之外那几条同样关键的链条，是否也被建立起来了。例如：</p>

<ul>
  <li>模板和参数能否被稳定地生产出来；</li>
  <li>实施团队能否真正理解这套系统，而不是机械点工具；</li>
  <li>上位机平台能否围绕算法节拍和并行组织去设计；</li>
  <li>现场设备和上下料能否提供足够稳定的输入条件；</li>
  <li>跨部门配合是否真的以“把系统做成”为目标，而不是彼此转移压力。</li>
</ul>

<p>换句话说，算法主线只是工业系统的一根主梁，但一栋房子最后能不能立住，从来不只取决于主梁本身。</p>

<p>这个项目最后最让我感慨的，也正是这一点：</p>

<blockquote>
  <p><strong>一个人就算把算法主线扛起来了，也不等于整个系统就能自然产品化。</strong></p>
</blockquote>

<p>因为产品化不是把代码写出来，而是要让模板链、实施链、平台链、现场链和组织链一起收敛。</p>

<h2 id="三为什么实施工程师成了这个项目最大的痛点">三、为什么实施工程师成了这个项目最大的痛点</h2>

<p>如果要我在所有瓶颈里只挑一个最核心的，我会说：<strong>实施链没有真正建立起来</strong>，是这个项目最后没能大规模推广的最主要原因之一。</p>

<p>前面第七讲我已经反复讲过，这套系统里的“实施”并不是一个单一层次的概念。对于像 Apple AirPods、索尼游戏手柄这类已经相对定型的固定产品，只要核心算法链、模板体系和参数主干已经建立起来，经过训练的实施工程师——哪怕主要是执行型角色——往往就已经足以承担模板导入、参数微调、现场验证和上线维护等工作。</p>

<p>但问题在于，我当时真正追求的并不只是把某几个固定项目做完，而是希望这套系统能够在我逐步撤出之后，仍然继续推广到新产品、甚至推广到新的企业环境中。到了这个层次，对实施链的要求就不一样了。</p>

<p>因为这时需要承接的，已经不只是“会不会点工具、会不会改参数”，而是：</p>

<ul>
  <li>能不能理解模板生产链本身；</li>
  <li>能不能判断骨架、中心点和局部结构是否可信；</li>
  <li>能不能分辨哪些问题属于参数问题，哪些其实已经是对象差异或现场条件变化；</li>
  <li>能不能把实验室中的模板与参数体系迁移到新产品和新场景，并继续完成收敛。</li>
</ul>

<p>也就是说，如果目标只是维护固定产品，那么普通实施工程师在很多情况下已经够用；但如果目标是脱离核心研发者之后继续做跨产品、跨客户、跨企业的推广，那么这里真正需要的，就不再是普通意义上的实施，而是贴近现场、同时又能够承接模板生产链与参数闭环的应用算法工程师。而这恰恰是后来真正制约系统推广的地方：不是固定项目完全做不下去，而是当目标从“把一个项目做完”转向“把一套系统持续推广出去”时，实施链并没有同步升级到能够独立承接这种推广任务的层次。</p>

<p>问题恰恰在于，这类人非常难找。</p>

<p>稍微有点能力、也有发展诉求的人，往往不愿意长期承担这种又细、又苦、责任又重的工作；而能力不足的人，又根本承载不起模板质量、参数质量和现场收敛的压力。最后的结果就是：这条链要么没人愿意接，要么接住的人接不稳。</p>

<p>而对这套系统来说，这恰恰是致命的。因为模板生产链和参数闭环链一旦不稳，后面的漏检和过杀就会自然暴露出来。很多表面上看像“算法问题”的东西，实际上是模板和配置质量在前端已经开始失真了。</p>

<p>所以我后来越来越觉得，这个项目真正缺的，并不是某个局部功能，而是：</p>

<blockquote>
  <p><strong>一个能够持续承接模板生产、参数闭环和现场实施的合格团队。</strong></p>
</blockquote>

<h2 id="四为什么上位机平台吞掉了算法优势">四、为什么上位机平台吞掉了算法优势</h2>

<p>实施链之外，另一个非常硬的瓶颈，就是上位机平台。</p>

<p>这一点其实很现实。现场设备一个工位里并不是只放一个耳机，而是四个槽位可以同时放四个耳机。如果检测系统能够做到“拍完即过”，并把四个耳机的检测任务放进并行队列里去执行，那么整套系统的节拍会明显提升。</p>

<p>但问题在于，上位机那一侧长期采用的是串行处理逻辑。它虽然也做了插件化、配置化生成，但这种生成方式并没有真正生成出围绕并行检测组织的软件架构，最后导致整个节拍被平台层直接卡住。</p>

<p>这里最容易被忽略的一点是：<strong>算法速度和系统吞吐并不是同一个问题。</strong></p>

<p>算法核本身已经采用 <code class="language-plaintext highlighter-rouge">C++</code> 并行实现，线程间同步开销极小，继续从算法内部提速的空间其实有限；而上位机侧却仍然把 <code class="language-plaintext highlighter-rouge">PLC</code> 通信、图像采集、算法处理和结果显示组织成一条串行链，后一步必须等待前一步结束。结果就是，系统虽然在算法核内部已经很快，但在整条运行链上仍然会不断出现等待，从而把本来可以利用的生产间隙浪费掉。</p>

<p>也正因为如此，真正高性价比的提速点并不在算法核，而在运行架构。更合理的方向，本来应该是把采图、算法、回传这几段流程解耦开来，例如改成双缓存三线程，让相机线程、算法线程和通信线程异步推进。只要平台层不愿意围绕并行检测重写运行组织，算法再快，最终也会被系统吞吐结构直接吞掉。</p>

<p>于是就出现了一种非常典型、也非常荒谬的局面：<br />
平台一边坚持串行处理，一边又不断把节拍压力继续甩给算法，要求算法再提速。</p>

<p>可问题是，算法核本身在普通 <code class="language-plaintext highlighter-rouge">i5、4G</code> 内存上位机上，检测四十多个字符只需要约 <code class="language-plaintext highlighter-rouge">400ms</code>。换句话说，节拍真正的上限并不完全由算法决定，而是被整套任务调度架构决定的。</p>

<p>所以我会把这一层总结成一句话：</p>

<blockquote>
  <p><strong>当运行平台不是按并行检测来组织时，算法速度优势会被系统架构直接吞掉。</strong></p>
</blockquote>

<h2 id="五现场工艺与设备问题是不断放大系统难度的劣化因子">五、现场工艺与设备问题，是不断放大系统难度的劣化因子</h2>

<p>除了实施链和平台架构之外，现场设备与工艺状态本身也一直在放大系统难度。</p>

<p>这一层并不是主因，但它会不断把原本已经很难的事情推得更难。</p>

<p>例如，产线里的小残胶、透明残胶问题始终存在，而现场状态又不可能做到严格清理。这对视觉检测来说是非常大的阻碍，因为你既不能漏检，又必须去适应一个本来就不规范的现场环境。</p>

<p>再例如，整套检测系统并不只有算法。机械手上下料、电子、上位机这些环节只要持续不稳，算法就会不断被迫去吸收别的环节制造出来的扰动。机械臂曾经把耳机碰断过，也经常碰在设备壳体上；去越南实施时，过去的人并不真正承担算法链责任，最后整个设备在那边都实施得不好。</p>

<p>这些东西单看每一项，好像都只是“现场问题”；但把它们叠加起来，就会形成一种很典型的工业现实：</p>

<blockquote>
  <p><strong>算法不是在一个受控、标准化、配合良好的产品化环境里工作，而是在一个高扰动、低规范、协同很弱的环境里被不断要求兜底。</strong></p>
</blockquote>

<p>这也是为什么我后来越来越不愿意简单地用“项目做成了没有”来评价这套系统。因为它实际上是在非常差的外围生态里，仍然尽量撑住了最核心的检测部分。</p>

<h2 id="六为什么核心技术负责人未必同时拥有推动产品化闭环的组织权力">六、为什么核心技术负责人未必同时拥有推动产品化闭环的组织权力</h2>

<p>这个项目最后最深的一层约束，也许并不在某个具体技术点，而在于：真正最理解系统主线的人，未必同时拥有推动整条产品化链条收敛的组织权力。</p>

<p>从事实层面看，我一直是这个项目里最核心的算法负责人，这在一线研发工程师当中其实是共识。客户长期给算法最高评分，后续新的部门负责人去现场调研后得到的客户反馈也印证了这一点。也就是说，真正懂系统主线、最清楚问题出在哪、最知道资源该怎么配的人，其实并不是不存在。</p>

<p>但问题在于，理解系统和决定系统，不是同一件事。</p>

<p>这意味着，最懂系统主线的人，可以把算法核扛起来，却未必能决定：</p>

<ul>
  <li>谁来做实施；</li>
  <li>平台研发是否必须围绕并行检测重构；</li>
  <li>哪些问题应当被定义为平台问题、现场问题，而不是继续转嫁为算法问题；</li>
  <li>跨部门资源是否真的围绕系统主线倾斜；</li>
  <li>哪些环节需要以产品化为目标重新组织。</li>
</ul>

<p>于是就会出现一种很典型的局面：最懂系统的人，不断用个人能力去填组织协同留下的坑；而真正决定资源、节奏和边界的人，却未必对系统主线拥有同样清晰的理解。在这种情况下，一个人力量再大，也很难真正把系统推成产品。</p>

<p>所以我后来越来越觉得，这个项目最后差的那一步，不是技术最后一公里，而是：</p>

<blockquote>
  <p><strong>组织最后一公里。</strong></p>
</blockquote>

<h2 id="七所以这个项目差的不是技术最后一公里而是组织最后一公里">七、所以，这个项目差的不是技术最后一公里，而是组织最后一公里</h2>

<p>写到这里，其实已经可以很清楚地看见：这个项目最后没有真正被大规模推广，并不是因为算法路线不成立，也不是因为主检测链没有长出来，而是因为产品化所依赖的几条关键链条没有一起成熟：</p>

<ul>
  <li>实施链没有真正建立起来；</li>
  <li>平台没有围绕并行检测来组织；</li>
  <li>现场设备和工艺问题不断放大系统难度；</li>
  <li>跨部门协同和资源配置没有围绕技术主线收敛；</li>
  <li>最理解系统的人，又未必拥有把这些环节真正串起来的组织权力。</li>
</ul>

<p>所以，如果要为这一讲压缩成一句话，我更愿意这样说：</p>

<blockquote>
  <p><strong>一个工业算法系统能不能真正推广，取决的不只是算法能否成立，还取决于有没有一条足够可靠的实施链、平台链和组织协同链把它接住。</strong></p>
</blockquote>

<h2 id="八这一讲的结尾这套系统虽然没被真正推广但它并没有失败">八、这一讲的结尾：这套系统虽然没被真正推广，但它并没有失败</h2>

<p>如果只从结果看，这个项目最后确实没有被做成一个真正意义上的成熟产品，并让更多团队长期继承和受益。<br />
但我并不愿意把它简单定义成失败。</p>

<p>因为它至少证明了几件事：</p>

<ul>
  <li>这条算法路线是成立的；</li>
  <li>这套系统并不是纸上方案，而是能在真实现场跑起来的工业对象；</li>
  <li>它真正欠缺的，不是核心检测主线，而是产品化所需的协同链条；</li>
  <li>它之所以值得被我一讲一讲重新写出来，也正是因为它曾经非常接近“做成”。</li>
</ul>

<p>从这个意义上说，这个项目对我而言，并不是一个“差一点没成功”的遗憾案例，而更像一个很清楚的提醒：</p>

<blockquote>
  <p><strong>工业算法真正难的，不只是把方法做出来，而是把方法放进一条可靠的实施链、平台链和组织协同链里。</strong></p>
</blockquote>

<p>而这，也是我写完整个系列之后，最想留下来的最后一句话：<br />
<strong>技术能否成立，决定了一套系统能不能开始；而它能不能真正走完最后一步，往往取决于技术之外那条更难的产品化链条。</strong></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Why an Almost Productized Industrial Algorithm System Still Fell One Step Short: From Algorithmic Feasibility to the Last Mile of Organization]]></summary></entry><entry><title type="html">第九讲：曲面 Pattern 检测系统为什么不能写死——插件机制如何接住二维码、条码、Logo 与 AI 模块</title><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/03/why-the-curved-surface-pattern-inspection-system-should-not-be-hard-wired.html" rel="alternate" type="text/html" title="第九讲：曲面 Pattern 检测系统为什么不能写死——插件机制如何接住二维码、条码、Logo 与 AI 模块" /><published>2026-04-03T10:00:00+08:00</published><updated>2026-04-03T10:00:00+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/03/why-the-curved-surface-pattern-inspection-system-should-not-be-hard-wired</id><content type="html" xml:base="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/03/why-the-curved-surface-pattern-inspection-system-should-not-be-hard-wired.html"><![CDATA[<h1 id="why-the-curved-surface-pattern-inspection-system-should-not-be-hard-wired-a-plugin-mechanism-for-qr-codes-barcodes-logos-and-ai-modules">Why the Curved-Surface Pattern Inspection System Should Not Be Hard-Wired: A Plugin Mechanism for QR Codes, Barcodes, Logos, and AI Modules</h1>

<blockquote>
  <p><strong>English Abstract</strong><br />
This article explains why the curved-surface pattern inspection system should not be hard-wired into a single fixed detection chain. While the main pipeline is well suited to structured objects such as characters and regular patterns, many real production targets—such as QR codes, barcodes, rule-based logos, glue residue, and lint—do not fit naturally into the same template–registration–measurement framework. To keep the main chain stable and clean, the system introduces a plugin mechanism that routes heterogeneous objects into dedicated local analysis paths through XML and block-level configuration. This mechanism supports both rule-based plugins and AI modules, allowing the system to evolve from a fixed algorithm set into an extensible inspection platform.</p>
</blockquote>

<p>在前面几篇里，我已经把这套系统的主检测链讲得比较清楚了：模板如何组织，xml 如何承接结构对象，两层配准如何把模板带入实测图像，缺陷如何沿测量集被逐点测出来，以及整套系统为什么能够在真实产线上稳定跑起来。</p>

<p>但即便到了这一步，仍然有一个问题没有回答：</p>

<blockquote>
  <p><strong>这套系统是不是只能处理那类已经被模板、骨架、block、注册集和测量集组织好的标准对象？</strong></p>
</blockquote>

<p>真实现场里，答案显然是否定的。</p>

<p>因为产线对象并不总是规则地落在主检测链最擅长处理的那一类结构上。条码、二维码、某些规则 logo，甚至残胶、毛絮这类弱结构或纹理型异常，都不一定适合被强行塞进原有那条“模板—配准—测量”的主链之中。它们有的在几何组织上和字符 / pattern 完全不同，有的在检测逻辑上更像解码或分类问题，有的则更适合交给深度模型做局部复核。</p>

<p>这也就意味着：如果继续把所有对象都硬写进主 DLL，系统会越来越臃肿，越来越充满特例，最后既损伤主链的清晰性，也损伤整个系统的可维护性。</p>

<p>所以，这套系统必须再长出一层能力：</p>

<blockquote>
  <p><strong>当某类对象不适合继续纳入主检测链时，系统要能把它以插件的方式接进来。</strong></p>
</blockquote>

<p>也正是在这一层上，这套系统开始从“固定算法集合”转向“可扩展检测平台”。</p>

<p>如果把这套系统已经形成的主链、插件链和 AI 复核链压缩成一张总图，它大致可以表示为下面这样：</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/pipeline.png" alt="Overall runtime flow of the curved-surface pattern inspection system" /></p>

<p><em>图 1：系统整体流程图。从初始化、轮廓分析、并行配准，到主检测链上的局部测量，再到对可疑邻域引入深度神经网络二次分类，以及对不规则特殊对象走插件路径，这些环节共同构成了系统的完整运行链。</em></p>

<p>这一篇，我想专门把这层能力讲清楚：</p>

<ol>
  <li>为什么主检测链不该承包一切；</li>
  <li>插件接口到底在统一什么；</li>
  <li>XML 和 block 配置是怎么把插件正式接进系统的；</li>
  <li>为什么二维码、条码和规则 logo 特别适合走插件；</li>
  <li>为什么插件不仅能接住特殊规则对象，也能接住 AI 模块；</li>
  <li>为什么这一层真正改变的，不只是功能多少，而是系统的性质。</li>
</ol>

<h2 id="一为什么这套系统需要插件而不是继续往-dll-里加分支">一、为什么这套系统需要插件，而不是继续往 DLL 里加分支</h2>

<p>一个固定检测系统最容易走向的方向，就是每遇到一个新对象，就往核心代码里再加一个特例分支。开始时这样做看起来很方便，因为功能都还在同一个 DLL 里，调试路径也看似集中。</p>

<p>但这条路走不远。</p>

<p>因为前面几篇已经说明，这套系统的主检测链其实是围绕一类“可被结构化组织”的对象建立起来的：它们能够进入模板层，能够被 block 切分，能够在 xml 中形成骨架或特征点集合，能够进一步进入配准和测量链。这条主链之所以成立，是因为它处理的是<strong>结构化 Pattern 对象</strong>。</p>

<p>而一旦把完全不同性质的对象继续往里硬塞，主链就会开始变脏。主系统原本清楚的模板逻辑、配准逻辑和测量逻辑，会被越来越多的特例分支污染；配置会开始混乱；维护和验证的成本也会迅速上升。</p>

<p>所以，插件机制的意义不是“让系统显得高级一点”，而是：</p>

<blockquote>
  <p><strong>把那些不适合继续纳入主链的特殊对象，从核心检测链里剥离出来。</strong></p>
</blockquote>

<p>换句话说，主系统负责提供统一运行框架，插件负责承接不适合纳入主链的异构检测对象。</p>

<h2 id="二插件接口到底在统一什么">二、插件接口到底在统一什么</h2>

<p>如果说插件的意义是把异构对象从主链中剥离出来，那么接下来最关键的问题就是：这些完全不同的对象，最后为什么还能被统一接入同一套系统？</p>

<p>答案就在插件接口。</p>

<p>从系统边界上看，插件接收的是当前 block 的局部 ROI 图像、尺寸信息、检测类型、附加业务输入以及结果输出容器；插件执行完成后，再把缺陷结果以统一格式返回给主系统。也就是说，插件虽然内部算法可以完全不同，但在系统边界上，它必须满足统一输入输出协议。</p>

<p>它最核心的作用，并不是让主系统知道“插件内部具体怎么做”，而是让主系统知道：</p>

<ul>
  <li>当前拿到的是哪个 ROI；</li>
  <li>需要执行哪种检测任务；</li>
  <li>有没有附加业务输入，例如 SN code；</li>
  <li>返回的缺陷集合是什么；</li>
  <li>最终结果是 OK、NG 还是其他状态。</li>
</ul>

<p>也就是说，插件并不接管整套系统，它只接管：</p>

<blockquote>
  <p><strong>当前 ROI 上那一段不适合由主链继续完成的局部检测任务。</strong></p>
</blockquote>

<p>这一点在接口设计上体现得很清楚。插件的增强版接口大致如下：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/*!
measure defects for character or pattern
\param mat_roi bitmap of measurement area
\param rows
\param cols
\param code input sn code
\param detectType
\param res_set the result set
\param e_num the size of the result set
\return the result:1:OK;0:NG or other
*/</span>
<span class="k">typedef</span> <span class="nf">int</span><span class="p">(</span><span class="o">*</span><span class="n">Pattern_analysis</span><span class="p">)(</span>
    <span class="kt">unsigned</span> <span class="kt">char</span> <span class="o">*</span><span class="n">mat_roi</span><span class="p">,</span>
    <span class="kt">int</span> <span class="n">rows</span><span class="p">,</span>
    <span class="kt">int</span> <span class="n">cols</span><span class="p">,</span>
    <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">code</span><span class="p">,</span>
    <span class="kt">int</span> <span class="n">detectType</span><span class="p">,</span>
    <span class="kt">int</span><span class="p">(</span><span class="o">*</span><span class="n">res_set</span><span class="p">)[</span><span class="mi">5</span><span class="p">],</span>
    <span class="kt">int</span> <span class="o">&amp;</span><span class="n">e_num</span><span class="p">);</span>
</code></pre></div></div>

<p>这里还有一个值得说明的细节：插件接口本身也不是一开始就完全固定不变的。早期接口更多服务于纯图像型对象，而在后续演进中，为了支持 <code class="language-plaintext highlighter-rouge">SN code</code>、条码、二维码等依赖附加业务输入的对象，接口进一步引入了 <code class="language-plaintext highlighter-rouge">code</code> 参数。也就是说，这套插件机制本身也经历了从“纯图像型插件”向“图像 + 业务输入型插件”的扩展。</p>

<p>与此同时，<code class="language-plaintext highlighter-rouge">detectType</code> 也并不只是一个简单开关，而是直接沿用了系统原有的缺陷语义集合，例如：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">1</code>：镭断</li>
  <li><code class="language-plaintext highlighter-rouge">2</code>：色异常</li>
  <li><code class="language-plaintext highlighter-rouge">4</code>：偏移</li>
  <li><code class="language-plaintext highlighter-rouge">8</code>：多雕</li>
  <li><code class="language-plaintext highlighter-rouge">16</code>：码数量异常</li>
  <li><code class="language-plaintext highlighter-rouge">32</code>：无码</li>
  <li><code class="language-plaintext highlighter-rouge">64</code>：镭错</li>
  <li><code class="language-plaintext highlighter-rouge">128</code>：SN 码数量异常</li>
  <li><code class="language-plaintext highlighter-rouge">256</code>：杂质</li>
</ul>

<p>这意味着插件虽然内部算法不同，但它的输出并不是“另起一套语义”，而是继续回到系统统一的缺陷表达里。</p>

<h2 id="三xml-和-block-配置是怎么把插件正式接进系统的">三、XML 和 block 配置是怎么把插件正式接进系统的</h2>

<p>插件之所以真正成为系统能力，不只是因为代码里可以 <code class="language-plaintext highlighter-rouge">LoadLibrary</code>，而是因为它已经被正式接进了配置链。</p>

<p>也就是说，插件不是一个游离在系统之外的外挂，而是会通过 <code class="language-plaintext highlighter-rouge">XML</code> 与 <code class="language-plaintext highlighter-rouge">block</code> 配置进入主系统的运行框架。某个对象一旦被配置为插件检测路径，它就不再走标准的主检测链，而是被路由到对应的插件模块中去完成局部分析。</p>

<p>这一点非常关键。因为它说明这套系统并不是简单地“支持动态库”，而是已经在配置层面承认了插件的存在：模板主链继续负责标准结构化对象，而插件链则作为正式分支，承接那些不适合硬塞进主链的特殊对象。</p>

<p>从系统组织角度看，这种分工主要体现在两部分。</p>

<p>第一部分，是 <code class="language-plaintext highlighter-rouge">XML</code> 中对插件信息本身的配置。<br />
第二部分，是 <code class="language-plaintext highlighter-rouge">block</code> 标签中通过 <code class="language-plaintext highlighter-rouge">methods="1"</code> 等方式，把当前 <code class="language-plaintext highlighter-rouge">block</code> 路由到插件检测路径。</p>

<p>所以，插件机制之所以成立，不只是因为 <code class="language-plaintext highlighter-rouge">DLL</code> 能动态加载，而是因为它已经进入了 <code class="language-plaintext highlighter-rouge">XML</code> 与 <code class="language-plaintext highlighter-rouge">block</code> 的配置组织层。换句话说，主系统不是在运行到一半时“临时想起还有个插件可调”，而是在模板、<code class="language-plaintext highlighter-rouge">block</code> 和检测路径这一层就已经明确：哪些对象继续走主检测链，哪些对象应当转交给插件去处理。</p>

<p>如果借用软件架构中的语言来描述，这一层更接近一种“工厂式装配 + 策略式调用”的插件化边界。主系统并不在编译期写死二维码、条码、规则 <code class="language-plaintext highlighter-rouge">logo</code> 或 <code class="language-plaintext highlighter-rouge">AI</code> 模块的具体实现，而是先根据配置在运行时装配对应插件，再通过统一分析入口把它们作为当前 <code class="language-plaintext highlighter-rouge">ROI</code> 上的局部检测策略接回系统。对主系统来说，被统一起来的并不是各类对象内部到底采用了什么算法，而是它们在系统边界上的输入输出协议。</p>

<p>这也正是插件机制真正有价值的地方。它不是把所有特殊对象重新塞回主链，而是在主系统和对象特定算法之间建立起一条清楚的边界：主系统负责统一装配、统一调度和统一结果回收，而二维码、条码、规则 <code class="language-plaintext highlighter-rouge">logo</code>、<code class="language-plaintext highlighter-rouge">AI</code> 模块等对象，则各自通过统一入口接入当前 <code class="language-plaintext highlighter-rouge">ROI</code> 的局部检测任务。</p>

<p>插件管理相关代码也很直观。下面这段代码的关键不在于 Windows API 本身，而在于它明确了：主系统按统一方式加载插件，插件再通过统一函数入口回到系统中。</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">PlugIn</span>
<span class="p">{</span>
<span class="nl">public:</span>
    <span class="n">PlugIn</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">hDll</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">virtual</span> <span class="o">~</span><span class="n">PlugIn</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nb">NULL</span> <span class="o">!=</span> <span class="n">hDll</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">FreeLibrary</span><span class="p">(</span><span class="n">hDll</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kt">bool</span> <span class="n">Load</span><span class="p">(</span><span class="n">Param4PlugIn</span> <span class="o">*</span><span class="n">param</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">wchar_t</span> <span class="n">w_path</span><span class="p">[</span><span class="mi">100</span><span class="p">];</span>
        <span class="n">swprintf</span><span class="p">(</span><span class="n">w_path</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="s">L"%hs"</span><span class="p">,</span> <span class="n">param</span><span class="o">-&gt;</span><span class="n">path</span><span class="p">);</span>
        <span class="n">hDll</span> <span class="o">=</span> <span class="n">LoadLibrary</span><span class="p">(</span><span class="n">w_path</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="nb">NULL</span> <span class="o">==</span> <span class="n">hDll</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">else</span>
        <span class="p">{</span>
            <span class="n">strcpy</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">param</span><span class="o">-&gt;</span><span class="n">name</span><span class="p">);</span>
            <span class="n">analysis</span> <span class="o">=</span> <span class="p">(</span><span class="n">Pattern_analysis</span><span class="p">)</span><span class="n">GetProcAddress</span><span class="p">(</span><span class="n">hDll</span><span class="p">,</span> <span class="s">"pattern_analysis"</span><span class="p">);</span>
            <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="n">Pattern_analysis</span> <span class="n">analysis</span><span class="p">;</span>

<span class="nl">private:</span>
    <span class="n">HINSTANCE</span> <span class="n">hDll</span><span class="p">;</span>
    <span class="kt">char</span> <span class="n">name</span><span class="p">[</span><span class="n">MAX_PATH</span><span class="p">];</span>
<span class="p">};</span>
</code></pre></div></div>
<p>这段代码表面上只是 <code class="language-plaintext highlighter-rouge">DLL</code> 加载，但从系统边界看，它真正完成的是两件事。</p>

<p>第一，它把“插件如何被找到、被装配、被注册”统一了起来。主系统不需要在主链代码里到处写特例判断，而是通过统一装配入口，把对象特定算法接入当前运行框架。</p>

<p>第二，它把“插件内部算法是什么”和“主系统如何使用插件”清楚地分开了。对主系统来说，二维码插件、条码插件、规则 <code class="language-plaintext highlighter-rouge">logo</code> 插件，甚至后面的 <code class="language-plaintext highlighter-rouge">AI</code> 插件，最终都只是统一边界上的一个 <code class="language-plaintext highlighter-rouge">analysis</code> 入口。被统一起来的不是算法内部细节，而是系统边界上的输入输出协议。</p>

<p>所以，这一层最值得强调的，并不是 Windows API，也不是“动态加载”这几个字本身，而是：</p>

<blockquote>
  <p>插件机制通过统一配置、统一装配和统一调用，把主系统框架和对象特定算法清楚地分开了。</p>
</blockquote>

<p>也正因为如此，插件才不是一个附着在系统外部的小技巧，而是真正进入了这套曲面 <code class="language-plaintext highlighter-rouge">Pattern</code> 检测系统内部结构的一层平台化边界。</p>

<h2 id="四为什么二维码条码和规则-logo-特别适合走插件">四、为什么二维码、条码和规则 logo 特别适合走插件</h2>

<p>这一层最典型的例子，就是二维码。</p>

<p>二维码并不是“再加一种 <code class="language-plaintext highlighter-rouge">pattern</code>”这么简单。它会直接打破原有 <code class="language-plaintext highlighter-rouge">block</code> 的几何格局。对标准字符或规则 <code class="language-plaintext highlighter-rouge">pattern</code> 来说，系统可以先通过 <code class="language-plaintext highlighter-rouge">block</code> 把局部结构组织起来，再在 <code class="language-plaintext highlighter-rouge">block</code> 内做模板、配准和测量；但二维码的几何组织方式与这一套主链并不匹配。它既不是简单字符，也不是那类能够自然被骨架、注册集和测量集组织起来的标准局部对象。</p>

<p>如果硬要把二维码写进原有 <code class="language-plaintext highlighter-rouge">block</code> 主链，不只是代码不优雅，更重要的是会破坏原有体系的稳定性。也正因为如此，二维码最适合作为插件来处理。它不必强行服从原有 <code class="language-plaintext highlighter-rouge">block</code> 的几何组织逻辑，而可以走自己的检测与解码路径。</p>

<p>这不是抽象推理，而是实际发生过的需求：二维码确实出现在索尼游戏手柄的镭雕图案中，而且还专门形成过针对二维码缺陷检测的专利。也就是说，二维码并不是理论上的“也许会遇到”，而是真实出现过、并且已经证明不适合硬塞进主检测链的对象。</p>

<p>条码与某些规则 <code class="language-plaintext highlighter-rouge">logo</code> 的情况也是类似的。条码和二维码都更适合独立检测与解码；而一些规则 <code class="language-plaintext highlighter-rouge">logo</code> 虽然未必需要复杂解码，但也不一定非要走骨架配准主链，很多时候用灰度模板匹配等更轻量的方式就足够。</p>

<p>这时再看这套系统的整体结构，主链与插件链之间的分工就会更清楚：</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure9_main_chain_vs_plugin_path_v2.png" alt="Standard main chain vs. plugin path for heterogeneous objects" /></p>

<p><strong>图 2：</strong> 主检测链与插件路径的对照示意图。左侧是面向标准结构化对象的模板—配准—测量主链，右侧是面向二维码、条码、规则 <code class="language-plaintext highlighter-rouge">logo</code> 以及残胶、毛絮等异构对象的插件路径。插件的价值，不在于“多一种功能”，而在于避免用特例污染主链。</p>

<p>所以，插件在这里的意义非常明确：它不是只为“高难对象”服务，而是为所有与主检测链组织方式不匹配的对象提供统一接入方式。</p>

<h2 id="五插件管理器真正体现的不是动态库而是平台边界">五、插件管理器真正体现的，不是动态库，而是平台边界</h2>

<p>如果只从代码层面看，插件管理器做的事情似乎并不复杂：加载 <code class="language-plaintext highlighter-rouge">DLL</code>、获取函数地址、保存插件指针、按索引取回插件实例。</p>

<p>但这件事真正重要的地方，并不在于 Windows API 本身，而在于它明确划出了一条系统边界：</p>

<ul>
  <li>主系统知道如何加载插件；</li>
  <li>插件知道如何按统一接口返回结果；</li>
  <li>主系统不需要知道插件内部到底是模板匹配、解码算法、规则算法，还是别的东西。</li>
</ul>

<p>换句话说，插件管理器真正做的，不是“动态加载”这个动作，而是：</p>

<blockquote>
  <p><strong>把系统框架和对象特定算法清楚地分开。</strong></p>
</blockquote>

<p>这意味着，主系统可以继续保持自己的主干清晰，而不同对象的特定检测逻辑，则被隔离在各自插件内部。真正被统一起来的，不是算法内部细节，而是系统边界上的输入输出协议。</p>

<h2 id="六为什么插件不仅能接住特殊规则对象也能接住-ai-模块">六、为什么插件不仅能接住特殊规则对象，也能接住 AI 模块</h2>

<p>插件机制如果只能接规则型特殊对象，它还只是“扩展几个功能”；但一旦它连 <code class="language-plaintext highlighter-rouge">AI</code> 模块都能接住，系统的性质就变了。</p>

<p>这套系统里，<code class="language-plaintext highlighter-rouge">AI</code> 模块并不是主链的一部分，而是以插件形式接入，用来检测残胶和毛絮这类对象。这个设计本身就说明了一件事：系统并没有把深度学习写死进主检测链，而是把它放在一个更合适的位置上。</p>

<p>这背后的原因其实很清楚。</p>

<p>残胶、毛絮这类对象，并不像字符或规则 <code class="language-plaintext highlighter-rouge">pattern</code> 那样容易被模板、骨架、注册集和测量集稳定刻画。它们更像是局部杂质、弱结构异常或纹理型异常。如果把这类对象硬塞进主链，不仅会让主链变脏，而且效果也未必稳。更合理的方式，是让主链继续负责结构化对象，而让 <code class="language-plaintext highlighter-rouge">AI</code> 插件去承接这类弱结构、杂质型或纹理型对象。</p>

<p>更重要的是，这里的 <code class="language-plaintext highlighter-rouge">AI</code> 也不是“扫整张图”的主干模型，而更像一个二级判别器。算法会先把置信区间之外的可疑点邻域位图截取出来，再由深度模型对这些局部邻域进行分类。也就是说，<code class="language-plaintext highlighter-rouge">AI</code> 并不是替代主链，而是在主链筛出可疑对象之后，对这些局部区域做进一步复核。</p>

<p>如果把这一层再压缩成一条工程链，<code class="language-plaintext highlighter-rouge">AI</code> 插件在系统中的位置大致如下：</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure9_ai_plugin_path_v2.png" alt="Engineering path of the AI plugin" /></p>

<p><strong>图 3：</strong> <code class="language-plaintext highlighter-rouge">AI</code> 插件的工程接入路径。主检测链先筛出可疑局部邻域，再把这些局部样本转为批量输入，加载 <code class="language-plaintext highlighter-rouge">.pb</code> 模型并通过 OpenCV DNN 与 CUDA 完成推理，最后将分类结果回写为统一缺陷输出。</p>

<p>从工程上看，这个模块也已经很完整：</p>

<ul>
  <li>训练框架可以来自 TensorFlow、PyTorch、Caffe、Keras；</li>
  <li>模型最终导出为 <code class="language-plaintext highlighter-rouge">*.pb</code> 文件；</li>
  <li>运行时由 <code class="language-plaintext highlighter-rouge">ini</code> 配置加载模型路径、输入尺寸、输出维度、阈值等参数；</li>
  <li>推理阶段通过 OpenCV DNN 调用 TensorFlow 模型，并使用 CUDA 加速。</li>
</ul>

<p>这一点从代码里看得很清楚，例如模型加载和 CUDA 后端：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">bool</span> <span class="n">NeuralNetwork</span><span class="o">::</span><span class="n">initial_tf_model</span><span class="p">(</span><span class="n">string</span> <span class="n">path</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="mi">6</span> <span class="o">&gt;</span> <span class="n">path</span><span class="p">.</span><span class="n">length</span><span class="p">())</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">net</span> <span class="o">=</span> <span class="n">readNetFromTensorflow</span><span class="p">(</span><span class="n">path</span><span class="p">);</span>

    <span class="n">net</span><span class="p">.</span><span class="n">setPreferableBackend</span><span class="p">(</span><span class="n">DNN_BACKEND_CUDA</span><span class="p">);</span>
    <span class="n">net</span><span class="p">.</span><span class="n">setPreferableTarget</span><span class="p">(</span><span class="n">DNN_TARGET_CUDA</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">net</span><span class="p">.</span><span class="n">empty</span><span class="p">())</span>
    <span class="p">{</span>
        <span class="n">Logger</span><span class="o">::</span><span class="n">Instance</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">TraceError</span><span class="p">(</span><span class="s">"dnn:no model"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">else</span>
    <span class="p">{</span>
        <span class="n">Logger</span><span class="o">::</span><span class="n">Instance</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">TraceError</span><span class="p">(</span><span class="s">"Deep network model has loaded successfully..."</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>以及批量局部样本的输入与前向推理：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="n">NeuralNetwork</span><span class="o">::</span><span class="n">tf_inv_net_defect</span><span class="p">(</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span> <span class="n">result_set</span><span class="p">,</span> <span class="n">vector</span><span class="o">&lt;</span><span class="n">Mat</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">matset4dl</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">int</span> <span class="n">size</span> <span class="o">=</span> <span class="n">matset4dl</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isvalid</span> <span class="o">||</span> <span class="mi">0</span> <span class="o">==</span> <span class="n">size</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">Mat</span> <span class="n">inputBlob</span> <span class="o">=</span> <span class="n">blobFromImages</span><span class="p">(</span><span class="n">matset4dl</span><span class="p">,</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="mi">255</span><span class="p">,</span> <span class="n">Size</span><span class="p">(</span><span class="n">cols</span><span class="p">,</span> <span class="n">rows</span><span class="p">),</span> <span class="n">Scalar</span><span class="p">(),</span> <span class="nb">false</span><span class="p">,</span> <span class="nb">false</span><span class="p">);</span>

    <span class="n">net</span><span class="p">.</span><span class="n">setInput</span><span class="p">(</span><span class="n">inputBlob</span><span class="p">);</span>
    <span class="n">Mat</span> <span class="n">pred</span> <span class="o">=</span> <span class="n">net</span><span class="p">.</span><span class="n">forward</span><span class="p">();</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">!=</span> <span class="n">size</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">float</span> <span class="o">*</span><span class="n">v</span> <span class="o">=</span> <span class="n">pred</span><span class="p">.</span><span class="n">ptr</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
        <span class="n">softmax</span><span class="p">(</span><span class="n">v</span><span class="p">);</span>
        <span class="n">result_set</span><span class="p">.</span><span class="n">push_back</span><span class="p">(</span><span class="n">v</span><span class="p">[</span><span class="n">ngIndex</span><span class="p">]);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>所以，这一层最值得强调的，不是“用了 AI”，而是：</p>

<blockquote>
  <p><strong>AI 也被纳入了同一套插件化接入机制。</strong></p>
</blockquote>

<h2 id="七所以这一层让系统从固定算法变成可扩展平台">七、所以，这一层让系统从“固定算法”变成“可扩展平台”</h2>

<p>把前面的这些内容放在一起再看，就会发现：插件机制真正带来的，不只是几个额外功能，而是系统性质的变化。</p>

<p>如果没有插件，这套系统再强，也仍然更像一个“固定算法集合”：它有很强的主检测链，但新对象一来，就必须继续往 <code class="language-plaintext highlighter-rouge">DLL</code> 里加特例。</p>

<p>而有了插件之后，局面就不同了：</p>

<ul>
  <li>主链负责标准结构化检测对象；</li>
  <li>规则插件负责条码、二维码、规则 <code class="language-plaintext highlighter-rouge">logo</code> 等异构对象；</li>
  <li><code class="language-plaintext highlighter-rouge">AI</code> 插件负责残胶、毛絮等弱结构或纹理型异常；</li>
  <li>所有这些对象虽然内部算法不同，但都能通过统一边界接回主系统。</li>
</ul>

<p>也正因为如此，这套系统才开始从“固定算法集合”长成“可扩展检测平台”。</p>

<p>所以，如果要把这一篇压缩成一句话，我更愿意这样说：</p>

<blockquote>
  <p>插件机制真正改变的，不只是系统能多做几件事，而是它让这套曲面 <code class="language-plaintext highlighter-rouge">Pattern</code> 检测系统从一组固定算法，长成了一个能够持续接入新对象、新算法范式和新业务需求的平台。</p>
</blockquote>]]></content><author><name></name></author><summary type="html"><![CDATA[Why the Curved-Surface Pattern Inspection System Should Not Be Hard-Wired: A Plugin Mechanism for QR Codes, Barcodes, Logos, and AI Modules]]></summary></entry><entry><title type="html">第八讲：曲面 Pattern 缺陷检测里，为什么有些区域要故意不检——ROI 擦除与局部禁检机制</title><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/02/why-some-regions-must-be-intentionally-ignored-in-curved-surface-pattern-inspection.html" rel="alternate" type="text/html" title="第八讲：曲面 Pattern 缺陷检测里，为什么有些区域要故意不检——ROI 擦除与局部禁检机制" /><published>2026-04-02T19:00:00+08:00</published><updated>2026-04-02T19:00:00+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/02/why-some-regions-must-be-intentionally-ignored-in-curved-surface-pattern-inspection</id><content type="html" xml:base="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/02/why-some-regions-must-be-intentionally-ignored-in-curved-surface-pattern-inspection.html"><![CDATA[<h1 id="why-some-regions-in-curved-surface-pattern-inspection-must-be-intentionally-ignored-roi-erasure-and-local-exclusion-rules">Why Some Regions in Curved-Surface Pattern Inspection Must Be Intentionally Ignored: ROI Erasure and Local Exclusion Rules</h1>

<blockquote>
  <p><strong>English Abstract</strong><br />
This article explains why some local regions inside a block ROI should be explicitly excluded from detection in a curved-surface pattern inspection system. The point is not to make ROI geometry arbitrarily complex, but to preserve the stability of the rectangular block framework while introducing a lightweight rule layer for local exclusion. The XML-based <code class="language-plaintext highlighter-rouge">eraseSet</code> mechanism describes where an exclusion region starts, from which side it enters the ROI, and how far it extends inward. The article also distinguishes this configuration-layer ROI erasure from another type of ambiguity caused by block overlap, which is more effectively handled at runtime by execution order control. In this sense, local exclusion and execution priority belong to different layers of the system, but both serve the same goal: bringing local ambiguity back into a controllable range.</p>
</blockquote>

<p>在前面几篇里，我已经讨论了这套系统的问题本体、模板如何组织、配准如何发挥作用、缺陷如何沿测量集被逐点测出来，以及整套系统为什么能够在真实产线上稳定跑起来。</p>

<p>但即便到了这一步，检测区域本身仍然不意味着“框出来就都该检”。</p>

<p>这其实是工业系统里一个很容易被忽略、但又非常现实的问题：</p>

<blockquote>
  <p><strong>某个 block 的 ROI 一旦确定，并不等于这个 ROI 内的所有局部都适合继续参与检测。</strong></p>
</blockquote>

<p>换句话说，问题并不总是出在“算法不会检”；有时恰恰出在：</p>

<blockquote>
  <p><strong>某些局部区域本来就不该被纳入当前 block 的检测范围。</strong></p>
</blockquote>

<p>如果这一点没有被显式处理，那么后面的误判就很容易被一路带出来。而在这套系统里，解决这个问题的办法，并不是推翻原有的矩形 ROI 体系，也不是贸然引入更复杂、更高风险的多边形 block，而是在 <code class="language-plaintext highlighter-rouge">xml</code> 层增加了一组非常轻量、但很有力量的局部几何规则：<code class="language-plaintext highlighter-rouge">eraseSet</code>。</p>

<p>这一篇，我想专门把这件事讲清楚：</p>

<ol>
  <li>为什么有些 ROI 内部需要“局部禁检”；</li>
  <li><code class="language-plaintext highlighter-rouge">eraseSet</code> 到底在描述什么；</li>
  <li>为什么这不是补丁，而是一组显式的局部几何规则；</li>
  <li>为什么 ROI 擦除和 block 相交其实不是同一个问题；</li>
  <li>为什么某些 block 相交带来的歧义，反而更适合由执行顺序去消解。</li>
</ol>

<h2 id="一为什么有些-roi-内部需要局部禁检">一、为什么有些 ROI 内部需要“局部禁检”</h2>

<p>如果只从表面理解，检测系统似乎只要把一个 block 框出来，然后在这个矩形 ROI 内继续完成配准、测量和缺陷判定即可。</p>

<p>但真实现场里，事情并不总是这么干净。</p>

<p>因为 block 的矩形 ROI，本质上是一种工程上稳定、实现上简单、并行上友好的局部组织方式。它的优点是清楚、稳健、容易实施，也便于和模板、配准、多线程任务拆分一起形成完整系统；但它并不意味着 ROI 里的每一个像素区域都天然适合参与检测。</p>

<p>换句话说，矩形 ROI 是一种<strong>检测组织单元</strong>，而不是天然精确的语义边界。</p>

<p>这就意味着：在某些具体产品和具体图样里，ROI 内部可能会出现一些局部区域，虽然几何上被框进来了，但从检测逻辑上看，并不应该继续参与当前 block 的缺陷判断。如果不把这些区域显式排除掉，误判就很容易被带出来。</p>

<p>所以，这一层真正要解决的问题并不是“如何更复杂地表达 ROI”，而是：</p>

<blockquote>
  <p><strong>在保留矩形 block 体系稳定性的前提下，如何把当前 block 中那些本来就不该检的局部区域，从检测链里显式拿掉。</strong></p>
</blockquote>

<h2 id="二eraseset-到底在描述什么">二、<code class="language-plaintext highlighter-rouge">eraseSet</code> 到底在描述什么</h2>

<p>在前面第三篇讨论模板结构时，已经看到这套系统会把字符模板、全局几何锚点、block 序列和模板入口统一组织进同一份 xml 文件中。<code class="language-plaintext highlighter-rouge">eraseSet</code> 正是这份模板 xml 里的另一层标签。它并不负责描述字符骨架或几何锚点，而是专门用来定义：在某个 block 的矩形 ROI 内，哪些局部区域应当被显式排除出当前检测范围。</p>

<p>最直接的形式，就是下面这样的配置：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;eraseSet</span> <span class="na">size=</span><span class="s">"1"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;erase</span> <span class="na">dock0=</span><span class="s">"right"</span> <span class="na">dock1=</span><span class="s">"begin"</span> <span class="na">enterDist=</span><span class="s">"0"</span> <span class="na">range=</span><span class="s">"0"</span> <span class="na">type=</span><span class="s">"1"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/eraseSet&gt;</span>
</code></pre></div></div>

<p>这段配置看起来很短，但它做的事情并不简单。它并不是在“随便删掉一块区域”，而是在描述：</p>

<ul>
  <li>针对某一类 <code class="language-plaintext highlighter-rouge">ROI</code>，从哪一边进入；</li>
  <li>在这一边的哪个位置开始；</li>
  <li>向内擦除多深；</li>
  <li>沿这一边擦除多大范围。</li>
</ul>

<p>具体来说：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">dock0</code>：标识从 <code class="language-plaintext highlighter-rouge">ROI</code> 的哪一边进入擦除，可取 <code class="language-plaintext highlighter-rouge">left</code>、<code class="language-plaintext highlighter-rouge">right</code>、<code class="language-plaintext highlighter-rouge">top</code>、<code class="language-plaintext highlighter-rouge">bottom</code></li>
  <li><code class="language-plaintext highlighter-rouge">dock1</code>：标识在该边的哪个位置开始擦除，可取 <code class="language-plaintext highlighter-rouge">begin</code>、<code class="language-plaintext highlighter-rouge">center</code>、<code class="language-plaintext highlighter-rouge">end</code></li>
  <li><code class="language-plaintext highlighter-rouge">enterDist</code>：标识擦除区域向 <code class="language-plaintext highlighter-rouge">ROI</code> 内部伸进的距离</li>
  <li><code class="language-plaintext highlighter-rouge">range</code>：标识擦除沿该边扩展的范围</li>
  <li><code class="language-plaintext highlighter-rouge">type</code>：对应当前作用的 <code class="language-plaintext highlighter-rouge">block / 模板</code> 类型</li>
</ul>

<p>也就是说，<code class="language-plaintext highlighter-rouge">eraseSet</code> 不是模糊补丁，而是一组<strong>显式的局部几何规则</strong>。它把“哪些区域不该继续参与当前检测”这件事，从实现层的临时判断，提升成了配置层的正式描述。</p>

<h2 id="三擦除不是手工补丁而是一组显式局部几何规则">三、擦除不是手工补丁，而是一组显式局部几何规则</h2>

<p>如果只看参数名，<code class="language-plaintext highlighter-rouge">dock0="left"</code>、<code class="language-plaintext highlighter-rouge">dock1="center"</code> 这样的写法会显得有些抽象。下面这两幅示意图可以更直观地说明：擦除规则并不是随意抹掉一块区域，而是在 <code class="language-plaintext highlighter-rouge">ROI</code> 内显式定义“从哪一边进入、从哪个位置开始、向内延伸多深”的局部禁检区。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/b1.bmp" alt="dock0=&quot;left&quot;, dock1=&quot;center&quot; 的擦除示意图" /></p>

<p><strong>图 1：</strong> <code class="language-plaintext highlighter-rouge">dock0="left"</code>、<code class="language-plaintext highlighter-rouge">dock1="center"</code> 的局部擦除示意。表示从 <code class="language-plaintext highlighter-rouge">ROI</code> 左边进入，并在左边的中段位置开始定义擦除区域。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/b2.bmp" alt="dock0=&quot;bottom&quot;, dock1=&quot;begin&quot; 的擦除示意图" /></p>

<p><strong>图 2：</strong> <code class="language-plaintext highlighter-rouge">dock0="bottom"</code>、<code class="language-plaintext highlighter-rouge">dock1="begin"</code> 的局部擦除示意。表示从 <code class="language-plaintext highlighter-rouge">ROI</code> 下边进入，并在该边的起始位置开始定义擦除区域。</p>

<p>一旦结合这两幅图再看 <code class="language-plaintext highlighter-rouge">eraseSet</code>，它的工程意义就会很清楚：这不是一种“现场看着不顺眼就手工抹掉”的做法，而是在矩形 <code class="language-plaintext highlighter-rouge">ROI</code> 体系内部，增加一层<strong>可配置、可解释、可复查</strong>的局部禁检规则。</p>

<p>这也是为什么我会觉得它非常“工业”。因为它并没有推翻原有 <code class="language-plaintext highlighter-rouge">block</code> 体系，也没有把问题升级成更大、更复杂的几何表达问题，而是：</p>

<ul>
  <li>保留矩形 <code class="language-plaintext highlighter-rouge">ROI</code>；</li>
  <li>保留原有模板与配准链；</li>
  <li>保留原有实施方式与并行组织；</li>
  <li>只对局部确实不该参与检测的区域做最小修正。</li>
</ul>

<p>从这个角度看，擦除功能的真正意义不在于“删掉一块区域”，而在于：</p>

<blockquote>
  <p><strong>把局部不该检的地方，从系统层面明确写出来。</strong></p>
</blockquote>

<h2 id="四为什么没有直接把-block-改成多边形">四、为什么没有直接把 block 改成多边形</h2>

<p>从纯几何表达角度看，如果 <code class="language-plaintext highlighter-rouge">block</code> 的目标区域本来就不是矩形，那么一个看起来很自然的想法就是：</p>

<blockquote>
  <p>既然矩形 <code class="language-plaintext highlighter-rouge">ROI</code> 会把一些不该检测的区域一起框进来，那为什么不干脆把 <code class="language-plaintext highlighter-rouge">block</code> 设计成多边形？</p>
</blockquote>

<p>这个想法在几何上当然成立。多边形 <code class="language-plaintext highlighter-rouge">ROI</code> 理论上更自由，也更容易贴合对象边界。</p>

<p>但在工业检测系统里，“表达更自由”并不天然等于“系统更好”。</p>

<p>因为一旦从矩形 <code class="language-plaintext highlighter-rouge">ROI</code> 改成多边形 <code class="language-plaintext highlighter-rouge">ROI</code>，复杂度会立刻沿着整条链扩散：</p>

<ul>
  <li>模板创建复杂化；</li>
  <li>实施工程师标注难度上升；</li>
  <li><code class="language-plaintext highlighter-rouge">ROI</code> 提取与内存组织复杂化；</li>
  <li>后续 <code class="language-plaintext highlighter-rouge">mask</code>、裁切和边界处理复杂化；</li>
  <li>配准误差与边界误差的容忍度变低；</li>
  <li>多线程任务拆分和现场调试风险一起上升。</li>
</ul>

<p>更重要的是，多边形 <code class="language-plaintext highlighter-rouge">ROI</code> 虽然在表达上更灵活，但在真实现场里往往更“脆”。因为它贴得越紧，对局部定位误差、局部形变和成像扰动就越敏感。于是原本为了“更精确”而引入的复杂表达，最后反而可能把系统带向更高的误判风险和更低的可实施性。</p>

<p>所以，从工程角度看，多边形 <code class="language-plaintext highlighter-rouge">block</code> 并不是一个“更先进”的自然升级，而更像是把一个局部问题，推向了整条系统链的高风险复杂化。</p>

<p>也正因为如此，<code class="language-plaintext highlighter-rouge">eraseSet</code> 这种做法才显得非常成熟：它不追求几何表达的最大自由度，而是在保留矩形 <code class="language-plaintext highlighter-rouge">block</code> 体系稳定性的前提下，用最小改动去消除局部歧义。</p>

<h2 id="五为什么-roi-擦除和-block-相交其实不是同一个问题">五、为什么 ROI 擦除和 block 相交其实不是同一个问题</h2>

<p>这里需要特别澄清一件事：</p>

<blockquote>
  <p><strong>ROI 擦除并不等于 block 相交问题的主解法。</strong></p>
</blockquote>

<p>这两件事在表面上都像“局部区域不该继续参与检测”，但它们其实属于两个不同层次的问题。</p>

<h3 id="1-roi-擦除解决的是单个-block-内部的局部禁检">1. ROI 擦除解决的是单个 block 内部的局部禁检</h3>

<p>也就是说，当前这个 <code class="language-plaintext highlighter-rouge">block</code> 已经确定，<code class="language-plaintext highlighter-rouge">ROI</code> 也已经确定。问题在于：这个 <code class="language-plaintext highlighter-rouge">ROI</code> 里有一小块区域，本来就不该继续参与当前 <code class="language-plaintext highlighter-rouge">block</code> 的检测。这时，<code class="language-plaintext highlighter-rouge">eraseSet</code> 的作用就是把这块区域显式擦掉。</p>

<h3 id="2-block-相交解决的是不同-block-之间的局部歧义">2. block 相交解决的是不同 block 之间的局部歧义</h3>

<p>而另一个问题——两个矩形 <code class="language-plaintext highlighter-rouge">block</code> 几何上发生交集，容易引起多镭误判——本质上并不是“当前 <code class="language-plaintext highlighter-rouge">ROI</code> 内有块地方不该检”，而是：</p>

<blockquote>
  <p><strong>同一局部区域同时进入了两个 block 的检测视野。</strong></p>
</blockquote>

<p>这是另一层问题。它的核心不是“当前 <code class="language-plaintext highlighter-rouge">block</code> 自己该不该看这块区域”，而是“两个 <code class="language-plaintext highlighter-rouge">block</code> 谁该先看、谁该后看，以及谁的结果应该在这个局部优先成立”。</p>

<p>也正因为如此，它最漂亮的处理方式，并不是继续往 <code class="language-plaintext highlighter-rouge">eraseSet</code> 里堆规则，而是运行层的执行顺序控制。</p>

<h2 id="六为什么某些-block-相交反而更适合靠执行顺序解决">六、为什么某些 block 相交，反而更适合靠执行顺序解决</h2>

<p>对于少数存在几何交集的 <code class="language-plaintext highlighter-rouge">block</code>，现场上效果最好的办法，并不是强行再去增加一层显式擦除，而是：</p>

<ol>
  <li>先运行那个“插入别人区域”的 <code class="language-plaintext highlighter-rouge">block</code>；</li>
  <li>它运行完以后清理自身影响；</li>
  <li>再运行另一个受影响的 <code class="language-plaintext highlighter-rouge">block</code>；</li>
  <li>后者就不会再受到相交区域的污染。</li>
</ol>

<p>这个思路本质上并不是 <code class="language-plaintext highlighter-rouge">ROI</code> 擦除，而是一种<strong>执行层的局部消歧策略</strong>。</p>

<p>我更愿意把它理解成：</p>

<blockquote>
  <p><strong>通过 block 执行优先级，先处理局部歧义的主动一方，再让后续 block 在更干净的局部区域上继续检测。</strong></p>
</blockquote>

<p>这件事之所以重要，不只是因为它“现场效果很好”，而是因为它说明这套系统在遇到复杂局部歧义时，并不是执着于用同一类几何规则去解决所有问题，而是：</p>

<ul>
  <li>配置层的问题，用配置层规则解决；</li>
  <li>执行层的问题，用执行层调度解决。</li>
</ul>

<p>这其实是一种非常成熟的系统判断。</p>

<p>当然，这里也要说清楚：这种顺序控制更接近实现层的工程经验，而不是像 <code class="language-plaintext highlighter-rouge">eraseSet</code> 那样的显式几何规则。它可以作为少数特殊局部歧义的漂亮解法，但不应取代 <code class="language-plaintext highlighter-rouge">ROI</code> 擦除，去承担“哪些区域本来就不该检”这种更基础的配置层职责。</p>

<h2 id="七所以这一层体现的不是补丁思维而是工程判断">七、所以，这一层体现的不是补丁思维，而是工程判断</h2>

<p>把这一篇讲到这里，其实就能看清一件事：这套系统面对局部歧义时，并没有简单地走向“更复杂的几何表达”，也没有把所有问题都压成同一种补丁，而是很清楚地区分了两类事情：</p>

<ul>
  <li><strong>哪些地方本来就不该检</strong>：用 <code class="language-plaintext highlighter-rouge">eraseSet</code> 这样的显式局部几何规则解决；</li>
  <li><strong>哪些地方是不同 block 之间的局部相交歧义</strong>：用执行顺序这样的运行层策略去消解。</li>
</ul>

<p>这两者分别属于配置层和执行层。真正体现成熟度的，并不是“功能越多越好”，而是：</p>

<blockquote>
  <p><strong>在什么层次的问题上，用什么层次的规则去解决。</strong></p>
</blockquote>

<p>所以，如果要把这一篇压缩成一句话，我更愿意这样说：</p>

<blockquote>
  <p>显式 <code class="language-plaintext highlighter-rouge">ROI</code> 擦除解决的是“哪些地方本来就不该检”，而执行优先级解决的是“局部相交时谁先看、谁后看”。这两者分别属于配置层和运行层，共同把局部歧义收回到了可控范围。</p>
</blockquote>

<p>下一讲，我更愿意继续沿着这一点往下写：当模板、<code class="language-plaintext highlighter-rouge">ROI</code>、局部测量与运行层调度都已经建立起来之后，这套系统是如何在现场不断吸收新增需求、同时又努力维持主干稳定的。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Why Some Regions in Curved-Surface Pattern Inspection Must Be Intentionally Ignored: ROI Erasure and Local Exclusion Rules]]></summary></entry><entry><title type="html">第七讲：为什么模板不是随手截个图？这套系统真正难的是模板生产链</title><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/02/why-templates-are-not-just-cropped-images-the-template-production-chain.html" rel="alternate" type="text/html" title="第七讲：为什么模板不是随手截个图？这套系统真正难的是模板生产链" /><published>2026-04-02T12:00:00+08:00</published><updated>2026-04-02T12:00:00+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/02/why-templates-are-not-just-cropped-images-the-template-production-chain</id><content type="html" xml:base="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/02/why-templates-are-not-just-cropped-images-the-template-production-chain.html"><![CDATA[<h1 id="why-templates-are-not-just-cropped-images-the-real-difficulty-lies-in-the-template-production-chain">Why Templates Are Not Just Cropped Images: The Real Difficulty Lies in the Template Production Chain</h1>

<blockquote>
  <p><strong>English Abstract</strong><br />
This article explains why template creation in the inspection system is not a trivial image-cropping step, but a full production chain. Templates must be validated, corrected, standardized, and organized into structural objects before they can enter XML, CSV, and the downstream detection pipeline. More importantly, template production also determines the minimal registration unit: some templates are built from a single character, while others deliberately merge multiple characters or local structures to improve the robustness of nonrigid registration. From this perspective, template production is not a preparatory convenience, but an upstream structural mechanism that directly affects registration stability, missed defects, and false alarms.</p>
</blockquote>

<p>在前面几篇里，我已经讲了这套系统的问题本体、xml 如何组织模板与几何、两层配准如何发挥作用、缺陷如何沿测量集被逐点测出来，以及整套系统为什么能够在真实产线上跑起来。</p>

<p>但这些内容其实都默认了一件事：</p>

<blockquote>
  <p><strong>模板已经存在。</strong></p>
</blockquote>

<p>而真实现场里，这恰恰不是理所当然的。</p>

<p>模板不是从天上掉下来的，也不是在原图上随手框一个区域、截一张图就能直接拿来用。对这套系统来说，模板必须先被生产出来，而且要被生产成一种能够进入后续检测链的结构对象：它要能被验证、能被修正、能被保存、能被标准化组织，最后还要能稳定地进入 <code class="language-plaintext highlighter-rouge">xml</code>、<code class="language-plaintext highlighter-rouge">csv</code> 和参数链。</p>

<p>也正因为如此，我一直觉得，这套系统真正容易被忽略的一层，不是检测函数本身，而是模板生产链。很多人谈工业检测时，只盯着“最后怎么判 NG”，却很少追问：这些后续检测所依赖的骨架、中心点、局部结构模板，最开始到底是怎么来的？如果这一层没有被可靠建立起来，后面的配准、测量和缺陷判定，其实都失去了稳定起点。</p>

<p>这一篇，我就想把这条线单独讲清楚：</p>

<ol>
  <li>为什么模板不是“截图”这么简单；</li>
  <li>模板创建工具真正解决了什么问题；</li>
  <li>为什么模板必须先经过验证、修正和标准化保存，才能进入后续检测链；</li>
  <li>为什么模板生产链本身就是这套系统能力的一部分。</li>
</ol>

<h2 id="一为什么模板不是截图这么简单">一、为什么模板不是“截图”这么简单</h2>

<p>如果只从表面看，模板创建这件事似乎很容易被理解成：打开一张图片，框出一个字符区域，然后存起来，供后面做匹配或检测使用。</p>

<p>但对这套系统来说，这样的理解太浅了。</p>

<p>因为这里的模板并不是一张单纯拿来参考的图片，而是后续配准、测量和缺陷判定的起点。它不仅要告诉系统“这个局部结构长什么样”，还要进一步提供骨骼点、中心点以及与 <code class="language-plaintext highlighter-rouge">block</code>、<code class="language-plaintext highlighter-rouge">Geo</code>、注册集、测量集相衔接的结构信息。也就是说，在这套系统里，模板不是视觉参考，而是一个会继续进入检测链的结构对象。</p>

<p>一旦把这一点看清楚，很多事情就会自然变得不同。模板创建不再只是“把图截出来”，而必须回答更严格的问题：这个局部区域是否足够干净？骨架是否可信？中心点是否正确？噪点是否已经剔除？保存出来的数据，后面能否被 <code class="language-plaintext highlighter-rouge">xml</code>、<code class="language-plaintext highlighter-rouge">csv</code> 和参数系统稳定接住？</p>

<p>所以，从这个意义上说，模板不是截图，而是模板生产链输出的结构对象；而这个结构对象最终如何被组织成后续配准的最小单元，又会继续决定检测的稳定性。</p>

<h2 id="二模板创建工具真正解决了什么问题">二、模板创建工具真正解决了什么问题</h2>

<p>这套系统后来之所以能跨产品、跨国别、跨产线复用，一个很重要的原因，就是模板并不是靠人手工零散拼出来的，而是已经被纳入了一条相对完整的生产链。模板创建工具的意义，也恰恰在这里。</p>

<p>从工具文档可以看得很清楚，这个工具较上一版增加了矩形框标注、中心点自动生成、中心点自动保存为标准格式等功能；在实际使用时，先输入单张源图像，再显示算法处理后的二值图像，而不是直接停留在原始灰度图上。之所以优先显示二值图，是为了让模板创建者能够更清楚地查看局部结构，并判断模板是否有效。</p>

<p>也就是说，这个工具首先解决的，不是“怎么方便地截个图”，而是：</p>

<ul>
  <li>模板区域如何被可靠截取；</li>
  <li>模板是否可信，如何被验证；</li>
  <li>模板如何进一步变成后续系统可用的标准化数据。</li>
</ul>

<p>一旦把这个工具放回整个检测链来看，它的地位就会很清楚：它不是一个附属小工具，而是模板生产链的入口。</p>

<h2 id="三为什么处理后的二值图比原图更重要">三、为什么“处理后的二值图”比原图更重要</h2>

<p>模板创建工具里有一个很关键的设计：在打开原始灰度图像之后，界面上优先呈现的并不是原图本身，而是算法处理后的二值图像。</p>

<p>这件事表面上看只是显示方式的选择，但其实非常能说明模板创建的立场。因为在这套系统里，模板并不是给人“看着像不像”的视觉参考图，而是后续骨架提取、中心点生成、配准与测量的上游输入。真正需要被确认的，并不是原图在视觉上是否清楚，而是这个局部区域经过算法处理之后，是否还能稳定地保留出后续检测真正依赖的结构信息。</p>

<p>也就是说，模板创建并不是站在人眼直观的层面上判断“这个字符看起来像不像”，而是站在后续检测链的层面上判断：这个局部区域是否足够干净，二值结果是否足够稳定，骨架是否有机会被可靠抽取，中心点和结构点能否被进一步组织成可进入 xml 与 csv 的标准化对象。</p>

<p>从这个意义上说，二值图之所以比原图更重要，并不是因为它在视觉上更简洁，而是因为它离后续检测真正使用的结构层更近。模板创建从一开始就不是视觉截图，而是在做面向检测链的结构确认。</p>

<h2 id="四为什么要有骨骼点可视化和噪点删除">四、为什么要有骨骼点可视化和噪点删除</h2>

<p>如果模板只是图像块，那么截出来保存即可；但如果模板要作为后续检测链的结构对象，它就不能只停留在图像块层面，而必须进一步进入骨架与中心点层面。</p>

<p>这也是为什么工具里会有骨骼点可视化功能。文档里明确提到，经算法处理后的骨骼点骨架图会在界面上显示出来，目的是让使用者能够判断模板是否可信。对于英文阿拉伯字符，骨骼点坐标会存储到 <code class="language-plaintext highlighter-rouge">xml</code> 文件中，中心点坐标会以标准格式存储到 <code class="language-plaintext highlighter-rouge">csv</code> 文件中；对于中韩等字符，中心点同样会以标准格式输出。</p>

<p>这说明模板创建并不是“一次性自动完成”的，而是一种半自动的人机协同过程。算法先把局部结构抽出来，工具再把骨架可视化，让人去判断这个模板是否值得保留。更进一步，当模板中存在干扰点时，工具还支持通过鼠标右键框选删除噪点，并在删除后自动保存骨骼点数据，以提高模板质量。</p>

<p>也就是说，这套系统里的模板质量，并不是靠“算法一次性输出完美结果”来保证的，而是通过可视化、人工修正和标准化保存共同保证的。</p>

<h2 id="五为什么模板创建不是自动导出而是一条高门槛的半自动生产链">五、为什么模板创建不是自动导出，而是一条高门槛的半自动生产链</h2>

<p>如果只看工具界面，模板创建似乎只是打开图像、框选 AOI、保存数据而已。但对这套系统来说，真实情况并不是这样。模板创建并不是一个自动导出步骤，而是一条强依赖实施工程师参与的半自动生产链。</p>

<p>首先，模板本身就不是简单截图。模板创建工具文档已经说明，打开原始灰度图之后，界面优先显示的是算法处理后的二值图，而不是原图本身；这样做的目的，是为了通过二值图清晰查看并确保模板是否有效。对于英文阿拉伯字符，工具会把骨骼点坐标存入 <code class="language-plaintext highlighter-rouge">xml</code>、中心点坐标以标准格式存入 <code class="language-plaintext highlighter-rouge">csv</code>；对于中韩等字符，则至少会把中心点标准化输出。也就是说，这里的模板从一开始就不是视觉参考图，而是后续检测链中的结构数据对象。</p>

<p>如果把这条过程压缩成一条更直观的链，它大致可以表示为下面这样：</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure7_template_creation_pipeline_v2.png" alt="Template creation pipeline for the laser-carving pattern inspection system" /></p>

<p><em>图 1：模板创建链流程图。模板并不是从原图中随手截取的图块，而是经过二值图确认、AOI 标注、骨架/特征点可视化、删噪修正以及 XML/CSV 导出之后，才能进入后续检测链的结构对象。</em></p>

<p>更进一步看，这里的模板甚至不能简单理解成“一个局部图块加若干附属点”。在这套系统里，模板是会真正进入 <code class="language-plaintext highlighter-rouge">xml</code> 的结构对象。最直观的形式，就是 <code class="language-plaintext highlighter-rouge">character</code> 下的 <code class="language-plaintext highlighter-rouge">skeleton</code> 节点。例如，在实际模板文件里，可以看到类似下面这样的结构：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;character&gt;</span>
    <span class="nt">&lt;skeleton</span> <span class="na">name=</span><span class="s">"L"</span> <span class="na">size=</span><span class="s">"30"</span> <span class="na">genus=</span><span class="s">"1"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"1534"</span> <span class="na">y=</span><span class="s">"322"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"1535"</span> <span class="na">y=</span><span class="s">"326"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"1535"</span> <span class="na">y=</span><span class="s">"330"</span> <span class="nt">/&gt;</span>
        <span class="c">&lt;!-- ... --&gt;</span>
    <span class="nt">&lt;/skeleton&gt;</span>

    <span class="nt">&lt;skeleton</span> <span class="na">name=</span><span class="s">"vci"</span> <span class="na">size=</span><span class="s">"1104"</span> <span class="na">genus=</span><span class="s">"1"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"522"</span> <span class="na">y=</span><span class="s">"30"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"527"</span> <span class="na">y=</span><span class="s">"30"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"479"</span> <span class="na">y=</span><span class="s">"31"</span> <span class="nt">/&gt;</span>
        <span class="c">&lt;!-- ... --&gt;</span>
    <span class="nt">&lt;/skeleton&gt;</span>
<span class="nt">&lt;/character&gt;</span>
</code></pre></div></div>
<p>这里最值得注意的，并不是 <code class="language-plaintext highlighter-rouge">p(x,y)</code> 这些点本身，而是：不同类型的检测对象——字符、<code class="language-plaintext highlighter-rouge">logo</code>、实心区域 <code class="language-plaintext highlighter-rouge">pattern</code> 等——都会以类似形式进入模板层。也就是说，<code class="language-plaintext highlighter-rouge">skeleton</code> 在 <code class="language-plaintext highlighter-rouge">xml</code> 里首先是一个统一的模板容器，而不是对几何对象类型的严格限定。</p>

<p>也正因为如此，<code class="language-plaintext highlighter-rouge">skeleton</code> 这个名字其实很容易让人误解。详细设计文档已经明确指出：<code class="language-plaintext highlighter-rouge">skeleton</code> 名义上虽然叫 <code class="language-plaintext highlighter-rouge">skeleton</code>，但实际上并不一定就是严格意义上的 <code class="language-plaintext highlighter-rouge">skeleton</code>，也可以是各种类型的特征点。换句话说，这一层的真正作用，并不是给对象贴一个几何学名词，而是把后续配准与测量所需要的结构点集合统一组织进模板文件。</p>

<p>对枝杈结构，例如英文字符，这类模板更接近 <code class="language-plaintext highlighter-rouge">medial axis</code> 一类的中轴特征；而对实心区域 <code class="language-plaintext highlighter-rouge">pattern</code>，则需要用另一类特征提取方式。到了检测阶段，这种差异会进一步体现在注册集和测量集的组织上：枝杈结构的注册集和测量集可以都取 <code class="language-plaintext highlighter-rouge">medial axis</code>，而实心结构的注册集则是边缘点集合，测量集是内点集合。也就是说，<code class="language-plaintext highlighter-rouge">medial axis</code> 和实心区域 <code class="language-plaintext highlighter-rouge">pattern</code> 本来就是两类不同对象，它们并不依赖同一种模板生成算法。</p>

<p>这条链并不是纸上抽象流程，而是被具体工具支撑起来的。下面这张局部界面图可以更直观地说明：模板创建同时包含图像确认、骨架判读、删噪修正和结构数据导出等环节。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/aa.bmp" alt="Masked local view of the template-creation tool" /></p>

<p><strong>图 2：</strong> 模板创建工具的局部界面示意。工具并不只是完成图像截取，还提供二值图确认、骨架可视化、删噪修正和结构数据导出等功能，从而把模板创建变成一条可验证、可修正、可保存的半自动生产链。</p>

<p>这件事之所以必须强调，是因为它直接决定了模板创建并不是“会点工具就行”。实施工程师不仅要会框 <code class="language-plaintext highlighter-rouge">AOI</code>、会保存 <code class="language-plaintext highlighter-rouge">xml</code> 和 <code class="language-plaintext highlighter-rouge">csv</code>，更要理解：当前面对的到底是哪一类对象，骨架生成算法输出出来的点集在几何上意味着什么，哪些点是可信结构，哪些只是噪声。如果把这件事理解错了，那么后面的骨骼点可视化、删噪、参数调节和模板保存都会变成机械操作，而机械操作恰恰最容易把错误带进后续检测链。</p>

<p>而且，这条模板生产链并不只停留在“把骨架点存出来”这一步。模板是否可信，必须通过处理后的二值图与骨架图共同判断：二值结果决定局部结构是否被稳定提取，骨架结果决定这些结构能否进一步进入后续配准与测量。对于干扰点较多的局部区域，工具允许通过右键框选直接删除噪点，以提高模板质量；缩放因子和 <code class="language-plaintext highlighter-rouge">blocksize</code> 也都可以调整，分别影响匹配效率与二值化效果。</p>

<p>这说明工具真正做的，并不是“一键导出模板”，而是把模板创建过程中那些会实质影响质量的判断点显式暴露出来，让实施工程师参与确认、修正和收敛。模板创建因此不是一次自动输出，而是一条半自动的人机协同生产链。</p>

<p>这也正是模板创建门槛高的地方。实施工程师真正面对的问题，并不是“会不会用界面”，而是：</p>

<ul>
  <li>能不能判断 <code class="language-plaintext highlighter-rouge">AOI</code> 是否合理；</li>
  <li>能不能看懂骨架质量；</li>
  <li>能不能分辨哪些点属于结构本体，哪些只是噪声；</li>
  <li>能不能理解阈值和参数会怎样影响后续骨架生成与检测结果。</li>
</ul>

<p>骨架点一旦生成不好，后面的漏检和过杀就会被直接带进系统；而 <code class="language-plaintext highlighter-rouge">ini</code> 和 <code class="language-plaintext highlighter-rouge">XML</code> 即便能够自动生成，也并不意味着配置已经合格。真正可上线的配置，仍然需要拿现场 <code class="language-plaintext highlighter-rouge">CCD</code> 采集图在实验室调试，再到产线继续收敛，直到最终上线。也就是说，模板创建工具只是模板生产链的入口，而不是整条链的终点。</p>

<p>从这个意义上说，这条链真正缺的并不是普通意义上的“实施人员”，而是贴近现场的<strong>应用算法工程师</strong>。这样的人既要愿意长期做现场，又要有足够的图像处理理解力、经验和责任心；稍有能力的人往往不愿长期承担这类又细又重的工作，而能力不足的人又承载不起模板质量和参数质量。对这套系统而言，这恰恰是后来推广困难的核心原因之一：不是系统主线不成立，而是模板生产链和参数闭环链对人的要求太高，而组织层面对这一岗位的定义、权限和资源未必匹配。</p>

<p>所以，如果要把这一节压缩成一句话，我更愿意这样说：</p>

<blockquote>
  <p>模板创建并不是检测系统之外的准备动作，而是一条强依赖实施工程师能力的半自动生产链；而这条链的质量，直接决定了后续配准、测量以及最终漏检和过杀的水平。</p>
</blockquote>

<h2 id="六为什么模板生产不仅在提点更在决定配准单元如何组织">六、为什么模板生产不仅在“提点”，更在决定配准单元如何组织</h2>

<p>模板生产链里还有一个特别关键、但前面一直没有单独展开的问题：模板并不只是“把骨架点提出来”就结束了，更重要的是，这些点最终按什么单元进入 <code class="language-plaintext highlighter-rouge">xml</code>，决定了后续配准的稳定性。</p>

<p>在这套系统里，<code class="language-plaintext highlighter-rouge">skeleton</code> 名义上看像是在存某个字符或某个局部图样的结构点，但从检测链条来看，它首先对应的是一个更本质的角色：<strong>配准的最小单元</strong>。而这个最小单元，并不总是“一个字符对应一个 <code class="language-plaintext highlighter-rouge">skeleton</code>”。有时候，一个字符会单独形成一个 <code class="language-plaintext highlighter-rouge">skeleton</code>；有时候，多个字符或多个局部结构会被合并成同一个 <code class="language-plaintext highlighter-rouge">skeleton</code>。这并不是模板命名方式上的随意变化，而是模板生产阶段就必须做出的几何决策。</p>

<p>单个字符作为一个 <code class="language-plaintext highlighter-rouge">skeleton</code> 的好处，是局部定位更细，配准质量往往更高，也更不容易漏掉局部字符本身；但它也有代价：当对象本身过于简单、拓扑约束过弱时，非刚性配准有可能把真实缺陷“吸收进形变里”。例如，对字符 <code class="language-plaintext highlighter-rouge">1</code> 这样结构相对单薄的对象，即使原图中已经少掉一段，模板在配准时仍然可能被平滑地拉过去，看上去“配准成功”，从而在现场形成漏检。</p>

<p>相反，带有环柄、闭合回路或更强整体结构约束的对象，例如 <code class="language-plaintext highlighter-rouge">B</code>、<code class="language-plaintext highlighter-rouge">6</code> 一类字符，通常会更稳定。这种稳定性并不只是因为它们“更复杂”，而是因为环柄或闭合结构会给非刚性配准提供更强的整体约束：模板不再容易靠局部平滑形变去掩盖真实缺损，局部 <code class="language-plaintext highlighter-rouge">break</code> 也更难被整体形变无代价地吞掉。换句话说，拓扑结构本身会直接影响配准的鲁棒性与漏检风险。</p>

<p>而对于汉字、不规则图形，或者那些单独拿出来拓扑结构不够强的局部对象，这种风险则可以通过另一种方式缓解：把多个字符或多个局部结构合并为同一个 <code class="language-plaintext highlighter-rouge">skeleton</code>。这样做的意义，不只是“多放一些点”，而是在模板层面人为增强整体几何约束，使配准时不再只盯住一个过于脆弱的小对象，而是面对一个更完整、更稳定的结构单元。这样一来，虽然单点级别的自由度下降了一些，但整体配准更不容易失败，也更不容易因为局部缺损而被非刚性形变掩盖。</p>

<p>所以，模板生产链真正难的地方，并不只是骨架提取、删噪和标准化保存，还在于：必须根据对象的几何与拓扑特征，决定什么样的 <code class="language-plaintext highlighter-rouge">skeleton</code> 才适合作为后续配准的最小单元。从这个意义上说，模板生产并不是单纯的数据导出过程，而是已经在为后续检测的鲁棒性、漏检率和过杀率预先做结构设计。</p>

<h2 id="七为什么缩放因子和-blocksize-不是小参数而是模板生产的一部分">七、为什么缩放因子和 blocksize 不是小参数，而是模板生产的一部分</h2>

<p>还有两个看似不起眼、但其实很能说明模板生产链成熟度的点，就是缩放因子和 <code class="language-plaintext highlighter-rouge">blocksize</code> 参数。</p>

<p>文档里明确写到：缩放因子设置范围为 <code class="language-plaintext highlighter-rouge">0</code> 到 <code class="language-plaintext highlighter-rouge">1</code>，可以提高字符匹配速度；而 <code class="language-plaintext highlighter-rouge">blocksize</code> 二值化参数则可以调整二值化效果。更重要的是，修改缩放因子后，需要重新打开图像才能实现参数加载。</p>

<p>这说明这些参数并不只是界面上的附属选项，而是直接参与模板质量与后续检测效率之间的平衡。缩放因子影响的是模板进入检测时的速度，<code class="language-plaintext highlighter-rouge">blocksize</code> 影响的是模板生产时的结构可判读性。换句话说，这些参数虽然出现在模板创建工具里，但本质上仍然属于模板生产链的一部分。</p>

<h2 id="八所以这条模板创建链本身就是系统能力">八、所以，这条模板创建链本身就是系统能力</h2>

<p>把这些环节放在一起再看，就会发现：模板创建工具并不是一个“小工具”，而是在做一件很完整的事情。</p>

<p>它把：</p>

<ul>
  <li>原始图像读取；</li>
  <li>二值图确认；</li>
  <li><code class="language-plaintext highlighter-rouge">AOI</code> 区域标注；</li>
  <li>骨骼点可视化；</li>
  <li>中心点自动生成；</li>
  <li>噪点人工修正；</li>
  <li>标准化保存输出；</li>
  <li>配准单元组织决策；</li>
</ul>

<p>连成了一条模板生产链。</p>

<p>而没有这条链，后面的 <code class="language-plaintext highlighter-rouge">xml</code>、配准、测量、缺陷判定和现场实施，其实都缺少一个稳定起点。也正因为如此，我越来越觉得，这套系统里真正容易被低估的，不只是检测算法，而是这种模板如何被可靠生产出来的能力。</p>

<p>所以，如果要把这一篇压缩成一句话，我更愿意这样说：</p>

<blockquote>
  <p>模板不是检测系统之外的准备工作，而是整套系统最上游的生产机制；它不仅生产点集，还在生产后续配准所依赖的最小结构单元。</p>
</blockquote>

<h2 id="九这一篇的结尾模板不是死的下一篇继续讲局部几何修正">九、这一篇的结尾：模板不是死的，下一篇继续讲“局部几何修正”</h2>

<p>到这里，这条模板生产链其实已经比较清楚了：模板不是随手截出来的图块，而是经过验证、修正、标准化保存之后，才能进入后续检测链的结构对象。</p>

<p>但真实现场里，模板并不是一旦入库就永远不动。很多时候，局部区域仍然需要进一步屏蔽、擦除或几何修正，才能更稳定地适配具体产品和具体现场。</p>

<p>所以下一讲，我想继续沿着这里往下写：</p>

<blockquote>
  <p>为什么模板不是死的——<code class="language-plaintext highlighter-rouge">eraseSet</code> 与局部几何修正机制到底在解决什么问题。</p>
</blockquote>]]></content><author><name></name></author><summary type="html"><![CDATA[Why Templates Are Not Just Cropped Images: The Real Difficulty Lies in the Template Production Chain]]></summary></entry><entry><title type="html">第六讲：曲面 Pattern 检测系统为什么能在产线上稳定跑起来——节拍、并行与配置路由</title><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/01/why-this-curved-surface-pattern-inspection-system-can-run-stably-on-production-lines.html" rel="alternate" type="text/html" title="第六讲：曲面 Pattern 检测系统为什么能在产线上稳定跑起来——节拍、并行与配置路由" /><published>2026-04-01T12:00:00+08:00</published><updated>2026-04-01T12:00:00+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/01/why-this-curved-surface-pattern-inspection-system-can-run-stably-on-production-lines</id><content type="html" xml:base="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/04/01/why-this-curved-surface-pattern-inspection-system-can-run-stably-on-production-lines.html"><![CDATA[<h1 id="why-this-curved-surface-pattern-inspection-system-can-run-stably-on-production-lines-takt-time-parallelism-and-configuration-routing">Why This Curved-Surface Pattern Inspection System Can Run Stably on Production Lines: Takt Time, Parallelism, and Configuration Routing</h1>

<blockquote>
  <p><strong>English Abstract</strong><br />
This article explains why the system can run stably on real production lines after the detection logic has already been established. The key lies not only in whether defects can be measured, but in how configurations are routed, how tasks are organized, how takt-time constraints are satisfied, and how the implementation layer is made operational. In this sense, a real industrial system must not only detect correctly, but also know how to load the right configuration, execute within cycle time, and remain maintainable in deployment.</p>
</blockquote>

<p>在前一篇里，我主要讨论了模板落位之后，缺陷是如何沿着测量集被逐点测出来的。到那一步为止，这套系统已经回答了一个核心检测问题：它不是在整张图里盲目寻找异常，而是在模板已经进入实测图像、测量点已经落位之后，沿着结构关系去感知局部偏差。</p>

<p>但检测逻辑成立，还不等于系统就能在产线上稳定工作。</p>

<p>真正的工业系统，除了要“能检出来”，还必须回答另外几个同样现实的问题：它如何知道这次该加载哪一套配置？不同产品、不同国别、不同图档之间如何切换？检测如何满足产线节拍？为什么便宜的上位机也能把整套流程跑起来？以及，为什么实施层不是简单地点点工具，而是一层真正的系统能力？</p>

<p>到这里，问题已经不再只是“能不能检出来”，而变成了：这套检测能力能否被真正组织进一条持续运行的工业流程里。</p>

<p>如果说前面的模板、配准和测量层构成了这套系统的检测主干，那么这一篇要进入的，就是它的运行主干：配置如何被路由进来，任务如何被组织起来，节拍如何被保证，以及实施层为什么本身就是系统能力的一部分。</p>

<p>所以，这一篇真正要讲清楚的是：</p>

<ol>
  <li>为什么工业系统的关键不只是“能检出来”，而是“能稳定跑起来”；</li>
  <li>系统如何知道当前该加载哪一套产品 / 国别配置；</li>
  <li>为什么换产品时主要改的是配置，而不是核心代码；</li>
  <li>为什么 block 级组织、多线程和实施工具链对节拍与落地至关重要。</li>
</ol>

<h2 id="一为什么工业系统的关键不只是能检出来而是能稳定跑起来">一、为什么工业系统的关键不只是“能检出来”，而是“能稳定跑起来”</h2>

<p>很多算法在实验室里都能跑出结果，但真正到了产线，问题会立刻变得不一样。产线不会只关心“这次有没有检出来”，它更关心的是：下一次还稳不稳、换一批产品还行不行、节拍顶不顶得住、现场人员能不能接得住。</p>

<p>这就是为什么我一直觉得，工业算法真正难的，不只是把结果做出来，而是把它放进一条持续运行的生产流程里。</p>

<p>对于这套系统来说，这一点尤其明显。因为它面对的不是单一图像，而是一套要长期服务于不同产品、不同国别、不同图档、不同实施人员和不同节拍要求的检测链。也就是说，它必须同时满足几件事：</p>

<ul>
  <li>检出能力要成立；</li>
  <li>配置切换要顺畅；</li>
  <li>运行速度要满足节拍；</li>
  <li>系统行为要足够稳定；</li>
  <li>现场实施要可操作、可维护。</li>
</ul>

<p>如果这些条件只能偶尔满足一部分，那么这个系统就还算不上真正的工业系统。</p>

<p>从这个意义上说，“能跑起来”并不是一个比“能检出来”更低级的问题，恰恰相反，它往往更接近系统成熟度本身。</p>

<h2 id="二系统是怎么知道这次该加载哪一套配置的">二、系统是怎么知道“这次该加载哪一套配置”的</h2>

<p>如果核心 DLL 并没有针对每一个产品、每一个国别都写一套独立代码，那么系统首先就必须回答一个更上层的问题：当前这一轮检测，到底该进入哪一套配置链？</p>

<p>这件事并不是靠人工在代码里改路径来实现的，而是通过一层更轻量级的配置路由机制来完成。也就是说，在真正进入 ini、xml 和模板图之前，系统上方其实还有一层负责“分发配置”的入口。这个入口层最直观的体现，就是 <code class="language-plaintext highlighter-rouge">CarvingPatternType.xml</code>。</p>

<p>它本身并不存具体模板结构，也不直接参与缺陷检测逻辑，而是先根据某个 <code class="language-plaintext highlighter-rouge">pattern</code> 名称或方案 ID，把系统导向对应的配置入口，也就是某一份 <code class="language-plaintext highlighter-rouge">LaserCarvingPatt.ini</code>。下面这份实际配置已经能很清楚地说明这一点：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8" ?&gt;</span>
<span class="nt">&lt;entity&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"AM"</span> <span class="na">id=</span><span class="s">"635"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\AM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"BE"</span> <span class="na">id=</span><span class="s">"636"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\BE\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"LZ"</span> <span class="na">id=</span><span class="s">"642"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\BE\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"CH"</span> <span class="na">id=</span><span class="s">"637"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\CH\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"ID"</span> <span class="na">id=</span><span class="s">"639"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\ID\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"KH"</span> <span class="na">id=</span><span class="s">"641"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\KH\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"TA"</span> <span class="na">id=</span><span class="s">"644"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\TA\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"ZM"</span> <span class="na">id=</span><span class="s">"650"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\ZM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"HN"</span> <span class="na">id=</span><span class="s">"638"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\ZM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"VN"</span> <span class="na">id=</span><span class="s">"647"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\ZM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"RU"</span> <span class="na">id=</span><span class="s">"643"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\ZM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"TY"</span> <span class="na">id=</span><span class="s">"646"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\ZM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"ZA"</span> <span class="na">id=</span><span class="s">"648"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\ZM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"ZE"</span> <span class="na">id=</span><span class="s">"649"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\ZM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"TU"</span> <span class="na">id=</span><span class="s">"645"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\ZM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"ZP"</span> <span class="na">id=</span><span class="s">"651"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\ZM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
   <span class="nt">&lt;pattern</span> <span class="na">name=</span><span class="s">"J"</span> <span class="na">id=</span><span class="s">"640"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;path</span> <span class="na">name=</span><span class="s">"ini"</span> <span class="na">value=</span><span class="s">".\lab_config\AM\LaserCarvingPatt.ini"</span><span class="nt">/&gt;</span>
   <span class="nt">&lt;/pattern&gt;</span>
<span class="nt">&lt;/entity&gt;</span>
</code></pre></div></div>
<p>这份文件最值得注意的地方，不只是“系统能够根据代码找到对应 ini”，而是它已经明显体现出一种<strong>配置族式的复用关系</strong>。也就是说，这里并不是每个国别或方案代码都独占一套完全独立的配置，而是允许多个不同代码被路由到同一份 <code class="language-plaintext highlighter-rouge">LaserCarvingPatt.ini</code>。例如，<code class="language-plaintext highlighter-rouge">AM</code> 与 <code class="language-plaintext highlighter-rouge">J</code> 共享同一入口，<code class="language-plaintext highlighter-rouge">BE</code> 与 <code class="language-plaintext highlighter-rouge">LZ</code> 共享同一入口，而 <code class="language-plaintext highlighter-rouge">ZM</code> 则进一步承接了多个不同代码的配置入口。</p>

<p>这说明系统在更上层已经做了归类：不同代码未必对应完全不同的检测逻辑，它们可以先在路由层被归并到少量共享配置族中，再由对应 <code class="language-plaintext highlighter-rouge">ini</code> 继续挂接 <code class="language-plaintext highlighter-rouge">xml</code>、<code class="language-plaintext highlighter-rouge">bmp</code> 与参数文件。于是，对核心 DLL 来说，它并不需要知道“当前是哪个国别、该去找哪张图、该进哪份 xml”，它只需要沿着这条外部配置链去加载当前方案即可。</p>

<p>从系统架构的角度看，这一点非常重要。因为它说明这套系统的复用能力，不只是建立在 <code class="language-plaintext highlighter-rouge">xml + ini + bmp</code> 这些外部描述层之上，在它们之上其实还有一层更轻量级的<strong>配置路由机制</strong>。也正因为有了这一层，系统在面对不同产品、不同国别和不同图档时，才不需要从 DLL 开始重新改动，而是可以先通过路由层进入正确的配置链，再由后续的参数、模板和图档去承接具体差异。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure6_configuration_routing_chain.png" alt="Configuration routing chain above the core DLL" /></p>

<p><strong>图 1：配置路由链条图。</strong><br />
系统并不是直接进入某个固定 <code class="language-plaintext highlighter-rouge">ini</code> 或 <code class="language-plaintext highlighter-rouge">xml</code>，而是先通过顶层路由层选择当前方案所属的配置族，再进入后续的 <code class="language-plaintext highlighter-rouge">ini</code>、模板与参数链。</p>

<h2 id="三为什么换产品时主要改的是配置而不是核心代码">三、为什么换产品时主要改的是配置，而不是核心代码</h2>

<p>如果只从外部看，很多人会以为系统之所以能复用，主要靠的是“代码写得比较通用”。但真正做过的人会知道，事情并不是这样。</p>

<p>这套系统真正的复用能力，来自于设计阶段就把变化压缩进了外部描述层。也就是说，真正会随着产品、国别和图档变化而变化的东西，并没有被埋进 DLL 里，而是被拆到更外层的配置链条中去承接：</p>

<ul>
  <li>顶层路由决定进入哪一套方案；</li>
  <li><code class="language-plaintext highlighter-rouge">ini</code> 决定当前方案的参数、检测类型和运行方式；</li>
  <li><code class="language-plaintext highlighter-rouge">xml</code> 决定模板、几何锚点、block 组织和容错规则；</li>
  <li><code class="language-plaintext highlighter-rouge">bmp</code> 决定图档入口和模板底图。</li>
</ul>

<p>于是，对核心 DLL 来说，变化并不是“每来一个新产品就加一套新逻辑”，而是“按新的外部描述重新组织同一套检测能力”。</p>

<p>这件事真正厉害的地方，不只是把路径写到配置文件里，而是把“变化本身”做了<strong>层级化拆分</strong>。最上层先回答“该进哪一套配置族”，中间层再回答“这套方案的参数和运行方式是什么”，更下层才回答“模板、几何和图档长什么样”。这样一来，系统面对新产品、新国别或新图档时，并不需要一次性从底层代码开始重写，而是可以优先在外部配置链中完成切换。</p>

<p>这也解释了为什么换产品时主要改的是配置，而不是核心代码。因为对一个真正成熟的工业系统来说，最理想的状态并不是“代码足够万能”，而是“代码尽量稳定，变化被成功约束到外部描述层”。只有这样，系统才可能长期活下来；否则，一旦每次换产品都要改 DLL、重新编译、重新验证，系统的维护成本很快就会失控。</p>

<p>所以，换产品时主要改配置而不是改代码，并不是一个“省事一点”的工程细节，而恰恰是这套系统成熟度的重要体现。它说明这套系统已经不只是能跑，而是已经形成了一条相对清楚的外部配置体系：由路由层选择方案，由参数层控制行为，由模板层承接结构差异，由图档层提供入口对象，而核心 DLL 尽量保持稳定。</p>

<h2 id="四为什么-block-级组织多线程和任务拆分对节拍至关重要">四、为什么 block 级组织、多线程和任务拆分对节拍至关重要</h2>

<p>工业系统不仅要“对”，还要“快”。而这套系统能够在真实现场满足节拍，一个非常关键的原因，就是它从一开始就不是按“整张图串行慢慢跑”的思路组织的。</p>

<p>前面几讲已经讲过，系统在检测逻辑上本来就是按 <code class="language-plaintext highlighter-rouge">block</code> 来组织的。每个 <code class="language-plaintext highlighter-rouge">block</code> 都有自己的模板结构、自己的注册集和测量集，也有各自的局部检测任务。正因为如此，这种结构天然适合进一步被拆成并行任务。</p>

<p>但这里真正有分量的，并不只是“用了多线程”这件事，而是它在数据组织上也同时做了适合并行执行的设计。系统并没有把所有检测任务的中间结果都塞进一个共享容器里，再靠锁去协调谁先写、谁后写；相反，它从一开始就把 <code class="language-plaintext highlighter-rouge">block</code> 编号与对应的数据槽位静态绑定起来。每个 <code class="language-plaintext highlighter-rouge">block</code> 对应独立的测量数据、模板数据、结果数据和完成标志，线程只需要按 <code class="language-plaintext highlighter-rouge">block</code> 编号读写各自的固定内存槽位即可。</p>

<p>这种静态定址映射并不是抽象说法，在实现层上，它就是类似下面这样的代码结构：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">inline</span> <span class="kt">void</span> <span class="nf">getmeasuredata</span><span class="p">(</span><span class="n">RegistModel</span> <span class="o">*&amp;</span><span class="n">rm</span><span class="p">,</span> <span class="kt">int</span> <span class="n">i</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">switch</span> <span class="p">(</span><span class="n">i</span><span class="p">)</span>
    <span class="p">{</span>
    <span class="k">case</span> <span class="mi">1</span><span class="p">:</span>  <span class="n">rm</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">measuredData1</span><span class="p">;</span>  <span class="k">break</span><span class="p">;</span>
    <span class="k">case</span> <span class="mi">2</span><span class="p">:</span>  <span class="n">rm</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">measuredData2</span><span class="p">;</span>  <span class="k">break</span><span class="p">;</span>
    <span class="k">case</span> <span class="mi">3</span><span class="p">:</span>  <span class="n">rm</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">measuredData3</span><span class="p">;</span>  <span class="k">break</span><span class="p">;</span>
    <span class="c1">// ...</span>
    <span class="k">case</span> <span class="mi">80</span><span class="p">:</span> <span class="n">rm</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">measuredData80</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kr">inline</span> <span class="kt">void</span> <span class="n">assignmeasureddata</span><span class="p">(</span><span class="n">RegistModel</span> <span class="o">&amp;</span><span class="n">rm</span><span class="p">,</span> <span class="kt">int</span> <span class="n">i</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">switch</span> <span class="p">(</span><span class="n">i</span><span class="p">)</span>
    <span class="p">{</span>
    <span class="k">case</span> <span class="mi">1</span><span class="p">:</span>  <span class="n">measuredData1</span>  <span class="o">=</span> <span class="n">rm</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
    <span class="k">case</span> <span class="mi">2</span><span class="p">:</span>  <span class="n">measuredData2</span>  <span class="o">=</span> <span class="n">rm</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
    <span class="k">case</span> <span class="mi">3</span><span class="p">:</span>  <span class="n">measuredData3</span>  <span class="o">=</span> <span class="n">rm</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
    <span class="c1">// ...</span>
    <span class="k">case</span> <span class="mi">80</span><span class="p">:</span> <span class="n">measuredData80</span> <span class="o">=</span> <span class="n">rm</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这类代码表面上看只是一个很长的 <code class="language-plaintext highlighter-rouge">switch-case</code>，但从运行层角度看，它做的是一件非常工程化的事情：把每个 <code class="language-plaintext highlighter-rouge">block</code> 与其对应的测量数据、结果数据和完成标志做静态定址映射。一旦这种映射建立起来，线程在主检测路径上就不需要围绕共享结果容器竞争写入权，也不需要为了中间结果回收和合并频繁进入临界区。换句话说，这里看似朴素的实现，背后对应的其实是一种<strong>以空间换同步、以预分配换锁竞争</strong>的并行组织方式。</p>

<p>与此同时，原始灰度图只保留一份，各线程按需获取对应 <code class="language-plaintext highlighter-rouge">block</code> 的 <code class="language-plaintext highlighter-rouge">ROI</code> 视图，而不是为每个任务重复复制整张图像。这样做有两个直接好处：一是节省了内存复制成本，二是让并行任务的启动与执行开销维持在较低水平。也就是说，这套系统的快，不只是来自检测函数本身，更来自它在更上层就已经把任务拆分方式和数据流组织对了。</p>

<p>从这个意义上说，这套系统的多线程并不是后期为了提速临时加上的优化，而更接近它从结构设计上自然长出来的运行能力。主检测框架在绝大多数 <code class="language-plaintext highlighter-rouge">block</code> 上实现的是<strong>零同步开销的并行执行</strong>：各线程按 <code class="language-plaintext highlighter-rouge">block</code> 独立处理、独立落位、独立写回，不需要显式共享写入协调。也正因为如此，在便宜的上位机上，整套检测链仍然有机会压进产线可接受的节拍内。前面提到的那个结果之所以有分量——一个四十多个字符的镭雕检测只需要约 <code class="language-plaintext highlighter-rouge">400ms</code>——真正起作用的，并不只是某个局部函数写得快，而是系统在更上层就已经完成了正确的任务组织。</p>

<p>当然，这并不意味着所有 <code class="language-plaintext highlighter-rouge">block</code> 在任何情况下都完全等价。对少数存在空间重叠或局部写入耦合的 <code class="language-plaintext highlighter-rouge">block</code>，线程执行顺序会带来工程上的次级影响。这里要强调的是，这种影响并不是主检测框架的常态，也不是系统成立所依赖的理论前提，而更接近于现场实现层中的特殊现象：在某些局部重叠情形下，不同线程先后作用于局部区域，可能会带来略有差异的现场效果。后来现场实施中甚至总结出了一些顺序经验，这恰恰说明系统进入真实产线后，还会与具体产品布局和局部几何关系进一步发生作用。</p>

<p>所以，如果要把这一节压缩成一句话，我更愿意这样说：</p>

<blockquote>
  <p>这套系统之所以能在真实现场满足节拍，不只是因为检测逻辑成立，更因为它在 <code class="language-plaintext highlighter-rouge">block</code> 级任务拆分、固定槽位内存映射和零同步开销并行执行上，已经提前长成了一套适合工业运行的结构。</p>
</blockquote>

<h2 id="五为什么真正高性价比的提速点在上位机而不是算法核">五、为什么真正高性价比的提速点在上位机，而不是算法核</h2>

<p>很多人在看到产线节拍压力时，第一反应往往是继续要求算法提速。但对这套系统来说，这种直觉并不总是对的。原因并不是算法不重要，而是当算法核本身已经做了并行化、线程间临界区已经压到极小之后，继续从算法内部硬挤效率，边际收益会越来越低；这时，真正更高性价比的提速点，往往已经转移到了上位机架构。</p>

<p>这里真正需要分清的是：算法速度和系统吞吐并不是同一个问题。算法核本身已经采用 <code class="language-plaintext highlighter-rouge">C++</code> 并行实现，线程间同步开销很小，继续从算法内部提速的空间其实有限；而上位机侧却仍然把 <code class="language-plaintext highlighter-rouge">PLC</code> 通信、图像采集、算法处理和结果显示组织成一条串行链，后一步必须等待前一步结束。结果就是，系统虽然在算法核内部已经很快，但在整条运行链上仍然会不断出现等待，从而把原本可以利用的生产间隙浪费掉。</p>

<p>从系统角度看，这就意味着：节拍上限并不只由算法核决定，而是由整套任务调度架构共同决定。只要平台侧仍然把采图、算法、显示和通信锁成一条串行链，那么算法即使再快，整体吞吐也仍然会被系统架构吞掉。</p>

<p>也正因为如此，更合理的方向不是继续要求算法单点提速，而是修改上位机架构，把原本串行执行的几段流程解耦开来。对此，我当时提出的方案是“双缓存三线程”结构：一个图像队列、一个结果队列，相机线程负责把采集到的图像送入图像队列，算法线程从图像队列中取图处理，再把结果写入结果队列，通信线程则从结果队列取回结果并返回控制单元与显示器。这样一来，采图、计算和回传不再被锁成一条单链，而是可以在生产间隙中并行推进。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure6_twin_cache_architecture.png" alt="双缓存三线程结构示意图" /></p>

<p><strong>图 3：</strong> 双缓存三线程结构示意图。相机线程先将图像送入图像队列，算法线程异步完成检测，再把结果写入结果队列，通信线程从结果队列中回传结果并负责显示。它真正解决的不是某个局部函数是否更快，而是如何把原本串行的系统吞吐重新组织起来。</p>

<p>如果把这一层压缩成一句话，我更愿意这样说：</p>

<blockquote>
  <p>当算法核已经接近自身效率上限时，真正决定系统节拍的，往往不再是算法本身，而是上位机是否愿意围绕并行检测去重写运行架构。</p>
</blockquote>

<h2 id="六为什么实施层不是点点工具就行而是一层真正的系统能力">六、为什么实施层不是“点点工具就行”，而是一层真正的系统能力</h2>

<p>很多外人一看到实施工程师在现场框框 <code class="language-plaintext highlighter-rouge">AOI</code>、调调参数、删删噪点，就容易误以为这部分工作没有多少技术含量，仿佛只是把算法系统“点开”而已。但只要真正看过模板创建工具的使用方式，就会知道，这种理解其实很浅。工具表面上提供的是框选、可视化、保存、删点、调缩放因子和二值化参数等操作，但这些操作背后对应的，并不是机械点击，而是一层<strong>半自动的人机协同建模过程</strong>。模板创建工具会把原始图像处理成便于判读的二值图，支持 <code class="language-plaintext highlighter-rouge">AOI</code> 标注、骨骼点可视化、中心点自动生成与标准化保存，也允许对骨架中的干扰点进行人工删除，以提高模板质量。</p>

<p>这说明实施层真正面对的问题，并不是“会不会用界面”，而是：能不能判断一个模板是否可信，能不能分辨哪些骨骼点属于结构本体、哪些只是噪点，能不能理解缩放因子和二值化参数会怎样影响后续匹配速度与模板质量。换句话说，实施层并不是在替算法做一些琐碎劳动，而是在协助系统完成模板生产、模板验证和模板标准化入库。</p>

<p>从系统角度看，这一点非常关键。因为前面几讲讲到的 <code class="language-plaintext highlighter-rouge">xml</code> 模板、注册集、测量集、<code class="language-plaintext highlighter-rouge">block</code> 组织和局部缺陷测量，都不是凭空存在的，它们最终都要落到具体模板对象上。如果实施层无法稳定地生产出可信模板，那么前面的结构化检测主线就会从源头开始失真。也正因为如此，实施工程师培养才会难：愿意做的人未必真正理解骨架、中心点、局部结构和参数之间的关系；而理解这些关系的人，又未必愿意长期做贴近现场、反复打磨模板与配置的工作。</p>

<p>所以，这里真正值得强调的，不是“实施很麻烦”，而是：</p>

<blockquote>
  <p><strong>实施本身就是系统能力的一部分。</strong></p>
</blockquote>

<p>没有这一层，前面的模板组织、分层配准和局部测量机制，就很难被可靠地迁移到新产品和新产线上；而只有当模板创建、模板验证、参数承接和现场导入形成了稳定流程，这套系统才算真正具备了<strong>可落地、可复制、可继承</strong>的工业形态。</p>

<p>从这个意义上说，实施层并不是检测系统之外的一圈“人工补丁”，而是把检测层、配置层和现场导入层真正接起来的那一层。也正因为如此，这套系统能够跨产品、跨国别、跨产线持续工作，靠的从来不只是核心 <code class="language-plaintext highlighter-rouge">DLL</code> 或某个局部算法，而是连模板工具、实施流程和现场经验一起，被组织进了同一套系统能力之中。</p>

<h2 id="七为什么这套系统能在真实现场长期稳定工作">七、为什么这套系统能在真实现场长期稳定工作</h2>

<p>如果把前面的内容合起来，其实就能看清一件事：这套系统之所以能在真实现场长期稳定运行，并不是因为某一个模块特别强，而是因为检测层、配置层、调度层和实施层被组织成了同一套系统。</p>

<ul>
  <li>检测层回答“能不能检出来”；</li>
  <li>配置层回答“该加载哪一套方案”；</li>
  <li>调度层回答“如何在节拍内把任务跑完”；</li>
  <li>实施层回答“模板和配置如何在现场被可靠生产和继承”。</li>
</ul>

<p>只有这几层同时成立，系统才会从“一个能跑通的算法”变成“一个能长期工作的工业对象”。</p>

<p>这也是为什么我始终不太认同把这类项目理解成“某个检测算法做得不错”这么简单。因为真正决定系统寿命的，往往不是某个局部指标，而是这些不同层级之间能不能咬合起来：配置路由是否清楚，模板对象是否稳定，检测主线是否可复用，并行组织是否能压进节拍，实施层是否能持续接住。缺了其中任何一层，系统都可能在真实现场很快失去稳定性。</p>

<p>从这个意义上说，这套系统真正可贵的地方，不只是它当年在某个项目里做成了，而是它已经形成了一条相对完整的工业技术链：从配置路由，到模板组织，到配准测量，到并行运行，再到现场实施和维护，都被纳入了同一条方法主线之中。也正因为如此，它的稳定运行不是偶然结果，而是一种系统结构自然导出的结果。</p>

<p>如果一定要把这一层再压缩成一句话，我更愿意这样说：</p>

<blockquote>
  <p>这套系统之所以能长期稳定工作，不是因为某个模块“特别强”，而是因为检测层、配置层、调度层和实施层被真正组织成了一套相互支撑的系统。</p>
</blockquote>

<h2 id="八这一篇的结尾从它为什么能跑起来回到它为什么值得被重述">八、这一篇的结尾：从“它为什么能跑起来”回到“它为什么值得被重述”</h2>

<p>写到这里其实可以看清，这套系统之所以值得我反复写，并不仅仅是因为它能检出某几类缺陷，或者在某些实验里效果不错，而是因为它在当时已经具备了比较完整的工业系统形态。</p>

<p>它不是一个悬空的算法模块，<br />
也不是一个只在实验室里成立的 demo，<br />
而是一套能够知道该加载哪套配置、如何在节拍里运行、如何被实施层接住并在现场持续工作的系统。</p>

<p>所以，如果要为这一篇压缩成一句话，我更愿意这样说：</p>

<blockquote>
  <p><strong>真正的工业系统，不只是能检出来，还必须知道该加载哪套配置、怎样在节拍内跑完，以及如何让实施层接得住。</strong></p>
</blockquote>

<p>到这里，这个系列其实已经走完了它最核心的前半段：问题是什么，系统怎样组织，模板如何建立，配准如何发挥作用，缺陷如何被测出来，以及它为什么能在真实产线上跑起来。</p>

<p>如果后面继续写，我更愿意进一步写两类内容。<br />
一类是回到更高层，讨论为什么这类项目不应该被草率地理解成“普通字符检测”，以及为什么真正做成过的系统，值得被重新表述成一个可传递的技术对象。<br />
另一类则是回到我自己的研究线，讨论这类工业系统如何反过来影响了我后来在点集配准、结构化表示和几何算法上的思考。</p>

<p>也就是说，到这一篇为止，这个系列已经不再只是对某个旧项目的回忆，而开始逐渐显露出它更重要的意义：<strong>它不是为了证明我做过什么，而是为了把一个真正成立过的工业算法系统，重新还原成一个能够被别人理解、检验和继承的技术对象。</strong></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Why This Curved-Surface Pattern Inspection System Can Run Stably on Production Lines: Takt Time, Parallelism, and Configuration Routing]]></summary></entry><entry><title type="html">第五讲：曲面 Pattern 缺陷是怎么被测出来的——从模板落位到缺陷判定</title><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/31/how-defects-are-measured-after-template-alignment.html" rel="alternate" type="text/html" title="第五讲：曲面 Pattern 缺陷是怎么被测出来的——从模板落位到缺陷判定" /><published>2026-03-31T12:00:00+08:00</published><updated>2026-03-31T12:00:00+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/31/how-defects-are-measured-after-template-alignment</id><content type="html" xml:base="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/31/how-defects-are-measured-after-template-alignment.html"><![CDATA[<h1 id="how-curved-surface-pattern-defects-are-actually-measured-from-template-alignment-to-defect-decision">How Curved-Surface Pattern Defects Are Actually Measured: From Template Alignment to Defect Decision</h1>

<blockquote>
  <p><strong>English Abstract</strong><br />
This article explains how defect detection is actually carried out after template alignment. Once the measure set has been brought into the measured image, the system does not scan the whole bitmap blindly; instead, it performs point-wise local measurement along aligned structural points, records point-level abnormal responses, and then aggregates them into final defect regions and defect types. From this perspective, defects are not directly “found” in the image, but measured out of an already established geometric relation.</p>
</blockquote>

<p>在前一篇里，我主要讨论了这套系统的核心几何机制：为什么不能跳过配准直接谈检测，为什么系统采用了两层配准，以及为什么注册集与测量集必须分工。到那一步为止，模板已经不再只是静态文件中的结构对象，而是已经通过配准真正进入了实测图像。也就是说，系统已经回答了“模板落在哪里”这个问题。</p>

<p>但这还不是检测结果。</p>

<p>真正的缺陷判定，并不是配准结束时自动给出来的。配准只是把模板和实测对象之间的几何关系建立起来，而后续检测真正依赖的，是已经落位的测量集。只有当测量点进入了正确位置，系统才可能沿着这些点展开局部观察，进而把局部结构偏差、灰度异常、宽度变化、残余区域等信息逐步转化成可判定的缺陷类型。</p>

<p>所以，这一篇真正要讲清楚的是：</p>

<ol>
  <li>为什么缺陷检测不能脱离测量集单独谈；</li>
  <li>系统到底在“测”什么；</li>
  <li>为什么它不是沿整张图漫扫，而是沿测量点逐点展开；</li>
  <li>不同缺陷类型为什么能够被统一纳入同一套测量框架。</li>
</ol>

<h2 id="一为什么缺陷检测不能脱离测量集单独谈">一、为什么缺陷检测不能脱离测量集单独谈</h2>

<p>如果把问题理解得很浅，最容易形成的印象就是：检测系统面对一张图像，最终只需要回答“有没有缺陷”。但对这套系统来说，这样的理解仍然太粗。因为它真正面对的，并不是一张尚未组织的原始图像，而是一个已经经过模板组织、两层配准和局部落位之后的结构化检测对象。</p>

<p>也就是说，系统并不是先对整张图做全图异常扫描，再回头去解释这些异常到底落在什么字符、什么图样、什么局部结构上。它的路径正好相反：先有模板，先有几何关系，先有测量点，然后才沿着这些测量点去判断局部是否异常。</p>

<p>从这个意义上说，测量集并不是某种附属数据，而更接近后续缺陷检测的工作坐标系。只有当测量点已经进入正确位置，系统才知道“应该在什么地方看”“应该沿着什么结构看”“局部偏差应该如何解释”。一旦脱离了这一层，很多原本可测的细微缺陷，就会重新退化成位图层面的模糊差异。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure5_measurement_pipeline.png" alt="配准完成之后，系统并不是直接判定缺陷，而是沿已经落位的测量集逐点测量，再将点级异常聚并为最终缺陷结果。" /></p>

<p><em>图 1：测量层总流程图。模板与测量集落位之后，系统沿测量点逐点展开局部测量，并将点级异常进一步组织为最终缺陷输出。</em></p>

<h2 id="二系统到底在测什么">二、系统到底在“测”什么</h2>

<p>当我们说“沿测量集做检测”时，真正发生的并不是某种抽象的“看一看周围是否正常”，而是一系列具体的局部测量。换句话说，这套系统不是在“猜缺陷”，而是在测一组明确的局部量。</p>

<p>从工程上看，这些局部量大致可以分成三类。</p>

<p>第一类，是局部灰度与对比关系。<br />
这类测量主要对应色异常、局部发白发黑、填充状态异常等问题。系统并不满足于看某个点是亮还是暗，而是要看该点邻域的灰度区间、对比关系以及局部投影结构是否与应有 pattern 保持一致。</p>

<p>第二类，是局部宽度、断裂与连通状态。<br />
这类测量主要对应镭断、局部缺口、细微中断等问题。系统沿着测量点展开局部观察，并不只是为了“看到某个点”，而是为了进一步感知与该点相关的结构连续性是否还成立。</p>

<p>第三类，是局部面积、残余与异常聚集。<br />
这类测量更接近多镭、残胶、局部残余区域或异常填充等问题。它们往往不表现为单点灰度异常，而表现为某种局部区域的宽度扩张、面积残留或异常堆积。</p>

<p>因此，这一篇真正应该让读者看清的是：这套系统里的“测量”从来都不是泛泛的，它始终对应某类明确的局部几何量或灰度量，而后续缺陷判定也正是建立在这些量之上。</p>

<h2 id="三为什么系统不是沿整张图测而是沿测量点逐点展开">三、为什么系统不是沿整张图测，而是沿测量点逐点展开</h2>

<p>这套系统最有特色的地方之一，就是测量不是对整张图做盲目的密集搜索，而是沿着已经落位的测量集逐点展开。</p>

<p>这一点非常重要。因为一旦测量点已经通过配准进入了正确位置，系统就不再需要在整张图里到处“猜”哪里值得看，而是可以直接把注意力集中在那些本来就应该存在结构的位置上。也就是说，后续测量不是在全图漫扫，而是在一个已经被模板和配准缩小过、组织过的局部空间里进行。</p>

<p>这样做的好处有两个。</p>

<p>第一，它让系统对细微缺陷更敏感。<br />
因为测量不是在无上下文的位图区域里比较亮暗，而是在“本来应该有结构”的位置上比较结构偏差。这样一来，非常小的缺口、局部中断、极细的异常区域，都会更容易被稳定地显现出来。</p>

<p>第二，它让系统对无关噪声更不敏感。<br />
因为系统并没有把整张图都当成等价检测对象，而是把注意力限制在已经落位的局部结构上。很多来自背景、局部污染、边缘偶然波动的干扰，根本不会轻易进入最终判定链条。</p>

<p>所以，这套系统的检测并不是“看整张图哪里不对”，而更接近于：</p>

<blockquote>
  <p><strong>沿着一组已经落位的结构点，逐点展开局部测量，再把这些局部测量组织成最终缺陷。</strong></p>
</blockquote>

<h2 id="四不同缺陷类型为什么能够被统一纳入同一套测量框架">四、不同缺陷类型为什么能够被统一纳入同一套测量框架</h2>

<p>从表面上看，这套系统里最终面对的缺陷类型很多：镭断、色异常、多镭、局部失配、码数量异常等等。它们的视觉表现也并不一样，似乎应该对应不同的算法模块。</p>

<p>但如果从更深一层的机制看，这些缺陷并不是若干套彼此孤立的逻辑，而是被统一纳入了同一条主线之中：</p>

<ol>
  <li>测量集已经落位；</li>
  <li>局部邻域被展开；</li>
  <li>某种局部几何量或灰度量被测出；</li>
  <li>点级异常被记录；</li>
  <li>再通过邻域关系聚并为缺陷区域或缺陷类型。</li>
</ol>

<p>也就是说，不同缺陷类型虽然“长得不一样”，但它们在系统里进入检测链条的方式其实非常相似：都先表现为某些测量点上的局部异常，再逐步从点级异常上升为可输出的缺陷结果。</p>

<p>这一点很关键。因为它说明这套系统并不是把每一类缺陷都单独写成一套完全不同的流程，而是在一个统一的测量框架上，允许不同类型的局部量去触发不同的异常解释。正因为如此，系统才没有退化成“缺陷类型越多，逻辑越碎”的规则堆叠，而是仍然保持了一条比较清楚的技术主线。</p>

<h2 id="五从点级异常到最终缺陷系统是怎么聚并和判定的">五、从点级异常到最终缺陷：系统是怎么聚并和判定的</h2>

<p>测量点上的局部异常并不会立刻等价于最终缺陷。因为在真实现场里，孤立的异常点可能来自局部波动、偶然噪声或者边界不稳定。如果系统只要某一个点异常就直接判 NG，那么误报会非常高。</p>

<p>因此，这套系统真正的判定过程并不是“点异常 = 缺陷”，而是更接近下面这条链：</p>

<ul>
  <li>某些测量点先表现出局部异常；</li>
  <li>系统再观察这些异常点在邻域内是否形成足够稳定、足够密集的异常集合；</li>
  <li>当这种集合达到一定结构条件后，才进一步形成缺陷区域、缺陷类型和最终输出。</li>
</ul>

<p>这一步的意义非常大。因为它解释了为什么系统既能抓住很细微的缺陷，又不会因为少数孤立噪声点就轻易乱报。换句话说，点级测量负责“发现异常苗头”，而后续聚并负责“把异常苗头解释成真正可输出的缺陷”。</p>

<p>也正是在这一层，系统才真正完成了从“局部测量”到“工业输出”的转换。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure5_point_to_defect.png" alt="系统先在测量点层面记录局部异常，再通过邻域聚并把这些点级异常转化为可输出的缺陷区域与缺陷类型。" /></p>

<p><em>图 2：从点级异常到最终缺陷。系统不是看到单个异常点就立刻判 NG，而是通过邻域聚并，将局部异常点组织成最终可输出的缺陷区域。</em></p>

<h2 id="六为什么这套系统能测到非常细微的缺陷">六、为什么这套系统能测到非常细微的缺陷</h2>

<p>如果只看最终结果，很多人会把系统能抓到细微缺陷归功于某个特别强的分类器、某个很高深的特征，或者某种特别神秘的阈值策略。但对这套系统来说，更根本的原因并不在那里。</p>

<p>它之所以能测到非常细微的缺陷，首先是因为模板结构已经通过配准真正落入了实测图像；其次是因为后续测量并不是在整张图里盲扫，而是沿着这些已经落位的结构点逐点展开。也就是说，系统始终是在“应该有结构的地方”测偏差，而不是在无差别地扫整张图。</p>

<p>为了让这一点更直观，不妨把“模板已经真正落位”和“细微缺陷如何显现”放在同一张图里看。左侧给出的是中轴线配准后的现场效果图：绿色线条并不是人工标注，而是由现场软件依据模板结构在实测图像上自动生成的骨架，也就是 xml 中 <code class="language-plaintext highlighter-rouge">skeleton</code> 在图像中的实际落位结果。它的意义不只是“看起来对齐了”，而是说明后续测量所依赖的结构关系已经真正进入了实测对象。</p>

<p>右侧则给出几类真实产线样本中的局部缺陷效果图，包括镭断和多镭等典型情形。图中保留的是与缺陷相关的局部区域，红色方框标示系统检测到的异常位置。这样并排来看，这套系统之所以能测到非常细微的缺陷，原因就会更清楚：它不是先在整张图里盲目寻找异常，而是在模板已经落位、骨架关系已经建立之后，沿着这些结构去感知局部偏差。正因为如此，一些在位图层面容易淹没在噪声、亮度波动或局部不均匀中的细微缺陷，才会变成可测的结构异常。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/inspection_overview_registration_and_defects.png" alt="左侧为配准后的中轴线覆盖效果，右侧为真实产线样本中的局部缺陷检测结果。" /></p>

<p><em>图 3：左侧为配准后的中轴线覆盖效果，右侧为真实产线样本中的局部缺陷检测结果。左：绿色中轴线为配准后叠加到实测图像中的结构模板；右：红色方框标示系统检测到的局部缺陷。</em></p>

<p>正因为如此，它才能够对一些极其细小但真实的结构异常保持敏感。例如，某个局部点缺失、一段笔画中间断掉一个很小的方块，或者某个局部邻域里出现轻微但真实的异常膨胀。如果只在原始位图层面去看，这些现象很容易与噪声、局部亮度波动或成像不均匀混在一起；但一旦放到已经落位的骨架和测量关系上，它们就会变成更可解释、也更可测的局部结构偏差。</p>

<h2 id="七为什么这套测量层本质上仍然是结构化测量而不是简单规则堆砌">七、为什么这套测量层本质上仍然是结构化测量，而不是简单规则堆砌</h2>

<p>从表面上看，这套系统当然有不少参数、阈值和局部判别逻辑。于是很容易有人觉得，它最终不过是一套调参很多的规则系统。这个印象并不奇怪，因为如果只看配置文件里的参数名，而不去看它们依附在哪一层机制上，确实会觉得“规则很多”。</p>

<p>但问题的关键不在于“有没有参数”，而在于：<strong>这些参数是不是悬空存在的。</strong></p>

<p>为了说明这一点，不妨先看一小段经过简化后的参数组织方式：</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[Path]</span>
<span class="py">XmlPath</span><span class="p">=</span><span class="s">.</span><span class="se">\l</span><span class="s">ab_config</span><span class="se">\A</span><span class="s">M</span><span class="se">\l</span><span class="s">aserCarving_TCharge.xml</span>

<span class="nn">[Param]</span>
<span class="py">ThresholdBlockSize</span><span class="p">=</span><span class="s">...</span>
<span class="py">contAreaMin</span><span class="p">=</span><span class="s">...</span>
<span class="py">contAreaMax</span><span class="p">=</span><span class="s">...</span>
<span class="py">NeighbourSize</span><span class="p">=</span><span class="s">...</span>
<span class="py">NgPointNumThres</span><span class="p">=</span><span class="s">...</span>
<span class="py">CharPointNumThres</span><span class="p">=</span><span class="s">...</span>
<span class="py">DiffThres</span><span class="p">=</span><span class="s">...</span>
<span class="py">MatchingThres</span><span class="p">=</span><span class="s">...</span>
<span class="py">IterCount</span><span class="p">=</span><span class="s">...</span>
<span class="py">MaxPointDist</span><span class="p">=</span><span class="s">...</span>
<span class="py">DistYThres</span><span class="p">=</span><span class="s">...</span>

<span class="nn">[Defect]</span>
<span class="py">detectType</span><span class="p">=</span><span class="s">11</span>
<span class="py">LightThres</span><span class="p">=</span><span class="s">...</span>
<span class="py">DarkThres</span><span class="p">=</span><span class="s">...</span>
<span class="py">GlobalColorThres</span><span class="p">=</span><span class="s">...</span>
<span class="py">GumThres</span><span class="p">=</span><span class="s">...</span>
<span class="py">DiaThres</span><span class="p">=</span><span class="s">...</span>
<span class="py">ResidualAreaThres</span><span class="p">=</span><span class="s">...</span>
<span class="py">ZeroDiaRate</span><span class="p">=</span><span class="s">...</span>
<span class="py">detNormThres</span><span class="p">=</span><span class="s">...</span>
</code></pre></div></div>
<p>如果只看这些名字，确实会觉得参数不少。但稍微往下看一层，就会发现，它们并不是杂乱堆在一起的，而是分属不同层级。</p>

<p>第一层参数，服务于<strong>模板入口与对象形成</strong>。<br />
最上面的 <code class="language-plaintext highlighter-rouge">XmlPath</code> 并不是一个普通路径项，它实际上把当前这份 ini 直接连到了对应国别或产品面的 xml 模板上。也就是说，这套系统一开始就不是“先有一堆阈值，再想办法去适配对象”，而是先由 xml 给出字符骨架、全局几何锚点、block 组织和模板入口，ini 再去调节这一整条检测链如何工作。</p>

<p>第二层参数，服务于<strong>前端提取与 block 形成</strong>。<br />
例如 <code class="language-plaintext highlighter-rouge">ThresholdBlockSize</code>、<code class="language-plaintext highlighter-rouge">contAreaMin</code>、<code class="language-plaintext highlighter-rouge">contAreaMax</code>、<code class="language-plaintext highlighter-rouge">SquareKernelSize</code>、<code class="language-plaintext highlighter-rouge">blurringKernelSize</code>、<code class="language-plaintext highlighter-rouge">KernelSizeForCont</code>，控制的是局部二值化、轮廓筛选、膨胀与降噪等前处理过程。它们回答的不是“最后判什么 defect”，而是“哪些区域有资格进入后续检测链”。</p>

<p>这里面 <code class="language-plaintext highlighter-rouge">CharPointNumThres</code> 又尤其重要。它并不是一个随意的点数限制，而是字符区域的<strong>有效点阈值</strong>：只有当字符区域中的有效点数量达到这一阈值，系统才认为该字符区域正常，并把它计入对应 block。也就是说，这个参数控制的不是最后怎么判缺陷，而是某个局部结构是否足够成立、足够完整，能够进入后续几何关系之中。</p>

<p>第三层参数，服务于<strong>配准与结构落位</strong>。<br />
例如 <code class="language-plaintext highlighter-rouge">MatchingThres</code>、<code class="language-plaintext highlighter-rouge">IterCount</code>、<code class="language-plaintext highlighter-rouge">MaxPointDist</code>、<code class="language-plaintext highlighter-rouge">DistYThres</code>、<code class="language-plaintext highlighter-rouge">NeighbourSize</code> 等量，控制的是模板匹配是否成立、点集对应范围有多宽、迭代配准做多少轮，以及局部测量尺度取多大。这里特别值得注意的是：这些参数并不直接决定某个缺陷是否成立，而是首先决定模板、block 和测量点能否被稳定地带到合理位置。换句话说，它们服务的是“结构如何落位”，而不是“结果如何直接给出”。</p>

<p>第四层参数，直接对应<strong>局部测量对象本身</strong>。<br />
例如 <code class="language-plaintext highlighter-rouge">LightThres</code>、<code class="language-plaintext highlighter-rouge">DarkThres</code>、<code class="language-plaintext highlighter-rouge">GlobalColorThres</code>、<code class="language-plaintext highlighter-rouge">DiaThres</code>、<code class="language-plaintext highlighter-rouge">ResidualAreaThres</code>、<code class="language-plaintext highlighter-rouge">ZeroDiaRate</code>、<code class="language-plaintext highlighter-rouge">detNormThres</code>，分别对应局部灰度与对比关系、整体色异常、局部宽度或直径类量、残余区域以及整体失配程度等辅助量。也就是说，这些参数并不是拍脑袋写出来的，而是挂在“系统到底在测什么”这一层上的。</p>

<p>这里的 <code class="language-plaintext highlighter-rouge">detectType</code> 也不能被简单看成一个普通开关。它实际上决定了当前这份配置到底激活哪几类缺陷通道。例如 <code class="language-plaintext highlighter-rouge">detectType=11</code>，按位展开之后对应的是 <code class="language-plaintext highlighter-rouge">1 + 2 + 8</code>，也就是当前场景下主要启用了 <strong>镭断、色异常和多雕</strong> 这几类检测。这说明系统并不是在所有场景下把所有缺陷一股脑都打开，而是会针对具体产品面、具体图档和具体业务场景，有选择地组织当前真正需要的测量任务。</p>

<p>第五层参数，控制的是<strong>点级异常如何上升为最终缺陷</strong>。<br />
例如 <code class="language-plaintext highlighter-rouge">NgPointNumThres</code>，以及更完整配置中出现的 <code class="language-plaintext highlighter-rouge">BreakPointNumThres</code>、<code class="language-plaintext highlighter-rouge">ColorDiffPointNumThres</code>、<code class="language-plaintext highlighter-rouge">LargeDiaPointNumThres</code>、<code class="language-plaintext highlighter-rouge">convergRadius</code> 一类量，本质上都不是在定义单点异常本身，而是在定义：异常点在邻域内达到什么规模、什么密度之后，才足以形成可输出的缺陷。这也解释了为什么系统不会因为某一个孤立点的轻微异常就立刻乱报。</p>

<p>还有一类参数，控制的是<strong>局部展开方式与局部测量环境</strong>。<br />
例如 <code class="language-plaintext highlighter-rouge">DiffThres</code>、<code class="language-plaintext highlighter-rouge">availNeighbourRate4Break</code>、<code class="language-plaintext highlighter-rouge">availNeighbourRate4ColorDiff</code>、<code class="language-plaintext highlighter-rouge">binaryzationType</code>、<code class="language-plaintext highlighter-rouge">bgRate</code> 这类量，决定的是局部邻域怎样形成、哪些局部信息被视为有效、背景与结构如何区分。这里的 <code class="language-plaintext highlighter-rouge">DiffThres</code> 也并不是一个普通差分阈值，而是和<strong>是否通过投影来做阈值分割</strong>相关，尤其在 <strong>SN 码区域</strong> 上更有效。这说明系统的某些参数并不是在“直接判 defect”，而是在决定局部测量该以什么方式展开。</p>

<p>换句话说，参数在这里只是<strong>控制量</strong>，而不是<strong>问题本体</strong>。问题本体始终是：模板如何落位，测量点如何展开，局部异常如何被解释。</p>

<p>所以，从更本质的角度看，这一层并不是简单规则堆砌，而是一种建立在模板、配准、测量点和局部邻域之上的结构化测量系统。参数当然存在，但它们不是主干；主干始终是：<strong>先把结构关系建立起来，再让参数和规则落到已经成立的结构之上。</strong></p>

<p>也正因为如此，这套系统虽然有不少可调参数，但它并没有退化成“规则越写越多、阈值越调越碎”的经验拼装。相反，它的参数之所以能够工作，恰恰是因为前面的模板组织、两层配准和测量点落位，已经把问题约束在了一个足够稳定的结构框架里。</p>

<h2 id="八这一篇的结尾从如何测走向如何输出">八、这一篇的结尾：从“如何测”走向“如何输出”</h2>

<p>到这里其实可以看清：这套系统里的缺陷检测，并不是直接从图像里“扫出来”的，而是沿着已经落位的测量集被逐点测量、再逐步聚并出来的。</p>

<p>因此，这一篇最重要的结论可以压缩成一句话：</p>

<blockquote>
  <p><strong>缺陷不是直接从整张图里找出来的，而是沿着已经落位的测量集被测出来的。</strong></p>
</blockquote>

<p>这也意味着，系统真正的难点并不只是“有没有一个判定规则”，而在于：模板、配准、测量点、局部邻域和缺陷输出之间，能否被组织成一条连贯的技术主线。</p>

<p>下一讲，我会继续沿着这里往下写：这些已经形成的缺陷结果，最终是如何进入并适应产线节拍、并行处理、多线程组织和工程实施流程的。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[How Curved-Surface Pattern Defects Are Actually Measured: From Template Alignment to Defect Decision]]></summary></entry><entry><title type="html">第四讲：曲面 Pattern 缺陷检测的核心几何机制——两层配准与注册集、测量集的角色分工</title><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/30/the-core-geometric-mechanism-of-this-system-two-layer-registration-and-the-division-of-roles-between-register-and-measure-sets.html" rel="alternate" type="text/html" title="第四讲：曲面 Pattern 缺陷检测的核心几何机制——两层配准与注册集、测量集的角色分工" /><published>2026-03-30T18:00:00+08:00</published><updated>2026-03-30T18:00:00+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/30/the-core-geometric-mechanism-of-this-system-two-layer-registration-and-the-division-of-roles-between-register-and-measure-sets</id><content type="html" xml:base="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/30/the-core-geometric-mechanism-of-this-system-two-layer-registration-and-the-division-of-roles-between-register-and-measure-sets.html"><![CDATA[<h1 id="the-core-geometric-mechanism-of-curved-surface-pattern-inspection-two-layer-registration-and-the-functional-division-between-the-register-set-and-the-measure-set">The Core Geometric Mechanism of Curved-Surface Pattern Inspection: Two-Layer Registration and the Functional Division Between the Register Set and the Measure Set</h1>

<blockquote>
  <p><strong>English Abstract</strong><br />
This article explains the core geometric mechanism of the inspection system: a two-layer registration strategy together with the functional separation between the register set and the measure set. The first layer establishes coarse correspondence between local units at the block level, while the second layer brings structural templates into the measured image at a finer scale. At the same time, the system does not force the same points to serve both localization and defect sensing. Instead, it separates the points used for registration from those used for measurement, so that stability and sensitivity can be optimized for different purposes. In addition, the article clarifies that the skeleton is not merely a visual template, but the minimum registration unit whose granularity and topological complexity directly affect the robustness of nonrigid alignment. From this perspective, registration is not the final decision maker, but the geometric foundation on which subsequent defect measurement becomes reliable.</p>
</blockquote>

<p>在前一篇里，我主要讨论了这套系统为什么能够在不同产品、不同国别、不同产线之间复用，以及 xml 为什么不只是普通配置文件，而更接近一份产品几何描述文件。但模板文件本身仍然只是“静态结构”。它保存了字符库、全局几何锚点、block 序列、拓扑先验和方向容错，却还没有回答一个更核心的问题：这些静态模板究竟如何进入实测图像，并真正支撑起后续缺陷测量？</p>

<p>答案就在配准。</p>

<p>不过，这里的配准并不是一个孤立的小模块，也不是做完“对齐一下”就结束的技术动作。对这套系统来说，配准承担的是更基础的任务：它要把模板和实测对象之间的<strong>可比较关系</strong>建立起来，使后续检测不再停留在位图层面的直接碰撞，而是落在一个已经被几何组织过的结构关系上。</p>

<p>也正因为如此，这套系统并不是“一次配准解决全部问题”。在当前工程实现中，这一思想被具体落成了两层配准：先在较粗层级上建立局部单元之间的大致对应，再在更细层级上让结构模板真正落入实测图像。这里的“两层”不必被理解为唯一可能的形式，它更接近于一种分层配准思想在当时工程规模下的有效实现。若从更一般的角度看，这类机制完全可以被进一步推广为层级化、图结构化的配准组织；但在这套系统所面对的对象规模与节拍约束下，两层已经足够有效，也足够稳定。</p>

<p>与此同时，这套系统并没有把“模板”简单理解成一堆统一处理的点。前面第三篇已经看到，模板 xml 的 <code class="language-plaintext highlighter-rouge">character</code> 层下面组织着多个 <code class="language-plaintext highlighter-rouge">skeleton</code> 节点；但到了检测阶段，这些 <code class="language-plaintext highlighter-rouge">skeleton</code> 并不只是静态模板条目，而会进一步成为配准组织中的最小单元。更进一步，系统又不会把单元内部的所有点一视同仁地统一处理，而是继续区分出注册集与测量集。这样做的原因很简单：最适合建立定位关系的单元粒度，不一定最适合感知局部缺陷；而最适合用于定位的点，也不一定最适合用于测量。于是，配准和测量虽然连续发生，却并不依赖完全同一种组织方式。</p>

<p>所以，这一篇真正要讲清楚的是四件事：</p>

<ol>
  <li>为什么这套系统不能跳过配准直接做缺陷判别；</li>
  <li>为什么不是一次配准，而是两层配准；</li>
  <li>为什么 <code class="language-plaintext highlighter-rouge">skeleton</code> 的粒度本身就是配准设计的一部分；</li>
  <li>为什么注册集与测量集要在模板层就区分角色。</li>
</ol>

<h2 id="一为什么这套系统不能跳过配准直接做缺陷判别">一、为什么这套系统不能跳过配准，直接做缺陷判别</h2>

<p>如果把问题理解得很浅，最容易产生的想法就是：既然目标是检测缺陷，那么为什么不直接在图像上判断哪里异常、哪里不异常？但这条路在这类项目里通常走不远。原因并不只是现场存在扰动——这在平面检测里同样成立。更关键的是，这套系统并不满足于在位图层面直接比较“像不像”，而是进一步抓住了 pattern 的几何结构本身。</p>

<p>换句话说，这里的检测对象并不只是原始位图区域，而是经过模板组织与配准落位之后的一组结构点及其邻域关系。只有当这种结构关系被建立起来，系统才有可能把局部几何偏离从普通成像波动中分离出来，并进一步测量出非常细微但真实的缺陷。例如，一个字符局部少掉一个点，一段笔画中间断掉一个很小的方块，或者骨架邻域里出现极小但真实的结构中断，这些都不是简单靠位图阈值就能稳定抓住的。</p>

<p>因此，配准之所以不可跳过，不是因为“曲面对象更复杂”这一句泛泛的判断，而是因为：<strong>只有先把模板结构带到实测图像中，后续测量才有真正可靠的几何参照。</strong><br />
从这个意义上说，缺陷判别不是起点，配准后的几何关系才是起点。</p>

<h2 id="二为什么不是一次配准而是两层配准">二、为什么不是一次配准，而是两层配准</h2>

<p>如果已经承认配准不可跳过，接下来自然会问：为什么不能一次完成，为什么系统里会长成目前这种分层结构？</p>

<p>原因在于，这套系统面对的并不是一个单一、干净、局部的小对象，而是一整张产品面上的多个 block、多个局部结构、多个模板入口，以及实时业务输入带来的动态变化。如果一上来就直接拿所有精细结构点去做统一配准，问题通常会变得既慢又不稳：搜索空间太大，候选关系太杂，局部细节又很容易被全局误差拖偏。</p>

<p>所以，系统必须先回答一个更粗的问题，再回答一个更细的问题。</p>

<p>第一层配准，作用在较粗层级上。它面对的是 block 的几何代表点，例如中心点集合或全局锚点集合，本质上仍然是点集配准，只不过粒度更粗。它要解决的不是精细测量，而是先建立局部单元之间的大致对应：当前图像里哪些 block 成立，它们分别大致对应模板中的哪一部分，后续精配准应该在什么局部范围内展开。</p>

<p>第二层配准，才真正进入结构模板层。也就是在 block 大致落位之后，再把注册集带入实测对象，在更细的几何层面上建立局部模板与实测对象的对应关系，并让测量集随之同步进入正确位置。只有做到这一步，后续缺陷测量才真正拥有了足够细的几何基础。</p>

<p>因此，当前实现里的“两层配准”并不是重复劳动，而是分别承担两个层级的问题：<br />
第一层回答“哪一块对哪一块、整体大致在哪”；<br />
第二层回答“结构模板如何真正落位、后续测量点如何进入实测图像”。</p>

<p>如果把这两层任务混在一起，系统就很容易既不稳定，也不经济。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure4_hierarchical_registration_v2.png" alt="上层是 block 级粗配准，下层是注册集 / 测量集上的精配准与测量。" /></p>

<p><em>图 1：两层配准的层级示意图。上层负责 block 级粗配准，下层负责 register set / measure set 的精配准与测量。</em></p>

<h2 id="三为什么-skeleton-的粒度本身就是配准设计的一部分">三、为什么 <code class="language-plaintext highlighter-rouge">skeleton</code> 的粒度本身就是配准设计的一部分</h2>

<p>讨论第二层配准时，一个很容易被忽略的问题是：<strong>模板到底以什么作为最小配准单元进入实测图像？</strong></p>

<p>在这套系统里，这个单元并不是抽象的“全部点集”，而往往就是一个 <code class="language-plaintext highlighter-rouge">skeleton</code>。而 <code class="language-plaintext highlighter-rouge">skeleton</code> 又不一定总是对应单个字符，它既可以表示一个字符的骨架，也可以把多个字符、一个 logo、甚至一段更复杂的局部图样合并成一个整体骨架单元。也就是说，<code class="language-plaintext highlighter-rouge">skeleton</code> 不只是模板库里的一个名字，它实际上决定了第二层配准的基本粒度。</p>

<p>这一点非常重要，因为不同粒度的 <code class="language-plaintext highlighter-rouge">skeleton</code> 有不同的工程取舍。</p>

<p>如果把单个字符做成一个 <code class="language-plaintext highlighter-rouge">skeleton</code>，局部配准通常会更精细，对单字符的形变也更敏感，某些缺陷更不容易被“平均掉”。但它的代价是：对一些结构过于简单的对象，尤其是几何约束很弱、拓扑信息很少的字符，配准本身会变得不够稳。</p>

<p>例如像 “1” 这类简单字符，如果它局部少掉一小段，非刚性配准在某些情况下仍然可能把模板“配上去”。原因并不神秘：这类对象的结构自由度高而约束少，局部 break 可能会被平滑形变吸收掉。换句话说，模板虽然对齐了，但缺陷本身却没有因此被可靠地显现出来，这就会带来现场漏检风险。</p>

<p>而对带有回环、分叉，或者更复杂内部几何组织的对象，情况就会不一样。像 “B”“6” 这类结构，或者把多个字符合并成一个整体骨架之后，配准时会得到更强的整体约束。其原因在于：一旦对象内部存在更丰富的拓扑关系或更长程的结构耦合，非刚性配准就不那么容易只靠局部拉伸或滑移把缺损“抹平”。简单说，<strong>结构越复杂，模板越不容易在错误位置上“假装配准成功”</strong>。</p>

<p>这就是为什么在汉字、不规则图形，或者一些局部结构过于简单的字符区域里，把多个字符合并为一个 <code class="language-plaintext highlighter-rouge">skeleton</code> 往往反而更鲁棒。这样做虽然会牺牲一部分单字符粒度的灵活性，但它会显著降低配准失败和过杀的风险，也更不容易让缺陷被非刚性形变吸收掉。</p>

<p>因此，这里的 <code class="language-plaintext highlighter-rouge">skeleton</code> 粒度并不是模板制作上的随手选择，而本身就是配准设计的一部分。<br />
单字符 <code class="language-plaintext highlighter-rouge">skeleton</code> 的优势在于灵敏；<br />
多字符 <code class="language-plaintext highlighter-rouge">skeleton</code> 的优势在于鲁棒；<br />
而真正合理的取舍，来自对象结构本身及其拓扑约束强弱。</p>

<h2 id="四为什么注册集与测量集必须在模板层就分开">四、为什么注册集与测量集必须在模板层就分开</h2>

<p>这套系统里另一个非常关键、也非常容易被忽略的设计，是注册集与测量集的角色分工。</p>

<p>很多人在做检测时，会很自然地默认：既然一组点能用来定位模板，那它们当然也可以直接拿来做缺陷测量。但在真实工程里，这两件事往往并不完全重合。最适合用来建立定位关系的点，不一定最适合感知局部异常；反过来，对缺陷最敏感的点，也不一定在注册时最稳定。</p>

<p>因此，这套系统并不是在运行时才临时把点分成两类，而是在模板层就已经把这种角色分工编码了进去。最直接的证据，就是读取 <code class="language-plaintext highlighter-rouge">skeleton</code> 点属性时的这段逻辑：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">x</span> <span class="o">=</span> <span class="n">atoi</span><span class="p">(</span><span class="n">p_p</span><span class="o">-&gt;</span><span class="n">Attribute</span><span class="p">(</span><span class="s">"x"</span><span class="p">));</span>
<span class="n">y</span> <span class="o">=</span> <span class="n">atoi</span><span class="p">(</span><span class="n">p_p</span><span class="o">-&gt;</span><span class="n">Attribute</span><span class="p">(</span><span class="s">"y"</span><span class="p">));</span>
<span class="n">t</span> <span class="o">=</span> <span class="n">atoi</span><span class="p">(</span><span class="n">p_p</span><span class="o">-&gt;</span><span class="n">Attribute</span><span class="p">(</span><span class="s">"t"</span><span class="p">));</span> <span class="c1">// point type</span>

<span class="k">if</span> <span class="p">(</span><span class="mi">1</span> <span class="o">==</span> <span class="n">t</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">s</span><span class="p">.</span><span class="n">ps4regist</span><span class="p">[</span><span class="n">s</span><span class="p">.</span><span class="n">num4regist</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">x</span><span class="p">;</span>
    <span class="n">s</span><span class="p">.</span><span class="n">ps4regist</span><span class="p">[</span><span class="n">s</span><span class="p">.</span><span class="n">num4regist</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">y</span><span class="p">;</span>
    <span class="n">s</span><span class="p">.</span><span class="n">num4regist</span><span class="o">++</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="mi">0</span> <span class="o">==</span> <span class="n">t</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">s</span><span class="p">.</span><span class="n">ps4measure</span><span class="p">[</span><span class="n">s</span><span class="p">.</span><span class="n">num4measure</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">x</span><span class="p">;</span>
    <span class="n">s</span><span class="p">.</span><span class="n">ps4measure</span><span class="p">[</span><span class="n">s</span><span class="p">.</span><span class="n">num4measure</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">y</span><span class="p">;</span>
    <span class="n">s</span><span class="p">.</span><span class="n">num4measure</span><span class="o">++</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这段代码的意义并不只是“把点读进来”。它真正说明的是：同一个 <code class="language-plaintext highlighter-rouge">skeleton</code> 内部的点，并不是天然同质的，而是从模板描述层就已经被区分成了两种角色：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">t = 1</code> 的点进入注册集；</li>
  <li><code class="language-plaintext highlighter-rouge">t = 0</code> 的点进入测量集。</li>
</ul>

<p>也就是说，注册集与测量集并不是后处理阶段才拆开的两个点集，而是一开始就写在模板里的角色分工。这种设计的意义非常大，因为它使得“定位稳定性”和“缺陷敏感性”可以在模板层就被分别优化，而不必强迫同一批点同时承担两个并不完全一致的目标。</p>

<p>这种区分在不同对象上会有不同体现。对于枝杈结构，例如字符，中轴线往往同时具有较强的结构代表性和测量可用性，因此注册集和测量集可以接近甚至重合。但对于实心结构，例如某些实心 logo 或填充区域，边缘点更适合用来建立定位关系，而内部点可能更适合承接后续测量。也就是说，这套系统并不是先验地规定“所有对象都用同一批点做所有事”，而是根据对象结构去决定：哪些点最适合注册，哪些点最适合测量。</p>

<p>一旦把注册和测量分开，系统就获得了更高的灵活性：定位可以优先追求稳定性，测量则可以优先追求敏感性，而不必让同一批点同时承担两个并不完全一致的目标。反过来，如果把两者强行混在一起，往往就是注册不够稳，测量也不够准。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure4_register_vs_measure_sets_v2.png" alt="左侧为枝杈结构，注册集与测量集可接近甚至重合；右侧为实心结构，边缘点用于注册，内点用于测量。" /></p>

<p><em>图 2：注册集与测量集分离示意图。左侧为枝杈结构，右侧为实心结构。</em></p>

<h2 id="五两层配准在系统里到底是怎么串起来的">五、两层配准在系统里到底是怎么串起来的</h2>

<p>从系统流程上看，分层配准、<code class="language-plaintext highlighter-rouge">skeleton</code> 粒度以及注册集、测量集的角色分工，并不是彼此分离的几个概念，而是紧密串成一条主线。</p>

<p>系统首先读取产品模板与 block 组织信息，并在较粗层级上对 block 的几何代表点——例如中心点集合或全局锚点集合——进行第一层点集配准，以确定哪些局部单元在当前图像中成立，以及它们与模板之间的大致对应关系。换句话说，第一层并不是一个模糊的“先找一找位置”，而已经是在较粗粒度上建立局部单元之间的几何对应。</p>

<p>随后，对已经落入合理局部范围的 block，系统再读取其对应的 <code class="language-plaintext highlighter-rouge">skeleton</code> 单元，并根据模板中已经编码好的点角色，把注册集与测量集分别取出。在第二层配准中，系统用注册集把模板真正带到实测图像上；而测量集并不是另起一套独立流程，而是随着配准关系同步推进。最后，镭断、色异常、多镭、偏移等局部缺陷测量，才沿着这些已经被带到实测图像中的测量点展开。</p>

<p>如果把这一过程再压缩一点，它的逻辑其实就是：</p>

<blockquote>
  <p>先在粗层级上建立局部单元之间的大致对应，再在细层级上以 <code class="language-plaintext highlighter-rouge">skeleton</code> 为最小单元建立结构模板与实测对象之间的精细对应，最后把缺陷测量放在这个对应关系之上。</p>
</blockquote>

<p>所以，配准在这里并不只是“求一个变换”或者“返回一个分数”，它真正做的是把模板关系变成后续检测可以依赖的几何事实。</p>

<p>从工程实现的角度看，这一过程更接近下面这样的流程：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>for each matched block:
    coarse registration on block-level representative points
    determine approximate correspondence
    select skeleton unit(s) in this block
    load register set and measure set from template
    fine registration on the register set
    move the measure set together
    perform local defect measurement along measurement points
</code></pre></div></div>

<p>这段流程看上去并不复杂，但它把系统里最关键的分工都压缩进去了：<br />
第一层负责建立 block 级对应；<br />
第二层负责让模板真正落位；<br />
<code class="language-plaintext highlighter-rouge">skeleton</code> 决定了精配准的最小组织单元；<br />
而缺陷测量则始终建立在已经落位的测量集之上。</p>

<h2 id="六为什么配准在这里不是最终判决器">六、为什么配准在这里不是最终判决器</h2>

<p>很多人一看到配准，就很容易把它理解成“最后给出一个分数”或者“最后决定是不是匹配成功”。但在这套系统里，配准并不是最终判决器。它当然可以给出某种全局失配量，并在某些情形下辅助 mismatch 一类的判定；但对于镭断、色异常、多镭这类真正依赖局部结构的缺陷类型来说，配准分数本身并不构成结论。</p>

<p>真正的结论来自哪里？来自配准之后。</p>

<p>更准确地说，来自这样一个过程：模板已经通过注册关系被带到实测图像上，测量集也已经随之进入了正确位置，系统随后再沿着这些测量点去展开局部邻域分析、灰度与对比度测量、宽度与残余面积测量，最后才对缺陷类型作出判断。</p>

<p>从这个意义上说，配准最重要的输出，并不是某个抽象的返回值，而是：<strong>它把后续检测所依赖的测量点关系，变成了实测图像中的几何事实。</strong></p>

<p>这也是为什么我一直更愿意把配准理解为几何基础，而不是最终判决器。它本身不是“最后答案”，但没有它，后面的答案就没有可靠的参照系。</p>

<h2 id="七这一层为什么是整套系统真正的核心几何机制">七、这一层为什么是整套系统真正的核心几何机制</h2>

<p>如果把 xml 看作这套系统的静态骨架，那么两层配准、<code class="language-plaintext highlighter-rouge">skeleton</code> 作为最小配准单元的组织方式，以及注册集 / 测量集在模板层就被区分开的角色分工，就是这套系统真正开始“动起来”的地方。</p>

<p>模板在这里不再只是存储对象，而是开始进入图像；<br />
block 不再只是组织单元，而是开始建立对应；<br />
<code class="language-plaintext highlighter-rouge">skeleton</code> 不再只是模板库中的一个名字，而是开始决定精配准的基本粒度；<br />
注册集不再只是模板点的一部分，而是开始承担定位任务；<br />
测量集也不再只是附属点，而是开始沿着正确的几何关系进入实测对象，并真正支撑起后续缺陷测量。</p>

<p>也就是说，到这一层为止，这套系统才第一次把“模板组织”真正转化成了“检测能力”。</p>

<p>这也是为什么我会把这一层看作整套系统的核心几何机制。因为它真正解决了这样一个问题：模板如何不只是静态存在，而是成为后续检测可以依赖的几何事实；以及，模板的粒度、拓扑复杂度与点角色分工，又如何共同影响配准的稳定性与后续测量的可靠性。</p>

<p>下一讲，我会继续沿着这里往下写：在测量集已经落位之后，镭断、色异常、多镭等缺陷究竟是如何被逐点测量、逐类归并，并最终转化成系统输出结果的。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The Core Geometric Mechanism of Curved-Surface Pattern Inspection: Two-Layer Registration and the Functional Division Between the Register Set and the Measure Set]]></summary></entry><entry><title type="html">第三讲：为什么曲面 Pattern 缺陷检测系统能跨产品复用——一份 XML 里的模板、几何组织、拓扑与运行分支</title><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/30/why-this-inspection-system-hardly-needs-code-changes-across-products.html" rel="alternate" type="text/html" title="第三讲：为什么曲面 Pattern 缺陷检测系统能跨产品复用——一份 XML 里的模板、几何组织、拓扑与运行分支" /><published>2026-03-30T14:00:00+08:00</published><updated>2026-03-30T14:00:00+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/30/why-this-inspection-system-hardly-needs-code-changes-across-products</id><content type="html" xml:base="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/30/why-this-inspection-system-hardly-needs-code-changes-across-products.html"><![CDATA[<h1 id="why-curved-surface-pattern-inspection-systems-can-be-reused-across-products-templates-geometric-organization-topology-and-runtime-branching-in-one-xml-file">Why Curved-Surface Pattern Inspection Systems Can Be Reused Across Products: Templates, Geometric Organization, Topology, and Runtime Branching in One XML File</h1>

<blockquote>
  <p><strong>English Abstract</strong><br />
This article explains why the inspection system can be reused across different products, regions, and production lines with minimal changes to the core DLL. The key lies not merely in universally adaptable code, but in an external XML-based geometric description layer that organizes structural templates, minimal registration units, register/measure point roles, global geometric objects, block-level organization, topology-aware priors, directional tolerance, and runtime product branches in a unified way. From this perspective, the XML file is not a passive configuration file, but an explicit structural description of how the system understands, organizes, and activates its detection objects.</p>
</blockquote>

<p>如果只从外部看，很多人会以为这套系统之所以能在不同国别、不同产品甚至不同产线之间复用，主要靠的是“代码写得通用”。但真正做过这套系统的人会知道，事情并不是这样。它真正的关键，不在于不断修改核心 DLL 去适配变化，而在于把大量会变化的东西，提前组织进了一套外部几何模板文件里。</p>

<p>对这套系统来说，xml 不是附属配置，而是模板、几何组织、拓扑、容错与运行分支逻辑的共同载体。也正因为如此，很多产品切换时，真正需要改动的并不是核心代码，而主要是图档、xml 和 ini 这一层。换句话说，这套系统真正可复用的前提，不是“什么都写死在程序里”，而是把变化压缩进一套结构化、可替换、可维护的模板描述层。</p>

<p>为了避免一上来就陷入字段细节，先看这份 xml 的整体结构。只有先看到它的层次，后面关于模板、几何锚点、block 和产品入口的讨论才不会显得零散。</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;entity&gt;</span>
    <span class="nt">&lt;character&gt;</span>
        <span class="nt">&lt;skeleton</span> <span class="na">name=</span><span class="s">"jt"</span> <span class="na">size=</span><span class="s">"..."</span> <span class="na">genus=</span><span class="s">"0"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"..."</span> <span class="na">y=</span><span class="s">"..."</span> <span class="na">t=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
            <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"..."</span> <span class="na">y=</span><span class="s">"..."</span> <span class="na">t=</span><span class="s">"0"</span> <span class="nt">/&gt;</span>
            <span class="c">&lt;!-- ... --&gt;</span>
        <span class="nt">&lt;/skeleton&gt;</span>

        <span class="nt">&lt;skeleton</span> <span class="na">name=</span><span class="s">"logo"</span> <span class="na">size=</span><span class="s">"..."</span> <span class="na">genus=</span><span class="s">"1"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"..."</span> <span class="na">y=</span><span class="s">"..."</span> <span class="nt">/&gt;</span>
            <span class="c">&lt;!-- ... --&gt;</span>
        <span class="nt">&lt;/skeleton&gt;</span>

        <span class="nt">&lt;skeleton</span> <span class="na">name=</span><span class="s">"CCAI19LP"</span> <span class="na">size=</span><span class="s">"..."</span> <span class="na">genus=</span><span class="s">"1"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"..."</span> <span class="na">y=</span><span class="s">"..."</span> <span class="nt">/&gt;</span>
            <span class="c">&lt;!-- ... --&gt;</span>
        <span class="nt">&lt;/skeleton&gt;</span>

        <span class="c">&lt;!-- ... --&gt;</span>
    <span class="nt">&lt;/character&gt;</span>

    <span class="nt">&lt;globalGeo</span> <span class="na">size=</span><span class="s">"..."</span><span class="nt">&gt;</span>
        <span class="nt">&lt;Geo</span> <span class="na">name=</span><span class="s">"sn"</span> <span class="na">size=</span><span class="s">"12"</span> <span class="na">extValue=</span><span class="s">"1186"</span> <span class="na">smode=</span><span class="s">"0"</span> <span class="na">repres=</span><span class="s">"0"</span> <span class="na">type=</span><span class="s">"1"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"..."</span> <span class="na">y=</span><span class="s">"..."</span> <span class="na">prio=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
            <span class="c">&lt;!-- ... --&gt;</span>
        <span class="nt">&lt;/Geo&gt;</span>

        <span class="nt">&lt;Geo</span> <span class="na">name=</span><span class="s">"logo"</span> <span class="na">size=</span><span class="s">"3"</span> <span class="na">extValue=</span><span class="s">"1111"</span> <span class="na">smode=</span><span class="s">"0"</span> <span class="na">repres=</span><span class="s">"1"</span> <span class="na">type=</span><span class="s">"1"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;p</span> <span class="na">x=</span><span class="s">"..."</span> <span class="na">y=</span><span class="s">"..."</span> <span class="na">prio=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
            <span class="c">&lt;!-- ... --&gt;</span>
        <span class="nt">&lt;/Geo&gt;</span>

        <span class="c">&lt;!-- ... --&gt;</span>
    <span class="nt">&lt;/globalGeo&gt;</span>

    <span class="nt">&lt;blockSet</span> <span class="na">size=</span><span class="s">"..."</span><span class="nt">&gt;</span>
        <span class="nt">&lt;block</span> <span class="na">extValue=</span><span class="s">"0"</span> <span class="na">prio=</span><span class="s">"1"</span> <span class="na">type=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;block</span> <span class="na">extValue=</span><span class="s">"0"</span> <span class="na">prio=</span><span class="s">"1"</span> <span class="na">type=</span><span class="s">"2"</span> <span class="nt">/&gt;</span>
        <span class="c">&lt;!-- ... --&gt;</span>
    <span class="nt">&lt;/blockSet&gt;</span>

    <span class="nt">&lt;eraseSet</span> <span class="na">size=</span><span class="s">"..."</span><span class="nt">&gt;</span>
        <span class="nt">&lt;erase</span> <span class="err">...</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/eraseSet&gt;</span>

    <span class="nt">&lt;templPathSet</span> <span class="na">size=</span><span class="s">"2"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;product</span> <span class="na">templatePath=</span><span class="s">"tw1.bmp"</span> <span class="na">type=</span><span class="s">"1"</span> <span class="na">rows=</span><span class="s">"..."</span> <span class="na">cols=</span><span class="s">"..."</span> <span class="na">xOffset=</span><span class="s">"0"</span> <span class="na">yOffset=</span><span class="s">"0"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;holder</span> <span class="na">relocation=</span><span class="s">"1"</span> <span class="na">relocRadius=</span><span class="s">"100"</span> <span class="na">xRange=</span><span class="s">"400"</span> <span class="na">yRange=</span><span class="s">"600"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;/product&gt;</span>
        <span class="nt">&lt;product</span> <span class="na">templatePath=</span><span class="s">"tw2.bmp"</span> <span class="na">type=</span><span class="s">"2"</span> <span class="na">rows=</span><span class="s">"..."</span> <span class="na">cols=</span><span class="s">"..."</span> <span class="na">xOffset=</span><span class="s">"0"</span> <span class="na">yOffset=</span><span class="s">"0"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;holder</span> <span class="na">relocation=</span><span class="s">"1"</span> <span class="na">relocRadius=</span><span class="s">"100"</span> <span class="na">xRange=</span><span class="s">"400"</span> <span class="na">yRange=</span><span class="s">"600"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;/product&gt;</span>
    <span class="nt">&lt;/templPathSet&gt;</span>

    <span class="nt">&lt;defectParam&gt;</span>
        <span class="nt">&lt;param</span> <span class="na">baseIndex=</span><span class="s">"15"</span> <span class="na">infimum=</span><span class="s">"50"</span> <span class="na">supermum=</span><span class="s">"150"</span> <span class="na">type=</span><span class="s">"skewing"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/defectParam&gt;</span>
<span class="nt">&lt;/entity&gt;</span>
</code></pre></div></div>
<p>上面这段代码只保留了 <code class="language-plaintext highlighter-rouge">xml</code> 的主干结构。实际文件中，<code class="language-plaintext highlighter-rouge">character</code> 下会列出项目里出现的各类字符或局部图样 <code class="language-plaintext highlighter-rouge">block</code> 的结构模板，<code class="language-plaintext highlighter-rouge">globalGeo</code> 下则保存对应的全局几何锚点。这里省略了大量具体点坐标，只保留其组织方式，是为了更直观地展示这份模板文件背后的几何逻辑。</p>

<p>如果先不陷入字段细节，而只抓住这份 <code class="language-plaintext highlighter-rouge">xml</code> 的主干结构，那么它做的事情其实非常明确：一方面保存字符或局部图样的结构模板，另一方面保存全局几何锚点、<code class="language-plaintext highlighter-rouge">block</code> 组织方式、局部擦除规则以及产品模板入口。也就是说，它并不是单纯存一张参考图，而是在系统层面描述：这个产品上有哪些可检测对象，它们大致位于哪里，哪些局部结构要被拿来做注册与测量，哪些区域需要额外容错，以及不同产品面分别对应哪一套模板入口。</p>

<p>从这个角度看，这份 <code class="language-plaintext highlighter-rouge">xml</code> 更接近一份<strong>产品几何描述文件</strong>，而不是普通意义上的配置文件。其内部层次也很清楚：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">character</code> 下组织的是多个 <code class="language-plaintext highlighter-rouge">skeleton</code>，也就是字符或图样的局部结构模板；</li>
  <li><code class="language-plaintext highlighter-rouge">globalGeo</code> 下保存的是命名的全局几何点集，用来提供更上层的定位锚点；</li>
  <li><code class="language-plaintext highlighter-rouge">blockSet</code> 定义了当前产品实际参与检测的 <code class="language-plaintext highlighter-rouge">block</code> 序列；</li>
  <li><code class="language-plaintext highlighter-rouge">eraseSet</code> 给出局部擦除与屏蔽规则；</li>
  <li><code class="language-plaintext highlighter-rouge">templPathSet</code> 负责把具体产品模板图接入系统。</li>
</ul>

<p>更进一步，<code class="language-plaintext highlighter-rouge">holder</code>、<code class="language-plaintext highlighter-rouge">repres</code>、<code class="language-plaintext highlighter-rouge">t</code> 和 <code class="language-plaintext highlighter-rouge">defectParam</code> 这些字段还说明，这套系统保存的并不只是“点和路径”，而是点的角色、表示方式、重定位策略和某些更高层的检测语义。</p>

<p>还有一层此前很容易被低估，但实际上非常关键的关系，就是 <code class="language-plaintext highlighter-rouge">Geo</code>、<code class="language-plaintext highlighter-rouge">block</code> 与 <code class="language-plaintext highlighter-rouge">type</code> 之间的对应。</p>

<p>如果只把 <code class="language-plaintext highlighter-rouge">globalGeo</code> 看成“全局锚点集合”，那仍然说浅了。更准确地说，<code class="language-plaintext highlighter-rouge">globalGeo</code> 在这套系统里承担的是 <strong>block 的几何组织层</strong>：它先把产品上的局部对象组织成一组精确几何单元，再由这些几何单元展开成后续运行时真正参与检测的 <code class="language-plaintext highlighter-rouge">block</code>。与此同时，<code class="language-plaintext highlighter-rouge">type</code> 又把 <code class="language-plaintext highlighter-rouge">templPathSet</code> 中的模板图入口、<code class="language-plaintext highlighter-rouge">globalGeo</code> 中的几何对象集合以及 <code class="language-plaintext highlighter-rouge">blockSet</code> 中的运行单元序列绑成同一条产品分支。</p>

<p>也就是说，这份 <code class="language-plaintext highlighter-rouge">XML</code> 并不是“先有模板图，再有若干 block”这么简单，而是在模板层、几何层和运行层之间，已经预先建立了严格的组织关系。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure2_xml_to_detection_pipeline_en.png" alt="这份 xml 真正驱动的不是简单模板匹配，而是从模板组织、几何定位到局部缺陷测量的一整条检测链。" /></p>

<p><strong>图 1：</strong> 这份 <code class="language-plaintext highlighter-rouge">xml</code> 真正驱动的不是简单模板匹配，而是从模板组织、几何定位到局部缺陷测量的一整条检测链。</p>

<h2 id="一character这里存的不是字而是字符图样的结构模板">一、character：这里存的不是“字”，而是字符/图样的结构模板</h2>

<p>很多人第一次看到 <code class="language-plaintext highlighter-rouge">xml</code> 里的 <code class="language-plaintext highlighter-rouge">character</code>，会下意识把它理解成“文本内容”。其实不是。这里真正保存的，并不是字符语义，而是字符或局部图样的<strong>结构表达</strong>。在项目里，像 <code class="language-plaintext highlighter-rouge">jt</code>、<code class="language-plaintext highlighter-rouge">logo</code>、<code class="language-plaintext highlighter-rouge">CCAI19LP</code>、<code class="language-plaintext highlighter-rouge">E</code> 这样不同名字的 <code class="language-plaintext highlighter-rouge">skeleton</code>，下面都挂着一组 <code class="language-plaintext highlighter-rouge">p(x,y)</code> 点，它们共同构成了该对象的局部模板。</p>

<p>最常见的情况下，这类点集就是 <strong>medial axis</strong>，也就是中轴/骨架点集；但更准确地说，它们是一类可用于注册与测量的<strong>结构点模板</strong>。这里的重点并不在于“骨架”这个词本身，而在于：一旦模板保存的不是图片，而是结构点集，后面的系统行为就会完全不同。</p>

<ul>
  <li>图片只能拿来“比”；</li>
  <li>而点集结构既可以用来配准，也可以用来测量，还可以继续挂接拓扑信息、点角色和表示方式等更深一层的几何语义。</li>
</ul>

<p>也就是说，从这一层开始，这套系统就已经不是在做简单模板匹配，而是在做<strong>结构模板驱动的检测</strong>。</p>

<h2 id="二骨架点并不都一样有些点用于注册有些点用于测量">二、骨架点并不都一样：有些点用于注册，有些点用于测量</h2>

<p>更完整的 <code class="language-plaintext highlighter-rouge">XML</code> 里，骨架点甚至不只是“属于某个 <code class="language-plaintext highlighter-rouge">skeleton</code>”，而是还带有 <code class="language-plaintext highlighter-rouge">t</code> 这样的角色字段。实现代码里，<code class="language-plaintext highlighter-rouge">t=1</code> 的点会进入注册集，<code class="language-plaintext highlighter-rouge">t=0</code> 的点会进入测量集。也就是说，注册集与测量集并不是事后在程序里随意拆分出来的，而是在模板描述层就已经被明确编码了进去。</p>

<p>下面这段代码已经把这件事说得很清楚：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">x</span> <span class="o">=</span> <span class="n">atoi</span><span class="p">(</span><span class="n">p_p</span><span class="o">-&gt;</span><span class="n">Attribute</span><span class="p">(</span><span class="s">"x"</span><span class="p">));</span>
<span class="n">y</span> <span class="o">=</span> <span class="n">atoi</span><span class="p">(</span><span class="n">p_p</span><span class="o">-&gt;</span><span class="n">Attribute</span><span class="p">(</span><span class="s">"y"</span><span class="p">));</span>
<span class="n">t</span> <span class="o">=</span> <span class="n">atoi</span><span class="p">(</span><span class="n">p_p</span><span class="o">-&gt;</span><span class="n">Attribute</span><span class="p">(</span><span class="s">"t"</span><span class="p">));</span> <span class="c1">// topological / role property</span>

<span class="k">if</span> <span class="p">(</span><span class="mi">1</span> <span class="o">==</span> <span class="n">t</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">s</span><span class="p">.</span><span class="n">ps4regist</span><span class="p">[</span><span class="n">s</span><span class="p">.</span><span class="n">num4regist</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">x</span><span class="p">;</span>
    <span class="n">s</span><span class="p">.</span><span class="n">ps4regist</span><span class="p">[</span><span class="n">s</span><span class="p">.</span><span class="n">num4regist</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">y</span><span class="p">;</span>
    <span class="n">s</span><span class="p">.</span><span class="n">num4regist</span><span class="o">++</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="mi">0</span> <span class="o">==</span> <span class="n">t</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">s</span><span class="p">.</span><span class="n">ps4measure</span><span class="p">[</span><span class="n">s</span><span class="p">.</span><span class="n">num4measure</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">x</span><span class="p">;</span>
    <span class="n">s</span><span class="p">.</span><span class="n">ps4measure</span><span class="p">[</span><span class="n">s</span><span class="p">.</span><span class="n">num4measure</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">y</span><span class="p">;</span>
    <span class="n">s</span><span class="p">.</span><span class="n">num4measure</span><span class="o">++</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>这件事非常关键。因为它说明 <code class="language-plaintext highlighter-rouge">XML</code> 保存的并不只是几何点的位置，还在更深一层上保存了这些点在系统中的<strong>功能角色</strong>：哪些点更适合承担定位约束，哪些点更适合承接后续测量。换句话说，这套系统并不是先有一堆骨架点，再临时决定怎么用它们，而是在模板层就已经开始区分“为配准服务的点”和“为检测服务的点”。</p>

<p>这也意味着，后面第四篇里要讲的注册集与测量集分工，并不是某种运行时临时策略，而在 <code class="language-plaintext highlighter-rouge">XML</code> 这一层就已经埋下了结构基础。</p>

<h2 id="三skeleton-不只是字符骨架而是配准的最小结构单元">三、skeleton 不只是“字符骨架”，而是配准的最小结构单元</h2>

<p>如果只从字段名字看，<code class="language-plaintext highlighter-rouge">skeleton</code> 很容易被理解成“某个字符的骨架模板”。但从更真实的工程意义上说，它并不只是字符骨架，而更接近这套系统里<strong>配准的最小结构单元</strong>。这件事非常重要，因为最小单元到底取多小，并不是无关紧要的实现细节，而会直接影响配准稳定性、漏检风险和过杀风险。</p>

<p>在一些场景下，一个字符会单独作为一个 <code class="language-plaintext highlighter-rouge">skeleton</code>；而在另一些场景下，多个字符甚至一整个局部图样会被合并成一个 <code class="language-plaintext highlighter-rouge">skeleton</code>。两种做法各有优势。</p>

<p>单字符 <code class="language-plaintext highlighter-rouge">skeleton</code> 的优点是定位更细、更灵敏，局部异常更容易暴露出来；但它的缺点也很明显：对某些结构过于简单、拓扑信息过弱的字符，非刚性配准的自由度可能会把局部缺失“吸收”掉。比如字符 <code class="language-plaintext highlighter-rouge">"1"</code>，即使原图中已经少掉一段，模板仍然可能在整体上配准成功，从而带来现场漏检。</p>

<p>相比之下，多个字符合并而成的 <code class="language-plaintext highlighter-rouge">skeleton</code> 会在配准阶段提供更强的结构约束。因为这时系统面对的不再是一个局部过于简单的对象，而是一个包含更多相对位置关系、更多几何冗余、也更不容易被自由形变悄悄解释掉的整体结构。换句话说，单字符 <code class="language-plaintext highlighter-rouge">skeleton</code> 更强调灵敏度，而多字符 <code class="language-plaintext highlighter-rouge">skeleton</code> 更强调鲁棒性。</p>

<p>这里面最核心的，其实是<strong>拓扑和结构复杂度对配准稳定性的影响</strong>。像 <code class="language-plaintext highlighter-rouge">"B"</code>、<code class="language-plaintext highlighter-rouge">"6"</code> 这类具有环柄、回绕或更强结构闭合感的对象，在非刚性配准中通常会比 <code class="language-plaintext highlighter-rouge">"1"</code> 这类细长、单一、低复杂度对象更稳定。原因并不神秘：结构越复杂，模板中各局部之间的相互制约就越强，能够被自由形变吸收掉的空间就越小。对于汉字或不规则图形，如果单个局部本身过于脆弱，也可以通过合并多个字符或多个局部结构来提升配准稳定性。</p>

<p>从这个角度看，<code class="language-plaintext highlighter-rouge">skeleton</code> 的意义就不只是“骨架点集”，而是系统在模板层面对“最小配准单元”所做的一种结构设计。它既关系到模板如何表达对象，也关系到配准如何在灵敏度与鲁棒性之间取得平衡。</p>

<h2 id="四genus模板不仅有几何还有拓扑">四、genus：模板不仅有几何，还有拓扑</h2>

<p>这份 <code class="language-plaintext highlighter-rouge">xml</code> 里很多 <code class="language-plaintext highlighter-rouge">skeleton</code> 都带了 <code class="language-plaintext highlighter-rouge">genus</code>。如果不解释，这个字段会显得很怪；但它其实非常有意思。这里的 <code class="language-plaintext highlighter-rouge">genus</code> 指的是<strong>亏格</strong>，也就是对象从骨架角度看具有多少环柄或孔洞结构。换句话说，这套系统不是把字符模板只看成一堆点，而是进一步把它们看成带有拓扑类别的结构对象。</p>

<p>这一点很值得注意。它说明这套系统并不是在“尽量把工程写简单”，而是在真正有价值的地方引入了很少但很有效的数学信息。一个字符或者图样，仅靠局部几何位置，有时会因为形变、缺损、提取误差而变得不够稳；但一旦把亏格也带进来，模板匹配和结构选择就多了一层更稳的先验。它未必总是决定性的，但在复杂现场里，往往足以提高成功率。</p>

<p>更进一步说，<code class="language-plaintext highlighter-rouge">genus</code> 的意义并不只是“给对象贴一个拓扑标签”，而是和前面谈到的最小配准单元选择互相呼应：结构越复杂、拓扑越强，非刚性配准越不容易把真实缺失解释成“正常形变”；而对那些拓扑过弱的对象，就需要通过合并多个字符或局部图样来补强约束。也就是说，拓扑信息不是孤立存在的字段，它实际上参与了系统如何平衡配准鲁棒性与缺陷敏感性的思路。</p>

<h2 id="五globalgeo这不只是全局锚点层而是-block-的几何组织层">五、globalGeo：这不只是全局锚点层，而是 block 的几何组织层</h2>

<p>如果说 <code class="language-plaintext highlighter-rouge">character / skeleton</code> 负责局部结构模板，那么 <code class="language-plaintext highlighter-rouge">globalGeo</code> 负责的就不是简单意义上的“补充定位信息”，而是更上层的 <strong>block 几何组织</strong>。</p>

<p>这一点在完整 <code class="language-plaintext highlighter-rouge">XML</code> 里已经非常清楚。像 <code class="language-plaintext highlighter-rouge">sn</code>、<code class="language-plaintext highlighter-rouge">logo</code>、<code class="language-plaintext highlighter-rouge">A2190</code>、<code class="language-plaintext highlighter-rouge">CCAI19LP</code>、<code class="language-plaintext highlighter-rouge">jt</code> 这样的 <code class="language-plaintext highlighter-rouge">Geo</code>，表面上看只是命名点集；但从系统运行的角度看，它们真正提供的是：<strong>某个局部检测对象在产品面上的几何代表信息</strong>。在当前实现里，这类几何信息最典型的形式就是中心点集合，它首先服务于较粗层级上的点集配准，然后再把后续的局部模板、注册集和测量集带入实测图像。</p>

<p>也就是说，<code class="language-plaintext highlighter-rouge">globalGeo</code> 不是泛泛的“锚点层”，而是介于模板层与运行层之间的 <strong>几何组织层</strong>：它先告诉系统“产品上有哪些局部对象、它们大致在哪里、如何以中心点集合的方式被几何表示”，然后这些对象才进一步进入后续的 <code class="language-plaintext highlighter-rouge">block</code> 检测链。</p>

<p>这里还有一个很容易被忽略、但其实非常关键的事实：<code class="language-plaintext highlighter-rouge">Geo</code> 和 <code class="language-plaintext highlighter-rouge">block</code> 并不是彼此独立的两套描述。在很多配置里，普通 <code class="language-plaintext highlighter-rouge">Geo</code> 往往对应一个局部检测单元；而像 <code class="language-plaintext highlighter-rouge">sn</code> 这种槽位型对象，则会按字符位数进一步展开成多个 <code class="language-plaintext highlighter-rouge">block</code>。例如，在某些配置中，<code class="language-plaintext highlighter-rouge">globalGeo</code> 里除了 <code class="language-plaintext highlighter-rouge">sn</code> 之外还有若干普通 <code class="language-plaintext highlighter-rouge">Geo</code>，而 <code class="language-plaintext highlighter-rouge">sn</code> 自身包含 12 个字符槽位点；于是，前面的普通 <code class="language-plaintext highlighter-rouge">Geo</code> 各自对应一个 <code class="language-plaintext highlighter-rouge">block</code>，<code class="language-plaintext highlighter-rouge">sn</code> 再按 12 个字符位展开成 12 个 <code class="language-plaintext highlighter-rouge">block</code>，最后与 <code class="language-plaintext highlighter-rouge">blockSet</code> 的总数严格对应。</p>

<p>这件事非常重要。因为它说明系统并不是先把检测对象机械切成若干小块，再倒过来补一些几何信息；恰恰相反，它是先在 <code class="language-plaintext highlighter-rouge">globalGeo</code> 这一层完成对象的几何组织，再把这些几何对象投影成运行层真正参与检测的 <code class="language-plaintext highlighter-rouge">block</code> 单元。换句话说，<code class="language-plaintext highlighter-rouge">Geo</code> 给出的不是抽象参考点，而是 <strong>block 如何从产品几何中被组织出来</strong> 的依据。</p>

<p>从这个角度看，<code class="language-plaintext highlighter-rouge">globalGeo</code> 的意义就远不只是“粗定位用的点集”。它实际上承担的是：把产品上的局部对象组织成一组可配准、可展开、可进入检测链的几何单元。</p>

<h2 id="六repres-与-holderxml-里不仅有模板还有表示方式与重定位策略">六、repres 与 holder：XML 里不仅有模板，还有表示方式与重定位策略</h2>

<p>这份 <code class="language-plaintext highlighter-rouge">XML</code> 进一步有意思的地方，在于它并不只是存了骨架点和锚点，还存了这些对象在系统里“该如何被看待”。</p>

<p>例如，代码里对应的表示方式有这样一个枚举：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">enum</span> <span class="n">REPRES</span>
<span class="p">{</span>
    <span class="n">MEDIALAXIS</span><span class="p">,</span> <span class="n">POINTTOPO</span>
<span class="p">}</span> <span class="n">Represent</span><span class="p">;</span>
</code></pre></div></div>

<p>这说明模板对象至少支持两种不同的结构表述：一种更接近中轴骨架，另一种则更接近点拓扑式表达。换句话说，系统并不是用单一方式理解所有对象，而是允许不同对象以不同结构语义进入检测链。</p>

<p>另一方面，<code class="language-plaintext highlighter-rouge">templPathSet</code> 下面的 <code class="language-plaintext highlighter-rouge">holder</code> 也不只是“顺手挂了个子节点”。从对应结构体可以看出，它实际上是产品级整体模板的<strong>重定位与约束信息</strong>：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">WholeTempl</span>
<span class="p">{</span>
    <span class="kt">char</span> <span class="n">path</span><span class="p">[</span><span class="n">MAX_PATH</span><span class="p">];</span>
    <span class="kt">int</span> <span class="n">xOffset</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">yOffset</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">xRange</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">yRange</span><span class="p">;</span>
    <span class="kt">bool</span> <span class="n">avail</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">relocation</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">relocRadius</span><span class="p">;</span>
    <span class="n">Mat</span> <span class="n">mat</span><span class="p">;</span>
<span class="p">}</span> <span class="n">Holder</span><span class="p">;</span>
</code></pre></div></div>

<p>这意味着产品模板入口除了路径本身，还保存了偏移范围、搜索范围、是否可用、是否重定位以及重定位半径等信息。也就是说，系统并不是简单读入一张 <code class="language-plaintext highlighter-rouge">bmp</code> 然后“拿来比一比”，而是在产品级整体模板这一层就已经准备了后续定位所需的整体上下文。</p>

<p>从这个角度看，<code class="language-plaintext highlighter-rouge">XML</code> 保存的已经不是“模板文件路径 + 点坐标”这么简单，而是一整套关于对象如何表示、如何进入图像、如何被重定位的描述。</p>

<h2 id="七blockset系统真正运行的检测单元是从几何对象层展开出来的">七、blockSet：系统真正运行的检测单元，是从几何对象层展开出来的</h2>

<p>这份 <code class="language-plaintext highlighter-rouge">XML</code> 里还有一层特别关键，就是 <code class="language-plaintext highlighter-rouge">blockSet</code>。如果只看字段名，最容易把它理解成“当前产品有哪些 block 的清单”；但从更真实的系统意义上说，它并不是独立存在的一张列表，而是由前面的几何组织层进一步展开出来的 <strong>运行单元序列</strong>。</p>

<p>也就是说，<code class="language-plaintext highlighter-rouge">blockSet</code> 不是凭空定义“这一页有多少个检测块”，而是在 <code class="language-plaintext highlighter-rouge">globalGeo</code> 已经把局部对象组织出来之后，再把这些对象转成运行时真正参与配准、测量和结果输出的 <code class="language-plaintext highlighter-rouge">block</code>。普通 <code class="language-plaintext highlighter-rouge">Geo</code> 往往各自承接一个 <code class="language-plaintext highlighter-rouge">block</code>；而像 <code class="language-plaintext highlighter-rouge">sn</code> 这样包含多个字符槽位的对象，则会按字符位数拆成多个 <code class="language-plaintext highlighter-rouge">block</code>。这样一来，<code class="language-plaintext highlighter-rouge">blockSet</code> 的大小就不是随意决定的，而是和 <code class="language-plaintext highlighter-rouge">globalGeo</code> 中的对象组织严格相关。</p>

<p>这一点很能说明这套系统的层次感。它不是“整张图像里有什么就扫什么”，而是先在几何层上明确有哪些对象值得作为检测单元，再让这些对象进入 <code class="language-plaintext highlighter-rouge">block</code> 层成为真正的运行任务。于是，<code class="language-plaintext highlighter-rouge">block</code> 的意义就不只是代码实现上的一个局部小块，而是：</p>

<ul>
  <li>它承接了模板对象；</li>
  <li>承接了几何粗配准；</li>
  <li>承接了后续的局部模板落位；</li>
  <li>也承接了最终的缺陷测量与结果输出。</li>
</ul>

<p>这也解释了为什么系统天然适合并行。因为系统真正工作的，不是“整张图像里到底发生了什么”，而是“这些 block 中，每一个是否在自己的几何位置上以正确方式出现了”。一旦对象已经在几何层上被切分成这样的检测单元，后面的两层配准、局部测量和线程任务拆分才都会变得自然。</p>

<h2 id="八extvalue容错不是模糊地放大而是方向化编码">八、extValue：容错不是模糊地放大，而是方向化编码</h2>

<p>这份 <code class="language-plaintext highlighter-rouge">xml</code> 里还有一个很值得注意的设计，就是 <code class="language-plaintext highlighter-rouge">extValue</code>。它并不是简单地给 <code class="language-plaintext highlighter-rouge">block</code> 增加一个统一 <code class="language-plaintext highlighter-rouge">margin</code>，而是把扩展量编码成了四个方向上的显式规则：左、右、上、下分别由不同位上的数字决定，再统一乘以扩展倍数。这样做的意义很直接：前级提取出来的 <code class="language-plaintext highlighter-rouge">block</code> 在现场往往存在误差，而这种误差通常又不是均匀的，不同方向上的偏移风险并不相同。把容错写成方向化的几何规则之后，系统就不必把这类补偿零散地留给后续代码去处理，而是可以在模板描述层面提前吸收一部分定位误差。</p>

<p>这就是我说这份 <code class="language-plaintext highlighter-rouge">xml</code> 很有“系统之美”的原因。它不是靠一个神秘参数去赌运气，而是把容错做成了几何上可解释、工程上可维护的规则。这样一来，成功率的提升不是来自运气，而是来自设计。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/figure3_genus_extvalue_en_v2.png" alt="模板文件中不仅保存几何点集，还通过 genus 与 extValue 分别引入了拓扑先验和方向性容错。" /></p>

<p><strong>图 2：</strong> 模板文件中不仅保存几何点集，还通过 <code class="language-plaintext highlighter-rouge">genus</code> 与 <code class="language-plaintext highlighter-rouge">extValue</code> 分别引入了拓扑先验和方向性容错。</p>

<h2 id="九erasesettemplpathsettype-与-defectparam模板不是死的运行分支也不是单入口">九、eraseSet、templPathSet、type 与 defectParam：模板不是死的，运行分支也不是单入口</h2>

<p><code class="language-plaintext highlighter-rouge">eraseSet</code> 和 <code class="language-plaintext highlighter-rouge">templPathSet</code> 这两层，看起来像工程尾巴，其实很能体现这套系统的成熟度。前者说明模板不是死存储，而是允许做局部擦除和结构性屏蔽；后者则说明系统并不是只面对单一模板图，而是可以在运行时根据当前对象所对应的产品分支，进入不同的模板图入口。</p>

<p>这一点在完整 <code class="language-plaintext highlighter-rouge">XML</code> 里尤其清楚。<code class="language-plaintext highlighter-rouge">templPathSet</code> 中的不同 <code class="language-plaintext highlighter-rouge">product</code> 不只是挂着不同的 <code class="language-plaintext highlighter-rouge">bmp</code> 路径，它们还带有 <code class="language-plaintext highlighter-rouge">type</code>、<code class="language-plaintext highlighter-rouge">angle</code>、<code class="language-plaintext highlighter-rouge">rows</code>、<code class="language-plaintext highlighter-rouge">cols</code>、<code class="language-plaintext highlighter-rouge">xOffset</code>、<code class="language-plaintext highlighter-rouge">yOffset</code> 以及 <code class="language-plaintext highlighter-rouge">holder</code> 等信息。这里最关键的，并不是“有两张模板图”，而是：<strong>不同模板入口会进一步对应不同的几何对象集合和不同的 <code class="language-plaintext highlighter-rouge">block</code> 序列。</strong></p>

<p>也就是说，<code class="language-plaintext highlighter-rouge">type</code> 在这里不是普通分类字段，而是把几层东西绑成同一条运行分支：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">templPathSet / product@type</code> 决定当前进入哪一个模板图入口；</li>
  <li><code class="language-plaintext highlighter-rouge">globalGeo / Geo@type</code> 决定当前启用哪一组几何组织对象；</li>
  <li><code class="language-plaintext highlighter-rouge">blockSet / block@type</code> 决定当前激活哪一组运行检测单元。</li>
</ul>

<p>这件事非常重要。因为它说明系统并不是“同一张模板图上换一些参数”那么简单，而是在运行时已经能够根据当前产品面别或图档分支，进入不同的模板入口，并随之切换不同的几何组织与 <code class="language-plaintext highlighter-rouge">block</code> 集合。对于蓝牙耳机充电盒这类对象，这一点尤其有意义：正面和反面都可能存在镭雕，而同一条产线上的相机往往需要分两次拍摄。于是，算法面对的就不再是“固定一张模板图”，而是必须根据当前拍摄对象所属的面别，进入不同的 <code class="language-plaintext highlighter-rouge">product</code> 分支，并进一步带出不同的 <code class="language-plaintext highlighter-rouge">Geo</code> 组织和不同的 <code class="language-plaintext highlighter-rouge">block</code> 检测链。</p>

<p>与此同时，<code class="language-plaintext highlighter-rouge">angle</code> 这样的属性还说明系统在产品级入口层就已经考虑了旋转适配问题。也就是说，模板图入口并不是假定 <code class="language-plaintext highlighter-rouge">ROI</code> 永远是严格水平的矩形，而是允许在一定旋转条件下进入后续定位和检测链。这并不意味着系统对任意姿态完全不敏感，而是说明在产品级模板入口这一层，已经开始吸收实际 CCD 拍摄中不可避免的角度偏差。</p>

<p>而 <code class="language-plaintext highlighter-rouge">holder</code> 进一步说明，这里保存的也不只是路径本身，还包括整体模板的重定位与范围约束信息。换句话说，<code class="language-plaintext highlighter-rouge">templPathSet</code> 不是在“给后面找一张参考图”，而是在定义：<strong>当前产品分支应该从哪一个整体模板入口开始、以什么范围与什么约束进入后续检测链。</strong></p>

<p>完整 <code class="language-plaintext highlighter-rouge">XML</code> 里进一步出现的 <code class="language-plaintext highlighter-rouge">defectParam</code>，则更说明这份文件不只是“模板清单”，而是已经开始承接一部分更上层的检测语义。比如 <code class="language-plaintext highlighter-rouge">type="skewing"</code> 这样的字段，就已经在告诉系统：除了局部结构模板和定位锚点之外，还有某些整体层面的缺陷参数也会在这里被外部描述。</p>

<p>这几层叠加在一起，就使得 <code class="language-plaintext highlighter-rouge">XML</code> 不再只是“模板文件”，而更接近一份真正的 <strong>产品几何描述文件</strong>：它既告诉系统“你要看什么”，也告诉系统“你不用看什么”；既告诉系统“当前应从哪一个模板入口进入”，也告诉系统“这一入口下有哪些几何对象和哪些 block 应该被激活”；甚至还开始承接某些更高层的检测参数。</p>

<h2 id="十所以这份-xml-真正保存的是系统对检测对象的结构理解">十、所以，这份 XML 真正保存的，是系统对检测对象的结构理解</h2>

<p>如果一定要用一句话概括这份 <code class="language-plaintext highlighter-rouge">XML</code>，我不会说它是“检测配置文件”，也不会说它只是“模板文件”。更准确的说法是：</p>

<blockquote>
  <p>它是一份把字符库、最小配准单元、注册/测量点角色、block 的几何组织、拓扑先验、方向容错以及产品模板运行分支统一到一起的几何描述文件。</p>
</blockquote>

<p>这句话听起来有点长，但它比“配置文件”接近真实得多。因为真正支撑这套系统跨国别、跨产品、跨产线复用的，恰恰不是不断改代码，而是这种把变化压缩进模板描述层、几何组织层和运行分支层的能力。</p>

<p>从这份 <code class="language-plaintext highlighter-rouge">XML</code> 可以看出，这套系统并没有把关键判断零散地埋在代码里，而是把检测对象的组织方式、配准与测量的角色分工，以及若干灵敏度—鲁棒性平衡，尽量外化到了同一套模板描述层中。</p>

<p>下一讲将进入这套系统最核心的几何机制：两次配准如何工作，注册集与测量集为什么要同步推进，以及配准为何只是后续缺陷测量的几何基础。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Why Curved-Surface Pattern Inspection Systems Can Be Reused Across Products: Templates, Geometric Organization, Topology, and Runtime Branching in One XML File]]></summary></entry><entry><title type="html">第二讲：曲面 Pattern 缺陷检测的本质：结构化归一化与缺陷测量</title><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/30/why-curved-surface-pattern-inspection-is-not-ordinary-character-recognition.html" rel="alternate" type="text/html" title="第二讲：曲面 Pattern 缺陷检测的本质：结构化归一化与缺陷测量" /><published>2026-03-30T08:00:00+08:00</published><updated>2026-03-30T08:00:00+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/30/why-curved-surface-pattern-inspection-is-not-ordinary-character-recognition</id><content type="html" xml:base="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/30/why-curved-surface-pattern-inspection-is-not-ordinary-character-recognition.html"><![CDATA[<h1 id="the-nature-of-curved-surface-pattern-inspection-structural-normalization-and-defect-measurement">The Nature of Curved-Surface Pattern Inspection: Structural Normalization and Defect Measurement</h1>

<blockquote>
  <p><strong>English Abstract</strong><br />
This article argues that curved-surface pattern inspection should not be reduced to ordinary character recognition, simple template matching, or generic defect classification. Its core lies in structural normalization and defect measurement over structured patterns under geometric deformation, imaging disturbance, process variation, and changing business inputs. What matters is not merely recognizing isolated characters, but establishing a stable and comparable relation between the expected pattern and the measured one, and then performing defect measurement on top of that relation. From this perspective, the system is better understood as a structured pattern inspection system rather than a conventional character inspection pipeline.</p>
</blockquote>

<p>很多人第一次接触曲面 Pattern 缺陷检测时，往往会自然把它理解为字符识别、模板匹配，或者某种缺陷分类问题。但在我当时设计这套系统时，问题的核心并不在那里。我一开始就很清楚，这件事真正困难的，不是把字符“看出来”，而是如何在产品、图档、业务输入和现场条件不断变化的情况下，仍然建立起稳定、可比较、可实施的检测关系。也正因为如此，这套系统从设计之初就不是围绕“识别字符”展开的，而是围绕结构化归一化与缺陷测量展开的。</p>

<p>如果只从抽象层面谈“曲面形变”和“产品差异”，读者未必能立刻感受到这类问题为什么会把普通字符检测思路彻底推翻。更直观的方式，是先看两张真实现场 CCD 采集图。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/inspection_problem_overview_real_ccd_v2.png" alt="Two real-world sources of difficulty in curved-surface pattern inspection" /></p>

<p><em>图 1：曲面 Pattern 检测中两类最基本、也最容易被低估的难点。左图展示了曲面成像带来的明显形变；右图展示了不同国别 Pattern 之间的巨大差异。真正关键的并不是“变化很多”，而是这些变化不能每来一次都回到核心程序里重写。对这套系统而言，耳机与充电盒、不同国别乃至不同产品对象之间的切换，原则上应通过图档、XML、INI 与运行配置层完成，而不是频繁改动 DLL 本身。这也正是它能够走向产品化和跨场景推广的前提。</em></p>

<p>从这两张图其实就能看出，这个问题一开始就不是“把某几个字符认出来”那么简单。左图说明，同样的结构对象一旦落到曲面成像条件下，局部形态就会发生明显变化；右图则说明，系统面对的并不是单一版式，而是大量国别 Pattern、图样组合乃至产品对象本身的变化。</p>

<p>真正困难的地方并不只是“变化很多”，而是：这些变化不能每来一次就回到核心程序里重写一遍。对一个真正想走向产品化的工业检测系统来说，核心 DLL 及其软件主架构必须尽可能保持稳定；产品切换、国别切换、产线切换乃至面别分支，原则上都应通过图档、XML、INI 以及相关运行配置层来完成，而不是不断回灌到核心代码里。换句话说，这套系统真正有价值的地方，不是“同一套代码勉强还能适配不同对象”，而是它从一开始就试图把变化压缩到外部描述层中，把 DLL 的改动降到例外，而不是常态。</p>

<p>也就是说，这套系统既要处理曲面带来的成像与几何变化，又要把不同国别、不同图档、不同产品对象所带来的差异，尽可能吸收到外部模板与配置体系中。这正是为什么后面必须引入模板、配准、参数链和配置链，而不能把它理解成普通字符检测。因为普通字符检测的思路默认对象本身相对单纯，变化主要落在识别层；而在这里，变化本身已经进入了系统组织层，必须被一种更稳定的结构化方式接住。</p>

<p>从这个角度看，曲面 Pattern 检测首先不是一个读取字符类别的问题，而是一个建立结构对应关系的问题。系统真正要回答的是：当前图像中本来应该出现什么 Pattern，实际出现了什么；它们之间哪些差异只是正常的几何形变、成像扰动或工艺波动，哪些差异才构成真正的缺陷；而在包含实时 SN code 的场景里，图像中的字符内容还必须与当前业务侧输入保持一致。也就是说，系统面对的从来不是一张孤立的字符图像，而是一个会随着产品、图档、业务输入和现场条件共同变化的检测对象。</p>

<p>只要这一点想清楚，后面很多设计就会自然变得顺理成章：为什么图档本身必须兼顾检测，为什么模板不能只是参考图片，为什么系统里会有 block、骨架/特征点、注册集与测量集，以及为什么不同国别、不同产品、不同产线之间的切换，应当优先通过 XML、INI、图档与运行分支来完成，而不是不断改动 DLL 本身。对这类系统而言，真正的产品化并不是“同一套代码大致还能用”，而是：新变化优先进入外部描述层，而不是重新进入 DLL。</p>

<p>如果说前一组现场 CCD 图说明了这类问题为什么天然复杂，那么下面这组真实产线检测结果图则更直接地说明：系统最终面对的，并不是“字符整体是否存在”，而是字符或图样内部那些尺度很小、却又确实存在的局部异常。下面这组图都来自现场上位机保存的检测结果，红色框标出了系统实际判定出的缺陷区域。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/inspection_defect_detection_examples.png" alt="Typical defect examples from curved-surface pattern inspection" /></p>

<p><em>图 2：真实产线中的几类典型缺陷检测结果，包括镭断（laser break）、多镭（excess laser）、色浅（shallow engraving）和色深（deep engraving）。图中的红色框为系统实际检出的缺陷区域。尤其左上角那处镭断缺陷及其检测框都非常微小，这也说明这类问题并不是粗粒度的字符识别，而是面向细微局部异常的工业检测任务。</em></p>

<p>从这组图其实就能看出，系统要处理的并不是“字符整体是否存在”这种简单判断，而是字符或图样内部某一小段结构是否断裂、某一小块区域是否多镭、局部颜色或深浅是否偏离正常状态。换句话说，检测对象表面上是字符或 Pattern，真正需要被判别的却是它们内部的局部几何与局部成像异常。</p>

<p>所以，如果让我用一句更接近问题本体的话来概括它，我会说：曲面 Pattern 缺陷检测的关键，不在于把字符图像单独拿出来做识别，而在于先对结构化 Pattern 建立可比较关系，再在这个关系上做缺陷测量。只有这样理解，后面关于骨架模板、参数体系、两次配准、实时编码输入、插件接口以及系统节拍的讨论，才会真正连成一个整体。否则，这个项目就很容易被轻描淡写地概括成“做了一个字符检测算法”，而它最有价值的部分，恰恰会在这种概括里被抹平。</p>

<h2 id="一它不是把字符认出来而是先把结构关系建立起来">一、它不是把字符“认出来”，而是先把结构关系建立起来</h2>

<p>普通字符识别最关心的是类别判断：这是不是某个字母，这是不是某串字符，这段内容到底是什么。而我当时做这套系统时，关注点从一开始就不在那里。对产线检测来说，更重要的问题是：这个位置本来应该有什么结构，这个结构在当前图像里是否以正确的方式出现了，它和应有结构之间的偏离是正常波动，还是已经构成缺陷。也就是说，系统面对的不是一个“读字”问题，而是一个“应有结构与实测结构之间如何建立比较关系”的问题。</p>

<p>这也是为什么单纯把它说成 OCR，会一下子把问题说浅。因为真正困难的部分并不在字符类别本身，而在于字符只是承载 pattern 的一种常见形态。项目里真正要处理的，是一类具有内部几何组织、局部笔画结构、拓扑关系和可测量特征的 pattern。字符只是最常见、也最典型的一类对象，但不是问题的全部。</p>

<h2 id="二它也不只是模板匹配因为模板本身就是检测系统的一部分">二、它也不只是模板匹配，因为模板本身就是检测系统的一部分</h2>

<p>如果说 OCR 离这个问题太远，那么模板匹配似乎近一点。毕竟这套系统里确实有图档、有模板、有对位、有匹配。但如果把它简单理解成“拿模板去和图像比一比”，仍然是不够的。</p>

<p>因为我当时设计的模板，并不是一张单纯拿来参考的图片，而是整个检测系统的一部分。图档本身就必须兼顾检测；xml 也不是为了存一张视觉参考图，而是为了存储可用于定位、注册和测量的结构对象。最常见的模板之一，就是 medial axis，也就是骨架点集。但这里的 skeleton 又不必被理解得太死，它在工程上更像是一类可用于注册与测量的结构特征点集。也正因此，模板不是静态附属物，而是后续整个检测过程的几何基准。</p>

<p>从这个角度看，模板的意义已经和普通模板匹配完全不同了。它不只是“像不像”的参照物，而是一个要被带入现场、带入参数体系、带入实施流程的结构载体。它不仅决定能不能检，还决定能不能导入、能不能交付、能不能在不同产品和不同国别之间平稳切换。</p>

<h2 id="三真正的问题主线是归一化对应注册测量而不是直接判缺陷">三、真正的问题主线，是归一化、对应、注册、测量，而不是直接判缺陷</h2>

<p>很多视觉问题容易给人一种错觉，好像缺陷检测的核心就是“最后怎么判”。但对这个项目来说，我从一开始就知道，真正的主线不在最后那一下判别，而在更前面的几步：先把对象分成合理的 block，先建立 block 与 block 的对应关系，再把模板中的结构点带到实测图像里，最后才沿着这些结构点去做局部测量和缺陷判别。</p>

<p>换句话说，缺陷检测不是起点，而是终点。前面的归一化和对应关系没有建立起来，后面的镭断、色异常、多镭、偏移、码数量异常这些判断都会失去基础。很多人看项目时会先盯着“最终是怎么判 NG 的”，但真正把系统做起来的时候，决定成败的恰恰是前面的几何准备阶段。</p>

<p>这也是为什么系统里会自然地出现 block、Geo、skeleton、注册集、测量集以及两次配准。它们不是为了把系统写复杂，而是因为只有把这些层次建立起来，后面的检测才真正有了可比性。</p>

<h2 id="四实时-sn-code-的存在使它从纯视觉问题变成了视觉与业务流耦合的问题">四、实时 SN code 的存在，使它从纯视觉问题变成了视觉与业务流耦合的问题</h2>

<p>这个项目里还有一个特别容易被低估的点，就是实时 SN code 的接入。只要场景里涉及动态生成的编码内容，问题就不再是“拿静态模板和静态图像做比较”这么简单了。系统必须同时知道两件事：当前业务流程要求出现什么内容，图像里实际又出现了什么内容。只有这两者对应上，后面的缺陷判断才成立。</p>

<p>也就是说，系统不只是看图，还要接收业务侧的实时输入，并把它转成当前应有的 pattern 结构，再和实测图像建立对应关系。到这一步，它就已经不再是一个单纯的图像处理模块，而是一套视觉、编码信息、模板系统和上位机流程共同耦合的工业系统。</p>

<p>正因为如此，这类项目的门槛其实并不只在图像算法本身，而在于你能不能把视觉对象、业务输入和工程接口同时组织起来。很多看上去“也会做视觉”的团队，一旦碰到这一步，复杂度就会迅速上升。</p>

<h2 id="五这套系统真正难的地方是把变化压缩到少量可控的外部对象里">五、这套系统真正难的地方，是把变化压缩到少量可控的外部对象里</h2>

<p>如果这只是一个单次 demo，那么最直接的做法当然是：换一个产品就改一次代码，换一个图档就重新调一次流程，换一个国别就重新写一套逻辑。但我当时的设计思路不是这样。从设计阶段开始，我追求的就是让内核稳定，让变化尽量收敛到少量可控对象里。</p>

<p>所以最后形成的形态才会是：核心 DLL 尽量不动，主要通过图档、xml 模板和 ini 参数去承接不同产品、不同国别、不同产线带来的变化。今天回头看，这其实正是这套系统真正高级的地方。因为一个系统是否成熟，很多时候不在于内部写得多复杂，而在于你能不能把外部世界的复杂性压缩进几个清晰、可替换、可维护的对象里。</p>

<p>这件事说起来很朴素，但真正做起来并不容易。因为只有当你在设计时就抓住了问题本体，知道哪些东西应该固化进内核，哪些东西应该留在模板层和参数层，系统才可能成长出这样的结构。</p>

<h2 id="六所以它更准确的名字应该是结构化-pattern-缺陷检测系统">六、所以，它更准确的名字，应该是“结构化 Pattern 缺陷检测系统”</h2>

<p>如果一定要给这类问题一个更准确的名字，我不会把它叫作字符识别，也不会把它简单叫作模板匹配或缺陷分类。我更愿意把它理解为一套面向曲面/平面结构化 Pattern 的归一化、注册、测量与判别系统。</p>

<p>这里面最重要的词其实不是“检测”，而是“结构化”。因为一旦把对象理解成结构对象，而不是孤立图像，后面的很多东西就都会回到正确位置：为什么图档必须兼顾检测，为什么模板要保存骨架或特征点，为什么需要注册集和测量集，为什么要做两次配准，为什么系统最后会长成一套模板、参数、接口和节拍共同组织起来的工业框架。</p>

<p>从这个意义上说，这个项目最怕的，不是技术难，而是从一开始就把名字叫错。一旦把它理解成普通字符检测，后面的很多设计就都会显得过重、过繁、甚至莫名其妙；但如果把它理解成结构化 Pattern 的归一化与测量问题，那么整套系统的逻辑反而会变得非常自然。</p>

<p>下一讲，我会继续写这套系统真正的骨架：图档、block、Geo、skeleton、注册集、测量集，以及两次配准为什么能把一个工业检测系统真正撑起来。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The Nature of Curved-Surface Pattern Inspection: Structural Normalization and Defect Measurement]]></summary></entry><entry><title type="html">总述：为什么我要重新讲清这套曲面 Pattern 缺陷检测系统</title><link href="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/29/why-i-am-rewriting-this-curved-surface-pattern-inspection-system.html" rel="alternate" type="text/html" title="总述：为什么我要重新讲清这套曲面 Pattern 缺陷检测系统" /><published>2026-03-29T12:00:00+08:00</published><updated>2026-03-29T12:00:00+08:00</updated><id>https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/29/why-i-am-rewriting-this-curved-surface-pattern-inspection-system</id><content type="html" xml:base="https://monge-ampere.github.io/curved-surface-pattern-defect-inspection/2026/03/29/why-i-am-rewriting-this-curved-surface-pattern-inspection-system.html"><![CDATA[<h1 id="overview-why-i-want-to-reconstruct-this-curved-surface-pattern-inspection-system">Overview: Why I Want to Reconstruct This Curved-Surface Pattern Inspection System</h1>

<blockquote>
  <p><strong>English Abstract</strong><br />
This article explains why I decided to systematically rewrite and preserve a curved-surface pattern inspection system that had once formed a complete industrial technical loop, but was later described in fragmented and distorted ways. The point of this series is not emotional retrospection, but to reconstruct the system as a coherent technical object: its problem definition, structural logic, engineering abstraction, and reproducible design philosophy.</p>
</blockquote>

<p>这些年我做过不少算法项目，但真正让我一直想重新写清楚的，并不多，这套曲面 Pattern 缺陷检测系统算是其中之一。</p>

<p>一方面，它曾在真实工业场景中形成过完整而有效的技术闭环；另一方面，它后续在某些并非出自我本人、也未充分反映真实技术贡献的成果呈现与知识产权文本中，却被以一种失真、凌乱、甚至难以复现的方式重新表述了。与其让一个本来具有明确问题主线、方法结构和工程逻辑的系统继续以错误文本流传下去，不如由我自己把它重新讲清楚。</p>

<p>写下这一组文章，不是为了争论谁说了什么，也不是为了做情绪化的追责，而是希望把这套算法系统以一种更规范、更有灵魂、也更能帮助后续研发者理解与复现的方式保留下来。</p>

<hr />

<h2 id="一这不是一套普通的字符检测算法">一、这不是一套普通的“字符检测算法”</h2>

<p>如果只看表面描述，很多人会把它理解成一个“镭雕字符缺陷检测”项目，甚至进一步把它理解为某种 OCR、模板匹配，或者缺陷分类问题。</p>

<p>但在我看来，这样的理解过于表层。</p>

<p>为了让这个问题先落到一个更直观的层面，不妨先看几类真实产线中的代表性缺陷。下面这张图并不是算法示意图，而是现场检测中实际会遇到的局部异常：包括镭断、多镭和色异常。它们看上去都只是局部结构上的小偏差，但真正困难的地方恰恰在于：系统必须先区分这些偏差究竟来自正常成像波动、曲面形变与工艺扰动，还是已经构成了需要输出的真实缺陷。</p>

<p><img src="/curved-surface-pattern-defect-inspection/assets/images/representative_defect_examples.png" alt="代表性缺陷示意图" /></p>

<p><em>图 1：真实产线样本中的三类代表性局部缺陷示意，包括镭断、多镭与色异常。为避免暴露客户与业务信息，图中仅保留与缺陷相关的局部区域。</em></p>

<p>这套系统真正面对的问题，并不是“字符识别”本身，而是：</p>

<blockquote>
  <p><strong>如何在光滑曲面上的结构化 Pattern 存在形变、成像扰动、工艺波动和产品差异的条件下，建立一套可归一化、可比较、可配置、可扩展的缺陷检测系统。</strong></p>
</blockquote>

<p>这和普通平面字符检测不是一个难度层级的问题。</p>

<p>平面上的字符检测，很多时候只需要解决成像质量、局部噪声、对位偏差等问题；而曲面上的 Pattern 检测，首先就要面对一个更根本的事实：<strong>对象的几何形态本身会参与成像与检测。</strong></p>

<p>也就是说，图像中的局部差异——哪怕已经表现成镭断、多镭或色异常这样的可见现象——也并不天然等于“缺陷”。</p>

<p>它可能来自：</p>

<ul>
  <li>曲面带来的几何形变；</li>
  <li>不同产品或不同国别图档带来的结构差异；</li>
  <li>光照、焦距、姿态、夹具、上下料过程带来的成像扰动；</li>
  <li>工艺侧带胶、填充不均、边缘毛刺等复杂因素；</li>
  <li>以及节拍、参数配置、模板迁移所带来的工程约束。</li>
</ul>

<p>所以，这个问题如果处理得太浅，很容易退化成“规则越写越多，阈值越调越碎”的局面。<br />
而一旦走到那一步，系统基本就失去生命力了。</p>

<hr />

<h2 id="二我为什么一直觉得这件事值得单独写出来">二、我为什么一直觉得这件事值得单独写出来</h2>

<p>原因很简单：这套系统真正有价值的地方，并不只在于它“做成了”，而在于它体现了一种我始终非常看重的算法路线。</p>

<p>我一直更认同一种结构化的思路：<br />
面对复杂工业问题时，首先不是去堆砌模块，也不是急着引入流行名词，而是先抓住问题本身的结构，把对象抽象对，把归一化机制搭对，把可比较的中间表示建立起来，然后再围绕它生长出模板、参数、判别逻辑和工程系统。</p>

<p>这套曲面 Pattern 缺陷检测系统，正是这种思路在工业场景中的一次集中体现。</p>

<p>它不是某个孤立算子的成功，不是某种训练技巧的偶然有效，也不是单条产线调通后的经验堆叠。它真正的价值在于：</p>

<ul>
  <li>它抓住了问题本体；</li>
  <li>它建立了稳定的技术主线；</li>
  <li>它形成了可配置、可迁移、可实施的系统能力；</li>
  <li>它在真实产线环境中得到了验证。</li>
</ul>

<p>而这些东西，恰恰最容易在后续转述中被写丢。</p>

<hr />

<h2 id="三为什么我要用规范重述这个说法">三、为什么我要用“规范重述”这个说法</h2>

<p>这组文章虽然分篇展开，但在我心里，它们其实都在做同一件事：</p>

<blockquote>
  <p><strong>把这套曲面 Pattern 缺陷检测系统，重新讲回来。</strong></p>
</blockquote>

<p>所以我后来才越来越觉得，“规范重述”这个说法是合适的。</p>

<p>这里的“规范”，并不是为了显得书面，也不是为了把事情说得更正式一点，而是因为我慢慢意识到：<strong>一个真正做成过的工业算法系统，如果后来没有被用合适的结构重新讲清楚，它就很容易在后续转述里被讲窄、讲散、讲偏。</strong></p>

<p>有的表述会把它说成一个表层问题，仿佛它不过是在“检测字符缺陷”；<br />
有的表述会把模块摊开来写，什么都有一点，但真正的主线反而不见了；<br />
还有的表述会把枝节当主干，把真正有价值的部分讲没，最后留下来的东西甚至已经不值得参考。</p>

<p>从技术角度看，这其实是很可惜的。</p>

<p>因为算法系统和成品代码不一样。<br />
代码会迭代，工程会下线，人员会变动，但只要问题主线、方法结构和系统逻辑还被准确保留下来，这个系统就没有真的死。<br />
反过来，如果留下来的叙述本身就是错的，那么哪怕表面上还剩一堆名词、流程图和模块说明，也未必真的把那个系统留下来了。</p>

<p>所以我写这一组文章，并不是为了把过去重复一遍，也不是为了做情绪化的追溯，而是想做一件更具体的事：</p>

<blockquote>
  <p><strong>把一套后来被讲偏了的工业算法系统，重新建立为一个可以被理解、被传递、也值得被后来者参考的技术对象。</strong></p>
</blockquote>

<hr />

<h2 id="四这套系统真正难的地方到底在哪里">四、这套系统真正难的地方到底在哪里</h2>

<p>这类问题之所以难，并不是因为缺陷种类多，也不只是因为现场环境复杂，而是因为几个层面的困难会同时成立。</p>

<h3 id="1-几何层面的困难">1. 几何层面的困难</h3>

<p>Pattern 位于曲面上，成像中天然会带入几何变形。<br />
这意味着检测前必须先解决某种意义上的结构归一化问题，否则后面的差异比较很容易把正常形变误判成缺陷。</p>

<h3 id="2-表示层面的困难">2. 表示层面的困难</h3>

<p>工业检测里最忌讳的一件事，就是直接在原始图像上硬判。<br />
因为原始图像同时混杂了产品结构、成像扰动、工艺波动和偶然噪声。<br />
如果没有一个足够稳定的中间表示，后面的阈值、模板和规则都会越来越脆。</p>

<h3 id="3-系统层面的困难">3. 系统层面的困难</h3>

<p>一个算法能不能在单次实验里跑通，和它能不能在多产品、多图档、多国别、多产线条件下持续工作，是两回事。<br />
真正的系统一定要回答：</p>

<ul>
  <li>模板怎么建立？</li>
  <li>参数怎么继承？</li>
  <li>新产品怎么导入？</li>
  <li>谁来维护？</li>
  <li>如何让一线工程师接得住？</li>
  <li>如何在节拍要求下保持稳定？</li>
</ul>

<h3 id="4-工艺耦合层面的困难">4. 工艺耦合层面的困难</h3>

<p>很多所谓“缺陷”，其实并不是纯粹的视觉问题，而是工艺状态通过图像投影出来的结果。<br />
带胶、填充不均、边缘不完整、色差异常，这些都可能让检测边界变得非常微妙。<br />
如果算法设计者不了解工艺约束，就很容易把问题做偏。</p>

<p>也正因为如此，我始终认为，这类项目真正难的不是“有没有一个分类器”，而是：</p>

<blockquote>
  <p><strong>能不能把几何、表示、工艺、参数化和工程实施这些层面，统一到同一套方法主线里。</strong></p>
</blockquote>

<hr />

<h2 id="五它为什么不是再加一点深度学习就能讲清楚的系统">五、它为什么不是“再加一点深度学习”就能讲清楚的系统</h2>

<p>过去这些年，工业视觉里很容易出现一种叙述惯性：<br />
仿佛一个项目只要加上了深度学习模块，就显得更先进，也更容易被人理解。</p>

<p>但对这套系统来说，至少在我自己的设计视角里，深度学习从来不是主干。</p>

<p>真正的主干始终是：</p>

<ul>
  <li>对检测对象的结构化理解；</li>
  <li>对曲面形变的归一化处理；</li>
  <li>对模板与参数体系的建立；</li>
  <li>对局部异常与正常波动之间边界的刻画；</li>
  <li>对整套系统可实施性的组织。</li>
</ul>

<p>如果这些东西不成立，那么后面即使再挂上分类器、分割器或者神经网络，也只是在一个没有骨架的体系上继续加零件。</p>

<p>而如果这些东西已经成立，那么深度学习最多也只是某些边界场景下的补充工具，而不是这个系统真正的灵魂所在。</p>

<p>我并不反对深度学习。<br />
我反对的是：<strong>用一个附属模块去替代问题本体的叙述。</strong></p>

<hr />

<h2 id="六为什么这件事在今天仍然值得写">六、为什么这件事在今天仍然值得写</h2>

<p>从时间上说，这已经不是一个刚结束的新项目。<br />
但越往后，我越觉得它值得写下来，而且应该认真写。</p>

<p>原因并不只是“它当年做得不错”，而是它背后包含的几件事，在今天仍然很有代表性：</p>

<ul>
  <li>工业界仍然大量存在“把复杂问题说浅”的倾向；</li>
  <li>很多项目能跑，却没有形成真正可传承的技术表述；</li>
  <li>很多系统明明有结构，却在汇报和交接中被写成了模块拼装；</li>
  <li>很多研发者做了真正难的事情，却没有给自己留下足够准确的技术记录。</li>
</ul>

<p>把这些文章一篇篇写下来，其实也是在反过来提醒我自己：</p>

<blockquote>
  <p><strong>真正做过、做成过的系统，不应该只以零散记忆、项目摘要或失真文本的形式存在。</strong></p>
</blockquote>

<p>它值得被重新组织成一个更清楚的技术对象。</p>

<hr />

<h2 id="七这一系列文章准备写什么">七、这一系列文章准备写什么</h2>

<p>这不是一篇讲完所有细节的文章。<br />
这一篇只是总述，主要回答一个问题：</p>

<blockquote>
  <p><strong>为什么我要重新讲清这套曲面 Pattern 缺陷检测系统。</strong></p>
</blockquote>

<p>后面的文章，我会继续围绕下面几个方面展开：</p>

<h3 id="1-这到底是一个什么问题">1. 这到底是一个什么问题</h3>
<p>为什么它不是普通字符检测，也不是简单模板匹配。</p>

<h3 id="2-这套系统真正的技术主线是什么">2. 这套系统真正的技术主线是什么</h3>
<p>从曲面形变、结构归一化到参数化模板机制，哪些是主干，哪些只是附属。</p>

<h3 id="3-为什么工业项目难的不是做出算法而是做成系统">3. 为什么工业项目难的不是“做出算法”，而是“做成系统”</h3>
<p>包括模板创建、参数管理、实施交付、节拍与稳定性这些常被忽视的部分。</p>

<h3 id="4-这类系统里最容易被误解的几个点">4. 这类系统里最容易被误解的几个点</h3>
<p>例如深度学习的位置、归一化的重要性、可复现性的问题，以及为什么一些表述看起来很热闹，却失去了真正的灵魂。</p>

<p>如果这些内容最后能对后来做相关工作的研发者有一点帮助，那我写这个系列就有意义。</p>

<hr />

<h2 id="八这一篇的结尾把系统重新还给问题本身">八、这一篇的结尾：把系统重新还给问题本身</h2>

<p>我想用一句很简单的话，作为这篇总述的结尾：</p>

<p><strong>一套真正有价值的工业算法系统，不应该只留下“做过”的痕迹，更应该留下“为什么这样做、它真正依赖什么结构、它为何能够成立”的清晰表达。</strong></p>

<p>这套曲面 Pattern 缺陷检测系统，对我来说正是这样一个对象。</p>

<p>它不该被草率地理解成一个字符检测项目，<br />
也不该被错误地表述成几个模块和若干名词的拼接，<br />
更不该在失真的叙述中失去自己原本的技术灵魂。</p>

<p>所以我决定重新讲它。</p>

<p>这，就是这个系列的起点。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Overview: Why I Want to Reconstruct This Curved-Surface Pattern Inspection System]]></summary></entry></feed>