AR Photo Booth — Real-Time AI Compositing Kiosk
2024–2026 · Seven furry conventions in South Korea · Product, engineering, and operations, solo
Repository: https://github.com/auejin/mirror-selfie 한국어판: the full portfolio
Contents
- Executive Summary
- Problem Definition
- System Architecture
- Technical Highlights
- Product Iteration
- Field Operations and Running a Brand
- Quantified Impact
- Incidents and Structural Fixes
- Limitations and Roadmap
- Appendix — Tech Stack
Evidence grades
Every figure in this document carries a grade.
| Grade | Meaning |
|---|---|
[Verified] |
Derived directly from code, files, or git. A reproduction command exists |
[Ledger] |
Measured and recorded by the operator in the event expense ledger. Not third-party reproducible |
[Git history] |
Inferred from commit history |
Nothing was omitted for having a weak grade, and nothing is presented as stronger than it is.
Currency conversions throughout use ₩1,400 ≈ $1, rounded. They exist to give non-Korean readers a sense of scale, not as precise figures.
1. Executive Summary
AR Photo Booth is an unattended kiosk that separates a person in a full-body costume from the background using a single webcam, composites them between illustrated character layers, and prints the result immediately. The entire pipeline runs on-device and requires no network connection.
Between August 2024 and April 2026 it captured 703 photos across seven conventions
[Ledger]. For the two 2026 events the application auto-saved the source frame, the mask,
and the composite, so 141 complete sets survive [Verified]. The longest single-day
run was 7 hours 53 minutes, with a median interval between captures of
2 minutes 13 seconds [Verified].
What this document is actually about is not model accuracy — it is design decisions under constraints. The constraint that participants cannot operate a screen produced the automatic prompt-generation architecture. The constraint that convention halls have no internet produced the on-device architecture. And that architecture turned out to coincide exactly with the privacy policy the project needed.
2. Problem Definition
2.1 Domain context
On furry fandom, fursuits, and conventions
Furry fandom is a creative and costuming community built around anthropomorphic animal characters. A fursuit is a full-body costume realizing such a character, typically consisting of a sculpted head covering the entire face and a body made of synthetic fur. South Korea hosts several conventions a year, and a substantial share of attendees participate in fursuit.
This domain matters here for a technical reason rather than a cultural one: a fursuit is a naturally occurring adversarial sample for human matting models.
2.2 Why I built it
Convention attendees want photographs of their fursuits, but professional shoots involve booking, waiting, and post-production lead time. Walking away with the result in hand is a categorically different experience.
Commercial photo booths satisfy half of this. They handle capture and instant printing, but they cannot composite a background and props matched to the participant's own character. Chroma-key booths can composite, but they demand floor space, setup time, and lighting control — and a convention booth controls none of those.
2.3 What made it hard
(a) Fursuits break general-purpose human segmentation
Nearly every cue such models rely on is invalidated:
| What the model expects | What a fursuit actually presents |
|---|---|
| A silhouette matching human body statistics | Muzzle, ears, and tail push the outline outside that distribution |
| Crisp skin/clothing boundaries | Synthetic fur edges are translucent at the pixel level |
| Matte surfaces | Glossy fur reflects the background |
| Face and skin-tone cues | None — the wearer is fully covered |
| Consistent lighting | Convention halls have mixed lighting |
(b) There is nobody available to operate it
Participants wearing sculpted heads have restricted vision and limited hand dexterity. The operator is attending to the next person in line. In other words, no human can intervene anywhere in the pipeline.
(c) The field violates every assumption of the development environment
No internet. Multiple cameras enumerated. A window accidentally closed. A printer jam. And all of it happens while people are standing in line watching.
(d) A quality problem with no ground truth
Whether a composite looks natural is not measurable by any automatic metric. High IoU means nothing if the tip of an ear is clipped — the participant will notice. The evaluation function is a human being, and nothing else.
3. System Architecture
3.1 Overall structure
graph TB
subgraph Entry["Entry point"]
Main["main.py · Application<br/>tkinter + cv2 dual loop"]
end
subgraph Cam["Camera layer"]
ICamera["ICamera (ABC)"] --> WC["Webcam<br/>4K · per-OS backend"]
end
subgraph AI["AI pipeline"]
DEPTH["DepthEstimator<br/>Depth Anything V2 Small"]
SEG["SAM2ImageProcessor<br/>SAM 2.1 Tiny"]
DEPTH -->|"rough mask + smart points"| SEG
end
subgraph Comp["Compositing layer"]
MIRROR["MirrorCompositor<br/>layer compositing + color harmonization"]
CV["OpenCVUtils<br/>UMat/numpy abstraction"]
MIRROR --> CV
end
PRINTER["BasePrinter<br/>CUPS / Win32"]
Main --> ICamera
Main --> DEPTH
Main --> SEG
Main --> MIRROR
Main --> PRINTER
3.2 Segmentation pipeline — depth generates the prompt
flowchart LR
A["RGB frame<br/>4K"] --> B["downscale to 1024px"]
B --> C["Depth Anything V2<br/>depth estimation"]
C --> D["suppress_depth_gradient<br/>remove floor/wall low-frequency"]
D --> E["get_rough_mask<br/>depth 0.7 + center prior 0.3<br/>→ Otsu → connected component"]
E --> F["FPS sampling<br/>FG/BG points"]
E --> G["rough mask"]
F --> H["SAM 2.1<br/>hybrid prompt"]
G --> H
H --> I["union"]
G --> I
I --> J["depth-gradient<br/>edge refinement"]
J --> K["mask upscaled to 4K"]
The key idea is that depth produces the prompt SAM 2 requires. SAM 2 is promptable, but an unattended booth has nobody to supply the prompt.
3.3 Threading model — separating preview from capture
sequenceDiagram
participant T0 as Main thread
participant T3 as Detection thread
participant D as Display / disk
loop every frame
T0->>T0: get_frame()
T0->>T0: consume _pending_result (lock)
alt previous detection finished
T0->>T3: start _run_detection_async
T3->>T3: depth → points → SAM2 → refine
T3->>T0: store _pending_result (lock)
end
T0->>D: composite with cached mask → imshow
end
Note over T0,D: shutter pressed
T0->>T3: join(timeout=5s)
T0->>T0: synchronous re-inference on current frame
T0->>D: save pixel-accurate composite + print
The preview path and the capture path are separate. The preview mask may be up to one frame stale, and that is a cost accepted deliberately — while someone is settling into a pose, the discrepancy is not perceptible.
3.4 Layer compositing
Background (background.png)
└ Rear character layer ← illustration behind the subject
└ Person cutout ← participant, extracted by mask (color-harmonized)
└ Shared stickers
└ Front character layer ← illustration occluding the subject
This structure only works if each character asset is drawn as two separate images, front
and rear. The software architecture dictates how the illustration is produced, and the
composition of the illustration in turn dictates a segmentation parameter (center_ratio).
Section 5 returns to this.
4. Technical Highlights
4.1 Automatic prompt generation — taking the human out of the loop
The problem. SAM 2 accepts point, box, or mask prompts to segment arbitrary objects. Not needing per-class training is the advantage; the flip side is that someone has to supply the prompt. In an unattended booth, that someone does not exist.
The approach. Monocular depth estimation became the prompt generator.
- Estimate a depth map with Depth Anything V2 Small
suppress_depth_gradient()— remove the slowly varying low-frequency component contributed by floors and walls: extract it via Gaussian blur, subtract, renormalize to the [2, 98] percentile rangeget_rough_mask()— Otsu-threshold a blended score of depth × 0.7 + center Gaussian prior × 0.3, clean up morphologically, keep only the center connected component (dropping floor and wall), then expand using LAB color distance- Sample foreground points inside the rough mask with farthest point sampling, and fix eight image-border points as background
- Feed the rough mask (as logits) plus the points to SAM 2.1 as a hybrid prompt
Trade-off. Adding a model increased latency. In exchange, the system adapts to variation in height, pose, and costume type with no human involvement. Section 4.2 absorbs the added latency.
A fixed-coordinate prompt was the cheaper option. I rejected it because of the variance in participant height and the freedom of pose. The value of the booth lies in participants striking the pose they want, and a fixed prompt converts that freedom directly into a failure rate.
Evidence: utils/depth.py, utils/segment.py, main.py:_run_auto_detect,
commits b7d9114, 06c7856, 75546e4
4.2 Asynchronous double buffering — accuracy only when it counts
The problem. Running depth + SAM2 + refinement synchronously on every frame stalls the preview. Slowing the inference cadence instead means the mask at the moment of capture belongs to the previous pose, and arms or ears get clipped in the output. Two requirements with different time scales were sharing one path.
The approach. Split the paths.
- Main loop: composite immediately from the cached mask and
imshow. If the detection thread is idle, dispatch new work; consume completed results under a lock. - On capture:
_flush_detection()joins any in-flight thread (5 s timeout), consumes the pending result, then runs one more synchronous inference pass on the current frame and re-composites with that mask before saving.
Trade-off. The preview mask lags by one frame. That is imperceptible while someone is posing, and the accuracy cost is paid only at the decisive moment.
Second-order effect. Growing to six background threads (segmenter load, depth load,
detection, asset load, file write, print) made shared-state protection a real concern. In
particular, the _smart_points / _rough_mask caches are protected not by a lock but by
structural serialization — they are read and written only within the same thread, and
_flush_detection runs only after a join. That reasoning cannot be reconstructed from the
code alone, so it is written down in .agent/knowledge/threading-model.md.
Evidence: main.py:253-357, .agent/knowledge/threading-model.md, commits 6837615, 3bf6ddf
4.3 Decoupled resolution — print quality without losing real time
Printing needs 4K, but inference at 4K is not real-time. Naively upscaling a low-resolution mask produces staircase artifacts along the boundary that are immediately visible in print.
The mask alone is computed at 1024px on the long axis while the pixels stay at 4K. The staircase artifacts are handled by the next section.
Evidence: main.py:33, 195-211, commit 026c54d
4.4 Edge refinement — reusing depth as a discriminator
The problem. Applying a uniform blur to remove staircase artifacts also destroys real detail — ear tips, fingers, tail. Not applying it leaves the artifacts. From the mask alone there is no way to tell which pixels are true boundaries and which are upscaling artifacts.
The approach. Reuse the depth map already computed upstream as the discriminator.
- Apply Sobel to the depth map, take gradient magnitude, normalize to 0–1 (higher = more likely a true boundary)
- Prepare a morphologically smoothed (Close/Open) version of the mask
- Define a boundary band as the difference between dilate and erode, and blend only
inside it:
original × edge_weight + smoothed × (1 − edge_weight) - Binarize at the end to retain a crisp, vector-like edge
Where there is a genuine depth discontinuity the original edge survives; only the staircase in flat regions is smoothed away. The point of the design is that it solves the problem by recycling an existing computation rather than adding another model.
Evidence: main.py:374-425, commit 7b5a2b7
4.5 Color harmonization — three generations, and a regression test for a problem with no metric
There is no ground truth here. It cannot be validated automatically, the hall lighting differs every event, and synthetic fur has reflectance characteristics unlike human skin.
| Generation | Method | Date |
|---|---|---|
| 1st | Match statistics to the background in LAB color space | Oct 2025 |
| 2nd | Teal & orange cinematic LUT — per-channel tone curves interpolated from five anchor points | Apr 2, 2026 |
| 3rd | Dynamic luma matching | Apr 4, 2026 |
What matters about the third generation is that it does not shift brightness linearly.
- Target luminance = subject L mean × 0.4 + background L mean × 0.6, clipped to
[80, 180] - Gamma exponent =
log(target_normalized) / log(current_normalized), clipped to[0.4, 2.5] - Apply that gamma to the L channel
A linear offset crushes shadows or blows out highlights. Gamma correction shifts overall luminance while preserving tonal gradation. Targeting a 6:4 blend rather than matching the background outright is an art-direction judgment — blend fully into the background and the subject dies. The two clips are guardrails against divergence on extreme inputs.
How it was validated. scripts/test_color_grading.py renders a four-way comparison —
original / 1st / 2nd / 3rd generation — from the same input. It substitutes a human-eye
regression test for a metric that does not exist, making it possible to check whether a new
algorithm is worse than its predecessor under some particular lighting.
Evidence: utils/mirror.py:106-184, scripts/test_color_grading.py,
commits ba9489b → 3e9355f → 845d41e
4.6 Cross-platform acceleration
cv2.UMat does not work on macOS, and scalar-UMat operations (1.0 - mask) fail on
Windows. Scatter that asymmetry across a codebase and one platform silently breaks.
Every call routes through an OpenCVUtils static-method layer, and direct cv2.UMat()
construction is codified as a prohibited pattern. PyTorch device selection falls back
MPS → CUDA → CPU.
| Windows | macOS | |
|---|---|---|
| OpenCV acceleration | cv2.UMat (OpenCL) |
np.ndarray |
| PyTorch | CUDA → CPU | MPS → CPU |
| Camera backend | CAP_MSMF |
CAP_AVFOUNDATION |
| Printing | Win32 API | lpr (CUPS) |
Evidence: utils/opencv.py, .agent/knowledge/cross-platform-patterns.md,
commits b8be00e, 62a6f4e
4.7 Externalizing tacit knowledge as machine-readable rules
With six threads sharing models, mask caches, and compositor layers, a judgment like "why does this variable not need a lock?" (answer: same-thread serialization) is not recoverable from the code. Not by me three months later, and not by an AI coding agent.
I wrote 1,371 lines into an .agent/ tree in the repository [Verified]:
| Directory | Contents | Lines |
|---|---|---|
knowledge/ |
Architecture, AI pipeline, threading model, cross-platform patterns | 512 |
skills/ |
SAM2 integration, thread safety, cross-platform OpenCV, compositor layers | 459 |
workflows/ |
Environment setup, running, adding a character, adding a camera, refactoring | 400 |
The threading-safety skill is written in a checkable form rather than a descriptive
one: four explicit prohibitions and a five-item checklist for adding new features.
Evidence: the .agent/ tree, commits 05ce587, 3bf6ddf, 358ab59
5. Product Iteration
5.1 The event was the release cycle
Classifying all 61 commits by their distance to an event date [Git history]:
| Event | D-day | D-30 to D-1 | D-day to D+7 |
|---|---|---|---|
| FUR:RAID 2025 | 2025-10-12 | 26 | 0 |
| HowlSoup 2026 | 2026-01-24 | 16 | 1 |
| FurryJoA 2026 | 2026-02-21 | 22 | 3 |
| FurstClass 2026 | 2026-04-04 | 2 | 1 |
All 61 of 61 commits fall inside a D-30 to D+7 window.
The base rate, stated up front. The repository spans 2025-09-21 to 2026-04-04, or 196 days; the union of the four D-30/D+7 windows covers 126 days (64%). Commits scattered at random would land in that window 64% of the time. The observed 100% is a meaningful concentration, but quoted alone the number reads more dramatic than it is.
Also, only three of the event dates are firmly established (FUR:RAID 2024 from a filename; FurryJoA and FurstClass 2026 from capture timestamps). The rest are inferred from archive file modification times and dated folder names. A ±3-day error does not flip the window classification, but the inference should be visible.
There is no such thing as "development during a quiet period" in this project. Every line of code was tied to the deadline of the next event, and that was the prioritization mechanism.
Honestly, this is a risky way to work. Nine commits landed on D-1 before HowlSoup 2026, and among them was an architectural swap from MODNet to SAM2. Had it failed, the booth would not have opened. Two things mitigated it: commits were split into individually revertible units, and a working previous version always remained. All seven events ran without incident — but I would not present this as good process.
5.2 What I chose not to build
Product judgment shows more clearly in what gets cut than in what gets added, and this repository has the evidence.
(a) I deleted the manual editing feature — commit d463b89, "remove manual editing"
Early on the operator could correct the mask by clicking. Accuracy improves, but it creates
operator time consumed per capture, which directly reduces throughput in a booth with a
queue. Once automatic detection (06c7856) proved good enough, the feature was removed
entirely. Lowering the accuracy ceiling to buy throughput.
(b) I added a depth camera and then removed it — e42dc1a (Oct 2025) → b0e64e7 (Feb 2026)
Covered in detail as CASE 2 in section 8.
(c) I did not insist on full automation — commit 9b7fe70
Each character has a different composition, so the subject's center position differs too.
Estimating it automatically means solving another problem. Instead, CHARACTER_CONFIG holds
per-character center_ratio_x/y constants. A problem that terminates in 9 × 2 = 18 numbers
did not need an algorithm.
5.3 The path from feedback to code
The event archives retain 194 files of post-event feedback [Verified].
| Event | Feedback files |
|---|---|
| FUR:RAID 2024 | 36 |
| HowlSoup 2024 | 28 |
| FurryJoA 2025 | 97 |
| HowlSoup 2026 | 10 |
| FurryJoA 2026 | 23 |
Commits record where that feedback turned into code:
| Surfaced in the field | Resulting commit | Timing |
|---|---|---|
| The venue has no internet | de9d569 offline mode |
HowlSoup 2026 D+0 |
| macOS picks up an iPhone as a camera | b3d2b61 exclude Continuity Camera |
FurryJoA 2026 D+2 |
| Composites look detached under hall lighting | 845d41e dynamic luma matching |
FurstClass 2026 D+0 |
| Crash while models load | 25f3b6a loading bug fix |
FurryJoA 2026 D+0 |
A commit timestamped on the event day means the problem was found while running the booth and fixed on the spot. These four are the most direct evidence of the field feedback loop.
5.4 Supply followed demand
Classifying the HowlSoup 2024 captures by character [Verified]:
| Rank | Character | Captures | Share |
|---|---|---|---|
| 1 | A | 42 | 31.1% |
| 2 | B | 27 | 20.0% |
| 3 | C | 25 | 18.5% |
| 4 | D | 25 | 18.5% |
| 5 | E | 10 | 7.4% |
| 6 | F | 6 | 4.4% |
The top two account for 51%; the bottom two together reach 12%. This distribution is not a capture log — it is the product metric that decides which assets deserve more production time.
The roster subsequently grew from 6 to 9, and every addition landed immediately before a
specific event (6443616 "new characters for howlsoup 2026"; a812ad2, one character
added at D-2).
5.5 The software constrains the artwork, and the artwork constrains the parameters
The layer structure in §3.4 only works if each character is drawn as two separate images,
front and rear. Once the composition is fixed, the segmentation center_ratio has to be
tuned to match it.
With a designer and an engineer as separate people, that round trip takes days. Working solo made the round trip zero, which is why a new character could ship two days before an event.
The cost is a real ceiling on quality. A professional illustrator would have produced better assets; a dedicated engineer would have produced a better structure. Working solo was a trade of quality ceiling for iteration speed.
6. Field Operations and Running a Brand
6.1 Seven events over three years, and the booth opened every time
| Event | Date | Captures [Ledger] |
|---|---|---|
| FUR:RAID | 2024-08 | 80 |
| HowlSoup | 2024-12 | 120 |
| FurryJoA | 2025-02 | 64 |
| FUR:RAID | 2025-10 | 200 |
| HowlSoup | 2026-01 | 100 |
| FurryJoA | 2026-02 | 64 |
| FurstClass | 2026-04 | 75 |
| Total | 7 events, 3 years | 703 |
On growth: FUR:RAID went from 80 (Aug 2024) to 200 (Oct 2025), a 2.5× increase. I make no causal claim. Event size, booth location, operating hours, and competing booths were all uncontrolled. What is certain is seven consecutive returns with the booth running every time.
6.2 Measured booth operations
For the two 2026 events the application auto-saved three files per capture, so operating
metrics come out directly [Verified]:
| Metric | FurryJoA 2026 | FurstClass 2026 |
|---|---|---|
| Captures | 63 | 75 |
| Operating window | 10:08 → 16:02 (5h 53m) | 08:29 → 16:22 (7h 53m) |
| Throughput | 10.7/hour | 9.5/hour |
| Inter-capture interval, p50 | 2m 13s | 2m 31s |
| Inter-capture interval, p90 | 16m 46s | 17m 25s |
| Peak hour | 15:00 (18) | 15:00 (22) |
What these numbers do and do not prove. The operating window is the span between the first and last capture timestamp. It proves the booth kept taking photographs across that span; it does not prove the program never restarted. A crash and relaunch partway through would produce the same range. Read it as "operating window," not "uninterrupted."
p50 of roughly 2.5 minutes is the real per-group service time — greeting, character selection, posing, capture, printing, handoff. When a queue exists, that is the system's throughput ceiling.
The gap between p50 and p90 (2.5 min vs 17 min) is not instability but variance in demand across the day. Both events are quiet right after opening and around lunch and peak at 15:00 — the same shape twice.
For FurstClass, the ledger count of 75 and the auto-saved 75 sets match exactly, which is a cross-validation of the ledger's reliability.
6.3 I made the booth branding myself
The event archives contain deliverables beyond software [Verified]:
Booth signage · menu boards · nameplates · four A3 posters · business cards · participation cards · display panels · kiosk button icons · per-character face icons · merchandise · photo-frame templates · a Korean-language operations manual
Tracking what was added year over year shows the booth becoming more refined: 2024 menu board → 2025 participation cards and panels → 2026 four A3 posters.
The character-selection button icons on the kiosk screen correspond to the
change_character() popup in main.py — evidence that the same person moved back and
forth between design and implementation.
6.4 Earning the right to the data
Fursuit photographs are assets the community feels strongly about in terms of ownership and likeness rights. Scraping them is technically trivial and permanently destroys trust.
Collection procedure [Ledger]
- An open call on Twitter within the furry fandom community
- Purpose stated up front — the AR photo booth, and potential future AI training for the furry / fursuit fandom
- Opt-in intake via Google Form, with donors submitting links to their own drives
- Booth participant photos are stored locally only and never leave the machine
Four things worth noting about this design
- Opt-in open call rather than scraping — legitimacy of provenance
- Future training use included in the stated purpose — purpose specification
- Separate handling tiers for donated data (training) and participant photos (service output)
- The local-only rule for participant photos coincides with the on-device inference architecture. Not using cloud APIs was a latency and cost decision and a privacy design — one architectural choice satisfying two requirements.
The result is a small dataset, but one whose right to use is unambiguous. What scraping would have accomplished in days took months; what it bought was community trust.
This document honors that policy too. Neither donated images nor participant photos appear here. The scarcity of result imagery is not a defect in the document — it is the policy applied to the document itself.
7. Quantified Impact
7.1 Summary
| Axis | Figure | Grade |
|---|---|---|
| Operations | 7 events over 3 years, 703 captures | [Ledger] |
| Uptime | Longest single-day run 7h 53m | [Verified] |
| Throughput | 9.5–10.7/hour, interval p50 2m 13s | [Verified] |
| Data | 9 donors / 1,289 images consent-sourced + 141 sets auto-collected | [Verified] |
| Development | All 61 commits inside an event D-30/D+7 window | [Git history] |
| Code | 3,436 lines of Python + 1,371 lines of agent knowledge docs | [Verified] |
| Expansion | 6 → 9 characters, new-character lead time D-2 | [Verified] |
7.2 Unit economics — from the ledger [Ledger]
The booth charged ₩5,000 (~$3.60) per print. The figures below are actual transactions recorded in the operator's event ledger, not industry estimates.
Scope — the photo booth segment only
The same booth also carried a separate product line (merchandise, acrylic goods, plushies). Its production costs and revenue are entirely excluded here. Assessing the photo booth's profitability requires matching only its own direct costs against its own revenue. What follows is the AR Photo Booth as a single segment.
Per-print economics
| Item | Amount |
|---|---|
| Price | ₩5,000 (~$3.60) |
| Paper cost (RP-54 pack, ₩27,500 / 54 prints) | ₩509 (~$0.36) |
| Contribution margin | ₩4,491 (~$3.20), 90% margin |
Fixed costs
| Item | Amount | Nature |
|---|---|---|
| Commissioned background artwork | ₩1,000,000 (~$715) | One-time. The AR composite background |
| Depth camera | ₩317,517 (~$225) | One-time. Removed in CASE 2 → sunk cost |
| Webcam (4K) | ₩206,520 (~$148) | One-time |
| Dye-sublimation photo printer | ₩186,990 (~$134) | One-time |
| Booth interior props | ₩73,000 (~$52) | One-time |
| One-time subtotal | ₩1,784,027 (~$1,275) | |
| Booth/event fees | ₩80,000 × 7 = ₩560,000 (~$400) | Per event. Shared cost with the merchandise line |
| Standing-sign reprints | ₩16,000 × 7 = ₩112,000 (~$80) | Per event. Photo booth direct cost |
The single largest expense was not technology. It was a drawing. The ₩1,000,000 commissioned background is 56% of one-time spend and equals 223 prints — 1.4× the entire AI hardware budget (₩711,027). Foundation models were free to use, but a background people actually want to stand in front of had to be paid for. In this project, technology was the cheapest component.
Break-even, under two allocations of the shared cost
| Allocation basis | Fixed cost | Break-even | vs. 703 cumulative |
|---|---|---|---|
| A. Direct costs only (event fee excluded as shared) | ₩1,896,027 | 422 prints | 60% |
| B. Full event-fee allocation (baseline, conservative) | ₩2,456,027 | 547 prints | 78% |
The photo booth consumed most of the booth's floor space and operating hours, so B is closer to reality, and this document uses B as its baseline — choosing the unfavorable basis rather than the flattering one.
Break-even took 2 years and 5 months, and five events.
| # | Event | Captures | Cumulative |
|---|---|---|---|
| 1 | 2024-08 FUR:RAID | 80 | 80 |
| 2 | 2024-12 HowlSoup | 120 | 200 |
| 3 | 2025-02 FurryJoA | 64 | 264 |
| 4 | 2025-10 FUR:RAID | 200 | 464 |
| 5 | 2026-01 HowlSoup | 100 | 564 ← crosses 547 |
| 6 | 2026-02 FurryJoA | 64 | 628 |
| 7 | 2026-04 FurstClass | 75 | 703 |
Looking only at per-event recurring cost (₩96,000 for fee plus signage), break-even is 21 prints. Since events ran 64–200 captures, every one of the seven comfortably covered its own recurring cost. In other words, each event was profitable from the first one; what took five events was recovering the upfront investment. That is the difference between "absorbing a loss each time and holding on" and "profitable each time, with a large initial outlay" — and it is the actual reason three years and seven events were possible.
The scrapped depth camera cost ₩317,517, or 71 prints. That is 45% of the AI hardware budget (₩711,027) and 18% of all one-time spend. It is the monetary size of the misjudgment described in CASE 2 — optimizing for accuracy without putting field operational risk into the decision. Without that purchase, break-even would have been 476 prints instead of 547 — one event sooner.
Cumulative P&L
Excluding print failures (under 5%), captures converted to paid prints essentially
one-for-one [Ledger]. Failed prints consume paper without revenue, which the conservative
scenario reflects.
Revenue is 668–703 paid prints × ₩5,000 = ₩3,339,000 – ₩3,515,000 (~$2,385–2,510); paper cost is 703–738 sheets × ₩509 = ₩358,000 – ₩376,000 (~$256–269).
| Allocation basis | Net (conservative) | Net (upper bound) |
|---|---|---|
| A. Direct costs only | +₩1,067,000 (~$762) | +₩1,261,000 (~$901) |
| B. Full allocation (baseline) | +₩507,000 (~$362) | +₩701,000 (~$501) |
Baseline net: roughly ₩507K–701K (~$360–500). That is what remains after running seven events across three years. Because print success exceeded 95%, the two scenarios differ by under ₩200K — with margins this thin, print reliability is the P&L. At a 20% failure rate the net would fall by more than half.
What was excluded, and why
| Excluded | Reason |
|---|---|
| Merchandise / acrylic / plushie production | Separate product line. It has its own revenue; mixing it in distorts the segment |
| Fursuit transport and travel incidentals | Not an incremental cost. I would have attended the convention regardless, so these were not caused by the booth |
| Labor | See below |
The photo booth segment's direct costs are closed as enumerated above. No further line items are outstanding.
Two caveats — read them alongside the numbers
- Labor is booked at zero. Product definition, engineering, asset creation, and on-site operation were all unpaid self-labor. Development alone amounts to 61 commits; price that at market rates and the net goes immediately negative. The ₩507K–701K remaining after three years is not compensation for labor — it is only the fact that cash flow was never negative.
- This booth was never intended to make money. The figures demonstrate that it sustained itself for three years without consuming my own money, not that it is a business. Sustainability is precisely what made seven consecutive events possible.
7.3 Opportunity-cost comparison (reference)
Unlike §7.2, this section is a comparison against alternatives, not actual spend. All assumptions are stated and figures are given as ranges.
① Commercial photo booth rental
7 events × ₩500K–800K = ₩3.5M–5.6M (~$2,500–4,000) worth of booth service replaced by building it myself.
Assumptions: one operating day per event, rental including unlimited printing and staff, VAT excluded. Rates are Korean public list prices (SimpleCube from ₩500K; photo kiosk rental ₩800K minimum).
Important limitation: commercial rentals do not provide per-character AR compositing, which is the core feature. This is therefore a reference figure, not an equivalent comparison. Requiring feature parity turns it into a custom development quote, which would be far larger but unverifiable, so it is not included.
② Eliminating post-production lead time
At an assumed 5–15 minutes per image for manual compositing (cutout, layer assembly, color correction), 703 images × 5–15 min = 59–176 hours.
Assumptions: a Photoshop-proficient operator working from prepared character layer templates; 5 min optimistic, 15 min conservative. This range is my own assumption, not a sourced figure.
The real point: the substance is not saved hours but the elimination of lead time itself. Manual post-production, however fast, cannot hand the participant a print on the spot. The value of this booth is not labor savings — it is making "walk away with it now" possible at all.
③ Marginal cost of adding a character
Adding a character to a physical set or prop-based booth incurs fabrication, transport, and
storage. Here it costs two PNGs and three config lines.
Evidence: commit a812ad2 added a character two days before an event and it ran
normally. A D-2 addition is not possible with physical props. The roster grew from 6 (2024)
to 9 (2026).
§7.2's segment direct costs are closed, so this section is the only estimated territory in the document.
8. Incidents and Structural Fixes
CASE 1 · General-purpose matting collapses on fursuits
Symptom — A matting model that worked well on people would severely clip the silhouette of fursuit wearers, or mistake background for subject.
Field impact — Output quality varied unpredictably between participants. Some were delighted, some disappointed — a direct hit to booth credibility.
Root cause — The five factors in §2.3(a). Nearly every cue the model relies on is invalidated.
Options and trade-offs
| Option | Upside | Downside | Decision |
|---|---|---|---|
| Solve with data (fine-tuning) | Strong domain accuracy | One person carrying the retraining, labeling, and versioning burden | Adopted first |
| Solve with architecture (drop the "person" prior) | Almost no maintenance; generalizes | Residual domain error | Adopted second |
| Solve physically (chroma key) | Reliable | Requires floor space, setup time, lighting control | Rejected |
Resolution — First, MODNet fine-tuning on community-donated images with a
human-in-the-loop verification cycle. Later, a switch to depth-based foreground separation
combined with SAM2 promptable segmentation (b7d9114).
Why I switched is the important part. Not because fine-tuning was inaccurate. Fine-tuning is not a decision to build one model — it is a decision to carry the entire apparatus of data collection, labeling, retraining, versioning, and donor relationships, alone, for three years. Every new costume type restarts that cycle. I chose not the more accurate option but the one a single person could sustain.
Prevention — The "person" prior was removed from the pipeline entirely so it generalizes
to any costume. Residual error is absorbed by 18 per-character constants (9b7fe70).
CASE 2 · Adding a depth camera, then removing it four months later
Symptom — The depth camera (e42dc1a, Oct 2025) genuinely did improve separation
accuracy. The problem was not accuracy; it was the field.
Field impact — Additional equipment to transport, USB bandwidth, per-OS drivers, keeping the rig aligned. And decisively, it was a single point of failure that would take the entire booth down.
Root cause — I adopted it looking only at accuracy and did not put field operational risk into the decision. That was the actual mistake.
Monetary size — The unit cost ₩317,517 (~$225), which is 45% of the AI hardware budget (₩711,027) and equals 71 prints. It was removed from the pipeline after four months, making it entirely sunk. Without it, project break-even would have been 476 prints instead of 547 — recovery one event sooner. The reason for recording the misjudgment as a number rather than a qualitative note is that remembering the size is what prevents the repeat.
Resolution — Fully replaced by Depth Anything V2 Small (b0e64e7, Feb 2026). The lost
accuracy was recovered through SAM2 hybrid prompting.
Prevention — Subsequent hardware decisions now begin with "does the booth stop if this device fails?" Trading accuracy to remove operational risk became an explicit criterion.
CASE 3 · Preview smoothness vs. capture accuracy
Symptom — Running the AI pipeline synchronously every frame stalled the preview. Slowing the cadence meant the mask at capture belonged to the previous pose, misaligning the saved frame.
Field impact — Either the screen stuttered while participants were posing, making adjustment difficult, or arms and ears were clipped in the output. Once it is printed, there is no undo.
Root cause — Two requirements on different time scales sharing one path. The preview needs continuous smoothness; the save needs single-moment accuracy.
Resolution — Asynchronous detection with double buffering, and a synchronous flush only
on capture (6837615). See §4.2.
Prevention — The shared-state protection contract for all six threads is documented in
.agent/knowledge/threading-model.md, with a new-feature checklist codified in
.agent/skills/threading-safety/ (3bf6ddf).
CASE 4 · Failure modes manufactured by the field
Symptom — ① No internet at the venue, so automatic model download fails ② macOS enumerates an iPhone as a Continuity Camera and the wrong device is selected ③ accidentally closing the button window makes the booth unusable ④ the print button is clickable while the printer is not ready.
Field impact — All of these occur while a queue is waiting. The booth stops in front of an audience.
Root cause — The development environment's implicit assumptions — internet available, one webcam, windows stay open, printer always ready — all break in the field.
Resolution
| Failure mode | Response | Commit |
|---|---|---|
| No network | Offline mode + model download split into a pre-run script | de9d569, cb375ba |
| Camera misidentification | Skip Continuity Camera when a USB camera is present | b3d2b61 (D+2) |
| Window closed | Detect close and recreate automatically | main.py:138-146 |
| Printer not ready | 3-second polling with button gating | main.py:561-579 |
Prevention — Each failure mode is absorbed by its own recovery path rather than
crash-and-restart. The added code complexity was offset by codifying the rules in
.agent/skills/. Three one-click launch scripts (run.command / run.bat / run.ps1)
provide a handoff path for an operator.
CASE 5 · Losing data the community entrusted to me — unresolved
Symptom — The open call brought in donations from 21 people. Only 9 (1,289 images) remain.
Root cause — I had the capability to collect but no policy to preserve. A free storage tier's capacity limit, no backup or lifecycle policy, no local mirroring at intake. Donor contact channels were not maintained systematically enough to re-request.
What was lost — 57% by donor count. Not merely files, but assets the community entrusted to me; recovering them means going back to the same people and asking again. That weighs more than the technical loss.
What I would do now
- Local mirroring at intake plus cold-storage duplication
- Checksum-based inventory
- A separate donation register (donor, quantity, intake date, scope of consent)
- Capacity threshold alerts
Prevention — unresolved. Even the surviving nine donors' data has no duplication yet. This is carried to the roadmap as the top priority.
9. Limitations and Roadmap
9.1 Known limitations
| Limitation | Detail |
|---|---|
| No quantitative quality metric | Segmentation accuracy was never measured via IoU or similar. Quality judgment was entirely visual. The 141 raw+mask pairs in photos/ now make measurement possible |
| Donated data not duplicated | CASE 5. The surviving nine donors' data still has no backup policy |
| Fine-tuning pipeline not preserved | Training code and configuration live outside the repository, so it is not reproducible |
main.py is a single 597-line class |
Application owns camera, AI, compositing, UI, and printing. A refactoring guide exists at .agent/workflows/refactor-module.md but has not been executed |
| No automated tests | scripts/test_*.py are comparison-image generators, not unit tests |
| Agent docs are stale in places | Some documents still reference an old branch name after a branch change |
| No operating metrics for 2024–2025 | Auto-save arrived in Feb 2026, so the first five events have no measured throughput |
| Confounds in the growth trend | Event size, booth location, and operating hours were uncontrolled, so capture growth cannot be attributed to system improvements |
9.2 Roadmap
Priority 1 — Data preservation (resolving CASE 5) Local mirroring plus cold-storage duplication, checksum inventory, a proper donation register.
Priority 2 — Quality measurement
Produce manual ground-truth masks for a subset of the 141 sets in photos/ and measure IoU
and boundary F1. Confirm the third-generation color harmonization improvement numerically
rather than visually.
Priority 3 — Structural cleanup
Separate main.py's responsibilities, add unit tests for the segmentation pipeline, refresh
the agent documentation.
Priority 4 — Feature expansion Multi-person capture (the current pipeline assumes a single centered subject), video/GIF output, participant-facing self-service UI.
10. Appendix — Tech Stack
Models
| Model | Role | Size |
|---|---|---|
| SAM 2.1 Hiera Tiny | Promptable segmentation (mask + points) | ~149 MB |
| Depth Anything V2 Small | Monocular depth → automatic prompt generation | HuggingFace snapshot |
| MediaPipe Selfie Segmenter / Pose Landmarker | Early experiments. Present in the download script but not imported by the current pipeline | — |
Runtime
Python 3.11 · PyTorch (MPS / CUDA / CPU auto-selection) · OpenCV (UMat/OpenCL) ·
transformers · NumPy · tkinter · uv
Platform support
| Windows | macOS | |
|---|---|---|
| OpenCV acceleration | cv2.UMat (OpenCL) |
np.ndarray |
| PyTorch | CUDA → CPU | MPS → CPU |
| Camera backend | CAP_MSMF |
CAP_AVFOUNDATION |
| Printing | Win32 API | lpr (CUPS) |
Hardware
4K USB webcam · dye-sublimation photo printer · laptop (GPU acceleration optional)
Repository layout
main.py Application (597 lines)
camera/ ICamera ABC + Webcam
utils/
depth.py DepthEstimator, rough mask, smart points (602 lines)
segment.py SAM2ImageProcessor, hybrid prompting (498 lines)
mirror.py MirrorCompositor, color harmonization (259 lines)
opencv.py UMat/numpy abstraction (149 lines)
printer.py Per-OS printer factory (250 lines)
scripts/ Model download; segmentation & color-grading comparison tests
asset/ Character front/rear layers + background + stickers (23 PNGs)
.agent/ knowledge 4 / skills 4 / workflows 5 (1,371 lines)
Notes on presentation
- Character names include characters owned by other people, so all are anonymized as labels.
- No third-party identifying information (donors, photographers) appears in this document.
- Donated images and booth participant photographs are excluded, as consent covered training use rather than publication.
- Dates are given to year-month precision only.