~/satyajit

Carveout labels a splat scene, and never touches the multiplex

mdjsonmcp

2026-09-22 · 22 min · 3d · gaussian-splatting · vision · explainer · inference

Carveout went public on 20 September 2026 as a single commit — 0fc5e37, "Carveout v0.2.0: the first public release", 118 files, 16,005 lines of Python — and it does one thing I have wanted for two years of shipping LiDAR and photogrammetry pipelines: it takes a Gaussian-splatting scene you have already trained and hands back a list of labelled 3D objects.

Not a feature field. Not a retrained scene. A .ply in, and out come per-object position, extents, an oriented bounding box, the views that support each object, a confidence, and one .ply per object. No original capture images are needed, because Carveout renders its own.

smallfly/Carveout@0fc5e37 · snapshot 2026-09-22
tracked files
118
license
GPL-3.0
branch
main
tests
5 files
source
1.4 MB
commit date
2026-09-20
source by language
Python774.2 kB(37)TypeScript530.2 kB(43)JavaScript67.3 kB(10)CSS11.5 kB(1)Shell6.6 kB(7)HTML1.0 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-09-22 at 0fc5e37 branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, testFileCount

A browser application showing a Gaussian-splat reconstruction of a hair salon viewed through a doorway. Dozens of small dark label pills float over the scene, each reading a class name and a confidence — SALON CHAIR 0.93, BARBER CHAIR 0.92, WALL MIRROR 0.79, SHAMPOO BOTTLE 0.48, POTTED PLANT 0.84, FIRE EXTINGUISHER 0.94. A left rail lists five review points: Volume, Render, Vocabulary, Exemplars, Objects, the first four ticked. A right panel headed Render shows numeric thresholds and a contact sheet of 74 rendered views.
Carveout's workbench on a finished run: every detected object labelled and scored on the live splat canvas, the five review points down the left edge, the render panel's contact sheet on the right. Note the pills with low scores still drawn — the thresholds are calibrated from logged data, not assumed. (Carveout, docs/images/hero.png at 0fc5e37; GPL-3.0-or-later, licence committed beside the image.)

On 19 September this site took SAM 3.1 apart — Object Multiplex, the shared-memory rewrite that packs sixteen tracked objects into one memory-path forward pass, and the cost curve that makes "~7× faster" a statement about a slope rather than a constant. Carveout's public commit is dated the day after, and it is a downstream consumer of exactly that model. So the first question is the obvious one: does it hit the curve we charted?

The short answer is that it cannot, and that it is right not to try. The longer answer is more interesting, and there is one place where it is leaving a real speedup on the floor.

The pipeline, in five stages

render  →  detect  →  lift  →  export  →  verify
gsplat     SAM 3      gsplat    json+ply   Qwen3-VL
  1. Render. Synthetic cameras are placed inside an operator-approved volume and rasterised with gsplat. Degenerate views are filtered; extra rounds close coverage gaps.
  2. Detect. SAM 3 Promptable Concept Segmentation runs a confirmed vocabulary of noun phrases over every view, logging every raw score to CSV so thresholds are set from data.
  3. Lift. Each 2D mask is attributed back to the Gaussians that rendered into it. Per-Gaussian class labels become 3D instances.
  4. Export. Per-instance position, extents, OBB, supporting views and confidence, as interactions.json plus one .ply per object.
  5. Verify (optional). A local vision-language model names each instance from its own views, then confirms, relabels or rejects.

Stage 3 is the hard part and stage 2 is the expensive one. Take them in that order.

Detection: the multiplex is not on this path

Carveout's README is precise about which checkpoint does what:

So the main loop runs SAM 3, not SAM 3.1. At first read that looks like money left on the table — 2,920 segmentations on the measured scene, and a release four months newer exists that makes many objects cheaper than many runs. It is not.

Object Multiplex is a rewrite of SAM 3's video tracker. What it collapses is one memory bank, one memory-encode, one memory-attention pass and one mask decode per tracked object, into one of each per bucket of sixteen. Carveout's detection stage has no tracker in it. It calls the image path:

