Full portfolio

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

  1. Executive Summary
  2. Problem Definition
  3. System Architecture
  4. Technical Highlights
  5. Product Iteration
  6. Field Operations and Running a Brand
  7. Quantified Impact
  8. Incidents and Structural Fixes
  9. Limitations and Roadmap
  10. 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.

  1. Estimate a depth map with Depth Anything V2 Small
  2. 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 range
  3. get_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
  4. Sample foreground points inside the rough mask with farthest point sampling, and fix eight image-border points as background
  5. 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.

  1. Apply Sobel to the depth map, take gradient magnitude, normalize to 0–1 (higher = more likely a true boundary)
  2. Prepare a morphologically smoothed (Close/Open) version of the mask
  3. Define a boundary band as the difference between dilate and erode, and blend only inside it: original × edge_weight + smoothed × (1 − edge_weight)
  4. 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 ba9489b3e9355f845d41e

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 ite42dc1a (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]

  1. An open call on Twitter within the furry fandom community
  2. Purpose stated up front — the AR photo booth, and potential future AI training for the furry / fursuit fandom
  3. Opt-in intake via Google Form, with donors submitting links to their own drives
  4. 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

  1. 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.
  2. 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 causeI 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 lost57% 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.