Inspiration

In Still: A Michael J. Fox Movie (2023, dir. Davis Guggenheim), Fox, diagnosed with Parkinson's at 29, describes a world that keeps getting smaller. The documentary is not about a man who wants to draw again. It is about someone losing, piece by piece, the ordinary physical acts that carry a personality: staying upright, speaking reliably, and holding a hand still enough to do what the mind is still perfectly capable of imagining. Speaking about his mind, Fox says "I love my mind, and I love the place it takes me", and then that he does not want that cut short. It lands so hard precisely because the intent is all still there. The body just stops transmitting it.

That gap between intact intent and an unreliable hand is the whole problem. Parkinson's has a documented handwriting symptom, micrographia, where writing becomes small and cramped and often shrinks further the longer you write, until it is hard to read. What gets lost with it is not art. It is signing your own name on a form, initialling a document at a bank, writing a birthday card in your own hand, completing a signature box on a website without asking someone else to do it for you. Those are the small, unglamorous acts of independence that a tremor quietly takes away, and every one of them now happens on a screen with a pen.

This is not a small population. An estimated 11.8 million people worldwide were living with Parkinson's disease in 2021, and the Global Burden of Disease study projects that it will more than double to 25.2 million by 2050, with population ageing accounting for roughly 89% of the growth. Essential tremor is more common still. It is the most common adult movement disorder, affecting around 7 million people in the United States alone and about 5.8% of everyone over 65, roughly one person in every seventeen at that age. Between the two conditions, tens of millions of people are holding a pen that no longer does what they tell it to, and that number is growing every year.

Drawing apps have had stroke smoothing for years, but it lives inside one application and was designed for artists' line quality, not for tremor. The person signing a PDF, filling a web form, or writing a note in OneNote gets nothing. We wanted the help to arrive everywhere the pen goes, without asking anyone to change the software they already use, or learn anything new, or announce their diagnosis to an app.

What it does

Tracey sits between the pen and Windows. It intercepts pen input before any application receives it, removes the tremor from the path in real time, and re-injects the cleaned stroke into the system. Because the filtering happens at the input layer rather than inside one program, it works in every app: Paint, OneNote, Photoshop, a signature box in a browser, a PDF form. Nothing to relearn, no app to replace, no plugin to install.

  • System-wide. One background process; every pen-aware app benefits.
  • Personal. A ten-second scribble measures how much your hand actually moves and picks the smoothing level to match. Three presets (Gentle, Balanced, Steadiest) let you override it at any time.
  • Unobtrusive. Mouse and keyboard pass through completely untouched. Only the pen is filtered. Global hotkeys toggle strength or quit from anywhere.
  • Proven in the apps people actually use. Verified end to end on real tablet hardware in Microsoft Paint and Adobe Photoshop. Photoshop needs nothing special from us: it takes the pen through Windows Ink, the same pathway everything else uses.
  • Honest about scope. Tracey reduces shake. It does not guess what you meant to draw.

Measured on 61 real Parkinson's patients' drawings, at the Steadiest setting:

4–8 Hz clinical tremor band removed 19.0%
Intended motion (<2 Hz) changed under 1%
Latency added at normal writing speed approx. 10 ms, of which 35 µs is our own processing

How we built it

The core is native C on the Windows pointer stack. Pen events are captured with RegisterPointerInputTarget(PT_PEN), filtered, and re-injected with InjectSyntheticPointerInput. Redirecting the raw pen away from the target app is what prevents a double-drawn stroke, and injected events carry a sentinel ID so we never filter our own output.

The filter is the one-euro filter (Casiez, Roussel & Vogel, 2012), an adaptive low-pass whose cutoff rises with pen speed, so it smooths hard when the pen is nearly still (where tremor dominates) and gets out of the way during fast intentional strokes. For a position \( x_i \) at time \( t_i \), the filter is a first-order low-pass:

$$ \hat{x}i = \alpha x_i + (1-\alpha)\hat{x}{i-1}, \qquad \alpha = \frac{1}{1 + \tau/T_e} $$

where the time constant \( \tau = 1/(2\pi f_c) \) is driven by a cutoff that adapts to the estimated speed, \( f_c = f_{c_{min}} + \beta|\dot{\hat{x}}_i| \). Tuning is exactly those two numbers: fmin (smoothing at rest) and beta (how fast it yields to intent).

The UI is Electron: a system tray app, a settings window with live sliders and a practice pad, and a calibration wizard that draws your scribble back to you as you make it.

The two halves are separate processes on purpose. The core must run privileged (Windows only grants system-wide pen capture, uiAccess, to a signed executable in a protected folder); the UI must not. They talk through small text files in %PROGRAMDATA%: the UI owns config.cfg, and the core writes status.cfg with a once-per-second heartbeat so the UI can tell a live core from a crashed one.

