#!/usr/bin/env python3 """ BDD100K数据解析与任务生成 (v3 - JSON Label + 更充分利用 Lane/Drivable) 目标 - 解析你本地的 BDD100K(Scalabel 单文件 JSON 标注 + 分目录存储) - 生成 Stage A 可训练的单帧任务样本,并保存到: PROJECT_ROOT/data/dataset/pretrain/train/bdd100k_tasks.pkl 相较 v2 的主要升级 1) Label 可强制为 JSON 字符串(默认开启),便于后续自动评测 / SFT / DPO / reward 计算 2) lane 信息更充分利用:统计 style + direction + continuity + category(BDD100K lane 标注的关键属性) 3) 任务 label 中增加可追溯字段:label_json_path / drivable_id_path(若存在) 4) detection 可选附带 top-k 目标的粗空间位置(left/center/right, top/mid/bottom),默认关闭以控制 token 5) 更稳健的图片路径解析:对每个 split 建立一次图片索引(避免逐样本大量 os.path.exists) 生成 4 类任务(每图最多 4 条): 1) bdd_attributes : weather/scene/timeofday 2) bdd_detection : 交通要素统计摘要(可选 top-k 空间粗定位) 3) bdd_drivable : 可行驶区域 + lane marking 摘要(direct/alternative 比例优先来自 drivable_id mask) 4) bdd_risk : 弱监督粗风险(属性 + 交通密度 + 弱势交通参与者) 运行 - 直接运行(默认 JSON label): python prepare_bdd100k_data.py - 输出自然语言 label(不推荐): python prepare_bdd100k_data.py --label_format text - 采样(调试用): python prepare_bdd100k_data.py --sample_ratio 0.02 - 额外导出 jsonl(体量较大,默认不导出): python prepare_bdd100k_data.py --export_jsonl --jsonl_max_per_split 20000 """ import argparse import json import pickle import random from collections import Counter from pathlib import Path from typing import Dict, List, Optional, Tuple import numpy as np from PIL import Image from tqdm import tqdm # ----------------- 固定随机种子(保证可复现) ----------------- random.seed(42) # ========================= 配置 ========================= BDD_ROOT = Path("PROJECT_ROOT/BDD-100K/bdd100k") OUTPUT_DIR = Path("PROJECT_ROOT/data/dataset/pretrain/train") OUTPUT_DIR.mkdir(parents=True, exist_ok=True) # BDD100K detection 常见 10 类(与你当前生成逻辑一致) DETECTION_CATEGORIES = [ "car", "bus", "truck", "person", "rider", "bike", "motor", "train", "traffic light", "traffic sign" ] # BDD100K keyframe 通常为 720p;这里用于 detection 空间粗定位(不打开图片也能估相对位置) # 若你的 images 实际分辨率不同,可在命令行覆盖(--image_size_w/h) DEFAULT_IMAGE_W = 1280 DEFAULT_IMAGE_H = 720 # detection 的粗空间信息(会增加 token,建议默认 False) DEFAULT_INCLUDE_TOPK_SPATIAL = False DEFAULT_TOPK = 10 # ========================= label 输出控制 ========================= def to_json_str(obj: Dict) -> str: """稳定、紧凑的 JSON 字符串(用于监督信号)""" return json.dumps(obj, ensure_ascii=False, separators=(",", ":"), sort_keys=True) def wrap_label(label_obj: Dict, label_text: str, label_format: str) -> str: """在不改训练器的前提下,把 label 统一塞进字符串字段""" if label_format.lower() == "json": return to_json_str(label_obj) return label_text def wrap_prompt(base: str, schema_hint: str, label_format: str) -> str: """如果 label_format=json,则强制模型输出 JSON,避免自由文本漂移""" if label_format.lower() != "json": return base return ( base + "\nReturn ONLY a single JSON object with the following schema:\n" + schema_hint + "\nDo not add any extra text." ) # ========================= BDD100K标注解析器 ========================= class BDD100KParser: """BDD100K JSON标注解析器(适配 Scalabel 单文件标注 + 分目录存储)""" def __init__(self, bdd_root: Path): self.bdd_root = bdd_root self.images_dir = bdd_root / "images" / "100k" self.labels_dir = bdd_root / "labels" / "100k" self.drivable_dir = bdd_root / "drivable_maps" / "labels" # split -> { filename: path, stem: path } self._image_index: Dict[str, Dict[str, Path]] = {} # ---------- path helpers ---------- @staticmethod def _ensure_jpg(name: str) -> str: if not name: return "" return name if name.lower().endswith(".jpg") else f"{name}.jpg" def build_image_index(self, split: str) -> None: """ 为每个 split 建立一次索引:在 images/100k/{split} 下递归扫 *.jpg 70K 扫描一次开销可接受,换来后续 resolve_image_path 的稳定性与速度。 """ if split in self._image_index: return root = self.images_dir / split idx: Dict[str, Path] = {} if not root.exists(): self._image_index[split] = idx return for p in root.rglob("*.jpg"): idx[p.name] = p idx[p.stem] = p # 允许用不带 .jpg 的 name 查 self._image_index[split] = idx def resolve_image_path(self, split: str, name: str) -> Optional[Path]: """ 优先用索引查;若索引未建则 fallback 两种常见路径: 1) images/100k/{split}/{name}.jpg 2) images/100k/{split}/{stem[:4]}/{name}.jpg """ name_jpg = self._ensure_jpg(name) stem = Path(name_jpg).stem prefix = stem[:4] # index lookup if split in self._image_index: idx = self._image_index[split] if name_jpg in idx: return idx[name_jpg] if stem in idx: return idx[stem] candidates = [ self.images_dir / split / name_jpg, self.images_dir / split / prefix / name_jpg, ] for p in candidates: if p.exists(): return p return None def resolve_drivable_map_path(self, split: str, name: str) -> Optional[Path]: """ drivable_maps/labels/{split}/{prefix}/{stem}_drivable_id.png prefix 通常是 stem 的前4位 """ name_jpg = self._ensure_jpg(name) stem = Path(name_jpg).stem prefix = stem[:4] candidates = [ self.drivable_dir / split / f"{stem}_drivable_id.png", self.drivable_dir / split / prefix / f"{stem}_drivable_id.png", ] for p in candidates: if p.exists(): return p return None # ---------- label loading ---------- def load_labels(self, split: str) -> List[Dict]: """ 递归扫描 labels/100k/{split}/**/*.json 返回每个 json 的 dict(frame) """ label_dir = self.labels_dir / split if not label_dir.exists(): print(f"⚠️ 标注目录不存在: {label_dir}") return [] print(f"加载 {split} 标注: {label_dir}") json_files = sorted(label_dir.rglob("*.json")) print(f" 找到 {len(json_files)} 个标注文件") frames: List[Dict] = [] for json_file in tqdm(json_files, desc=f"Loading {split}"): try: frame = json.loads(json_file.read_text()) # name 可能不带 .jpg if "name" in frame and isinstance(frame["name"], str): frame["name"] = self._ensure_jpg(frame["name"]) else: frame["name"] = self._ensure_jpg(json_file.stem) frame["_label_path"] = str(json_file) # 便于追溯/调试 frames.append(frame) except Exception as e: print(f" ⚠️ 读取失败 {json_file}: {e}") continue print(f" 成功加载 {len(frames)} 个标注") return frames # ---------- parsers ---------- def parse_attributes(self, frame: Dict) -> Dict: attrs = frame.get("attributes", {}) or {} return { "weather": attrs.get("weather", "undefined"), "scene": attrs.get("scene", "undefined"), "timeofday": attrs.get("timeofday", "undefined"), } @staticmethod def _get_objects(frame: Dict) -> List[Dict]: """ Scalabel(BDD100K)常见结构: {"frames":[{"objects":[...]}], "attributes":{...}, "name":...} """ frames = frame.get("frames", []) if isinstance(frames, list) and frames and isinstance(frames[0], dict): objs = frames[0].get("objects", []) if isinstance(objs, list): return objs objs = frame.get("objects", []) return objs if isinstance(objs, list) else [] def parse_detections(self, frame: Dict) -> List[Dict]: detections: List[Dict] = [] for obj in self._get_objects(frame): category = obj.get("category", "") if category not in DETECTION_CATEGORIES: continue box2d = obj.get("box2d") if not box2d: continue detections.append( { "category": category, "box2d": box2d, "attributes": obj.get("attributes", {}) or {}, } ) return detections def parse_drivable_area(self, frame: Dict, split: str) -> Dict: """ 优先用 drivable_id.png 统计 direct/alternative 比例; 若不存在,则根据 poly2d 的 area/* 类别粗略判断。 """ info: Dict = { "has_direct": False, "has_alternative": False, "direct_ratio": None, "alternative_ratio": None, "background_ratio": None, "source": "none", "num_polygons": 0, "drivable_id_path": None, } name = frame.get("name", "") mask_path = self.resolve_drivable_map_path(split, name) if mask_path is not None: try: mask = np.array(Image.open(mask_path)) if mask.ndim == 3: mask = mask[:, :, 0] total = float(mask.size) if total > 0: bg = float(np.sum(mask == 0)) / total direct = float(np.sum(mask == 1)) / total alt = float(np.sum(mask == 2)) / total info.update( { "has_direct": direct > 1e-4, "has_alternative": alt > 1e-4, "direct_ratio": direct, "alternative_ratio": alt, "background_ratio": bg, "source": "drivable_id", "drivable_id_path": str(mask_path), } ) return info except Exception: pass # 失败则 fallback # fallback: poly2d categories for obj in self._get_objects(frame): cat = obj.get("category", "") poly = obj.get("poly2d") if not poly: continue if isinstance(cat, str) and cat.startswith("area/"): info["num_polygons"] += 1 # 兼容不同命名 if "drivable" in cat: info["has_direct"] = True if "alternative" in cat: info["has_alternative"] = True if info["num_polygons"] > 0: info["source"] = "poly2d" return info def parse_lanes(self, frame: Dict) -> Dict: """ lane poly2d 的类别通常为 lane/*。 BDD100K lane 标注(论文)强调三属性:direction、continuity、category(并常带 style/continuity 等字段)。 我们尽可能从 attributes 中抽取:style/direction/continuity,并统计 category(lane/ 后缀)。 """ style = Counter() direction = Counter() continuity = Counter() category = Counter() num_markings = 0 for obj in self._get_objects(frame): cat = obj.get("category", "") poly = obj.get("poly2d") if not poly: continue if not (isinstance(cat, str) and cat.startswith("lane/")): continue num_markings += 1 category[cat.split("/", 1)[1]] += 1 attrs = obj.get("attributes", {}) or {} # style:你的样例里用 attributes.style;部分工具会写 laneStyle v_style = (attrs.get("style") or attrs.get("laneStyle") or "unknown") style[str(v_style).lower()] += 1 # direction:BDD100K lane 标注常用 parallel/perpendicular(论文),工具里也可能用 vertical v_dir = (attrs.get("direction") or attrs.get("laneDirection") or "unknown") direction[str(v_dir).lower()] += 1 # continuity:full/dashed 或类似字段;不同导出可能键名不同 v_cont = (attrs.get("continuity") or attrs.get("laneContinuity") or "unknown") continuity[str(v_cont).lower()] += 1 return { "has_lanes": num_markings > 0, "num_markings": num_markings, "style": dict(style), "direction": dict(direction), "continuity": dict(continuity), "category": dict(category), } # ========================= 任务生成器 ========================= class BDDTaskGenerator: def __init__( self, parser: BDD100KParser, label_format: str = "json", include_topk_spatial: bool = DEFAULT_INCLUDE_TOPK_SPATIAL, topk: int = DEFAULT_TOPK, image_size: Tuple[int, int] = (DEFAULT_IMAGE_W, DEFAULT_IMAGE_H), ): self.parser = parser self.label_format = label_format self.include_topk_spatial = include_topk_spatial self.topk = max(1, int(topk)) self.image_w, self.image_h = int(image_size[0]), int(image_size[1]) # ---------- small helpers ---------- def _common_metadata(self, frame: Dict, name: str) -> Dict: md = { "frame_name": name, "dataset": "bdd100k", "label_json_path": frame.get("_label_path"), } return md def _bbox_to_region(self, box2d: Dict) -> Dict: """ 把 bbox 映射成粗空间区域(不打开图片) - x: left/center/right by bbox center x - y: top/mid/bottom by bbox center y """ x1, y1, x2, y2 = box2d.get("x1", 0), box2d.get("y1", 0), box2d.get("x2", 0), box2d.get("y2", 0) cx = (float(x1) + float(x2)) / 2.0 cy = (float(y1) + float(y2)) / 2.0 rx = cx / max(1.0, float(self.image_w)) ry = cy / max(1.0, float(self.image_h)) if rx < 1/3: x_bin = "left" elif rx < 2/3: x_bin = "center" else: x_bin = "right" if ry < 1/3: y_bin = "top" elif ry < 2/3: y_bin = "middle" else: y_bin = "bottom" area = max(0.0, (float(x2) - float(x1))) * max(0.0, (float(y2) - float(y1))) area_ratio = area / max(1.0, float(self.image_w * self.image_h)) return {"x": x_bin, "y": y_bin, "area_ratio": round(area_ratio, 4)} # ---------- tasks ---------- def generate_task1_attributes(self, frame: Dict, split: str) -> Optional[Dict]: name = frame.get("name", "") image_path = self.parser.resolve_image_path(split, name) if image_path is None: return None attrs = self.parser.parse_attributes(frame) if all(v == "undefined" for v in attrs.values()): return None label_obj = {"weather": attrs["weather"], "scene": attrs["scene"], "timeofday": attrs["timeofday"]} label_text = f"Weather: {attrs['weather']}; Scene: {attrs['scene']}; Time: {attrs['timeofday']}" return { "task": "bdd_attributes", "subtask": "scene_attributes", "image_path": str(image_path), "user_prompt": wrap_prompt( "Describe the driving scene attributes: weather, scene type, and time of day.", '{"weather":"...","scene":"...","timeofday":"..."}', self.label_format, ), "label": wrap_label(label_obj, label_text, self.label_format), "difficulty": "easy", "metadata": {**self._common_metadata(frame, name), **attrs}, } def generate_task2_detection_summary(self, frame: Dict, split: str) -> Optional[Dict]: name = frame.get("name", "") image_path = self.parser.resolve_image_path(split, name) if image_path is None: return None detections = self.parser.parse_detections(frame) if len(detections) == 0: return None category_counts = Counter(d["category"] for d in detections) # 便于人读的 text label(当 label_format=text) parts = [] for cat, count in category_counts.most_common(): if count == 1: parts.append(f"1 {cat}") else: plural = cat + "s" if cat not in ["person", "traffic light", "traffic sign"] else ( "people" if cat == "person" else cat + "s" ) parts.append(f"{count} {plural}") summary = f"There is {parts[0]}." if len(parts) == 1 else f"There are {', '.join(parts[:-1])}, and {parts[-1]}." label_obj: Dict = { "counts": dict(category_counts), "num_objects": len(detections), } if self.include_topk_spatial: # 选面积最大的 top-k(近似“更重要”) det_sorted = sorted( detections, key=lambda d: max(0.0, (float(d["box2d"].get("x2", 0)) - float(d["box2d"].get("x1", 0)))) * max(0.0, (float(d["box2d"].get("y2", 0)) - float(d["box2d"].get("y1", 0)))), reverse=True ) topk = det_sorted[: self.topk] label_obj["topk_objects"] = [ {"category": d["category"], **self._bbox_to_region(d["box2d"])} for d in topk ] return { "task": "bdd_detection", "subtask": "traffic_elements", "image_path": str(image_path), "user_prompt": wrap_prompt( "Summarize the traffic elements in this image (vehicles, pedestrians, traffic lights/signs).", '{"counts":{"car":3,"person":1,...},"num_objects":N' + (', "topk_objects":[{"category":"car","x":"left|center|right","y":"top|middle|bottom","area_ratio":0.0123},...]' if self.include_topk_spatial else "") + "}", self.label_format, ), "label": wrap_label(label_obj, summary, self.label_format), "difficulty": "easy", "metadata": { **self._common_metadata(frame, name), "num_objects": len(detections), "categories": dict(category_counts), }, } def generate_task3_drivable_area(self, frame: Dict, split: str) -> Optional[Dict]: name = frame.get("name", "") image_path = self.parser.resolve_image_path(split, name) if image_path is None: return None drivable_info = self.parser.parse_drivable_area(frame, split) lane_info = self.parser.parse_lanes(frame) # 如果什么都解析不到,就跳过(避免噪声) if ( not drivable_info.get("has_direct") and not drivable_info.get("has_alternative") and not lane_info.get("has_lanes") and drivable_info.get("num_polygons", 0) == 0 and drivable_info.get("source") == "none" ): return None parts = [] if drivable_info.get("source") == "drivable_id" and drivable_info.get("direct_ratio") is not None: d = drivable_info["direct_ratio"] * 100 a = drivable_info["alternative_ratio"] * 100 parts.append(f"Drivable area coverage: direct {d:.1f}%, alternative {a:.1f}%") else: if drivable_info.get("has_direct"): parts.append("Direct drivable path available") if drivable_info.get("has_alternative"): parts.append("Alternative drivable region exists") if not drivable_info.get("has_direct") and not drivable_info.get("has_alternative"): parts.append("Drivable area present") if lane_info.get("has_lanes"): # style 里常见 solid/dashed/full/unknown,尽量人读 s = lane_info.get("style", {}) if s.get("solid", 0) and s.get("dashed", 0): parts.append("Lane markings: mixed solid and dashed") elif s.get("solid", 0): parts.append("Lane markings: solid") elif s.get("dashed", 0): parts.append("Lane markings: dashed") else: parts.append("Lane markings present") label_text = "; ".join(parts) + "." # JSON label:尽量结构化,数值做 round 便于稳定训练 dri = dict(drivable_info) if dri.get("direct_ratio") is not None: dri["direct_ratio"] = round(float(dri["direct_ratio"]), 6) if dri.get("alternative_ratio") is not None: dri["alternative_ratio"] = round(float(dri["alternative_ratio"]), 6) if dri.get("background_ratio") is not None: dri["background_ratio"] = round(float(dri["background_ratio"]), 6) label_obj = {"drivable": dri, "lanes": lane_info} return { "task": "bdd_drivable", "subtask": "drivable_description", "image_path": str(image_path), "user_prompt": wrap_prompt( "Describe the drivable area and lane marking structure in this scene.", '{"drivable":{"source":"drivable_id|poly2d|none","direct_ratio":0.0,"alternative_ratio":0.0,"background_ratio":0.0,' '"has_direct":true,"has_alternative":false,"drivable_id_path":"...|null"},' '"lanes":{"has_lanes":true,"num_markings":N,"style":{...},"direction":{...},"continuity":{...},"category":{...}}}', self.label_format, ), "label": wrap_label(label_obj, label_text, self.label_format), "difficulty": "medium", "metadata": {**self._common_metadata(frame, name), **drivable_info, **lane_info}, } def generate_task4_risk_level(self, frame: Dict, split: str) -> Optional[Dict]: name = frame.get("name", "") image_path = self.parser.resolve_image_path(split, name) if image_path is None: return None attrs = self.parser.parse_attributes(frame) detections = self.parser.parse_detections(frame) risk_score = 0 risk_factors: List[str] = [] # weather weather = attrs.get("weather", "undefined") if weather in ["rainy", "snowy", "foggy"]: risk_score += 2 risk_factors.append(f"{weather} weather") elif weather == "overcast": risk_score += 1 risk_factors.append("overcast conditions") # time timeofday = attrs.get("timeofday", "undefined") if timeofday == "night": risk_score += 2 risk_factors.append("nighttime") elif timeofday in ["dawn/dusk", "dawn", "dusk"]: risk_score += 1 risk_factors.append("low light") # scene scene = attrs.get("scene", "undefined") if scene in ["city street", "residential"]: risk_score += 1 risk_factors.append("urban area") # traffic density num_vehicles = sum(1 for d in detections if d["category"] in ["car", "bus", "truck"]) if num_vehicles >= 10: risk_score += 2 risk_factors.append("high traffic density") elif num_vehicles >= 5: risk_score += 1 risk_factors.append("moderate traffic") # vulnerable users num_vulnerable = sum(1 for d in detections if d["category"] in ["person", "rider", "bike"]) if num_vulnerable > 0: risk_score += 1 risk_factors.append(f"{num_vulnerable} vulnerable road user(s)") if risk_score >= 5: risk_level = "High risk" elif risk_score >= 3: risk_level = "Medium risk" elif risk_score >= 1: risk_level = "Low-medium risk" else: risk_level = "Low risk" label_text = f"{risk_level}: {', '.join(risk_factors)}" if risk_factors else f"{risk_level}: normal conditions" label_obj = { "risk_level": risk_level, "risk_score": int(risk_score), "risk_factors": risk_factors, "evidence": { "weather": attrs.get("weather", "undefined"), "scene": attrs.get("scene", "undefined"), "timeofday": attrs.get("timeofday", "undefined"), "num_vehicles": int(num_vehicles), "num_vulnerable": int(num_vulnerable), "num_objects": int(len(detections)), }, } return { "task": "bdd_risk", "subtask": "risk_assessment", "image_path": str(image_path), "user_prompt": wrap_prompt( "Assess the risk level of this driving scenario based on environmental and traffic conditions.", '{"risk_level":"Low risk|Low-medium risk|Medium risk|High risk","risk_score":N,"risk_factors":["..."],' '"evidence":{"weather":"...","scene":"...","timeofday":"...","num_vehicles":N,"num_vulnerable":N,"num_objects":N}}', self.label_format, ), "label": wrap_label(label_obj, label_text, self.label_format), "difficulty": "medium", "metadata": {**self._common_metadata(frame, name), "risk_score": int(risk_score), "risk_factors": risk_factors, **attrs}, } # ========================= 主流程 ========================= def process_split( parser: BDD100KParser, generator: BDDTaskGenerator, split: str, sample_ratio: float = 1.0, ) -> Dict[str, List[Dict]]: print(f"\n{'='*70}") print(f"处理 {split.upper()} Split") print("=" * 70) parser.build_image_index(split) frames = parser.load_labels(split) if len(frames) == 0: print(f"⚠️ {split} split没有数据") return {"bdd_attributes": [], "bdd_detection": [], "bdd_drivable": [], "bdd_risk": []} if sample_ratio < 1.0: n_sample = max(1, int(len(frames) * sample_ratio)) frames = random.sample(frames, n_sample) print(f" 采样: {len(frames)} / {int(len(frames)/sample_ratio)}") tasks: Dict[str, List[Dict]] = {"bdd_attributes": [], "bdd_detection": [], "bdd_drivable": [], "bdd_risk": []} print("\n生成任务样本...") for frame in tqdm(frames, desc=f"Generating {split}"): s1 = generator.generate_task1_attributes(frame, split) if s1: tasks["bdd_attributes"].append(s1) s2 = generator.generate_task2_detection_summary(frame, split) if s2: tasks["bdd_detection"].append(s2) s3 = generator.generate_task3_drivable_area(frame, split) if s3: tasks["bdd_drivable"].append(s3) s4 = generator.generate_task4_risk_level(frame, split) if s4: tasks["bdd_risk"].append(s4) print(f"\n{split.upper()} 任务统计:") for task_name, samples in tasks.items(): print(f" {task_name}: {len(samples)} 样本") print(f" 总计: {sum(len(v) for v in tasks.values())} 样本") return tasks def export_jsonl(all_data: Dict, out_path: Path, max_per_split: int = 0) -> None: """ 导出 jsonl(便于人工检查/交给其他模型写 code) max_per_split=0 表示全量导出;>0 表示每个 split 只导出最多 N 条(建议调试时用) """ out_path.parent.mkdir(parents=True, exist_ok=True) with out_path.open("w", encoding="utf-8") as f: for split, task_dict in all_data.items(): # 混合导出,保留 task 字段即可筛 samples = [] for arr in task_dict.values(): samples.extend(arr) if max_per_split and len(samples) > max_per_split: samples = random.sample(samples, max_per_split) for s in samples: rec = dict(s) rec["split"] = split f.write(json.dumps(rec, ensure_ascii=False) + "\n") def main(): ap = argparse.ArgumentParser(description="Prepare BDD100K Stage-A tasks") ap.add_argument("--bdd_root", type=str, default=str(BDD_ROOT)) ap.add_argument("--output_dir", type=str, default=str(OUTPUT_DIR)) ap.add_argument("--sample_ratio", type=float, default=1.0) ap.add_argument("--label_format", type=str, default="json", choices=["json", "text"]) ap.add_argument("--include_topk_spatial", action="store_true", help="bdd_detection label 中加入 top-k 粗空间位置(会增加 token)") ap.add_argument("--topk", type=int, default=DEFAULT_TOPK) ap.add_argument("--image_size_w", type=int, default=DEFAULT_IMAGE_W) ap.add_argument("--image_size_h", type=int, default=DEFAULT_IMAGE_H) ap.add_argument("--export_jsonl", action="store_true", help="额外导出 jsonl(体量较大)") ap.add_argument("--jsonl_max_per_split", type=int, default=0, help="jsonl 每个 split 最大导出条数(0=全量)") args = ap.parse_args() bdd_root = Path(args.bdd_root) output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) print("=" * 70) print("BDD100K数据处理 (v3)") print("JSON label + lane/drivable 更充分利用 + 可追溯 metadata") print("=" * 70) parser = BDD100KParser(bdd_root) generator = BDDTaskGenerator( parser, label_format=args.label_format, include_topk_spatial=args.include_topk_spatial, topk=args.topk, image_size=(args.image_size_w, args.image_size_h), ) if not parser.images_dir.exists(): print(f"❌ 图像目录不存在: {parser.images_dir}") return if not parser.labels_dir.exists(): print(f"❌ 标注目录不存在: {parser.labels_dir}") return print(f"✓ BDD根目录: {bdd_root}") print(f"✓ 图像目录: {parser.images_dir}") print(f"✓ 标注目录: {parser.labels_dir}") print(f"✓ Drivable目录: {parser.drivable_dir}") print(f"✓ Label Format: {args.label_format}") print(f"✓ include_topk_spatial: {bool(args.include_topk_spatial)} (topk={args.topk})") print(f"✓ image_size (for spatial binning): {args.image_size_w}x{args.image_size_h}") print(f"✓ sample_ratio: {args.sample_ratio}") all_data: Dict[str, Dict[str, List[Dict]]] = {} for split in ["train", "val", "test"]: all_data[split] = process_split(parser, generator, split, sample_ratio=args.sample_ratio) # 保存 pkl print("\n" + "=" * 70) print("保存数据...") output_file = output_dir / "bdd100k_tasks.pkl" with output_file.open("wb") as f: pickle.dump(all_data, f) print(f"✓ 保存到: {output_file}") # summary json summary: Dict[str, Dict] = {} for split in ["train", "val", "test"]: summary[split] = {k: len(v) for k, v in all_data[split].items()} summary[split]["total"] = sum(summary[split].values()) summary_file = output_dir / "bdd100k_summary.json" summary_file.write_text(json.dumps(summary, ensure_ascii=False, indent=2)) print(f"✓ 统计: {summary_file}") # 可选导出 jsonl if args.export_jsonl: jsonl_path = output_dir / "bdd100k_tasks.jsonl" print(f"导出 JSONL 到: {jsonl_path} (max_per_split={args.jsonl_max_per_split})") export_jsonl(all_data, jsonl_path, max_per_split=args.jsonl_max_per_split) print("✓ JSONL 导出完成") print("\n" + "=" * 70) print("数据处理完成 - 统计:") print("=" * 70) for split in ["train", "val", "test"]: print(f"\n{split.upper()}:") for task_name in ["bdd_attributes", "bdd_detection", "bdd_drivable", "bdd_risk"]: print(f" {task_name}: {summary[split].get(task_name, 0)}") print(" ─────────────────") print(f" 总计: {summary[split]['total']}") print("\n✅ 完成!") print("\n下一步:") print("1. 运行 python prepare_stage_a_data.py 整合Stage A数据") print("2. 运行 python train_two_stage.py --stage A 开始Stage A训练") if __name__ == "__main__": main()