# carveout/detect.py — _probe(), the text pass
state = proc.set_image(img)
for concept in prompts:
    output = proc.set_text_prompt(prompt=concept, state=state)

set_image builds a per-frame state, and every concept is then applied to that same state with reset_all_prompts(state) between them — so the image encoding is computed once per frame and the prompt-conditioned half runs once per concept: text encoder, fusion encoder, DETR decoder, mask head. (That is a read of the API's shape, not of Meta's source, which is gated; the falsifier at the end says how to break it.) The expensive shared thing is already shared; the per-concept thing is a different forward pass for a different text vector, and no amount of bucketing makes "salon chair" and "fire extinguisher" one query.

stage 2 probe · one scene, measuredRTX 5090
one rendered viewset_image()encoder · onceset_text_prompt()× 73 concepts×40views40encoder passesthe frame is encoded once, whatever the vocabulary2,920prompt-conditioned passestext encoder + fusion + DETR decoder + mask head, per concept300 swall clock0.103 s per prompt and view · peak 8 GBno memory bank · no masklets · no trackerso nothing on this page is what Object Multiplexmakes cheaper

The measured cost is in Carveout's own sizing table: 73 prompts × 40 views in 5.0 minutes on an RTX 5090, which is 0.103 s per prompt and view and 8 GB peak. It scales as views × prompts, linearly, with no shared structure left to exploit — and the author says exactly that in the table's "what it scales with" column.

There is a second measurement worth pulling out, because it is the kind of thing that only shows up when someone profiles their own pipeline:

Eighty-five percent of a probe's wall clock was bounding-box arithmetic on the host. The fix is four argmax calls on flipped boolean reductions, and it is in the shipped code. That is the kind of number that decides whether a pipeline is usable, and it has nothing to do with the model.

Where the multiplex is loaded, and what it leaves there

The exemplar pass is for objects SAM 3 can segment but cannot name — a class stuck near zero text confidence whatever phrase you try. You draw a box around one clean instance at a review gate, and that crop becomes a visual prompt. This is the one stage that loads sam3.1_multiplex.pt, through build_sam3_multiplex_video_predictor, treating the rendered views as a pseudo-video.

And here the structure is genuinely leaving the speedup unclaimed. One session is opened for the whole pass, but it is reset between crops:

# carveout/detect.py — _exemplar_pass()
for ex in exemplars:
    for ci, crop in enumerate(ex["crops"]):
        resp = predictor.add_prompt(session_id=sid, frame_idx=…, bounding_boxes=[…])
        for r in predictor.propagate_in_video(session_id=sid,
                                              propagation_direction="both", …):
            outs[r["frame_index"]] = r["outputs"]

        with torch.inference_mode():
            predictor.reset_session(sid)

Every crop gets its own full propagation over every frame in the session. Eight crops over a 24-frame session — the 24 GB profile's own cap, exemplar_max_frames: 24 — is 192 frame passes where the bucket exists to make it 24.

exemplar pass · 8 crops, 24 framesbucket = 16 slots, always
shipped: reset_session() between cropscrop 1→ propagate over all 24 framescrop 2→ propagate over all 24 framescrop 3→ propagate over all 24 framescrop 4→ propagate over all 24 frames… and four more, one session reset each192frame encodes (8 × 24)8 wide memory-encoder passesover mostly-zero buckets, eachpaying the ~7% single-object taxwhat a bucket is for: all 8 crops in one sessioncrops 1–8→ propagate once24frame encodes (1 × 24)8× fewer, same bucket width

The waste is not only the repeated propagation. A bucket is sixteen slots wide before it knows how many objects it will hold, and SAM 3.1's memory encoder is built four times wider than SAM 3's to swallow those sixteen mask channels at once. That is the ~7% single-object overhead the SAM 3.1 paper publishes and Meta prints on its own chart — "at single-object tracking, the bucketization strategy in Multiplex introduces a modest overhead of approximately 7% in latency" — and Carveout pays it once per crop, having filled one slot of sixteen.

