diff --git a/STEP_RECORD.md b/STEP_RECORD.md index 96bd229..cf4d873 100644 --- a/STEP_RECORD.md +++ b/STEP_RECORD.md @@ -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 当前认可的可信审计方法: diff --git a/navsea_minimize_release_tiles.py b/navsea_minimize_release_tiles.py new file mode 100644 index 0000000..b629dd6 --- /dev/null +++ b/navsea_minimize_release_tiles.py @@ -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() diff --git a/navsea_strict_audit.py b/navsea_strict_audit.py index 448ea15..017f621 100644 --- a/navsea_strict_audit.py +++ b/navsea_strict_audit.py @@ -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, diff --git a/navsea_tile_builder.py b/navsea_tile_builder.py index 648cdab..84db77b 100644 --- a/navsea_tile_builder.py +++ b/navsea_tile_builder.py @@ -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, diff --git a/report/strict_audit_final_20nm/strict_audit_summary.json b/report/strict_audit_final_20nm/strict_audit_summary.json new file mode 100644 index 0000000..0114ff9 --- /dev/null +++ b/report/strict_audit_final_20nm/strict_audit_summary.json @@ -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." + ] +} \ No newline at end of file diff --git a/report/strict_audit_final_20nm/strict_audit_summary.md b/report/strict_audit_final_20nm/strict_audit_summary.md new file mode 100644 index 0000000..869de4b --- /dev/null +++ b/report/strict_audit_final_20nm/strict_audit_summary.md @@ -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. diff --git a/src/pbf/navsea-compare-karatsu-20nm.html b/src/pbf/navsea-compare-karatsu-20nm.html index f893314..050a560 100644 --- a/src/pbf/navsea-compare-karatsu-20nm.html +++ b/src/pbf/navsea-compare-karatsu-20nm.html @@ -314,21 +314,21 @@