Build 20nm final release candidate

This commit is contained in:
OpenAI Codex
2026-04-01 09:48:01 +08:00
parent d1477f49ec
commit 36f3a2f2f4
8 changed files with 2943 additions and 36 deletions

View File

@@ -1,6 +1,6 @@
# PBF Step Record
Last Updated: 2026-03-31
Last Updated: 2026-04-01
Repo: `/root/sourceserver/pbf`
Remote: `ssh://git@nas:2222/tei/pbf.git`
@@ -12,13 +12,13 @@ Remote: `ssh://git@nas:2222/tei/pbf.git`
当前页面版本体系:
- `HTML: compare-r10-20260331-2213`
- `HTML: compare-r11-20260401-0115`
当前页面支持 2 个 profile但仍是同一张 HTML
1. 唐津 20 海里
- `Style: karatsu-style-r7-20260331-2045 (style.navsea-delivery-karatsu-10nm.json)`
- `PBF: karatsu-pbf-20nm-20260325-1757 (pbf-delivery-karatsu-20nm)`
- `Style: karatsu-final-style-r1-20260401-0115 (style.navsea-delivery-karatsu-20nm-final.json)`
- `PBF: karatsu-final-pbf-20nm-20260401-0115 (pbf-delivery-karatsu-20nm-final)`
2. 九州
- `Style: kyushu-style-r1-20260331-2205 (style.navsea-delivery-kyushu.json)`
@@ -45,6 +45,7 @@ Remote: `ssh://git@nas:2222/tei/pbf.git`
当前继续收敛的主 style 文件:
- [`src/pbf/style.navsea-delivery-karatsu-10nm.json`](/root/sourceserver/pbf/src/pbf/style.navsea-delivery-karatsu-10nm.json)
- [`src/pbf/style.navsea-delivery-karatsu-20nm-final.json`](/root/sourceserver/pbf/src/pbf/style.navsea-delivery-karatsu-20nm-final.json)
当前九州 delivery style
@@ -64,6 +65,65 @@ Remote: `ssh://git@nas:2222/tei/pbf.git`
- 当前主矛盾是 `style`
- 不是 `PBF 不见了`
## Final Release Candidate
当前已经产出一版 `20nm final release` 候选:
- 输出目录:
- `/home/wwwroot/pbf-delivery-karatsu-20nm-final`
- 当前在线对比页:
- `http://192.168.200.184/newpec/navsea-compare-karatsu-20nm.html`
- 当前候选 style
- [`src/pbf/style.navsea-delivery-karatsu-20nm-final.json`](/root/sourceserver/pbf/src/pbf/style.navsea-delivery-karatsu-20nm-final.json)
当前候选的生成方式:
- 以当前稳定 `/home/wwwroot/pbf-delivery-karatsu-20nm` 为输入
- 使用 protobuf 层属性裁剪脚本生成
- 脚本:
- [`navsea_minimize_release_tiles.py`](/root/sourceserver/pbf/navsea_minimize_release_tiles.py)
当前 final candidate 只保留 `18` 个字段:
- `canonical_object_type`
- `chart_fill_pattern`
- `chart_fill_style`
- `chart_icon_image`
- `chart_label_position_code`
- `chart_label_subtext`
- `chart_label_text`
- `chart_line_color`
- `chart_line_width`
- `chart_symbol_code`
- `chart_text_color`
- `chart_text_style`
- `depth_value_m`
- `least_depth_m`
- `light_color_code`
- `light_sector_mode`
- `name_ja`
- `place_name_en`
当前 final candidate 量化结果:
- feature instances:
- `269883`
- distinct `canonical_object_type`:
- `137`
- 原始 `20nm delivery` 大小:
- `29,527,981` bytes
- final candidate 大小:
- `23,988,432` bytes
- 压缩幅度约:
- `18.8%`
当前判断:
- 这版已经满足“字段显著裁剪”
- 但当前 AOI 图像审计仍显示大差异
- 所以它目前只能算 `final release candidate`
- 还不能直接认定为稳定最终版
## Audit Method
当前认可的可信审计方法:

View File

