CoolFace
Datasetpublic

ULM-DS-Lab/Sawit-Weight

Sawit-Weight Two-view field photographs of oil palm fresh fruit bunches (FFB, tandan buah segar), each paired with a bounding box and the ground-truth weight measured on a scale at the collection point. The dataset targets vision-based weight estimation and bunch detection for smallholder and estate harvest logistics. Ringkasan: 31 tandan buah segar kelapa sawit varietas TANERA dari blok 303, difoto dari dua sisi dan ditimbang langsung di lapangan. Setiap gambar disertai kotak… See the full description on the dataset page: https://huggingface.co/datasets/ULM-DS-Lab/Sawit-Weight.

sourceHugging Facecc-by-nc-4.0updated 6d agoView on Hugging Face
0likes179downloads
verify_dataset.py187 linesDownload Raw Back to scripts
1"""Verify the Sawit-Weight dataset package.2 3Two layers are checked. The as-captured layer under annotations/raw/ must still match4every SHA-256 its manifests recorded. The corrected layer (annotations/labels/,5annotations/trees.jsonl, data/train/metadata.jsonl) must be internally consistent and6must differ from the raw layer only where a correction is declared below.7 8    python scripts/verify_dataset.py [dataset_root]9"""10 11import hashlib12import json13import os14import sys15 16TOL = 2e-3  # bbox_pixel and bbox_yolo agree to within this fraction of the image side17 18# Corrections applied on top of the as-captured annotations, mirrored from the README.19ADDED_BOXES = {("TANERA_303_BW_0004", 1)}          # (tree, side_index)20FRACTION_OVERRIDES = {"TANERA_303_BW_0003", "TANERA_303_BW_0004", "TANERA_303_BW_0005"}21 22 23def sha256(path):24    h = hashlib.sha256()25    with open(path, "rb") as f:26        for chunk in iter(lambda: f.read(1 << 20), b""):27            h.update(chunk)28    return h.hexdigest()29 30 31def read_json(path):32    with open(path, encoding="utf-8") as f:33        return json.load(f)34 35 36def read_jsonl(path):37    with open(path, encoding="utf-8") as f:38        return [json.loads(line) for line in f if line.strip()]39 40 41def read_label(path):42    with open(path, encoding="utf-8") as f:43        return [line.split() for line in f.read().splitlines() if line.strip()]44 45 46def main(root):47    images = os.path.join(root, "data", "train")48    ann = os.path.join(root, "annotations")49    raw = os.path.join(ann, "raw")50    fail = []51 52    def check(ok, message):53        if not ok:54            fail.append(message)55 56    trees = sorted(f[:-5] for f in os.listdir(os.path.join(raw, "trees")) if f.endswith(".json"))57    check(len(trees) > 0, "no tree JSON found")58 59    seen_images, seen_labels, digests = set(), set(), {}60 61    for name in trees:62        tree = read_json(os.path.join(raw, "trees", f"{name}.json"))63        manifest = read_json(os.path.join(raw, "manifests", f"{name}.json"))64        device = os.path.join(raw, "device_metadata", f"{name}.json")65 66        check(tree["tree_name"] == name, f"{name}: tree_name does not match filename")67 68        # Layer 1: the as-captured artifacts still hash to what the device recorded.69        for side in manifest["sides"]:70            rgb = os.path.join(images, side["rgb"]["file"])71            raw_label = os.path.join(raw, "labels", side["label"]["file"])72            check(os.path.exists(rgb), f"{name}: missing image {side['rgb']['file']}")73            check(os.path.exists(raw_label), f"{name}: missing raw label {side['label']['file']}")74            if os.path.exists(rgb):75                digest = sha256(rgb)76                check(digest == side["rgb"]["sha256"], f"{name}: SHA-256 mismatch {side['rgb']['file']}")77                check(digest not in digests, f"{name}: duplicate image content, also in {digests.get(digest)}")78                digests[digest] = side["rgb"]["file"]79                seen_images.add(side["rgb"]["file"])80            if os.path.exists(raw_label):81                check(sha256(raw_label) == side["label"]["sha256"],82                      f"{name}: SHA-256 mismatch {side['label']['file']}")83                seen_labels.add(side["label"]["file"])84 85        check(sha256(os.path.join(raw, "trees", f"{name}.json")) == manifest["outputJson"]["sha256"],86              f"{name}: SHA-256 mismatch on tree JSON")87        check(sha256(device) == manifest["metadata"]["sha256"], f"{name}: SHA-256 mismatch on device metadata")88 89        # Layer 2: the corrected labels are the raw ones plus only the declared additions.90        for key, side in tree["images"].items():91            raw_rows = read_label(os.path.join(raw, "labels", side["label_file"]))92            fixed_rows = read_label(os.path.join(ann, "labels", side["label_file"]))93            expected = side["bbox_count"] + (1 if (name, side["side_index"]) in ADDED_BOXES else 0)94            check(len(raw_rows) == side["bbox_count"],95                  f"{name}/{key}: raw label has {len(raw_rows)} rows, bbox_count is {side['bbox_count']}")96            check(len(fixed_rows) == expected,97                  f"{name}/{key}: corrected label has {len(fixed_rows)} rows, expected {expected}")98 99            for parts in fixed_rows:100                check(parts[0] == "0", f"{name}/{key}: class id is not 0")101                check(all(0.0 <= float(v) <= 1.0 for v in parts[1:]),102                      f"{name}/{key}: bbox outside [0, 1]")103            for i, parts in enumerate(raw_rows):104                check([round(float(v), 5) for v in parts[1:]] == [round(float(v), 5) for v in fixed_rows[i][1:]],105                      f"{name}/{key}: corrected label altered the original box {i}")106 107    # Capture set coverage.108    capture_set = read_json(os.path.join(raw, "capture_set.json"))109    check({t["treeName"] for t in capture_set["trees"]} == set(trees),110          "capture_set.json does not list exactly the trees present")111 112    # No orphan files.113    on_disk_images = {f for f in os.listdir(images) if f.lower().endswith(".jpg")}114    on_disk_raw = {f for f in os.listdir(os.path.join(raw, "labels")) if f.endswith(".txt")}115    on_disk_fixed = {f for f in os.listdir(os.path.join(ann, "labels")) if f.endswith(".txt")}116    check(on_disk_images == seen_images, f"orphan images: {sorted(on_disk_images ^ seen_images)}")117    check(on_disk_raw == seen_labels, f"orphan raw labels: {sorted(on_disk_raw ^ seen_labels)}")118    check(on_disk_fixed == seen_labels, f"orphan corrected labels: {sorted(on_disk_fixed ^ seen_labels)}")119 120    # metadata.jsonl agrees with the corrected labels and with itself.121    rows = read_jsonl(os.path.join(images, "metadata.jsonl"))122    check({r["file_name"] for r in rows} == seen_images, "metadata.jsonl does not cover every image")123    check(len(rows) == len(seen_images), "metadata.jsonl has duplicate file_name rows")124 125    for row in rows:126        objects = row["objects"]127        lengths = {len(v) for v in objects.values()}128        check(lengths == {row["num_bunches"]}, f"{row['file_name']}: objects lists disagree with num_bunches")129 130        label_rows = read_label(os.path.join(ann, "labels", row["file_name"][:-4] + ".txt"))131        check(len(label_rows) == row["num_bunches"],132              f"{row['file_name']}: label row count disagrees with num_bunches")133        for i, parts in enumerate(label_rows):134            check([round(float(v), 6) for v in parts[1:]] == [round(v, 6) for v in objects["bbox_yolo"][i]],135                  f"{row['file_name']}: label row {i} disagrees with bbox_yolo")136 137        w, h = row["width"], row["height"]138        for i, (cx, cy, bw, bh) in enumerate(objects["bbox_yolo"]):139            x1, y1, x2, y2 = objects["bbox_pixel"][i]140            check(x1 < x2 and y1 < y2, f"{row['file_name']}: box {i} has no positive area")141            check(abs((x1 + x2) / 2 / w - cx) < TOL and abs((y1 + y2) / 2 / h - cy) < TOL142                  and abs((x2 - x1) / w - bw) < TOL and abs((y2 - y1) / h - bh) < TOL,143                  f"{row['file_name']}: box {i} pixel and yolo forms disagree")144 145        for i, frac in enumerate(objects["ripeness_fraction"]):146            source = objects["ripeness_fraction_source"][i]147            check(frac in (1, 2, 3, 4, 5), f"{row['file_name']}: ripeness_fraction {frac} outside 1-5")148            check(source in ("field_note", "visual_estimate"),149                  f"{row['file_name']}: unknown ripeness_fraction_source {source}")150            check(source == "field_note" or row["tree_name"] in FRACTION_OVERRIDES,151                  f"{row['file_name']}: undeclared ripeness override")152        for i, source in enumerate(objects["bbox_source"]):153            check(source in ("field", "visual_estimate"), f"{row['file_name']}: unknown bbox_source {source}")154            check(source == "field" or (row["tree_name"], row["side_index"]) in ADDED_BOXES,155                  f"{row['file_name']}: undeclared added box")156        for note in objects["notes"]:157            check(note is None or "FAKSI" not in note.upper().replace("FRAKSI", ""),158                  f"{row['file_name']}: FAKSI left unnormalised in notes")159 160    tree_rows = read_jsonl(os.path.join(ann, "trees.jsonl"))161    check({r["tree_name"] for r in tree_rows} == set(trees), "trees.jsonl does not cover every tree")162    boxes_by_tree = {}163    for row in rows:164        boxes_by_tree[row["tree_name"]] = boxes_by_tree.get(row["tree_name"], 0) + row["num_bunches"]165    for row in tree_rows:166        check(row["total_detections"] == boxes_by_tree[row["tree_name"]],167              f"{row['tree_name']}: total_detections disagrees with metadata.jsonl")168        check(len(row["bunches"]) == row["total_unique_bunches"],169              f"{row['tree_name']}: bunch count disagrees with total_unique_bunches")170        for bunch in row["bunches"]:171            check(len(bunch["appearances"]) == bunch["appearance_count"],172                  f"{row['tree_name']}: appearance_count disagrees with appearances")173 174    print(f"trees {len(trees)} | images {len(seen_images)} | labels {len(seen_labels)} | "175          f"boxes {sum(r['num_bunches'] for r in rows)}")176    if fail:177        print(f"FAIL: {len(fail)} problem(s)")178        for message in fail:179            print(f"  - {message}")180        return 1181    print("PASS: raw SHA-256 digests hold and the corrected layer is consistent")182    return 0183 184 185if __name__ == "__main__":186    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))187