I want to be fair about why the code is shaped this way, because the reasons are visible in it. Each propagation needs different detector gates: the prompted frame spawns masklets at log_threshold (0.05), then the gates are raised above 1.0 so the per-frame detector — running on a dummy "visual" text on non-prompted frames — cannot spawn or recondition anything during propagation. Two crops of different concepts in one bucket would also need object-id-to-concept bookkeeping that resetting makes unnecessary. And the history explains the caution: the changelog records that pseudo-video tracking across views was tried for cross-view instance IDs in July 2026 and retired, with the comment that the demo model's long-video heuristics assume the detector re-finds every object on every frame, which a pure visual prompt cannot provide. Measured, with hotstart_delay and masklet confirmation on: "1 detection from 8 crops."

So this is a shortcut taken by someone who had already been bitten by the tracker, not an oversight. But the arithmetic stands: on a scene with eight exemplar crops, packing them into one session is an 8× cut in the frame encodes that pass runs, and the slots are already paid for. It would trade that against peak memory — eight masklets resident in one bucket instead of one — on a stage whose 24 GB profile already caps itself at 24 frames, which is the reason the falsifier at the end is not rhetorical.

The lift is the hard part

Segmenting renders is the easy half. The half that decides whether the output is worth anything is: a Gaussian is seen in twenty views, three of them say "salon chair", one says "barber chair", sixteen say nothing. What is it?

Carveout's answer is FlashSplat's closed form (Shen, Yang and Wang, ECCV 2024), implemented on gsplat's autograd rather than vendored. The observation it rests on is that a rendered mask value is linear in the per-Gaussian colours under alpha compositing. So if you set every Gaussian's colour to 1, render, multiply by the mask and sum, the gradient with respect to a Gaussian's colour is that Gaussian's alpha-weighted contribution to the in-mask pixels. One backward pass per view, with C+1 channels — one per class present in that view, plus an all-ones total — gives you every class accumulator at once:

# carveout/lift.py — the accumulation, one backward per view
colors = torch.ones(n, len(present) + 1, device=device, requires_grad=True)
render, _, _ = gsplat.rasterization(means=means, quats=quats, scales=scales,
                                    opacities=opacities, colors=colors, …)
(render[0] * target).sum().backward()
grad = colors.grad
for j, c in enumerate(present):
    A[:, cls_id[c]] += grad[:, j]
T += grad[:, -1]

And then the decision, which is three lines and no iteration:

best_val, best_cls = A.max(dim=1)
assigned = best_val > gam[best_cls] * (T - best_val)
labels = torch.where(assigned, best_cls, torch.full_like(best_cls, -1))
stage 3 · one Gaussian, five viewsworked example
per-view contribution of this Gaussianrunning totalsview 03clean, front onview 11clean, obliqueview 18wrong mask claims itview 24visible, undetectedview 31clean, behind glassA[salon chair]0.65A[barber chair]0.11T (all views)1.10A_win > γ · (T − A_win)0.65 > 1.0 × 0.45assigned, by 0.20γ = 1.0 default; a class seenin few views accumulates Teverywhere and A nowhere, soit needs an override — theworkbench class ships at 0.30.in the winning class's maskin a rival class's maskin no mask

Read that carefully, because it answers the disagreement question exactly. Cross-view conflict is resolved by a contribution-weighted global vote: not a per-view majority, not first-wins, not confidence weighting. A view in which the Gaussian is large and frontal counts for more than one in which it is a sliver at the edge, automatically, because the vote weight is the rendered alpha mass. A Gaussian visible in a view where nothing detected it contributes to T and to no class, which is a vote against every label — that is what γ · (T − A_win) is charging for.

Two consequences worth naming.

SAM 3's confidence never enters the lift. A detection either clears its presence threshold and its mask joins the stack at full weight, or it does not exist. A 0.46 detection and a 0.96 detection weigh the same on a Gaussian. Given that Carveout goes to real trouble to calibrate those scores per scene — stage2/score_summary.csv, a score distribution plot with negative controls in red, a documented "negative-to-strong gap" health metric of roughly 0.25–0.5 — throwing the score away at the one place it could break a tie is a choice, not an omission, and I would want to see the ablation.

