CoolFace
Datasetpublic

yrlyrl/spatial-mmcot-messytable

Spatial MMCoT v1 · messytable Multi-camera tabletop counting on real photographs from MessyTable (Z. Cai, J. Zhang, D. Ren, C. Yu, H. Zhao, S. Yi, C. K. Yeo and C. C. Loy, "MessyTable: Instance Association in Multiple Camera Views", ECCV 2020; https://github.com/caizhongang/MessyTable). The questions, counts and reasoning text come from leo66666/messytable, which pairs those photographs with a counting chain of thought. Several angled views of one cluttered table are the input;… See the full description on the dataset page: https://huggingface.co/datasets/yrlyrl/spatial-mmcot-messytable.

sourceHugging Faceotherupdated 19h agoView on Hugging Face
0likes
Dataset Card

Spatial MMCoT v1 · messytable

Multi-camera tabletop counting on real photographs from MessyTable (Z. Cai, J. Zhang, D. Ren, C. Yu, H. Zhao, S. Yi, C. K. Yeo and C. C. Loy, "MessyTable: Instance Association in Multiple Camera Views", ECCV 2020; https://github.com/caizhongang/MessyTable). The questions, counts and reasoning text come from leo66666/messytable, which pairs those photographs with a counting chain of thought. Several angled views of one cluttered table are the input; the plan notes that angled views double-count under occlusion; the target is the real photograph from MessyTable's overhead camera (cam1), which never appears among the inputs (when the read-back says 'the generated top-down view' it means the view the model is trained to produce); the read-back counts off it. The overhead frame is a crop, though, and on a few rows the read-back adds objects it says are outside or hidden in that frame, taking them from the angled views. The answer is upstream's gt_answer, and on questions about a broad category ('canned beverage/food', 'opaque cup', ...) it is often lower than the number of such objects on the table (see Known issues). Only the upstream train split is used, and both splits here, validation included, are carved from it by capture scene. The upstream test split is the pool from which the IPT paper's MessyTable counting benchmark (MVCMessyTableImaginativePerceptionToken) draws its items. This release shares no capture scene with that split, but it has the same camera rig, object inventory and question templates: a model trained on this source is in-domain for that benchmark and should not report it as out-of-distribution. If you use these images, cite MessyTable.

Supervision kind (supervision_kind in meta): full_interleaved on every row: the upstream trace itself interleaves text and target images (drawn or rendered states on most sources; the source note above says which), and the read-back comes after the target image it reads. The text is upstream's and was not checked against the images.

Upstream: `leo66666/messytable`. Licence: undeclared. The upstream repository declares no licence; this converted copy is shared for research use only, whatever terms the upstream authors set apply to it as well, and it will be taken down at their request. The photographs are MessyTable's: its code repository is MIT-licensed, and neither that repository nor the project page states separate terms for the image data.

Known issues

Rows with a measured per-row problem are listed in reports/known_issues/, one TSV per issue (a # <description> line, then row_uid<TAB>split<TAB>detail lines), so they can be filtered out. They are still in this release: no row was removed for these issues.

issuerowstrainvalidationwhathow it was foundfile
readback_excludes_same_category28271The question names a broad category, and the read-back reaches the (lower) label by setting aside objects of that category as a different type/brand/style ('The two orange cups are also opaque but are a different style'), by naming only the 'boxed snacks of interest', or by counting groups as one ('treating each group as one'); upstream's gt&#95;answer appears to count one product, not the whole category.the read-back says 'of interest', 'treating each group' or 'this scene's labeling', or it has an exclusion phrase ('different type&#124;kind&#124;brand&#124;style&#124;product&#124;item&#124;drink&#124;packaging&#124;container', 'not counted / not the queried ... category&#124;type', 'specified') whose subject (from the sentence start, or from an unclosed '(' that opens its own clause, to the phrase) names an object of the queried category (its non-generic words, or their container noun: bottle, can, box/carton, bag/pouch/packet, cup/mug/tumbler); measured 2026-09-25; known&#95;issues.py sha1 655e5307; export 4aaee1f rows be1d3dc7/6070fad9reports/known_issues/readback_excludes_same_category.tsv
readback_counts_beyond_target770The read-back does not count off the overhead target alone: it adds objects it says the angled input views show but the overhead crop does not (a second pineapple outside the crop, a stack seen from the side; on at least one row the added object is in no image).a read-back sentence mentions the angled/input/original/side views and adds something (additional, extra, another, one more, a second, bringing the total, not visible in the top-down view, outside the crop, a stack of two) with no negation (no/not/without/any/rather than) in the 30 characters before it, not in a clause whose verb is 'confirm(s)' unless it confirms 'an additional/another ...', and not 'appear(s) as additional'; measured 2026-09-25; known&#95;issues.py sha1 655e5307; export 4aaee1f rows be1d3dc7/6070fad9reports/known_issues/readback_counts_beyond_target.tsv

To leave the listed rows out (the snippet in the loader section downloads reports/known_issues/ with the data):

python
import glob, os
root = "<root>/messytable"
drop = {line.split("\t")[0] for f in glob.glob(os.path.join(root, "reports/known_issues/*.tsv"))
        for line in open(f) if line.strip() and not line.startswith(("#", "row_uid\t"))}
# keep a row when its row_uid (a column of train/, meta/ and preview/) is not in drop

Measured caveats

Measured on this release by the pre-publication review (2026-09-25): problems that cannot be listed row by row (a shortcut in the options, a label convention, an upstream labelling scheme) and what the review found around the lists above. Where a caveat counts listed rows ("listed as ..."), the count is the table's, read from reports/known_issues/summary.json. Its other numbers are the review's own measurements, which no file carries: they hold for exactly these rows and are not re-measured automatically. Items marked Training-signal defect are problems in what the rows teach, not only in how they are described; no row was removed for them.

  • —Training-signal defect. The answer is upstream's gt_answer, and on questions about a broad category it is often lower than the number of such objects on the table. The questions name a category ('canned beverage/food', 'bottled beverage', 'boxed snack', 'instant cup meal', 'opaque cup', ...); where the table holds several products of that category, the label appears to count only one of them, and no label is above 8. For example 3679ee397a0860d4 asks 'Count canned beverage/food in this scene.' and the answer is 5, while its overhead photograph shows about 22 cans in five groups. Of 20 randomly drawn training rows about such categories, at least 4 plainly show more objects of the named category than the answer, and about 8 more probably do. The read-backs were written to reach the stored answer, so on these rows they leave out the other products, exclude them outright ('The two orange cups are also opaque but are a different style', 36b8d38bf3a3396e) or regroup them ('Treating each group as one distinct canned-beverage/food instance', 3679ee397a0860d4); the 28 rows (27 train, 1 validation; listed as readback_excludes_same_category) are the read-backs that say so in words. A model trained on these rows learns to under-count a named category. All 61 S4c quarantines point the same way (each read-back counted more than the label), so the rows kept lean toward read-backs that agree with the narrower count. Read the answer as the count of one product, not of the whole category. The upstream test split, from which the IPT MessyTable benchmark draws, is built the same way.
  • —The overhead frame is a crop (union_center) and does not always show every counted object: on 7 rows (7 train, 0 validation; listed as readback_counts_beyond_target), 0.5% of the rows, the read-back adds objects it says are outside or hidden in that frame, taking them from the angled views (e.g. 37eff3f7b91a3eb2, a second pineapple outside the crop); at least one such object is in no image (9ba7d5ebb7e52811), and a few read-backs' own arithmetic does not match their stated total. 122 of the 1,517 read-backs appeal to the angled views.

Size

splitrowstarget image slotsdistinct target images
train1,4901,4901,490
validation272727
tasktrainvalidation
multiview_counting1,49027

Input images per row: 2 to 7. Target images per row (the images the model is trained to generate): 1. Image corpus (source_scene_corpus): messytable 1,517.

Row format

One row is: input image(s) and a question, then K rounds of thought → target image (the target is the source's own ground-truth image, which the model is trained to generate), then a final thought (normally a read-back of the last target; where a source's final thought is something else, or often leaves out the answer, the source note or Known issues says so) and the answer; here K is 1. In the train config:

image_list        list<binary>  inputs first, then the K target images in order
num_input_images  int64         how many of image_list are inputs
instruction_list  list<string>  one element: system prompt + question
output_text_list  list<string>  K+1 elements:
  [0]   <think>plan 1</think><image_start>
  [j]   <image_end><think>plan j+1</think><image_start>
  [K]   <image_end><think>read-back</think><answer>answer</answer>
row_uid           string        join key to `meta` and `preview`

Every image is a JPEG, and no input image is larger than 512 px on its long edge (measured on this release, 2026-09-25); the size each target was stored at is target_px in meta.

<answer> holds exactly meta.answer_value (also the answer column of preview) on every row: score model output against that string.

The system prompt is ThinkMorph's VLM_THINK_SYSTEM_PROMPT from its inferencer.py, verbatim (GEN_THINK_SYSTEM_PROMPT there has the same text), including its leading and trailing newline. The markers are plain strings, not tokenizer special tokens; the prompt writes </image_end> and the data writes <image_end>, exactly as the ThinkMorph-7B checkpoint was trained.

preview shows the same rows with one column per slot: input_image_i for the inputs; for each of the K = num_steps rounds, the plan thought_j and its target target_image_j; and the read-back in thought_1 on every row.

meta holds the per-row sidecar: task, scene_id and geometry_uid (the scene and geometry keys; the split key is named in the split paragraph below), trajectory_id (a camera-path or sample label, empty where the source has none), num_steps, num_input_images, answer_type, answer_value, majority_class_rate, target_image_kind, target_px, est_tokens, licence, split (train / validation, the Hub split names), supervision_kind (full_interleaved / visual_aux / visual_only) and filter_flags. majority_class_rate is the share of the task's most frequent answer_value among its training rows: it measures answer skew and is not a guessing baseline (where a task mixes question types or each row has its own options it can be far below chance); compare scores with the text-only baselines below.

Per-row license in meta: undeclared 1,517.

Flags on released rows (filter_flags in meta and preview, comma-separated):

flagrowsmeaning
S5.replay_unsupported1,517no solver re-derives this task's answer from the trace, so S5 did not replay it
S14.sampled_qa200chosen for the S14 human spot-check (reports/s14_sample.tsv)

Training with a BAGEL-family loader

Rows here have 2 to 7 input images: the first num_input_images entries of image_list are inputs and the rest are targets, so the loader must read num_input_images. The UnifiedEditIterableDataset of the IPT release (https://github.com/weikaih04/Imaginative-Perception-Token, data/interleave_datasets/edit_dataset.py) does: its parse_row conditions on image_list[:num_input_images] and trains the remaining images as targets, one after each output_text_list element but the last. The stock ThinkMorph loader (the same class in https://github.com/ThinkMorph/ThinkMorph) does not: it conditions on image_list[0] only and trains image_list[j+1] after output_text_list[j], the answer element included, so every input after the first is trained as a generated image and every target moves one slot later per extra input. Every row here has one target, so on a row with exactly two inputs it trains image_list[1] (the second input view) after the plan and image_list[2] (the real target) after the answer; on a row with three or more inputs it trains image_list[1] after the plan and image_list[2] after the answer, both of them input views, and the real target is never trained. It raises no error. To use it, make two changes in its parse_row:

k = int(row.get("num_input_images", 1) or 1)
for im in images[:k]:          # replaces the single _add_image(images[0], ...)
    data = self._add_image(data, pil_img2rgb(Image.open(io.BytesIO(im))),
                           need_loss=False, need_vae=True, need_vit=True)
...
img_idx = idx + k               # replaces img_idx = idx + 1

The stock BAGEL edit loader (ByteDance-Seed/Bagel) cannot train these rows: it never reads output_text_list and expects each instruction_list element to be a list of paraphrases.

parquet_info.json keys each training chunk as <source>/<split>/<file>, here messytable/train/chunk_00000.parquet, with row-group counts read from the parquet footers. The loader matches a chunk only when its key equals the path it builds, os.path.join(data_dir, file), and skips a chunk with no key without a warning: a source that is alone in its group then fails with IndexError: list index out of range, and in a mixed group it adds no rows. Download into a directory named after the source, not after the repository:

python
from huggingface_hub import snapshot_download
snapshot_download("yrlyrl/spatial-mmcot-messytable", repo_type="dataset", local_dir="<root>/messytable",
                  allow_patterns=["train/*", "validation/*", "parquet_info.json", "reports/known_issues/*"])

Then either run from <root> with data_dir: messytable/train and parquet_info_path: messytable/parquet_info.json, or rebuild the index with absolute keys and use an absolute data_dir:

python
import json, os
root = "/abs/path/to/root"                      # the directory that holds messytable/
info = json.load(open(os.path.join(root, "messytable", "parquet_info.json")))
info = {os.path.join(root, k): v for k, v in info.items()}
json.dump(info, open(os.path.join(root, "messytable", "parquet_info_abs.json"), "w"))
# data_dir = os.path.join(root, "messytable", "train")   (spelled exactly so, no trailing slash)
# parquet_info_path = os.path.join(root, "messytable", "parquet_info_abs.json")

The Hugging Face cache (.../snapshots/<hash>/train/) or a folder named spatial-mmcot-messytable matches no key.

num_used_data counts chunk files, not rows: the loader repeats this source's file list up to that number, lists every (file, row group) pair, and deals whole row groups out, floor(R / worldsize) to each rank and floor(that / numworkers) to each DataLoader worker. The remainder is never read. This source has 1 training chunk file holding 12 row groups of up to 128 rows, so keep num_used_data large, e.g. the 128 of ThinkMorph's interleaved_reasoning.yaml (upstream's example.yaml asks for more than GPUs x workers); every row group is then read. Set to 1 and alone in its group on 8 GPUs with 4 workers, it gives every DataLoader worker an empty list, and the iterator then loops forever printing repeat without yielding a row. In a run that mixes sources, give each source the same multiple of its own training chunk-file count, e.g. 128 per file (128 here): the file list is repeated up to num_used_data entries, so a flat 128 for every source would read a two-file source's rows half as often as a one-file source's.

How the rows were chosen

stagerows
upstream rows read (train split only)1,880
refused before conversion (S0raw)0
quarantined at S4c (an automatic check could not match the read-back's conclusion to the label)61
after conversion and per-row filters1,819
removed by S10 (none)0
removed by answer-prior balancing (S13)302
released1,517

Every removed row has one line, with its reason, in reports/:

filestepreason (the line's `flag`, or the field shown)rows
build/quarantine.jsonlS4cS4c.cot_label_conflict61
s13_dropped.jsonlS13step: answer302

Every line of s13_dropped.jsonl has reason: prior_downsample; step names the balancing pass that removed it, and split is written train or val (the Hub's validation).

61 rows were quarantined (S4c) and are not in this release. On every one the read-back's final sentence states a total for the object the question asks about, and that total is higher than the stored label on all 61, by 1 to 14 (each line's detail in build/quarantine.jsonl gives both numbers). All of them were read: none is a phrasing mismatch. Whether the label or the read-back is wrong was not checked against every photograph (see Known issues on how the labels count).

<details><summary>Per-step counters of the conversion</summary>

1,880 upstream rows were read; S0raw refused 0 before a row existed and passed 1,880 to the first step. S0 runs once more, last, on the final bytes. The reason for every refused, dropped or quarantined row is in the files above.

stepinoutdroppedquarantinedrejectedrepaired
S41,8801,8800000
S4c1,8801,81906100
S51,8191,8190000
S81,8191,8190000
S91,8191,8190000
S0 (final structural check, after S9)1,8191,8190000

</details>

The train/validation split keeps rows sharing a scene_id in meta on one side, and the assignment is frozen (splits/ in the summary repository). S12 saw 1,819 rows under 191 keys. No validation input image has the content of a training input image, and none is a pixel-level near-copy of one. S12 does not record per source whether that test ran, but it skips it only for a source whose spec sets split_leak_pixels: false, and no spec does; over all sources it compared 21,661 candidate pairs (perceptual hash within 6 bits) pixel by pixel and found no near-copy (checked 2026-09-25).

Answer-prior balancing (S13)

Each (task, split) group is checked separately. An answer is the answer value compared as lower-cased text without a trailing full stop, with 'farther' read as 'further' and 'nearer' as 'closer' (for multiple choice, the option text, not the letter; where the candidates are drawn in the image, as in zebrajigsaw and zebratetris, the answer is the letter itself). An answer is real when it holds at least 5 rows and 2% of the group; k is the number of real answers. Answer step: the target is max(30%, 1/k) when k >= 2, and max(30%, 1/d) over the d distinct answers when k = 1; a validation group uses the larger of its own target and its task's train target. A group is cut only when k >= 1 and its most common answer holds more than the target plus 5 percentage points; every answer is then capped at one common count, chosen so that none exceeds the target, and smaller answers keep all their rows. At the answer step, a group at or below that trigger, or with no real answer (k = 0), is left as it is, so its most common answer can hold up to the target plus 5 percentage points. A task whose train group has exactly two real answers is instead cut, in every split, so that its two largest answers have equal counts, with no trigger. Rank and label steps: then, in a group where every option value of every row is a number, the rank of the correct option among the sorted values, and after it, in a group where every trained answer is an option label, the label, are each capped by the same cut-and-trigger rule on their own counts (own target, validation included): capped, never evened out, so two labels are cut only when one exceeds 55%, and then only down to 50%. These steps can also cut groups the answer step left whole, including k = 0 groups, and can raise an answer's final share above its target; the run fails if a real answer ends above the target plus 5 percentage points. A train group of at least 20 rows in which one answer holds 90% or more fails the run. PET (exactcellspet) instead cuts each (question type x turn direction) cell to equal counts of its two answers; a PET cell that shows only one answer is removed.

tasksplitpassrulerows in → outreal answers ktargetlargest share, before → aftercut
multiview_countingtrainanswercap30[canon]1,792 → 1,490530.0%41.8% → 30.0%yes
multiview_countingvalidationanswercap30[canon]27 → 27333.3%33.3% → 33.3%no

S13 removed 302 rows from this source.

Text-only baselines

Accuracy of guessers that never see an image. For each task the released training rows are split into two fixed halves by a hash of row_uid; each guesser is fitted on one half and scored once on the other (one held-out half, not cross-validation; eval rows below). The reference is chance (the mean of 1 / number of options) where every row is multiple choice, and otherwise the eval-half accuracy of always giving the answer most common in the fit half (when a task's top answers are nearly tied, this need not be the task's most common answer; the line after the table gives that answer's validation score). Accuracies are recounted from the stored rates and eval rows, so they are exact. A task is flagged when a text-only guesser beats its reference by more than 0.15 (for a free-form task, a guesser other than the most common answer). A flagged task can be partly answered from the text alone; an unflagged task passed only these probes, which do not prove the text carries no answer. Report scores on every task next to this baseline.

Guessers: keywords: the most common answer per set of spatial words in the question; majority: the answer most common in the fit half; template: the most common answer per question wording (numbers masked, object names kept).

taskbest text-only guesseraccuracyreferencemargineval rowsflagged
multiview_countingtemplate0.4210.298 (majority)+0.123731no

Always giving the most common training answer, scored on the validation split (the constant baseline to compare validation scores with): multiview_counting: always answering 2 (30.0% of training rows) scores 0.296 (8/27).

Spot-check (S14)

Pending. The S14 rows are chosen and flagged S14.sampled_qa in meta and preview; the human pass over them has not been signed off yet.

Citation

Please cite MessyTable, whose photographs these are (bibtex from its repository), and credit the question and reasoning release `leo66666/messytable`, whose card gives no citation:

bibtex
@inproceedings{CaiZhang2020MessyTable,
  title={MessyTable: Instance Association in Multiple Camera Views},
  author={Zhongang Cai and Junzhe Zhang and Daxuan Ren and Cunjun Yu and Haiyu Zhao and Shuai Yi and Chai Kiat Yeo and Chen Change Loy},
  booktitle={Proceedings of the European Conference on Computer Vision (ECCV)},
  month={August},
  year={2020}
}

Provenance

The release files were written by our conversion code (the code repository is not public yet), scripts/convert/export.py at commit 4aaee1f4e946, from build messytable_r2. The build was made by scripts/convert/run_source.py from the same repository at commit 949af62f8ab7. S10, S12 and S13 ran before the export; reports/export_manifest.json pins every input the export read by SHA-1 (build_manifest_sha1, s10_keep_sha1, s12_assignments_sha1, s13_balanced_keep_sha1).

Every row removed between upstream and this release has one line, with its reason, in reports/: build/dropped.jsonl (rows refused before conversion or dropped by a conversion step); build/quarantine.jsonl (rows set aside by S4c because an automatic check could not match the read-back's conclusion to the label); s10_dropped.jsonl (duplicates removed by S10); s10_label_conflicts.jsonl (rows S10 withheld because another row asks the identical question, options in the same order, of the same images with a different answer); s13_dropped.jsonl (rows removed by answer-prior balancing). known_issues/ lists rows with a measured problem (see Known issues); reports/ also holds the build manifest (absolute paths cut to basenames) and counters, the S14 sample list (s14_sample.tsv: rowuid, task, split) and `exportmanifest.json. Part of [yrlyrl/spatial-mmcot`](https://huggingface.co/datasets/yrlyrl/spatial-mmcot).