Bloom-Counter 使用 HSV 分割和轮廓分析统计花朵与花苞,但模拟器的颜色专门适配检测阈值,且排除了目标重叠。文章据此质疑其 99.7% 召回率,并强调总数正确不代表分类结构可靠。
作物计数 pipeline 交付的不是一个数字。为了估算产量、安排用工,种植者需要两个数字:已经开放的花有多少,尚未开放的花蕾有多少。前者决定本周的采收,后者决定下周的采收。真正需要交付的是两者的分类计数,而不是总和。
我的开源项目 Bloom-Counter 做的正是这件事:输入照片,通过 HSV 颜色分割和轮廓分析,输出每张图像中的开花数与花蕾数,再按区域和日期汇总。它的 README 上有一组漂亮的成绩:6 张模拟照片、357 朵花、99.7% 的检测召回率。本文从一个令人不太舒服的判断开始:这个 99.7% 证明不了什么。原因有两个,都摆在代码里,任何人都可以核查。
首先,模拟器和检测器是相互配合着调出来的。simulator/generate.py 第 23 行的注释说得很直白:BGR 格式的花朵颜色“经过调整,以落入检测器的 HSV 花瓣颜色范围”。生成器按检测器的偏好画花,检测器再去数这些花。满分是由这种设计决定的。
其次,模拟器的 _place 函数使用拒绝采样,保证任意两朵花都不会重叠;而 examples/demo.py 中的演示评分方式是 (detected blooms + buds) / (true blooms + buds),只看总数,没有逐对象匹配,也没有按类别分别统计。
真实温室恰好会打破这两个假设:光照在一天中不断变化,叶片遮挡是常态,花朵也会成簇生长。因此,我没有改动检测器,而是系统性地打破模拟器中这两个隐含假设,测量它先在哪里失效、失效有多严重,以及两种低成本补救措施是否真的有效。
检测器:使用原版 ColorSegmentationDetector(detector/color_segmentation.py),参数保持不变:min_area=120 px²、bloom_area_min=900 px²(连通分量的面积达到或超过 900 px² 就算开花,低于该值则算花蕾),形态学核大小为 7。它在 HSV 中对粉色、白色和黄色花瓣范围做阈值分割,执行闭运算和开运算,再对连通分量计数。
场景:使用仓库自带模拟器生成的四张 1280×960 图像,随机种子为 101/202/303/404,共 210 个对象:134 朵已开放的花、76 个花蕾,每个对象都有真实标注的中心和半径。
评分:不使用总数比值,而是采用基于中心距离的贪心匹配。只有检测结果的中心与同类别真实对象中心之间的距离不超过 max(38 px, 1.1 × radius),才算命中。由此计算精确率、召回率和 F1,并分别报告开花召回率与花蕾召回率,四个场景采用微平均。先做合理性检查:在未经修改的场景上,这套更严格的评分依然得到精确率 = 召回率 = F1 = 1.000,误报为零。基线之所以完美,是因为生成器与检测器经过配套调优,而这正是问题所在。
光照:通过对整张图像施加参数化变换,模拟不同时段和天气:gamma 0.6 / 1.6 / 2.2;曝光降低 −50(α=0.78,β=−50)和过曝 +60(α=1.25,β=+60);从左到右的阴影渐变(0.35→1.0);半幅阴影(左侧 45% 区域乘以 0.40);暖色偏移(B×0.72,R×1.18);以及低光照(gamma 1.9 加上 σ=12 的高斯噪声)。
遮挡:在 60% 的对象真实中心位置覆盖叶片颜色的椭圆,半径分别为对象半径的 0.5 / 0.8 / 1.1 倍;另外,在已有花朵旁边,按半径的 2.2 / 1.6 / 1.2 / 0.8 / 0.45 倍偏移绘制额外的已开放花朵,每个场景增加 10 朵,使真实开花总数增至 174,以模拟成簇生长。
两种补救措施:一是预处理,先做灰度世界白平衡,再在 V 通道上应用 CLAHE;二是基于面积的拆分估算器,对于面积远大于单朵开花包围框面积中位数的开花连通分量,按面积比例增加计数。本批次的面积中位数为 5,889.5 px²。
原始数据和脚本:data/2026-10-17-bloom-bench.json 和 data/2026-10-17-bloom-bench.py,在仓库的 virtualenv 中用一条命令即可复现。
看看曝光降低 −50 和两种阴影条件:总数误差都是 0.0%。按仓库演示程序的评分方式,这三次运行全都完美。然而,已有六分之一到四分之一的开花消失,还冒出了二十多个并不存在的花蕾。把第一个半幅阴影场景分区来看,问题就很直观了:阴影中的 20 个对象只找回了 70%,而亮处的 28 个对象全部找回。
机制很容易追踪。变暗首先会从边缘开始侵蚀花瓣 mask:V 值跌破粉色范围的下限,mask 缩小,连通分量的面积也随之缩小。一旦面积跌破 900 px² 这条线,对象并不会消失,而是被重新分类为花蕾。这个分类器会先降级,再删除:当 gamma 为 2.2 时,只剩 45 朵开花,而“花蕾”膨胀到 179 个,总数误差却只有 +6.7%。
暖色偏移的失效方向恰好相反。开花完整保留下来,白色和黄色范围提供了兜底;但花蕾召回率恰好降到了零:花蕾只存在于粉色这一个颜色范围内,色偏把它们的色相推到了范围之外。全部 76 个花蕾都消失了,检测出的 25 个“花蕾”则都是碎片造成的误报。
过曝又是另一种失效方式:召回率维持在 1.000,但精确率降至 0.933,出现了 15 个误报,是被冲淡颜色的叶片碎片冒充了花蕾。变亮没有让对象丢失,而是制造了对象。
好消息也确实存在:gamma 0.6 的提亮、最高到 gamma 1.6 的变暗,以及带噪声的低光照,都没有影响检测器。它能稳定工作的范围比我预期的更宽。失效阈值位于结果完好的 1.9 和结果崩溃的 2.2 之间,但采样还很稀疏,而且 1.9 那次运行也加入了噪声,并非纯 gamma 对照,因此我无法确定断崖究竟在哪里。
灰度世界白平衡加上 V 通道 CLAHE 的效果,按失效类型呈现出清晰的分界。下文误报数均为四个场景的合计:
对于全局亮度和色相偏移,这条 pipeline 几乎能找回所有对象。但它并非没有代价:在原本表现正常的图像上,它会凭空制造 13–18 个花蕾。对于过曝图像,高光已经被裁切,信息已经丢失,召回率从满分跌到了 0.319。在含噪的低光照图像上,它会制造 155 个错误的花蕾检测。这里需要坦诚说明一个限制:白平衡与 CLAHE 始终作为一个串联组合接受测试,从未分别做消融实验,所以我无法判断在表现糟糕的案例中,究竟是哪一步造成了破坏。无论如何,实际使用中的结论不变:为预处理设置启用条件。先测量图像的平均亮度、色偏统计等指标,只有确实存在全局偏移时才开启。
遮挡半径为对象半径的一半时,所有未被遮挡的对象仍然都能找到,但被遮挡的对象有 31% 消失了,受损的主要是花蕾。花蕾半径只有 11–17 px;遮住中心后,剩下的碎片面积低于 min_area=120,会被当作噪声丢弃。开花更大,在相同处理下仍能保留一圈花瓣,因此存活下来。
遮挡达到半径的 0.8 倍时,出现了本研究中最奇怪的数字:总数误差 +81%。遮挡不只会删除对象,还会创造对象。一朵被部分遮住的开花会裂成多个碎片,每个碎片都落在 120–900 px² 区间内,于是每个都被记成一个花蕾:真实花蕾只有 76 个,却检测出了 297 个。叶片茂密的照片会同时低估开花数、高估花蕾数,而且高估造成的误差更大。
做一项参数消融:把 min_area 从 120 降到 40。
在轻度遮挡下,降低面积下限确实能救回花蕾,但代价是误报达到原来的十倍,总数误差从 −20.5% 翻转为 +21.4%。在重度遮挡下,它几乎没有收益,只会增加碎片。这是用精确率换召回率,只有在轻度遮挡时才划算,并不是解决方案。脚本为 data/2026-10-17-bloom-minarea-ablation.py,结果已合并到同一份原始数据文件中。
两朵开花一旦接触,形态学闭运算就会把它们焊成一个连通分量,各个偏移条件下都会少计 42–57 朵开花。面积拆分在 0.8–1.2 倍半径区间确实有帮助(−49 → −4,−54 → +10)。偏移更大时,它会矫枉过正,2.2 倍时误差达到 +27。过度修正主要是估算器自身造成的:它按面积重新折算每个开花包围框,而单朵开花的半径本来就会在 30–49 px 之间变化,所以一朵独立的大花也会被向上折算成“一朵半”。偏移越大,真正发生的合并越少,这种系统性膨胀就越占主导。还有第二个混杂因素:在这些密集场景中,偏移为 2.2 倍时,新增花朵经常会落到第三朵花上,因此这一端原始的 −35 误差混入了三朵花合并的情况,不能作为纯粹的两两重叠结果解读。
组合压力条件,即 gamma 1.6 加上 0.8 倍半径的叶片遮挡,会让召回率降至 0.429;预处理也只能将它提升到 0.500。预处理能修复全局偏移,却无法修复物理上被遮住的东西,这条边界值得明确说清楚。
从业者可以直接借鉴四点:
计数 pipeline 的验收指标必须包括按类别、逐对象匹配的精确率和召回率,还要显式衡量开花与花蕾比例的误差。在我的实验中,只看总数会把三种条件判为 0.0% 误差,但其中六分之一到四分之一的开花分类已经出错。我会修改仓库演示程序的评分方式,并为 README 中的 99.7% 补充方法说明。
在两种最常见的退化条件,也就是变暗和中度遮挡下,面积阈值分类器的主要失效方式是类别迁移,而不是漏检。监控类别构成的漂移,比监控总数更早发出警报。但适用边界很重要:在极端欠曝和 1.1 倍半径遮挡下,对象会直接消失,被遮挡对象的召回率只有 0.014,此时也就没有类别迁移可供监控。
预处理,包括白平衡和 CLAHE,应该有条件地启用,由测量到的全局偏移触发。对于高光已裁切或含噪的输入,它带来的损害大于收益。
小尺寸类别的目标需要专门的遮挡保护策略。降低 min_area 只在轻度遮挡下划算。超越单帧的一种候选方案是跨天跟踪:昨天的花蕾今天被叶片挡住了,就从时间序列中恢复它。本文的任何实验都没有验证这个想法,因为所有实验都是静态单帧,风吹晃动和视角变化都没有测试。这是这些失效模式指向的一个方向,也让路线图中的视频 / 多帧去重获得了更高优先级。
为什么不直接换成训练出来的检测器?BloomDetector 接口已经存在,替换后端不会影响其他部分。但没有真实标注照片,就没有可用的训练数据。这项研究的作用,是绘出基线在光照和遮挡下的失效边界,让未来的任何后端都必须通过同一套破坏性测试,并把数字并排比较。
一切仍然都是合成的:程序绘制的花朵、纯色绘制的叶片遮挡物、全局参数化光照。真实温室的光具有方向性和光谱特征,真实叶片也有纹理和半透明性。应将这些数字视为失效方向和幅度的下限,而不是田间准确率。
光照变换作用于整张图像,没有模拟局部镜面反射和镜头眩光。
白平衡与 CLAHE 从未分别做过消融实验,两者各自的贡献与损害纠缠在一起。
在 2.2 倍半径偏移下,重叠实验受到第三朵花参与合并的混杂影响:原始误差包含了这些情况,拆分修正又过头了,达到 +27。这一行修正前后的两个结果,都不能作为纯粹的测量结果解读。
只有四个场景、210 个对象,没有计算置信区间。我保留的结论都是在不同条件下表现一致的结论;精确的点值,比如 gamma 断崖究竟在哪里,需要更密集的采样。
拆分估算器的单位面积取自同一批次的中位数,必须针对不同栽培品种和相机距离重新校准,而我尚未测试这种迁移。
真实照片验证仍然缺失。路线图中的训练式检测器后端和真实标注数据集都尚未启动。这仍然是这条研究路线中最大的开放问题。
Bloom-Counter 仓库采用 MIT 许可证,包含本文使用的全部代码。检测器位于 detector/color_segmentation.py;配套调优的注释位于 simulator/generate.py 第 23 行,不重叠放置逻辑位于其中的 _place 函数;只看总数的评分位于 examples/demo.py。
本研究的脚本和原始数据:配套文件 data/2026-10-17-bloom-bench.py、data/2026-10-17-bloom-minarea-ablation.py 和 data/2026-10-17-bloom-bench.json,包含 4 个场景,随机种子为 101/202/303/404,固定且可重复运行。这些文件与文章一同归档在 tech-deep-dives 仓库中。
OpenCV 文档:HSV 色彩空间与 inRange、形态学操作、createCLAHE。
作者:Guangyi Zhao。本文描述的全部代码和实验,均为作者独立完成的净室工作:检测器、模拟器和压力测试脚本均由作者编写,实验使用本地生成的合成数据运行。不涉及任何客户数据、客户图像或身份识别信息。
如需采取进一步行动,可以考虑屏蔽此人和/或举报滥用行为。