@@ -0,0 +1,127 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
from mapbox_vector_tile.Mapbox import vector_tile_pb2
FINAL_RELEASE_PROPERTY_ALLOWLIST = frozenset(
{
"canonical_object_type",
"chart_fill_pattern",
"chart_fill_style",
"chart_icon_image",
"chart_label_position_code",
"chart_label_subtext",
"chart_label_text",
"chart_line_color",
"chart_line_width",
"chart_symbol_code",
"chart_text_color",
"chart_text_style",
"depth_value_m",
"least_depth_m",
"light_color_code",
"light_sector_mode",
"name_ja",
"place_name_en",
}
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Create a final release tile set by stripping non-style properties from an existing PBF tree."
)
parser.add_argument("--src-root", type=Path, required=True)
parser.add_argument("--dst-root", type=Path, required=True)
return parser.parse_args()
def clone_value(dst_value: vector_tile_pb2.tile.value, src_value: vector_tile_pb2.tile.value) -> None:
dst_value.CopyFrom(src_value)
def strip_tile_properties(raw_tile: bytes, allowlist: set[str] | frozenset[str]) -> bytes:
src_tile = vector_tile_pb2.tile()
src_tile.ParseFromString(raw_tile)
dst_tile = vector_tile_pb2.tile()
for src_layer in src_tile.layers:
dst_layer = dst_tile.layers.add()
dst_layer.version = src_layer.version
dst_layer.name = src_layer.name
dst_layer.extent = src_layer.extent
kept_key_index: dict[int, int] = {}
kept_value_index: dict[tuple[int, bytes], int] = {}
for src_feature in src_layer.features:
dst_feature = dst_layer.features.add()
if src_feature.HasField("id"):
dst_feature.id = src_feature.id
dst_feature.type = src_feature.type
dst_feature.geometry.extend(src_feature.geometry)
tags = list(src_feature.tags)
for pos in range(0, len(tags), 2):
src_key_idx = tags[pos]
src_val_idx = tags[pos + 1]
key = src_layer.keys[src_key_idx]
if key not in allowlist:
continue
dst_key_idx = kept_key_index.get(src_key_idx)
if dst_key_idx is None:
dst_key_idx = len(dst_layer.keys)
dst_layer.keys.append(key)
kept_key_index[src_key_idx] = dst_key_idx
src_value = src_layer.values[src_val_idx]
value_sig = (src_val_idx, src_value.SerializeToString())
dst_val_idx = kept_value_index.get(value_sig)
if dst_val_idx is None:
dst_val_idx = len(dst_layer.values)
clone_value(dst_layer.values.add(), src_value)
kept_value_index[value_sig] = dst_val_idx
dst_feature.tags.extend([dst_key_idx, dst_val_idx])
return dst_tile.SerializeToString()
def main() -> None:
args = parse_args()
src_root = args.src_root
dst_root = args.dst_root
if not src_root.exists():
raise SystemExit(f"src root not found: {src_root}")
if dst_root.exists():
for path in sorted(dst_root.glob("**/*"), reverse=True):
if path.is_file() or path.is_symlink():
path.unlink()
elif path.is_dir():
try:
path.rmdir()
except OSError:
pass
dst_root.mkdir(parents=True, exist_ok=True)
written = 0
for src_path in sorted(src_root.glob("*/*/*.pbf")):
rel = src_path.relative_to(src_root)
dst_path = dst_root / rel
dst_path.parent.mkdir(parents=True, exist_ok=True)
dst_path.write_bytes(strip_tile_properties(src_path.read_bytes(), FINAL_RELEASE_PROPERTY_ALLOWLIST))
written += 1
print(f"minimized release tiles written={written} dst={dst_root}")
if __name__ == "__main__":
main()

View File