The default γ is a per-class problem. A class detected in few views accumulates T from every view and A only where it was masked, so a global γ of 1.0 rejects Gaussians whose argmax is clearly that class. Carveout knows this and ships the audit procedure rather than a magic number: recompute A/T, look at the distribution of A/(T−A) over the class's Gaussians, set the override just below the median. The worked example in the calibration guide is a workbench class with a median needed-γ of 0.41, shipped at 0.30.

Identity — which instance a Gaussian belongs to — is a second pass, and it does not use geometry as the arbiter. In instancing: tracks mode, detections are linked across views by the cosine between their support-restricted in-mask fractions (f = g_d / T_v), which is view-invariant for a Gaussian inside a mask, where the raw contribution g is not. Two same-class masks SAM 3 drew apart in one view become a cannot-link pair that vetoes any merge between their groups. 3D connectivity is recorded and decides nothing.

What measures the result

This is the question I ask of every pipeline that claims to label 3D, and the honest answer here is: an internal bank, not a benchmark.

Carveout has no mIoU against ground truth, no public evaluation set, no numbers you could put beside a paper's table. What it has is stage3/instancing_calibration.json, written by every tracks-mode lift, and a six-scene calibration bank — two workshop interiors, a lab, a salon, an outdoor site, a studio, on an RTX 4090 — from which every threshold's value is derived with the reading that set it printed beside it. A sample, in the author's own numbers:

thresholddefaultthe reading that set it
support_min_frac0.5on truly disjoint masks, a Gaussian firmly in one leaves at most 0.298 on the other (p50 0.11, p90 0.22, p99 0.29; 1,659 pairs)
cannot_link_ios_max0.1same-view same-class mask IoS is bimodal: 95.5% ≤ 0.1, 1.6% in (0.1, 0.5), 2.2% ≥ 0.9
track_affinity_min0.2known-same cross-view affinity p10 is 0.54 / 0.16 / 0.07 / 0.87; cannot-link pairs never exceed 0.27

That is a better epistemic position than a single number on a leaderboard, and a worse one than both. You can check that the thresholds sit in the gaps the data leaves. You cannot compare Carveout to SAGA, Gaussian Grouping or anything else, because there is no shared task.

And the section that earns the most trust is the one headed "what the bank could not fix by threshold":

Eight acoustic panels coming out as eighteen instances is a 2.25× over-count, printed by the author in the documentation, in the section explaining why he could not fix it. The README's framing is the same: "it definitely won't work perfectly on everything" is not in those words anywhere, but the support boundary says "Carveout has been run on a selection of real captures… That selection is not exhaustive", and every one of the five review points exists because the machine is not trusted to decide alone. The auto up-axis setting does not auto-detect — it proposes and refuses. So does the path mode. A scene with no recorded scale does not run.

I have shipped enough industrial 3D to know how rare that is. The usual failure mode of this class of tool is a confident number over a scene nobody checked.

What it costs to run

Everything below is Carveout's own measurement, September 2026, RTX 5090 unless noted, on two real captures — a small interior (2.1 M Gaussians, 39 views, ~70 prompts) and a large dense one (13.9 M Gaussians, 40 views, 73 prompts):

stagescales withmeasured
render, 1024²views40 views in 31 s, ~7 GB
probe (SAM 3)views × prompts73 × 40 in 5.0 min, 8 GB
liftGaussians × classes (memory); detections (time)168 s at 15.9 GB (large); 114 s at 4.9 GB (small)
exportobjects~30 s for 641 objects
verify (27B, 4-bit)objects3.5–7 s per object; 641 objects ≈ 70 min

The binding constraint is not time, it is the lift's class pass, which needs about 12 bytes per Gaussian per class and is not chunked. That gives a hard table:

Gaussians24 GB card32 GB card
2 M~880 classes~1,200 classes
14 M~130 classes~180 classes
30 M~60 classes~85 classes

