把街景语义分割结果,变成经得起追问的指标
在朝向、类别体系、拍摄季节和校验方式确定下来之前,一个类别占比还算不上指标。这四件事,每一件都会改变那个数字。
- 作者
- HuiTu Technology
- 发布时间
- 更新时间
把分割模型跑在街景影像上,是整件事里最容易的部分。开源的城市场景模型效果不错、跑得也快,而那张彩色输出图看上去足够有说服力,以至于人们会接受从另一端吐出来的任何百分比。
难的是从像素图到一个真会被拿去做决策的数字之间的全部环节。下面就按这些问题实际发作的顺序,把中间的决策逐一走一遍。
一个点,四个朝向,一份画像
单一相机朝向测量的不是这条街,而是相机恰好对着的那个方向。镜头朝着公园,这条街看起来绿意盎然;镜头沿着车行道望过去,同一个位置就变得光秃秃。
- 每个采样点渲染 0°、90°、180°、270° 四张透视截图,逐张计算指标,再平均成该点的一份画像。
- 整个项目的视场角必须固定。视场角一放大,天空和建筑顶部就被更多地纳入画面,你接下来要算的每一个占比都会随之偏移。
- 俯仰角同样要固定,并保持接近水平。轻微向上仰是最快抬高天空占比、又最不容易被察觉的做法。
- 推理之前先把全景重投影成平面透视截图。模型是在普通的直线透视照片上训练的,直接喂等距柱状全景,等于把弯曲的建筑和拉伸的天空塞给一个从没见过它们的网络。
- 除了平均后的画像,也把四个朝向各自的值留下来。它们之间的离散程度能告诉你,这个点是落在一条形态确实均质的街道上,还是落在两种截然不同街道的交界处。


