| import argparse |
| import json |
| from pathlib import Path |
|
|
|
|
| TASKS = ("semantics", "interactable", "interaction") |
|
|
|
|
| def extract_image_id(image_name): |
| stem = Path(image_name).stem |
| parts = stem.split("_") |
| if len(parts) != 2: |
| raise ValueError(f"Cannot infer image_id from image name: {image_name}") |
| app_id, frame_id = parts |
| return int(f"{app_id}{int(frame_id):03d}") |
|
|
|
|
| def load_questions(path): |
| if path is None: |
| return {} |
|
|
| mapping = {} |
| with Path(path).open() as file: |
| for line_number, line in enumerate(file, start=1): |
| line = line.strip() |
| if not line: |
| continue |
| question = json.loads(line) |
| try: |
| question_id = str(question["question_id"]) |
| image_id = question.get("image_id") |
| if image_id is None: |
| image_id = extract_image_id(question["image"]) |
| except KeyError as exc: |
| raise ValueError( |
| f"Question file {path} line {line_number} is missing {exc.args[0]!r}" |
| ) from exc |
| mapping[question_id] = image_id |
| return mapping |
|
|
|
|
| def normalize_bbox(item): |
| bbox = item.get("bbox", item.get("bbox_pixels")) |
| if bbox is None: |
| raise ValueError(f"Prediction is missing bbox/bbox_pixels: {item}") |
| if len(bbox) != 4: |
| raise ValueError(f"Prediction bbox must have four values: {item}") |
| return bbox |
|
|
|
|
| def normalize_score(item): |
| return item.get("score", item.get("probability", 1.0)) |
|
|
|
|
| def normalize_category(category, item, task): |
| if task == "interactable": |
| return 1 |
|
|
| category_id = item.get("category_id", category) |
| if category_id is None: |
| raise ValueError(f"Prediction is missing category/category_id for {task}: {item}") |
| return category_id |
|
|
|
|
| def normalize_image_id(question_id, content, item, questions): |
| for source in (item, content): |
| if isinstance(source, dict) and "image_id" in source: |
| return source["image_id"] |
| if isinstance(source, dict) and "image" in source: |
| return extract_image_id(source["image"]) |
|
|
| if question_id is not None and str(question_id) in questions: |
| return questions[str(question_id)] |
|
|
| raise ValueError( |
| "Prediction is missing image_id/image. Provide --questions for old " |
| f"question-keyed prediction files. question_id={question_id!r}" |
| ) |
|
|
|
|
| def iter_old_format(data): |
| for question_id, content in data.items(): |
| if not isinstance(content, dict): |
| raise ValueError(f"Prediction for question {question_id!r} must be an object") |
|
|
| results = content.get("oovd_result") |
| if results is None: |
| continue |
| if not isinstance(results, dict): |
| raise ValueError(f"oovd_result for question {question_id!r} must be an object") |
|
|
| for category, objects in results.items(): |
| if not isinstance(objects, list): |
| raise ValueError( |
| f"oovd_result[{category!r}] for question {question_id!r} must be a list" |
| ) |
| for item in objects: |
| if not isinstance(item, dict): |
| raise ValueError(f"Prediction item must be an object: {item!r}") |
| yield question_id, content, category, item |
|
|
|
|
| def iter_prediction_items(data): |
| if isinstance(data, dict): |
| yield from iter_old_format(data) |
| return |
|
|
| if not isinstance(data, list): |
| raise ValueError("Prediction input must be a list or a question-keyed object") |
|
|
| for item in data: |
| if not isinstance(item, dict): |
| raise ValueError(f"Prediction item must be an object: {item!r}") |
| yield None, {}, item.get("category_id"), item |
|
|
|
|
| def convert_predictions(input_path, task, questions_path=None): |
| with Path(input_path).open() as file: |
| data = json.load(file) |
|
|
| questions = load_questions(questions_path) |
| output = [] |
| for question_id, content, category, item in iter_prediction_items(data): |
| output.append( |
| { |
| "image_id": normalize_image_id(question_id, content, item, questions), |
| "category_id": normalize_category(category, item, task), |
| "bbox": normalize_bbox(item), |
| "score": normalize_score(item), |
| } |
| ) |
| return output |
|
|
|
|
| def output_path_for_task(output_path, task): |
| path = Path(output_path) |
| if path.suffix: |
| return path.with_name(f"{path.stem}_{task}{path.suffix}") |
| return path / f"{task}.json" |
|
|
|
|
| def write_json(path, data): |
| path = Path(path) |
| path.parent.mkdir(parents=True, exist_ok=True) |
| with path.open("w") as file: |
| json.dump(data, file, indent=2) |
| file.write("\n") |
|
|
|
|
| def build_parser(): |
| parser = argparse.ArgumentParser( |
| description="Convert Orienter prediction files to COCO-style result JSON." |
| ) |
| parser.add_argument( |
| "--task", |
| choices=TASKS + ("all",), |
| required=True, |
| help="Evaluation task to convert for.", |
| ) |
| parser.add_argument("--input", required=True, help="Prediction JSON path.") |
| parser.add_argument( |
| "--questions", |
| help="Question JSONL path. Required only for old question-keyed inputs without image_id.", |
| ) |
| parser.add_argument("--output", required=True, help="Output JSON path or directory.") |
| return parser |
|
|
|
|
| def main(argv=None): |
| args = build_parser().parse_args(argv) |
| tasks = TASKS if args.task == "all" else (args.task,) |
|
|
| for task in tasks: |
| converted = convert_predictions(args.input, task, args.questions) |
| output_path = ( |
| output_path_for_task(args.output, task) if args.task == "all" else Path(args.output) |
| ) |
| write_json(output_path, converted) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|