<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Projects |</title><link>https://changshenghsun.com/projects/</link><atom:link href="https://changshenghsun.com/projects/index.xml" rel="self" type="application/rss+xml"/><description>Projects</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Sun, 19 May 2024 00:00:00 +0000</lastBuildDate><image><url>https://changshenghsun.com/media/icon_hu_d41247041d33f7ba.png</url><title>Projects</title><link>https://changshenghsun.com/projects/</link></image><item><title>GridStage</title><link>https://changshenghsun.com/projects/gridstage/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://changshenghsun.com/projects/gridstage/</guid><description>&lt;p&gt;&lt;strong&gt;2026 — in active development&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="why-i-built-this"&gt;Why I built this&lt;/h2&gt;
&lt;p&gt;As a dancer, formations were always the part nobody wanted to handle. How do you
place everyone so the stage actually looks right? Will two people run into each
other halfway through a transition? Those questions bothered me every time we
put a piece together. I went looking for an open-source tool and there was
none — and I was not about to pay a subscription for the commercial ones. So I
built &lt;strong&gt;GridStage&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="what-it-is"&gt;What it is&lt;/h2&gt;
&lt;p&gt;A top-down 2D stage where you place performers, add formations along a timeline
synced to the music, and preview the walk-in transitions in 2D or 3D. It exports
walk-chart PDFs (one page per dancer), videos, and GIFs. Several people can edit
the same piece at once — live cursors, presence, and undo that only reverts your
own changes — over a CRDT, a data structure where concurrent edits merge without
a central referee.
The part I care most about is the vision pipeline: pause a rehearsal video on a
formation, click the four stage corners once, and the app detects the dancers,
maps their foot positions onto the stage floor, and drops them into the editor
as a normal editable formation. All of it runs in the browser.&lt;/p&gt;
&lt;h2 id="where-it-stands"&gt;Where it stands&lt;/h2&gt;
&lt;p&gt;v0.8.6 · 130 commits · ~16k lines of strict-mode TypeScript in a pnpm monorepo ·
24 unit-test files plus a Playwright end-to-end suite. Three ways to run it, none
requiring an account: a Windows/macOS desktop app, an installable PWA that works
offline on a phone, and a Docker self-host stack.&lt;/p&gt;
&lt;p&gt;Measured, not claimed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;60 performers × 4 formations&lt;/strong&gt; in a permanent end-to-end stress test —
dragging stays accurate and playback holds ~57 fps (CI floor 20).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Video → formation capture&lt;/strong&gt; runs entirely client-side (YOLOX-nano through
onnxruntime-web, WebGPU with a wasm fallback): 6 of 7 dancers recovered from a
real six-dancer rehearsal photo, ~1.8 s per frame on CPU alone. Zero server
cost, and no one&amp;rsquo;s rehearsal footage leaves their laptop.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Against &lt;strong&gt;ArrangeUs&lt;/strong&gt;, the category leader, and the other commercial formation
apps I audited before starting, these are the things they do not do at all
(checked July 2026; their feature lists do move):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;GridStage&lt;/th&gt;
&lt;th&gt;ArrangeUs &amp;amp; other commercial apps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real-time multi-user editing&lt;/td&gt;
&lt;td&gt;CRDT (Yjs) — live cursors, presence, per-user undo&lt;/td&gt;
&lt;td&gt;one editor at a time, files passed around&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formations captured from a rehearsal video&lt;/td&gt;
&lt;td&gt;on-device detection + homography&lt;/td&gt;
&lt;td&gt;nothing on the market does this&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rehearsal-vs-plan review&lt;/td&gt;
&lt;td&gt;per-dancer drift, C-position centering, left–right asymmetry&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transition collision warnings&lt;/td&gt;
&lt;td&gt;Hungarian matching + sweep-line crossing detection&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3D preview&lt;/td&gt;
&lt;td&gt;camera presets + follow-a-performer&lt;/td&gt;
&lt;td&gt;limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video / GIF export with music&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price and licence&lt;/td&gt;
&lt;td&gt;free, MIT, self-hostable&lt;/td&gt;
&lt;td&gt;subscription, closed source&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The place I am clearly behind is distribution, not capability: ArrangeUs has
457k+ users and years of tutorials, GridStage has an install page.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Building a user community.&lt;/strong&gt; The capability gap is closed; the distribution
one is not. Demo videos and an in-app guide are already in progress, and the
next step is getting the editor in front of real dance teams and cheer squads
and running on their feedback — the work that decides whether GridStage stays
a portfolio piece or becomes a tool people actually choreograph with.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accounts and cloud sync.&lt;/strong&gt; Today a piece lives on one machine and travels as
a &lt;code&gt;.gridstage&lt;/code&gt; file. This is the largest remaining gap against commercial
tools, and the one architectural decision I still owe the project — self-host
or a hosted backend.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full rehearsal-vs-plan deviation report.&lt;/strong&gt; The first cut compares one paused
frame against the plan. Scanning a whole rehearsal into a per-formation error
timeline is the version that closes the plan → rehearse → correct loop, and
it is the hardest part for anyone to copy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stage lighting and cue sheets.&lt;/strong&gt; Colored lights on the 2D stage and a 3D
wash, cues on the timeline, exported as a printable cue sheet — the other half
of what a stage manager actually needs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugins and a community template library&lt;/strong&gt;, so formation libraries can grow
without me writing every one of them.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>MakeNTU</title><link>https://changshenghsun.com/projects/makentu/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://changshenghsun.com/projects/makentu/</guid><description>&lt;p&gt;&lt;strong&gt;May 8–10, 2026&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="the-brief"&gt;The brief&lt;/h2&gt;
&lt;p&gt;AUO makes display panels, and the sponsor prompt came down to a single
question: if the windshield itself becomes the display, what is actually worth
putting on it? The easy answer is a bigger speedometer. We treated it as an
interface problem instead — the glass should show what the driver&amp;rsquo;s current
state calls for, and stay out of the way otherwise.&lt;/p&gt;
&lt;h2 id="what-we-built"&gt;What we built&lt;/h2&gt;
&lt;p&gt;A first-person driving simulator that runs entirely in the browser, built on
real map data: the Xinyi district road network pulled live from OpenStreetMap
Overpass, routes from OSRM, and procedurally generated buildings filling the
blocks in between. Four scenarios ship — city, highway, coastal drive, and a
parked rest stop.&lt;/p&gt;
&lt;p&gt;The windshield is composed in four layers, so the result reads as glass rather
than as a game overlay:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What is in it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 · Glass&lt;/td&gt;
&lt;td&gt;Vignette, film grain, top tint, A-pillars and mirror silhouettes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 · HUD&lt;/td&gt;
&lt;td&gt;Frosted DOM/SVG panels — speed, compass, nav card, ETA, climate, now playing, minimap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 · AR&lt;/td&gt;
&lt;td&gt;POI cards projected into screen space onto building facades, fading when occluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 · World&lt;/td&gt;
&lt;td&gt;Three.js — roads, buildings, cabin, traffic agents, route arrows, destination pillar&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;And the subsystems that make it more than a renderer:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Subsystem&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Driver monitoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MediaPipe FaceLandmarker blendshapes feed one state machine with two outputs. Hard warning: both eyes shut past 1.2 s, cooldown-gated so it does not machine-gun at a driver who is genuinely asleep. Continuous 0–1 fatigue score: rolling PERCLOS (a one-minute EMA of eyelid closure) combined with yawn rate — a jaw held open past 0.55 for 800 ms counts as a yawn, over a two-minute window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fatigue-driven HUD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The score is the interesting half. As fatigue climbs the HUD raises contrast on rear traffic, sheds non-essential chrome, and drifts wake-up particles across the glass — the display responds to the driver instead of shouting at them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Head-pose lane-change intent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Looking more than 18° to one side for 800 ms reads as a shoulder check, and the panel surfaces rear traffic in that adjacent lane. Releases at a looser 8° with a 500 ms cooldown — hysteresis, so a brief glance forward does not flicker the panel on and off.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Door-opening safety gate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In rest mode the car is parked roadside with traffic flowing past. Asking to get out forward-simulates the traffic schedule: if any vehicle will come within 12 m of the door in the next 4 s the gate stays red, otherwise a tap confirms.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Voice + AI reservation call&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Speech recognition is always listening. Say 「找餐廳」and five nearby POIs come back from Overpass; pick one by number and the in-car assistant dials it, speaking a reservation script over TTS while the overlay renders the transcript as chat bubbles. Phone numbers carry a separate spoken form so Chinese TTS reads them digit by digit instead of turning 09 into a place value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Four-screen cabin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each screen runs the same app with &lt;code&gt;?role=driver / passenger / left / right&lt;/code&gt;. The driver publishes state snapshots to a small WebSocket relay that fans them out unparsed; each role applies its own camera offset — passenger 0.45 m inboard of the right door, side windows yawed ±90° — and keeps its own mouse-look on top, so a passenger can still turn their head independently.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Physical steering wheel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You drive it with an actual wheel. Steering angle comes off a six-axis IMU (three-axis accelerometer plus gyroscope) mounted on the wheel itself, so the rotation is read directly from how the wheel moves — no shaft encoder or mechanical end-stops to fabricate in three days. An Arduino Nano packages the reading as one JSON line per packet at 115200 baud, roughly 20 packets a second, which the browser reads through Web Serial. Every value is clamped on arrival so a noisy reading cannot push the vehicle physics past its own caps, and the 60 fps render interpolates between packets rather than stepping.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;
&lt;figure id="figure-demoing-to-the-judges--hands-on-the-laser-cut-control-rig-the-simulator-running-on-the-screen-in-front-of-me"&gt;
&lt;div class="flex justify-center "&gt;
&lt;div class="w-full" &gt;
&lt;img alt="Demoing the simulator to the judges with hands on the laser-cut control rig"
srcset="https://changshenghsun.com/projects/makentu/judging-demo_hu_ba6de5e9835c4b8.webp 320w, https://changshenghsun.com/projects/makentu/judging-demo_hu_769bc8dca1b8db81.webp 480w, https://changshenghsun.com/projects/makentu/judging-demo_hu_cbdeeb749b3a6359.webp 760w"
sizes="(max-width: 480px) 100vw, (max-width: 768px) 90vw, (max-width: 1024px) 80vw, 760px"
src="https://changshenghsun.com/projects/makentu/judging-demo_hu_ba6de5e9835c4b8.webp"
width="760"
height="507"
loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;figcaption&gt;
Demoing to the judges — hands on the laser-cut control rig, the simulator running on the screen in front of me.
&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="my-role"&gt;My role&lt;/h2&gt;
&lt;p&gt;I was the team lead. The sponsor prompt named a display, not a product, so most
of the work up front was deciding what a windshield should actually do — I ran
that ideation with the team, and then led the software side through to a working
demo. My own commits landed the highway, coastal and rest-stop scenarios, the
driver-monitoring pipeline and everything the fatigue score drives, collision
handling, the head-pose lane-change intent, and the exit gate.&lt;/p&gt;
&lt;p&gt;Roughly 20k lines of TypeScript went in over the three days, with
194 unit tests across 16 files covering the pure logic — the state machines,
geometry and traffic simulation are all written as pure functions precisely so
they could be tested at hackathon speed instead of debugged through the UI.&lt;/p&gt;
&lt;p&gt;
&lt;figure id="figure-team-37-nothingunique--劉允中-賴泓安-鄭竣陽-胡軒綸-張勝勛--at-our-bench-on-the-auo-track"&gt;
&lt;div class="flex justify-center "&gt;
&lt;div class="w-full" &gt;
&lt;img alt="The five-person team at our bench during the hackathon"
srcset="https://changshenghsun.com/projects/makentu/team-bench_hu_c3d440bc2ec8763f.webp 320w, https://changshenghsun.com/projects/makentu/team-bench_hu_c0affe0bea0380a6.webp 480w, https://changshenghsun.com/projects/makentu/team-bench_hu_1461225f7144b0f0.webp 760w"
sizes="(max-width: 480px) 100vw, (max-width: 768px) 90vw, (max-width: 1024px) 80vw, 760px"
src="https://changshenghsun.com/projects/makentu/team-bench_hu_c3d440bc2ec8763f.webp"
width="760"
height="507"
loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;figcaption&gt;
Team 37, NoThingUnique — 劉允中, 賴泓安, 鄭竣陽, 胡軒綸, 張勝勛 — at our bench on the AUO track.
&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;p&gt;First place, AUO corporate award.&lt;/p&gt;</description></item><item><title>RecoveryDAgger</title><link>https://changshenghsun.com/projects/recoverydagger/</link><pubDate>Mon, 01 Sep 2025 00:00:00 +0000</pubDate><guid>https://changshenghsun.com/projects/recoverydagger/</guid><description>&lt;p&gt;&lt;strong&gt;Sep – Dec 2025&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;p&gt;Imitation learning lets an agent pick up complex behaviour from expert
demonstrations, but plain behaviour cloning drifts. Small errors push the agent
into states the expert never demonstrated, and from there the mistakes compound.&lt;/p&gt;
&lt;p&gt;DAgger fixes the drift by querying the expert on the states the learner actually
visits. That works, but it asks constantly — and expert supervision is the
expensive part of the whole setup.&lt;/p&gt;
&lt;h2 id="what-we-built"&gt;What we built&lt;/h2&gt;
&lt;p&gt;RecoveryDAgger keeps DAgger&amp;rsquo;s interactive loop but adds a cheaper first response
to trouble. In a risky state the agent does not call the expert straight away.
It first runs a &lt;strong&gt;recovery policy&lt;/strong&gt; that corrects itself locally by ascending the
gradient of a learned &lt;strong&gt;Success Q-function&lt;/strong&gt; — a network that estimates the
probability the task will still be completed from the current state. The expert
is queried only when recovery is unreliable, which is to say only when the state
is genuinely novel.&lt;/p&gt;
&lt;p&gt;The point is to spend expert supervision on new information, rather than on
situations the agent could have dug itself out of.&lt;/p&gt;
&lt;h2 id="my-role"&gt;My role&lt;/h2&gt;
&lt;p&gt;I led a four-person team and built the expert policy and the training pipeline
myself, in PyTorch and Stable-Baselines3. The expert is a rule-based controller
that steers toward the goal from the current state — deterministic, 100% success
on the task — and it generates the 100-trajectory offline dataset every method
in the paper starts from. Behaviour cloning on that dataset lands at roughly 26%
success, which is exactly the gap online training has to close.&lt;/p&gt;
&lt;p&gt;The design decision I care most about is how recovery actually produces an
action. PointMaze has a continuous action space (forces along x and y), so the
obvious version of recovery — enumerate candidate actions, take the arg max of
the Success Q-function — is not available. We instead start from the action the
imitation policy already proposed and run a few steps of gradient ascent on the
Success Q-function with respect to the action, projecting back into the valid
range after each step. Recovery is a local correction of the policy&amp;rsquo;s own
intent, not a second controller fighting it.&lt;/p&gt;
&lt;p&gt;The other decision was to stop trusting a single Q-network. One learned Success
Q is only an approximation, and recovery follows its gradient, so its errors get
amplified rather than averaged out. We trained an ensemble instead: the ensemble
mean defines risk, the ensemble variance defines novelty, and the recovery
objective subtracts a penalty λ·Var(Q) so recovery never climbs toward states
the ensemble disagrees about. One family of networks ends up serving all three
roles — risk detector, novelty detector, and recovery direction.&lt;/p&gt;
&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;p&gt;Evaluated on a four-room PointMaze navigation task against ThriftyDAgger, the
standard query-efficient baseline. Every method starts from the same
behaviour-cloned policy.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Expert queries&lt;/th&gt;
&lt;th&gt;Recovery queries&lt;/th&gt;
&lt;th&gt;Success rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Behaviour cloning&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0.26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ThriftyDAgger (baseline)&lt;/td&gt;
&lt;td&gt;20,048&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-Q recovery (ours)&lt;/td&gt;
&lt;td&gt;2,504&lt;/td&gt;
&lt;td&gt;9,232&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ensemble-Q recovery (ours)&lt;/td&gt;
&lt;td&gt;2,693&lt;/td&gt;
&lt;td&gt;9,296&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Ensemble-Q recovery ends up more accurate than the baseline while querying the
expert &lt;strong&gt;7.4× less often&lt;/strong&gt;. Partway through training the gap is even wider: both
recovery variants reach ~0.7 success after about 2k expert queries, where
ThriftyDAgger needs roughly 20k to get to the same place.&lt;/p&gt;
&lt;p&gt;The interesting part is &lt;em&gt;where&lt;/em&gt; the savings come from. Most of what a risk-gated
method spends its query budget on is not genuinely unfamiliar — it is
recoverable: local deviations near a doorway or a wall that the agent could fix
by itself. Separating &amp;ldquo;novel&amp;rdquo; from &amp;ldquo;merely risky&amp;rdquo; is what buys the 7.4×.&lt;/p&gt;
&lt;p&gt;The project placed 3rd among 30 teams, most of them graduate students, and the
work has been accepted to IEEE Potentials.&lt;/p&gt;</description></item></channel></rss>