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
Dataset Card

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 pembatas format YOLO serta berat, tinggi, lingkar, fraksi kematangan, dan berat brondolan.

Contents

QuantityValue
Trees (capture units)31
Images62 (2 sides per tree)
Annotated bunches31
Bounding boxes62
Image resolution2448 x 3264 (portrait)
VarietyTANERA
Block303
Capture date2026-09-14
Total size~359 MB

Every bunch is photographed twice, from side 1 and side 2. The two boxes of the same bunch are linked through bunch_id, so a model can be trained on single views or on paired views.

Loading

python
from datasets import load_dataset

ds = load_dataset("ULM-DS-Lab/Sawit-Weight", split="train")
sample = ds[0]
sample["image"]                       # PIL.Image
sample["objects"]["weight_kg"]        # [16.928]
sample["objects"]["bbox_yolo"]        # [[cx, cy, w, h]] normalised

Per-tree records, which group both sides of a bunch together, are in annotations/trees.jsonl:

python
import json

trees = [json.loads(line) for line in open("annotations/trees.jsonl", encoding="utf-8")]

Layout

data/train/                    62 JPEG images + metadata.jsonl (one row per image)
annotations/labels/            YOLO .txt labels, one per image (62 files)
annotations/trees.jsonl        one row per tree, both sides grouped by bunch_id
annotations/classes.txt        class names, one per line
annotations/data.yaml          Ultralytics YOLO configuration
annotations/raw/               the as-captured record, untouched by the corrections below
annotations/raw/trees/         original per-tree capture JSON (31 files)
annotations/raw/labels/        original YOLO labels (62 files)
annotations/raw/manifests/     SHA-256 manifest per tree (31 files)
annotations/raw/device_metadata/  capture device and GPS record per tree (31 files)
annotations/raw/capture_set.json  session descriptor and per-tree content digests
scripts/verify_dataset.py      re-checks every SHA-256 and every annotation invariant

Fields

data/train/metadata.jsonl — one row per image

FieldTypeDescription
file_namestringImage path relative to data/train/
tree_namestringCapture unit identifier, e.g. TANERA_303_BW_0001
tree_indexintSequential tree number within the session
side_indexint0 = side 1, 1 = side 2
width, heightintPixel dimensions
varietystringPalm variety
blockstringPlantation block
capture_datestringYYYY-MM-DD
captured_atstringISO 8601 timestamp of the capture
session_id, capture_set_id, device_tokenstringSession provenance
capture_originstringPHONE_CAMERA
rgb_sha256stringSHA-256 of the JPEG bytes
num_bunchesintNumber of boxes in this image
gps_statusstringLOCATION_OFF, FRESH, or STALE
gps_source, gps_providerstringCURRENT_FIX/LAST_KNOWN, gps/network
gps_lat, gps_lng, gps_accuracy_mfloatNull when GPS was off
objectsstruct of listsPer-box annotations, described below

objects — parallel lists, one entry per box

FieldTypeDescription
bbox_yololist[float[4]][cx, cy, w, h], normalised to [0, 1]
bbox_pixellist[int[4]][x1, y1, x2, y2] in pixels
bunch_idlist[int]Links the same bunch across both sides of a tree
weight_kglist[float]Scale-measured bunch weight
height_cmlist[int]Bunch height
circumference_cmlist[int]Bunch circumference
ripeness_fractionlist[int]Ripeness class 1-5, higher means riper
ripeness_fraction_sourcelist[string]field_note or visual_estimate
loose_fruit_kglist[float]Detached loose fruit (brondolan) weighed with the bunch
noteslist[string]Field note, with the FAKSI misspelling normalised to FRAKSI
bbox_sourcelist[string]field or visual_estimate

annotations/trees.jsonl carries the same measurements grouped per tree, plus total_weight_kg, total_detections, and the appearances of each bunch across sides.

Measurement ranges

VariableMinMaxMean
weight_kg3.2034.2216.89
height_cm487262.68
circumference_cm88135110.65
loose_fruit_kg0.8146.6502.776

Ripeness distribution: fraction 1 (1), 2 (3), 3 (7), 4 (14), 5 (6).