We validated against real patients, not our own hands. We used the UCI Parkinson Disease Spiral Drawings Using Digitized Graphics Tablet dataset (Isenkul, Sakar & Sakar, 2014): pen X/Y, pressure, grip angle, and timestamp, sampled at either 111 Hz or 143 Hz depending on the recording, while patients and controls trace a printed spiral on a Wacom Cintiq 12WX. We used the Static Spiral Test, where the target shape is fixed, so deviation from the intended spiral is measurable rather than guessed.

The distributed archive ships a second folder, "Improved Spiral Test", that is a second copy of the base set, so the widely quoted "115 drawings" figure double-counts. Hashing the files is not enough to catch it: five of its "Healthy" files are the same five control subjects' same drawings with a handful of extra trailing samples, so they are not byte-identical and survive the hash. We exclude the folder outright, which leaves 61 Parkinson's drawings and 15 controls, matching the dataset's own description of 62 Parkinson's and 15 healthy participants, one of whose Static-Spiral segments is empty. We then ran those real pen traces through the same one-euro filter the product uses, at the exact fmin/beta values behind the UI's preset cards, and measured the result in the frequency bands clinicians name.

Challenges we ran into

Our first architecture was impossible, and it took a while to prove it. We built the interception layer in Python. It ran, it logged, it did nothing. The cause turned out to be that a uiAccess process dies inside the Windows loader before any interpreted code executes. We proved it with a first-line boot log that never got written. uiAccess executables also cannot be launched directly at all, even from an elevated shell, which is why every diagnostic had been silent. The entire capture layer was rewritten in C.

A bug in our analysis invalidated every frequency claim we had. Each record in the spiral dataset is X, Y, Z, pressure, grip angle, timestamp, test ID. All four of our analysis scripts reshaped that into a smaller array and then read the grip angle column as the timestamp. The comment directly above the line said # x, y, t_ms; the index was simply wrong, in every script.

Nothing about the output looked broken, which is why it survived so long. It only became visible when we sanity-checked the implied sample rate: grip angle produced 3,500–65,000 Hz, and sometimes NaN, where the tablet's true rate is 111 or 143 Hz depending on the recording. Every frequency-domain result rested on that axis: the FFT bins, the 2 Hz split between intended motion and tremor, and the dt we fed our own filter.

We fixed it and re-derived everything. Our headline moved from 21.2% to 19.0%, and the un-filterable share from 95% to 92%. The conclusion held; the numbers moved. We then went back and corrected every document, comment, and figure that quoted the old ones, and we kept the sample-rate check as a permanent guard, because it catches this entire class of bug in one line.

We caught our own tremor-frequency detector scoring at or below chance, and then fixed it. This was never the filter; it was an optional readout we wanted to add on top, an FFT tracker that would tell you "tremor detected: 8.4 Hz". Tracey's smoothing does not depend on it and never did.

We tested it against the same 61 patients and 15 controls. It failed: AUC 0.36–0.40, at or below chance, meaning healthy controls tended to score higher. Its bootstrap intervals, [0.21, 0.51] and [0.25, 0.53], straddle 0.5 completely, so it was not reliably measuring anything.

Rather than quietly drop the feature, we went after the cause. A 128-sample window with only a linear detrend leaves a large 1–2 Hz residue from the natural curve of a spiral, and its mainlobe spills directly into the lowest bin of our 3–15 Hz search band, so the detector was confidently reporting the very drift it was supposed to ignore.

The fix was a 2.5 Hz high-pass ahead of the FFT, and it works. On synthetic signals, genuine tremor detection under heavy drift went from 0 of 6 to 4 of 6, with peak strength roughly doubling and no new false positives. On the patient data, the statistic we score it by (mean in-band peak strength across a drawing) went from AUC 0.395, at or below chance, to 0.754 (95% CI 0.64–0.86): above chance with the whole interval clear of it. Both the AUC and that interval regenerate from src/analyze_tremor_detect.py in the repo. That fix ships in the product today, where it seeds the experimental tremor notch.

Then we tested it on the task we actually ship, and that is where it earned its keep. Every number above comes from an offline spiral task at 111 Hz, but Tracey's calibration is a free scribble at the tablet's approx. 250 Hz, a different task at a different rate, and we had never measured it. So we built a full-rate trace recorder and captured 20 ten-second steady-hand scribbles: a hand with no tremor, where any frequency the detector reports is a false positive. The pre-fix detector claimed a tremor in 20 of 20, at a confident approx. 4.0 Hz, which sits squarely inside the Parkinson's rest-tremor band. With the high-pass and floor-bin rejection it ships with today, that falls to 3 of 20, and the share of analysis windows carrying a false peak drops from 95.8% to 6.7%.

We then tried to tune it further, and reported the result even though it was negative. We swept 1,152 combinations of window length, band edges, high-pass cutoff, and gate threshold under repeated 5-fold cross-validation. The best combination on the full dataset reaches AUC 0.815, but that gain is an artifact of choosing on your test set. Cross-validated, tuning scores 0.749 against the shipped defaults' 0.751: a difference of −0.002, which is nothing. We kept the defaults, and now we can say why rather than assume it.