@@ -53,6 +53,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--toolbar-height", type=int, default=150)
parser.add_argument("--bottom-trim", type=int, default=170)
parser.add_argument("--timeout-sec", type=int, default=120)
parser.add_argument("--virtual-time-budget-ms", type=int, default=30000)
parser.add_argument("--hotspots-json", type=Path, default=DEFAULT_HOTSPOTS_PATH)
return parser.parse_args()
@@ -62,11 +63,18 @@ def fetch_compare_version(compare_url: str) -> str | None:
text = requests.get(compare_url, timeout=20).text
except Exception:
return None
match = re.search(r'const\s+COMPARE_VERSION\s*=\s*"([^"]+)"', text)
match = re.search(r'const\s+(?:COMPARE_VERSION|HTML_VERSION)\s*=\s*"([^"]+)"', text)
return match.group(1) if match else None
def run_browser_screenshot(compare_url: str, output_png: Path, width: int, height: int, timeout_sec: int) -> None:
def run_browser_screenshot(
compare_url: str,
output_png: Path,
width: int,
height: int,
timeout_sec: int,
virtual_time_budget_ms: int,
) -> None:
cmd = [
"timeout",
f"{timeout_sec}s",
@@ -75,7 +83,7 @@ def run_browser_screenshot(compare_url: str, output_png: Path, width: int, heigh
"--disable-gpu",
"--enable-unsafe-swiftshader",
"--no-sandbox",
"--virtual-time-budget=15000",
f"--virtual-time-budget={virtual_time_budget_ms}",
f"--window-size={width},{height}",
f"--screenshot={output_png}",
compare_url,
@@ -354,7 +362,14 @@ def main() -> None:
right_png = args.output_dir / "compare_right.png"
diff_png = args.output_dir / "compare_diff.png"
run_browser_screenshot(args.compare_url, full_png, args.width, args.height, args.timeout_sec)
run_browser_screenshot(
args.compare_url,
full_png,
args.width,
args.height,
args.timeout_sec,
args.virtual_time_budget_ms,
)
visual_metrics = save_visual_outputs(
full_png=full_png,
left_png=left_png,

View File

@@ -52,6 +52,29 @@ LIGHT_COLOR_REMARK_MAP = {
"A": "amber",
}
FINAL_RELEASE_PROPERTY_ALLOWLIST = frozenset(
{
"canonical_object_type",
"chart_fill_pattern",
"chart_fill_style",
"chart_icon_image",
"chart_label_position_code",
"chart_label_subtext",
"chart_label_text",
"chart_line_color",
"chart_line_width",
"chart_symbol_code",
"chart_text_color",
"chart_text_style",
"depth_value_m",
"least_depth_m",
"light_color_code",
"light_sector_mode",
"name_ja",
"place_name_en",
}
)
@dataclass(frozen=True)
class DbConfig:
@@ -163,6 +186,18 @@ def parse_number(value: object) -> float | None:
return None
def feature_id_from_current_fid(fid_value: object) -> int | None:
text = text_or_none(fid_value)
if not text:
return None
try:
if re.fullmatch(r"[0-9A-Fa-f]{8}", text):
return int(text, 16)
return int(text)
except ValueError:
return None
def infer_light_color_code_from_remark(light_remark: object) -> str | None:
remark_text = text_or_none(light_remark)
if not remark_text:
@@ -213,8 +248,10 @@ class NavSeaTileBuilder:
zmax: int,
workers: int,
all_tiles: bool,
reference_tile_root: Path | None,
engineering_mode: bool,
strip_legacy_japanese_delivery: bool,
release_minimal: bool,
fid_codec: NavSeaFidCodec | None,
fid_key_id: str | None,
bundle_id: str | None,
@@ -229,8 +266,10 @@ class NavSeaTileBuilder:
self.zmax = zmax
self.workers = workers
self.all_tiles = all_tiles
self.reference_tile_root = reference_tile_root
self.engineering_mode = engineering_mode
self.strip_legacy_japanese_delivery = strip_legacy_japanese_delivery
self.release_minimal = release_minimal
self.fid_codec = fid_codec
self.fid_key_id = fid_key_id
self.bundle_id = bundle_id
@@ -305,6 +344,8 @@ class NavSeaTileBuilder:
return [int(row["z"]) for row in cur.fetchall()]
def build_jobs(self, supported_zooms: list[int]) -> list[TileJob]:
if self.reference_tile_root is not None:
return self.build_reference_jobs(supported_zooms)
if self.all_tiles:
return self.build_all_jobs(supported_zooms)
@@ -335,6 +376,26 @@ class NavSeaTileBuilder:
return jobs
def build_reference_jobs(self, supported_zooms: list[int]) -> list[TileJob]:
jobs: list[TileJob] = []
zoom_set = set(supported_zooms)
assert self.reference_tile_root is not None
for path in sorted(self.reference_tile_root.glob("*/*/*.pbf")):
try:
z = int(path.parent.parent.name)
x = int(path.parent.name)
y = int(path.stem)
except ValueError:
continue
if z not in zoom_set or z < self.zmin or z > self.zmax:
continue
if not (self.source_root / str(z) / str(x) / f"{y}.pbf").exists():
continue
jobs.append(TileJob(z=z, x=x, y=y))
return jobs
def aoi_bbox(self) -> tuple[float, float, float, float]:
radius_km = self.radius_nm * 1.852
lat_delta = radius_km / 111.32
@@ -380,7 +441,6 @@ class NavSeaTileBuilder:
COALESCE(fid.v, '') AS fid,
r.source_layer,
r.geom_type,
COALESCE(NULLIF(r.render_layer, ''), r.source_layer) AS output_layer,
r.canonical_object_type,
r.canonical_family,
r.semantic_key,
@@ -438,27 +498,27 @@ class NavSeaTileBuilder:
)
for source_feature, row in zip(source_features, db_rows):
output_layer = str(row["output_layer"])
source_layer_jp = str(row["source_layer"])
output_layer, source_layer_rule_id = self.mapping_registry.resolve_source_layer(source_layer_jp)
properties = dict(source_feature.get("properties") or {})
legacy_fid_raw = properties.get("fid", row["fid"])
current_feature_id = feature_id_from_current_fid(legacy_fid_raw)
navsea_fid_pair = None
if self.fid_codec is not None:
navsea_fid_pair = self.build_engineering_fid(legacy_fid_raw, row, job)
navsea_fid_int, navsea_fid_hex = navsea_fid_pair
properties["fid"] = navsea_fid_hex
properties["fid_algo_id"] = "feistel32_aes_cyclewalk_v1"
properties["fid_key_id"] = self.fid_key_id
current_feature_id = navsea_fid_int
if self.engineering_mode:
if navsea_fid_pair is None:
raise RuntimeError("engineering mode requires encrypted fid generation")
navsea_fid_int, _ = navsea_fid_pair
properties["fid_algo_id"] = "feistel32_aes_cyclewalk_v1"
properties["fid_key_id"] = self.fid_key_id
properties["fid_legacy_raw"] = legacy_fid_raw
properties["fid_navsea_int"] = navsea_fid_int
properties["source_layer_jp"] = row["source_layer"]
source_layer_std, source_layer_rule_id = self.mapping_registry.resolve_source_layer(
str(row["source_layer"])
)
properties["source_layer_std"] = source_layer_std
properties["source_layer_jp"] = source_layer_jp
properties["source_layer_std"] = output_layer
properties["normalization_bundle_id"] = self.bundle_id
properties["source_layer_rule_id"] = source_layer_rule_id
properties["feature_id"] = row["feature_id"]
@@ -470,7 +530,7 @@ class NavSeaTileBuilder:
properties["render_layer"] = output_layer
chart_properties, feature_unresolved, trace_status = self.build_chart_properties(
properties=properties,
source_layer=str(row["source_layer"]),
source_layer=source_layer_jp,
output_layer=output_layer,
canonical_object_type=text_or_none(row["canonical_object_type"]) or "",
canonical_family=text_or_none(row["canonical_family"]) or "",
@@ -483,13 +543,15 @@ class NavSeaTileBuilder:
properties["trace_status"] = trace_status
properties, field_name_unresolved = self.standardize_output_properties(
properties=properties,
source_layer=str(row["source_layer"]),
source_layer=source_layer_jp,
canonical_object_type=text_or_none(row["canonical_object_type"]) or "",
canonical_family=text_or_none(row["canonical_family"]) or "",
geom_type=str(source_feature.get("geometry", {}).get("type", "")),
feature_id=int(row["feature_id"]),
tile=job,
)
if self.release_minimal and not self.engineering_mode:
properties = self.minimize_delivery_properties(properties)
unresolved.extend(feature_unresolved)
unresolved.extend(field_name_unresolved)
@@ -497,11 +559,13 @@ class NavSeaTileBuilder:
"geometry": source_feature["geometry"],
"properties": properties,
}
if source_feature.get("id") is not None:
if current_feature_id is not None:
output_feature["id"] = current_feature_id
elif source_feature.get("id") is not None:
output_feature["id"] = source_feature["id"]
layer_features[output_layer].append(output_feature)
layer_extents.setdefault(output_layer, source_extents[str(row["source_layer"])])
layer_extents.setdefault(output_layer, source_extents[source_layer_jp])
encoded_layers = []
per_layer_options: dict[str, dict[str, int]] = {}
@@ -779,9 +843,10 @@ class NavSeaTileBuilder:
for key, value in properties.items():
if key == "fid":
# By the time we normalize output properties, fid already means
# the NavSea public ID, not the original source fid field.
normalized[key] = value
if self.engineering_mode:
# Engineering output keeps the public NavSea fid in properties
# so it can be inspected alongside trace metadata.
normalized[key] = value
continue
rule = self.mapping_registry.get_field_name_rule(str(key))
if rule is None:
@@ -820,6 +885,14 @@ class NavSeaTileBuilder:
return normalized, unresolved
@staticmethod
def minimize_delivery_properties(properties: dict[str, object]) -> dict[str, object]:
return {
key: value
for key, value in properties.items()
if key in FINAL_RELEASE_PROPERTY_ALLOWLIST
}
@staticmethod
def make_unresolved_event(
*,
@@ -1376,6 +1449,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--source-root", type=Path, default=SOURCE_TILE_ROOT)
parser.add_argument("--workers", type=int, default=4)
parser.add_argument("--all-tiles", action="store_true")
parser.add_argument("--reference-tile-root", type=Path)
parser.add_argument("--engineering", action="store_true")
parser.add_argument(
"--strip-legacy-japanese-delivery",
@@ -1389,13 +1463,18 @@ def parse_args() -> argparse.Namespace:
dest="strip_legacy_japanese_delivery",
help="keep mapped Japanese structural keys in delivery tiles for legacy compatibility",
)
parser.add_argument(
"--release-minimal",
action="store_true",
help="emit a final release tile payload with only the minimal style-driven delivery fields",
)
parser.add_argument("--fid-key")
parser.add_argument("--fid-key-id", default="navsea-fid-key-v1")
parser.add_argument("--bundle-id", default="navsea-reversible-v1")
parser.set_defaults(strip_legacy_japanese_delivery=True)
args = parser.parse_args()
if not args.all_tiles:
if not args.all_tiles and args.reference_tile_root is None:
missing = [
name
for name in ("center_lat", "center_lon", "radius_nm")
@@ -1405,8 +1484,8 @@ def parse_args() -> argparse.Namespace:
parser.error(
"--center-lat, --center-lon, and --radius-nm are required unless --all-tiles is used"
)
if args.engineering and not args.fid_key:
parser.error("--fid-key is required when --engineering is used")
if not args.fid_key:
parser.error("--fid-key is required for NavSea delivery and engineering builds")
return args
@@ -1425,8 +1504,10 @@ def main() -> None:
zmax=args.zmax,
workers=max(1, args.workers),
all_tiles=args.all_tiles,
reference_tile_root=args.reference_tile_root,
engineering_mode=args.engineering,
strip_legacy_japanese_delivery=args.strip_legacy_japanese_delivery,
release_minimal=args.release_minimal,
fid_codec=fid_codec,
fid_key_id=args.fid_key_id,
bundle_id=args.bundle_id,

View File

@@ -0,0 +1,323 @@
{
"compare_url": "http://192.168.200.184/newpec/navsea-compare-karatsu-20nm.html",
"compare_version": "compare-r11-20260401-0115",
"backend_audit_json": "/root/sourceserver/pbf/NavSea_Original_vs_Delivery_Render_Audit_Karatsu_20nm_2026-03-31.r7.json",
"visual_metrics": {
"image_width": 640,
"image_height": 400,
"total_pixels": 256000,
"changed_pixels": 235857,
"changed_ratio": 0.921316,
"mean_abs_rgb": [
32.7825,
31.9123,
51.3612
],
"rms_rgb": [
56.2597,
51.4488,
69.7204
],
"diff_bbox": [
0,
0,
640,
400
],
"full_crop_left": [
0,
150,
640,
550
],
"full_crop_right": [
640,
150,
1280,
550
],
"hotspots": [
{
"image_width": 109,
"image_height": 64,
"total_pixels": 6976,
"changed_pixels": 6975,
"changed_ratio": 0.999857,
"mean_abs_rgb": [
111.3999,
94.5026,
86.0499
],
"rms_rgb": [
148.6028,
125.1472,
112.7118
],
"diff_bbox": [
0,
0,
109,
64
],
"id": "east_breakwater_marks",
"label": "东侧防波堤灯台区",
"notes": "防波堤灯台和港口标记区。优先验证远一点的灯台/航标是否也能稳定打出来。",
"backend_focus": [
"p航路標識群",
"safety_icon_missing"
],
"pane_box_norm": [
0.83,
0.0,
1.0,
0.16
],
"pane_box_pixels": [
531,
0,
640,
64
],
"left_image": "report/strict_audit_final_20nm/hotspots/east_breakwater_marks_left.png",
"right_image": "report/strict_audit_final_20nm/hotspots/east_breakwater_marks_right.png",
"diff_image": "report/strict_audit_final_20nm/hotspots/east_breakwater_marks_diff.png"
},
{
"image_width": 147,
"image_height": 80,
"total_pixels": 11760,
"changed_pixels": 9533,
"changed_ratio": 0.810629,
"mean_abs_rgb": [
24.2571,
22.39,
27.698
],
"rms_rgb": [
48.344,
45.8837,
50.5034
],
"diff_bbox": [
0,
0,
147,
80
],
"id": "takashima_main_harbor_marks",
"label": "高岛主港航标区",
"notes": "主港口灯塔、红绿标识、近岸障碍标记。优先验证灯塔、小灯、红绿标识是否真正恢复。",
"backend_focus": [
"p航路標識群",
"safety_icon_missing",
"text_missing_in_engineering"
],
"pane_box_norm": [
0.34,
0.05,
0.57,
0.25
],
"pane_box_pixels": [
218,
20,
365,
100
],
"left_image": "report/strict_audit_final_20nm/hotspots/takashima_main_harbor_marks_left.png",
"right_image": "report/strict_audit_final_20nm/hotspots/takashima_main_harbor_marks_right.png",
"diff_image": "report/strict_audit_final_20nm/hotspots/takashima_main_harbor_marks_diff.png"
},
{
"image_width": 115,
"image_height": 68,
"total_pixels": 7820,
"changed_pixels": 5708,
"changed_ratio": 0.729923,
"mean_abs_rgb": [
21.7578,
22.3706,
25.4146
],
"rms_rgb": [
36.6022,
39.835,
43.5517
],
"diff_bbox": [
0,
0,
115,
68
],
"id": "takashima_inner_nearshore_hazards",
"label": "高岛近岸碍航与鱼礁区",
"notes": "近岸鱼礁、碍航点、灯塔与小灯混合区。优先验证鱼礁和碍航物是否还在。",
"backend_focus": [
"p投錨注意障害物",
"p航行危険障害物",
"p航路標識群"
],
"pane_box_norm": [
0.44,
0.14,
0.62,
0.31
],
"pane_box_pixels": [
282,
56,
397,
124
],
"left_image": "report/strict_audit_final_20nm/hotspots/takashima_inner_nearshore_hazards_left.png",
"right_image": "report/strict_audit_final_20nm/hotspots/takashima_inner_nearshore_hazards_right.png",
"diff_image": "report/strict_audit_final_20nm/hotspots/takashima_inner_nearshore_hazards_diff.png"
}
]
},
"backend_summary": {
"original_feature_instances": 258317,
"engineering_feature_instances": 258317,
"result_count": 516634,
"status_counts": {
"extra_in_engineering": 258317,
"missing_in_engineering": 258317
},
"top_annotation_issues": [
{
"issue": "extra_text_in_engineering",
"source_layer": "bathymetry_line",
"count": 24168
},
{
"issue": "text_missing_in_engineering",
"source_layer": "L海底地形",
"count": 24168
},
{
"issue": "extra_text_in_engineering",
"source_layer": "depth_contour",
"count": 11929
},
{
"issue": "text_missing_in_engineering",
"source_layer": "L等深線",
"count": 11929
},
{
"issue": "extra_text_in_engineering",
"source_layer": "seabed_text_point",
"count": 4499
},
{
"issue": "text_missing_in_engineering",
"source_layer": "p底質",
"count": 4499
},
{
"issue": "extra_text_in_engineering",
"source_layer": "navigation_marks",
"count": 1516
},
{
"issue": "text_missing_in_engineering",
"source_layer": "p航路標識群",
"count": 1516
},
{
"issue": "text_missing_in_engineering",
"source_layer": "p地名",
"count": 1444
},
{
"issue": "extra_text_in_engineering",
"source_layer": "place_label_sea",
"count": 1444
}
],
"top_style_semantic_issues": [
{
"issue": "depth_numeric_missing",
"source_layer": "L海底地形",
"count": 24168
},
{
"issue": "depth_numeric_missing",
"source_layer": "L等深線",
"count": 11929
},
{
"issue": "safety_icon_missing",
"source_layer": "p航路標識群",
"count": 1885
},
{
"issue": "depth_numeric_missing",
"source_layer": "L概略等深線",
"count": 350
},
{
"issue": "clearance_numeric_missing",
"source_layer": "p高さ制限",
"count": 33
}
],
"top_source_layer_issues": [
{
"status": "extra_in_engineering",
"source_layer": "baseline_outline",
"count": 87709
},
{
"status": "missing_in_engineering",
"source_layer": "P基本線ククリ",
"count": 87709
},
{
"status": "extra_in_engineering",
"source_layer": "baseline_area",
"count": 50421
},
{
"status": "missing_in_engineering",
"source_layer": "P基本線",
"count": 50421
},
{
"status": "extra_in_engineering",
"source_layer": "depth_contour",
"count": 36460
},
{
"status": "missing_in_engineering",
"source_layer": "L等深線",
"count": 36460
},
{
"status": "extra_in_engineering",
"source_layer": "bathymetry_line",
"count": 35940
},
{
"status": "missing_in_engineering",
"source_layer": "L海底地形",
"count": 35940
},
{
"status": "extra_in_engineering",
"source_layer": "onshore_structure_line",
"count": 10544
},
{
"status": "missing_in_engineering",
"source_layer": "L陸上構造物陸",
"count": 10544
}
]
},
"notes": [
"Visual metrics are browser-rendered screenshot diff metrics from the fixed compare page.",
"Backend metrics are current render-audit summary metrics and may remain blind to sprite-load failures."
]
}

View File

@@ -0,0 +1,104 @@
# NavSea Strict Audit Summary
## Scope
- Compare page: `http://192.168.200.184/newpec/navsea-compare-karatsu-20nm.html`
- Compare version: `compare-r11-20260401-0115`
- Backend render audit: `/root/sourceserver/pbf/NavSea_Original_vs_Delivery_Render_Audit_Karatsu_20nm_2026-03-31.r7.json`
## Visual Audit
- changed pixels: `235857` / `256000`
- changed ratio: `0.921316`
- mean abs rgb: `[32.7825, 31.9123, 51.3612]`
- rms rgb: `[56.2597, 51.4488, 69.7204]`
- left crop: `[0, 150, 640, 550]`
- right crop: `[640, 150, 1280, 550]`
Artifacts:
- `report/strict_audit_final_20nm/compare_full.png`
- `report/strict_audit_final_20nm/compare_left.png`
- `report/strict_audit_final_20nm/compare_right.png`
- `report/strict_audit_final_20nm/compare_diff.png`
## Hotspot AOI Audit
### 东侧防波堤灯台区
- id: `east_breakwater_marks`
- notes: `防波堤灯台和港口标记区。优先验证远一点的灯台/航标是否也能稳定打出来。`
- backend focus: `['p航路標識群', 'safety_icon_missing']`
- changed pixels: `6975` / `6976`
- changed ratio: `0.999857`
- mean abs rgb: `[111.3999, 94.5026, 86.0499]`
- pane box norm: `[0.83, 0.0, 1.0, 0.16]`
- pane box pixels: `[531, 0, 640, 64]`
- left image: `report/strict_audit_final_20nm/hotspots/east_breakwater_marks_left.png`
- right image: `report/strict_audit_final_20nm/hotspots/east_breakwater_marks_right.png`
- diff image: `report/strict_audit_final_20nm/hotspots/east_breakwater_marks_diff.png`
### 高岛主港航标区
- id: `takashima_main_harbor_marks`
- notes: `主港口灯塔、红绿标识、近岸障碍标记。优先验证灯塔、小灯、红绿标识是否真正恢复。`
- backend focus: `['p航路標識群', 'safety_icon_missing', 'text_missing_in_engineering']`
- changed pixels: `9533` / `11760`
- changed ratio: `0.810629`
- mean abs rgb: `[24.2571, 22.39, 27.698]`
- pane box norm: `[0.34, 0.05, 0.57, 0.25]`
- pane box pixels: `[218, 20, 365, 100]`
- left image: `report/strict_audit_final_20nm/hotspots/takashima_main_harbor_marks_left.png`
- right image: `report/strict_audit_final_20nm/hotspots/takashima_main_harbor_marks_right.png`
- diff image: `report/strict_audit_final_20nm/hotspots/takashima_main_harbor_marks_diff.png`
### 高岛近岸碍航与鱼礁区
- id: `takashima_inner_nearshore_hazards`
- notes: `近岸鱼礁、碍航点、灯塔与小灯混合区。优先验证鱼礁和碍航物是否还在。`
- backend focus: `['p投錨注意障害物', 'p航行危険障害物', 'p航路標識群']`
- changed pixels: `5708` / `7820`
- changed ratio: `0.729923`
- mean abs rgb: `[21.7578, 22.3706, 25.4146]`
- pane box norm: `[0.44, 0.14, 0.62, 0.31]`
- pane box pixels: `[282, 56, 397, 124]`
- left image: `report/strict_audit_final_20nm/hotspots/takashima_inner_nearshore_hazards_left.png`
- right image: `report/strict_audit_final_20nm/hotspots/takashima_inner_nearshore_hazards_right.png`
- diff image: `report/strict_audit_final_20nm/hotspots/takashima_inner_nearshore_hazards_diff.png`
## Backend Render Audit
- original feature instances: `258317`
- delivery feature instances: `258317`
- result count: `516634`
Status counts:
- `extra_in_engineering`: `258317`
- `missing_in_engineering`: `258317`
Top annotation issues:
- `extra_text_in_engineering` | `bathymetry_line` | `24168`
- `text_missing_in_engineering` | `L海底地形` | `24168`
- `extra_text_in_engineering` | `depth_contour` | `11929`
- `text_missing_in_engineering` | `L等深線` | `11929`
- `extra_text_in_engineering` | `seabed_text_point` | `4499`
- `text_missing_in_engineering` | `p底質` | `4499`
- `extra_text_in_engineering` | `navigation_marks` | `1516`
- `text_missing_in_engineering` | `p航路標識群` | `1516`
- `text_missing_in_engineering` | `p地名` | `1444`
- `extra_text_in_engineering` | `place_label_sea` | `1444`
Top style semantic issues:
- `depth_numeric_missing` | `L海底地形` | `24168`
- `depth_numeric_missing` | `L等深線` | `11929`
- `safety_icon_missing` | `p航路標識群` | `1885`
- `depth_numeric_missing` | `L概略等深線` | `350`
- `clearance_numeric_missing` | `p高さ制限` | `33`
## Interpretation
- This report intentionally puts browser-rendered visual output and backend render-audit summary in one place.
- If the browser screenshot improves but backend counts do not, the current backend audit is likely blind to a browser/runtime issue such as sprite resolution.

View File

@@ -314,21 +314,21 @@
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<script>
const HTML_VERSION = "compare-r10-20260331-2213";
const HTML_VERSION = "compare-r11-20260401-0115";
const ORIGINAL_STYLE_URL = "./style.json";
const ORIGINAL_TILE_URL = "http://192.168.200.184/newpec/exported_auto/tile.mapple-on.jp__newpec-mvt-20260106__z___x___y_.pbf/tiles/{z}/{x}/{y}.pbf";
const PROFILES = {
karatsu20: {
label: "唐津 20 海里",
pageTitle: "唐津 20 海里对比页",
subtitle: "左侧是原始版 <code>style.json</code>,右侧是当前交付线使用的 delivery 样式 + 唐津 20 海里 delivery PBF。两边联动同步点击任一对象可查看属性适合直接肉眼排差异。",
deliveryPaneLabel: "正式 delivery style + 唐津 20 海里 PBF",
deliveryStyleUrl: "./domain/style.navsea-delivery-karatsu-10nm.json",
deliveryTileUrl: "http://192.168.200.184/pbf-delivery-karatsu-20nm/{z}/{x}/{y}.pbf",
deliveryStyleVersion: "karatsu-style-r7-20260331-2045",
deliveryPbfVersion: "karatsu-pbf-20nm-20260325-1757",
deliveryStyleFile: "style.navsea-delivery-karatsu-10nm.json",
deliveryPbfName: "pbf-delivery-karatsu-20nm",
subtitle: "左侧是原始版 <code>style.json</code>,右侧是当前交付线使用的 final release style + 唐津 20 海里 final PBF。两边联动同步点击任一对象可查看属性适合直接肉眼排差异。",
deliveryPaneLabel: "最终 release style + 唐津 20 海里 final PBF",
deliveryStyleUrl: "./domain/style.navsea-delivery-karatsu-20nm-final.json",
deliveryTileUrl: "http://192.168.200.184/pbf-delivery-karatsu-20nm-final/{z}/{x}/{y}.pbf",
deliveryStyleVersion: "karatsu-final-style-r1-20260401-0115",
deliveryPbfVersion: "karatsu-final-pbf-20nm-20260401-0115",
deliveryStyleFile: "style.navsea-delivery-karatsu-20nm-final.json",
deliveryPbfName: "pbf-delivery-karatsu-20nm-final",
initialView: {
center: [129.9697, 33.4425],
zoom: 11,

File diff suppressed because it is too large Load Diff