← back
Air Writing
github.com/daverlon/IMU-Air-Writing-Demo
Python · PyTorch · Kotlin · nRF52840 · BLE · IMU · Madgwick · EMNIST · HCI
Overview
Hands-free letter input from the same wrist-worn 6-axis IMU as Gesture Band. Firmware already runs Madgwick fusion and streams roll / pitch / yaw with the raw accel/gyro. The host treats orientation as a hinge: a virtual pen tip of fixed length is rotated with the wrist, a writing page is locked in space, and the tip is orthographically projected onto that plane. The resulting polyline is rasterised to 28×28 and classified by a small CNN trained on public EMNIST Letters — not on IMU windows.
Accel is never double-integrated. There is no ZUPT / INS position estimate. Path on the page is length × Δangle; speed used for ink gating is the Euclidean derivative of the smoothed 2D point. The Python demo (page_projection.py) is the reference pipeline. The same projection is used in the Kotlin field-service checklist, with navigation gestures still driving UI and air-written letters filling text fields.
Air writing pipeline demo
The clip shows the Python projection demo (virtual tip, locked page, 28×28 raster, letter commit), then the same pipeline in the Kotlin field-service app, with wrist gestures for navigation.
Failed approach: letters as gesture classes
Gesture Recording Studio already trains a compact classifier on 240 ms windows (24 samples at 100 Hz) with nine hand-crafted features (gyro/accel std, range, axis dominance) and multi-class logistic regression. That works for four mutually exclusive navigation bursts. Treating A–Z as more of those classes did not:
The fix is representational. Map the wrist into the ink domain handwriting models already occupy, then reuse a public 28×28 glyph classifier. The same projection then works for other alphabets or symbols by swapping only the recogniser.
Geometry
Each BLE sample carries Euler angles from Madgwick. They are converted to a quaternion with a pitch sign flip so the band frame matches the sketchbook pose (forearm bent, writing toward the other palm). The local +X axis is rotated to a unit pointing vector f and a tip p = R(q) · (L, 0, 0) with L = 0.16 m in the Python demo (0.28 m in the Kotlin 3D preview, so smaller wrist angles still span a readable glyph).
A page is a plane with origin at the tip and normal along f at lock time. Basis is gravity-upright: world up (0,0,1) is projected out of f; if the residual is shorter than 0.25 (forearm nearly vertical) a horizontal fallback is used, then right = up × f and up is re-orthogonalised. Instantaneous page coordinates are
x = −(p − origin) · right
y = (p − origin) · up
Position is EMA-smoothed toward that target (α = 0.30) so ink and speed share the same lag; speed is a second EMA (α = 0.55) of ‖Δxy‖ / Δt with Δt clamped to 4–200 ms. A 1.15 gain slightly enlarges the glyph without changing the speed gate. After lock, origin and forward are frozen for the stroke so later wrist roll cannot warp the letter.
Ink gate, page re-aim, trim
Usability was mostly a state machine on page speed, not more model capacity. Thresholds are in metres/second on the projected point:
A secondary segmenter can cut at velocity minima (speed falling to 50% of the current peak, then recovering above the stroke threshold, ignoring dips shorter than 80 ms / 5 points) for multi-stroke letters. Letter gap is 260 ms still; word gap 1200 ms. Live commit in the demo currently treats one pause-delimited polyline as one glyph.
Rasterisation
The polyline is drawn into a 224×224 buffer (8× the 28×28 output), centred, aspect-preserving, with padding 0.12–0.20 of the frame. Gaps ≥70 ms or ≥2.2 cm are pen-ups and are not connected. A 0.8 px Gaussian blur and bilinear downsample produce an EMNIST-like white-on-black glyph; y-up page coordinates are flipped to image y-down. Ink thickness is scaled so the downsampled stroke roughly matches EMNIST pen width.
Classifier
LetterCNN is trained on EMNIST Letters (A–Z), images transposed to upright, 40k train samples, Adam 1e−3, 3 epochs, CPU. Architecture:
Air-written glyphs are slanted and uneven relative to pen EMNIST. At inference each stroke is rendered as a 5×3×2 grid of views — rotations {−15, −8, 0, +8, +15}°, ink weights {0.7, 1.0, 1.4}, paddings {0.12, 0.20} — 30 tensors in one batch. Softmax is averaged across views, not maxed (max lets one unlucky high-confidence wrong view win). On 53 held-out air-written letters that average was worth ~4 points; widening rotation past ±15° lost accuracy again.
A shape prior then damps classes that cannot match the bounding-box aspect width/height. Below 0.36, only I/J keep full mass; T/E/F are halved; everything else is crushed to 0.12. Between 0.36 and 0.58, wide letters H/M/N/W/U are damped. A 0.20 threshold was too tight: a real I in “DAVID” landed at 0.31 and lost to L.
Averaging flattens scores: a correct letter sits around 0.59, a wrong one around 0.34. Live commit therefore uses 0.34 as a floor, not as “sure”.
Commit policy and lexicon
Inference runs every 100 ms once the stroke has ≥16 points. Commit requires the hand below the hold-ink threshold, a prediction newer than the still event, confidence ≥0.34, and top-1 minus top-2 ≥0.05. Three consecutive agreeing letters must hold for 400 ms, or 550 ms if the guess is in ILCJPT (stems of F/E/R/T — locking early turns an F into an I). If the margin is too tight the hold timer resets. After commit the page resets (~1.5 s) so the next letter does not inherit the last pose.
A prefix lexicon (system word list plus a small domain set) then reweights the 26-way distribution by how often each letter continues the current stem. Illegal continuations are multiplied by 0.28, not zeroed, so a novel word can still win if the shape is overwhelming. If confidence ≥0.50 and the top-two margin ≥0.12, the lexicon is skipped.
A 28×28 preview of the raster and a short 880 Hz ding on commit made timing obvious. False positives dropped more from that feedback and the hysteresis gate than from extra EMNIST epochs.
Hardware and transport
System design
Technical challenges
What I learned
Current status
Future work