We still show no frequency in the UI, and the live data is what settles it. Three false positives in twenty is a 15% chance of telling someone with a perfectly steady hand that they have a 5.2 Hz tremor, and 5.2 Hz reads as clinical. Separating two groups on a strength statistic is also a different claim from telling one person their number.

Then we injected synthetic tremor of known frequency onto those real scribbles to find the detector's floor, and the result closed the question. At the window size we ship, the frequency bins are 1.97 Hz apart, and the lowest is discarded as drift, so the detector cannot name any tremor between 3.5 and 5 Hz at any amplitude, which is most of the Parkinson's rest-tremor band. Doubling the window fixes that and costs everything: false positives on steady hands go from 3-in-20 to 20-in-20. There is no setting that both sees the tremor and stays quiet on a hand that does not have one. That is structural, not a threshold we failed to find. We have the control half of the live measurement now; the missing half is patients recorded on the same task, which is what would let us set a threshold instead of guessing at one. A number we cannot defend is worse than no number, so the readout stays off until we can.

Distribution nearly undid the accessibility. Getting uiAccess requires a signed binary in C:\Program Files, which means a certificate in the machine's trust store, an administrator prompt, and a SmartScreen warning: a wall of alarming dialogs for exactly the users least well served by alarming dialogs. We wrote a plain-language consent page into the installer explaining what it installs and why before it writes anything, and made the uninstaller remove the certificate again.

Accomplishments that we're proud of

We measured it on real patients and published what it can't do. 61 Parkinson's patients and 15 controls. We removed 19.0% of the 4–8 Hz clinical tremor band while changing intended motion by under 1%. Many smoothers buy their smoothness by eating the stroke; ours does not. We also say plainly what that does not mean: Tracey is a filter, not a classifier, and it removes a similar proportion of that band from a steady hand. What differs is how much there is to remove: a Parkinson's drawing misses its intended spiral by 3.5× as much.

We proved the honest limit of our own approach. 92% of the deviation in a Parkinson's spiral is slow, sub-2 Hz drift that overlaps intended motion. No real-time filter can separate that from intent without knowing the target shape. So we say Tracey reduces shake, never that it straightens lines, and we published the number that proves the distinction.

It genuinely works in every app, including Microsoft Paint and Adobe Photoshop, on real tablet hardware, not a canvas we wrote ourselves.

It is reliable at the input layer, where a dropped event is a broken stroke. Our last end-to-end run on a cold machine (one with our certificate absent from its trust store, exactly the state a stranger's PC is in) went: installer establishes trust → Windows grants uiAccess → pen captured → 19,656 pointer injections, zero failures, never once re-filtering its own output, at 30 µs per injection. Tremor compensation that drops one stroke in a thousand is not usable for signing a form.

Every hand is different, and we measured how different. Across the 61 patients, tremor amplitude in the 4–8 Hz band spans 0.199 px to 8.485 px, a 43× range. That is the entire argument for calibrating to the individual instead of shipping one smoothing level: the setting that helps the most affected person is heavy-handed for the least, and a single fixed default cannot serve both.

It installs like a normal Windows program. One signed installer, a desktop shortcut, and a clean uninstall.

What we learned

Check your time base before you trust a frequency. One wrong column index produced months of confident, wrong numbers. Every frequency-domain claim rests on the timestamp, and nothing about the output looked broken.

Test the honest version of your metric. The persistence rule for our tremor detector was tuned on the spiral dataset, where it looked disciplined: it called a tremor on none of the controls. Then we recorded the task we actually ship, a free scribble at 250 Hz, from a hand with no tremor. It was called a tremor in 20 recordings out of 20, on 95.8% of analysis windows. Validating on the task you shipped, not the task you had data for, is the whole ballgame.

Privilege boundaries shape architecture. Nearly every hard problem (why Python was impossible, why the UI can't stop the core, why calibration runs in-process, why config lives in a text file) traces back to one process being elevated and the other not.

Accessibility work is mostly the unglamorous parts. The filter is a page of math. The work was the UAC prompt, the certificate consent page, the pen-lift detection, and making sure a crashed core can't leave the UI claiming everything is fine while the pen runs unfiltered.

What's next for Tracey

  • Live user testing. Our numbers come from recorded patient drawings. The clearest next step is putting Tracey in the hands of people with tremor and measuring what actually helps.
  • An intent model. The 92% we cannot filter is the real research problem: inferring the shape someone meant to draw. That is what would turn a shaky line straight.
  • A properly signed release so no one has to click past SmartScreen.
  • macOS and Linux, and touch input.
  • A user-facing tremor reading. The high-pass took our detector above chance (AUC 0.754, 95% CI 0.64–0.86) and cut its false-positive rate on the live task from 20/20 to 3/20, which is what makes this worth finishing rather than abandoning. The missing half is patients recorded on that same live scribble: we now have twenty control recordings and zero patient ones. With both we could set a defensible threshold instead of guessing, and we already know what a threshold tuned on the wrong task is worth.

Built With

Share this project:

Updates

Submission history