Загрузка данных


#!/usr/bin/env python3
"""Restore the first Camera 08 YOLO labels from the companion Pastebin TXT.

Usage:
    python3 restore_carried_box_labels.py /path/carried_boxes_parts1-2_labels.txt
    python3 restore_carried_box_labels.py /path/labels.txt --output /path/to/labels

By default, creates a new "labels" directory beside the input TXT.
"""

import argparse
import hashlib
import json
import math
import os
import shutil
import tempfile
from pathlib import Path


FIRST_FRAME = 1
LAST_FRAME = 1536
FORMAT = "carried_box_yolo_labels_v1"


def decode_file(input_path):
    lines = [line for line in input_path.read_text(encoding="utf-8-sig").splitlines() if line.strip()]
    if not lines:
        raise ValueError("Input TXT is empty")
    header = json.loads(lines[0])
    if not isinstance(header, dict) or header.get("format") != FORMAT:
        raise ValueError("Wrong file format: use the raw labels TXT, not a Pastebin HTML page")
    if (header.get("first_frame"), header.get("last_frame")) != (FIRST_FRAME, LAST_FRAME):
        raise ValueError("This script expects frames 1 through 1536")
    if len(lines) != LAST_FRAME - FIRST_FRAME + 2:
        raise ValueError(f"Expected 1536 frame records, found {len(lines) - 1}")

    files = []
    digest = hashlib.sha256()
    positives = boxes = 0
    epsilon = 1e-7  # YOLO values rounded to 8 decimals can exceed an edge by a few nanounits
    for expected_frame, line in enumerate(lines[1:], FIRST_FRAME):
        record = json.loads(line)
        if not isinstance(record, dict) or record.get("frame") != expected_frame:
            raise ValueError(f"Missing, duplicate or out-of-order frame near {expected_frame}")
        label_lines = record.get("labels")
        if not isinstance(label_lines, list) or any(not isinstance(item, str) for item in label_lines):
            raise ValueError(f"Invalid labels for frame {expected_frame}")
        for label in label_lines:
            fields = label.split()
            if len(fields) != 5 or fields[0] != "0":
                raise ValueError(f"Invalid YOLO line at frame {expected_frame}: {label!r}")
            cx, cy, width, height = map(float, fields[1:])
            if not all(math.isfinite(value) for value in (cx, cy, width, height)):
                raise ValueError(f"Non-finite coordinate at frame {expected_frame}")
            if not (
                0 < width <= 1
                and 0 < height <= 1
                and -epsilon <= cx - width / 2 < cx + width / 2 <= 1 + epsilon
                and -epsilon <= cy - height / 2 < cy + height / 2 <= 1 + epsilon
            ):
                raise ValueError(f"Out-of-range box at frame {expected_frame}")
            boxes += 1
        if label_lines:
            positives += 1
        filename = f"frame_{expected_frame:06d}.txt"
        content = ("\n".join(label_lines) + ("\n" if label_lines else "")).encode("utf-8")
        digest.update(filename.encode("ascii") + b"\0" + content + b"\0")
        files.append((filename, content))

    expected = (header.get("files"), header.get("positives"), header.get("boxes"))
    actual = (len(files), positives, boxes)
    if expected != actual:
        raise ValueError(f"Count mismatch: expected {expected}, found {actual}")
    if digest.hexdigest() != header.get("sha256"):
        raise ValueError("SHA-256 mismatch: the Pastebin text changed or is incomplete")
    return files, positives, boxes, digest.hexdigest()


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("input", type=Path, help="Raw TXT downloaded or copied from Pastebin")
    parser.add_argument("--output", type=Path, help="Output labels folder (default: beside TXT)")
    args = parser.parse_args()
    input_path = args.input.expanduser().resolve()
    output = (args.output or input_path.parent / "labels").expanduser().resolve()
    if output.exists():
        raise FileExistsError(f"Output already exists; refusing to overwrite: {output}")
    files, positives, boxes, checksum = decode_file(input_path)

    output.parent.mkdir(parents=True, exist_ok=True)
    temporary = Path(tempfile.mkdtemp(prefix=".labels-restore-", dir=output.parent))
    try:
        for filename, content in files:
            (temporary / filename).write_bytes(content)
        if len(list(temporary.glob("*.txt"))) != len(files):
            raise ValueError("File count changed during write")
        os.replace(temporary, output)
    finally:
        if temporary.exists():
            shutil.rmtree(temporary)
    print(f"Restored {len(files)} labels to {output}")
    print(f"Positive: {positives}; empty: {len(files) - positives}; boxes: {boxes}")
    print(f"SHA-256: {checksum}")


if __name__ == "__main__":
    main()