把指标定义精确
下面每个指标都是被分类像素的占比,这就使类别定义成了承重结构。请把定义写在数字旁边——两支团队各自计算绿视率,仅仅因为把哪些类别算作绿色不同,结果就能差好几个百分点。
| 指标 | 定义 | 最敏感的因素 |
|---|---|---|
| 绿视率 | 植被像素占可见画面的比例,并在四个朝向上取平均 | 拍摄月份,以及草地与地被是否被算作植被 |
| 天空占比 | 天空像素占画面的比例 | 相机俯仰角、视场角,以及晴天下的过曝 |
| 围合度代理 | 竖向界面(建筑、围墙、栅栏)与可见天空之比 | 树冠遮挡立面,会在夏季把这个比值压低 |
| 地面构成 | 车行道、人行道与地被各自的占比 | 路边停放车辆对车行道与路缘的遮挡 |
| 行人与车辆存在度 | 检测计数,与像素占比并列报告 | 拍摄时段与星期几,而这两者在平台影像里是无法控制的 |
import numpy as np
# Cityscapes evaluation ids, the label space most open urban-scene models emit.
ROAD, SIDEWALK, BUILDING, WALL, FENCE = 0, 1, 2, 3, 4
VEGETATION, TERRAIN, SKY, PERSON = 8, 9, 10, 11
def indicators(labels: np.ndarray) -> dict:
"""Class shares for one perspective crop, expressed as percentages."""
total = labels.size
def share(class_ids):
return sum(int((labels == cid).sum()) for cid in class_ids) / total
green = share([VEGETATION]) # trees and shrubs, not grass verges
sky = share([SKY])
vertical = share([BUILDING, WALL, FENCE])
return {
"green_view_pct": 100 * green,
"sky_share_pct": 100 * sky,
# Enclosure proxy: vertical surface against visible sky. Clamped so a
# view with no sky at all cannot divide by zero.
"enclosure_ratio": vertical / max(sky, 1e-3),
"ground_pct": 100 * share([ROAD, SIDEWALK, TERRAIN]),
"person_pct": 100 * share([PERSON]),
}
def point_profile(crops: list[np.ndarray]) -> dict:
"""One profile per sample point: the mean across its four headings."""
per_heading = [indicators(crop) for crop in crops]
return {
key: float(np.mean([heading[key] for heading in per_heading]))
for key in per_heading[0]
}绿视率有一条值得在方法被质疑时援引的学术脉络:2015 年发表于《Urban Forestry & Urban Greening》的改进型绿视指数,以及 MIT Senseable City Lab 将其推广到多座城市的 Treepedia 项目。把自己的定义对齐到一个已发表的定义上,并且在交付时说明这一点,远比在评审会上为一个自创公式辩护来得省事。
Cityscapes 和 Mapillary Vistas 不能互换
无论你用哪个预训练模型,它的类别体系决定了你能测什么。城市场景领域占主导的这两套体系,差异大到在各自体系下算出的指标无法直接比较。
| Cityscapes | Mapillary Vistas | |
|---|---|---|
| 类别粒度 | 一组紧凑的评测类别,覆盖常见城市场景 | 体系大得多,在街道设施与路面标线内部有精细区分 |
| 采集背景 | 欧洲城市的车载前挡风采集,基本是晴好天气的白天 | 来自多个国家、多种相机、多种季节与天气的贡献者影像 |
| 植被处理 | 植被与地被分属不同类别,草地与树冠不在一起 | 植被与地面的划分更细,会改变「绿色」到底包含什么 |
| 更适合 | 快速、生态成熟的基线,以及跨研究的可比性 | 精细的资产与街道设施盘点,以及与德国街道毫不相像的影像 |
| 主要风险 | 用于全景或非欧洲街景时存在域偏移 | 体系复杂,必须事先自行定义聚合规则 |
混淆真正发生在哪里
- 植被与地被之争:一条草坪带算不算绿色,在郊区街道上能让绿视率相差好几个百分点,在市中心则几乎没有影响。
- 围墙、栅栏与建筑之争:对围合度而言这三者都是竖向界面,但如果模型把它们划分得前后不一致,围合度就会在本来平稳的地方显得很跳。
- 人行道与车行道之争:路缘很细,又常被停放车辆遮挡,是不同模型之间最常见的分歧来源。
- 骑行者与行人之争:一个骑车的人是算作一个行人,还是单列为骑行者,会直接影响任何街道活力指标。
季节与光照是系统性偏差,不是随机噪声
随机噪声会在大样本里被抹平,季节与光照效应不会:它们把所有在相同条件下拍摄的点,朝同一个方向推。这恰恰是那种能在聚合之后依然存活、并最终变成虚假结论的误差。
- 把拍摄月份一路带到最终结果表。它如果不是一列,季节性就是你所有对比中的一个隐藏变量。
- 绝不要拿落叶期与展叶期的影像去比较植被指标。影像年份混杂时,要么限定在可比的时间窗内,要么把月份显式建模进去。
- 留意阴天被读成明亮建筑立面的情况,以及高光溢出把细枝并入天空的情况。
- 低太阳角度的影像要单独检查。横跨车行道的长影会改变地面类别占比,并压低表观的活动计数。
- 把湿滑路面当作一种已知的失效模式:反射会把天空和建筑的像素铺到地面上。
- 按比较单元报告季节与光照构成,让读者能看出两个行政区究竟是不是在相近条件下被测量的。
用分层抽样做人工校验
任何指标在交付前都要经过人工核查,而这个核查必须是设计出来的,不能临时起意。挑几张看着有意思的图打分,得到的一致性数字没有任何意义。
- 跨行政区、跨拍摄月份、跨指标取值区间做分层抽样,确保绿意最浓和最光秃的街道都被覆盖,而不是全挤在中间段。
- 用固定的点阵网格逐张人工打分——用肉眼对规则排布的采样点逐点判类,无需全图标注就能得到一个可复现的参考占比。
- 一致性要以分布形式报告,而不是一个平均值:既给平均绝对偏差,也给最差的那几例,系统性问题就藏在那里。
- 逐个检查分歧样本。它们会成簇出现,而这一簇通常直接指明了失效原因:阴影、反光、不常见的立面材质、陌生的车辆类型。
- 只要模型、类别体系、视场角或影像来源发生变化,就要重新校验。这四项中的任何一项,都会让此前的一致性数字失效。
- 校验结果随数据一并交付,注明样本量与方法,而不是用笼统的措辞描述精度。
这次校验最终回答的是:这条流水线对当前决策是否足够精确。不要给出脱离项目的统一精度承诺,而要报告这批影像、这个模型和这组样本实际测得的误差分布。结果可能足以支持街道排序,却仍不足以把单个点位当作真值——这个区分应在交付前写清楚,否则总会有人把一个精确到两位小数的数字原样引回规划会上。