Ripeness follows the Indonesian fraksi scale for fresh fruit bunches, which grades a bunch by how many fruits have detached: 1 is the least ripe, with a full, tightly packed bunch and almost no empty sockets, and 5 is over-ripe, with most fruit shed and the spikelets exposed and drying.

Collection

Bunches were photographed at the collection point immediately after harvest with a phone camera, then weighed on a field scale. Height, circumference, ripeness fraction, and loose fruit weight were recorded by hand on the same form. Bounding boxes were drawn by the field team and linked across the two sides of each tree; one box the team missed was added later from the image and is flagged as such.

Each tree ships with a SHA-256 manifest covering its images, labels, and JSON. The script re-checks those digests and confirms that the corrected layer changes nothing beyond the declared corrections:

bash
python scripts/verify_dataset.py .

Corrections applied

The field record had three gaps. Each was resolved by reviewing the photographs against the bunches whose ripeness the field team did record, and every correction is flagged in the data so it can be filtered out. The untouched original is kept under annotations/raw/, where the per-tree SHA-256 manifests still validate.

TreeProblem in the field recordCorrectionFlag
0003Note read FRAKSI with no digitripeness_fraction = 4ripeness_fraction_source = visual_estimate
0004Note read FRAKSI with no digitripeness_fraction = 2ripeness_fraction_source = visual_estimate
0005No fraction line at allripeness_fraction = 5ripeness_fraction_source = visual_estimate
0004 side 2Bunch left unannotated, empty label filebox added at [820, 1185, 1692, 1826]bbox_source = visual_estimate
0014-0027Notes misspell FRAKSI as FAKSIspelling normalised in notesoriginal text in annotations/raw/trees/

Reasoning behind the three ripeness calls, for anyone who wants to revisit them:

  • —0003 retains far more fruit than the fraction 5 references but shows markedly more exposed spikelets and empty sockets than the fraction 3 references, matching fraction 4.
  • —0004 is the fullest bunch in the dataset, with tightly packed fruit and almost no empty sockets, but its fruit already carries the red tone of an early-ripe bunch rather than the darker cast of fraction 1. Graded fraction 2 by the data owner.
  • —0005 is almost entirely shed and drying, visually indistinguishable from the fraction 5 references.

To train only on field-recorded annotations, keep rows where every entry of ripeness_fraction_source is field_note and every entry of bbox_source is field.

Known limitations

Read these before training on the dataset.

  • —Single site, single variety, single day. All 31 trees are variety TANERA from block 303, captured on 2026-09-14. Models fitted here will not transfer to other varieties, regions, or lighting conditions without further data.
  • —Small sample. 31 weighed bunches is enough to prototype a pipeline, not to fit a production regressor.
  • —Three ripeness labels and one box are visual estimates, not field measurements. See the corrections above.
  • —Ripeness is imbalanced. Fraction 4 holds 14 of 31 bunches while fraction 1 holds a single one, so the dataset will not support a balanced five-class classifier as it stands.
  • —GPS is absent for 13 trees. Trees 0001-0013 were captured with location off. Two further readings (0020, 0024) come from the network provider at 300 m accuracy rather than the GNSS receiver; check gps_accuracy_m before using coordinates.
  • —Some measurements look estimated. Five bunches have integer weights (17, 30, 15, 15, 17 kg) and loose_fruit_kg of 1.3 repeats across five trees, which suggests rounding at entry time rather than scale readings.
  • —One implausible loose fruit value. Tree 0007 records 5.624 kg of loose fruit against a 3.2 kg bunch. The value is preserved as recorded but should be treated as suspect.
  • —No operator attribution. operator is UNKNOWN for every tree.

Coordinates

Trees 0014-0031 include GNSS coordinates at 5-15 m accuracy, published at full precision with the consent of the data owner. They identify a specific plantation block in South Kalimantan, Indonesia.

License

Released under CC BY-NC 4.0. Attribution required; commercial use is not permitted.

Citation

bibtex
@misc{ulm_ds_lab_sawit_weight,
  title  = {Sawit-Weight: Two-View Oil Palm Fresh Fruit Bunch Images with Measured Weights},
  author = {{ULM Data Science Lab}},
  year   = {2026},
  url    = {https://huggingface.co/datasets/ULM-DS-Lab/Sawit-Weight}
}