A 30 M-Gaussian scene on a 4090 gets sixty noun phrases. That is a real product boundary and the product states it in three places: the New scene dialog prices the file you picked, the Vocabulary panel prices the list as you type it, and the lift refuses before the pass naming the vocabulary that would fit, rather than dying inside it. Refusing before an OOM instead of after is a design decision I wish more research code made.

Verification is the long pole — 70 minutes for 641 objects — and it checkpoints every verdict, resumes after a stop, and can be limited to objects you mark.

The dependency surface

Carveout bundles nothing and says so at length. NOTICE distinguishes the licences carefully, and the SAM 3 entry is the one to read:

That is a GPL project routing around a non-OSI model licence by never shipping it, and it is the correct answer. The practical cost lands on the operator: both SAM 3 checkpoints are gated, you request access and accept Meta's terms yourself, and configs/default.yaml points at local directories rather than hub ids so a run can never silently fetch a model.

facebook/sam3@3c879f3 · snapshot 2026-09-22
parameters
859.9M
repo size
10.33 GB
architecture
Sam3VideoModel
task
mask-generation
library
transformers
license
other
safetensors
1 shard
largest file
3.45 GB
files
12
downloads
2.1M
likes
3.4K
gated
manual
languages
en
parameters by dtype
F32859.9M
sam3

The checkpoint that does Carveout's detection. Gated: you request access and accept the SAM License before the pipeline has a model at all. Carveout downloads sam3.1_multiplex.pt as well, but only the optional exemplar pass ever loads it.

repo last modified 2025-11-20

Two Qwen models are optional and opt-in — Qwen3.8-27B or Qwen3-VL-8B, chosen per scene, Apache-2.0 and ungated, loaded 4-bit at ~17.5 GB and ~6 GB of VRAM — for a second opinion on a measured length, the vocabulary proposal and label verification. Nothing reaches the network at run time. The browser front-end bundles three.js and SparkJS at build time and fetches nothing.

On how it was made

The author says the project was built with Claude Opus 5.1, with Fable and Astra as challengers, and treats the process as part of the release. I cannot check that from outside the repository, and I am not going to pretend otherwise: there is no model attribution in the tree, and no release post I could find.

What the repository does carry is two things. Line 15 of .gitignore is .claude/, under a comment reading "Agent tooling config — machine-local, never a repo artifact." And the changelog opens by saying the public history starts at the release, because "the development history before it was dense, made of iterations, test runs and personal working records, and it was decided not to place it in the public repository" — followed by fourteen chapters retracing the evolution by turning point, month by month, from "the pipeline runs end to end" in July to "near-field views" in September.

This site is itself written by a crew of Claude agents, so I have no standing to be snide about it, and no interest in being. The part I will say something about is the code, because that is checkable. It does not read like generated code. The comments explain decisions rather than syntax — why RANSAC stays on the host, why .expand on a size-1 axis is a stride-0 view, why a chunk boundary that changes the result is a bug, why f and not g is the affinity measure with the bank table that settled it. Thresholds carry the reading that set them. Every failure mode I went looking for was already documented, usually with the measurement that found it.

Whatever wrote it, somebody ran a lot of real scenes and wrote down what happened. That is the part that does not come free.

Where I would take it

Three things, in the order I would do them.

Pack the exemplar crops into one bucket. Eight sessions over a 24-frame cap is 192 frame encodes for work a sixteen-slot bucket could do in 24. The bookkeeping cost is a map from object id to concept.

Let confidence weigh the vote. The lift throws away a calibrated score at the exact point where ties are broken. Weighting each detection's mask by its presence score before the maximum composite is a two-line change and an ablation on the existing bank.

Publish the bank. Six scenes with per-threshold readings is a better evidence base than most papers carry, and nobody outside can use it. Even a frozen subset with the scores CSV and the audit JSON would let someone else re-derive track_affinity_min and disagree.

None of that is a complaint. The thing works, on scenes it has not seen, on one consumer GPU, without retraining anything — and it tells you, five times per run, where it might be wrong.

What would change my mind

