chore(pbf): retire migrated Provider pipeline entrypoints

This commit is contained in:
OpenAI Codex
2026-10-13 08:24:38 +08:00
parent f3da1a6a32
commit 1fef4c6dd1
35 changed files with 21 additions and 1013935 deletions

21
PBF_PROVIDER_MIGRATED.md Normal file
View File

@@ -0,0 +1,21 @@
# PBF Provider Pipeline 已迁移到独立 Git
**注意:本仓库 `tei/pbf` 已停止维护 PbfProvider 的视觉识别、Style/PBF/Sprite 解释和 Provider Package 构建。不得再在本仓库新增、修改或运行旧 Pipeline 代码。**
## 当前权威入口
- 独立 Git:<https://git.eyaku.jp/tei/PbfProvider>
- 独立仓库当前发行:`v0.1.0`(源码 GSRL Source Lock 和使用说明位于该仓库 `release/`)
- 本地独立项目:`/root/sourceserver/PbfProvider`
- 通用 Provider 输入:Style、Sprite、PBF 样本、人工审核资料;Provider Package 由独立项目维护。
## 原 pbf 仓库历史保留
- 原 `pbf/main` 的 Provider 视觉提取工具与流程文档已迁入独立仓库,从本仓库当前文件树删除;原 Git 历史仍可正常回溯。
- 原 Provider Pipeline 开发分支:`feature/provider-pipeline-v1`;末次历史归档 Commit:`7c64299c2237800cec06034b5b6ddfe8436bb8ce`,已成功 Push。
- **该历史分支只用于审计与回溯,不是以后开发 Provider 的入口,也不要把该分支合并回 pbf/main。**
- 旧 Pipeline 的 CM95 语义/候选审核仍有未完成内容,不能因为迁移认为已正式通过。
## 本仓库仍负责
原 NavSea PBF 瓦片生产、空间网格、天气数据以及现有相关审计等职责不受此次迁移影响。历史 `STEP_RECORD.md` 引用旧路径仅用于解释当时的工作,不代表路径仍有效。

View File

@@ -1,457 +0,0 @@
# PBF Pipeline 正式流程 v1
日期:2026-10-06
状态:实现基线
适用仓库:`/root/sourceserver/pbf`
## 1. 总体边界
PBF 项目负责从任意 Provider 的 Style、PBF/MVT、Sprite、Glyph 和 Provider 语义资料生产符合公开 NavSea Provider Contract 的 `NavSea Provider Package`。
PBF 项目不生成 `ProviderExecutionPlan`。ProviderExecutionPlan 只属于 NavSea。
NavSea 的职责是公开 Contract、验证 Provider Package、生成 Chart Provider Profile、ProviderCompiler 编译、ChartRuntime 安装和 Runtime 消费。
第三方可以完全绕过官方 PBF Pipeline,只要自行生产的数据满足公开 NavSea Provider Contract。
## 2. Pipeline v1
```text
PBF-01 Provider Source Intake
↓
PBF-02 Style Visual Object Extract
↓
PBF-03 Actual Visual Object Instantiate
↓
PBF-04 Provider → NavSea Standard Mapping
↓
PBF-05 Provider Policy Compile
↓
PBF-06 Provider Package Validate
↓
PBF-07 NavSea Provider Package Export
```
Pipeline 正式终点是 `NavSea Provider Package`,不是 `ProviderExecutionPlan`。
## 3. PBF-01 Provider Source Intake
目标:把不同 Provider 的原始输入统一整理成可重复、可校验的 Source Intake。
输入至少支持:
- provider identity
- dataset identity
- Style JSON / URL
- PBF / MVT / MBTiles / XYZ URL
- Sprite JSON / PNG
- Glyph URL 或资源
- Provider semantic metadata
- sample tiles
- bounds
- minZoom / maxZoom
建议输出:
```text
work/<provider-id>/
intake/
source-manifest.json
style.snapshot.json
sprite/
semantic-source/
samples/
intake-report.json
```
`source-manifest.json` 至少记录 providerId、datasetId、providerVersion、style、tileSource、sprite、glyphs、bounds、zoom、sampleTiles、semanticSources、sourceFingerprint。
Gate:
- Provider identity 已确定
- Dataset identity 已确定
- Style 可读取
- PBF 来源可定位
- Sprite/Glyph 状态明确
- 输入 Hash/Fingerprint 已记录
- 采样范围已确定
## 4. PBF-02 Style Visual Object Extract
目标:只从 Style + Sprite 中提取视觉事实,不在本阶段直接决定 NavSea Standard Layer。
当前工具:
```text
tools/style-visual-object/style_vo_tool.py
```
输入:
- style.snapshot.json
- Sprite
输出:
```text
anonymous-catalog.json
_sprite/sprite.json
_sprite/sprite.png
```
核心原则:产物为 Anonymous Visual Object。source-layer、Provider 字段名、Provider 对象类名称只作为 provenance/audit evidence,不得直接作为 NavSea Layer 映射依据。
Gate:
- Anonymous Visual Object 可重复稳定生成
- 同输入重复执行结果一致
- 视觉组件与 Style Layer 引用完整
- 不包含 Provider-specific NavSea Layer 硬编码
## 5. PBF-03 Actual Visual Object Instantiate
目标:把 Style 定义与真实 PBF feature 结合,生成实际出现的视觉对象实例。
当前工具:
```text
tools/visual-object-instantiation/instantiate_pbf_visual_objects.py
```
正式 Pipeline 必须去除 CM95-specific 主逻辑硬编码。Provider-specific 行为只能通过 manifest、adapter config、declarative metadata 或 policy input 表达。
输入:
- Anonymous Visual Object Catalog
- Style
- Sprite
- 真实 PBF/MVT
- sample tiles
- Provider semantic metadata(辅助证据)
建议输出:
```text
actual-visual-object-catalog.json
visual-object-occurrences.json
instantiation-report.json
```
每个 occurrence 至少可追踪 feature identity、tile、source、source-layer、matched style layers、render components、field evidence、geometry type、zoom、provenance。
Gate:
- 真实 PBF feature 与 Style 命中关系可对应
- 同一原始 feature 的多个 render component 可归并
- 不按屏幕距离拼接对象
- occurrence 可回溯原始 feature
## 6. PBF-04 Provider → NavSea Standard Mapping
目标:把 Provider Actual Visual Object / Composite Visual Evidence 映射到 NavSea Frozen Standard Catalog / Standard Layer。
输入:
- Provider Actual Visual Object Catalog
- NavSea Frozen Standard Catalog
- Visual Matcher 建议
- Provider 官方语义资料
- 人工审核结果
NavSea Frozen Standard Catalog:
```text
tools/style-visual-object/output/navsea-standard/standard-catalog.json
```
默认只读,不重新生成或修改。
最终映射状态:
- VISUAL_OBJECT_MATCH
- LAYER_ONLY
- COMPONENT_ONLY
- AMBIGUOUS
- UNKNOWN
VISUAL_OBJECT_MATCH:可确定到 Standard Visual Object + Standard Layer。
LAYER_ONLY:可确定 Standard Layer,但不要求存在 1:1 Standard Visual Object;这是正常结果。
COMPONENT_ONLY:只表示完整视觉对象组成部分,如 topmark、buoy_body、beacon_body、light、auxiliary_mark、fill_pattern、line_pattern、text、other;COMPONENT 自身不决定最终 Standard Layer。
AMBIGUOUS:存在多个合理候选,禁止 Runtime 任意选择。
UNKNOWN:无法可靠确定 Standard Layer。
建议输出:
```text
mapping/
provider-layer-assignment.json
provider-layer-assignment.csv
provider-layer-assignment.html
mapping-validation-report.json
```
Gate:
- 所有 occurrence 有明确状态
- 无未审核项
- AMBIGUOUS/UNKNOWN 写入 limitations
- 人工决定优先于自动 matcher
- matcher 不覆盖人工结论
- Provider 语义建议与人工结论冲突只产生 WARN
## 7. PBF-05 Provider Policy Compile
这是当前 Pipeline 缺失的关键桥梁。
输入:
```text
Visual Object / Layer Assignment
+ Provider Source Facts
+ Provider Semantic Metadata
+ Capabilities
+ Human Review
```
输出:
```text
Provider Policy
```
NavSea Provider、CM95 Provider 和未来第三方 Provider 必须使用同一 Policy Schema。
禁止:
```text
NavSea → 特殊 Runtime 直通
CM95 → Provider Policy
```
统一为:
```text
NavSea Provider → NavSea Provider Policy → NavSea Standard Layer
CM95 Provider → CM95 Provider Policy → NavSea Standard Layer
Provider B → Provider B Policy → NavSea Standard Layer
```
建议新增:
```text
tools/provider-policy-compiler/
README.md
compile_provider_policy.py
validate_provider_policy.py
schema/
tests/
```
Compiler 输入:
- source-manifest.json
- actual-visual-object-catalog.json
- provider-layer-assignment.json
- Provider semantic metadata
- capability declaration
- manual review result
- NavSea Standard Catalog identity
Provider Policy 至少覆盖:
- schemaVersion
- provider
- dataset
- standardCatalog
- resources
- mapping
- display
- pickup
- planning
- danger
- capabilities
- unresolved
- limitations
- provenance
- fingerprints
Provider Policy 不包含 ProviderExecutionPlan、Runtime cache、最终 query buckets、Runtime layer handles 或 React/MapLibre Runtime 对象。
Gate:
- NavSea 和 CM95 Policy 通过同一 validator
- Schema 无 provider-specific 分叉
- Python 主逻辑不得写 provider == cm95
- Mapping 可追溯到 PBF-04
- unresolved/limitations 不得静默丢失
## 8. PBF-06 Provider Package Validate
目标:在离开 PBF 项目前验证 Provider Package 是否符合公开 NavSea Provider Contract。
验证至少包括:
- providerId / datasetId / version
- Style / PBF / Sprite / Glyph / bounds / zoom
- Standard Layer mapping
- Standard Visual Object reference
- COMPONENT/LAYER_ONLY/UNKNOWN/AMBIGUOUS 约束
- Display / Pickup / Planning / Danger
- capabilities
- schema version
- file/resource hash
- package fingerprint
- standard catalog fingerprint
- source fingerprint
- unresolved / unsupported / partial capability / missing resource
输出:
```text
validation/
provider-package-validation.json
provider-package-validation.md
```
结果只能是:
- PASS
- PASS_WITH_WARNINGS
- FAIL
FAIL 时禁止进入 PBF-07。
## 9. PBF-07 NavSea Provider Package Export
目标:生成可直接交给 NavSea 或第三方 Validator 的正式 Provider Package。
建议:
```text
dist/
<provider-id>/
<dataset-id>/
<version>/
provider-package.json
provider-policy.json
resources/
style.json
sprite.json
sprite.png
mapping/
layer-assignment.json
reports/
validation.json
validation.md
MANIFEST.json
```
URL 型资源允许只保存 URL、hash、expected content identity、availability/fallback policy,不要求将所有 PBF 数据打入包。
MANIFEST 至少包含:
- package schema version
- provider identity
- dataset identity
- package version
- createdAt
- source fingerprint
- style fingerprint
- sprite fingerprint
- standard catalog fingerprint
- policy fingerprint
- mapping fingerprint
- validation result
- known limitations
只有 PASS 或明确允许发布的 PASS_WITH_WARNINGS 才能正式 export。
## 10. NavSea Provider Package Contract
公开 Contract 至少覆盖:
- provider identity
- dataset identity
- package/version identity
- Style resource
- PBF resource
- Sprite resource
- Glyph resource
- NavSea Standard Layer mapping
- Provider Policy
- Display
- Pickup
- Planning
- Danger
- capabilities
- validation requirements
- hash / version / fingerprint
- unresolved
- limitations
- provenance
公开 Contract 不要求第三方使用官方 PBF 工具。
## 11. NavSea 接收侧
```text
NavSea Provider Package
↓
NavSea Contract Validate
↓
Chart Profile Tools
↓
Chart Provider Profile
↓
ProviderCompiler
↓
ProviderExecutionPlan
↓
ChartRuntime
↓
NavSea Runtime
```
NavSea 不再负责 CM95/S-52 语义研究、Visual Object 人工分类、Provider-specific 图标研究或 Provider Policy 推理生成。
## 12. 当前工具迁移关系
`tools/style-visual-object/style_vo_tool.py` → PBF-02。
`tools/visual-object-instantiation/instantiate_pbf_visual_objects.py` → PBF-03,但必须去 CM95 硬编码。
`tools/visual-object-matcher/` 和 `visual_anchor_matcher.py` → PBF-04 辅助建议工具,不是最终权威。
`tools/style-visual-object/reviewer/` → PBF-04 人工确认。
`apply_reviewed_layer_assignment.py` → PBF-04 输出编译。
`validate_cm95_classification.py` 暂保留为 CM95 基线校验;后续增加 Provider-neutral validator。
## 13. 实施顺序
第一优先级:冻结 Provider Policy Schema v1 和 Provider Package Schema v1。
第二优先级:实现 `tools/provider-policy-compiler/`,让 NavSea 与 CM95 生成完全同 Schema 的 Provider Policy。
第三优先级:实现 Provider Package Validator / Export。
第四优先级:调整 NavSea `help/chart-profile-tools/`,收敛为读取公开 Provider Package → Validate → Profile → ProviderCompiler。
## 14. 核心原则
```text
PBF = 数据生产者
NavSea = Contract 定义者 + Validator + Compiler + Runtime
```
以及:
```text
PBF Pipeline 的终点是 Provider Package。
ProviderExecutionPlan 永远属于 NavSea。
```

View File

@@ -1,2 +0,0 @@
__pycache__/
*.py[cod]

View File

@@ -1,785 +0,0 @@
# Style Visual Object Tool
## 目标
本工具只做一件事:
> 从一份 Style JSON 以及它使用的 Sprite / Glyph 中,独立提取和生成 Visual Object。
工具输出的是匿名、可比较、可审计的 Visual Object,不负责给 Visual Object 赋予 NavSea 标准 Layer 名称,也不负责解释 Provider 的业务字段。
---
## 最重要原则:先生成 Visual Object,再做语义和 Layer 对齐
本工具必须严格遵守以下顺序:
```text
任意 Provider Style + Sprite / Glyph
↓
Style Visual Object Tool
↓
Anonymous Visual Object Catalog
↓
与 Standard Visual Object Catalog 比较
↓
MATCH / PARTIAL / UNKNOWN
↓
最后才允许赋予标准 Layer 名称或 Semantic
```
### 禁止走捷径
在 Visual Object 生成阶段,以下信息即使存在,也只能作为 provenance / audit 信息保存,**不得用于决定 Visual Object 属于哪个标准 Layer 或 Semantic**:
- `source-layer` 名称
- Style Layer `id`
- Provider 名称
- `canonical_object_type`
- S-52 / CM95 对象类名称
- S-52 symbol code 的文字含义
- 任何看起来与 NavSea 标准 Layer 同名或相似的字段
- 任何 Provider 私有业务字段名称
例如,某个 Provider 的 Style 中即使存在:
```text
source-layer = navigation_marks
```
本工具也不得因此把该 Visual Object 定义为 NavSea 的 `navigation_marks`。
它只能记录:
```text
provenance.source_layer = navigation_marks
```
真正的 Layer 名称必须在 Visual Object 生成完成后,通过与标准 Visual Object 的比较结果确定。
---
## Provider-neutral
本工具必须完全独立于具体 Provider。
同一套代码应能够处理:
- NavSea Style
- CM95 Style
- 未来 Provider B / C / D
- 任意符合支持范围的 MapLibre Style
主逻辑中禁止出现类似:
```text
if provider == "cm95"
if source_layer == "navigation_marks"
if symbol_code == "BOYCAN63" then ...
```
这类 Provider-specific Semantic 判断。
Provider-specific 名称可以进入 provenance,但不能进入 Visual Object 分类逻辑。
---
## 输入
第一阶段输入仅包括:
```text
Style JSON
Sprite JSON + PNG
Glyph(Style 使用时)
```
Visual Object 提取本身不要求读取 PBF。
如果 Style 中使用数据驱动表达式,例如:
```json
["get", "chart_icon_image"]
```
或:
```json
["get", "s52_symbol_code"]
```
工具必须保留为动态视觉参数,不得为了得到具体值去读取 PBF、猜测字段含义或套用 Provider 规则。
---
## 输出
建议至少生成:
```text
catalog.json
catalog.html
previews/
audit.json
```
其中每个 Visual Object 至少记录:
- 匿名 Visual Object ID
- geometry role
- Style Layer 原始顺序 / z-order
- 参与组成的 Render Components
- icon / line / fill / pattern / circle / text
- Sprite 实际视觉资源
- 动态视觉参数
- 出现条件 / Style filter
- 组件数量与组合关系
- Preview
- provenance
- 无法解析或无法确定的部分
---
## Visual Signature
用于后续跨 Provider 比较的核心是 Visual Signature,而不是 Provider 字段名称。
Visual Signature 应主要表达:
```text
geometry role
+
ordered render components
+
sprite visual
+
fill
+
line
+
pattern
+
text role
+
component multiplicity
+
component composition
+
style order
+
visual conditions / variants
```
后续 Visual Matcher 应基于这些视觉结构进行比较。
Provider 原始字段只作为辅助审计证据,不作为主要匹配依据。
---
## source-layer 的地位
`source-layer` 在本工具中只具有两个作用:
1. 帮助确定 Style Layer 的数据来源和可组合范围;
2. 作为 provenance / debug / audit 信息输出。
它**不是 Semantic,也不是最终 Layer 分类结果**。
不得执行:
```text
source-layer 名称
→ 直接得到 NavSea 标准 Layer
```
正确流程必须是:
```text
Style + Sprite
→ Visual Object
→ Visual Match
→ 标准 Layer / Semantic
```
---
## Standard Visual Object
NavSea 当前 Style 也必须使用同一套 Style Visual Object Tool 生成 Visual Object。
不得在 Tool 内置 NavSea Layer 规则。
NavSea 之所以成为 Standard,不是因为 Tool 对 NavSea 使用特殊逻辑,而是在 Tool 生成 Visual Object 之后,由独立的标准定义/人工确认把这些 Visual Object 与 NavSea 标准 Layer 关联起来。
因此:
```text
NavSea Style
→ Anonymous Visual Object Catalog
→ 标准确认
→ Standard Visual Object Catalog
```
其他 Provider:
```text
Provider Style
→ Anonymous Visual Object Catalog
→ 与 Standard Visual Object Catalog 比较
→ 标准 Layer
```
---
## 不属于本工具的职责
本工具不负责:
- PBF 数据读取和 Feature 实例化
- Provider 字段翻译
- CM95 专用判断
- NavSea 专用判断
- Semantic 命名
- 标准 Layer 命名
- Feature grouping
- 危险性判断
- 船舶吃水相关 Planning / Danger 判断
- 跨 Provider 的最终匹配决策
这些属于后续独立阶段或独立 Tool。
---
## 实现约束
后续实现必须满足:
- 未知 Style expression 必须显式标记 UNKNOWN / UNSUPPORTED,不得静默忽略;
- 必须保留 Style 原始顺序;
- 必须保留组件 multiplicity,不能用 set 去重破坏组合;
- 必须保留动态表达式,不得伪造成确定值;
- Sprite 必须根据 atlas metadata 提取真实视觉资源;
- 不得通过坐标接近、名字相似或 Provider 字段名自动拼接/分类 Visual Object;
- 不得因为某个 source-layer 与 NavSea 标准 Layer 同名就直接判定 MATCH;
- Visual Object 的生成和 Visual Object 的标准匹配必须是两个独立阶段。
---
## 一句话验收标准
> 给工具任意一套 Style + Sprite,即使它完全不知道这是什么 Provider、也没有任何 PBF 数据,仍然能够独立生成匿名 Visual Object Catalog;只有在该 Catalog 生成完成后,才允许进入与 NavSea Standard Visual Object Catalog 的比较和 Layer 命名阶段。
---
## NavSea Standard Visual Object Catalog 冻结规则
NavSea 的 Visual Object 与标准 Layer 名在首次完成生成、确认和命名后,应视为 Standard Visual Object Catalog 的冻结基线。
正常情况下:
```text
NavSea Anonymous Visual Object
↓
人工/标准确认 Layer 名
↓
Standard Visual Object Catalog
↓
冻结
```
冻结后,后续 CM95、Provider B、Provider C 等只能使用该 Standard Visual Object Catalog 进行 Visual Object 匹配和 Layer 命名。
除非用户明确要求重建、重新生成、修订标准或升级 Standard Catalog 版本,否则禁止:
- 每次处理新 Provider 时重新生成 NavSea Visual Object;
- 自动重新计算或改变 NavSea Visual Object ID;
- 自动改变已经确认的标准 Layer 名;
- 因 CM95 或其他 Provider 的数据特点反向修改 NavSea Standard Catalog;
- 用新的 Provider 匹配结果覆盖现有标准基线。
如果确实需要修改 Standard Visual Object Catalog,必须作为显式的标准升级处理,并保留旧版本,不得静默覆盖。
因此正常跨 Provider 工作流固定为:
```text
已冻结的 NavSea Standard Visual Object Catalog
│
│ 作为只读标准
▼
Provider Anonymous Visual Object
│
▼
Visual Matcher
│
▼
标准 Layer 命名结果
```
一句话原则:
> NavSea Standard Visual Object Catalog 是标准输入,不是每次 Provider 解析时重新生成的临时产物。
---
## Visual Object Layer Assignment 的补充硬规则
### 1. Sprite 存在不等于 Style 会绘制
Sprite atlas 中存在某个资源,只能说明资源可用,不能说明当前 Style 会实际引用它。
Anonymous Visual Object Catalog 只能从 Style 的正向可达规则生成:
```text
Style 明确允许的资源
→ 进入 Anonymous Visual Object Catalog
仅存在于 Sprite、但当前 Style 没有正向可达路径的资源
→ 不进入 Catalog
```
尤其不得把 `!match`、`!=` 等排除条件中的值误当成候选 Visual Object。
### 2. 同一视觉类可能对应多个标准 Layer
如果 NavSea Standard Visual Object Catalog 中同一实际视觉图形被多个标准 Layer 复用,则 Visual Matcher 不能仅凭图形强行选一个 Layer。
必须输出:
```text
AMBIGUOUS
candidateStandardLayers:
- layer_a
- layer_b
```
例如危险物公共符号可以同时被多个标准 Layer 使用;这种歧义是 Standard Catalog 自身的事实,不应由 Matcher 擅自消除。
### 3. 禁止整个匿名 group 继承单一 Layer
Style 的一个匿名 group 可能允许多个不同视觉家族,尤其是通用 S-52 Style。
因此禁止:
```text
group 命中一个视觉锚点
→ 整个 group 继承该 Layer
```
Layer Assignment 必须以单个 Visual Object / Visual Resource 为基本单位。
Group 只用于:
- 组合范围审计;
- Style provenance;
- 结果分布检查。
### 4. Layer Assignment 状态
正式状态固定为:
```text
MATCH
PARTIAL
AMBIGUOUS
COMPONENT
UNKNOWN
```
含义:
- `MATCH`:实际图形基本一致,可以直接作为标准 Layer 锚点;
- `PARTIAL`:不是同一完整图形,但视觉家族或组件关系足以支持唯一标准 Layer;
- `AMBIGUOUS`:视觉证据只能得到多个标准 Layer 候选,禁止强行选一个;
- `COMPONENT`:明确只是完整 Visual Object 的组成部分,不单独分配 Layer;可选记录组件类型;
- `UNKNOWN`:当前证据不足,不命名。
未知和歧义都是合法结果,禁止为了提高覆盖率而静默猜测。
---
## 人工确认 UI:CM95 Visual Object Reviewer
最终 Layer Assignment 必须允许人工确认。项目内提供独立 Web Reviewer:
```text
tools/style-visual-object/reviewer/
├── review_server.py
└── web/
├── index.html
├── app.js
└── style.css
```
默认启动:
```text
python3 tools/style-visual-object/reviewer/review_server.py --host 0.0.0.0 --port 18083
```
### 人工确认单位
Reviewer 以 **CM95 当前 Style 正向可达的唯一 Visual Resource** 为确认单位。
当前基线为 150 个资源。一个人工决定会应用到该视觉资源在 Anonymous Catalog 中的所有 occurrence,但不会修改 Anonymous Catalog 本身。
### 主审查界面必须隐藏 Provider 语义信息
为了避免人工被字段名或已有 source-layer 名称带偏,主审查界面默认只能看到:
- 匿名 Visual Object ID;
- 实际图形;
- component role;
- geometry role;
- occurrence 数;
- NavSea Standard Visual Object 候选;
- 标准 Layer;
- 当前人工决定。
以下 CM95 原始信息默认隐藏:
- resource key;
- source-layer;
- Style Layer id;
- Provider provenance。
只有用户主动展开“审计信息”后才能读取这些内容。
### 人工状态
人工决定固定使用:
```text
MATCH
PARTIAL
AMBIGUOUS
COMPONENT
UNKNOWN
```
规则:
- MATCH / PARTIAL:必须选择且只能选择一个标准 Layer;
- AMBIGUOUS:必须选择至少两个候选标准 Layer;
- COMPONENT:不选择标准 Layer、不绑定 Standard Visual Object;可选记录顶标、浮标主体、立标主体、灯光、附加标记、面填充图案、线型图案、文字或其他组件;
- UNKNOWN:不选择标准 Layer;
- Standard Visual Object 可以选择,也可以不选择;
- 人工确认优先于机器建议;
- 未人工确认的项目在最终人工导出时保持 UNKNOWN,不自动继承机器建议。
### 人工结果保存
增量人工决定保存到:
```text
tools/style-visual-object/output/cm95-manual-review/manual-review-v1.json
```
点击“生成当前人工确认结果”后生成:
```text
tools/style-visual-object/output/cm95-manual-review/
├── reviewed-anchors-manual-v1.json
└── assignment/
├── cm95-layer-assignment-v1.json
├── cm95-layer-assignment-v1.csv
└── cm95-layer-assignment-v1.html
```
人工确认过程不得覆盖:
- `navsea-standard/standard-catalog.json`;
- `cm95-anonymous/anonymous-catalog.json`;
- 既有自动/辅助建议文件。
NavSea Standard Catalog 继续保持 FROZEN,只作为只读标准库使用。
---
## Reviewer 中文名称显示规则
人工 Reviewer 中,CM95 与 NavSea Standard Visual Object 均应优先显示中文名称,帮助人工理解和确认。
中文名称来源分两类:
1. CM95:
- 优先读取当前 CM95 仓库中的 S-52 symbol / line / area-pattern metadata;
- 保留 S-52 原始英文 `description`;
- 生成独立的中文显示译名;
- 中文译名属于工程 UI enrichment,不冒充 IHO 官方中文译名。
2. NavSea Standard:
- 依据冻结 Standard Visual Object 的语义资源键和标准 Layer 生成中文显示名;
- 标准 Layer 同时显示中文名和原始 Layer ID。
名称补充表:
```text
tools/style-visual-object/output/reference-name-enrichment-v1.json
```
固定原则:
> 中文名、英文说明、S-52 code 只用于 UI 显示和人工理解,不得参与 Visual Matcher、自动聚类或 Layer Assignment。
Reviewer 主界面显示优先级:
```text
中文名称
→ 实际图形
→ 中文标准 Layer
→ 原始 code / 英文说明(辅助)
```
已有机器建议也必须显示:
```text
标准对象中文名
+
标准 Layer 中文名
+
MATCH / PARTIAL / AMBIGUOUS / COMPONENT / UNKNOWN
```
不得只显示资源 key 或 Layer ID 让用户自行猜测。
---
## Reviewer 宽屏固定确认布局
Reviewer 在宽屏下采用固定三栏工作区:
```text
左:CM95 当前 Visual Object
中:NavSea Standard 候选(竖排、内部滚动)
右:人工判断(固定可见)
```
固定要求:
- 主页面不得为了点击 MATCH / PARTIAL / AMBIGUOUS / COMPONENT / UNKNOWN 或保存结果而要求纵向滚动;
- NavSea Standard 候选采用竖排列表,并在候选区域内部滚动;
- 标准 Layer 列表在人工判断面板内部滚动;
- `保存并下一个 / 仅保存 / 撤销` 始终保持在右侧人工判断区可见;
- 左侧 CM95 队列独立滚动;
- 审计信息默认折叠,不抢占主要确认空间;
- 窄屏才允许退回上下布局。
---
## Standard Visual Object 缺失时的 Layer-only 人工命名
外部 Provider(例如 CM95 / S-52)的视觉内容可能多于当前 NavSea Standard Visual Object Catalog。
因此:
> 找不到对应的 NavSea Standard Visual Object,不等于无法确定标准 Layer。
人工 Reviewer 必须支持两种最终确认路径:
```text
A. 有对应标准图标
Provider Visual Object
→ Standard Visual Object
→ Standard Layer
assignment_mode = VISUAL_OBJECT_MATCH
B. 没有对应标准图标,但所属 Layer 可以人工确定
Provider Visual Object
→ Standard Layer
assignment_mode = LAYER_ONLY
standard_resource_key = null
```
Layer-only 的要求:
- 必须由人工明确选择标准 Layer;
- 不得伪造一个不存在的 Standard Visual Object;
- 不得因为没有对应图标就强制设为 UNKNOWN;
- 中文名称、S-52 官方英文说明和实际视觉可以作为人工理解证据;
- 这些名称仍不进入自动 Visual Matcher;
- 当前 Reviewer 中,点击“没有对应标准图标,只确认所属 Layer”会清除 Standard Visual Object 绑定,并保留唯一 Layer。
状态建议:
- 有明确对应完整图形时可使用 MATCH;
- 无对应标准图标但 Layer 明确时使用 PARTIAL + LAYER_ONLY;
- 只能确定多个可能 Layer 时使用 AMBIGUOUS;
- 连 Layer 也无法确定时才使用 UNKNOWN。
---
## COMPONENT:只作为完整 Visual Object 的组成部分
当当前显示的 S-52 / CM95 图形只是一个主体、顶标、灯光或其他附加组件时,不应强迫用户给它最终 Layer。
Reviewer 必须允许:
```text
status = COMPONENT
assignment_mode = COMPONENT_ONLY
standard_layer = null
standard_resource_key = null
```
可选 `component_kind`:
```text
topmark 顶标
buoy_body 浮标主体
beacon_body 立标主体
light 灯光
auxiliary_mark 附加标记
fill_pattern 面填充图案
line_pattern 线型图案
text 文字
other 其他组件
```
`COMPONENT` 与 `UNKNOWN` 必须区分:
- `COMPONENT`:已经知道它只是完整对象的一部分;
- `UNKNOWN`:当前连它的作用/归属关系也无法可靠判断。
后续 Composite Visual Object 阶段再把 COMPONENT 与其他组件组合,最终 Layer 应尽量由完整 Visual Object 确认。
---
## Reviewer 最可能候选提示
Reviewer 的 NavSea Standard 候选可以提供一个 **UI-only 的“最可能”提示**,帮助人工快速判断。
综合参考只用于界面提示,来源包括:
- Visual Matcher 的实际图形相似度;
- CM95 中文显示名与 NavSea Standard 中文显示名的文字相似度;
- 已有 reviewed visual suggestion 是否支持同一标准对象或同一 Layer。
界面规则:
- 综合排序第一的候选中文名称标红;
- 显示“最可能”标签;
- 同时显示“视觉 xx% / 综合 xx%”;
- 即使是第一名,综合分较低时也必须保留低分,不能隐藏不确定性;
- 红色提示不得自动选择 Standard Visual Object;
- 不得自动写入人工决定;
- 不参与最终 Layer Assignment。
一句话:
> 红色“最可能”只是人工审查提示,不是系统确认结果。
宽屏布局同时调整为:当前 CM95 对象区域更窄,NavSea Standard 视觉候选区域更宽,右侧人工判断继续固定显示。
---
## Provider Layer Classifier:当前工作的最终目标
当前多源 PBF 对应工作的目标不是寻找“外部 Provider 图标等于 NavSea 哪个图标”,而是:
> 把外部 Provider 的对象正确分类到现有 NavSea Standard Layer。
因此正式流程分成两层:
```text
第一层:Provider-neutral Style Visual Object Tool
Style + Sprite
→ Anonymous Visual Object
第二层:Provider Layer Classifier
Anonymous Visual Object
+ Provider 官方语义资料
+ 实际视觉证据
+ 可选 Standard Visual Object 参考
→ NavSea Standard Layer
→ 人工确认
```
关键规则:
1. `Style Visual Object Tool` 仍然不知道 NavSea Layer、CM95 语义或 Provider 私有分类,继续保持 Provider-neutral。
2. `Provider Layer Classifier` 可以使用 Provider 自己的官方语义资料。CM95 当前使用 S-52 metadata 的官方英文 `description`,再提供中文显示译名。
3. Provider 的 `source-layer` 仍不得作为分类依据,只保留 provenance/audit。
4. Standard Visual Object 一一对应不是最终目标,也不是 Layer 命名的强制前提。
5. 没有对应 NavSea 图标,但 Provider 语义足以确定 Layer 时,允许直接 `LAYER_ONLY`。
6. 当前图形只是顶标、灯光、主体、pattern 等组件时使用 `COMPONENT`,不单独分配最终 Layer。
7. 只有连 Layer 都无法可靠判断时才使用 `UNKNOWN`。
### CM95 语义 Layer 规则
当前 CM95 分类规则独立保存:
```text
tools/style-visual-object/cm95_semantic_layer_rules_v1.json
```
规则使用 S-52 官方 description 和资源类型进行声明式分类,例如:
```text
tower, black/white, full-chart and simplified
→ 对象大类:陆上塔状构筑物
→ onshore_structure_point
beacon tower, red, full-chart
→ 对象大类:航标塔体
→ navigation_marks
topmark for buoys ...
→ COMPONENT / topmark
```
这些规则属于 Provider Layer Classifier,不得回写进通用 Style Visual Object extraction。
### Reviewer 中间区域
Reviewer 中间主要区域现在显示 `NavSea Layer 建议`,综合参考:
- S-52 官方语义;
- 实际视觉相似度;
- 已有 reviewed visual suggestion。
语义证据优先于“长得像”。例如普通 `tower` 即使视觉上很像 beacon,也应优先按 S-52 的 tower 语义归到 `onshore_structure_point`。
Standard Visual Object 图标被降为折叠的辅助参考区,不再作为主要任务。
---
## Git 冻结基线与固定校验命令
CM95 v1 人工分类完成后,以下内容属于应进入 Git 的冻结基线:
- 工具实现:`style_vo_tool.py`、`visual_anchor_matcher.py`、`build_reference_names.py`、`apply_reviewed_layer_assignment.py`;
- Provider 分类规则:`cm95_semantic_layer_rules_v1.json`;
- 人工 Reviewer:`reviewer/`;
- 固定校验逻辑:`validate_cm95_classification.py`;
- NavSea Frozen Standard Catalog 与 CM95 Anonymous Catalog;
- S-52 中文名称 enrichment、视觉辅助结果;
- 150 项人工确认;
- 最终 occurrence 级 Layer Assignment;
- 固定校验报告和最终中文报告。
固定校验命令:
```bash
python3 tools/style-visual-object/validate_cm95_classification.py
```
提交前必须满足:
```text
RESULT: PASS
```
`WARN` 允许存在,但只能表示“人工结果与后续语义建议不同,需要时复核”;不得自动覆盖人工结果。
当前权威结果路径:
```text
tools/style-visual-object/output/cm95-manual-review/manual-review-v1.json
tools/style-visual-object/output/cm95-manual-review/reviewed-anchors-manual-v1.json
tools/style-visual-object/output/cm95-manual-review/assignment/cm95-layer-assignment-v1.json
tools/style-visual-object/output/cm95-manual-review/validation-report-v1.json
```
当前最终报告:
```text
tools/style-visual-object/output/CM95_Visual_Object_Layer命名报告_v2.md
```