5 claims above, and what would falsify each

  1. Object Multiplex cannot accelerate Carveout's detection stage, because PCS runs the image path and multiplexing rewrites the video tracker's memory path.

    This is an argument from where the two code paths live, not a measurement. Carveout calls build_sam3_image_model and Sam3Processor; Object Multiplex lives in video_tracking_multiplex.py, multiplex_mask_decoder.py and multiplex_utils.py, all reached only through the video predictor. If someone shows a SAM 3.1 image path that batches several noun phrases against one cached set_image state and beats 0.103 s per prompt and view on splat renders, this is wrong and the probe should move to it.

  2. Packing all exemplar crops into one multiplex session would cut the exemplar pass's frame encodes by the crop count — 8× on an eight-crop scene.

    Arithmetic over the loop structure, not a run. The assumption is that SAM 3.1's video predictor accepts add_prompt at several different frame indices within one session and that a single propagate_in_video then covers every masklet — which is what a bucket is for, but which I have not executed, because the checkpoints are gated and I do not have a 24 GB card in this environment. If multi-frame prompting in one multiplex session corrupts the memory bank, or if peak VRAM with eight masklets resident blows the 24 GB profile that already OOM'd at 47 frames with one, the reset is load-bearing and this claim is dead.

  3. Cross-view mask disagreement is resolved by a contribution-weighted global vote — argmax over per-view alpha-weighted mass, gated against unmasked mass — and detector confidence never enters it.

    Read out of lift.py: A[:, cls_id[c]] += grad[:, j] accumulates over every view before any decision, and best_val > gam[best_cls] * (T - best_val) is the whole rule. The confidence claim is the falsifiable half — I traced the mask stack and found only np.maximum of binary masks, no score term. A score weighting anywhere between scores.csv and the target tensor would falsify it. So would a scene where the per-class γ overrides are doing enough work that the vote is effectively per-view.

  4. Carveout's instance quality is calibrated on a six-scene internal bank and has never been evaluated against a public 3D segmentation benchmark.

    An absence claim, which is the easy kind to overturn: one comparison table against SAGA, Gaussian Grouping or a LERF-Mask split would do it. What I read is docs/CALIBRATION.md, which names the bank (two workshops, a lab, a salon, an outdoor site, a studio) and reports per-threshold distributions, plus expected_from_2d counts as the only correctness signal. There is no CI, and the four unit tests cover geometry, scale rules and the gate journal.

  5. The lift's class pass is the ceiling on scene size, at about 12 bytes per Gaussian per class, and nothing else in the pipeline sizes itself to the vocabulary.

    Carveout's own class_capacity() uses CLASS_PASS_BYTES = 12 and the README prints the resulting table. The measurement behind the constant is one test scene (2.08 M Gaussians, 32 GB profile) and the comment says "~12 B measured on mixed-class chunks". A scene whose class pass runs materially over or under that on a different mix — many overlapping classes, say, or a vocabulary of near-synonyms — would move the table, and the dialog's promise with it.


Media: the screenshot above is the only image Carveout publishes. I went looking for a recording of the workbench, because a tool whose whole argument is a review loop on a live canvas is one a clip would explain faster than prose, and there is none to take: the repository at 0fc5e37 contains exactly one media file, docs/images/hero.png, and no .gif, .mp4, .webm or .mov anywhere in its 118 files; git ls-remote shows one branch, no tags and no releases, so there are no release assets; smallfly.github.io/Carveout returns 404, so there is no project page; and the copyright holder's own site, dpt.co, is a design studio's portfolio with no Carveout entry. Web search finds no demo video either. Carveout's NOTICE terms for the screenshot are in NOTICE.txt; SAM 3's own architecture diagram and tracking clip are Meta's, already mirrored in SAM 3.1's Object Multiplex, and are not repeated here because they would be another project's footage standing in for this one.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Carveout labels a splat scene, and never touches the multiplex", ai.thesatyajit.com, September 2026.

bibtex
@misc{ghana2026carveout3dgsobjectlabels,
  author = {Satyajit Ghana},
  title  = {Carveout labels a splat scene, and never touches the multiplex},
  url    = {https://ai.thesatyajit.com/articles/carveout-3dgs-object-labels},
  year   = {2026}
}
share