View File

@@ -1,374 +0,0 @@
#!/usr/bin/env python3
"""把人工视觉复核结果应用到 CM95 Anonymous Visual Object Catalog。
本脚本不参与视觉判断,也不读取 CM95 source-layer 来决定 Layer。
它只消费已经完成的 reviewed anchors,并生成独立的 Layer Assignment 派生结果。
Anonymous Catalog 本身保持不变。
"""
from __future__ import annotations
import argparse
import csv
import html
import json
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def anchor_layers(anchor: dict[str, Any] | None) -> list[str]:
if not anchor:
return []
if anchor.get("standard_layer"):
return [str(anchor["standard_layer"])]
return [str(x) for x in anchor.get("standard_layers", []) if x]
def build_html(result: dict[str, Any]) -> str:
summary = result["summary"]
layer_rows: list[str] = []
for layer, stats in sorted(result["layer_summary"].items()):
layer_rows.append(
"<tr>"
f"<td>{html.escape(layer)}</td>"
f"<td>{stats['MATCH']}</td>"
f"<td>{stats['PARTIAL']}</td>"
f"<td>{stats['occurrences']}</td>"
"</tr>"
)
ambiguous_rows: list[str] = []
for item in result["ambiguous_summary"]:
ambiguous_rows.append(
"<tr>"
f"<td>{html.escape(' / '.join(item['candidate_standard_layers']))}</td>"
f"<td>{item['resource_count']}</td>"
f"<td>{item['occurrence_count']}</td>"
"</tr>"
)
group_rows: list[str] = []
for row in result["group_summary"]:
dist = ", ".join(
f"{html.escape(k)}={v}"
for k, v in sorted(row["assigned_layer_counts"].items())
) or "—"
group_rows.append(
"<tr>"
f"<td><code>{html.escape(row['group_id'])}</code></td>"
f"<td>{row['named_occurrences']}</td>"
f"<td>{row['ambiguous_occurrences']}</td>"
f"<td>{row['component_occurrences']}</td>"
f"<td>{row['unknown_occurrences']}</td>"
f"<td>{dist}</td>"
"</tr>"
)
return f"""<!doctype html>
<meta charset="utf-8">
<title>CM95 Visual Object Layer Assignment v1</title>
<style>
body{{font-family:Arial,"Noto Sans CJK JP",sans-serif;margin:24px;color:#17202a}}
.box{{display:inline-block;border:1px solid #d8dee6;border-radius:8px;padding:10px 14px;margin:0 10px 12px 0}}
table{{border-collapse:collapse;width:100%;margin:12px 0 28px}}
th,td{{border:1px solid #d8dee6;padding:7px;font-size:13px;text-align:left}}
th{{background:#eef2f6}}
code{{font-size:11px}}
.note{{background:#fff7df;border:1px solid #ead99c;padding:12px;border-radius:8px}}
</style>
<h1>CM95 Anonymous Visual Object → NavSea Standard Layer</h1>
<p>本结果只应用经人工视觉复核的共同图标/视觉家族锚点。CM95 source-layer 名称、Provider 字段语义和对象类名称均未参与 Layer 判定。</p>
<div class="box">CM95 Visual Objects <b>{summary['total_visual_objects']}</b></div>
<div class="box">Sprite-backed <b>{summary['sprite_backed_visual_objects']}</b></div>
<div class="box">唯一 Layer 已命名 <b>{summary['uniquely_named_visual_objects']}</b></div>
<div class="box">MATCH <b>{summary['match_visual_objects']}</b></div>
<div class="box">PARTIAL <b>{summary['partial_visual_objects']}</b></div>
<div class="box">AMBIGUOUS <b>{summary['ambiguous_visual_objects']}</b></div>
<div class="box">COMPONENT <b>{summary['component_visual_objects']}</b></div>
<div class="box">UNKNOWN <b>{summary['unknown_visual_objects']}</b></div>
<div class="box">Reviewed anchors <b>{summary['reviewed_anchor_count']}</b></div>
<div class="note">
<b>状态定义:</b>
MATCH = 实际图形基本一致;
PARTIAL = 不是同一完整图形,但视觉家族/组件足以支持唯一标准 Layer;
AMBIGUOUS = 图形证据只能得到多个标准 Layer 候选,禁止强行选一个;
COMPONENT = 只是完整 Visual Object 的组成部分,不单独分配 Layer;
UNKNOWN = 当前视觉证据不足。
</div>
<h2>唯一命名的标准 Layer</h2>
<table>
<thead><tr><th>标准 Layer</th><th>MATCH</th><th>PARTIAL</th><th>VO occurrence</th></tr></thead>
<tbody>{''.join(layer_rows)}</tbody>
</table>
<h2>视觉歧义</h2>
<table>
<thead><tr><th>候选标准 Layer</th><th>资源数</th><th>VO occurrence</th></tr></thead>
<tbody>{''.join(ambiguous_rows) if ambiguous_rows else '<tr><td colspan="3">无</td></tr>'}</tbody>
</table>
<h2>匿名 Group 分布</h2>
<p>Group 仅用于审计组合范围,不能由整个 group 直接继承一个 Layer。</p>
<table>
<thead><tr><th>Group</th><th>唯一命名</th><th>AMBIGUOUS</th><th>COMPONENT</th><th>UNKNOWN</th><th>唯一 Layer 分布</th></tr></thead>
<tbody>{''.join(group_rows)}</tbody>
</table>
<p>逐 Visual Object 明细见 <code>cm95-layer-assignment-v1.json</code>;
资源级摘要见 <code>cm95-layer-assignment-v1.csv</code>。
Anonymous Catalog 没有被修改。</p>
"""
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--anonymous", required=True)
ap.add_argument("--reviewed-anchors", required=True)
ap.add_argument("--output", required=True)
args = ap.parse_args()
anon_path = Path(args.anonymous)
anchor_path = Path(args.reviewed_anchors)
out = Path(args.output)
out.mkdir(parents=True, exist_ok=True)
anon = load_json(anon_path)
review = load_json(anchor_path)
anchors = {
x["cm95_resource_key"]: x
for x in review.get("anchors", [])
}
group_provenance = {
g["group_id"]: g.get("provenance", {})
for g in anon.get("groups", [])
}
assignments: list[dict[str, Any]] = []
layer_counts: dict[str, Counter] = defaultdict(Counter)
group_counts: dict[str, Counter] = defaultdict(Counter)
resource_status: dict[str, Counter] = defaultdict(Counter)
ambiguous_occurrence_counts: Counter = Counter()
for obj in anon.get("visual_objects", []):
resource_key = obj.get("resource_key")
anchor = anchors.get(resource_key)
candidate_layers = anchor_layers(anchor)
if anchor and anchor.get("status") in {"MATCH", "PARTIAL"} and len(candidate_layers) == 1:
status = str(anchor["status"])
layer = candidate_layers[0]
basis = "REVIEWED_VISUAL_ANCHOR"
std_resource = anchor.get("standard_resource_key")
layer_counts[layer][status] += 1
layer_counts[layer]["occurrences"] += 1
group_counts[obj["group_id"]][layer] += 1
group_counts[obj["group_id"]]["__named__"] += 1
resource_status[resource_key][status] += 1
elif anchor and anchor.get("status") == "AMBIGUOUS" and len(candidate_layers) >= 2:
status = "AMBIGUOUS"
layer = None
basis = "REVIEWED_VISUAL_AMBIGUITY"
std_resource = anchor.get("standard_resource_key")
group_counts[obj["group_id"]]["__ambiguous__"] += 1
resource_status[resource_key]["AMBIGUOUS"] += 1
ambiguous_occurrence_counts[tuple(sorted(candidate_layers))] += 1
elif anchor and anchor.get("status") == "COMPONENT":
status = "COMPONENT"
layer = None
candidate_layers = []
basis = "REVIEWED_COMPONENT_ONLY"
std_resource = None
group_counts[obj["group_id"]]["__component__"] += 1
resource_status[resource_key]["COMPONENT"] += 1
else:
status = "UNKNOWN"
layer = None
candidate_layers = []
basis = "INSUFFICIENT_VISUAL_EVIDENCE"
std_resource = None
group_counts[obj["group_id"]]["__unknown__"] += 1
if resource_key:
resource_status[resource_key]["UNKNOWN"] += 1
assignments.append({
"visual_object_id": obj["visual_object_id"],
"group_id": obj["group_id"],
"component_role": obj.get("component_role"),
"geometry_role": obj.get("geometry_role"),
"cm95_resource_key": resource_key,
"assignment_status": status,
"assigned_standard_layer": layer,
"candidate_standard_layers": candidate_layers,
"basis": basis,
"reviewed_standard_resource_key": std_resource,
"component_kind": anchor.get("component_kind") if anchor and status == "COMPONENT" else None,
# provenance 仅用于结果审计;不参与上述判定。
"provenance_for_audit_only": obj.get("provenance"),
})
for obj in anon.get("unresolved_visual_objects", []):
assignments.append({
"visual_object_id": obj["visual_object_id"],
"group_id": obj["group_id"],
"component_role": obj.get("component_role"),
"geometry_role": obj.get("geometry_role"),
"cm95_resource_key": None,
"assignment_status": "UNKNOWN",
"assigned_standard_layer": None,
"candidate_standard_layers": [],
"basis": "UNRESOLVED_DYNAMIC_VISUAL",
"reviewed_standard_resource_key": None,
"component_kind": None,
"provenance_for_audit_only": obj.get("provenance"),
})
group_counts[obj["group_id"]]["__unknown__"] += 1
group_summary: list[dict[str, Any]] = []
for gid in sorted(group_provenance):
c = group_counts[gid]
group_summary.append({
"group_id": gid,
"named_occurrences": c["__named__"],
"ambiguous_occurrences": c["__ambiguous__"],
"component_occurrences": c["__component__"],
"unknown_occurrences": c["__unknown__"],
"assigned_layer_counts": {
k: v for k, v in c.items() if not k.startswith("__")
},
"provenance_for_audit_only": group_provenance[gid],
})
layer_summary: dict[str, Any] = {}
for layer, c in layer_counts.items():
layer_summary[layer] = {
"MATCH": c["MATCH"],
"PARTIAL": c["PARTIAL"],
"occurrences": c["occurrences"],
}
all_resource_keys = sorted({
o.get("resource_key")
for o in anon.get("visual_objects", [])
if o.get("resource_key")
})
resource_summary: list[dict[str, Any]] = []
for key in all_resource_keys:
a = anchors.get(key)
layers = anchor_layers(a)
status = str(a["status"]) if a else "UNKNOWN"
unique_layer = layers[0] if status in {"MATCH", "PARTIAL"} and len(layers) == 1 else None
c = resource_status[key]
resource_summary.append({
"cm95_resource_key": key,
"assignment_status": status,
"assigned_standard_layer": unique_layer,
"candidate_standard_layers": layers,
"reviewed_standard_resource_key": a.get("standard_resource_key") if a else None,
"component_kind": a.get("component_kind") if a and status == "COMPONENT" else None,
"occurrence_count": sum(c.values()),
})
ambiguous_resource_counts: Counter = Counter()
for key, a in anchors.items():
if a.get("status") == "AMBIGUOUS":
ambiguous_resource_counts[tuple(sorted(anchor_layers(a)))] += 1
ambiguous_summary = [
{
"candidate_standard_layers": list(layer_set),
"resource_count": ambiguous_resource_counts[layer_set],
"occurrence_count": ambiguous_occurrence_counts[layer_set],
}
for layer_set in sorted(ambiguous_resource_counts)
]
total = len(assignments)
match_count = sum(x["assignment_status"] == "MATCH" for x in assignments)
partial_count = sum(x["assignment_status"] == "PARTIAL" for x in assignments)
ambiguous_count = sum(x["assignment_status"] == "AMBIGUOUS" for x in assignments)
component_count = sum(x["assignment_status"] == "COMPONENT" for x in assignments)
unknown_count = sum(x["assignment_status"] == "UNKNOWN" for x in assignments)
uniquely_named = match_count + partial_count
sprite_backed = sum(bool(x.get("cm95_resource_key")) for x in assignments)
unique_layers: set[str] = set()
for a in anchors.values():
unique_layers.update(anchor_layers(a))
result = {
"schema": "cm95-visual-object-layer-assignment/v1",
"source_anonymous_catalog": str(anon_path),
"reviewed_anchor_file": str(anchor_path),
"policy": {
"cm95_source_layer_used_for_assignment": False,
"provider_field_semantics_used_for_assignment": False,
"standard_layer_is_output_only": True,
"unknown_is_allowed": True,
"ambiguous_is_allowed": True,
"component_is_allowed": True,
"whole_group_inheritance_forbidden": True,
"anonymous_catalog_modified": False,
},
"summary": {
"total_visual_objects": total,
"sprite_backed_visual_objects": sprite_backed,
"reviewed_anchor_count": len(anchors),
"reviewed_standard_layer_candidates": sorted(unique_layers),
"uniquely_named_visual_objects": uniquely_named,
"match_visual_objects": match_count,
"partial_visual_objects": partial_count,
"ambiguous_visual_objects": ambiguous_count,
"component_visual_objects": component_count,
"unknown_visual_objects": unknown_count,
"uniquely_named_ratio": (uniquely_named / total) if total else 0.0,
"sprite_backed_uniquely_named_ratio": (uniquely_named / sprite_backed) if sprite_backed else 0.0,
},
"layer_summary": layer_summary,
"ambiguous_summary": ambiguous_summary,
"resource_summary": resource_summary,
"group_summary": group_summary,
"assignments": assignments,
}
(out / "cm95-layer-assignment-v1.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
(out / "cm95-layer-assignment-v1.html").write_text(
build_html(result), encoding="utf-8"
)
with (out / "cm95-layer-assignment-v1.csv").open("w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(
f,
lineterminator="\n",
fieldnames=[
"cm95_resource_key",
"assignment_status",
"assigned_standard_layer",
"candidate_standard_layers",
"reviewed_standard_resource_key",
"component_kind",
"occurrence_count",
],
)
writer.writeheader()
for row in resource_summary:
writer.writerow({
**row,
"candidate_standard_layers": ";".join(row["candidate_standard_layers"]),
})
print(json.dumps(result["summary"], ensure_ascii=False, indent=2))
print(json.dumps(result["layer_summary"], ensure_ascii=False, indent=2))
print(json.dumps(result["ambiguous_summary"], ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -1,461 +0,0 @@
#!/usr/bin/env python3
"""生成 CM95 / NavSea Visual Object Reviewer 使用的中文名称补充表。
注意:
- 该文件只用于 UI 显示和人工理解;
- 不参与 Visual Object 匹配、聚类、Layer 判定;
- CM95 英文说明来自当前 CM95 仓库中的 S-52 metadata;
- 中文名称为工程显示译名,不冒充 IHO 官方中文译名。
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parent
OUTPUT = ROOT / "output"
CM95_ROOT = Path("/root/sourceserver/cm95")
LAYER_ZH = {
"anchor_caution_hazard_outline": "锚泊注意/危险区轮廓",
"anchor_caution_hazard_point": "锚泊注意/危险点",
"anchorage_area": "锚地面",
"anchorage_outline": "锚地轮廓",
"anchorage_point": "锚地点",
"baseline_area": "基础区域",
"baseline_line": "基础线",
"baseline_outline": "基础区域轮廓",
"bathymetry_line": "海底地形线",
"bridge_structure": "桥梁结构",
"clearance_limit_line": "净空限制线",
"clearance_limit_point": "净空限制点",
"clip_outline_754": "特殊边界线",
"depth_contour": "等深线",
"depth_zone_739": "水深分区 739",
"depth_zone_741": "水深分区 741",
"facility_boundary_outline": "设施边界轮廓",
"facility_boundary_point": "港口/设施点",
"fixed_fishing_gear_area": "定置渔具区域",
"hazard_boundary_outline": "危险边界轮廓",
"hole_area": "孔洞/挖空区域",
"land_area": "陆地区域",
"navigation_hazard_outline": "航行危险轮廓",
"navigation_hazard_point": "航行危险点",
"navigation_marks": "航路标识",
"onshore_structure_area": "陆上结构物面",
"onshore_structure_line": "陆上结构物线",
"onshore_structure_point": "陆上结构物点",
"pilot_station_point": "引航站",
"place_label_land": "陆地地名",
"place_label_sea": "海域地名",
"seabed_line": "海底对象线",
"seabed_text_point": "海底文字点",
"submerged_reef_area": "水下礁区域",
"tide": "潮汐",
}
STD_KEY_ZH = {
"tidespot-daytime": "潮汐点",
"pattern_fill_fishery_or_fish_reef_area": "定置渔业/鱼礁填充",
"special_pattern_line_754": "特殊边界线",
"bollard_pile_stake": "系缆柱、桩、杭",
"cleared_danger_object": "扫海済危险物",
"dolphin_structure": "系泊墩",
"eddy_whirlpool": "涡流",
"fish_reef": "鱼礁",
"fish_reef_dangerous": "危险鱼礁",
"foul_ground": "碍锚海底",
"isolated_danger": "孤立危险物",
"obstruction": "障碍物",
"rock_exposed_drying_awash_group": "露出岩/干出岩/洗岩",
"sand_wave": "沙波",
"seaweed": "海草",
"subsea_installation_outfall_intake": "海底设施/放水口/取水口",
"sunken_rock": "暗岩",
"tide_rips_overfalls": "急潮/波纹",
"tower_yagura_observation_platform": "塔/架台/测台",
"wreck_fully_submerged_dangerous": "危险全沉船",
"wreck_hull_exposed": "船体或桅杆露出沉船",
"wreck_surveyed": "测定済沉船",
"anchorage_designated": "指定锚地",
"anchorage_quarantine": "检疫锚地",
"anchoring_prohibited": "禁止锚泊区",
"restriction_area_group": "限制区域",
"light_flare_green": "绿色灯光",
"light_flare_red": "红色灯光",
"light_flare_yellow_white": "黄色/白色灯光",
"harbor_lighthouse": "港湾灯塔",
"breakwater_lighthouse": "防波堤灯塔",
"coastal_lighthouse": "沿岸灯塔",
"light_minor": "小型灯标",
"light_beacon": "灯标",
"light_beacon_east_cardinal_variantt": "东方位灯标",
"light_beacon_isolated_danger_variantt_311359": "孤立危险标识灯标",
"light_beacon_north_cardinal_variantt": "北方位灯标",
"light_beacon_north_cardinal_variantt_311356": "北方位灯标",
"light_beacon_safe_water_variantt_311360": "安全水域标识灯标",
"light_beacon_south_cardinal_variantt": "南方位灯标",
"light_beacon_west_cardinal_variantt_311358": "西方位灯标",
"buoy_can": "罐形浮标",
"buoy_east_cardinal": "东方位浮标",
"buoy_isolated_danger": "孤立危险浮标",
"buoy_lattice": "架形浮标",
"buoy_north_cardinal": "北方位浮标",
"buoy_pillar": "柱形浮标",
"buoy_port_lateral_variantt_312350": "左舷浮标",
"buoy_safe_water": "安全水域浮标",
"buoy_south_cardinal": "南方位浮标",
"buoy_special_mark": "特殊标识浮标",
"buoy_starboard_lateral_variantt_312351": "右舷浮标",
"buoy_west_cardinal": "西方位浮标",
"mooring_buoy": "系泊浮标",
"fisherina": "渔港休闲设施",
"fishing_port": "渔港",
"marina": "游艇码头",
"port_general_small_harbor_group": "一般港湾/小港湾",
"sea_station_umi_no_eki": "海之驿",
"pilot_station": "引航站",
"symbol-daytime-401": "露出岩/干出岩/洗岩",
"symbol-daytime-404": "暗岩",
"symbol-daytime-405": "扫海済危险物",
"symbol-daytime-409": "孤立危险物",
"symbol-daytime-410": "船体或桅杆露出沉船",
"symbol-daytime-412": "危险全沉船",
"symbol-daytime-413": "测定済沉船",
"symbol-daytime-420": "碍锚海底",
"symbol-daytime-421": "沙波",
"symbol-daytime-422": "急潮/波纹",
"symbol-daytime-424": "涡流",
"symbol-daytime-425": "海草",
"symbol-daytime-427": "障碍物",
"symbol-daytime-428": "鱼礁",
"symbol-daytime-429": "危险鱼礁",
"symbol-daytime-431": "塔/架台/测台",
"symbol-daytime-432": "系缆柱、桩、杭",
"symbol-daytime-433": "系泊墩",
"symbol-daytime-434": "海底设施/放水口/取水口",
"symbol-daytime-640": "烟囱",
"symbol-daytime-650": "陆上显著结构物",
"symbol-daytime-660": "陆上显著结构物",
"symbol-daytime-661": "陆上显著结构物",
"symbol-daytime-681": "陆上显著结构物",
"symbol-daytime-698": "其他陆上结构物",
}
def color_zh(text: str) -> str:
replacements = [
("black/yellow/black", "黑/黄/黑"),
("yellow/black/yellow", "黄/黑/黄"),
("black/red/black", "黑/红/黑"),
("green/white", "绿/白"),
("red/white", "红/白"),
("black/yellow", "黑/黄"),
("yellow/black", "黄/黑"),
("white/black", "白/黑"),
("black/white", "黑/白"),
("white/red", "白/红"),
("red/green", "红/绿"),
("white or yellow", "白色或黄色"),
("yellow", "黄色"),
("green", "绿色"),
("red", "红色"),
("white", "白色"),
("black", "黑色"),
]
out = text
for en, zh in replacements:
out = out.replace(en, zh)
return out
def s52_zh(key: str, desc: str) -> tuple[str, str]:
"""返回 (中文显示名, 来源说明)。"""
d = desc.strip().lower()
if not d:
fallback = {
"STARPT01": "星形点标志",
"TOPMA115": "顶标",
"TOPMAR01": "浮标顶标",
}.get(key, "未提供说明")
return fallback, "工程显示译名"
exact = {
"anchorage area": "锚地",
"designated anchor berth for a single vessel": "指定单船锚泊位",
"symbol for opening bridge": "开启桥",
"berth number symbol": "泊位编号",
"built-up area": "建成区",
"coastguard station": "海岸警卫站",
"chimney": "烟囱",
"conspicuous chimney": "显著烟囱",
"ho information note": "海图机构信息注记",
"flare stack": "火炬塔",
"flagstaff, flagpole": "旗杆",
"fog signal": "雾号",
"fortified structure": "防御工事",
"fish haven": "鱼礁/鱼类栖息设施",
"fishing harbour": "渔港",
"light flare": "灯光标志",
"floodlight": "泛光灯",
"fish farm": "养殖场",
"mooring dolphin": "系泊墩",
"mast": "桅杆",
"offshore platform": "海上平台",
"pilot boarding place": "引航员登轮点",
"pile or bollard": "桩/系缆柱",
"traffic precautionary area": "通航注意区",
"radar reflector": "雷达反射器",
"radio station": "无线电台",
"radar transponder beacon": "雷达应答标",
"yacht harbour, marina": "游艇码头",
"sand waves": "沙波",
"traffic crossing area": "交通交叉区",
"traffic roundabout": "交通环岛",
"weed, kelp": "海草/海带",
"windmotor": "风车",
"submarine cable": "海底电缆",
"ferry route": "渡轮航线",
"fishing stakes": "定置渔桩",
"area with minor restrictions or information notices": "轻微限制/信息提示区域",
"beacon in general, black/yellow/back, full-chart": "一般立标(黑/黄/黑,完整显示)",
"beacon in general, yellow/back/yellow, full-chart": "一般立标(黄/黑/黄,完整显示)",
"beacon in general, black/red/back, full-chart": "一般立标(黑/红/黑,完整显示)",
"conical buoy,lateral-green,full-chart": "锥形侧面浮标(绿色,完整显示)",
"mooring buoy, can shape, paper-chart": "罐形系泊浮标(海图符号)",
"pillar buoy,cardianl-yby,full-chart": "柱形方位浮标(黄/黑/黄,完整显示)",
"topmark for buoys, cone point up, paper-chart": "浮标顶标:单锥尖向上",
"topmark for buoys, 2 cones point upward, paper-chart": "浮标顶标:双锥尖向上",
"topmark for buoys, 2 cones point downward, paper-chart": "浮标顶标:双锥尖向下",
"topmark for buoys, 2 cones base to base, paper-chart": "浮标顶标:双锥底相对",
"topmark for buoys, 2 cones point to point, paper-chart": "浮标顶标:双锥尖相对",
"topmark for buoys, sphere, paper-chart": "浮标顶标:球形",
"topmark for buoys, 2 spheres, paper-chart": "浮标顶标:双球",
"topmark for buoys, cylinder, paper-chart": "浮标顶标:圆柱形",
"beacon top mark, red boarded square board, vertical": "立标顶标:红边方形板(竖向)",
}
if d in exact:
return exact[d], "S-52 英文说明的中文显示译名"
if d.startswith("anchorage area as a point"):
return "锚地点", "S-52 英文说明的中文显示译名"
if d.startswith("beacon in general"):
suffix = d.split(",", 1)[1].strip() if "," in d else ""
return "一般立标" + (f"({color_zh(suffix.replace('full-chart','完整显示').replace('paper-chart','海图符号'))})" if suffix else ""), "S-52 英文说明的中文显示译名"
if "spar beacon" in d:
return color_zh(d.replace("spar beacon", "杆形立标").replace("full-chart", "完整显示")), "S-52 英文说明的中文显示译名"
if "stake or pole beacon" in d:
return color_zh(d.replace("minor, ", "小型").replace("stake or pole beacon", "桩/杆形立标").replace("full-chart", "完整显示")), "S-52 英文说明的中文显示译名"
if d.startswith("beacon tower"):
return color_zh(d.replace("beacon tower", "塔形立标").replace("full-chart", "完整显示").replace("brb", "黑/红/黑")), "S-52 英文说明的中文显示译名"
if "buoy" in d:
out = d
out = out.replace("can buoy", "罐形浮标").replace("conical buoy", "锥形浮标")
out = out.replace("mooring buoy", "系泊浮标").replace("pillar buoy", "柱形浮标")
out = out.replace("spherical buoy", "球形浮标").replace("spar buoy", "杆形浮标")
out = out.replace("super-buoy", "特大型浮标").replace("super buoy", "特大型浮标")
out = out.replace("default symbol for buoy", "一般浮标")
out = out.replace("paper-chart", "海图符号").replace("full-chart", "完整显示")
out = out.replace("lateral-r", "侧面标识-红").replace("lateral-g", "侧面标识-绿")
out = out.replace("special purpose-y", "特殊用途-黄")
out = out.replace("cardinal-byb", "方位标-黑/黄/黑").replace("cardinal-yby", "方位标-黄/黑/黄")
out = out.replace("cardinal-by", "方位标-黑/黄").replace("cardinal-yb", "方位标-黄/黑")
out = out.replace("isol.danger-brb", "孤立危险标-黑/红/黑").replace("safe water-rw", "安全水域标-红/白")
out = color_zh(out)
return out, "S-52 英文说明的中文显示译名"
if d.startswith("caution area"):
return "注意区域", "S-52 英文说明的中文显示译名"
if d.startswith("cautionary area"):
return "谨慎航行区域", "S-52 英文说明的中文显示译名"
if d.startswith("foul area of seabed"):
return "碍锚海底(航行安全但不宜锚泊)", "S-52 英文说明的中文显示译名"
if d.startswith("fish trap"):
return "渔具/鱼栅/金枪鱼网", "S-52 英文说明的中文显示译名"
if d.startswith("navigable lock gate"):
return "可通航闸门", "S-52 英文说明的中文显示译名"
if d.startswith("non-navigable lock gate"):
return "不可通航闸门", "S-52 英文说明的中文显示译名"
if d.startswith("light flare"):
return color_zh(d.replace("light flare", "灯光标志")), "S-52 英文说明的中文显示译名"
if d.startswith("light float"):
return "灯浮体", "S-52 英文说明的中文显示译名"
if d.startswith("light vessel"):
return color_zh(d.replace("light vessel", "灯船").replace("full-chart", "完整显示")), "S-52 英文说明的中文显示译名"
if d.startswith("cursor pick site for a magnetic anomaly"):
return "磁异常位置", "S-52 英文说明的中文显示译名"
if d.startswith("point feature or area of low accuracy"):
return "低位置精度对象/区域", "S-52 英文说明的中文显示译名"
if d.startswith("position of an elevation or control point"):
return "高程点/控制点", "S-52 英文说明的中文显示译名"
if d.startswith("object which is not sufficiently described"):
return "未充分描述/无对应符号的对象", "S-52 英文说明的中文显示译名"
if d.startswith("symbol indicating this object is radar conspicuous"):
return "雷达显著物标志", "S-52 英文说明的中文显示译名"
if d.startswith("recommended route"):
return "推荐航路", "S-52 英文说明的中文显示译名"
if d.startswith("topmark for buoys"):
tail = d.split(",", 1)[1].strip() if "," in d else ""
trans = tail.replace("2 cones point upward", "双锥尖向上").replace("2 cones point downward", "双锥尖向下")
trans = trans.replace("2 cones base to base", "双锥底相对").replace("2 cones point to point", "双锥尖相对")
trans = trans.replace("cone point up", "单锥尖向上").replace("2 spheres", "双球").replace("sphere", "球形").replace("cylinder", "圆柱形")
trans = trans.replace("paper-chart", "海图符号")
return f"浮标顶标:{trans}", "S-52 英文说明的中文显示译名"
if d.startswith("topmark for beacons"):
tail = d.split(",", 1)[1].strip() if "," in d else ""
trans = tail.replace("2 cones point upward", "双锥尖向上").replace("2 cones point downward", "双锥尖向下")
trans = trans.replace("2 cones base to base", "双锥底相对").replace("2 cones point to point", "双锥尖相对")
trans = trans.replace("cone point up", "单锥尖向上").replace("2 spheres", "双球").replace("sphere", "球形").replace("cylinder", "圆柱形")
trans = trans.replace("paper-chart", "海图符号")
return f"立标顶标:{trans}", "S-52 英文说明的中文显示译名"
if d.startswith("beacon top mark"):
return color_zh(d.replace("beacon top mark", "立标顶标").replace("square board", "方形板").replace("diagonal", "对角").replace("vertical", "竖向")), "S-52 英文说明的中文显示译名"
if d.startswith("tower"):
return color_zh(d.replace("tower", "塔").replace("full-chart and simplified", "完整/简化显示")), "S-52 英文说明的中文显示译名"
if d == "water tower":
return "水塔", "S-52 英文说明的中文显示译名"
if d == "conspicuous tower":
return "显著塔", "S-52 英文说明的中文显示译名"
if d == "radio, television tower":
return "无线电/电视塔", "S-52 英文说明的中文显示译名"
if d == "conspicuous radio, television tower":
return "显著无线电/电视塔", "S-52 英文说明的中文显示译名"
if d.startswith("traffic direction"):
return "分道通航单向航道交通方向", "S-52 英文说明的中文显示译名"
if d.startswith("overfalls, eddies and breakers"):
return "急流、涡流和碎波", "S-52 英文说明的中文显示译名"
if d.startswith("pattern of symbols for a fish haven"):
return "鱼礁填充图案", "S-52 英文说明的中文显示译名"
if d.startswith("pattern of symbols for a marine farm"):
return "养殖场填充图案", "S-52 英文说明的中文显示译名"
if d.startswith("pattern of symbols for a marsh"):
return "沼泽填充图案", "S-52 英文说明的中文显示译名"
if d.startswith("incompletely surveyed area"):
return "未完全测量区域", "S-52 英文说明的中文显示译名"
if d.startswith("pattern of symbols for an area which is not sufficiently described"):
return "未充分描述区域填充图案", "S-52 英文说明的中文显示译名"
if d.startswith("precautionary area or a traffic separation scheme"):
return "通航注意区/分道通航交叉或环岛", "S-52 英文说明的中文显示译名"
if d.startswith("pattern of symbols for wooded areas"):
return "林地区域填充图案", "S-52 英文说明的中文显示译名"
if d == "essa or pssa boundary":
return "特别敏感海域/环境敏感海域边界", "S-52 英文说明的中文显示译名"
if d.startswith("safety contour of low accuracy"):
return "低位置精度安全等深线", "S-52 英文说明的中文显示译名"
if d.startswith("coastline or shoreline construction of low accuracy"):
return "低位置精度海岸线/岸线构筑物", "S-52 英文说明的中文显示译名"
if d.startswith("oil, gas pipeline"):
return "油气管线(海底或陆上)", "S-52 英文说明的中文显示译名"
if d.startswith("water pipeline"):
return "水管/下水管等", "S-52 英文说明的中文显示译名"
if d.startswith("non-regulated recommended two-way track"):
return "非管制推荐双向航迹", "S-52 英文说明的中文显示译名"
if d.startswith("non-regulated recommended one-way track"):
return "非管制推荐单向航迹", "S-52 英文说明的中文显示译名"
# 兜底:避免伪造精确术语。仍展示英文原说明。
return "待人工确认中文名", "尚未建立可靠中文显示译名"
def build_cm95_names() -> dict[str, Any]:
cm = json.loads((OUTPUT / "cm95-anonymous" / "anonymous-catalog.json").read_text(encoding="utf-8"))
keys = sorted({o["resource_key"] for o in cm["visual_objects"] if o.get("resource_key")})
base = json.loads((CM95_ROOT / "build/s52_resources_v2/base/s52_symbol_metadata.json").read_text(encoding="utf-8"))
ap_raw = json.loads((CM95_ROOT / "build/s52_resources_v2/ap/s52-sprite-ap-pattern-metadata.json").read_text(encoding="utf-8"))
lc_raw = json.loads((CM95_ROOT / "build/s52_resources_v2/final/s52-sprite-full-line-metadata.json").read_text(encoding="utf-8"))
ap = {v.get("sprite_key"): v for v in ap_raw.values()}
lc = {v.get("sprite_key"): v for v in lc_raw.values()}
out: dict[str, Any] = {}
for key in keys:
rec = base.get(key) or ap.get(key) or lc.get(key)
source_type = "symbol" if key in base else ("fill-pattern" if key in ap else ("line-pattern" if key in lc else "unknown"))
desc = str((rec or {}).get("description") or "")
if not desc:
# 两个基础图标可从其同名 pattern metadata 得到安全的对象级概念。
if key == "FSHHAV02":
desc = "fish haven"
elif key == "MARSHES1":
desc = "marsh"
elif key == "CROSSX01":
desc = ""
zh, zh_source = s52_zh(key, desc)
if key == "MARSHES1":
zh, zh_source = "沼泽", "由同名 S-52 填充图案说明补充"
elif key == "FSHHAV02":
zh, zh_source = "鱼礁/鱼类栖息设施", "由同名 S-52 填充图案说明补充"
elif key == "CROSSX01":
zh, zh_source = "交叉标志(待确认)", "S-52 metadata 未找到说明,名称仅作 UI 提示"
out[key] = {
"code": key,
"resource_type": source_type,
"official_description_en": desc or None,
"display_name_zh": zh,
"display_name_source": zh_source,
}
return out
def std_name(key: str, layer: str) -> dict[str, Any]:
zh = STD_KEY_ZH.get(key)
source = "NavSea 标准资源语义键中文显示译名"
if not zh:
if key.startswith("leading_light"):
zh = "导灯"
elif key.startswith("leading_mark"):
zh = "导标"
elif key.startswith("arc-daytime"):
zh = "灯弧"
elif key.startswith("symbol-daytime"):
zh = LAYER_ZH.get(layer, "标准视觉对象")
source = "按标准 Layer 显示,具体对象名待补"
else:
zh = LAYER_ZH.get(layer, "标准视觉对象")
source = "按标准 Layer 显示,具体对象名待补"
return {
"resource_key": key,
"standard_layer": layer,
"standard_layer_zh": LAYER_ZH.get(layer, layer),
"display_name_zh": zh,
"display_name_source": source,
}
def build_std_names() -> dict[str, Any]:
std = json.loads((OUTPUT / "navsea-standard" / "standard-catalog.json").read_text(encoding="utf-8"))
out: dict[str, Any] = {}
for obj in std["visual_objects"]:
key = obj.get("resource_key")
layer = obj.get("standard_layer_name")
if not key or not layer:
continue
out[key] = std_name(key, layer)
return out
def main() -> None:
result = {
"schema": "visual-object-reference-names/v1",
"policy": {
"ui_only": True,
"used_for_matching": False,
"used_for_layer_assignment": False,
"chinese_names_are_display_translations": True,
},
"cm95": build_cm95_names(),
"navsea_standard": build_std_names(),
"standard_layer_zh": LAYER_ZH,
}
path = OUTPUT / "reference-name-enrichment-v1.json"
path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(path)
print("CM95", len(result["cm95"]))
print("NavSea Standard", len(result["navsea_standard"]))
print("CM95 待人工确认中文名", sum(x["display_name_zh"] == "待人工确认中文名" for x in result["cm95"].values()))
if __name__ == "__main__":
main()

View File

@@ -1,388 +0,0 @@
{
"schema": "cm95-provider-layer-semantic-rules/v1",
"purpose": "Provider Layer Classifier only. These rules are not part of Style Visual Object extraction.",
"rules": [
{
"id": "topmark",
"pattern": "^(topmark for |beacon top mark)",
"category_zh": "航标组件:顶标",
"component_kind": "topmark",
"layer_candidates": [
[
"navigation_marks",
0.98
]
],
"reason_zh": "S-52 官方说明明确为顶标。顶标通常不是独立对象,而是浮标/立标的组成部分。"
},
{
"id": "light_flare",
"pattern": "^light flare",
"category_zh": "航标组件:灯光",
"component_kind": "light",
"layer_candidates": [
[
"navigation_marks",
0.96
]
],
"reason_zh": "S-52 官方说明明确为灯光标志,属于航标显示组件。"
},
{
"id": "buoy",
"pattern": "\\bbuoy\\b",
"category_zh": "浮标/系泊浮标",
"layer_candidates": [
[
"navigation_marks",
0.98
]
],
"reason_zh": "S-52 官方说明明确为 buoy,属于航路标识。"
},
{
"id": "beacon",
"pattern": "\\bbeacon\\b|radar transponder beacon",
"category_zh": "立标/信标",
"layer_candidates": [
[
"navigation_marks",
0.98
]
],
"reason_zh": "S-52 官方说明明确为 beacon/信标,属于航路标识。"
},
{
"id": "light_vessel_float",
"pattern": "^light (float|vessel)",
"category_zh": "灯浮体/灯船",
"layer_candidates": [
[
"navigation_marks",
0.94
]
],
"reason_zh": "灯浮体和灯船属于助航标志。"
},
{
"id": "anchorage_point",
"pattern": "anchorage area as a point|anchor berth",
"category_zh": "锚地/锚泊位",
"layer_candidates": [
[
"anchorage_point",
0.96
]
],
"reason_zh": "S-52 说明为锚地点或单船锚泊位,优先归入锚地点。"
},
{
"id": "anchorage_area_symbol",
"pattern": "^anchorage area$",
"category_zh": "锚地",
"layer_candidates": [
[
"anchorage_point",
0.78
],
[
"anchorage_area",
0.68
]
],
"reason_zh": "当前资源是锚地符号;若实际 PBF 几何为点优先 anchorage_point,若为面则 anchorage_area。"
},
{
"id": "bridge",
"pattern": "bridge",
"category_zh": "桥梁结构",
"layer_candidates": [
[
"bridge_structure",
0.97
]
],
"reason_zh": "S-52 语义明确为桥梁。"
},
{
"id": "pilot",
"pattern": "pilot boarding place",
"category_zh": "引航设施",
"layer_candidates": [
[
"pilot_station_point",
0.96
]
],
"reason_zh": "S-52 语义为引航员登轮点,归入引航站类。"
},
{
"id": "harbor_facility",
"pattern": "fishing harbour|yacht harbour|marina|coastguard station|mooring dolphin|pile or bollard|berth number",
"category_zh": "港口/岸边设施",
"layer_candidates": [
[
"facility_boundary_point",
0.88
]
],
"reason_zh": "S-52 语义属于港口、码头或岸边设施点。"
},
{
"id": "beacon_tower",
"pattern": "^beacon tower",
"category_zh": "航标塔体",
"layer_candidates": [
[
"navigation_marks",
0.97
]
],
"reason_zh": "虽然外形像普通塔,但 S-52 官方说明明确是 beacon tower,因此属于航路标识。"
},
{
"id": "land_tower",
"pattern": "^(tower,|water tower$|conspicuous tower$|radio, television tower$|conspicuous radio, television tower$)",
"category_zh": "陆上塔状构筑物",
"layer_candidates": [
[
"onshore_structure_point",
0.97
]
],
"reason_zh": "S-52 官方说明是普通 tower,而不是 buoy/beacon;应作为固定陆上/岸上结构物理解。"
},
{
"id": "land_structure",
"pattern": "chimney|flare stack|flagstaff|flagpole|fortified structure|^mast$|windmotor|built-up area",
"category_zh": "陆上固定构筑物",
"layer_candidates": [
[
"onshore_structure_point",
0.93
]
],
"reason_zh": "S-52 语义是烟囱、旗杆、桅杆、风车等固定构筑物。"
},
{
"id": "radio_station",
"pattern": "^radio station$",
"category_zh": "陆上通信设施",
"layer_candidates": [
[
"onshore_structure_point",
0.82
]
],
"reason_zh": "无线电台属于固定通信设施,当前标准中最接近陆上结构物点。"
},
{
"id": "radar_nav",
"pattern": "radar reflector|radar conspicuous|radar transponder beacon",
"category_zh": "雷达助航标识",
"layer_candidates": [
[
"navigation_marks",
0.86
]
],
"reason_zh": "雷达反射器/应答标通常作为助航识别信息。"
},
{
"id": "fog_signal",
"pattern": "fog signal",
"category_zh": "雾号/助航设施",
"layer_candidates": [
[
"navigation_marks",
0.82
]
],
"reason_zh": "雾号属于助航设施,当前标准中优先归入航路标识。"
},
{
"id": "fish_gear",
"pattern": "fish trap|fish weir|tunny net|fish farm|marine farm|fishing stakes",
"category_zh": "定置渔具/养殖设施",
"layer_candidates": [
[
"fixed_fishing_gear_area",
0.94
]
],
"reason_zh": "S-52 语义明确为定置渔具或养殖设施。"
},
{
"id": "fish_haven",
"pattern": "fish haven",
"category_zh": "鱼礁/鱼类栖息设施",
"layer_candidates": [
[
"submerged_reef_area",
0.72
],
[
"navigation_hazard_point",
0.64
]
],
"reason_zh": "鱼礁/鱼类栖息设施属于水下对象;面积对象优先水下礁区域,点状符号可作为航行危险点。"
},
{
"id": "foul_ground",
"pattern": "foul area of seabed",
"category_zh": "碍锚海底",
"layer_candidates": [
[
"navigation_hazard_point",
0.72
],
[
"anchor_caution_hazard_point",
0.72
]
],
"reason_zh": "碍锚海底对锚泊/航行均有警示意义;当前标准存在两个可能的危险点 Layer,需要人工判断。"
},
{
"id": "seabed_hazard",
"pattern": "sand waves|weed, kelp|overfalls, eddies and breakers|magnetic anomaly",
"category_zh": "海底/水文注意对象",
"layer_candidates": [
[
"navigation_hazard_point",
0.68
],
[
"seabed_text_point",
0.48
]
],
"reason_zh": "属于海底或水文注意对象;当前标准缺少完全对应的专用点 Layer。"
},
{
"id": "caution",
"pattern": "caution area|cautionary area|traffic precautionary area|minor restrictions or information notices",
"category_zh": "注意/限制区域",
"layer_candidates": [
[
"anchor_caution_hazard_point",
0.66
],
[
"hazard_boundary_outline",
0.56
]
],
"reason_zh": "语义为注意或限制区域;点状符号可归注意/危险点,边界表达可归危险边界。"
},
{
"id": "submarine_cable_pipeline",
"pattern": "submarine cable|pipeline|sewer",
"category_zh": "海底电缆/管线",
"layer_candidates": [
[
"seabed_line",
0.94
]
],
"reason_zh": "S-52 语义明确为海底电缆或管线,归入海底对象线。"
},
{
"id": "safety_contour",
"pattern": "safety contour",
"category_zh": "安全等深线",
"layer_candidates": [
[
"depth_contour",
0.97
]
],
"reason_zh": "S-52 语义明确为安全等深线。"
},
{
"id": "coastline_lowacc",
"pattern": "coastline or shoreline construction",
"category_zh": "海岸线/岸线构筑物",
"layer_candidates": [
[
"baseline_line",
0.82
]
],
"reason_zh": "语义为海岸线或岸线构筑物,当前标准中最接近基础线。"
},
{
"id": "marsh_wooded",
"pattern": "marsh|wooded areas",
"category_zh": "陆地区域/植被区域",
"layer_candidates": [
[
"land_area",
0.86
]
],
"reason_zh": "沼泽或林地属于陆地区域表达。"
},
{
"id": "essa_pssa",
"pattern": "ESSA or PSSA boundary",
"category_zh": "环境敏感区域边界",
"layer_candidates": [
[
"hazard_boundary_outline",
0.78
]
],
"reason_zh": "属于受保护/敏感区域边界,当前标准中最接近危险/限制边界。"
},
{
"id": "traffic_route",
"pattern": "recommended route|traffic crossing area|traffic direction|traffic roundabout|ferry route|recommended two-way track|recommended one-way track",
"category_zh": "航路/交通组织",
"layer_candidates": [],
"reason_zh": "S-52 语义是航路或交通组织,但当前冻结 Standard Catalog 没有明显一一对应的 route Layer,暂不强推。"
},
{
"id": "question_unknown",
"pattern": "not sufficiently described|no symbol exists",
"category_zh": "未充分描述对象",
"layer_candidates": [],
"reason_zh": "S-52 本身就表示对象描述不足,不能可靠自动归 Layer。"
},
{
"id": "offshore_platform",
"pattern": "offshore platform",
"category_zh": "海上固定平台",
"layer_candidates": [
[
"navigation_hazard_point",
0.52
],
[
"facility_boundary_point",
0.46
]
],
"reason_zh": "属于海上固定设施;当前 Standard Catalog 缺少专用 offshore structure Layer,仅给低置信候选。"
},
{
"id": "fill_pattern_generic",
"resource_type": "fill-pattern",
"pattern": ".*",
"category_zh": "区域填充组件",
"component_kind": "fill_pattern",
"layer_candidates": [],
"reason_zh": "这是区域填充图案,本身可能只是完整对象的显示组件;优先结合 S-52 语义和实际 PBF 几何确认。"
},
{
"id": "line_pattern_generic",
"resource_type": "line-pattern",
"pattern": ".*",
"category_zh": "线型组件",
"component_kind": "line_pattern",
"layer_candidates": [],
"reason_zh": "这是线型图案,本身可能只是完整线对象的显示组件;优先结合 S-52 语义确认。"
}
]
}

View File

@@ -1,93 +0,0 @@
# CM95 Visual Object 与标准 Layer 命名报告 v1
日期:2026-10-05
## 1. 结论
本轮已经按固定三步流程完成一次 CM95 Visual Object 处理:
1. 使用当前 NavSea Style + Sprite 生成匿名 Visual Object,并在生成完成后挂标准 Layer 名;该 Standard Catalog 已冻结。
2. 使用同一套 provider-neutral Tool 独立生成 CM95 Anonymous Visual Object Catalog;生成阶段没有使用 NavSea Layer 名。
3. 只使用 Visual Object / Sprite 的实际视觉共同性进行人工复核和 Layer Assignment;CM95 source-layer 名称、Provider 字段语义和对象类名称不参与判定。
全部 CM95 Visual Object 都已经进入 `MATCH / PARTIAL / AMBIGUOUS / UNKNOWN` 四态之一。`UNKNOWN` 和 `AMBIGUOUS` 是正式结果,不进行强行补全。
## 2. 冻结的 NavSea Standard Visual Object Catalog
- Style:`NavSea Delivery Full Semantic ExtentFix`
- Style SHA256:`18b32ad1ea2d3798000abe21406d2f2a5225968910aed48d7ca5b1a1cb7f3ce6`
- Catalog 状态:`FROZEN`
- 匿名 group:`40`
- Visual Object:`143`
- Sprite-backed Visual Object:`99`
- 未完全展开的动态资源:`4`
后续处理 CM95 或其他 Provider 时,默认只读使用该 Standard Catalog;除非明确要求升级 Standard Catalog,否则不重新生成、不改 Visual Object ID、不改标准 Layer 名。
## 3. CM95 Anonymous Visual Object Catalog
- Style:`CM95 S-52 Japan nationwide comparison style v1`
- Style SHA256:`6c25ce3e5c1db65060be01ca899acf7f49228fb2277572dc1868c3d34019d4b0`
- 匿名 group:`27`
- Visual Object:`3699`
- Sprite-backed Visual Object:`3595`
- 当前 Style 正向可达的唯一视觉资源:`150`
注意:Sprite atlas 中存在但 Style 没有正向可达路径的资源不会进入 Anonymous Catalog;`!match` / `!=` 等排除列表中的资源也不会被误收为候选对象。
## 4. Visual Review 资源级结果
- Reviewed anchor:`88`
- MATCH 资源:`9`
- PARTIAL 资源:`74`
- AMBIGUOUS 资源:`5`
- UNKNOWN 资源:`62`
## 5. Visual Object occurrence 级 Layer Assignment
- 总 Visual Object:`3699`
- 唯一 Layer 已命名:`2224`(`60.12%`)
- MATCH:`152`
- PARTIAL:`2072`
- AMBIGUOUS:`140`
- UNKNOWN:`1335`
- Sprite-backed 对象的唯一 Layer 命名率:`61.86%`
### 已唯一命名的标准 Layer
- `anchorage_point`:MATCH `28`,PARTIAL `56`,合计 occurrence `84`
- `clip_outline_754`:MATCH `6`,PARTIAL `0`,合计 occurrence `6`
- `facility_boundary_point`:MATCH `56`,PARTIAL `0`,合计 occurrence `56`
- `navigation_marks`:MATCH `6`,PARTIAL `1876`,合计 occurrence `1882`
- `onshore_structure_point`:MATCH `28`,PARTIAL `140`,合计 occurrence `168`
- `pilot_station_point`:MATCH `28`,PARTIAL `0`,合计 occurrence `28`
### AMBIGUOUS
- `anchor_caution_hazard_point` / `navigation_hazard_point`:资源 `1`,occurrence `28`
- `navigation_marks` / `onshore_structure_point`:资源 `4`,occurrence `112`
其中危险物的歧义来自 NavSea Standard Catalog 自身:同一实际视觉图形同时被 `navigation_hazard_point` 与 `anchor_caution_hazard_point` 复用,因此视觉匹配不能擅自二选一。
## 6. 已确认的通用规则
- Layer 名是 Visual Match 的输出,不是匹配输入。
- `source-layer` 只作为 provenance / audit,不参与 CM95 Layer 判定。
- Sprite 存在不等于 Style 会绘制。
- 不允许整个匿名 group 继承单一 Layer;本轮实际证明多个 CM95 group 同时允许多个不同视觉家族。
- Layer Assignment 以单个 Visual Object / Visual Resource 为基本单位。
- 同图多 Layer 必须输出 AMBIGUOUS。
- 证据不足时保持 UNKNOWN。
## 7. 主要输出
- `tools/style-visual-object/output/navsea-standard/standard-catalog.json`
- `tools/style-visual-object/output/cm95-anonymous/anonymous-catalog.json`
- `tools/style-visual-object/output/cm95-visual-anchor-match/reviewed-anchors-v1.json`
- `tools/style-visual-object/output/cm95-layer-assignment/cm95-layer-assignment-v1.json`
- `tools/style-visual-object/output/cm95-layer-assignment/cm95-layer-assignment-v1.csv`
- `tools/style-visual-object/output/cm95-layer-assignment/cm95-layer-assignment-v1.html`
## 8. 当前边界
本轮没有为了追求 100% 覆盖率而使用 CM95 source-layer 名、S-52 名称文字含义或 Provider 私有字段去补答案。剩余 UNKNOWN 需要未来通过更多标准 Visual Object、更多视觉共同性或明确的人工视觉确认逐步收敛。

View File

@@ -1,105 +0,0 @@
# CM95 → NavSea Standard Layer 人工分类最终报告 v2
日期:2026-10-06
## 1. 最终结论
CM95 当前 Style 正向可达的 150 个唯一视觉资源已全部完成人工分类,未确认项为 0。
最终目标是把 CM95/S-52 对象归入现有 NavSea Standard Layer;Standard Visual Object 一一对应仅作为可选辅助证据。
## 2. 冻结输入基线
- CM95 Style SHA256:`6c25ce3e5c1db65060be01ca899acf7f49228fb2277572dc1868c3d34019d4b0`
- NavSea Standard Style SHA256:`18b32ad1ea2d3798000abe21406d2f2a5225968910aed48d7ca5b1a1cb7f3ce6`
- NavSea Standard Catalog 状态:`FROZEN`
- CM95 唯一视觉资源:`150`
- CM95 Anonymous Visual Object occurrence:`3699`
## 3. 人工资源级分类
- `MATCH`:`14`
- `PARTIAL`:`93`
- `AMBIGUOUS`:`6`
- `COMPONENT`:`35`
- `UNKNOWN`:`2`
Assignment mode:
- `VISUAL_OBJECT_MATCH`:`67`
- `LAYER_ONLY`:`46`
- `COMPONENT_ONLY`:`35`
- `UNKNOWN`:`2`
其中 `LAYER_ONLY` 是正常结果:表示所属 NavSea Layer 已确认,但当前 Standard Catalog 中没有要求必须绑定的一一对应标准图标。
## 4. 最终 occurrence 级结果
- 总 Visual Object:`3699`
- 唯一确定 NavSea Layer:`2536`
- MATCH:`323`
- PARTIAL:`2213`
- AMBIGUOUS:`168`
- COMPONENT:`835`
- UNKNOWN:`160`
### 已实际使用的唯一标准 Layer
- `anchor_caution_hazard_point`:occurrence `178`(MATCH `117` / PARTIAL `61`)
- `anchorage_point`:occurrence `84`(MATCH `84` / PARTIAL `0`)
- `baseline_line`:occurrence `6`(MATCH `0` / PARTIAL `6`)
- `bridge_structure`:occurrence `28`(MATCH `0` / PARTIAL `28`)
- `clip_outline_754`:occurrence `52`(MATCH `0` / PARTIAL `52`)
- `depth_contour`:occurrence `6`(MATCH `0` / PARTIAL `6`)
- `facility_boundary_outline`:occurrence `28`(MATCH `0` / PARTIAL `28`)
- `facility_boundary_point`:occurrence `168`(MATCH `28` / PARTIAL `140`)
- `fixed_fishing_gear_area`:occurrence `88`(MATCH `5` / PARTIAL `83`)
- `hazard_boundary_outline`:occurrence `6`(MATCH `0` / PARTIAL `6`)
- `land_area`:occurrence `10`(MATCH `5` / PARTIAL `5`)
- `navigation_hazard_point`:occurrence `61`(MATCH `28` / PARTIAL `33`)
- `navigation_marks`:occurrence `1182`(MATCH `0` / PARTIAL `1182`)
- `onshore_structure_point`:occurrence `588`(MATCH `28` / PARTIAL `560`)
- `pilot_station_point`:occurrence `28`(MATCH `28` / PARTIAL `0`)
- `place_label_land`:occurrence `5`(MATCH `0` / PARTIAL `5`)
- `seabed_line`:occurrence `18`(MATCH `0` / PARTIAL `18`)
## 5. 校验结果
- 总体:`PASS`
- 硬校验项:`18` PASS / `0` FAIL
- 语义复核 warning:`7` 项
固定校验命令:
```bash
python3 tools/style-visual-object/validate_cm95_classification.py
```
### 人工结果与 S-52 语义规则的复核 warning
这些 warning 不会覆盖人工结果,只作为后续需要时复核的线索:
- `FSHHAV01` 鱼礁/鱼类栖息设施:人工 `anchor_caution_hazard_point`;语义建议 `submerged_reef_area`(0.72)
- `FSHHAV02` 鱼礁/鱼类栖息设施:人工 `navigation_hazard_point`;语义建议 `submerged_reef_area`(0.72)
- `BRTHNO01` 泊位编号:人工 `navigation_marks`;语义建议 `facility_boundary_point`(0.88)
- `MARSHES1` 沼泽:人工 `place_label_land`;语义建议 `land_area`(0.86)
- `MORFAC03` 系泊墩:人工 `anchor_caution_hazard_point`;语义建议 `facility_boundary_point`(0.88)
- `PILPNT02` 桩/系缆柱:人工 `anchor_caution_hazard_point`;语义建议 `facility_boundary_point`(0.88)
- `ap-FSHHAV02` 鱼礁填充图案:人工 `anchor_caution_hazard_point`;语义建议 `submerged_reef_area`(0.72)
## 6. Git 中的权威结果
- `output/cm95-manual-review/manual-review-v1.json`:150 项人工决定原始记录。
- `output/cm95-manual-review/reviewed-anchors-manual-v1.json`:非 UNKNOWN 的人工导出锚点。
- `output/cm95-manual-review/assignment/cm95-layer-assignment-v1.json`:最终 occurrence 级 Layer Assignment。
- `output/cm95-manual-review/assignment/cm95-layer-assignment-v1.csv`:资源级摘要。
- `output/cm95-manual-review/assignment/cm95-layer-assignment-v1.html`:可视报告。
- `output/cm95-manual-review/validation-report-v1.json`:机器校验结果。
- `cm95_semantic_layer_rules_v1.json`:CM95/S-52 Provider Layer Classifier 声明式规则。
- `validate_cm95_classification.py`:固定一致性校验逻辑。
## 7. 架构边界
- 通用 Style Visual Object Tool 继续保持 Provider-neutral:`Style + Sprite → Anonymous Visual Object`。
- Provider Layer Classifier 才允许使用 S-52 官方语义资料。
- CM95 `source-layer` 不作为 Layer 分类依据。
- 人工分类是最终权威;语义/视觉建议只作辅助。
- 后续 CM95 Style/Sprite 或 NavSea Standard Layer 基线不变时,不重复人工分类。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

View File

@@ -1,151 +0,0 @@
cm95_resource_key,assignment_status,assigned_standard_layer,candidate_standard_layers,reviewed_standard_resource_key,component_kind,occurrence_count
ACHARE02,PARTIAL,anchorage_point,anchorage_point,anchorage_designated,,28
ACHARE51,MATCH,anchorage_point,anchorage_point,anchorage_designated,,28
ACHBRT07,PARTIAL,anchorage_point,anchorage_point,anchorage_quarantine,,28
BCNGEN01,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN05,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN60,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN61,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN68,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN69,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN70,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN71,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN76,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNSPR62,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNSTK77,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNSTK78,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNTOW60,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BCNTOW76,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYCAN63,PARTIAL,navigation_marks,navigation_marks,,,28
BOYCON01,PARTIAL,navigation_marks,navigation_marks,,,28
BOYCON60,PARTIAL,navigation_marks,navigation_marks,,,28
BOYCON61,PARTIAL,navigation_marks,navigation_marks,,,28
BOYCON62,PARTIAL,navigation_marks,navigation_marks,,,28
BOYGEN03,PARTIAL,navigation_marks,navigation_marks,,,28
BOYMOR03,PARTIAL,navigation_marks,navigation_marks,,,28
BOYPIL01,PARTIAL,navigation_marks,navigation_marks,,,28
BOYPIL60,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL61,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL62,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL68,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL69,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL70,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL71,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL72,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL73,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYSPH62,PARTIAL,navigation_marks,navigation_marks,,,28
BOYSPR60,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYSPR61,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYSPR62,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYSUP01,PARTIAL,navigation_marks,navigation_marks,,,28
BOYSUP62,PARTIAL,navigation_marks,navigation_marks,,,28
BRIDGE01,UNKNOWN,,,,,28
BRTHNO01,UNKNOWN,,,,,28
BUAARE02,UNKNOWN,,,,,28
CGUSTA02,UNKNOWN,,,,,28
CHIMNY01,MATCH,onshore_structure_point,onshore_structure_point,symbol-daytime-640,,28
CHIMNY11,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-640,,28
CHINFO07,UNKNOWN,,,,,28
CROSSX01,UNKNOWN,,,,,5
CTNARE51,UNKNOWN,,,,,28
CTYARE51,UNKNOWN,,,,,28
FLASTK01,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
FLGSTF01,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
FOGSIG01,UNKNOWN,,,,,28
FORSTC01,UNKNOWN,,,,,28
FOULGND1,AMBIGUOUS,,navigation_hazard_point;anchor_caution_hazard_point,foul_ground,,28
FSHFAC02,UNKNOWN,,,,,34
FSHHAV01,UNKNOWN,,,,,28
FSHHAV02,UNKNOWN,,,,,5
GATCON03,UNKNOWN,,,,,28
GATCON04,UNKNOWN,,,,,28
HRBFAC09,MATCH,facility_boundary_point,facility_boundary_point,fishing_port,,28
INFARE51,UNKNOWN,,,,,28
LIGHTS11,MATCH,navigation_marks,navigation_marks,light_flare_red,,2
LIGHTS12,MATCH,navigation_marks,navigation_marks,light_flare_green,,2
LIGHTS13,MATCH,navigation_marks,navigation_marks,light_flare_yellow_white,,2
LIGHTS82,UNKNOWN,,,,,2
LITDEF11,UNKNOWN,,,,,2
LITFLT01,UNKNOWN,,,,,28
LITVES60,UNKNOWN,,,,,28
LOCMAG51,UNKNOWN,,,,,28
LOWACC01,UNKNOWN,,,,,6
MARCUL02,UNKNOWN,,,,,33
MARSHES1,UNKNOWN,,,,,5
MORFAC03,UNKNOWN,,,,,28
MSTCON04,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
OFSPLF01,UNKNOWN,,,,,28
PILBOP02,MATCH,pilot_station_point,pilot_station_point,pilot_station,,28
PILPNT02,UNKNOWN,,,,,28
POSGEN04,UNKNOWN,,,,,28
PRCARE51,UNKNOWN,,,,,28
QUESMRK1,UNKNOWN,,,,,36
RACNSP01,UNKNOWN,,,,,28
RADRFL03,UNKNOWN,,,,,28
RDOSTA02,UNKNOWN,,,,,28
RTLDEF51,UNKNOWN,,,,,28
RTPBCN02,UNKNOWN,,,,,28
SMCFAC02,MATCH,facility_boundary_point,facility_boundary_point,marina,,28
SNDWAV02,UNKNOWN,,,,,28
STARPT01,UNKNOWN,,,,,28
TOPMA115,PARTIAL,navigation_marks,navigation_marks,,,28
TOPMAR01,UNKNOWN,,,,,28
TOPMAR02,PARTIAL,navigation_marks,navigation_marks,,,28
TOPMAR05,PARTIAL,navigation_marks,navigation_marks,buoy_north_cardinal,,28
TOPMAR06,PARTIAL,navigation_marks,navigation_marks,,,28
TOPMAR07,PARTIAL,navigation_marks,navigation_marks,buoy_north_cardinal,,28
TOPMAR08,PARTIAL,navigation_marks,navigation_marks,buoy_north_cardinal,,28
TOPMAR10,PARTIAL,navigation_marks,navigation_marks,,,28
TOPMAR12,PARTIAL,navigation_marks,navigation_marks,,,28
TOPMAR13,PARTIAL,navigation_marks,navigation_marks,,,28
TOPMAR22,PARTIAL,navigation_marks,navigation_marks,,,28
TOPMAR25,PARTIAL,navigation_marks,navigation_marks,buoy_north_cardinal,,28
TOPMAR26,PARTIAL,navigation_marks,navigation_marks,buoy_north_cardinal,,28
TOPMAR27,PARTIAL,navigation_marks,navigation_marks,,,28
TOPMAR28,PARTIAL,navigation_marks,navigation_marks,buoy_north_cardinal,,28
TOPMAR30,UNKNOWN,,,,,28
TOPMAR32,UNKNOWN,,,,,28
TOPMAR33,PARTIAL,navigation_marks,navigation_marks,,,28
TOPMAR98,PARTIAL,navigation_marks,navigation_marks,,,28
TOPSHP51,PARTIAL,navigation_marks,navigation_marks,,,28
TOPSHPI1,PARTIAL,navigation_marks,navigation_marks,,,28
TOPSHPP2,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS02,AMBIGUOUS,,navigation_marks;onshore_structure_point,,,28
TOWERS03,AMBIGUOUS,,navigation_marks;onshore_structure_point,,,28
TOWERS05,AMBIGUOUS,,navigation_marks;onshore_structure_point,,,28
TOWERS15,AMBIGUOUS,,navigation_marks;onshore_structure_point,,,28
TOWERS60,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS61,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS63,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS71,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS72,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS78,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS79,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS80,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS83,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS85,PARTIAL,navigation_marks,navigation_marks,,,28
TOWERS95,PARTIAL,navigation_marks,navigation_marks,,,28
TSSCRS51,UNKNOWN,,,,,28
TSSLPT51,UNKNOWN,,,,,28
TSSRON51,UNKNOWN,,,,,28
WATTUR02,UNKNOWN,,,,,28
WEDKLP03,UNKNOWN,,,,,28
WIMCON01,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
ap-FSHHAV02,UNKNOWN,,,,,5
ap-MARCUL02,UNKNOWN,,,,,5
ap-MARSHES1,UNKNOWN,,,,,5
ap-PRTSUR01,UNKNOWN,,,,,5
ap-QUESMRK1,UNKNOWN,,,,,5
ap-TSSJCT02,UNKNOWN,,,,,5
ap-VEGATN03,UNKNOWN,,,,,5
lc-CBLSUB06,UNKNOWN,,,,,6
lc-ESSARE01,MATCH,clip_outline_754,clip_outline_754,special_pattern_line_754,,6
lc-FERYRT01,UNKNOWN,,,,,6
lc-FSHFAC02,UNKNOWN,,,,,6
lc-LOWACC01,UNKNOWN,,,,,6
lc-LOWACC21,UNKNOWN,,,,,6
lc-PIPSOL05,UNKNOWN,,,,,6
lc-PIPSOL06,UNKNOWN,,,,,6
lc-RECTRC09,UNKNOWN,,,,,6
lc-RECTRC10,UNKNOWN,,,,,6
lc-RECTRC11,UNKNOWN,,,,,6
1 cm95_resource_key assignment_status assigned_standard_layer candidate_standard_layers reviewed_standard_resource_key component_kind occurrence_count
2 ACHARE02 PARTIAL anchorage_point anchorage_point anchorage_designated 28
3 ACHARE51 MATCH anchorage_point anchorage_point anchorage_designated 28
4 ACHBRT07 PARTIAL anchorage_point anchorage_point anchorage_quarantine 28
5 BCNGEN01 PARTIAL navigation_marks navigation_marks light_beacon 28
6 BCNGEN05 PARTIAL navigation_marks navigation_marks light_beacon 28
7 BCNGEN60 PARTIAL navigation_marks navigation_marks light_beacon 28
8 BCNGEN61 PARTIAL navigation_marks navigation_marks light_beacon 28
9 BCNGEN68 PARTIAL navigation_marks navigation_marks light_beacon 28
10 BCNGEN69 PARTIAL navigation_marks navigation_marks light_beacon 28
11 BCNGEN70 PARTIAL navigation_marks navigation_marks light_beacon 28
12 BCNGEN71 PARTIAL navigation_marks navigation_marks light_beacon 28
13 BCNGEN76 PARTIAL navigation_marks navigation_marks light_beacon 28
14 BCNSPR62 PARTIAL navigation_marks navigation_marks light_beacon 28
15 BCNSTK77 PARTIAL navigation_marks navigation_marks light_beacon 28
16 BCNSTK78 PARTIAL navigation_marks navigation_marks light_beacon 28
17 BCNTOW60 PARTIAL navigation_marks navigation_marks buoy_pillar 28
18 BCNTOW76 PARTIAL navigation_marks navigation_marks buoy_pillar 28
19 BOYCAN63 PARTIAL navigation_marks navigation_marks 28
20 BOYCON01 PARTIAL navigation_marks navigation_marks 28
21 BOYCON60 PARTIAL navigation_marks navigation_marks 28
22 BOYCON61 PARTIAL navigation_marks navigation_marks 28
23 BOYCON62 PARTIAL navigation_marks navigation_marks 28
24 BOYGEN03 PARTIAL navigation_marks navigation_marks 28
25 BOYMOR03 PARTIAL navigation_marks navigation_marks 28
26 BOYPIL01 PARTIAL navigation_marks navigation_marks 28
27 BOYPIL60 PARTIAL navigation_marks navigation_marks buoy_pillar 28
28 BOYPIL61 PARTIAL navigation_marks navigation_marks buoy_pillar 28
29 BOYPIL62 PARTIAL navigation_marks navigation_marks buoy_pillar 28
30 BOYPIL68 PARTIAL navigation_marks navigation_marks buoy_pillar 28
31 BOYPIL69 PARTIAL navigation_marks navigation_marks buoy_pillar 28
32 BOYPIL70 PARTIAL navigation_marks navigation_marks buoy_pillar 28
33 BOYPIL71 PARTIAL navigation_marks navigation_marks buoy_pillar 28
34 BOYPIL72 PARTIAL navigation_marks navigation_marks buoy_pillar 28
35 BOYPIL73 PARTIAL navigation_marks navigation_marks buoy_pillar 28
36 BOYSPH62 PARTIAL navigation_marks navigation_marks 28
37 BOYSPR60 PARTIAL navigation_marks navigation_marks buoy_pillar 28
38 BOYSPR61 PARTIAL navigation_marks navigation_marks buoy_pillar 28
39 BOYSPR62 PARTIAL navigation_marks navigation_marks buoy_pillar 28
40 BOYSUP01 PARTIAL navigation_marks navigation_marks 28
41 BOYSUP62 PARTIAL navigation_marks navigation_marks 28
42 BRIDGE01 UNKNOWN 28
43 BRTHNO01 UNKNOWN 28
44 BUAARE02 UNKNOWN 28
45 CGUSTA02 UNKNOWN 28
46 CHIMNY01 MATCH onshore_structure_point onshore_structure_point symbol-daytime-640 28
47 CHIMNY11 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-640 28
48 CHINFO07 UNKNOWN 28
49 CROSSX01 UNKNOWN 5
50 CTNARE51 UNKNOWN 28
51 CTYARE51 UNKNOWN 28
52 FLASTK01 PARTIAL onshore_structure_point onshore_structure_point 28
53 FLGSTF01 PARTIAL onshore_structure_point onshore_structure_point 28
54 FOGSIG01 UNKNOWN 28
55 FORSTC01 UNKNOWN 28
56 FOULGND1 AMBIGUOUS navigation_hazard_point;anchor_caution_hazard_point foul_ground 28
57 FSHFAC02 UNKNOWN 34
58 FSHHAV01 UNKNOWN 28
59 FSHHAV02 UNKNOWN 5
60 GATCON03 UNKNOWN 28
61 GATCON04 UNKNOWN 28
62 HRBFAC09 MATCH facility_boundary_point facility_boundary_point fishing_port 28
63 INFARE51 UNKNOWN 28
64 LIGHTS11 MATCH navigation_marks navigation_marks light_flare_red 2
65 LIGHTS12 MATCH navigation_marks navigation_marks light_flare_green 2
66 LIGHTS13 MATCH navigation_marks navigation_marks light_flare_yellow_white 2
67 LIGHTS82 UNKNOWN 2
68 LITDEF11 UNKNOWN 2
69 LITFLT01 UNKNOWN 28
70 LITVES60 UNKNOWN 28
71 LOCMAG51 UNKNOWN 28
72 LOWACC01 UNKNOWN 6
73 MARCUL02 UNKNOWN 33
74 MARSHES1 UNKNOWN 5
75 MORFAC03 UNKNOWN 28
76 MSTCON04 PARTIAL onshore_structure_point onshore_structure_point 28
77 OFSPLF01 UNKNOWN 28
78 PILBOP02 MATCH pilot_station_point pilot_station_point pilot_station 28
79 PILPNT02 UNKNOWN 28
80 POSGEN04 UNKNOWN 28
81 PRCARE51 UNKNOWN 28
82 QUESMRK1 UNKNOWN 36
83 RACNSP01 UNKNOWN 28
84 RADRFL03 UNKNOWN 28
85 RDOSTA02 UNKNOWN 28
86 RTLDEF51 UNKNOWN 28
87 RTPBCN02 UNKNOWN 28
88 SMCFAC02 MATCH facility_boundary_point facility_boundary_point marina 28
89 SNDWAV02 UNKNOWN 28
90 STARPT01 UNKNOWN 28
91 TOPMA115 PARTIAL navigation_marks navigation_marks 28
92 TOPMAR01 UNKNOWN 28
93 TOPMAR02 PARTIAL navigation_marks navigation_marks 28
94 TOPMAR05 PARTIAL navigation_marks navigation_marks buoy_north_cardinal 28
95 TOPMAR06 PARTIAL navigation_marks navigation_marks 28
96 TOPMAR07 PARTIAL navigation_marks navigation_marks buoy_north_cardinal 28
97 TOPMAR08 PARTIAL navigation_marks navigation_marks buoy_north_cardinal 28
98 TOPMAR10 PARTIAL navigation_marks navigation_marks 28
99 TOPMAR12 PARTIAL navigation_marks navigation_marks 28
100 TOPMAR13 PARTIAL navigation_marks navigation_marks 28
101 TOPMAR22 PARTIAL navigation_marks navigation_marks 28
102 TOPMAR25 PARTIAL navigation_marks navigation_marks buoy_north_cardinal 28
103 TOPMAR26 PARTIAL navigation_marks navigation_marks buoy_north_cardinal 28
104 TOPMAR27 PARTIAL navigation_marks navigation_marks 28
105 TOPMAR28 PARTIAL navigation_marks navigation_marks buoy_north_cardinal 28
106 TOPMAR30 UNKNOWN 28
107 TOPMAR32 UNKNOWN 28
108 TOPMAR33 PARTIAL navigation_marks navigation_marks 28
109 TOPMAR98 PARTIAL navigation_marks navigation_marks 28
110 TOPSHP51 PARTIAL navigation_marks navigation_marks 28
111 TOPSHPI1 PARTIAL navigation_marks navigation_marks 28
112 TOPSHPP2 PARTIAL navigation_marks navigation_marks 28
113 TOWERS02 AMBIGUOUS navigation_marks;onshore_structure_point 28
114 TOWERS03 AMBIGUOUS navigation_marks;onshore_structure_point 28
115 TOWERS05 AMBIGUOUS navigation_marks;onshore_structure_point 28
116 TOWERS15 AMBIGUOUS navigation_marks;onshore_structure_point 28
117 TOWERS60 PARTIAL navigation_marks navigation_marks 28
118 TOWERS61 PARTIAL navigation_marks navigation_marks 28
119 TOWERS63 PARTIAL navigation_marks navigation_marks 28
120 TOWERS71 PARTIAL navigation_marks navigation_marks 28
121 TOWERS72 PARTIAL navigation_marks navigation_marks 28
122 TOWERS78 PARTIAL navigation_marks navigation_marks 28
123 TOWERS79 PARTIAL navigation_marks navigation_marks 28
124 TOWERS80 PARTIAL navigation_marks navigation_marks 28
125 TOWERS83 PARTIAL navigation_marks navigation_marks 28
126 TOWERS85 PARTIAL navigation_marks navigation_marks 28
127 TOWERS95 PARTIAL navigation_marks navigation_marks 28
128 TSSCRS51 UNKNOWN 28
129 TSSLPT51 UNKNOWN 28
130 TSSRON51 UNKNOWN 28
131 WATTUR02 UNKNOWN 28
132 WEDKLP03 UNKNOWN 28
133 WIMCON01 PARTIAL onshore_structure_point onshore_structure_point 28
134 ap-FSHHAV02 UNKNOWN 5
135 ap-MARCUL02 UNKNOWN 5
136 ap-MARSHES1 UNKNOWN 5
137 ap-PRTSUR01 UNKNOWN 5
138 ap-QUESMRK1 UNKNOWN 5
139 ap-TSSJCT02 UNKNOWN 5
140 ap-VEGATN03 UNKNOWN 5
141 lc-CBLSUB06 UNKNOWN 6
142 lc-ESSARE01 MATCH clip_outline_754 clip_outline_754 special_pattern_line_754 6
143 lc-FERYRT01 UNKNOWN 6
144 lc-FSHFAC02 UNKNOWN 6
145 lc-LOWACC01 UNKNOWN 6
146 lc-LOWACC21 UNKNOWN 6
147 lc-PIPSOL05 UNKNOWN 6
148 lc-PIPSOL06 UNKNOWN 6
149 lc-RECTRC09 UNKNOWN 6
150 lc-RECTRC10 UNKNOWN 6
151 lc-RECTRC11 UNKNOWN 6

View File

@@ -1,55 +0,0 @@
<!doctype html>
<meta charset="utf-8">
<title>CM95 Visual Object Layer Assignment v1</title>
<style>
body{font-family:Arial,"Noto Sans CJK JP",sans-serif;margin:24px;color:#17202a}
.box{display:inline-block;border:1px solid #d8dee6;border-radius:8px;padding:10px 14px;margin:0 10px 12px 0}
table{border-collapse:collapse;width:100%;margin:12px 0 28px}
th,td{border:1px solid #d8dee6;padding:7px;font-size:13px;text-align:left}
th{background:#eef2f6}
code{font-size:11px}
.note{background:#fff7df;border:1px solid #ead99c;padding:12px;border-radius:8px}
</style>
<h1>CM95 Anonymous Visual Object → NavSea Standard Layer</h1>
<p>本结果只应用经人工视觉复核的共同图标/视觉家族锚点。CM95 source-layer 名称、Provider 字段语义和对象类名称均未参与 Layer 判定。</p>
<div class="box">CM95 Visual Objects <b>3699</b></div>
<div class="box">Sprite-backed <b>3595</b></div>
<div class="box">唯一 Layer 已命名 <b>2224</b></div>
<div class="box">MATCH <b>152</b></div>
<div class="box">PARTIAL <b>2072</b></div>
<div class="box">AMBIGUOUS <b>140</b></div>
<div class="box">COMPONENT <b>0</b></div>
<div class="box">UNKNOWN <b>1335</b></div>
<div class="box">Reviewed anchors <b>88</b></div>
<div class="note">
<b>状态定义:</b>
MATCH = 实际图形基本一致;
PARTIAL = 不是同一完整图形,但视觉家族/组件足以支持唯一标准 Layer;
AMBIGUOUS = 图形证据只能得到多个标准 Layer 候选,禁止强行选一个;
COMPONENT = 只是完整 Visual Object 的组成部分,不单独分配 Layer;
UNKNOWN = 当前视觉证据不足。
</div>
<h2>唯一命名的标准 Layer</h2>
<table>
<thead><tr><th>标准 Layer</th><th>MATCH</th><th>PARTIAL</th><th>VO occurrence</th></tr></thead>
<tbody><tr><td>anchorage_point</td><td>28</td><td>56</td><td>84</td></tr><tr><td>clip_outline_754</td><td>6</td><td>0</td><td>6</td></tr><tr><td>facility_boundary_point</td><td>56</td><td>0</td><td>56</td></tr><tr><td>navigation_marks</td><td>6</td><td>1876</td><td>1882</td></tr><tr><td>onshore_structure_point</td><td>28</td><td>140</td><td>168</td></tr><tr><td>pilot_station_point</td><td>28</td><td>0</td><td>28</td></tr></tbody>
</table>
<h2>视觉歧义</h2>
<table>
<thead><tr><th>候选标准 Layer</th><th>资源数</th><th>VO occurrence</th></tr></thead>
<tbody><tr><td>anchor_caution_hazard_point / navigation_hazard_point</td><td>1</td><td>28</td></tr><tr><td>navigation_marks / onshore_structure_point</td><td>4</td><td>112</td></tr></tbody>
</table>
<h2>匿名 Group 分布</h2>
<p>Group 仅用于审计组合范围,不能由整个 group 直接继承一个 Layer。</p>
<table>
<thead><tr><th>Group</th><th>唯一命名</th><th>AMBIGUOUS</th><th>COMPONENT</th><th>UNKNOWN</th><th>唯一 Layer 分布</th></tr></thead>
<tbody><tr><td><code>GRP-0eb1b887613d3117</code></td><td>0</td><td>0</td><td>0</td><td>3</td><td>—</td></tr><tr><td><code>GRP-118c2f0bf0863e77</code></td><td>317</td><td>20</td><td>0</td><td>213</td><td>anchorage_point=12, clip_outline_754=1, facility_boundary_point=8, navigation_marks=268, onshore_structure_point=24, pilot_station_point=4</td></tr><tr><td><code>GRP-3ca77ff69bbae513</code></td><td>0</td><td>0</td><td>0</td><td>1</td><td>—</td></tr><tr><td><code>GRP-4368c84087270fd4</code></td><td>79</td><td>5</td><td>0</td><td>41</td><td>anchorage_point=3, facility_boundary_point=2, navigation_marks=67, onshore_structure_point=6, pilot_station_point=1</td></tr><tr><td><code>GRP-5a627b60da2761de</code></td><td>164</td><td>10</td><td>0</td><td>86</td><td>anchorage_point=6, facility_boundary_point=4, navigation_marks=140, onshore_structure_point=12, pilot_station_point=2</td></tr><tr><td><code>GRP-64125b100c4ca018</code></td><td>474</td><td>30</td><td>0</td><td>238</td><td>anchorage_point=18, facility_boundary_point=12, navigation_marks=402, onshore_structure_point=36, pilot_station_point=6</td></tr><tr><td><code>GRP-6425cff78278014f</code></td><td>0</td><td>0</td><td>0</td><td>2</td><td>—</td></tr><tr><td><code>GRP-64eeae4d26a05ac3</code></td><td>1</td><td>0</td><td>0</td><td>20</td><td>clip_outline_754=1</td></tr><tr><td><code>GRP-6c625dd02fb4d766</code></td><td>81</td><td>5</td><td>0</td><td>81</td><td>anchorage_point=3, clip_outline_754=2, facility_boundary_point=2, navigation_marks=67, onshore_structure_point=6, pilot_station_point=1</td></tr><tr><td><code>GRP-735ad3da23e1857e</code></td><td>0</td><td>0</td><td>0</td><td>2</td><td>—</td></tr><tr><td><code>GRP-7738e161f8470be0</code></td><td>79</td><td>5</td><td>0</td><td>42</td><td>anchorage_point=3, facility_boundary_point=2, navigation_marks=67, onshore_structure_point=6, pilot_station_point=1</td></tr><tr><td><code>GRP-7dddca2f59b6bb9f</code></td><td>0</td><td>0</td><td>0</td><td>35</td><td>—</td></tr><tr><td><code>GRP-7fc32eb9501b8414</code></td><td>79</td><td>5</td><td>0</td><td>43</td><td>anchorage_point=3, facility_boundary_point=2, navigation_marks=67, onshore_structure_point=6, pilot_station_point=1</td></tr><tr><td><code>GRP-805a8de96cbbb56d</code></td><td>158</td><td>10</td><td>0</td><td>78</td><td>anchorage_point=6, facility_boundary_point=4, navigation_marks=134, onshore_structure_point=12, pilot_station_point=2</td></tr><tr><td><code>GRP-8158b1d36777923b</code></td><td>0</td><td>0</td><td>0</td><td>15</td><td>—</td></tr><tr><td><code>GRP-93e05ed1dc014fb5</code></td><td>79</td><td>5</td><td>0</td><td>39</td><td>anchorage_point=3, facility_boundary_point=2, navigation_marks=67, onshore_structure_point=6, pilot_station_point=1</td></tr><tr><td><code>GRP-9dece9b2b501ed21</code></td><td>79</td><td>5</td><td>0</td><td>40</td><td>anchorage_point=3, facility_boundary_point=2, navigation_marks=67, onshore_structure_point=6, pilot_station_point=1</td></tr><tr><td><code>GRP-a2907670cc537678</code></td><td>237</td><td>15</td><td>0</td><td>118</td><td>anchorage_point=9, facility_boundary_point=6, navigation_marks=201, onshore_structure_point=18, pilot_station_point=3</td></tr><tr><td><code>GRP-a85fbd02b1a2353a</code></td><td>79</td><td>5</td><td>0</td><td>40</td><td>anchorage_point=3, facility_boundary_point=2, navigation_marks=67, onshore_structure_point=6, pilot_station_point=1</td></tr><tr><td><code>GRP-a90e6bd8ea5e247d</code></td><td>0</td><td>0</td><td>0</td><td>1</td><td>—</td></tr><tr><td><code>GRP-a975b300e119fe20</code></td><td>79</td><td>5</td><td>0</td><td>41</td><td>anchorage_point=3, facility_boundary_point=2, navigation_marks=67, onshore_structure_point=6, pilot_station_point=1</td></tr><tr><td><code>GRP-bf8e00c2d3aa9af1</code></td><td>0</td><td>0</td><td>0</td><td>2</td><td>—</td></tr><tr><td><code>GRP-dba4e6019fe110eb</code></td><td>2</td><td>0</td><td>0</td><td>30</td><td>clip_outline_754=2</td></tr><tr><td><code>GRP-ed62480b27dbc023</code></td><td>79</td><td>5</td><td>0</td><td>41</td><td>anchorage_point=3, facility_boundary_point=2, navigation_marks=67, onshore_structure_point=6, pilot_station_point=1</td></tr><tr><td><code>GRP-edea53f2ccdbe24a</code></td><td>158</td><td>10</td><td>0</td><td>79</td><td>anchorage_point=6, facility_boundary_point=4, navigation_marks=134, onshore_structure_point=12, pilot_station_point=2</td></tr><tr><td><code>GRP-fd773b57053bf635</code></td><td>0</td><td>0</td><td>0</td><td>2</td><td>—</td></tr><tr><td><code>GRP-fdccabd4cc28871b</code></td><td>0</td><td>0</td><td>0</td><td>2</td><td>—</td></tr></tbody>
</table>
<p>逐 Visual Object 明细见 <code>cm95-layer-assignment-v1.json</code>;
资源级摘要见 <code>cm95-layer-assignment-v1.csv</code>。
Anonymous Catalog 没有被修改。</p>

View File

@@ -1,151 +0,0 @@
cm95_resource_key,assignment_status,assigned_standard_layer,candidate_standard_layers,reviewed_standard_resource_key,component_kind,occurrence_count
ACHARE02,MATCH,anchorage_point,anchorage_point,anchorage_designated,,28
ACHARE51,MATCH,anchorage_point,anchorage_point,anchorage_designated,,28
ACHBRT07,MATCH,anchorage_point,anchorage_point,anchorage_quarantine,,28
BCNGEN01,PARTIAL,navigation_marks,navigation_marks,,,28
BCNGEN05,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN60,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN61,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN68,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN69,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN70,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN71,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNGEN76,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNSPR62,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNSTK77,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNSTK78,PARTIAL,navigation_marks,navigation_marks,light_beacon,,28
BCNTOW60,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BCNTOW76,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYCAN63,PARTIAL,navigation_marks,navigation_marks,buoy_can,,28
BOYCON01,PARTIAL,navigation_marks,navigation_marks,buoy_lattice,,28
BOYCON60,PARTIAL,navigation_marks,navigation_marks,buoy_lattice,,28
BOYCON61,PARTIAL,navigation_marks,navigation_marks,buoy_lattice,,28
BOYCON62,PARTIAL,navigation_marks,navigation_marks,buoy_lattice,,28
BOYGEN03,PARTIAL,navigation_marks,navigation_marks,,,28
BOYMOR03,PARTIAL,navigation_marks,navigation_marks,mooring_buoy,,28
BOYPIL01,PARTIAL,navigation_marks,navigation_marks,,,28
BOYPIL60,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL61,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL62,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL68,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL69,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL70,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL71,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL72,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYPIL73,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYSPH62,PARTIAL,navigation_marks,navigation_marks,,,28
BOYSPR60,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYSPR61,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYSPR62,PARTIAL,navigation_marks,navigation_marks,buoy_pillar,,28
BOYSUP01,PARTIAL,navigation_marks,navigation_marks,mooring_buoy,,28
BOYSUP62,PARTIAL,navigation_marks,navigation_marks,mooring_buoy,,28
BRIDGE01,PARTIAL,bridge_structure,bridge_structure,,,28
BRTHNO01,PARTIAL,navigation_marks,navigation_marks,leading_mark_variantt_308,,28
BUAARE02,AMBIGUOUS,,onshore_structure_line;onshore_structure_point,symbol-daytime-661,,28
CGUSTA02,PARTIAL,facility_boundary_point,facility_boundary_point,,,28
CHIMNY01,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-640,,28
CHIMNY11,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-640,,28
CHINFO07,PARTIAL,facility_boundary_outline,facility_boundary_outline,,,28
CROSSX01,PARTIAL,fixed_fishing_gear_area,fixed_fishing_gear_area,pattern_fill_fishery_or_fish_reef_area,,5
CTNARE51,PARTIAL,clip_outline_754,clip_outline_754,,,28
CTYARE51,PARTIAL,navigation_hazard_point,navigation_hazard_point,,,28
FLASTK01,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
FLGSTF01,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
FOGSIG01,PARTIAL,navigation_marks,navigation_marks,,,28
FORSTC01,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
FOULGND1,MATCH,navigation_hazard_point,navigation_hazard_point,foul_ground,,28
FSHFAC02,PARTIAL,fixed_fishing_gear_area,fixed_fishing_gear_area,pattern_fill_fishery_or_fish_reef_area,,34
FSHHAV01,PARTIAL,anchor_caution_hazard_point,anchor_caution_hazard_point,symbol-daytime-428,,28
FSHHAV02,PARTIAL,navigation_hazard_point,navigation_hazard_point,fish_reef,,5
GATCON03,PARTIAL,facility_boundary_point,facility_boundary_point,,,28
GATCON04,PARTIAL,facility_boundary_point,facility_boundary_point,,,28
HRBFAC09,PARTIAL,facility_boundary_point,facility_boundary_point,fishing_port,,28
INFARE51,COMPONENT,,,,auxiliary_mark,28
LIGHTS11,COMPONENT,,,,light,2
LIGHTS12,COMPONENT,,,,light,2
LIGHTS13,COMPONENT,,,,light,2
LIGHTS82,COMPONENT,,,,light,2
LITDEF11,COMPONENT,,,,light,2
LITFLT01,COMPONENT,,,,light,28
LITVES60,COMPONENT,,,,buoy_body,28
LOCMAG51,COMPONENT,,,,other,28
LOWACC01,PARTIAL,clip_outline_754,clip_outline_754,,,6
MARCUL02,PARTIAL,fixed_fishing_gear_area,fixed_fishing_gear_area,pattern_fill_fishery_or_fish_reef_area,,33
MARSHES1,PARTIAL,place_label_land,place_label_land,,,5
MORFAC03,MATCH,anchor_caution_hazard_point,anchor_caution_hazard_point,symbol-daytime-433,,28
MSTCON04,COMPONENT,,,,other,28
OFSPLF01,AMBIGUOUS,,navigation_hazard_outline;navigation_hazard_point,,,28
PILBOP02,MATCH,pilot_station_point,pilot_station_point,pilot_station,,28
PILPNT02,MATCH,anchor_caution_hazard_point,anchor_caution_hazard_point,symbol-daytime-432,,28
POSGEN04,UNKNOWN,,,,,28
PRCARE51,AMBIGUOUS,,navigation_hazard_outline;navigation_hazard_point,,,28
QUESMRK1,COMPONENT,,,,other,36
RACNSP01,AMBIGUOUS,,onshore_structure_area;onshore_structure_point,,,28
RADRFL03,AMBIGUOUS,,onshore_structure_line;onshore_structure_point,,,28
RDOSTA02,COMPONENT,,,,other,28
RTLDEF51,PARTIAL,navigation_marks,navigation_marks,,,28
RTPBCN02,AMBIGUOUS,,onshore_structure_line;onshore_structure_point,,,28
SMCFAC02,MATCH,facility_boundary_point,facility_boundary_point,marina,,28
SNDWAV02,MATCH,anchor_caution_hazard_point,anchor_caution_hazard_point,symbol-daytime-421,,28
STARPT01,PARTIAL,navigation_marks,navigation_marks,light_minor,,28
TOPMA115,COMPONENT,,,,topmark,28
TOPMAR01,COMPONENT,,,,topmark,28
TOPMAR02,COMPONENT,,,,topmark,28
TOPMAR05,COMPONENT,,,,topmark,28
TOPMAR06,COMPONENT,,,,topmark,28
TOPMAR07,COMPONENT,,,,topmark,28
TOPMAR08,COMPONENT,,,,topmark,28
TOPMAR10,COMPONENT,,,,topmark,28
TOPMAR12,COMPONENT,,,,topmark,28
TOPMAR13,COMPONENT,,,,topmark,28
TOPMAR22,COMPONENT,,,,topmark,28
TOPMAR25,COMPONENT,,,,topmark,28
TOPMAR26,COMPONENT,,,,topmark,28
TOPMAR27,COMPONENT,,,,topmark,28
TOPMAR28,COMPONENT,,,,topmark,28
TOPMAR30,COMPONENT,,,,topmark,28
TOPMAR32,COMPONENT,,,,topmark,28
TOPMAR33,COMPONENT,,,,topmark,28
TOPMAR98,COMPONENT,,,,topmark,28
TOPSHP51,COMPONENT,,,,topmark,28
TOPSHPI1,COMPONENT,,,,topmark,28
TOPSHPP2,COMPONENT,,,,topmark,28
TOWERS02,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
TOWERS03,MATCH,onshore_structure_point,onshore_structure_point,symbol-daytime-650,,28
TOWERS05,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-650,,28
TOWERS15,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-650,,28
TOWERS60,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-698,,28
TOWERS61,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-698,,28
TOWERS63,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-698,,28
TOWERS71,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-698,,28
TOWERS72,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-698,,28
TOWERS78,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-698,,28
TOWERS79,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-698,,28
TOWERS80,PARTIAL,onshore_structure_point,onshore_structure_point,symbol-daytime-698,,28
TOWERS83,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
TOWERS85,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
TOWERS95,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
TSSCRS51,UNKNOWN,,,,,28
TSSLPT51,PARTIAL,navigation_marks,navigation_marks,,,28
TSSRON51,PARTIAL,facility_boundary_point,facility_boundary_point,,,28
WATTUR02,MATCH,anchor_caution_hazard_point,anchor_caution_hazard_point,symbol-daytime-422,,28
WEDKLP03,PARTIAL,anchor_caution_hazard_point,anchor_caution_hazard_point,symbol-daytime-425,,28
WIMCON01,PARTIAL,onshore_structure_point,onshore_structure_point,,,28
ap-FSHHAV02,MATCH,anchor_caution_hazard_point,anchor_caution_hazard_point,symbol-daytime-428,,5
ap-MARCUL02,MATCH,fixed_fishing_gear_area,fixed_fishing_gear_area,,,5
ap-MARSHES1,MATCH,land_area,land_area,,,5
ap-PRTSUR01,COMPONENT,,,,fill_pattern,5
ap-QUESMRK1,PARTIAL,fixed_fishing_gear_area,fixed_fishing_gear_area,,,5
ap-TSSJCT02,PARTIAL,anchor_caution_hazard_point,anchor_caution_hazard_point,,,5
ap-VEGATN03,PARTIAL,land_area,land_area,,,5
lc-CBLSUB06,PARTIAL,seabed_line,seabed_line,,,6
lc-ESSARE01,PARTIAL,hazard_boundary_outline,hazard_boundary_outline,,,6
lc-FERYRT01,PARTIAL,navigation_marks,navigation_marks,,,6
lc-FSHFAC02,PARTIAL,fixed_fishing_gear_area,fixed_fishing_gear_area,,,6
lc-LOWACC01,PARTIAL,depth_contour,depth_contour,,,6
lc-LOWACC21,PARTIAL,baseline_line,baseline_line,,,6
lc-PIPSOL05,PARTIAL,seabed_line,seabed_line,,,6
lc-PIPSOL06,PARTIAL,seabed_line,seabed_line,,,6
lc-RECTRC09,PARTIAL,clip_outline_754,clip_outline_754,,,6
lc-RECTRC10,PARTIAL,clip_outline_754,clip_outline_754,,,6
lc-RECTRC11,PARTIAL,clip_outline_754,clip_outline_754,,,6
1 cm95_resource_key assignment_status assigned_standard_layer candidate_standard_layers reviewed_standard_resource_key component_kind occurrence_count
2 ACHARE02 MATCH anchorage_point anchorage_point anchorage_designated 28
3 ACHARE51 MATCH anchorage_point anchorage_point anchorage_designated 28
4 ACHBRT07 MATCH anchorage_point anchorage_point anchorage_quarantine 28
5 BCNGEN01 PARTIAL navigation_marks navigation_marks 28
6 BCNGEN05 PARTIAL navigation_marks navigation_marks light_beacon 28
7 BCNGEN60 PARTIAL navigation_marks navigation_marks light_beacon 28
8 BCNGEN61 PARTIAL navigation_marks navigation_marks light_beacon 28
9 BCNGEN68 PARTIAL navigation_marks navigation_marks light_beacon 28
10 BCNGEN69 PARTIAL navigation_marks navigation_marks light_beacon 28
11 BCNGEN70 PARTIAL navigation_marks navigation_marks light_beacon 28
12 BCNGEN71 PARTIAL navigation_marks navigation_marks light_beacon 28
13 BCNGEN76 PARTIAL navigation_marks navigation_marks light_beacon 28
14 BCNSPR62 PARTIAL navigation_marks navigation_marks light_beacon 28
15 BCNSTK77 PARTIAL navigation_marks navigation_marks light_beacon 28
16 BCNSTK78 PARTIAL navigation_marks navigation_marks light_beacon 28
17 BCNTOW60 PARTIAL navigation_marks navigation_marks buoy_pillar 28
18 BCNTOW76 PARTIAL navigation_marks navigation_marks buoy_pillar 28
19 BOYCAN63 PARTIAL navigation_marks navigation_marks buoy_can 28
20 BOYCON01 PARTIAL navigation_marks navigation_marks buoy_lattice 28
21 BOYCON60 PARTIAL navigation_marks navigation_marks buoy_lattice 28
22 BOYCON61 PARTIAL navigation_marks navigation_marks buoy_lattice 28
23 BOYCON62 PARTIAL navigation_marks navigation_marks buoy_lattice 28
24 BOYGEN03 PARTIAL navigation_marks navigation_marks 28
25 BOYMOR03 PARTIAL navigation_marks navigation_marks mooring_buoy 28
26 BOYPIL01 PARTIAL navigation_marks navigation_marks 28
27 BOYPIL60 PARTIAL navigation_marks navigation_marks buoy_pillar 28
28 BOYPIL61 PARTIAL navigation_marks navigation_marks buoy_pillar 28
29 BOYPIL62 PARTIAL navigation_marks navigation_marks buoy_pillar 28
30 BOYPIL68 PARTIAL navigation_marks navigation_marks buoy_pillar 28
31 BOYPIL69 PARTIAL navigation_marks navigation_marks buoy_pillar 28
32 BOYPIL70 PARTIAL navigation_marks navigation_marks buoy_pillar 28
33 BOYPIL71 PARTIAL navigation_marks navigation_marks buoy_pillar 28
34 BOYPIL72 PARTIAL navigation_marks navigation_marks buoy_pillar 28
35 BOYPIL73 PARTIAL navigation_marks navigation_marks buoy_pillar 28
36 BOYSPH62 PARTIAL navigation_marks navigation_marks 28
37 BOYSPR60 PARTIAL navigation_marks navigation_marks buoy_pillar 28
38 BOYSPR61 PARTIAL navigation_marks navigation_marks buoy_pillar 28
39 BOYSPR62 PARTIAL navigation_marks navigation_marks buoy_pillar 28
40 BOYSUP01 PARTIAL navigation_marks navigation_marks mooring_buoy 28
41 BOYSUP62 PARTIAL navigation_marks navigation_marks mooring_buoy 28
42 BRIDGE01 PARTIAL bridge_structure bridge_structure 28
43 BRTHNO01 PARTIAL navigation_marks navigation_marks leading_mark_variantt_308 28
44 BUAARE02 AMBIGUOUS onshore_structure_line;onshore_structure_point symbol-daytime-661 28
45 CGUSTA02 PARTIAL facility_boundary_point facility_boundary_point 28
46 CHIMNY01 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-640 28
47 CHIMNY11 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-640 28
48 CHINFO07 PARTIAL facility_boundary_outline facility_boundary_outline 28
49 CROSSX01 PARTIAL fixed_fishing_gear_area fixed_fishing_gear_area pattern_fill_fishery_or_fish_reef_area 5
50 CTNARE51 PARTIAL clip_outline_754 clip_outline_754 28
51 CTYARE51 PARTIAL navigation_hazard_point navigation_hazard_point 28
52 FLASTK01 PARTIAL onshore_structure_point onshore_structure_point 28
53 FLGSTF01 PARTIAL onshore_structure_point onshore_structure_point 28
54 FOGSIG01 PARTIAL navigation_marks navigation_marks 28
55 FORSTC01 PARTIAL onshore_structure_point onshore_structure_point 28
56 FOULGND1 MATCH navigation_hazard_point navigation_hazard_point foul_ground 28
57 FSHFAC02 PARTIAL fixed_fishing_gear_area fixed_fishing_gear_area pattern_fill_fishery_or_fish_reef_area 34
58 FSHHAV01 PARTIAL anchor_caution_hazard_point anchor_caution_hazard_point symbol-daytime-428 28
59 FSHHAV02 PARTIAL navigation_hazard_point navigation_hazard_point fish_reef 5
60 GATCON03 PARTIAL facility_boundary_point facility_boundary_point 28
61 GATCON04 PARTIAL facility_boundary_point facility_boundary_point 28
62 HRBFAC09 PARTIAL facility_boundary_point facility_boundary_point fishing_port 28
63 INFARE51 COMPONENT auxiliary_mark 28
64 LIGHTS11 COMPONENT light 2
65 LIGHTS12 COMPONENT light 2
66 LIGHTS13 COMPONENT light 2
67 LIGHTS82 COMPONENT light 2
68 LITDEF11 COMPONENT light 2
69 LITFLT01 COMPONENT light 28
70 LITVES60 COMPONENT buoy_body 28
71 LOCMAG51 COMPONENT other 28
72 LOWACC01 PARTIAL clip_outline_754 clip_outline_754 6
73 MARCUL02 PARTIAL fixed_fishing_gear_area fixed_fishing_gear_area pattern_fill_fishery_or_fish_reef_area 33
74 MARSHES1 PARTIAL place_label_land place_label_land 5
75 MORFAC03 MATCH anchor_caution_hazard_point anchor_caution_hazard_point symbol-daytime-433 28
76 MSTCON04 COMPONENT other 28
77 OFSPLF01 AMBIGUOUS navigation_hazard_outline;navigation_hazard_point 28
78 PILBOP02 MATCH pilot_station_point pilot_station_point pilot_station 28
79 PILPNT02 MATCH anchor_caution_hazard_point anchor_caution_hazard_point symbol-daytime-432 28
80 POSGEN04 UNKNOWN 28
81 PRCARE51 AMBIGUOUS navigation_hazard_outline;navigation_hazard_point 28
82 QUESMRK1 COMPONENT other 36
83 RACNSP01 AMBIGUOUS onshore_structure_area;onshore_structure_point 28
84 RADRFL03 AMBIGUOUS onshore_structure_line;onshore_structure_point 28
85 RDOSTA02 COMPONENT other 28
86 RTLDEF51 PARTIAL navigation_marks navigation_marks 28
87 RTPBCN02 AMBIGUOUS onshore_structure_line;onshore_structure_point 28
88 SMCFAC02 MATCH facility_boundary_point facility_boundary_point marina 28
89 SNDWAV02 MATCH anchor_caution_hazard_point anchor_caution_hazard_point symbol-daytime-421 28
90 STARPT01 PARTIAL navigation_marks navigation_marks light_minor 28
91 TOPMA115 COMPONENT topmark 28
92 TOPMAR01 COMPONENT topmark 28
93 TOPMAR02 COMPONENT topmark 28
94 TOPMAR05 COMPONENT topmark 28
95 TOPMAR06 COMPONENT topmark 28
96 TOPMAR07 COMPONENT topmark 28
97 TOPMAR08 COMPONENT topmark 28
98 TOPMAR10 COMPONENT topmark 28
99 TOPMAR12 COMPONENT topmark 28
100 TOPMAR13 COMPONENT topmark 28
101 TOPMAR22 COMPONENT topmark 28
102 TOPMAR25 COMPONENT topmark 28
103 TOPMAR26 COMPONENT topmark 28
104 TOPMAR27 COMPONENT topmark 28
105 TOPMAR28 COMPONENT topmark 28
106 TOPMAR30 COMPONENT topmark 28
107 TOPMAR32 COMPONENT topmark 28
108 TOPMAR33 COMPONENT topmark 28
109 TOPMAR98 COMPONENT topmark 28
110 TOPSHP51 COMPONENT topmark 28
111 TOPSHPI1 COMPONENT topmark 28
112 TOPSHPP2 COMPONENT topmark 28
113 TOWERS02 PARTIAL onshore_structure_point onshore_structure_point 28
114 TOWERS03 MATCH onshore_structure_point onshore_structure_point symbol-daytime-650 28
115 TOWERS05 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-650 28
116 TOWERS15 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-650 28
117 TOWERS60 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-698 28
118 TOWERS61 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-698 28
119 TOWERS63 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-698 28
120 TOWERS71 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-698 28
121 TOWERS72 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-698 28
122 TOWERS78 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-698 28
123 TOWERS79 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-698 28
124 TOWERS80 PARTIAL onshore_structure_point onshore_structure_point symbol-daytime-698 28
125 TOWERS83 PARTIAL onshore_structure_point onshore_structure_point 28
126 TOWERS85 PARTIAL onshore_structure_point onshore_structure_point 28
127 TOWERS95 PARTIAL onshore_structure_point onshore_structure_point 28
128 TSSCRS51 UNKNOWN 28
129 TSSLPT51 PARTIAL navigation_marks navigation_marks 28
130 TSSRON51 PARTIAL facility_boundary_point facility_boundary_point 28
131 WATTUR02 MATCH anchor_caution_hazard_point anchor_caution_hazard_point symbol-daytime-422 28
132 WEDKLP03 PARTIAL anchor_caution_hazard_point anchor_caution_hazard_point symbol-daytime-425 28
133 WIMCON01 PARTIAL onshore_structure_point onshore_structure_point 28
134 ap-FSHHAV02 MATCH anchor_caution_hazard_point anchor_caution_hazard_point symbol-daytime-428 5
135 ap-MARCUL02 MATCH fixed_fishing_gear_area fixed_fishing_gear_area 5
136 ap-MARSHES1 MATCH land_area land_area 5
137 ap-PRTSUR01 COMPONENT fill_pattern 5
138 ap-QUESMRK1 PARTIAL fixed_fishing_gear_area fixed_fishing_gear_area 5
139 ap-TSSJCT02 PARTIAL anchor_caution_hazard_point anchor_caution_hazard_point 5
140 ap-VEGATN03 PARTIAL land_area land_area 5
141 lc-CBLSUB06 PARTIAL seabed_line seabed_line 6
142 lc-ESSARE01 PARTIAL hazard_boundary_outline hazard_boundary_outline 6
143 lc-FERYRT01 PARTIAL navigation_marks navigation_marks 6
144 lc-FSHFAC02 PARTIAL fixed_fishing_gear_area fixed_fishing_gear_area 6
145 lc-LOWACC01 PARTIAL depth_contour depth_contour 6
146 lc-LOWACC21 PARTIAL baseline_line baseline_line 6
147 lc-PIPSOL05 PARTIAL seabed_line seabed_line 6
148 lc-PIPSOL06 PARTIAL seabed_line seabed_line 6
149 lc-RECTRC09 PARTIAL clip_outline_754 clip_outline_754 6
150 lc-RECTRC10 PARTIAL clip_outline_754 clip_outline_754 6
151 lc-RECTRC11 PARTIAL clip_outline_754 clip_outline_754 6

View File

@@ -1,233 +0,0 @@
{
"schema": "cm95-layer-classification-validation/v1",
"generated_at": "2026-10-06T00:39:21.897737+00:00",
"ok": true,
"baseline": {
"cm95_style_sha256": "6c25ce3e5c1db65060be01ca899acf7f49228fb2277572dc1868c3d34019d4b0",
"standard_style_sha256": "18b32ad1ea2d3798000abe21406d2f2a5225968910aed48d7ca5b1a1cb7f3ce6",
"standard_catalog_status": "FROZEN",
"unique_cm95_resources": 150,
"manual_status_counts": {
"AMBIGUOUS": 6,
"COMPONENT": 35,
"MATCH": 14,
"PARTIAL": 93,
"UNKNOWN": 2
},
"final_assignment_status_counts": {
"MATCH": 323,
"PARTIAL": 2213,
"AMBIGUOUS": 168,
"COMPONENT": 835,
"UNKNOWN": 160
},
"final_assignment_total": 3699
},
"checks": [
{
"name": "required_files_exist",
"status": "PASS",
"detail": {
"missing": []
}
},
{
"name": "standard_catalog_frozen",
"status": "PASS",
"detail": "FROZEN"
},
{
"name": "cm95_anonymous_catalog_has_no_standard_layer",
"status": "PASS"
},
{
"name": "cm95_unique_visual_resources_150",
"status": "PASS",
"detail": 150
},
{
"name": "manual_decisions_150",
"status": "PASS",
"detail": 150
},
{
"name": "manual_covers_all_resources",
"status": "PASS",
"detail": {
"missing": [],
"extra": []
}
},
{
"name": "style_hash_cm95_matches",
"status": "PASS",
"detail": {
"manual": "6c25ce3e5c1db65060be01ca899acf7f49228fb2277572dc1868c3d34019d4b0",
"catalog": "6c25ce3e5c1db65060be01ca899acf7f49228fb2277572dc1868c3d34019d4b0"
}
},
{
"name": "style_hash_standard_matches",
"status": "PASS",
"detail": {
"manual": "18b32ad1ea2d3798000abe21406d2f2a5225968910aed48d7ca5b1a1cb7f3ce6",
"catalog": "18b32ad1ea2d3798000abe21406d2f2a5225968910aed48d7ca5b1a1cb7f3ce6"
}
},
{
"name": "manual_decision_state_constraints",
"status": "PASS",
"detail": []
},
{
"name": "manual_status_counts_total_150",
"status": "PASS",
"detail": {
"MATCH": 14,
"PARTIAL": 93,
"COMPONENT": 35,
"AMBIGUOUS": 6,
"UNKNOWN": 2
}
},
{
"name": "manual_anchor_export_matches_non_unknown_decisions",
"status": "PASS",
"detail": {
"expected": 148,
"actual": 148,
"missing": [],
"extra": []
}
},
{
"name": "manual_anchor_fields_match_decisions",
"status": "PASS",
"detail": []
},
{
"name": "reference_names_cover_all_cm95_resources",
"status": "PASS",
"detail": {
"missing": [],
"extra": []
}
},
{
"name": "semantic_rules_are_valid",
"status": "PASS",
"detail": []
},
{
"name": "assignment_total_count",
"status": "PASS",
"detail": {
"expected": 3699,
"actual": 3699
}
},
{
"name": "final_assignment_matches_manual_decisions",
"status": "PASS",
"detail": []
},
{
"name": "assignment_summary_counts_match_rows",
"status": "PASS",
"detail": {
"recomputed": {
"MATCH": 323,
"PARTIAL": 2213,
"AMBIGUOUS": 168,
"COMPONENT": 835,
"UNKNOWN": 160
},
"summary": {
"MATCH": 323,
"PARTIAL": 2213,
"AMBIGUOUS": 168,
"COMPONENT": 835,
"UNKNOWN": 160
}
}
},
{
"name": "assignment_policy_preserves_architecture",
"status": "PASS",
"detail": {
"cm95_source_layer_used_for_assignment": false,
"provider_field_semantics_used_for_assignment": false,
"standard_layer_is_output_only": true,
"unknown_is_allowed": true,
"ambiguous_is_allowed": true,
"component_is_allowed": true,
"whole_group_inheritance_forbidden": true,
"anonymous_catalog_modified": false
}
}
],
"warnings": [
{
"name": "manual_vs_semantic_classifier_conflicts",
"status": "WARN",
"detail": [
{
"anonymous_id": "CM95-VO-A9F2024C2B",
"resource_key": "FSHHAV01",
"display_name_zh": "鱼礁/鱼类栖息设施",
"manual_layer": "anchor_caution_hazard_point",
"semantic_suggestion": "submerged_reef_area",
"semantic_score": 0.72
},
{
"anonymous_id": "CM95-VO-D1DA37E296",
"resource_key": "FSHHAV02",
"display_name_zh": "鱼礁/鱼类栖息设施",
"manual_layer": "navigation_hazard_point",
"semantic_suggestion": "submerged_reef_area",
"semantic_score": 0.72
},
{
"anonymous_id": "CM95-VO-DA05B079BB",
"resource_key": "BRTHNO01",
"display_name_zh": "泊位编号",
"manual_layer": "navigation_marks",
"semantic_suggestion": "facility_boundary_point",
"semantic_score": 0.88
},
{
"anonymous_id": "CM95-VO-5157F24143",
"resource_key": "MARSHES1",
"display_name_zh": "沼泽",
"manual_layer": "place_label_land",
"semantic_suggestion": "land_area",
"semantic_score": 0.86
},
{
"anonymous_id": "CM95-VO-0A90325CD0",
"resource_key": "MORFAC03",
"display_name_zh": "系泊墩",
"manual_layer": "anchor_caution_hazard_point",
"semantic_suggestion": "facility_boundary_point",
"semantic_score": 0.88
},
{
"anonymous_id": "CM95-VO-0BFA2564A3",
"resource_key": "PILPNT02",
"display_name_zh": "桩/系缆柱",
"manual_layer": "anchor_caution_hazard_point",
"semantic_suggestion": "facility_boundary_point",
"semantic_score": 0.88
},
{
"anonymous_id": "CM95-VO-A5D22731AF",
"resource_key": "ap-FSHHAV02",
"display_name_zh": "鱼礁填充图案",
"manual_layer": "anchor_caution_hazard_point",
"semantic_suggestion": "submerged_reef_area",
"semantic_score": 0.72
}
]
}
]
}

View File

@@ -1,620 +0,0 @@
{
"schema": "cm95-reviewed-visual-anchors/v1",
"basis": "人工复核实际 Sprite 对照图;未使用 CM95 source-layer 名称参与判定。",
"rules": {
"source_layer_used": false,
"provider_field_semantics_used": false,
"match_means": "实际图形基本一致,可直接作为标准 Layer 锚点",
"partial_means": "不是同一完整图形,但属于明确的同一视觉家族/组件,仅用于 Layer 归类",
"unknown_policy": "未列出的 CM95 Visual Object 保持 UNKNOWN"
},
"anchors": [
{
"cm95_resource_key": "LIGHTS11",
"status": "MATCH",
"standard_resource_key": "light_flare_red",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "LIGHTS12",
"status": "MATCH",
"standard_resource_key": "light_flare_green",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "LIGHTS13",
"status": "MATCH",
"standard_resource_key": "light_flare_yellow_white",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "ACHARE51",
"status": "MATCH",
"standard_resource_key": "anchorage_designated",
"standard_layer": "anchorage_point"
},
{
"cm95_resource_key": "SMCFAC02",
"status": "MATCH",
"standard_resource_key": "marina",
"standard_layer": "facility_boundary_point"
},
{
"cm95_resource_key": "HRBFAC09",
"status": "MATCH",
"standard_resource_key": "fishing_port",
"standard_layer": "facility_boundary_point"
},
{
"cm95_resource_key": "PILBOP02",
"status": "MATCH",
"standard_resource_key": "pilot_station",
"standard_layer": "pilot_station_point"
},
{
"cm95_resource_key": "CHIMNY01",
"status": "MATCH",
"standard_resource_key": "symbol-daytime-640",
"standard_layer": "onshore_structure_point"
},
{
"cm95_resource_key": "ACHARE02",
"status": "PARTIAL",
"standard_resource_key": "anchorage_designated",
"standard_layer": "anchorage_point"
},
{
"cm95_resource_key": "ACHBRT07",
"status": "PARTIAL",
"standard_resource_key": "anchorage_quarantine",
"standard_layer": "anchorage_point"
},
{
"cm95_resource_key": "CHIMNY11",
"status": "PARTIAL",
"standard_resource_key": "symbol-daytime-640",
"standard_layer": "onshore_structure_point"
},
{
"cm95_resource_key": "BCNGEN05",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BCNGEN60",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BCNGEN69",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BCNGEN76",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BOYSPR60",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BOYSPR61",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BOYPIL60",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BOYPIL68",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BOYPIL69",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BOYPIL72",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BOYPIL73",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "TOPMAR05",
"status": "PARTIAL",
"standard_resource_key": "buoy_north_cardinal",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "TOPMAR08",
"status": "PARTIAL",
"standard_resource_key": "buoy_north_cardinal",
"standard_layer": "navigation_marks"
},
{
"cm95_resource_key": "BOYPIL70",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BOYPIL61",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BOYPIL62",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BOYPIL71",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BCNGEN70",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BCNSPR62",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BCNGEN61",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BCNGEN68",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BCNGEN71",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BCNGEN01",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BCNSTK78",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BCNSTK77",
"status": "PARTIAL",
"standard_resource_key": "light_beacon",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BOYSPR62",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "TOPMAR25",
"status": "PARTIAL",
"standard_resource_key": "buoy_north_cardinal",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "TOPMAR28",
"status": "PARTIAL",
"standard_resource_key": "buoy_north_cardinal",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "TOPMAR07",
"status": "PARTIAL",
"standard_resource_key": "buoy_north_cardinal",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "TOPMAR26",
"status": "PARTIAL",
"standard_resource_key": "buoy_north_cardinal",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BCNTOW60",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BCNTOW76",
"status": "PARTIAL",
"standard_resource_key": "buoy_pillar",
"standard_layer": "navigation_marks",
"review_stage": "visual_family_propagation_reviewed"
},
{
"cm95_resource_key": "BOYCAN63",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "BOYCON01",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "BOYCON60",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "BOYCON61",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "BOYCON62",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "BOYGEN03",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "BOYMOR03",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "BOYPIL01",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "BOYSPH62",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "BOYSUP01",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "BOYSUP62",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_buoy_body"
},
{
"cm95_resource_key": "FLASTK01",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "onshore_structure_point",
"review_stage": "anonymous_visual_review_onshore_landmark"
},
{
"cm95_resource_key": "FLGSTF01",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "onshore_structure_point",
"review_stage": "anonymous_visual_review_onshore_landmark"
},
{
"cm95_resource_key": "MSTCON04",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "onshore_structure_point",
"review_stage": "anonymous_visual_review_onshore_landmark"
},
{
"cm95_resource_key": "WIMCON01",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "onshore_structure_point",
"review_stage": "anonymous_visual_review_onshore_landmark"
},
{
"cm95_resource_key": "TOPMA115",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPMAR02",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPMAR06",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPMAR10",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPMAR12",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPMAR13",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPMAR22",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPMAR27",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPMAR33",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPMAR98",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPSHP51",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPSHPI1",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOPSHPP2",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_topmark"
},
{
"cm95_resource_key": "TOWERS60",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS61",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS63",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS71",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS72",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS78",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS79",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS80",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS83",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS85",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS95",
"status": "PARTIAL",
"standard_resource_key": null,
"standard_layer": "navigation_marks",
"review_stage": "anonymous_visual_review_colored_aid_tower"
},
{
"cm95_resource_key": "TOWERS02",
"status": "AMBIGUOUS",
"standard_resource_key": null,
"standard_layers": [
"navigation_marks",
"onshore_structure_point"
],
"review_stage": "anonymous_visual_review"
},
{
"cm95_resource_key": "TOWERS03",
"status": "AMBIGUOUS",
"standard_resource_key": null,
"standard_layers": [
"navigation_marks",
"onshore_structure_point"
],
"review_stage": "anonymous_visual_review"
},
{
"cm95_resource_key": "TOWERS05",
"status": "AMBIGUOUS",
"standard_resource_key": null,
"standard_layers": [
"navigation_marks",
"onshore_structure_point"
],
"review_stage": "anonymous_visual_review"
},
{
"cm95_resource_key": "TOWERS15",
"status": "AMBIGUOUS",
"standard_resource_key": null,
"standard_layers": [
"navigation_marks",
"onshore_structure_point"
],
"review_stage": "anonymous_visual_review"
},
{
"cm95_resource_key": "FOULGND1",
"status": "AMBIGUOUS",
"standard_resource_key": "foul_ground",
"standard_layers": [
"navigation_hazard_point",
"anchor_caution_hazard_point"
],
"review_stage": "anonymous_visual_review"
},
{
"cm95_resource_key": "lc-ESSARE01",
"status": "MATCH",
"standard_resource_key": "special_pattern_line_754",
"standard_layer": "clip_outline_754",
"review_stage": "pattern_visual_review"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 KiB

View File

@@ -1,748 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import hashlib
import difflib
import re
import json
import mimetypes
import subprocess
import sys
import urllib.parse
from datetime import datetime, timezone
from collections import Counter
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from PIL import Image
TOOL_DIR = Path(__file__).resolve().parents[1]
OUTPUT_DIR = TOOL_DIR / "output"
CM95_CATALOG_PATH = OUTPUT_DIR / "cm95-anonymous" / "anonymous-catalog.json"
STD_CATALOG_PATH = OUTPUT_DIR / "navsea-standard" / "standard-catalog.json"
SUGGESTION_PATH = OUTPUT_DIR / "cm95-visual-anchor-match" / "reviewed-anchors-v1.json"
VISUAL_CANDIDATE_PATH = OUTPUT_DIR / "cm95-visual-anchor-match" / "visual-anchor-matches.json"
REFERENCE_NAME_PATH = OUTPUT_DIR / "reference-name-enrichment-v1.json"
SEMANTIC_RULES_PATH = TOOL_DIR / "cm95_semantic_layer_rules_v1.json"
MANUAL_DIR = OUTPUT_DIR / "cm95-manual-review"
MANUAL_REVIEW_PATH = MANUAL_DIR / "manual-review-v1.json"
MANUAL_ANCHOR_PATH = MANUAL_DIR / "reviewed-anchors-manual-v1.json"
MANUAL_ASSIGNMENT_DIR = MANUAL_DIR / "assignment"
WEB_DIR = Path(__file__).resolve().parent / "web"
VALID_STATUS = {"MATCH", "PARTIAL", "AMBIGUOUS", "COMPONENT", "UNKNOWN"}
def _normalize_name(text: str | None) -> str:
if not text:
return ""
return re.sub(r"[^0-9a-zA-Z\u4e00-\u9fff]+", "", str(text).lower())
def _text_similarity(a: str | None, b: str | None) -> float:
na = _normalize_name(a)
nb = _normalize_name(b)
if not na or not nb:
return 0.0
seq = difflib.SequenceMatcher(None, na, nb).ratio()
sa, sb = set(na), set(nb)
jac = len(sa & sb) / max(1, len(sa | sb))
contain = 0.0
if na in nb or nb in na:
contain = min(len(na), len(nb)) / max(len(na), len(nb))
contain = 0.70 + 0.30 * contain
return max(seq, jac, contain)
COMPONENT_KIND_ZH = {
"topmark": "顶标",
"buoy_body": "浮标主体",
"beacon_body": "立标主体",
"light": "灯光",
"auxiliary_mark": "附加标记",
"fill_pattern": "面填充图案",
"line_pattern": "线型图案",
"text": "文字",
"other": "其他组件",
}
def read_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def write_json_atomic(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
tmp.replace(path)
class ReviewState:
def __init__(self) -> None:
self.cm = read_json(CM95_CATALOG_PATH)
self.std = read_json(STD_CATALOG_PATH)
self.suggestions = read_json(SUGGESTION_PATH) if SUGGESTION_PATH.exists() else {"anchors": []}
self.visual_candidates = read_json(VISUAL_CANDIDATE_PATH) if VISUAL_CANDIDATE_PATH.exists() else {"top_candidates": []}
self.reference_names = read_json(REFERENCE_NAME_PATH) if REFERENCE_NAME_PATH.exists() else {"cm95": {}, "navsea_standard": {}, "standard_layer_zh": {}}
self.semantic_rules = read_json(SEMANTIC_RULES_PATH).get("rules", []) if SEMANTIC_RULES_PATH.exists() else []
self.cm_names = self.reference_names.get("cm95", {})
self.std_names = self.reference_names.get("navsea_standard", {})
self.layer_zh = self.reference_names.get("standard_layer_zh", {})
self.cm_atlas = Image.open(OUTPUT_DIR / "cm95-anonymous" / "_sprite" / "sprite.png").convert("RGBA")
self.std_atlas = Image.open(OUTPUT_DIR / "navsea-anonymous" / "_sprite" / "sprite.png").convert("RGBA")
self.cm_resources = self._build_cm_resources()
self.cm_by_id = {x["anonymous_id"]: x for x in self.cm_resources}
self.cm_by_key = {x["resource_key"]: x for x in self.cm_resources}
self.std_resources = self._build_std_resources()
self.std_by_key = {x["resource_key"]: x for x in self.std_resources}
self.standard_layers = sorted({
str(obj.get("standard_layer_name"))
for obj in self.std.get("visual_objects", [])
if obj.get("standard_layer_name")
})
self.suggestion_by_key = {
x["cm95_resource_key"]: x
for x in self.suggestions.get("anchors", [])
}
self.candidate_by_key = {
x["cm95_resource_key"]: x
for x in self.visual_candidates.get("top_candidates", [])
}
self.review = self._load_or_initialize_review()
def _anonymous_id(self, resource_key: str, visual_hash: str | None) -> str:
seed = f"{resource_key}|{visual_hash or ''}".encode("utf-8")
return "CM95-VO-" + hashlib.sha256(seed).hexdigest()[:10].upper()
def _build_cm_resources(self) -> list[dict[str, Any]]:
grouped: dict[str, dict[str, Any]] = {}
for obj in self.cm.get("visual_objects", []):
key = obj.get("resource_key")
meta = obj.get("resource_meta")
if not key or not meta:
continue
item = grouped.setdefault(key, {
"resource_key": key,
"resource_meta": meta,
"visual_hash": obj.get("visual_hash"),
"component_roles": set(),
"geometry_roles": set(),
"occurrence_count": 0,
"group_ids": set(),
"provenance": [],
})
if obj.get("component_role"):
item["component_roles"].add(obj["component_role"])
if obj.get("geometry_role"):
item["geometry_roles"].add(obj["geometry_role"])
item["occurrence_count"] += 1
item["group_ids"].add(obj["group_id"])
if obj.get("provenance"):
item["provenance"].append(obj["provenance"])
out = []
for key in sorted(grouped):
item = grouped[key]
item["anonymous_id"] = self._anonymous_id(key, item.get("visual_hash"))
item["component_roles"] = sorted(item["component_roles"])
item["geometry_roles"] = sorted(item["geometry_roles"])
item["group_ids"] = sorted(item["group_ids"])
out.append(item)
return out
def _build_std_resources(self) -> list[dict[str, Any]]:
grouped: dict[tuple[str, str], dict[str, Any]] = {}
for obj in self.std.get("visual_objects", []):
key = obj.get("resource_key")
layer = obj.get("standard_layer_name")
meta = obj.get("resource_meta")
if not key or not layer or not meta:
continue
gkey = (key, layer)
item = grouped.setdefault(gkey, {
"resource_key": key,
"standard_layer": layer,
"resource_meta": meta,
"visual_hash": obj.get("visual_hash"),
"component_role": obj.get("component_role"),
"geometry_role": obj.get("geometry_role"),
"visual_object_id": obj.get("visual_object_id"),
})
return sorted(grouped.values(), key=lambda x: (x["standard_layer"], x["resource_key"]))
def _semantic_info(self, resource_key: str) -> dict[str, Any]:
info = self.cm_names.get(resource_key, {}) or {}
desc = str(info.get("official_description_en") or "")
resource_type = str(info.get("resource_type") or "")
for rule in self.semantic_rules:
rule_type = rule.get("resource_type")
if rule_type and rule_type != resource_type:
continue
pattern = str(rule.get("pattern") or "")
if pattern and not re.search(pattern, desc, flags=re.IGNORECASE):
continue
return {
"rule_id": rule.get("id"),
"category_zh": rule.get("category_zh") or "未分类",
"reason_zh": rule.get("reason_zh") or "",
"component_kind": rule.get("component_kind"),
"layer_candidates": [
{"layer": str(x[0]), "semantic_score": float(x[1])}
for x in rule.get("layer_candidates", [])
if isinstance(x, list) and len(x) >= 2
],
}
return {
"rule_id": None,
"category_zh": "尚未建立语义分类",
"reason_zh": "当前 S-52 说明没有命中已声明的 Layer 语义规则,需人工确认。",
"component_kind": None,
"layer_candidates": [],
}
def _layer_suggestions(
self,
semantic: dict[str, Any],
candidates: list[dict[str, Any]],
suggestion: dict[str, Any] | None,
) -> list[dict[str, Any]]:
valid_layers = set(self.standard_layers)
semantic_map = {
x["layer"]: float(x.get("semantic_score") or 0.0)
for x in semantic.get("layer_candidates", [])
if x.get("layer") in valid_layers
}
visual_map: dict[str, tuple[float, str | None]] = {}
for c in candidates:
layer = c.get("standard_layer")
if layer not in valid_layers:
continue
score = float(c.get("score") or 0.0)
prev = visual_map.get(layer)
if prev is None or score > prev[0]:
visual_map[layer] = (score, c.get("resource_key"))
prior_layers: set[str] = set()
if suggestion:
if suggestion.get("standard_layer") in valid_layers:
prior_layers.add(str(suggestion.get("standard_layer")))
for layer in suggestion.get("standard_layers") or []:
if layer in valid_layers:
prior_layers.add(str(layer))
layers = set(semantic_map) | set(visual_map) | prior_layers
rows: list[dict[str, Any]] = []
has_semantic = bool(semantic_map)
for layer in layers:
sem = semantic_map.get(layer, 0.0)
vis, best_resource = visual_map.get(layer, (0.0, None))
prior = 1.0 if layer in prior_layers else 0.0
if has_semantic:
combined = 0.75 * sem + 0.15 * vis + 0.10 * prior
else:
combined = 0.72 * vis + 0.28 * prior
reasons = []
if sem > 0:
reasons.append(f"S-52 语义 {sem*100:.0f}%")
if vis > 0:
reasons.append(f"视觉证据 {vis*100:.0f}%")
if prior > 0:
reasons.append("已有 reviewed suggestion 支持")
rows.append({
"standard_layer": layer,
"standard_layer_zh": self.layer_zh.get(layer, layer),
"semantic_score": round(sem, 4),
"visual_score": round(vis, 4),
"prior_support": bool(prior),
"combined_score": round(min(1.0, combined), 4),
"best_standard_resource_key": best_resource,
"reason_zh": ";".join(reasons) or "辅助证据不足",
})
rows.sort(key=lambda x: (x["combined_score"], x["semantic_score"], x["visual_score"]), reverse=True)
return rows[:8]
def _load_or_initialize_review(self) -> dict[str, Any]:
if MANUAL_REVIEW_PATH.exists():
data = read_json(MANUAL_REVIEW_PATH)
return data
data = {
"schema": "cm95-manual-review/v1",
"created_at": datetime.now(timezone.utc).isoformat(),
"updated_at": datetime.now(timezone.utc).isoformat(),
"cm95_style_sha256": self.cm.get("style", {}).get("sha256"),
"standard_style_sha256": self.std.get("style", {}).get("sha256"),
"standard_catalog_status": self.std.get("catalog_status"),
"policy": {
"source_layer_hidden_from_main_review": True,
"provider_field_semantics_used": False,
"standard_catalog_mutated": False,
"cm95_anonymous_catalog_mutated": False,
},
"decisions": {},
}
write_json_atomic(MANUAL_REVIEW_PATH, data)
return data
def save_decision(self, payload: dict[str, Any]) -> dict[str, Any]:
anonymous_id = str(payload.get("anonymous_id") or "")
item = self.cm_by_id.get(anonymous_id)
if not item:
raise ValueError("未知 anonymous_id")
status = str(payload.get("status") or "")
if status not in VALID_STATUS:
raise ValueError("status 必须是 MATCH / PARTIAL / AMBIGUOUS / COMPONENT / UNKNOWN")
layers = payload.get("standard_layers") or []
if not isinstance(layers, list):
raise ValueError("standard_layers 必须是数组")
layers = sorted({str(x) for x in layers if x})
std_resource = payload.get("standard_resource_key")
if std_resource is not None:
std_resource = str(std_resource)
if std_resource not in self.std_by_key:
raise ValueError("未知 standard_resource_key")
valid_layers = set(self.standard_layers)
if any(x not in valid_layers for x in layers):
raise ValueError("standard_layers 包含未知标准 Layer")
if status in {"MATCH", "PARTIAL"} and len(layers) != 1:
raise ValueError(f"{status} 必须且只能选择 1 个标准 Layer")
if status == "AMBIGUOUS" and len(layers) < 2:
raise ValueError("AMBIGUOUS 至少选择 2 个标准 Layer")
component_kind = payload.get("component_kind")
if component_kind is not None:
component_kind = str(component_kind)
if component_kind not in COMPONENT_KIND_ZH:
raise ValueError("未知 component_kind")
if status in {"UNKNOWN", "COMPONENT"}:
layers = []
std_resource = None
if status != "COMPONENT":
component_kind = None
if std_resource:
std_layer = self.std_by_key[std_resource]["standard_layer"]
if status in {"MATCH", "PARTIAL"} and layers and std_layer != layers[0]:
raise ValueError("选择的 Standard Visual Object 与 Layer 不一致")
now = datetime.now(timezone.utc).isoformat()
decision = {
"anonymous_id": anonymous_id,
"resource_key_internal": item["resource_key"],
"status": status,
"standard_layers": layers,
"standard_resource_key": std_resource,
"component_kind": component_kind,
"assignment_mode": (
"COMPONENT_ONLY" if status == "COMPONENT"
else ("VISUAL_OBJECT_MATCH" if std_resource else ("LAYER_ONLY" if layers else "UNKNOWN"))
),
"note": str(payload.get("note") or ""),
"reviewed_at": now,
}
self.review["decisions"][anonymous_id] = decision
self.review["updated_at"] = now
write_json_atomic(MANUAL_REVIEW_PATH, self.review)
return decision
def delete_decision(self, anonymous_id: str) -> None:
self.review["decisions"].pop(anonymous_id, None)
self.review["updated_at"] = datetime.now(timezone.utc).isoformat()
write_json_atomic(MANUAL_REVIEW_PATH, self.review)
def abandon_unreviewed(self, dry_run: bool = False) -> dict[str, Any]:
decisions = self.review.get("decisions", {})
pending = [x for x in self.cm_resources if x["anonymous_id"] not in decisions]
if dry_run:
return {"affected": len(pending), "applied": False}
now = datetime.now(timezone.utc).isoformat()
for item in pending:
anonymous_id = item["anonymous_id"]
decisions[anonymous_id] = {
"anonymous_id": anonymous_id,
"resource_key_internal": item["resource_key"],
"status": "UNKNOWN",
"standard_layers": [],
"standard_resource_key": None,
"component_kind": None,
"assignment_mode": "UNKNOWN",
"note": "批量放弃未确认项",
"reviewed_at": now,
"bulk_action": "ABANDON_UNREVIEWED",
}
self.review["updated_at"] = now
write_json_atomic(MANUAL_REVIEW_PATH, self.review)
return {"affected": len(pending), "applied": True}
def bootstrap(self) -> dict[str, Any]:
decisions = self.review.get("decisions", {})
items = []
for item in self.cm_resources:
anon = item["anonymous_id"]
suggestion = self.suggestion_by_key.get(item["resource_key"])
candidate = self.candidate_by_key.get(item["resource_key"])
candidates = []
if candidate:
best = candidate.get("best_standard_resource_key")
best_layer = candidate.get("best_standard_layer")
if best and best_layer:
name_info_std = self.std_names.get(best, {}) or {}
candidates.append({
"resource_key": best,
"standard_layer": best_layer,
"standard_layer_zh": self.layer_zh.get(best_layer, best_layer),
"display_name_zh": name_info_std.get("display_name_zh") or self.layer_zh.get(best_layer, best_layer),
"score": candidate.get("score"),
"margin": candidate.get("margin"),
"source": "visual_candidate",
})
for alt in candidate.get("alternatives", []):
key = alt.get("standard_resource_key")
layer = alt.get("standard_layer")
if key and layer:
name_info_std = self.std_names.get(key, {}) or {}
candidates.append({
"resource_key": key,
"standard_layer": layer,
"standard_layer_zh": self.layer_zh.get(layer, layer),
"display_name_zh": name_info_std.get("display_name_zh") or self.layer_zh.get(layer, layer),
"score": alt.get("score"),
"source": "visual_candidate",
})
name_info = self.cm_names.get(item["resource_key"], {})
cm_name_zh = name_info.get("display_name_zh") or "未命名视觉对象"
suggestion_layers = set()
suggestion_resource = None
if suggestion:
suggestion_resource = suggestion.get("standard_resource_key")
if suggestion.get("standard_layer"):
suggestion_layers.add(str(suggestion.get("standard_layer")))
for layer_name in suggestion.get("standard_layers") or []:
if layer_name:
suggestion_layers.add(str(layer_name))
# UI-only recommendation: actual visual score + Chinese name similarity +
# existing reviewed visual suggestion. This never writes Layer Assignment.
for c in candidates:
visual_score = float(c.get("score") or 0.0)
text_score = _text_similarity(cm_name_zh, c.get("display_name_zh"))
bonus = 0.0
if suggestion_resource and c.get("resource_key") == suggestion_resource:
bonus += 0.16
elif c.get("standard_layer") in suggestion_layers:
bonus += 0.08
c["text_score"] = round(text_score, 4)
c["recommendation_score"] = round(min(1.0, 0.62 * visual_score + 0.38 * text_score + bonus), 4)
candidates.sort(key=lambda x: (float(x.get("recommendation_score") or 0.0), float(x.get("score") or 0.0)), reverse=True)
recommended_resource_key = candidates[0]["resource_key"] if candidates else suggestion_resource
semantic = self._semantic_info(item["resource_key"])
layer_suggestions = self._layer_suggestions(semantic, candidates, suggestion)
top_layer = layer_suggestions[0]["standard_layer"] if layer_suggestions else None
current_decision = decisions.get(anon)
decision_conflict = False
if current_decision and top_layer and layer_suggestions and layer_suggestions[0]["combined_score"] >= 0.72:
selected = current_decision.get("standard_layers") or []
if current_decision.get("status") in {"MATCH", "PARTIAL"} and len(selected) == 1 and selected[0] != top_layer:
decision_conflict = True
items.append({
"anonymous_id": anon,
"display_name_zh": cm_name_zh,
"official_description_en": name_info.get("official_description_en"),
"name_source": name_info.get("display_name_source"),
"semantic": semantic,
"layer_suggestions": layer_suggestions,
"recommended_layer": top_layer,
"decision_conflict": decision_conflict,
"component_roles": item["component_roles"],
"geometry_roles": item["geometry_roles"],
"occurrence_count": item["occurrence_count"],
"suggestion": {
"status": suggestion.get("status"),
"standard_layer": suggestion.get("standard_layer"),
"standard_layers": suggestion.get("standard_layers"),
"standard_resource_key": suggestion.get("standard_resource_key"),
"standard_name_zh": (self.std_names.get(suggestion.get("standard_resource_key") or "", {}) or {}).get("display_name_zh"),
"standard_layer_zh": self.layer_zh.get(suggestion.get("standard_layer") or ""),
"review_stage": suggestion.get("review_stage"),
} if suggestion else None,
"candidates": candidates,
"recommended_resource_key": recommended_resource_key,
"decision": current_decision,
})
std_layers = list(self.standard_layers)
std_resources = [
{
"resource_key": x["resource_key"],
"standard_layer": x["standard_layer"],
"standard_layer_zh": self.layer_zh.get(x["standard_layer"], x["standard_layer"]),
"display_name_zh": (self.std_names.get(x["resource_key"], {}) or {}).get("display_name_zh") or self.layer_zh.get(x["standard_layer"], x["standard_layer"]),
"component_role": x.get("component_role"),
"geometry_role": x.get("geometry_role"),
"visual_object_id": x.get("visual_object_id"),
}
for x in self.std_resources
]
summary = Counter()
for d in decisions.values():
summary[d.get("status", "UNKNOWN")] += 1
return {
"items": items,
"standard_layers": std_layers,
"standard_layer_zh": {x: self.layer_zh.get(x, x) for x in std_layers},
"standard_resources": std_resources,
"progress": {
"total": len(items),
"reviewed": len(decisions),
"unreviewed": len(items) - len(decisions),
"MATCH": summary["MATCH"],
"PARTIAL": summary["PARTIAL"],
"AMBIGUOUS": summary["AMBIGUOUS"],
"COMPONENT": summary["COMPONENT"],
"UNKNOWN": summary["UNKNOWN"],
},
"component_kinds": [
{"id": key, "name_zh": value}
for key, value in COMPONENT_KIND_ZH.items()
],
"policy": self.review.get("policy"),
}
def audit_info(self, anonymous_id: str) -> dict[str, Any]:
item = self.cm_by_id.get(anonymous_id)
if not item:
raise ValueError("未知 anonymous_id")
unique_prov = []
seen = set()
for p in item["provenance"]:
key = json.dumps(p, ensure_ascii=False, sort_keys=True)
if key not in seen:
seen.add(key)
unique_prov.append(p)
return {
"anonymous_id": anonymous_id,
"resource_key": item["resource_key"],
"visual_hash": item.get("visual_hash"),
"group_ids": item["group_ids"],
"provenance": unique_prov,
}
def crop_png(self, which: str, identifier: str) -> bytes:
if which == "cm95":
item = self.cm_by_id.get(identifier)
if not item:
raise KeyError(identifier)
atlas = self.cm_atlas
meta = item["resource_meta"]
elif which == "std":
item = self.std_by_key.get(identifier)
if not item:
raise KeyError(identifier)
atlas = self.std_atlas
meta = item["resource_meta"]
else:
raise KeyError(which)
crop = atlas.crop((
int(meta["x"]), int(meta["y"]),
int(meta["x"]) + int(meta["width"]),
int(meta["y"]) + int(meta["height"]),
)).convert("RGBA")
from io import BytesIO
buf = BytesIO()
crop.save(buf, format="PNG")
return buf.getvalue()
def finalize(self) -> dict[str, Any]:
decisions = self.review.get("decisions", {})
anchors = []
for anon, d in decisions.items():
item = self.cm_by_id.get(anon)
if not item:
continue
status = d["status"]
if status == "UNKNOWN":
continue
layers = d.get("standard_layers") or []
row: dict[str, Any] = {
"cm95_resource_key": item["resource_key"],
"status": status,
"standard_resource_key": d.get("standard_resource_key"),
"component_kind": d.get("component_kind"),
"assignment_mode": d.get("assignment_mode") or (
"COMPONENT_ONLY" if status == "COMPONENT"
else ("VISUAL_OBJECT_MATCH" if d.get("standard_resource_key") else "LAYER_ONLY")
),
"review_stage": "user_manual_ui_review",
"note": d.get("note", ""),
}
if status in {"MATCH", "PARTIAL"} and len(layers) == 1:
row["standard_layer"] = layers[0]
elif status == "AMBIGUOUS":
row["standard_layers"] = layers
anchors.append(row)
exported = {
"schema": "cm95-reviewed-visual-anchors/manual-v1",
"generated_at": datetime.now(timezone.utc).isoformat(),
"source_manual_review": str(MANUAL_REVIEW_PATH),
"rules": {
"source_layer_used": False,
"provider_field_semantics_used": False,
"manual_review_is_authoritative": True,
"unreviewed_resources_are_unknown": True,
},
"anchors": anchors,
}
write_json_atomic(MANUAL_ANCHOR_PATH, exported)
script = TOOL_DIR / "apply_reviewed_layer_assignment.py"
cmd = [
sys.executable, str(script),
"--anonymous", str(CM95_CATALOG_PATH),
"--reviewed-anchors", str(MANUAL_ANCHOR_PATH),
"--output", str(MANUAL_ASSIGNMENT_DIR),
]
proc = subprocess.run(cmd, text=True, capture_output=True, timeout=120)
if proc.returncode != 0:
raise RuntimeError(proc.stderr or proc.stdout or "assignment 生成失败")
return {
"ok": True,
"manual_review_path": str(MANUAL_REVIEW_PATH),
"anchor_path": str(MANUAL_ANCHOR_PATH),
"assignment_dir": str(MANUAL_ASSIGNMENT_DIR),
"stdout": proc.stdout,
}
STATE = ReviewState()
class Handler(BaseHTTPRequestHandler):
server_version = "StyleVisualObjectReviewer/1.0"
def _send_json(self, obj: Any, status: int = 200) -> None:
data = json.dumps(obj, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(data)
def _send_bytes(self, data: bytes, content_type: str, status: int = 200) -> None:
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(data)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(data)
def _read_json(self) -> dict[str, Any]:
length = int(self.headers.get("Content-Length", "0") or 0)
if length <= 0 or length > 2_000_000:
raise ValueError("请求体大小不合法")
return json.loads(self.rfile.read(length).decode("utf-8"))
def do_GET(self) -> None:
try:
parsed = urllib.parse.urlparse(self.path)
path = parsed.path
if path == "/api/bootstrap":
return self._send_json(STATE.bootstrap())
if path == "/api/audit":
q = urllib.parse.parse_qs(parsed.query)
anon = (q.get("id") or [""])[0]
return self._send_json(STATE.audit_info(anon))
if path.startswith("/sprite/cm95/") and path.endswith(".png"):
anon = urllib.parse.unquote(path[len("/sprite/cm95/"):-4])
return self._send_bytes(STATE.crop_png("cm95", anon), "image/png")
if path.startswith("/sprite/std/") and path.endswith(".png"):
key = urllib.parse.unquote(path[len("/sprite/std/"):-4])
return self._send_bytes(STATE.crop_png("std", key), "image/png")
if path == "/" or path == "/index.html":
file_path = WEB_DIR / "index.html"
else:
rel = path.lstrip("/")
if ".." in Path(rel).parts:
return self._send_json({"error": "invalid path"}, 400)
file_path = WEB_DIR / rel
if not file_path.exists() or not file_path.is_file():
return self._send_json({"error": "not found"}, 404)
content_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
return self._send_bytes(file_path.read_bytes(), content_type)
except Exception as exc:
return self._send_json({"error": str(exc)}, 500)
def do_POST(self) -> None:
try:
parsed = urllib.parse.urlparse(self.path)
payload = self._read_json()
if parsed.path == "/api/review":
decision = STATE.save_decision(payload)
return self._send_json({"ok": True, "decision": decision, "progress": STATE.bootstrap()["progress"]})
if parsed.path == "/api/unreview":
anon = str(payload.get("anonymous_id") or "")
STATE.delete_decision(anon)
return self._send_json({"ok": True, "progress": STATE.bootstrap()["progress"]})
if parsed.path == "/api/abandon-unreviewed":
dry_run = bool(payload.get("dry_run"))
result = STATE.abandon_unreviewed(dry_run=dry_run)
return self._send_json({"ok": True, **result, "progress": STATE.bootstrap()["progress"]})
if parsed.path == "/api/finalize":
return self._send_json(STATE.finalize())
return self._send_json({"error": "not found"}, 404)
except ValueError as exc:
return self._send_json({"error": str(exc)}, 400)
except Exception as exc:
return self._send_json({"error": str(exc)}, 500)
def log_message(self, fmt: str, *args: Any) -> None:
sys.stdout.write("%s - %s\n" % (self.address_string(), fmt % args))
sys.stdout.flush()
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="0.0.0.0")
ap.add_argument("--port", type=int, default=18083)
args = ap.parse_args()
server = ThreadingHTTPServer((args.host, args.port), Handler)
print(f"CM95 Visual Object Reviewer: http://{args.host}:{args.port}/")
server.serve_forever()
if __name__ == "__main__":
main()

View File

@@ -1,538 +0,0 @@
const state = {
data: null,
filter: "UNREVIEWED",
visibleItems: [],
currentIndex: 0,
selectedStatus: null,
selectedLayers: new Set(),
selectedStdKey: null,
selectedComponentKind: null
};
const $ = function(id){ return document.getElementById(id); };
function esc(v){
return String(v == null ? "" : v).replace(/[&<>"']/g, function(ch){
return {"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[ch];
});
}
async function api(path, options){
const res = await fetch(path, options || {});
const data = await res.json();
if(!res.ok) throw new Error(data.error || ("HTTP " + res.status));
return data;
}
async function load(){
state.data = await api("/api/bootstrap");
populateLayerFilter();
applyFilter(true);
updateProgress(state.data.progress);
}
function decision(item){ return item.decision || null; }
function applyFilter(resetIndex){
const f = state.filter;
state.visibleItems = state.data.items.filter(function(item){
const d = decision(item);
if(f === "ALL") return true;
if(f === "UNREVIEWED") return !d;
return d && d.status === f;
});
if(resetIndex) state.currentIndex = 0;
if(state.currentIndex >= state.visibleItems.length) state.currentIndex = Math.max(0, state.visibleItems.length - 1);
renderQueue();
renderCurrent();
}
function renderQueue(){
$("queueCount").textContent = state.visibleItems.length + " 项";
$("queue").innerHTML = state.visibleItems.map(function(item, idx){
const d = decision(item);
const st = d ? d.status : "未确认";
return '<button class="queue-item ' + (idx===state.currentIndex?'active':'') + '" data-index="' + idx + '">' +
'<span class="queue-thumb"><img loading="lazy" src="/sprite/cm95/' + encodeURIComponent(item.anonymous_id) + '.png"></span>' +
'<span class="queue-info"><strong>' + esc(item.display_name_zh || item.anonymous_id) + '</strong>' +
'<small>' + esc(item.anonymous_id) + '</small>' +
'<small>' + esc(item.component_roles.join(", ") || "—") + ' · ' + item.occurrence_count + ' 次</small>' +
'<span class="queue-status">' + esc(st) + '</span></span></button>';
}).join("");
document.querySelectorAll(".queue-item").forEach(function(btn){
btn.addEventListener("click", function(){
state.currentIndex = Number(btn.dataset.index);
renderQueue();
renderCurrent();
});
});
}
function renderCurrent(){
const item = state.visibleItems[state.currentIndex];
const empty = !item;
$("emptyState").classList.toggle("hidden", !empty);
$("reviewContent").classList.toggle("hidden", empty);
if(empty) return;
$("cmNameZh").textContent = item.display_name_zh || "未命名视觉对象";
$("anonId").textContent = item.anonymous_id;
$("cmOfficialDesc").textContent = item.official_description_en ? ("S-52: " + item.official_description_en) : (item.name_source || "");
$("cmImage").src = "/sprite/cm95/" + encodeURIComponent(item.anonymous_id) + ".png";
$("componentRoles").textContent = item.component_roles.join(", ") || "—";
$("geometryRoles").textContent = item.geometry_roles.join(", ") || "—";
$("occurrenceCount").textContent = item.occurrence_count;
const sem = item.semantic || {};
$("semanticCategory").textContent = sem.category_zh || "尚未建立语义分类";
$("semanticReason").textContent = sem.reason_zh || "当前没有可靠的 Provider 语义规则。";
const componentHint = $("semanticComponentHint");
if(sem.component_kind){
const kind = (state.data.component_kinds || []).find(function(x){ return x.id === sem.component_kind; });
componentHint.classList.remove("hidden");
componentHint.textContent = "建议操作:COMPONENT · " + (kind ? kind.name_zh : sem.component_kind);
} else {
componentHint.classList.add("hidden");
componentHint.textContent = "";
}
$("decisionConflict").classList.toggle("hidden", !item.decision_conflict);
const s = item.suggestion;
if(s){
const layers = s.standard_layer ? [s.standard_layer] : (s.standard_layers || []);
$("suggestionBox").classList.remove("hidden");
const layerText = layers.map(function(layer){
return (state.data.standard_layer_zh[layer] || layer) + "(" + layer + ")";
}).join(" / ");
const nameText = s.standard_name_zh || "未指定标准对象名";
$("suggestionBox").innerHTML = "<b>旧视觉匹配建议(辅助证据)</b><br>" +
'<span class="candidate-name">' + esc(nameText) + '</span><br>' +
esc(s.status || "—") + " → " + esc(layerText || "—") +
(s.review_stage ? '<br><span class="muted">' + esc(s.review_stage) + "</span>" : "");
} else {
$("suggestionBox").classList.add("hidden");
$("suggestionBox").innerHTML = "";
}
const d = decision(item);
state.selectedStatus = d ? d.status : null;
state.selectedLayers = new Set(d ? (d.standard_layers || []) : []);
state.selectedStdKey = d ? d.standard_resource_key : null;
state.selectedComponentKind = d ? (d.component_kind || null) : null;
$("note").value = d ? (d.note || "") : "";
renderDecisionBadge(d);
renderStatusButtons();
renderModeFields();
renderComponentKindSelection();
renderLayerSelection();
renderLayerSuggestions(item);
renderCandidates(item);
renderSelectedStd();
$("auditDetails").open = false;
$("auditText").textContent = "尚未读取。";
$("message").textContent = "";
$("message").className = "message";
}
function renderDecisionBadge(d){
const badge = $("decisionBadge");
const st = d ? d.status : null;
badge.className = "badge " + (st ? st.toLowerCase() : "neutral");
badge.textContent = st || "未确认";
}
function renderStatusButtons(){
document.querySelectorAll(".status-btn").forEach(function(btn){
btn.classList.toggle("active", btn.dataset.status === state.selectedStatus);
});
}
function renderModeFields(){
const isComponent = state.selectedStatus === "COMPONENT";
$("componentKindField").classList.toggle("hidden", !isComponent);
$("standardLayerField").classList.toggle("hidden", isComponent);
$("standardObjectField").classList.toggle("hidden", isComponent);
}
function renderComponentKindSelection(){
const kinds = state.data.component_kinds || [];
$("componentKindSelection").innerHTML = kinds.map(function(k){
return '<button class="component-kind-chip ' + (state.selectedComponentKind===k.id?'selected':'') +
'" data-kind="' + esc(k.id) + '">' + esc(k.name_zh) + '</button>';
}).join("");
document.querySelectorAll(".component-kind-chip").forEach(function(btn){
btn.addEventListener("click", function(){
const kind = btn.dataset.kind;
state.selectedComponentKind = state.selectedComponentKind === kind ? null : kind;
renderComponentKindSelection();
});
});
}
function renderLayerSelection(){
$("layerSelection").innerHTML = state.data.standard_layers.map(function(layer){
const zh = state.data.standard_layer_zh[layer] || layer;
return '<button class="layer-chip ' + (state.selectedLayers.has(layer)?'selected':'') +
'" data-layer="' + esc(layer) + '">' + esc(zh) + '<span class="layer-code"> · ' + esc(layer) + '</span></button>';
}).join("");
document.querySelectorAll(".layer-chip").forEach(function(btn){
btn.addEventListener("click", function(){
const layer = btn.dataset.layer;
if(state.selectedStatus === "UNKNOWN" || state.selectedStatus === "COMPONENT") return;
if(state.selectedStatus === "MATCH" || state.selectedStatus === "PARTIAL"){
state.selectedLayers = new Set([layer]);
if(state.selectedStdKey){
const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; });
if(sr && sr.standard_layer !== layer) state.selectedStdKey = null;
}
} else {
if(state.selectedLayers.has(layer)) state.selectedLayers.delete(layer);
else state.selectedLayers.add(layer);
}
renderLayerSelection();
renderSelectedStd();
});
});
}
function renderLayerSuggestions(item){
const rows = item.layer_suggestions || [];
const box = $("layerSuggestionList");
if(item.semantic && item.semantic.component_kind){
const kind = (state.data.component_kinds || []).find(function(x){ return x.id === item.semantic.component_kind; });
const kindName = kind ? kind.name_zh : item.semantic.component_kind;
box.innerHTML = '<div class="component-recommendation">' +
'<div class="layer-rank-title">建议先作为组件处理</div>' +
'<div class="layer-rank-name">COMPONENT · ' + esc(kindName) + '</div>' +
'<div class="layer-rank-reason">' + esc(item.semantic.reason_zh || '') + '</div>' +
'<button id="applyComponentSuggestion" class="apply-suggestion-btn">采用 COMPONENT 建议</button>' +
'</div>' + renderLayerRows(rows);
const btn = $("applyComponentSuggestion");
if(btn){ btn.addEventListener("click", function(){
chooseStatus("COMPONENT");
state.selectedComponentKind = item.semantic.component_kind;
renderComponentKindSelection();
}); }
bindLayerSuggestionClicks();
return;
}
if(!rows.length){
box.innerHTML = '<div class="no-layer-suggestion">当前 S-52 语义没有可靠的 NavSea Layer 建议。请人工选择,或保留 UNKNOWN。</div>';
return;
}
box.innerHTML = renderLayerRows(rows);
bindLayerSuggestionClicks();
}
function renderLayerRows(rows){
return rows.map(function(r, idx){
const top = idx === 0;
const score = (Number(r.combined_score || 0) * 100).toFixed(1);
const sem = (Number(r.semantic_score || 0) * 100).toFixed(0);
const vis = (Number(r.visual_score || 0) * 100).toFixed(0);
return '<button class="layer-suggestion-card ' + (top?'top':'') + '" data-layer="' + esc(r.standard_layer) + '">' +
'<div class="layer-rank-head"><span class="layer-rank-index">' + (idx+1) + '</span>' +
'<span class="layer-rank-name">' + esc(r.standard_layer_zh || r.standard_layer) + '</span>' +
(top?'<span class="layer-rank-top">最可能</span>':'') + '</div>' +
'<div class="layer-rank-code">' + esc(r.standard_layer) + '</div>' +
'<div class="layer-rank-score">综合 ' + score + '% · 语义 ' + sem + '% · 视觉 ' + vis + '%</div>' +
'<div class="layer-rank-reason">' + esc(r.reason_zh || '') + '</div>' +
'</button>';
}).join('');
}
function bindLayerSuggestionClicks(){
document.querySelectorAll(".layer-suggestion-card").forEach(function(card){
card.addEventListener("click", function(){
const layer = card.dataset.layer;
state.selectedLayers = new Set([layer]);
if(!state.selectedStatus || state.selectedStatus === "UNKNOWN") state.selectedStatus = "PARTIAL";
if(state.selectedStdKey){
const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; });
if(sr && sr.standard_layer !== layer) state.selectedStdKey = null;
}
renderStatusButtons();
renderModeFields();
renderLayerSelection();
renderSelectedStd();
showMessage("已选择 Layer:" + (state.data.standard_layer_zh[layer] || layer) + "。请确认后保存。", true);
});
});
}
function renderCandidates(item){
const seen = new Set();
const candidates = [];
if(item.suggestion && item.suggestion.standard_resource_key){
const sr = state.data.standard_resources.find(function(x){ return x.resource_key === item.suggestion.standard_resource_key; });
const raw = (item.candidates || []).find(function(x){ return x.resource_key === item.suggestion.standard_resource_key; });
if(sr){
candidates.push({
resource_key:sr.resource_key,standard_layer:sr.standard_layer,standard_layer_zh:sr.standard_layer_zh,display_name_zh:sr.display_name_zh,
score:raw ? raw.score : null,text_score:raw ? raw.text_score : null,recommendation_score:raw ? raw.recommendation_score : null,
source:"已有建议"
});
seen.add(sr.resource_key);
}
}
(item.candidates || []).forEach(function(c){
if(!seen.has(c.resource_key)){
candidates.push({resource_key:c.resource_key,standard_layer:c.standard_layer,standard_layer_zh:c.standard_layer_zh,display_name_zh:c.display_name_zh,score:c.score,text_score:c.text_score,recommendation_score:c.recommendation_score,source:"图标参考"});
seen.add(c.resource_key);
}
});
if(state.selectedStdKey && !seen.has(state.selectedStdKey)){
const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; });
if(sr) candidates.unshift({resource_key:sr.resource_key,standard_layer:sr.standard_layer,standard_layer_zh:sr.standard_layer_zh,display_name_zh:sr.display_name_zh,source:"当前选择"});
}
if(!candidates.length){
$("candidateGrid").innerHTML = '<div class="muted">没有 Standard Visual Object 图标参考。Layer 仍可单独人工确认。</div>';
return;
}
$("candidateGrid").innerHTML = candidates.map(function(c){
const selected = c.resource_key === state.selectedStdKey;
const recommended = c.resource_key === item.recommended_resource_key;
const score = c.score != null ? (" · 视觉 " + (Number(c.score)*100).toFixed(1) + "%") : "";
const recScore = c.recommendation_score != null ? (" · 综合 " + (Number(c.recommendation_score)*100).toFixed(1) + "%") : "";
return '<div class="candidate ' + (selected?'selected ':'') + (recommended?'recommended':'') + '" data-key="' + esc(c.resource_key) +
'" data-layer="' + esc(c.standard_layer) + '">' +
'<div class="candidate-img"><img src="/sprite/std/' + encodeURIComponent(c.resource_key) + '.png"></div>' +
'<div class="candidate-name ' + (recommended?'recommended-name':'') + '">' +
esc(c.display_name_zh || c.standard_layer_zh || c.resource_key) +
(recommended ? '<span class="recommended-tag">最可能</span>' : '') +
'</div>' +
'<div class="candidate-code">' + esc(c.resource_key) + '</div>' +
'<small>' + esc(c.standard_layer_zh || c.standard_layer) + ' · ' + esc(c.standard_layer) + '</small>' +
'<small>' + esc(c.source) + score + recScore + '</small></div>';
}).join("");
document.querySelectorAll("#candidateGrid .candidate").forEach(function(card){
card.addEventListener("click", function(){ selectStandard(card.dataset.key, card.dataset.layer); });
});
}
function selectStandard(key, layer){
if(state.selectedStatus === "COMPONENT" || state.selectedStatus === "UNKNOWN") return;
state.selectedStdKey = key;
if(state.selectedStatus !== "AMBIGUOUS" && state.selectedStatus !== "UNKNOWN"){
state.selectedLayers = new Set([layer]);
} else if(state.selectedStatus === "AMBIGUOUS"){
state.selectedLayers.add(layer);
}
renderCandidates(state.visibleItems[state.currentIndex]);
renderLayerSelection();
renderSelectedStd();
}
function renderSelectedStd(){
const el = $("selectedStd");
if(!state.selectedStdKey){
el.textContent = "未选择";
el.classList.add("muted");
return;
}
const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; });
if(!sr) return;
el.classList.remove("muted");
el.innerHTML = "<b>" + esc(sr.display_name_zh || sr.resource_key) + "</b> · " +
esc(sr.standard_layer_zh || sr.standard_layer) +
'<div class="candidate-code">' + esc(sr.resource_key) + ' · ' + esc(sr.standard_layer) + '</div>';
}
function useLayerOnly(){
if(state.selectedLayers.size !== 1){
showMessage("请先选择一个标准 Layer。", false);
return;
}
state.selectedStdKey = null;
if(!state.selectedStatus || state.selectedStatus === "MATCH") state.selectedStatus = "PARTIAL";
renderStatusButtons();
renderCandidates(state.visibleItems[state.currentIndex]);
renderSelectedStd();
showMessage("已设为:仅确认 Layer,不绑定标准图标。", true);
}
function chooseStatus(status){
state.selectedStatus = status;
if(status === "UNKNOWN" || status === "COMPONENT"){
state.selectedLayers.clear();
state.selectedStdKey = null;
} else {
state.selectedComponentKind = null;
if((status === "MATCH" || status === "PARTIAL") && state.selectedLayers.size > 1){
state.selectedLayers = new Set([Array.from(state.selectedLayers)[0]]);
}
}
renderStatusButtons();
renderModeFields();
renderComponentKindSelection();
renderLayerSelection();
renderCandidates(state.visibleItems[state.currentIndex]);
renderSelectedStd();
}
async function saveDecision(goNext){
const item = state.visibleItems[state.currentIndex];
if(!item) return;
if(!state.selectedStatus){ showMessage("请选择 MATCH / PARTIAL / AMBIGUOUS / COMPONENT / UNKNOWN。", false); return; }
const payload = {
anonymous_id:item.anonymous_id,
status:state.selectedStatus,
standard_layers:Array.from(state.selectedLayers),
standard_resource_key:state.selectedStdKey,
component_kind:state.selectedComponentKind,
note:$("note").value || ""
};
try{
const res = await api("/api/review", {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});
item.decision = res.decision;
updateProgress(res.progress);
showMessage("已保存。", true);
if(goNext){
if(state.filter === "UNREVIEWED") applyFilter(false);
else {
if(state.currentIndex < state.visibleItems.length-1) state.currentIndex++;
renderQueue(); renderCurrent();
}
} else {
renderDecisionBadge(item.decision); renderQueue();
}
}catch(e){ showMessage(e.message, false); }
}
async function clearDecision(){
const item = state.visibleItems[state.currentIndex];
if(!item || !item.decision) return;
try{
const res = await api("/api/unreview", {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({anonymous_id:item.anonymous_id})});
item.decision = null;
updateProgress(res.progress);
applyFilter(false);
}catch(e){ showMessage(e.message, false); }
}
function showMessage(text, ok){
$("message").textContent = text;
$("message").className = "message " + (ok?"ok":"err");
}
function updateProgress(p){
const pct = p.total ? (p.reviewed/p.total*100) : 0;
$("progressText").textContent = p.reviewed + "/" + p.total + " 已确认 · MATCH " + p.MATCH +
" · PARTIAL " + p.PARTIAL + " · AMBIGUOUS " + p.AMBIGUOUS + " · COMPONENT " + p.COMPONENT + " · UNKNOWN " + p.UNKNOWN;
$("progressBar").style.width = pct + "%";
}
function move(delta){
if(!state.visibleItems.length) return;
state.currentIndex = Math.max(0, Math.min(state.visibleItems.length-1, state.currentIndex+delta));
renderQueue(); renderCurrent();
const active = document.querySelector(".queue-item.active");
if(active) active.scrollIntoView({block:"nearest"});
}
function populateLayerFilter(){
$("layerFilter").innerHTML = '<option value="">全部标准 Layer</option>' +
state.data.standard_layers.map(function(x){ return '<option value="' + esc(x) + '">' + esc(state.data.standard_layer_zh[x] || x) + ' · ' + esc(x) + "</option>"; }).join("");
}
function openLibrary(){ renderLibrary(); $("libraryDialog").showModal(); }
function renderLibrary(){
const layer = $("layerFilter").value;
const q = $("librarySearch").value.trim().toLowerCase();
const rows = state.data.standard_resources.filter(function(x){
return (!layer || x.standard_layer === layer) &&
(!q || x.resource_key.toLowerCase().includes(q) || x.standard_layer.toLowerCase().includes(q) ||
(x.display_name_zh || "").toLowerCase().includes(q) || (x.standard_layer_zh || "").toLowerCase().includes(q));
});
$("libraryGrid").innerHTML = rows.map(function(x){
return '<div class="library-item" data-key="' + esc(x.resource_key) + '" data-layer="' + esc(x.standard_layer) + '">' +
'<img loading="lazy" src="/sprite/std/' + encodeURIComponent(x.resource_key) + '.png">' +
'<div class="candidate-name">' + esc(x.display_name_zh || x.resource_key) + '</div>' +
'<div class="candidate-code">' + esc(x.resource_key) + '</div>' +
'<small>' + esc(x.standard_layer_zh || x.standard_layer) + ' · ' + esc(x.standard_layer) + '</small></div>';
}).join("");
document.querySelectorAll(".library-item").forEach(function(el){
el.addEventListener("click", function(){
selectStandard(el.dataset.key, el.dataset.layer);
$("libraryDialog").close();
});
});
}
async function loadAudit(){
const item = state.visibleItems[state.currentIndex];
if(!item) return;
try{
const data = await api("/api/audit?id=" + encodeURIComponent(item.anonymous_id));
$("auditText").textContent = JSON.stringify(data, null, 2);
}catch(e){ $("auditText").textContent = e.message; }
}
async function abandonAllUnreviewed(){
const p = state.data && state.data.progress ? state.data.progress : null;
const count = p ? p.unreviewed : 0;
if(!count){ alert("当前没有未确认项。 "); return; }
if(!confirm("确定放弃剩余 " + count + " 个未确认项吗?\n\n它们会全部设为 UNKNOWN;已经人工确认的项目不会改变。")) return;
try{
const res = await api("/api/abandon-unreviewed", {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dry_run:false})});
state.data = await api("/api/bootstrap");
updateProgress(state.data.progress);
applyFilter(true);
alert("已放弃 " + res.affected + " 个未确认项,全部标记为 UNKNOWN。 ");
}catch(e){ alert("批量放弃失败:" + e.message); }
}
async function finalize(){
if(!confirm("生成当前人工确认结果?未确认项目会保持 UNKNOWN,不会自动继承机器建议。")) return;
try{
const res = await api("/api/finalize", {method:"POST",headers:{"Content-Type":"application/json"},body:"{}"});
alert("已生成。\n\n" + res.assignment_dir);
}catch(e){ alert("生成失败:" + e.message); }
}
document.addEventListener("click", function(e){
const f = e.target.closest(".filter");
if(f){
document.querySelectorAll(".filter").forEach(function(x){ x.classList.remove("active"); });
f.classList.add("active"); state.filter = f.dataset.filter; applyFilter(true);
}
const s = e.target.closest(".status-btn");
if(s) chooseStatus(s.dataset.status);
});
$("prevBtn").addEventListener("click", function(){ move(-1); });
$("nextBtn").addEventListener("click", function(){ move(1); });
$("saveBtn").addEventListener("click", function(){ saveDecision(true); });
$("saveStayBtn").addEventListener("click", function(){ saveDecision(false); });
$("clearBtn").addEventListener("click", clearDecision);
$("layerOnlyBtn").addEventListener("click", useLayerOnly);
$("openLibraryBtn").addEventListener("click", openLibrary);
$("closeLibraryBtn").addEventListener("click", function(){ $("libraryDialog").close(); });
$("layerFilter").addEventListener("change", renderLibrary);
$("librarySearch").addEventListener("input", renderLibrary);
$("loadAuditBtn").addEventListener("click", loadAudit);
$("abandonAllBtn").addEventListener("click", abandonAllUnreviewed);
$("finalizeBtn").addEventListener("click", finalize);
document.addEventListener("keydown", function(e){
const tag = document.activeElement ? document.activeElement.tagName : "";
if(["TEXTAREA","INPUT","SELECT"].includes(tag)) return;
if(e.key === "m" || e.key === "M") chooseStatus("MATCH");
else if(e.key === "p" || e.key === "P") chooseStatus("PARTIAL");
else if(e.key === "a" || e.key === "A") chooseStatus("AMBIGUOUS");
else if(e.key === "c" || e.key === "C") chooseStatus("COMPONENT");
else if(e.key === "u" || e.key === "U") chooseStatus("UNKNOWN");
else if(e.key === "ArrowLeft") move(-1);
else if(e.key === "ArrowRight") move(1);
else if(e.key === "Enter") saveDecision(true);
});
load().catch(function(e){
document.body.innerHTML = '<pre style="padding:20px;color:#b42318">' + esc(e.stack || e.message) + "</pre>";
});

View File

@@ -1,173 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>CM95 → NavSea Layer 人工确认台</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<header class="topbar">
<div>
<h1>CM95 → NavSea Layer 人工确认台</h1>
<p>左侧显示 CM95/S-52 对象与语义;中间给出 NavSea Standard Layer 建议;右侧由人工确认最终 Layer。Standard Visual Object 只作为可选视觉参考。</p>
</div>
<div class="progress-wrap">
<div id="progressText">加载中…</div>
<div class="progress"><div id="progressBar"></div></div>
</div>
</header>
<section class="toolbar">
<div class="filter-group">
<label>筛选</label>
<button data-filter="UNREVIEWED" class="filter active">未确认</button>
<button data-filter="ALL" class="filter">全部</button>
<button data-filter="MATCH" class="filter">MATCH</button>
<button data-filter="PARTIAL" class="filter">PARTIAL</button>
<button data-filter="AMBIGUOUS" class="filter">AMBIGUOUS</button>
<button data-filter="COMPONENT" class="filter">COMPONENT</button>
<button data-filter="UNKNOWN" class="filter">UNKNOWN</button>
</div>
<div class="toolbar-right">
<button id="prevBtn">← 上一个</button>
<button id="nextBtn">下一个 →</button>
<button id="abandonAllBtn" class="danger-ghost">全部放弃未确认项</button>
<button id="finalizeBtn" class="primary">生成当前人工确认结果</button>
</div>
</section>
<main class="layout">
<aside class="queue-panel">
<div class="queue-head">
<span id="queueTitle">队列</span>
<span id="queueCount"></span>
</div>
<div id="queue" class="queue"></div>
</aside>
<section class="review-panel">
<div id="emptyState" class="empty">没有符合当前筛选条件的对象。</div>
<div id="reviewContent" class="hidden">
<div class="comparison">
<section class="cm-card">
<div class="card-title">
<div>
<div class="eyebrow">CM95 匿名 Visual Object</div>
<h2 id="cmNameZh"></h2><div id="anonId" class="muted"></div><div id="cmOfficialDesc" class="official-desc"></div>
</div>
<div id="decisionBadge" class="badge neutral">未确认</div>
</div>
<div class="cm-main">
<div class="image-stage large">
<img id="cmImage" alt="CM95 Visual Object">
</div>
<div id="semanticBox" class="semantic-box">
<div class="semantic-label">S-52 对象大类</div>
<div id="semanticCategory" class="semantic-category">—</div>
<div id="semanticReason" class="semantic-reason">—</div>
<div id="semanticComponentHint" class="semantic-component-hint hidden"></div>
<div id="decisionConflict" class="semantic-conflict hidden">当前人工结果与新的语义分类建议不一致,建议复核。</div>
</div>
<div class="meta-grid">
<div><span>组件</span><b id="componentRoles">—</b></div>
<div><span>几何</span><b id="geometryRoles">—</b></div>
<div><span>出现次数</span><b id="occurrenceCount">—</b></div>
</div>
<div id="suggestionBox" class="suggestion hidden"></div>
</div>
</section>
<section class="std-card">
<div class="card-title">
<div>
<div class="eyebrow">Provider Layer Classifier</div>
<h2>NavSea Layer 建议</h2>
</div>
<button id="openLibraryBtn">打开标准库</button>
</div>
<div id="layerSuggestionList" class="layer-suggestion-list"></div>
<details id="visualReferenceDetails" class="visual-reference-details">
<summary>Standard Visual Object 图标参考</summary>
<div id="candidateGrid" class="candidate-grid"></div>
</details>
</section>
</div>
<section class="decision-panel">
<div class="decision-head">
<h3>人工判断</h3>
<div class="shortcut-hint">快捷键:M / P / A / C / U,Enter 保存并下一个</div>
</div>
<div class="status-buttons">
<button data-status="MATCH" class="status-btn match">MATCH</button>
<button data-status="PARTIAL" class="status-btn partial">PARTIAL</button>
<button data-status="AMBIGUOUS" class="status-btn ambiguous">AMBIGUOUS</button>
<button data-status="COMPONENT" class="status-btn component">COMPONENT</button>
<button data-status="UNKNOWN" class="status-btn unknown">UNKNOWN</button>
</div>
<div class="decision-form">
<div id="componentKindField" class="field hidden">
<label>组件类型(可选)</label>
<div id="componentKindSelection" class="component-kind-selection"></div>
<div class="help">COMPONENT 表示它只是完整 Visual Object 的组成部分,不单独分配 Layer。</div>
</div>
<div id="standardLayerField" class="field">
<label>标准 Layer</label>
<div id="layerSelection" class="layer-selection"></div>
<div class="help">MATCH / PARTIAL 必须选 1 个;AMBIGUOUS 至少选 2 个;UNKNOWN 不选。</div>
</div>
<div id="standardObjectField" class="field">
<label>Standard Visual Object(可选)</label>
<div id="selectedStd" class="selected-std muted">未选择</div>
<button id="layerOnlyBtn" class="layer-only-btn">没有对应标准图标,只确认所属 Layer</button>
<div class="help">S-52 内容多于 NavSea 时,可以不选标准图标,直接确认所属 Layer。</div>
</div>
<div class="field">
<label for="note">备注</label>
<textarea id="note" rows="3" placeholder="可选:记录为什么确认或为什么保留 UNKNOWN"></textarea>
</div>
<div class="decision-actions">
<button id="saveBtn" class="primary">保存并下一个</button>
<button id="saveStayBtn">仅保存</button>
<button id="clearBtn" class="danger-ghost">撤销本项人工确认</button>
</div>
<div id="message" class="message"></div>
</div>
</section>
<details id="auditDetails" class="audit">
<summary>审计信息(建议先完成判断,再展开)</summary>
<button id="loadAuditBtn">读取原始 provenance</button>
<pre id="auditText">尚未读取。</pre>
</details>
</div>
</section>
</main>
<dialog id="libraryDialog">
<div class="dialog-head">
<div>
<div class="eyebrow">NavSea Frozen Standard Catalog</div>
<h2>标准 Visual Object 库</h2>
</div>
<button id="closeLibraryBtn">关闭</button>
</div>
<div class="library-tools">
<select id="layerFilter"></select>
<input id="librarySearch" type="search" placeholder="搜索标准资源名">
</div>
<div id="libraryGrid" class="library-grid"></div>
</dialog>
<script src="/app.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,394 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import hashlib
import json
import re
import urllib.request
from collections import defaultdict
from pathlib import Path
from typing import Any
from PIL import Image
VISUAL_RESOURCE_PROPS = (
("layout", "icon-image", "icon"),
("paint", "fill-pattern", "fill-pattern"),
("paint", "line-pattern", "line-pattern"),
)
VECTOR_PROPS = {
"fill": ("fill-color", "fill-opacity", "fill-outline-color"),
"line": ("line-color", "line-width", "line-dasharray"),
"circle": ("circle-color", "circle-radius", "circle-stroke-color", "circle-stroke-width"),
"symbol": ("text-color", "text-halo-color", "text-halo-width"),
}
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def sid(prefix: str, value: Any) -> str:
return prefix + "-" + hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()[:16]
def load_json(path_or_url: str) -> dict[str, Any]:
if re.match(r"^https?://", path_or_url):
with urllib.request.urlopen(path_or_url, timeout=30) as r:
return json.loads(r.read().decode("utf-8"))
return json.loads(Path(path_or_url).read_text(encoding="utf-8"))
def resolve_sprite_base(style: dict[str, Any], override: str | None) -> str | None:
return override or style.get("sprite")
def load_sprite(base: str | None, output: Path) -> tuple[dict[str, Any], Image.Image | None]:
if not base:
return {}, None
output.mkdir(parents=True, exist_ok=True)
if re.match(r"^https?://", base):
with urllib.request.urlopen(base + ".json", timeout=30) as r:
meta_bytes = r.read()
with urllib.request.urlopen(base + ".png", timeout=60) as r:
png_bytes = r.read()
(output / "sprite.json").write_bytes(meta_bytes)
(output / "sprite.png").write_bytes(png_bytes)
else:
base_path = Path(base)
(output / "sprite.json").write_bytes(base_path.with_suffix(".json").read_bytes())
(output / "sprite.png").write_bytes(base_path.with_suffix(".png").read_bytes())
meta = json.loads((output / "sprite.json").read_text(encoding="utf-8"))
image = Image.open(output / "sprite.png").convert("RGBA")
return meta, image
def walk_gets(value: Any, out: set[str]) -> None:
if isinstance(value, list):
if len(value) >= 2 and value[0] == "get" and isinstance(value[1], str):
out.add(value[1])
for item in value[1:]:
walk_gets(item, out)
elif isinstance(value, dict):
for item in value.values():
walk_gets(item, out)
def collect_sprite_literals(value: Any, sprite_keys: set[str], out: set[str]) -> None:
if isinstance(value, str):
if value in sprite_keys:
out.add(value)
elif isinstance(value, list):
for item in value[1:]:
collect_sprite_literals(item, sprite_keys, out)
elif isinstance(value, dict):
for item in value.values():
collect_sprite_literals(item, sprite_keys, out)
def finite_positive_domain(expr: Any, field: str) -> set[Any] | None:
if not isinstance(expr, list) or not expr:
return None
head = expr[0]
if head == "==" and len(expr) >= 3:
left, right = expr[1], expr[2]
if isinstance(left, list) and len(left) >= 2 and left[0] == "get" and left[1] == field:
if isinstance(right, (str, int, float)):
return {right}
if isinstance(right, list) and len(right) >= 2 and right[0] == "get" and right[1] == field:
if isinstance(left, (str, int, float)):
return {left}
return None
if head == "match" and len(expr) >= 5:
input_expr = expr[1]
if not (isinstance(input_expr, list) and len(input_expr) >= 2 and input_expr[0] == "get" and input_expr[1] == field):
return None
arms = expr[2:]
fallback = arms[-1]
if fallback is not False:
return None
allowed: set[Any] = set()
pairs = arms[:-1]
for i in range(0, len(pairs), 2):
if i + 1 >= len(pairs):
break
labels, result = pairs[i], pairs[i + 1]
if result is not True:
continue
if isinstance(labels, list):
for x in labels:
if isinstance(x, (str, int, float)):
allowed.add(x)
elif isinstance(labels, (str, int, float)):
allowed.add(labels)
return allowed or None
if head == "all":
parts = [finite_positive_domain(x, field) for x in expr[1:]]
parts = [x for x in parts if x is not None]
if not parts:
return None
out = set(parts[0])
for part in parts[1:]:
out &= part
return out or None
if head == "any":
parts = [finite_positive_domain(x, field) for x in expr[1:]]
if not parts or any(x is None for x in parts):
return None
out: set[Any] = set()
for part in parts:
out |= part or set()
return out or None
# Negation and inequality provide exclusions, not a finite positive domain.
if head in {"!", "!="}:
return None
return None
def eval_simple_resource(expr: Any, values: dict[str, Any]) -> Any:
if not isinstance(expr, list):
return expr
if not expr:
return expr
head = expr[0]
if head == "get" and len(expr) >= 2:
return values.get(expr[1])
if head == "to-string" and len(expr) >= 2:
value = eval_simple_resource(expr[1], values)
return None if value is None else str(value)
if head == "concat":
parts = []
for item in expr[1:]:
value = eval_simple_resource(item, values)
if value is None:
return None
parts.append(str(value))
return "".join(parts)
return None
def concat_prefix(expr: Any) -> str | None:
if not isinstance(expr, list) or not expr or expr[0] != "concat":
return None
parts = expr[1:]
prefix = ""
saw_dynamic = False
for item in parts:
if isinstance(item, str) and not saw_dynamic:
prefix += item
else:
saw_dynamic = True
return prefix if prefix and saw_dynamic else None
def direct_get_field(expr: Any) -> str | None:
if isinstance(expr, list) and len(expr) >= 2 and expr[0] == "get" and isinstance(expr[1], str):
return expr[1]
return None
def resource_candidates(expr: Any, sprite_keys: set[str], filter_expr: Any) -> tuple[list[str], list[str]]:
literals: set[str] = set()
collect_sprite_literals(expr, sprite_keys, literals)
gets: set[str] = set()
walk_gets(expr, gets)
field = direct_get_field(expr)
if field:
domain = finite_positive_domain(filter_expr, field)
if domain:
for value in domain:
if isinstance(value, str) and value in sprite_keys:
literals.add(value)
# For simple generated keys (e.g. concat(prefix, to-string(get(field)))),
# only expand when the Style filter itself supplies a finite positive domain.
if len(gets) == 1:
dynamic_field = next(iter(gets))
domain = finite_positive_domain(filter_expr, dynamic_field)
if domain:
for value in domain:
resolved = eval_simple_resource(expr, {dynamic_field: value})
if isinstance(resolved, str) and resolved in sprite_keys:
literals.add(resolved)
return sorted(literals), sorted(gets)
def sprite_visual_hash(image: Image.Image, meta: dict[str, Any]) -> str:
crop = image.crop((
int(meta["x"]), int(meta["y"]),
int(meta["x"]) + int(meta["width"]),
int(meta["y"]) + int(meta["height"]),
))
return hashlib.sha256(crop.tobytes()).hexdigest()
def extract(style_path: str, sprite_override: str | None, output: Path, catalog_name: str) -> dict[str, Any]:
output.mkdir(parents=True, exist_ok=True)
style = load_json(style_path)
style_bytes = canonical(style).encode("utf-8")
sprite_meta, sprite_image = load_sprite(resolve_sprite_base(style, sprite_override), output / "_sprite")
sprite_keys = set(sprite_meta)
groups: dict[str, dict[str, Any]] = {}
objects: list[dict[str, Any]] = []
unresolved: list[dict[str, Any]] = []
for index, layer in enumerate(style.get("layers", [])):
source_layer = layer.get("source-layer")
group_seed = {
"source": layer.get("source"),
"source_layer": source_layer,
}
group_id = sid("GRP", group_seed)
groups.setdefault(group_id, {
"group_id": group_id,
"provenance": group_seed,
"style_layer_indexes": [],
})["style_layer_indexes"].append(index)
found_resource = False
for section, key, role in VISUAL_RESOURCE_PROPS:
expr = (layer.get(section) or {}).get(key)
if expr is None:
continue
found_resource = True
candidates, dynamic_fields = resource_candidates(expr, sprite_keys, layer.get("filter"))
base = {
"group_id": group_id,
"geometry_role": layer.get("type"),
"component_role": role,
"style_index": index,
"style_layer_id": layer.get("id"),
"filter": layer.get("filter"),
"resource_expression": expr,
"dynamic_fields": dynamic_fields,
"provenance": {
"source": layer.get("source"),
"source_layer": source_layer,
"style_layer_id": layer.get("id"),
},
}
for resource_key in candidates:
meta = sprite_meta[resource_key]
obj = dict(base)
obj["resource_key"] = resource_key
obj["resource_meta"] = meta
obj["visual_hash"] = sprite_visual_hash(sprite_image, meta) if sprite_image else None
signature = {
"geometry_role": obj["geometry_role"],
"component_role": role,
"visual_hash": obj["visual_hash"],
"style_index": index,
}
obj["visual_object_id"] = sid("VO", signature)
objects.append(obj)
if not candidates:
item = dict(base)
item["reason"] = "dynamic_or_unresolved_resource"
item["visual_object_id"] = sid("VO", {
"geometry_role": item["geometry_role"],
"component_role": role,
"expression": expr,
"style_index": index,
})
unresolved.append(item)
layer_type = layer.get("type")
paint = layer.get("paint") or {}
vector_payload = {k: paint[k] for k in VECTOR_PROPS.get(layer_type, ()) if k in paint}
text_expr = (layer.get("layout") or {}).get("text-field")
if vector_payload or text_expr is not None or not found_resource:
payload = {
"group_id": group_id,
"geometry_role": layer_type,
"component_role": "vector-or-text",
"style_index": index,
"style_layer_id": layer.get("id"),
"filter": layer.get("filter"),
"visual_properties": vector_payload,
"text_expression": text_expr,
"provenance": {
"source": layer.get("source"),
"source_layer": source_layer,
"style_layer_id": layer.get("id"),
},
}
payload["visual_object_id"] = sid("VO", {
"geometry_role": layer_type,
"component_role": "vector-or-text",
"visual_properties": vector_payload,
"text_expression": text_expr,
"style_index": index,
})
objects.append(payload)
catalog = {
"schema": "style-visual-object/v1",
"catalog_name": catalog_name,
"principle": "Visual Object extraction is provider-neutral; provenance names are not semantic classification.",
"style": {
"name": style.get("name"),
"version": style.get("version"),
"sha256": hashlib.sha256(style_bytes).hexdigest(),
"source": style_path,
"sprite": style.get("sprite"),
"glyphs": style.get("glyphs"),
"layer_count": len(style.get("layers", [])),
},
"sprite": {
"resource_count": len(sprite_meta),
},
"groups": list(groups.values()),
"visual_objects": objects,
"unresolved_visual_objects": unresolved,
"summary": {
"group_count": len(groups),
"visual_object_count": len(objects),
"unresolved_count": len(unresolved),
"sprite_backed_count": sum(1 for x in objects if x.get("resource_key")),
},
}
(output / "anonymous-catalog.json").write_text(json.dumps(catalog, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return catalog
def freeze_standard(anonymous_path: Path, output: Path) -> dict[str, Any]:
catalog = json.loads(anonymous_path.read_text(encoding="utf-8"))
standard = json.loads(json.dumps(catalog))
standard["schema"] = "style-visual-object-standard/v1"
standard["catalog_status"] = "FROZEN"
standard["standard_rule"] = "Layer name is attached only after anonymous Visual Object generation."
for obj in standard["visual_objects"]:
obj["standard_layer_name"] = obj.get("provenance", {}).get("source_layer")
for obj in standard["unresolved_visual_objects"]:
obj["standard_layer_name"] = obj.get("provenance", {}).get("source_layer")
output.mkdir(parents=True, exist_ok=True)
(output / "standard-catalog.json").write_text(json.dumps(standard, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return standard
def main() -> None:
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("extract")
p.add_argument("--style", required=True)
p.add_argument("--sprite")
p.add_argument("--output", required=True)
p.add_argument("--name", required=True)
f = sub.add_parser("freeze-standard")
f.add_argument("--anonymous", required=True)
f.add_argument("--output", required=True)
args = ap.parse_args()
if args.cmd == "extract":
result = extract(args.style, args.sprite, Path(args.output), args.name)
else:
result = freeze_standard(Path(args.anonymous), Path(args.output))
print(json.dumps(result["summary"], ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -1,426 +0,0 @@
#!/usr/bin/env python3
"""CM95 → NavSea Layer 分类基线一致性校验。
只读取已冻结/已完成人工确认的产物,不修改分类结果。
失败时退出码为 1;全部硬校验通过时退出码为 0。
语义建议与人工结果冲突仅记 warning,因为人工结果是最终权威。
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parent
OUTPUT = ROOT / "output"
DEFAULT_PATHS = {
"cm95_catalog": OUTPUT / "cm95-anonymous" / "anonymous-catalog.json",
"standard_catalog": OUTPUT / "navsea-standard" / "standard-catalog.json",
"manual_review": OUTPUT / "cm95-manual-review" / "manual-review-v1.json",
"manual_anchors": OUTPUT / "cm95-manual-review" / "reviewed-anchors-manual-v1.json",
"assignment": OUTPUT / "cm95-manual-review" / "assignment" / "cm95-layer-assignment-v1.json",
"reference_names": OUTPUT / "reference-name-enrichment-v1.json",
"semantic_rules": ROOT / "cm95_semantic_layer_rules_v1.json",
}
VALID_STATUS = {"MATCH", "PARTIAL", "AMBIGUOUS", "COMPONENT", "UNKNOWN"}
class Validation:
def __init__(self) -> None:
self.checks: list[dict[str, Any]] = []
self.warnings: list[dict[str, Any]] = []
def check(self, name: str, condition: bool, detail: Any = None) -> None:
row = {"name": name, "status": "PASS" if condition else "FAIL"}
if detail is not None:
row["detail"] = detail
self.checks.append(row)
def warn(self, name: str, detail: Any) -> None:
self.warnings.append({"name": name, "status": "WARN", "detail": detail})
@property
def ok(self) -> bool:
return all(x["status"] == "PASS" for x in self.checks)
def load(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def anonymous_resource_id(resource_key: str, visual_hash: str | None) -> str:
seed = f"{resource_key}|{visual_hash or ''}".encode("utf-8")
return "CM95-VO-" + hashlib.sha256(seed).hexdigest()[:10].upper()
def build_resource_index(cm95: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], dict[str, str]]:
by_key: dict[str, dict[str, Any]] = {}
for obj in cm95.get("visual_objects", []):
key = obj.get("resource_key")
if not key:
continue
row = by_key.setdefault(
key,
{
"resource_key": key,
"visual_hash": obj.get("visual_hash"),
"occurrences": 0,
},
)
row["occurrences"] += 1
if not row.get("visual_hash") and obj.get("visual_hash"):
row["visual_hash"] = obj.get("visual_hash")
by_id = {
anonymous_resource_id(key, row.get("visual_hash")): key
for key, row in by_key.items()
}
return by_key, by_id
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument(
"--report",
default=str(OUTPUT / "cm95-manual-review" / "validation-report-v1.json"),
help="校验报告输出路径",
)
args = ap.parse_args()
v = Validation()
missing = [str(p) for p in DEFAULT_PATHS.values() if not p.exists()]
v.check("required_files_exist", not missing, {"missing": missing})
if missing:
report = {
"schema": "cm95-layer-classification-validation/v1",
"generated_at": datetime.now(timezone.utc).isoformat(),
"ok": False,
"checks": v.checks,
"warnings": v.warnings,
}
Path(args.report).parent.mkdir(parents=True, exist_ok=True)
Path(args.report).write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, ensure_ascii=False, indent=2))
return 1
cm95 = load(DEFAULT_PATHS["cm95_catalog"])
std = load(DEFAULT_PATHS["standard_catalog"])
manual = load(DEFAULT_PATHS["manual_review"])
anchors = load(DEFAULT_PATHS["manual_anchors"])
assignment = load(DEFAULT_PATHS["assignment"])
names = load(DEFAULT_PATHS["reference_names"])
semantic_rules = load(DEFAULT_PATHS["semantic_rules"])
cm_resources, anon_to_key = build_resource_index(cm95)
resource_keys = set(cm_resources)
decisions = manual.get("decisions", {})
decision_ids = set(decisions)
std_layers = {
str(o["standard_layer_name"])
for o in std.get("visual_objects", [])
if o.get("standard_layer_name")
}
std_resource_layers: dict[str, set[str]] = defaultdict(set)
for o in std.get("visual_objects", []):
key = o.get("resource_key")
layer = o.get("standard_layer_name")
if key and layer:
std_resource_layers[str(key)].add(str(layer))
v.check("standard_catalog_frozen", std.get("catalog_status") == "FROZEN", std.get("catalog_status"))
v.check(
"cm95_anonymous_catalog_has_no_standard_layer",
not any("standard_layer_name" in o for o in cm95.get("visual_objects", [])),
)
v.check("cm95_unique_visual_resources_150", len(resource_keys) == 150, len(resource_keys))
v.check("manual_decisions_150", len(decisions) == 150, len(decisions))
v.check("manual_covers_all_resources", decision_ids == set(anon_to_key), {
"missing": sorted(set(anon_to_key) - decision_ids),
"extra": sorted(decision_ids - set(anon_to_key)),
})
v.check(
"style_hash_cm95_matches",
manual.get("cm95_style_sha256") == cm95.get("style", {}).get("sha256"),
{
"manual": manual.get("cm95_style_sha256"),
"catalog": cm95.get("style", {}).get("sha256"),
},
)
v.check(
"style_hash_standard_matches",
manual.get("standard_style_sha256") == std.get("style", {}).get("sha256"),
{
"manual": manual.get("standard_style_sha256"),
"catalog": std.get("style", {}).get("sha256"),
},
)
status_counts = Counter()
bad_decisions: list[dict[str, Any]] = []
for anon_id, d in decisions.items():
status = d.get("status")
status_counts[status] += 1
expected_key = anon_to_key.get(anon_id)
layers = [str(x) for x in d.get("standard_layers", [])]
std_key = d.get("standard_resource_key")
mode = d.get("assignment_mode")
component_kind = d.get("component_kind")
reasons: list[str] = []
if status not in VALID_STATUS:
reasons.append("invalid_status")
if d.get("resource_key_internal") != expected_key:
reasons.append("resource_key_mismatch")
if any(layer not in std_layers for layer in layers):
reasons.append("unknown_standard_layer")
if std_key and std_key not in std_resource_layers:
reasons.append("unknown_standard_resource")
if status in {"MATCH", "PARTIAL"}:
if len(layers) != 1:
reasons.append("match_partial_requires_one_layer")
if mode not in {"VISUAL_OBJECT_MATCH", "LAYER_ONLY"}:
reasons.append("invalid_match_partial_mode")
if std_key and layers and layers[0] not in std_resource_layers.get(std_key, set()):
reasons.append("standard_resource_layer_mismatch")
elif status == "AMBIGUOUS":
if len(layers) < 2:
reasons.append("ambiguous_requires_two_layers")
if component_kind:
reasons.append("ambiguous_has_component_kind")
elif status == "COMPONENT":
if layers or std_key is not None or mode != "COMPONENT_ONLY":
reasons.append("component_must_have_no_layer_or_standard_resource")
elif status == "UNKNOWN":
if layers or std_key is not None or mode != "UNKNOWN":
reasons.append("unknown_must_have_no_layer_or_standard_resource")
if reasons:
bad_decisions.append({"anonymous_id": anon_id, "reasons": reasons, "decision": d})
v.check("manual_decision_state_constraints", not bad_decisions, bad_decisions[:20])
v.check(
"manual_status_counts_total_150",
sum(status_counts.values()) == 150,
dict(status_counts),
)
# 人工导出 anchors:UNKNOWN 不导出;其余必须一一存在。
anchor_by_key = {
str(a["cm95_resource_key"]): a
for a in anchors.get("anchors", [])
if a.get("cm95_resource_key")
}
expected_anchor_keys = {
d["resource_key_internal"]
for d in decisions.values()
if d.get("status") != "UNKNOWN"
}
v.check(
"manual_anchor_export_matches_non_unknown_decisions",
set(anchor_by_key) == expected_anchor_keys,
{
"expected": len(expected_anchor_keys),
"actual": len(anchor_by_key),
"missing": sorted(expected_anchor_keys - set(anchor_by_key)),
"extra": sorted(set(anchor_by_key) - expected_anchor_keys),
},
)
anchor_mismatches: list[dict[str, Any]] = []
decision_by_key = {d["resource_key_internal"]: d for d in decisions.values()}
for key, a in anchor_by_key.items():
d = decision_by_key[key]
if a.get("status") != d.get("status"):
anchor_mismatches.append({"resource": key, "field": "status", "decision": d.get("status"), "anchor": a.get("status")})
if a.get("standard_resource_key") != d.get("standard_resource_key"):
anchor_mismatches.append({"resource": key, "field": "standard_resource_key"})
if a.get("assignment_mode") != d.get("assignment_mode"):
anchor_mismatches.append({"resource": key, "field": "assignment_mode"})
if d.get("status") in {"MATCH", "PARTIAL"}:
expected_layer = (d.get("standard_layers") or [None])[0]
if a.get("standard_layer") != expected_layer:
anchor_mismatches.append({"resource": key, "field": "standard_layer"})
elif d.get("status") == "AMBIGUOUS":
if sorted(a.get("standard_layers", [])) != sorted(d.get("standard_layers", [])):
anchor_mismatches.append({"resource": key, "field": "standard_layers"})
elif d.get("status") == "COMPONENT":
if a.get("component_kind") != d.get("component_kind"):
anchor_mismatches.append({"resource": key, "field": "component_kind"})
v.check("manual_anchor_fields_match_decisions", not anchor_mismatches, anchor_mismatches[:20])
# Reference names 必须覆盖全部当前 CM95 资源。
name_keys = set(names.get("cm95", {}))
v.check(
"reference_names_cover_all_cm95_resources",
name_keys == resource_keys,
{
"missing": sorted(resource_keys - name_keys),
"extra": sorted(name_keys - resource_keys),
},
)
# 声明式语义规则:正则可编译,Layer 必须存在于 Standard Catalog。
rule_errors: list[dict[str, Any]] = []
for rule in semantic_rules.get("rules", []):
try:
re.compile(str(rule.get("pattern") or ""), re.IGNORECASE)
except re.error as exc:
rule_errors.append({"id": rule.get("id"), "error": f"regex: {exc}"})
for item in rule.get("layer_candidates", []):
if not isinstance(item, list) or len(item) < 2:
rule_errors.append({"id": rule.get("id"), "error": "invalid layer_candidates shape"})
continue
if str(item[0]) not in std_layers:
rule_errors.append({"id": rule.get("id"), "error": f"unknown standard layer: {item[0]}"})
v.check("semantic_rules_are_valid", not rule_errors, rule_errors)
# 最终 Assignment 的状态/Layer 必须逐 occurrence 服从人工资源级决定。
assignments = assignment.get("assignments", [])
expected_total = len(cm95.get("visual_objects", [])) + len(cm95.get("unresolved_visual_objects", []))
v.check("assignment_total_count", len(assignments) == expected_total, {
"expected": expected_total,
"actual": len(assignments),
})
assignment_errors: list[dict[str, Any]] = []
assignment_status_counts = Counter()
for row in assignments:
status = row.get("assignment_status")
assignment_status_counts[status] += 1
key = row.get("cm95_resource_key")
if key:
d = decision_by_key.get(key)
if not d:
assignment_errors.append({"visual_object_id": row.get("visual_object_id"), "error": "resource_without_manual_decision"})
continue
expected_status = d.get("status")
if status != expected_status:
assignment_errors.append({"visual_object_id": row.get("visual_object_id"), "error": "status_mismatch", "expected": expected_status, "actual": status})
continue
if status in {"MATCH", "PARTIAL"}:
expected_layer = (d.get("standard_layers") or [None])[0]
if row.get("assigned_standard_layer") != expected_layer:
assignment_errors.append({"visual_object_id": row.get("visual_object_id"), "error": "layer_mismatch"})
elif status == "AMBIGUOUS":
if sorted(row.get("candidate_standard_layers", [])) != sorted(d.get("standard_layers", [])):
assignment_errors.append({"visual_object_id": row.get("visual_object_id"), "error": "ambiguous_layers_mismatch"})
elif status == "COMPONENT":
if row.get("assigned_standard_layer") is not None or row.get("component_kind") != d.get("component_kind"):
assignment_errors.append({"visual_object_id": row.get("visual_object_id"), "error": "component_assignment_mismatch"})
elif status == "UNKNOWN":
if row.get("assigned_standard_layer") is not None:
assignment_errors.append({"visual_object_id": row.get("visual_object_id"), "error": "unknown_has_layer"})
else:
# Style-only 未展开动态对象没有对应具体资源,必须保持 UNKNOWN。
if status != "UNKNOWN":
assignment_errors.append({"visual_object_id": row.get("visual_object_id"), "error": "unresolved_dynamic_not_unknown"})
v.check("final_assignment_matches_manual_decisions", not assignment_errors, assignment_errors[:20])
summary = assignment.get("summary", {})
recomputed = {
"MATCH": assignment_status_counts["MATCH"],
"PARTIAL": assignment_status_counts["PARTIAL"],
"AMBIGUOUS": assignment_status_counts["AMBIGUOUS"],
"COMPONENT": assignment_status_counts["COMPONENT"],
"UNKNOWN": assignment_status_counts["UNKNOWN"],
}
summary_expected = {
"MATCH": summary.get("match_visual_objects"),
"PARTIAL": summary.get("partial_visual_objects"),
"AMBIGUOUS": summary.get("ambiguous_visual_objects"),
"COMPONENT": summary.get("component_visual_objects"),
"UNKNOWN": summary.get("unknown_visual_objects"),
}
v.check("assignment_summary_counts_match_rows", recomputed == summary_expected, {
"recomputed": recomputed,
"summary": summary_expected,
})
v.check(
"assignment_policy_preserves_architecture",
assignment.get("policy", {}).get("cm95_source_layer_used_for_assignment") is False
and assignment.get("policy", {}).get("provider_field_semantics_used_for_assignment") is False
and assignment.get("policy", {}).get("anonymous_catalog_modified") is False,
assignment.get("policy"),
)
# 新语义分类器与历史人工结果的冲突只作 warning。
# 这里复现最小规则匹配,只比较高置信(>=0.72)首选 Layer。
high_conflicts: list[dict[str, Any]] = []
cm_name_map = names.get("cm95", {})
rules = semantic_rules.get("rules", [])
for d in decisions.values():
if d.get("status") not in {"MATCH", "PARTIAL"}:
continue
selected = d.get("standard_layers") or []
if len(selected) != 1:
continue
key = d["resource_key_internal"]
info = cm_name_map.get(key, {})
desc = str(info.get("official_description_en") or "")
rtype = str(info.get("resource_type") or "")
top: tuple[str, float] | None = None
for rule in rules:
if rule.get("resource_type") and rule.get("resource_type") != rtype:
continue
if not re.search(str(rule.get("pattern") or ""), desc, flags=re.IGNORECASE):
continue
candidates = rule.get("layer_candidates", [])
if candidates:
top = (str(candidates[0][0]), float(candidates[0][1]))
break
if top and top[1] >= 0.72 and top[0] != selected[0]:
high_conflicts.append({
"anonymous_id": d.get("anonymous_id"),
"resource_key": key,
"display_name_zh": info.get("display_name_zh"),
"manual_layer": selected[0],
"semantic_suggestion": top[0],
"semantic_score": top[1],
})
if high_conflicts:
v.warn("manual_vs_semantic_classifier_conflicts", high_conflicts)
report = {
"schema": "cm95-layer-classification-validation/v1",
"generated_at": datetime.now(timezone.utc).isoformat(),
"ok": v.ok,
"baseline": {
"cm95_style_sha256": cm95.get("style", {}).get("sha256"),
"standard_style_sha256": std.get("style", {}).get("sha256"),
"standard_catalog_status": std.get("catalog_status"),
"unique_cm95_resources": len(resource_keys),
"manual_status_counts": dict(sorted(status_counts.items())),
"final_assignment_status_counts": recomputed,
"final_assignment_total": len(assignments),
},
"checks": v.checks,
"warnings": v.warnings,
}
report_path = Path(args.report)
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print("CM95 → NavSea Layer 分类基线校验")
for row in v.checks:
print(f"[{row['status']}] {row['name']}")
for row in v.warnings:
print(f"[WARN] {row['name']}: {json.dumps(row['detail'], ensure_ascii=False)}")
print(f"report: {report_path}")
print("RESULT:", "PASS" if v.ok else "FAIL")
return 0 if v.ok else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,311 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import math
from collections import defaultdict
from pathlib import Path
from typing import Any
import numpy as np
from PIL import Image, ImageFilter
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def crop_sprite(atlas: Image.Image, meta: dict[str, Any]) -> Image.Image:
crop = atlas.crop((
int(meta["x"]), int(meta["y"]),
int(meta["x"]) + int(meta["width"]),
int(meta["y"]) + int(meta["height"]),
)).convert("RGBA")
alpha = np.asarray(crop.getchannel("A"))
ys, xs = np.where(alpha > 8)
if len(xs):
crop = crop.crop((int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1))
return crop
def resize_center(crop: Image.Image, size: int = 64, pad: int = 5) -> Image.Image:
w, h = crop.size
target = size - pad * 2
scale = min(target / max(w, 1), target / max(h, 1))
nw, nh = max(1, round(w * scale)), max(1, round(h * scale))
crop = crop.resize((nw, nh), Image.Resampling.LANCZOS)
canvas = Image.new("RGBA", (size, size), (255, 255, 255, 0))
canvas.alpha_composite(crop, ((size - nw) // 2, (size - nh) // 2))
return canvas
def dct_matrix(n: int) -> np.ndarray:
mat = np.zeros((n, n), dtype=np.float32)
factor = math.pi / n
for k in range(n):
scale = math.sqrt(1 / n) if k == 0 else math.sqrt(2 / n)
for i in range(n):
mat[k, i] = scale * math.cos((i + 0.5) * k * factor)
return mat
DCT32 = dct_matrix(32)
def phash(gray32: np.ndarray) -> np.ndarray:
coeff = DCT32 @ gray32 @ DCT32.T
low = coeff[:8, :8].copy()
vals = low.ravel()[1:]
med = float(np.median(vals))
return (low.ravel()[1:] > med).astype(np.uint8)
def color_hist(rgb: np.ndarray, alpha: np.ndarray, bins: int = 4) -> np.ndarray:
mask = alpha > 0.08
if not np.any(mask):
return np.zeros(bins ** 3, dtype=np.float32)
pix = np.clip(rgb[mask], 0, 0.999999)
q = (pix * bins).astype(np.int32)
idx = q[:, 0] * bins * bins + q[:, 1] * bins + q[:, 2]
hist = np.bincount(idx, minlength=bins ** 3).astype(np.float32)
s = hist.sum()
return hist / s if s else hist
def descriptor(crop: Image.Image) -> dict[str, Any]:
original_w, original_h = crop.size
canvas = resize_center(crop, 64, 5)
arr = np.asarray(canvas, dtype=np.float32) / 255.0
alpha = arr[:, :, 3]
rgb = arr[:, :, :3]
composited = rgb * alpha[:, :, None] + (1.0 - alpha[:, :, None])
gray_img = Image.fromarray(np.uint8(np.clip(
(0.299 * composited[:, :, 0] + 0.587 * composited[:, :, 1] + 0.114 * composited[:, :, 2]) * 255,
0, 255
)))
gray32 = np.asarray(gray_img.resize((32, 32), Image.Resampling.LANCZOS), dtype=np.float32) / 255.0
edge = np.asarray(gray_img.filter(ImageFilter.FIND_EDGES), dtype=np.float32) / 255.0
mask = alpha > 0.08
xproj = mask.sum(axis=0).astype(np.float32)
yproj = mask.sum(axis=1).astype(np.float32)
if xproj.sum():
xproj /= xproj.sum()
if yproj.sum():
yproj /= yproj.sum()
return {
"alpha": alpha,
"gray": gray32,
"edge": edge,
"phash": phash(gray32),
"hist": color_hist(rgb, alpha),
"xproj": xproj,
"yproj": yproj,
"aspect": original_w / max(1, original_h),
"occupancy": float(mask.mean()),
}
def cosine(a: np.ndarray, b: np.ndarray) -> float:
denom = float(np.linalg.norm(a) * np.linalg.norm(b))
if denom == 0:
return 0.0
return float(np.dot(a.ravel(), b.ravel()) / denom)
def similarity(a: dict[str, Any], b: dict[str, Any]) -> float:
alpha = cosine(a["alpha"], b["alpha"])
edge = cosine(a["edge"], b["edge"])
gray = 1.0 - float(np.mean((a["gray"] - b["gray"]) ** 2))
hamming = float(np.mean(a["phash"] != b["phash"]))
ph = 1.0 - hamming
hist = cosine(a["hist"], b["hist"])
proj = 0.5 * (cosine(a["xproj"], b["xproj"]) + cosine(a["yproj"], b["yproj"]))
ratio = min(a["aspect"], b["aspect"]) / max(a["aspect"], b["aspect"])
occ = 1.0 - min(1.0, abs(a["occupancy"] - b["occupancy"]) / max(a["occupancy"], b["occupancy"], 0.02))
return (
0.24 * alpha +
0.17 * edge +
0.16 * gray +
0.14 * ph +
0.12 * hist +
0.08 * proj +
0.05 * ratio +
0.04 * occ
)
def resource_records(catalog: dict[str, Any], standard: bool) -> list[dict[str, Any]]:
seen: set[tuple[Any, ...]] = set()
out = []
for obj in catalog.get("visual_objects", []):
if not obj.get("resource_key") or not obj.get("resource_meta"):
continue
layer = obj.get("standard_layer_name") if standard else None
key = (
obj.get("component_role"),
obj.get("resource_key"),
layer,
)
if key in seen:
continue
seen.add(key)
out.append({
"resource_key": obj["resource_key"],
"resource_meta": obj["resource_meta"],
"component_role": obj.get("component_role"),
"standard_layer_name": layer,
"visual_object_id": obj.get("visual_object_id"),
})
return out
def build_desc(records: list[dict[str, Any]], atlas: Image.Image) -> list[dict[str, Any]]:
return [descriptor(crop_sprite(atlas, r["resource_meta"])) for r in records]
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--standard", required=True)
ap.add_argument("--cm95", required=True)
ap.add_argument("--output", required=True)
ap.add_argument("--min-score", type=float, default=0.84)
ap.add_argument("--min-margin", type=float, default=0.025)
args = ap.parse_args()
std_path = Path(args.standard)
cm_path = Path(args.cm95)
out = Path(args.output)
out.mkdir(parents=True, exist_ok=True)
std_cat = load_json(std_path)
cm_cat = load_json(cm_path)
std_dir = std_path.parent.parent / "navsea-anonymous"
cm_dir = cm_path.parent
std_atlas = Image.open(std_dir / "_sprite" / "sprite.png").convert("RGBA")
cm_atlas = Image.open(cm_dir / "_sprite" / "sprite.png").convert("RGBA")
std = resource_records(std_cat, True)
cm = resource_records(cm_cat, False)
std_d = build_desc(std, std_atlas)
cm_d = build_desc(cm, cm_atlas)
std_by_role: dict[str, list[int]] = defaultdict(list)
cm_by_role: dict[str, list[int]] = defaultdict(list)
for i, r in enumerate(std):
std_by_role[r["component_role"]].append(i)
for i, r in enumerate(cm):
cm_by_role[r["component_role"]].append(i)
cm_rankings: dict[int, list[tuple[float, int]]] = {}
std_best: dict[int, tuple[float, int]] = {}
for role, cm_indices in cm_by_role.items():
std_indices = std_by_role.get(role, [])
if not std_indices:
continue
for ci in cm_indices:
vals = sorted(
((similarity(cm_d[ci], std_d[si]), si) for si in std_indices),
reverse=True,
)
cm_rankings[ci] = vals[:5]
for si in std_indices:
vals = max(
((similarity(std_d[si], cm_d[ci]), ci) for ci in cm_indices),
default=(0.0, -1),
)
std_best[si] = vals
anchors = []
candidates = []
for ci, ranked in cm_rankings.items():
if not ranked:
continue
best_score, si = ranked[0]
second_score = ranked[1][0] if len(ranked) > 1 else 0.0
mutual = std_best.get(si, (0.0, -1))[1] == ci
margin = best_score - second_score
row = {
"cm95_resource_key": cm[ci]["resource_key"],
"component_role": cm[ci]["component_role"],
"best_standard_resource_key": std[si]["resource_key"],
"best_standard_layer": std[si]["standard_layer_name"],
"score": best_score,
"margin": margin,
"mutual_nearest": mutual,
"standard_visual_object_id": std[si]["visual_object_id"],
"alternatives": [
{
"standard_resource_key": std[sj]["resource_key"],
"standard_layer": std[sj]["standard_layer_name"],
"score": score,
}
for score, sj in ranked[1:4]
],
}
candidates.append(row)
if mutual and best_score >= args.min_score and margin >= args.min_margin:
row = dict(row)
row["status"] = "MATCH"
anchors.append(row)
# Map accepted visual anchors back to CM95 anonymous Visual Object occurrences.
anchor_by_key_role = {
(x["cm95_resource_key"], x["component_role"]): x for x in anchors
}
occurrences = []
for obj in cm_cat.get("visual_objects", []):
k = (obj.get("resource_key"), obj.get("component_role"))
anchor = anchor_by_key_role.get(k)
if not anchor:
continue
occurrences.append({
"visual_object_id": obj["visual_object_id"],
"group_id": obj["group_id"],
"cm95_resource_key": obj["resource_key"],
"assigned_standard_layer": anchor["best_standard_layer"],
"score": anchor["score"],
"basis": "VISUAL_ANCHOR_MUTUAL_NEAREST",
})
summary = {
"standard_visual_resources": len(std),
"cm95_visual_resources": len(cm),
"candidate_pairs": len(candidates),
"accepted_visual_anchors": len(anchors),
"cm95_visual_object_occurrences_named": len(occurrences),
"standard_layers_reached": sorted(set(x["best_standard_layer"] for x in anchors if x.get("best_standard_layer"))),
}
result = {
"schema": "style-visual-anchor-match/v1",
"rules": {
"cm95_source_layer_used": False,
"provider_name_used": False,
"resource_key_text_semantics_used": False,
"standard_layer_is_output_only": True,
"min_score": args.min_score,
"min_margin": args.min_margin,
"requires_mutual_nearest": True,
},
"summary": summary,
"anchors": sorted(anchors, key=lambda x: x["score"], reverse=True),
"cm95_named_occurrences": occurrences,
"top_candidates": sorted(candidates, key=lambda x: x["score"], reverse=True)[:200],
}
(out / "visual-anchor-matches.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
print(json.dumps(summary, ensure_ascii=False, indent=2))
for x in result["anchors"][:80]:
print(
f'{x["score"]:.4f} margin={x["margin"]:.4f} '
f'{x["cm95_resource_key"]} -> {x["best_standard_resource_key"]} '
f'[{x["best_standard_layer"]}]'
)
if __name__ == "__main__":
main()