<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nariaki Wada</title>
    <description>The latest articles on DEV Community by Nariaki Wada (@kiarina).</description>
    <link>https://dev.to/kiarina</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3328007%2Ff28ae1f1-a65a-4aaa-bc43-8ae5c53d997d.png</url>
      <title>DEV Community: Nariaki Wada</title>
      <link>https://dev.to/kiarina</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kiarina"/>
    <language>en</language>
    <item>
      <title>Estimating the Side of Sudden Sounds in Unreal Engine 5.8: 35/35 Detected, 97.14% Side Accuracy</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Sat, 25 Jul 2026 11:28:30 +0000</pubDate>
      <link>https://dev.to/kiarina/estimating-the-side-of-sudden-sounds-in-unreal-engine-58-3535-detected-9714-side-accuracy-c93</link>
      <guid>https://dev.to/kiarina/estimating-the-side-of-sudden-sounds-in-unreal-engine-58-3535-detected-9714-side-accuracy-c93</guid>
      <description>&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;Even in a noisy place, people can quickly direct their attention toward a sudden sound. Giving a game agent a similar input becomes more interesting when it cannot inspect each sound source directly and must decide “something happened” and “it came from the left” using only the audio that reached its ears.&lt;/p&gt;

&lt;p&gt;Today, I mix 40 ms transients into continuous ambient sound in Unreal Engine 5.8, then detect them, estimate their side, and extract candidate clips using only post-HRTF stereo PCM. In a short fixed-listener demo, the system detected all 35 events and classified 34 of 35 sides correctly, for &lt;strong&gt;97.14%&lt;/strong&gt; accuracy. Detection latency ranged from 20 to 60 ms, with a median of 20 ms.&lt;/p&gt;

&lt;p&gt;This result needs an important qualification: it predates the movable listener and comes from a short demo. The five-minute negative test and SI-SDR measurement of extraction quality have not been completed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxzharmgvqygvr7ludeve.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxzharmgvqygvr7ludeve.webp" alt="MixedAudioAttention running in Unreal Engine 5.8, showing the central block pawn, six sources, and left/right waveforms and spectrograms" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Binaural Audio in Unreal Engine
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.unrealengine.com/en-US/blog/unreal-engine-5-is-now-available" rel="noopener noreferrer"&gt;Unreal Engine 5&lt;/a&gt; became generally available in April 2022. Its Audio Mixer goes beyond source playback and provides procedural synthesis, submix routing, DSP, and C++ APIs. This experiment uses Unreal Engine 5.8 from 2026.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.googleblog.com/en/resonance-audio-multi-platform-spatial-audio-at-scale/" rel="noopener noreferrer"&gt;Resonance Audio&lt;/a&gt; is a spatial-audio SDK released by Google in November 2017. It can apply HRTF, or Head-Related Transfer Function, processing that represents source direction through differences in arrival time, level, and frequency response at the two ears.&lt;/p&gt;

&lt;p&gt;I configured &lt;code&gt;BINAURAL_HIGH&lt;/code&gt;. Unreal Engine 5.8's &lt;a href="https://dev.epicgames.com/documentation/unreal-engine/resonance-audio-settings-in-the-unreal-engine-project-settings?lang=en-US" rel="noopener noreferrer"&gt;Resonance Audio settings&lt;/a&gt; describe it as the high-quality mode using third-order Ambisonics. Its output here is two-channel binaural audio intended for headphones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Licenses
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unreal Engine 5.8&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.unrealengine.com/eula/unreal" rel="noopener noreferrer"&gt;Unreal Engine EULA&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public Resonance Audio SDK&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/resonance-audio/resonance-audio/blob/master/LICENSE" rel="noopener noreferrer"&gt;Apache License 2.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Unreal Engine is not open-source software, and its EULA governs use and distribution. The public Resonance Audio source uses Apache License 2.0. When working with the plugin bundled into Unreal Engine or with Engine code, Epic's distribution terms must also be checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;p&gt;I mix the ambient and target sources, then capture the Main Output Submix as 48 kHz stereo after it reaches the listener. This PCM is the analyzer's only input. The detector does not receive source-actor positions or playback state directly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether per-band energy rise and full-band RMS rise can detect a 40 ms burst in ambient sound with low latency&lt;/li&gt;
&lt;li&gt;Whether masked GCC-PHAT on changed time-frequency bins plus ILD voting can determine left versus right&lt;/li&gt;
&lt;li&gt;Whether a soft mask derived from detection time and estimated direction can resynthesize a candidate clip&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete C++ source, configuration, Automation Tests, and aggregate JSON are available in the &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/25/unreal-mixed-audio-attention" rel="noopener noreferrer"&gt;unreal-mixed-audio-attention lab in kiarina/labs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the Lab
&lt;/h2&gt;

&lt;p&gt;Install Unreal Engine 5.8, a compatible Xcode version, and &lt;code&gt;mise&lt;/code&gt; on macOS. Set &lt;code&gt;UE_ROOT&lt;/code&gt; to the Engine root. Because this is a C++ project, close the project in the Editor before building it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml &lt;span class="se"&gt;\&lt;/span&gt;
  2026/07/25/unreal-mixed-audio-attention

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;UE_ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/UE_5.8
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/25/unreal-mixed-audio-attention run
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/25/unreal-mixed-audio-attention run &lt;span class="nb"&gt;test
&lt;/span&gt;mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/25/unreal-mixed-audio-attention run editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first &lt;code&gt;mise ... run&lt;/code&gt; builds the Editor target and verifies the project structure. &lt;code&gt;test&lt;/code&gt; runs Automation Tests for signal generation, side estimation, and the SI-SDR function. &lt;code&gt;editor&lt;/code&gt; opens the ordinary demo with a five-second calibration phase.&lt;/p&gt;

&lt;p&gt;After starting PIE, use &lt;code&gt;W/S&lt;/code&gt; to move forward and backward, &lt;code&gt;A/D&lt;/code&gt; to move sideways, and the mouse to rotate the camera and head listener. Touching a cyan &lt;code&gt;ENV&lt;/code&gt; sphere or orange &lt;code&gt;BURST&lt;/code&gt; sphere toggles that source. Press &lt;code&gt;Shift+F1&lt;/code&gt; to return control to the Editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Mixed Audio to a Decision
&lt;/h2&gt;

&lt;p&gt;The processing path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;two continuous ambient sources + 40 ms targets from four directions
  -&amp;gt; Resonance Audio / HRTF
  -&amp;gt; 48 kHz stereo Main Output Submix
  -&amp;gt; five-second ring buffer
  -&amp;gt; 1024-point STFT, 480-sample hop
       ├─ onset: eight-band energy rise + full-band RMS rise
       ├─ side: 200–1500 Hz masked GCC-PHAT + 1.5–8 kHz ILD
       └─ extraction: build a soft mask from time and direction
  -&amp;gt; raw/extracted stereo WAV + run JSON + HUD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An STFT divides a short segment of audio into frequency components. At 48 kHz, 1,024 samples span about 21.3 ms, and the 480-sample hop advances by 10 ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Detecting a Transient
&lt;/h3&gt;

&lt;p&gt;The analyzer divides the combined left/right spectrogram into eight bands and measures how much energy rose from the preceding context. A separate path measures the full-band RMS rise, and the larger value becomes the onset score.&lt;/p&gt;

&lt;p&gt;An early version averaged the rise across three bands, which diluted a narrow burst with unchanged bands. Switching to the maximum band rise made the fast path more responsive to short sounds. The threshold is derived from the median and median absolute deviation of the scores collected during calibration.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Estimating Left or Right
&lt;/h3&gt;

&lt;p&gt;GCC-PHAT finds the delay between the left and right waveforms through cross-correlation. Here, it uses only onset-changed bins between 200 and 1,500 Hz.&lt;/p&gt;

&lt;p&gt;At higher frequencies, head shadow tends to produce a stronger level difference, so ILD, or Interaural Level Difference, from 1.5 to 8 kHz also votes on the result. If GCC-PHAT and ILD do not provide enough agreement, the system returns &lt;code&gt;Unknown&lt;/code&gt; instead of forcing a side.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Extracting a Candidate Clip
&lt;/h3&gt;

&lt;p&gt;The analyzer takes a one-second interval from 250 ms before the event to 750 ms after it. It uses the pre-event interval as a baseline, then gives more mask weight to bins whose energy rises above that baseline and whose ILD agrees with the estimated side. The same mask is applied to both channels before reconstruction with the inverse STFT.&lt;/p&gt;

&lt;p&gt;Applying one mask to both channels avoids inventing a new interaural difference during extraction. The raw and extracted signals are saved as PCM16 stereo WAV files under &lt;code&gt;Saved/MixedAudioAttention/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scene and Test Conditions
&lt;/h2&gt;

&lt;p&gt;Two deterministic continuous-noise sources sit on an inner three-meter ring, while four target sources sit on an outer five-meter ring. Distance attenuation is disabled. Moving the listener therefore changes the HRTF direction relative to the head without using distance-dependent loudness as a cue.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engine&lt;/td&gt;
&lt;td&gt;Unreal Engine 5.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS / machine&lt;/td&gt;
&lt;td&gt;macOS 26.5.2 / Apple M4 Max&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio&lt;/td&gt;
&lt;td&gt;48 kHz stereo, Resonance Audio &lt;code&gt;BINAURAL_HIGH&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ambient sound&lt;/td&gt;
&lt;td&gt;Two deterministic continuous-noise sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Targets&lt;/td&gt;
&lt;td&gt;40 ms chirp / deterministic noise burst&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azimuths&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-120 / -60 / +60 / +120°&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requested SNR&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+12 / +6 / 0 / -6 dB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyzer&lt;/td&gt;
&lt;td&gt;1024-point Hann STFT, 480-sample hop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ordinary demo calibrates for five seconds and then cycles through enabled burst sources every two seconds. The implemented full mode uses a 60-second calibration, a 300-second negative period, and 160 events: two signal types, four azimuths, four SNR levels, and five repetitions.&lt;/p&gt;

&lt;p&gt;The block pawn at the center of the screenshot carries the listener at head height. Listener yaw follows the camera. The top HUD shows position, orientation, score, threshold, side prediction, confidence, lag, ILD, and latency. The bottom dock displays two seconds of waveform, four seconds of spectrogram, and the latest event for both ears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observed Results
&lt;/h2&gt;

&lt;p&gt;On July 25, 2026, I launched PIE through Unreal MCP and observed the fixed-listener demo from before the block pawn was added.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Observed value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Matched detections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35 / 35&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correct side&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34 / 35 (97.14%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20–60 ms, 20 ms median&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unmatched detections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyzer queue overruns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every target had a corresponding detection, and there were no extra detections during this short run. The side was correct on 34 events. With processing advancing in 10 ms hops, responses arrived at a median of 20 ms and a maximum of 60 ms.&lt;/p&gt;

&lt;p&gt;The HUD simultaneously updated the left/right stream waveforms, scrolling spectrograms, selected bins, detection markers, and raw/extracted waveforms for the latest event. The run also produced the expected WAV files and JSON record.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Plain-Language Reading of the Results
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The system found short changes and their side quickly from the mixed audio alone.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It did not use the Engine's knowledge of which actor emitted the sound as the detector input. Operating on post-HRTF stereo makes the design easier to adapt to inputs such as recordings or voice chat, where source metadata may not exist.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;35/35 is promising, but it is not a false-alarm evaluation.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The zero unmatched detections apply only to a short demo. The planned five-minute negative test has not run, so the provisional requirement of no more than one false alarm in five minutes has not been demonstrated.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Producing an extracted WAV is not the same as separating the sound well.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The soft mask generated output files, but there is no post-HRTF target-only reference yet. Without SI-SDR, improvement in extraction quality remains unmeasured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducibility Findings
&lt;/h2&gt;

&lt;p&gt;Signal processing was only part of the work. Several Unreal-specific issues mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adjusting volume alone did not prevent Resonance Audio's external send from becoming silent when PIE lacked focus. The experiment temporarily corrects VR focus and pause-on-focus-loss as well, then restores the previous values in &lt;code&gt;EndPlay&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The first implementation refilled procedural ambient audio on a timer and became silent after about two seconds. Queueing enough audio for the known experiment duration at startup fixed it.&lt;/li&gt;
&lt;li&gt;With an ordinary &lt;code&gt;TArray&amp;lt;float&amp;gt;&lt;/code&gt; and non-aligned settings, Unreal Engine 5.8's FFT factory returned &lt;code&gt;null&lt;/code&gt;. The analyzer now uses &lt;code&gt;Audio::FAlignedFloatBuffer&lt;/code&gt; and 128-bit alignment.&lt;/li&gt;
&lt;li&gt;The audio callback only copies into a preallocated five-second ring buffer. A dedicated worker performs STFT processing so that analysis does not block the callback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The aggregate comes from a short fixed-listener demo, not the current movable block pawn.&lt;/li&gt;
&lt;li&gt;The full 60-second calibration and 300-second negative run has not been performed.&lt;/li&gt;
&lt;li&gt;Results have not yet been broken down by SNR, signal type, or azimuth.&lt;/li&gt;
&lt;li&gt;No post-HRTF target-only reference exists, so raw and extracted SI-SDR are unmeasured.&lt;/li&gt;
&lt;li&gt;Generating a soft-masked WAV alone does not establish extraction quality.&lt;/li&gt;
&lt;li&gt;The experiment does not classify sound type or estimate front/back or elevation, occlusion, or reverberation.&lt;/li&gt;
&lt;li&gt;It uses a generalized HRTF and has not been tested with real-ear recordings or individualized HRTFs.&lt;/li&gt;
&lt;li&gt;It was observed on one setup: macOS 26.5.2, Apple M4 Max, and Unreal Engine 5.8.&lt;/li&gt;
&lt;li&gt;It assumes that the project is the only audio feeding the Main Output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Takeaway
&lt;/h2&gt;

&lt;p&gt;If I let the code inspect source actors, determining left and right would be trivial. Recovering the signal from the actual mixed audio at the listener, detecting all 35 events, and getting 34 sides right felt much more useful. Walking through the scene and turning the listener also makes the HUD's numbers tangible: the same source changes in the binaural stream and in the analyzer's decision as the head rotates.&lt;/p&gt;

&lt;p&gt;The extracted waveform looks convincing on screen, which makes it especially important not to call extraction successful while its quality metric is still blank. As a low-latency “auditory attention” sensor for an NPC or interactive agent, this is already a fun system to build with.&lt;/p&gt;

</description>
      <category>unrealengine</category>
      <category>cpp</category>
      <category>audio</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Running Qwen3 Through the ExecuTorch MLX Delegate: Up to 4.52x Faster on M1 Max</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Wed, 22 Jul 2026 03:57:08 +0000</pubDate>
      <link>https://dev.to/kiarina/running-qwen3-through-the-executorch-mlx-delegate-up-to-452x-faster-on-m1-max-4lc2</link>
      <guid>https://dev.to/kiarina/running-qwen3-through-the-executorch-mlx-delegate-up-to-452x-faster-on-m1-max-4lc2</guid>
      <description>&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;There are now many ways to run an LLM on a Mac, but exporting a PyTorch model for Apple Silicon and executing it in a lightweight runtime is still an evolving path. How much faster is it, and does 4-bit quantization change the output?&lt;/p&gt;

&lt;p&gt;Today, I am looking at ExecuTorch's experimental MLX delegate, released in May 2026. It enables PyTorch models to run on Apple Silicon GPUs. I use ExecuTorch 1.3.1 to run Qwen3-0.6B and compare it with PyTorch MPS.&lt;/p&gt;

&lt;p&gt;The short result is that decode throughput was &lt;strong&gt;41.8 tokens/s&lt;/strong&gt; with PyTorch MPS BF16, &lt;strong&gt;134.8 tokens/s&lt;/strong&gt; with MLX BF16, and &lt;strong&gt;188.9 tokens/s&lt;/strong&gt; with MLX INT4. MLX INT4 was 4.52x faster, and its file was 71.8% smaller than BF16. However, INT4 changed the generated output in two of three simple prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the ExecuTorch MLX Delegate?
&lt;/h2&gt;

&lt;p&gt;ExecuTorch is a runtime for running trained PyTorch models on desktops, phones, and embedded devices. Inference means feeding input into a trained model to obtain an output.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pytorch.org/blog/running-pytorch-models-on-apple-silicon-gpus-with-the-executorch-mlx-delegate/" rel="noopener noreferrer"&gt;MLX delegate&lt;/a&gt; was released on &lt;strong&gt;May 18, 2026&lt;/strong&gt;. It sends a PyTorch computation graph to Apple's MLX framework for execution on an Apple Silicon GPU. It supports BF16, FP16, FP32, and 2/4/8-bit quantization, among other formats. It is currently experimental, so its APIs and supported scope may change.&lt;/p&gt;

&lt;p&gt;Quantization stores weights with fewer bits to reduce size and computation. I compared ordinary BF16 with INT4, which quantizes the linear layers and embeddings to four bits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Qwen3-0.6B Model Used Here
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://qwenlm.github.io/blog/qwen3/" rel="noopener noreferrer"&gt;Qwen3&lt;/a&gt; is an LLM family announced by the Qwen Team on &lt;strong&gt;April 29, 2025&lt;/strong&gt;. A single model can switch between a thinking mode for step-by-step reasoning and a non-thinking mode for shorter answers. The family supports more than 100 languages and dialects.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://huggingface.co/Qwen/Qwen3-0.6B" rel="noopener noreferrer"&gt;Qwen3-0.6B&lt;/a&gt; used here is one of the smaller dense models in the family. “Dense” means that it generally uses the full model for each input, unlike a mixture-of-experts model that activates only selected parts. It has a nominal 0.6 billion parameters, 28 layers, and a context length of 32,768 tokens. A token is a small unit of text processed by the model.&lt;/p&gt;

&lt;p&gt;I disabled thinking mode for this test and pinned the model revision to &lt;code&gt;c1899de289a04d12100db370d81485cdf75e47ca&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Licenses
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-0.6B weights&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/Qwen/Qwen3-0.6B/blob/c1899de289a04d12100db370d81485cdf75e47ca/LICENSE" rel="noopener noreferrer"&gt;Apache License 2.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExecuTorch&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/pytorch/executorch/blob/v1.3.1/LICENSE" rel="noopener noreferrer"&gt;BSD 3-Clause License&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MLX&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/ml-explore/mlx/blob/main/LICENSE" rel="noopener noreferrer"&gt;MIT License&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/pytorch/pytorch/blob/main/LICENSE" rel="noopener noreferrer"&gt;BSD 3-Clause License&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These permissive open-source licenses allow broad use, including commercial use, but their conditions—such as retaining copyright notices when redistributing—still apply. Check the linked license text for your use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Paths and Their Data Flow
&lt;/h2&gt;

&lt;p&gt;Only one trained model, Qwen3-0.6B, was used. The same weights were tested through three execution paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same prompt
  -&amp;gt; tokenizer: convert text to token IDs
  -&amp;gt; Qwen3-0.6B
       ├─ ExecuTorch MLX BF16 -&amp;gt; .pte -&amp;gt; MLX / Metal GPU
       ├─ ExecuTorch MLX INT4 -&amp;gt; 4-bit .pte -&amp;gt; MLX / Metal GPU
       └─ PyTorch MPS BF16 ----------------&amp;gt; MPS / Metal GPU
  -&amp;gt; greedy generation: select the most likely next token each time
  -&amp;gt; tokenizer: convert token IDs back to text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;.pte&lt;/code&gt; file is a model program exported for ExecuTorch. The complete computation graph could be lowered into a single MLX subgraph. PyTorch MPS BF16 ran the same BF16 weights through the ordinary Transformers/PyTorch path and served as the reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Whether the complete Qwen3-0.6B graph could be lowered to MLX and executed&lt;/li&gt;
&lt;li&gt;Whether MLX BF16 and PyTorch MPS BF16 generated the same tokens&lt;/li&gt;
&lt;li&gt;How INT4 changed model size, speed, and process memory&lt;/li&gt;
&lt;li&gt;Whether the experiment could be reproduced using published wheels only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete code and JSON report are available in the &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/22/executorch-mlx-qwen3" rel="noopener noreferrer"&gt;executorch-mlx-qwen3 lab in kiarina/labs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the Lab
&lt;/h2&gt;

&lt;p&gt;You need an Apple Silicon Mac, Xcode Command Line Tools, &lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, and an internet connection. The first run downloads the Qwen3 weights and creates about 1.53 GB of PTE files in total.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml &lt;span class="se"&gt;\&lt;/span&gt;
  2026/07/22/executorch-mlx-qwen3
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/22/executorch-mlx-qwen3 run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The export and benchmark steps can also be run separately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/22/executorch-mlx-qwen3 run &lt;span class="nb"&gt;export
&lt;/span&gt;mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/22/executorch-mlx-qwen3 run benchmark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test Conditions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;machine: MacBook Pro (Apple M1 Max, 32 GPU cores, 64 GB)
OS: macOS 26.5.2
Python: 3.13.7
ExecuTorch: 1.3.1
PyTorch: 2.12.1
Transformers: 4.56.1
model: Qwen/Qwen3-0.6B
generation: greedy, batch 1, up to 16 tokens
PTE: custom MLX SDPA / KV cache, requested maximum sequence 128
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For performance, each backend was given the same Japanese prompt and forced to generate 16 tokens. The reported values are medians from five trials after warm-up. Prefill reads the input and produces the first token; decode generates the remaining tokens one at a time.&lt;/p&gt;

&lt;p&gt;Each backend ran in a separate process. The MLX path loaded a fresh forward method and initialized its KV cache for each trial, while the PyTorch path created a fresh cache for every trial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PTE Size
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PTE&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Relative to BF16&lt;/th&gt;
&lt;th&gt;Export time&lt;/th&gt;
&lt;th&gt;SHA-256&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MLX BF16&lt;/td&gt;
&lt;td&gt;1,192,264,196 bytes&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;41.61 s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;83da47c2…bfb8c0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MLX INT4&lt;/td&gt;
&lt;td&gt;335,662,976 bytes&lt;/td&gt;
&lt;td&gt;28.2%&lt;/td&gt;
&lt;td&gt;54.55 s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0e30a054…71267&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;INT4 was 856,601,220 bytes, or &lt;strong&gt;71.8% smaller&lt;/strong&gt;, than BF16. Quantization itself takes work, so exporting INT4 took about 13 seconds longer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generation Speed
&lt;/h3&gt;

&lt;p&gt;The input was the Japanese prompt “Briefly explain local inference on Apple Silicon.”&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Load&lt;/th&gt;
&lt;th&gt;Median prefill&lt;/th&gt;
&lt;th&gt;Median decode&lt;/th&gt;
&lt;th&gt;16-token total&lt;/th&gt;
&lt;th&gt;Peak RSS increase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ExecuTorch MLX BF16&lt;/td&gt;
&lt;td&gt;0.002 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.020 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;134.8 tokens/s&lt;/td&gt;
&lt;td&gt;0.131 s&lt;/td&gt;
&lt;td&gt;1.27 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExecuTorch MLX INT4&lt;/td&gt;
&lt;td&gt;0.003 s&lt;/td&gt;
&lt;td&gt;0.028 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;188.9 tokens/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.108 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.47 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch MPS BF16&lt;/td&gt;
&lt;td&gt;0.726 s&lt;/td&gt;
&lt;td&gt;0.038 s&lt;/td&gt;
&lt;td&gt;41.8 tokens/s&lt;/td&gt;
&lt;td&gt;0.396 s&lt;/td&gt;
&lt;td&gt;0.14 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MLX BF16 decoded &lt;strong&gt;3.22x&lt;/strong&gt; as fast as PyTorch MPS BF16, while MLX INT4 was &lt;strong&gt;4.52x&lt;/strong&gt; as fast. INT4 was also 1.40x faster than MLX BF16 and reduced the RSS increase by about 63%.&lt;/p&gt;

&lt;p&gt;There are important qualifications. The MLX load figure measures only opening the PTE program, not all work needed to materialize weights for GPU use. RSS measures the process's main memory, not GPU memory itself. PyTorch reported 1.20 GiB of MPS driver-allocated memory at the end of the run. Because GPU memory was not measured on the same basis, the table does not prove that PyTorch used the least memory.&lt;/p&gt;

&lt;p&gt;On the first invocation only, BF16 prefill took 0.434 seconds and INT4 took 1.014 seconds. Cold starts that include Metal setup and initial compilation were much slower than the warmed-up figures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generated Output
&lt;/h3&gt;

&lt;p&gt;I compared the generated token sequences on three short prompts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;PyTorch MPS BF16&lt;/th&gt;
&lt;th&gt;MLX BF16&lt;/th&gt;
&lt;th&gt;MLX INT4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Answer Japan's capital in one word&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;日本の首都は、**大阪**です。&lt;/code&gt; (Japan's capital is &lt;strong&gt;Osaka&lt;/strong&gt;.)&lt;/td&gt;
&lt;td&gt;Exact token match&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;日本の首都は、**东京**です。&lt;/code&gt; (Japan's capital is &lt;strong&gt;Tokyo&lt;/strong&gt;.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer 1+1 with one digit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1+1=2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exact token match&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copy &lt;code&gt;MLX&lt;/code&gt; unchanged&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MLX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exact token match&lt;/td&gt;
&lt;td&gt;Exact token match&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MLX BF16 matched PyTorch MPS BF16 token for token in all three cases. Within this narrow test, changing the execution path to MLX did not introduce a difference.&lt;/p&gt;

&lt;p&gt;INT4 matched in only one case. Quantization represents numbers more coarsely, so when candidate next tokens have similar scores, their ranking can change.&lt;/p&gt;

&lt;p&gt;The capital answer was wrong in both BF16 and INT4. The small 0.6B model, prompt, and greedy decoding could all contribute, but three questions are not enough to identify the cause. This probe checks differences between backends; it does not certify the model's knowledge or answer quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Plain-Language Reading of the Results
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The same small LLM ran substantially faster through MLX.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even BF16 decoded 3.22x as fast as PyTorch MPS. Exporting a PyTorch model to a lightweight Mac runtime looks promising.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Four-bit weights are smaller and faster, but answers can change.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;INT4 reduced the file from about 1.19 GB to 336 MB and produced the highest decode rate. Yet it changed two of only three token sequences. It should be evaluated on a task-specific quality set before adoption.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The gap cannot be attributed to MLX kernels alone.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This comparison covers an ExecuTorch MLX pipeline versus a Transformers/PyTorch MPS pipeline. Their runtimes, cache handling, and execution paths differ, so the numbers describe the complete pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducibility Findings
&lt;/h2&gt;

&lt;p&gt;The dependency metadata for &lt;code&gt;executorch==1.3.1&lt;/code&gt; allowed PyTorch 2.13.0, but importing the published ExecuTorch extension failed because the &lt;code&gt;materialize_cow_storage&lt;/code&gt; symbol was missing. Pinning PyTorch to 2.12.1 made the same wheel work. The failure can be reproduced with this optional task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/22/executorch-mlx-qwen3 run probe-torch-2-13
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bundled PTE inspector also failed because its included &lt;code&gt;flatc&lt;/code&gt; did not recognize the &lt;code&gt;--json&lt;/code&gt; option. I verified full-graph delegation from the partitioner log emitted during export instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only one M1 Max, Qwen3-0.6B, batch 1, and short Japanese prompts were tested.&lt;/li&gt;
&lt;li&gt;Five short trials do not control thermal state, power consumption, or other GPU workloads.&lt;/li&gt;
&lt;li&gt;Only BF16 and INT4 were compared; FP16, 2/8-bit, Core ML, and other paths were not tested.&lt;/li&gt;
&lt;li&gt;The quality probe had only three questions; no standard benchmark or perplexity was measured.&lt;/li&gt;
&lt;li&gt;The requested maximum sequence was 128, and long text was not tested.&lt;/li&gt;
&lt;li&gt;GPU memory could not be measured on the same basis for MLX and MPS.&lt;/li&gt;
&lt;li&gt;Cold-start latency was observed only once.&lt;/li&gt;
&lt;li&gt;The MLX delegate is experimental.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Takeaway
&lt;/h2&gt;

&lt;p&gt;Lowering the entire Qwen3-0.6B graph to MLX and increasing decode throughput by more than 3x without changing the BF16 tokens was a good result. INT4 reduced the file to less than one-third of its BF16 size and reached about 189 tokens/s, which is attractive when embedding a small model on a Mac.&lt;/p&gt;

&lt;p&gt;The output changes from 4-bit quantization appeared immediately, even in this tiny probe. Quantization is not a free speedup. Paired with a quality suite for the real task, this path could work well for local helper features or small, responsive agents.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pytorch</category>
      <category>llm</category>
      <category>applesilicon</category>
    </item>
    <item>
      <title>Testing PyTorch 2.13 MPS FlexAttention on M1 Max: Up to 7.83x Faster for Sparse Attention</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Tue, 21 Jul 2026 04:03:12 +0000</pubDate>
      <link>https://dev.to/kiarina/testing-pytorch-213-mps-flexattention-on-m1-max-up-to-783x-faster-for-sparse-attention-1p7d</link>
      <guid>https://dev.to/kiarina/testing-pytorch-213-mps-flexattention-on-m1-max-up-to-783x-faster-for-sparse-attention-1p7d</guid>
      <description>&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;Attention becomes expensive very quickly as more text is given to an AI model. Can a Mac GPU make it faster when every token is restricted to looking only at nearby tokens?&lt;/p&gt;

&lt;p&gt;Today, I am comparing FlexAttention, newly available on Apple Silicon in PyTorch 2.13, with standard SDPA on an M1 Max.&lt;/p&gt;

&lt;p&gt;The short answer is that, with 32,768 tokens and a 256-token local window, FlexAttention took &lt;strong&gt;75.27 ms&lt;/strong&gt; while SDPA took &lt;strong&gt;589.05 ms&lt;/strong&gt;: a &lt;strong&gt;7.83x speedup&lt;/strong&gt;. For ordinary causal attention, however, SDPA was about 19x faster. FlexAttention was not universally faster; it helped with long, extremely sparse attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  FlexAttention and Its MPS Support
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pytorch.org/blog/pytorch2-5/" rel="noopener noreferrer"&gt;FlexAttention&lt;/a&gt; was announced as a prototype with PyTorch 2.5 in &lt;strong&gt;October 2024&lt;/strong&gt;. It lets developers express an attention rule as a short Python function, which &lt;code&gt;torch.compile&lt;/code&gt; turns into a specialized fused kernel.&lt;/p&gt;

&lt;p&gt;Attention determines which tokens in the input should influence one another. The number of possible comparisons grows rapidly with sequence length. FlexAttention can describe rules such as “look only into the past” or “look only at the previous 256 tokens,” allowing unnecessary comparisons to be skipped. A pattern with many skipped comparisons is called &lt;strong&gt;sparse&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pytorch.org/blog/pytorch-2-13-release-blog/" rel="noopener noreferrer"&gt;PyTorch 2.13&lt;/a&gt;, announced on &lt;strong&gt;July 8, 2026&lt;/strong&gt;, added Metal/MPS kernels for FlexAttention on Apple Silicon. PyTorch reports up to roughly 12x higher performance than SDPA for sparse patterns. The API and kernel options remain unstable in 2.13.&lt;/p&gt;

&lt;p&gt;This experiment does not use a pretrained AI model. It is a kernel benchmark that sends the same randomly generated attention inputs through two implementations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Roles of the Technologies
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch 2.13.0 / FlexAttention&lt;/td&gt;
&lt;td&gt;Run custom attention rules as compiled kernels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDPA&lt;/td&gt;
&lt;td&gt;Standard PyTorch attention implementation used as the baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MPS / Metal&lt;/td&gt;
&lt;td&gt;Execute calculations on the Apple Silicon GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Roles and Data Flow
&lt;/h2&gt;

&lt;p&gt;Both paths receive the same query, key, and value tensors. Loosely speaking, these represent what to search for, what to match against, and what content to retrieve.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same random inputs (query / key / value)
  ├─ FlexAttention
  │    Python mask rule -&amp;gt; BlockMask -&amp;gt; torch.compile -&amp;gt; Metal kernel
  │
  └─ SDPA
       dense mask --------------------&amp;gt; MPS backend

              -&amp;gt; compare output error and forward time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;BlockMask&lt;/code&gt; groups the regions to compute into 128×128-token blocks. FlexAttention can skip unnecessary blocks. The SDPA path received an ordinary boolean mask describing the same rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Whether sparse attention is also faster on an M1 Max using the representative official shapes&lt;/li&gt;
&lt;li&gt;How wide an 8,192-token window can become before SDPA takes the lead&lt;/li&gt;
&lt;li&gt;Whether the benefit survives initial compilation and BlockMask construction&lt;/li&gt;
&lt;li&gt;Whether FlexAttention and SDPA produce matching outputs&lt;/li&gt;
&lt;li&gt;Whether backward gradient computation works on MPS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete code and JSON report are available in the &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/21/pytorch-2-13-flexattention-mps" rel="noopener noreferrer"&gt;pytorch-2-13-flexattention-mps lab in kiarina/labs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the Lab
&lt;/h2&gt;

&lt;p&gt;You will need an Apple Silicon Mac, &lt;code&gt;mise&lt;/code&gt;, and &lt;code&gt;uv&lt;/code&gt;. The default task also creates a 32,768×32,768 boolean mask, which alone occupies 1 GiB. Stop other GPU workloads and run it on a machine with sufficient memory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml &lt;span class="se"&gt;\&lt;/span&gt;
  2026/07/21/pytorch-2-13-flexattention-mps
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/21/pytorch-2-13-flexattention-mps run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To skip the long case, run this inside the lab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run python benchmark.py &lt;span class="nt"&gt;--quick&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test Conditions
&lt;/h2&gt;

&lt;p&gt;Query, key, and value were generated independently at random. Both implementations received the same tensors. I timed only the forward pass, excluding mask creation and compilation. Because MPS runs asynchronously, each measurement explicitly waited for the GPU before and after the call. The reported values are medians from ten trials after three warm-ups.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;machine: MacBook Pro (Apple M1 Max, 32 GPU cores, 64 GB)
OS: macOS 26.5.2
Python: 3.13.7
PyTorch: 2.13.0
shape: batch 1, 8 heads, head dimension 64
dtype: bfloat16
FlexAttention: torch.compile(..., dynamic=False)
BlockMask: 128×128 blocks
CPU fallback: disabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sliding window lets each position see the previous W tokens. With a window of 256, for example, each token is limited to the most recent 256 tokens even in a very long input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;SDPA / Flex&lt;/code&gt; value above one means that FlexAttention was faster. Density is the percentage of all token pairs that the pattern actually allows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Sequence&lt;/th&gt;
&lt;th&gt;Window&lt;/th&gt;
&lt;th&gt;Token / block density&lt;/th&gt;
&lt;th&gt;Flex median&lt;/th&gt;
&lt;th&gt;SDPA median&lt;/th&gt;
&lt;th&gt;SDPA / Flex&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;causal&lt;/td&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;50.01% / 50.78%&lt;/td&gt;
&lt;td&gt;231.22 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.33 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.05x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;0.78% / 3.10%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.75 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25.23 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.15x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;3.08% / 4.61%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.14 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25.25 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.32x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;1,024&lt;/td&gt;
&lt;td&gt;11.72% / 13.18%&lt;/td&gt;
&lt;td&gt;56.24 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25.30 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.45x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;4,096&lt;/td&gt;
&lt;td&gt;37.50% / 38.67%&lt;/td&gt;
&lt;td&gt;169.24 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25.30 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.15x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;0.78% / 1.17%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;75.27 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;589.05 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.83x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 8,192 tokens, FlexAttention won through a window of 256. At a window of 1,024, SDPA became 2.22x faster. The crossover on this shape lies between 3.08% and 11.72% token density.&lt;/p&gt;

&lt;p&gt;Causal attention allows each token to see the entire past, giving it a density of about 50%. SDPA has a specialized path for this common pattern and was &lt;strong&gt;18.75x faster&lt;/strong&gt; than FlexAttention. Simply replacing ordinary causal attention is counterproductive.&lt;/p&gt;

&lt;p&gt;For the 32,768 / window 256 case, one of ten FlexAttention trials rose to 147.89 ms. Its full range of 74.96–147.89 ms still did not overlap with SDPA's 586.08–590.03 ms range, so the performance ordering was unambiguous.&lt;/p&gt;

&lt;h3&gt;
  
  
  Difference from the Official Benchmark
&lt;/h3&gt;

&lt;p&gt;PyTorch reports 4.15x for 8,192 / window 64 and approximately 12.3x for 32,768 / window 256. The M1 Max produced 2.15x and 7.83x, respectively.&lt;/p&gt;

&lt;p&gt;The expected direction was reproduced: sparser patterns were faster, and the gap grew at longer sequence lengths. The speedups did not reach the official figures. Because the release blog does not identify the Apple Silicon model used for those figures, the difference cannot be attributed to hardware alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial Setup Cost
&lt;/h3&gt;

&lt;p&gt;In addition to steady-state forward time, FlexAttention needs BlockMask construction and a first compiled call.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Sequence / window&lt;/th&gt;
&lt;th&gt;BlockMask build&lt;/th&gt;
&lt;th&gt;First compiled call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;causal&lt;/td&gt;
&lt;td&gt;8,192 / —&lt;/td&gt;
&lt;td&gt;564.54 ms&lt;/td&gt;
&lt;td&gt;546.57 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;8,192 / 64&lt;/td&gt;
&lt;td&gt;95.90 ms&lt;/td&gt;
&lt;td&gt;94.31 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;8,192 / 256&lt;/td&gt;
&lt;td&gt;100.54 ms&lt;/td&gt;
&lt;td&gt;102.34 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;8,192 / 1,024&lt;/td&gt;
&lt;td&gt;95.02 ms&lt;/td&gt;
&lt;td&gt;130.75 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;8,192 / 4,096&lt;/td&gt;
&lt;td&gt;96.65 ms&lt;/td&gt;
&lt;td&gt;244.62 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;32,768 / 256&lt;/td&gt;
&lt;td&gt;1,120.85 ms&lt;/td&gt;
&lt;td&gt;162.57 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 32,768 / window 256 case saves about 514 ms per call, so reusing the same mask recovers the setup cost in roughly three forwards. The 8,192 / window 64 case needs about 14. A mask used only once should not be selected based on the steady-state 7.83x or 2.15x figure alone.&lt;/p&gt;

&lt;p&gt;The first-call figures include the state of the host's compile cache. BlockMask construction was eager; I did not benchmark compiling mask construction itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Output Agreement and Supported Scope
&lt;/h3&gt;

&lt;p&gt;On identical MPS bfloat16 inputs, maximum absolute error between FlexAttention and SDPA ranged from 0.0078125 to 0.015625, while mean absolute error ranged from 0.000079 to 0.000363.&lt;/p&gt;

&lt;p&gt;I also compared a smaller input against float32 SDPA on the CPU.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MPS implementation&lt;/th&gt;
&lt;th&gt;Maximum absolute error&lt;/th&gt;
&lt;th&gt;Mean absolute error&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FlexAttention bfloat16&lt;/td&gt;
&lt;td&gt;0.012440&lt;/td&gt;
&lt;td&gt;0.000553&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDPA bfloat16&lt;/td&gt;
&lt;td&gt;0.012440&lt;/td&gt;
&lt;td&gt;0.000677&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both MPS paths had the same maximum error, and I did not observe a large discrepancy specific to FlexAttention. This is a numerical comparison on synthetic inputs, not a quality evaluation of a complete model.&lt;/p&gt;

&lt;p&gt;A probe with &lt;code&gt;requires_grad=True&lt;/code&gt; failed with &lt;code&gt;FlexAttention does not support backward on MPS&lt;/code&gt;. The PyTorch 2.13 MPS path is &lt;strong&gt;forward-inference only&lt;/strong&gt;. The deterministic backward feature added in 2.13 applies to CUDA and does not add MPS training support.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Plain-Language Reading of the Results
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It was faster when a long input looked at only a tiny nearby region.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;FlexAttention was 7.83x faster when 32,768 tokens were limited to the previous 256. It benefits when a large share of the work can be skipped.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It is not a drop-in speedup for ordinary attention.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Standard SDPA was about 19x faster for causal attention over the full past. The pattern and density must be measured.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reusing the same rule matters.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mask construction and compilation add an initial wait. FlexAttention is better suited to work that reuses them across layers or inference calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only one M1 Max, one process, and synthetic random inputs were tested.&lt;/li&gt;
&lt;li&gt;Measurements covered only bfloat16, batch 1, 8 heads, and head dimension 64.&lt;/li&gt;
&lt;li&gt;Ten short trials after three warm-ups do not control long-term thermal throttling or other GPU workloads.&lt;/li&gt;
&lt;li&gt;Only forward prefill was tested; decode, GQA, captured buffers, and score modification were not.&lt;/li&gt;
&lt;li&gt;Peak memory, energy use, and per-kernel Metal profiles were not measured.&lt;/li&gt;
&lt;li&gt;The hardware, OS, and measurement process were not fully identical to the official benchmark.&lt;/li&gt;
&lt;li&gt;The FlexAttention API and kernel options remain unstable in PyTorch 2.13.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Takeaway
&lt;/h2&gt;

&lt;p&gt;Seeing the gap reach 7.83x for extremely sparse, long attention on an M1 Max was a good result. Writing the mask rule in Python and letting PyTorch produce a specialized Metal kernel substantially lowers the barrier to experimenting with custom attention on a Mac.&lt;/p&gt;

&lt;p&gt;The advantage disappeared quickly as the window widened, and the initial setup time was not trivial. Despite the “Flex” name, it should not be treated as a universal optimization; density and reuse count need to be measured. It looks useful for local LLM inference over long documents with nearby context or experiments that operate on sparse relationships.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pytorch</category>
      <category>machinelearning</category>
      <category>applesilicon</category>
    </item>
    <item>
      <title>Testing Japanese Streaming ASR with Apple SpeechAnalyzer: 0% CER and About One Second to Display</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Tue, 21 Jul 2026 02:56:57 +0000</pubDate>
      <link>https://dev.to/kiarina/testing-japanese-streaming-asr-with-apple-speechanalyzer-0-cer-and-about-one-second-to-display-4f91</link>
      <guid>https://dev.to/kiarina/testing-japanese-streaming-asr-with-apple-speechanalyzer-0-cer-and-about-one-second-to-display-4f91</guid>
      <description>&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;If meeting and note transcription can run entirely on a Mac, the audio never has to be sent to an external service. But how accurate and responsive is that experience while someone is speaking Japanese?&lt;/p&gt;

&lt;p&gt;Today, I am feeding Japanese audio incrementally into Apple's SpeechAnalyzer and measuring recognition accuracy and partial-result latency. I also built a local browser tool for trying it with a microphone.&lt;/p&gt;

&lt;p&gt;The short answer is that both configurations achieved a &lt;strong&gt;0% character error rate (CER) in all three trials&lt;/strong&gt; on a 14.171-second synthetic conversation. In progressive mode, however, the first text arrived after about 1.09 seconds, and partial-result delivery latency was about 1.01 seconds at p95. The content was accurate, but the text did not appear immediately after each word was spoken.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is SpeechAnalyzer?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2025/277/" rel="noopener noreferrer"&gt;SpeechAnalyzer&lt;/a&gt; is Apple's new speech-to-text API, announced at &lt;strong&gt;WWDC25 in June 2025&lt;/strong&gt; and introduced with the iOS 26 and macOS 26 generation.&lt;/p&gt;

&lt;p&gt;Its &lt;code&gt;SpeechTranscriber&lt;/code&gt; module uses a new general-purpose conversational speech-recognition model. Apple describes it as faster and more flexible than the previous model and suitable for long-form or distant audio such as meetings and lectures. Processing runs on the device.&lt;/p&gt;

&lt;p&gt;This is not an open-source model whose weights can be downloaded and redistributed independently. &lt;a href="https://developer.apple.com/documentation/speech/assetinventory" rel="noopener noreferrer"&gt;AssetInventory&lt;/a&gt; downloads the required model from Apple's servers, while the operating system stores and updates it. Apple does not present a standalone open-source license for the model; use of the Speech framework and its model is subject to the &lt;a href="https://developer.apple.com/support/terms/" rel="noopener noreferrer"&gt;applicable Apple OS, Xcode, SDK, and developer terms&lt;/a&gt;. The lab code created for this experiment uses the MIT License.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roles and Data Flow
&lt;/h2&gt;

&lt;p&gt;This experiment does not combine multiple recognition models. It uses one &lt;code&gt;ja_JP&lt;/code&gt; SpeechTranscriber model. The surrounding path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser microphone (normally 48 kHz Float32)
  -&amp;gt; AudioWorklet
  -&amp;gt; WebSocket (localhost)
  -&amp;gt; Python bridge (resample to 16 kHz)
  -&amp;gt; Swift CLI (convert to Int16 PCM)
  -&amp;gt; SpeechAnalyzer + SpeechTranscriber
  -&amp;gt; partial / final results
  -&amp;gt; browser display
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PCM&lt;/code&gt; is a basic audio representation that stores the sound wave as a sequence of numbers. The Python bridge and Swift CLI only prepare the format; they do not recognize speech. Audio remains within localhost and is neither uploaded nor saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;p&gt;The experiment asks three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can it transcribe a known Japanese conversation correctly?&lt;/li&gt;
&lt;li&gt;How long do the first and subsequent partial results take?&lt;/li&gt;
&lt;li&gt;Can a browser microphone be connected to the on-device SpeechAnalyzer path?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete code and JSON measurement report are available in the &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/20/apple-speech-analyzer-streaming-asr" rel="noopener noreferrer"&gt;apple-speech-analyzer-streaming-asr lab in kiarina/labs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the Lab
&lt;/h2&gt;

&lt;p&gt;You will need an Apple Silicon Mac, macOS 26 or later, Xcode Command Line Tools, &lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, and FFmpeg. These commands download the shared audio asset, run the tests, build a release binary, and execute three trials per configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml &lt;span class="se"&gt;\&lt;/span&gt;
  2026/07/20/apple-speech-analyzer-streaming-asr
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/20/apple-speech-analyzer-streaming-asr run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To try a live microphone in the browser, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/20/apple-speech-analyzer-streaming-asr run demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi7kg8v1qybh5twvy8dyb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi7kg8v1qybh5twvy8dyb.webp" alt="Apple SpeechAnalyzer Live ASR interface transcribing Japanese microphone input locally on a Mac" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Partial text appears in lime and finalized text in white. The local server has no authentication, so keep it on &lt;code&gt;127.0.0.1&lt;/code&gt; and do not expose it publicly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Conditions
&lt;/h2&gt;

&lt;p&gt;I fed a 14.171-second synthetic two-speaker conversation in 100 ms chunks, paced to match real time. I compared two presets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;transcription&lt;/code&gt;: prioritizes accuracy and returns finalized results&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;progressiveTranscription&lt;/code&gt;: also returns the accumulated in-progress transcript&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main environment was a MacBook Pro with an Apple M1 Max and 64 GB of memory, macOS 26.5.2, and Swift 6.3.3. The locale was &lt;code&gt;ja_JP&lt;/code&gt;, and input was 16 kHz mono Int16 PCM.&lt;/p&gt;

&lt;p&gt;I measured differences from the reference with CER. It is the proportion of character substitutions, deletions, and insertions needed to match the correct text. This experiment ignored whitespace and punctuation during that comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;CER&lt;/th&gt;
&lt;th&gt;Partial results&lt;/th&gt;
&lt;th&gt;First partial&lt;/th&gt;
&lt;th&gt;Partial delivery p50 / p95&lt;/th&gt;
&lt;th&gt;Final delivery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;transcription&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0% × 3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1.160–1.215 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;progressive&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0% × 3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;93, 93, 94&lt;/td&gt;
&lt;td&gt;1.082–1.101 s&lt;/td&gt;
&lt;td&gt;0.526 / 1.014 s&lt;/td&gt;
&lt;td&gt;1.033–1.223 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;End-to-end real-time factor (RTF), from input start to finalization, was 1.072–1.086. An &lt;code&gt;RTF of 1.0&lt;/code&gt; means the full processing time equals the audio duration. Because this test intentionally waits while feeding audio at real-time speed, that figure is not the model's isolated compute speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  All Six Utterances Retained Their Content
&lt;/h3&gt;

&lt;p&gt;After ignoring punctuation and whitespace, all six utterances matched the reference in every trial. The phrase containing the number, &lt;code&gt;あと5分くらいかな&lt;/code&gt; (“about five more minutes”), was also preserved.&lt;/p&gt;

&lt;p&gt;Punctuation varied between trials and presets. For example, one result joined &lt;code&gt;そっちはこっちは&lt;/code&gt;, while another inserted a full stop between the two phrases. A 0% CER here does not mean that every output was equally readable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Progressive Display Lagged by About One Second
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;progressiveTranscription&lt;/code&gt; returned its first partial after about 1.09 seconds and then kept updating the accumulated text. Across all 280 partial results, delivery lag relative to the corresponding audio time was 0.526 seconds at p50 and 1.014 seconds at p95.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;p95&lt;/code&gt; means that 95% of the results arrived within that amount of time. The preregistered targets—under one second for both the first partial and p95 delivery—were narrowly missed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Live-Microphone Path Also Worked
&lt;/h3&gt;

&lt;p&gt;The automated end-to-end test sent 48 kHz audio in browser-sized 128-frame chunks, resampled it to 16 kHz, and passed it to SpeechAnalyzer. It received 93 partial results and one final result, with the same final content as the benchmark.&lt;/p&gt;

&lt;p&gt;In an informal Chrome trial with a MacBook Pro's built-in microphone, one user also perceived about one second of display delay. Recognition errors seemed infrequent, but &lt;code&gt;十分&lt;/code&gt; (“enough”) was rendered as &lt;code&gt;10分&lt;/code&gt; (“ten minutes”), and some punctuation was missing. This was a subjective single-person check, not part of the measured CER evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Plain-Language Reading of the Results
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It captured the spoken content accurately in one short, clean conversation.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was only one synthetic recording. A noisy room or unfamiliar proper nouns may produce very different results.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It can update text while someone speaks, but the display is roughly one second behind.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That may be acceptable for meeting notes or draft captions, but it may feel slow for a voice command that should react immediately after one word.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Speech can be processed without sending it away from the device.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model runs on the Mac. Its initial asset download and later updates still come from Apple's servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Implementation Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The evaluation covered one clean, 14.171-second synthetic conversation and three trials per preset.&lt;/li&gt;
&lt;li&gt;It did not score punctuation quality.&lt;/li&gt;
&lt;li&gt;It did not test noise, reverberation, dialects, proper nouns, or long streams.&lt;/li&gt;
&lt;li&gt;The exact version and hash of Apple's system-managed model cannot be pinned.&lt;/li&gt;
&lt;li&gt;The benchmark excludes microphone buffering and browser permission time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation had to match the model's PCM type as well as its sample rate. Passing Float32 directly failed, while conversion to the 16 kHz Int16 format requested by SpeechAnalyzer worked. Reproductions should record the OS build, Speech framework, and locale together.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Takeaway
&lt;/h2&gt;

&lt;p&gt;Even though this was only one short conversation, a 0% CER across every on-device trial was better than I expected. The roughly one-second display delay was visible, but it did not feel extremely slow during informal free speech.&lt;/p&gt;

&lt;p&gt;Punctuation was less stable than content recognition, so a production tool would benefit from a readability cleanup stage. The current path looks useful for local meeting notes or draft captions when audio must not leave the device.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>macos</category>
      <category>machinelearning</category>
      <category>speechrecognition</category>
    </item>
    <item>
      <title>Can MeanVC Stream 200 ms Voice Conversion in Real Time on an M1 Max? 37.7 ms p95</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Sun, 19 Jul 2026 12:52:02 +0000</pubDate>
      <link>https://dev.to/kiarina/can-meanvc-stream-200-ms-voice-conversion-in-real-time-on-an-m1-max-377-ms-p95-1l9b</link>
      <guid>https://dev.to/kiarina/can-meanvc-stream-200-ms-voice-conversion-in-real-time-on-an-m1-max-377-ms-p95-1l9b</guid>
      <description>&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;It would be interesting if a Mac could keep what I say while changing only the characteristics of my voice, all locally and in real time.&lt;/p&gt;

&lt;p&gt;Today, I am testing MeanVC's 200 ms streaming model on an Apple M1 Max. I will measure processing speed, the shift in speaker characteristics, and discontinuities between audio chunks.&lt;/p&gt;

&lt;p&gt;The short answer is that one CPU thread converted a 200 ms audio chunk in &lt;strong&gt;37.7 ms at p95&lt;/strong&gt;. All 99 measured chunks finished before the next chunk arrived, and the converted voice became more similar to the target reference than to the source speaker. However, estimated time to the first output was about 0.28–0.37 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is MeanVC?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2510.08392" rel="noopener noreferrer"&gt;MeanVC&lt;/a&gt; is a lightweight streaming zero-shot voice conversion model whose paper was published on &lt;strong&gt;October 9, 2025&lt;/strong&gt;. &lt;code&gt;Voice conversion&lt;/code&gt; changes vocal characteristics while preserving the spoken content. &lt;code&gt;Zero-shot&lt;/code&gt; means that a target speaker can be specified from reference audio without training a separate model for that speaker.&lt;/p&gt;

&lt;p&gt;MeanVC divides audio into short chunks and carries context forward while converting them. It also uses MeanFlow to move from noise to generated audio features in a small number of steps. I used the officially released 200 ms model with two inference steps.&lt;/p&gt;

&lt;p&gt;The published &lt;a href="https://github.com/ASLP-lab/MeanVC" rel="noopener noreferrer"&gt;MeanVC code&lt;/a&gt; and &lt;a href="https://huggingface.co/ASLP-lab/MeanVC" rel="noopener noreferrer"&gt;pretrained model&lt;/a&gt; use the Apache License 2.0. It permits use, modification, and redistribution subject to conditions such as preserving copyright and license notices. The original WavLM implementation uses the MIT License, while S3PRL, which loads WavLM here, uses Apache License 2.0. The evaluation-only &lt;a href="https://huggingface.co/pranjal-pravesh/ecapa_tdnn_onnx" rel="noopener noreferrer"&gt;ECAPA-TDNN ONNX model&lt;/a&gt; uses the MIT License.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model Roles and Data Flow
&lt;/h2&gt;

&lt;p&gt;The conversion path uses four pretrained models.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fast-U2++&lt;/td&gt;
&lt;td&gt;Extracts linguistic content features from the source audio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WavLM speaker encoder&lt;/td&gt;
&lt;td&gt;Extracts target-speaker characteristics from the reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MeanFlow DiT&lt;/td&gt;
&lt;td&gt;Generates a converted audio spectrogram from the content and speaker features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vocos&lt;/td&gt;
&lt;td&gt;Decodes the generated spectrogram into a playable waveform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The data flows as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target reference -&amp;gt; WavLM + mel features ----┐
                                             v
source -&amp;gt; 200 ms chunks -&amp;gt; Fast-U2++ -&amp;gt; MeanFlow DiT -&amp;gt; Vocos -&amp;gt; converted audio

evaluation only:
source / target / converted -&amp;gt; ECAPA-TDNN -&amp;gt; compare speaker-feature distances
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Mel&lt;/code&gt; features describe energy at different frequencies on a scale closer to human hearing. ECAPA-TDNN is not involved in conversion. I used it only to check whether an independent model also saw a shift toward the target speaker.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;p&gt;The experiment asks three questions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can each 200 ms chunk finish before the next one arrives?&lt;/li&gt;
&lt;li&gt;Do the converted speaker features move closer to the target than the source?&lt;/li&gt;
&lt;li&gt;Do the 200 ms boundaries create large waveform discontinuities?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete code, pinned model revisions, SHA-256 hashes, and JSON report are available in the &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/19/meanvc-streaming-apple-silicon" rel="noopener noreferrer"&gt;meanvc-streaming-apple-silicon lab in kiarina/labs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the Lab
&lt;/h2&gt;

&lt;p&gt;You will need &lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, FFmpeg, and an internet connection for the initial download. The first run downloads about 2.7 GiB of models and related files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml &lt;span class="se"&gt;\&lt;/span&gt;
  2026/07/19/meanvc-streaming-apple-silicon
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/19/meanvc-streaming-apple-silicon run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The converted WAV files and measurement report are written to &lt;code&gt;output/&lt;/code&gt;. To try audio files or a live microphone from a browser, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/19/meanvc-streaming-apple-silicon run demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmjkk69a16ex383pi62h.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmjkk69a16ex383pi62h.webp" alt="MeanVC Voice Transfer Lab interface with source audio, target audio, and live microphone conversion" width="800" height="749"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser tool accepts source audio to convert and target audio as the voice reference. It also includes a button for the same synthetic sample used in this experiment and a live mode that converts microphone input in 200 ms chunks. Audio is processed only on localhost and is not uploaded to an external service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Conditions
&lt;/h2&gt;

&lt;p&gt;I used a 14.171-second synthetic conversation between two speakers. Three turns from Speaker 1, totaling 6.588 seconds, formed the source. Three different turns from Speaker 2, totaling 7.583 seconds, formed the target reference. No utterance appeared in both sets.&lt;/p&gt;

&lt;p&gt;The main conditions were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mac&lt;/td&gt;
&lt;td&gt;MacBook Pro, Apple M1 Max, 64 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;PyTorch CPU, one thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio&lt;/td&gt;
&lt;td&gt;16 kHz, mono&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chunk&lt;/td&gt;
&lt;td&gt;245 ms first, then 200 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MeanFlow&lt;/td&gt;
&lt;td&gt;Two steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Measurement&lt;/td&gt;
&lt;td&gt;33 chunks × 3 trials, 99 chunks total&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Instead of a physical microphone, the lab fed an audio file incrementally in microphone order. This measures model compute time but excludes buffering in the microphone, virtual audio device, and speakers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  200 ms Converted in 37.7 ms at p95
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Observed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200 ms chunk inference p50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35.1 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 ms chunk inference p95&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37.7 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 ms chunk inference p99&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;38.0 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum&lt;/td&gt;
&lt;td&gt;38.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deadline misses&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 / 99 chunks&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak resident memory&lt;/td&gt;
&lt;td&gt;2,849 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;p95&lt;/code&gt; means that 95% of measurements were at or below that value. Here, most 200 ms chunks finished within 37.7 ms. That left about 162 ms before the next chunk arrived, and the model never fell behind during the measurement.&lt;/p&gt;

&lt;p&gt;The first sound still has to wait for the initial 245 ms of input and its inference. Estimated first-output latency was 277 ms at p50, 359 ms at p95, and 368 ms at maximum. Continuous processing has ample compute headroom, but this is not a 20–100 ms low-latency voice changer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speaker Features Shifted Toward the Target
&lt;/h3&gt;

&lt;p&gt;ECAPA-TDNN represented each audio sample as 192 numbers, and I compared them with cosine similarity. A value closer to one means that the model considers the speaker characteristics more similar.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pair&lt;/th&gt;
&lt;th&gt;Similarity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source − target reference&lt;/td&gt;
&lt;td&gt;0.210&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Converted − source&lt;/td&gt;
&lt;td&gt;0.320&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Converted − target reference&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.725&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The converted voice was clearly closer to the target reference than to the source. Individual cross-speaker turns averaged 0.175, so 0.725 was a substantial shift for this sample.&lt;/p&gt;

&lt;p&gt;This score alone does not mean “the same person” or “natural speech.” Naturalness, pronunciation, and listener judgments of identity require separate evaluation.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Large Discontinuity Found at 200 ms Boundaries
&lt;/h3&gt;

&lt;p&gt;I measured the sample-value difference across 32 chunk boundaries. Boundary differences had a p95 of 0.0462, while ordinary adjacent differences across the full audio had a p95 of 0.0567. The boundaries were not systematically larger. The converted waveform also contained 0% clipped samples.&lt;/p&gt;

&lt;p&gt;This is a waveform check, not a human listening test for click noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Plain-Language Reading of the Results
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The M1 Max CPU converted audio faster than it arrived.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each 200 ms of speech took about 35–38 ms to process, and all 99 chunks met their deadline. The compute speed is suitable for applications such as streaming or online meetings that can tolerate some delayed output.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The voice moved in the target speaker's direction.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An independent speaker model also found the converted audio more similar to the target than the source. This does not establish naturalness or pronunciation quality.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;“Real-time processing” is not the same as “imperceptible latency.”&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sustained conversion was fast, but the first output took about 0.28–0.37 seconds. That delay may be very noticeable when monitoring your own voice through headphones.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Japanese
&lt;/h2&gt;

&lt;p&gt;The MeanVC paper trained the conversion model primarily on Mandarin speech. Fast-U2++, which extracts content, was also trained on the Mandarin-centered WenetSpeech dataset. The Japanese input used here is outside the main language conditions evaluated in the paper.&lt;/p&gt;

&lt;p&gt;Japanese features such as geminate consonants, long vowels, and pitch accent may not be fully preserved. With one Japanese input and one speaker pair, this experiment cannot isolate language-related degradation. For the reference, clean single-speaker audio with little noise or reverberation is the safest starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Responsible Use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The evaluation used one short Japanese conversation and one speaker pair.&lt;/li&gt;
&lt;li&gt;It did not measure full microphone-to-speaker latency.&lt;/li&gt;
&lt;li&gt;It did not run listening tests for naturalness, intelligibility, or pronunciation preservation.&lt;/li&gt;
&lt;li&gt;It did not test noise, singing, laughter, whispering, or long sessions.&lt;/li&gt;
&lt;li&gt;It used PyTorch CPU, not MPS, ONNX, or Core ML.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Voice conversion should only use audio whose speaker has consented to both its processing and intended use. This experiment used a synthetic conversation from shared test assets and did not create a model that imitates a real person.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Takeaway
&lt;/h2&gt;

&lt;p&gt;Because the model waits for 200 ms of input, I expected the conversion itself to be fairly heavy. In practice, one CPU thread reached 37.7 ms at p95, leaving substantial headroom for continuous processing. The independent speaker model also confirmed a shift toward the target, making this more than a speed-only demonstration.&lt;/p&gt;

&lt;p&gt;The full zero-shot path is less lightweight than the conversion core alone suggests: WavLM brings the initial download to about 2.7 GiB and peak memory to about 2.8 GB. Even so, MeanVC could work well for local streaming effects or prototyping character voices with consent when a modest delay is acceptable.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pytorch</category>
      <category>machinelearning</category>
      <category>audio</category>
    </item>
    <item>
      <title>Real-Time Webcam-to-VRM Retargeting with MediaPipe Holistic: 17.3 FPS on M1 Max</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Sat, 18 Jul 2026 11:37:06 +0000</pubDate>
      <link>https://dev.to/kiarina/real-time-webcam-to-vrm-retargeting-with-mediapipe-holistic-173-fps-on-m1-max-4be3</link>
      <guid>https://dev.to/kiarina/real-time-webcam-to-vrm-retargeting-with-mediapipe-holistic-173-fps-on-m1-max-4be3</guid>
      <description>&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;If I raise my hand in front of a webcam, can a 3D avatar raise its hand too? More specifically, can a browser do this without dedicated motion-capture hardware?&lt;/p&gt;

&lt;p&gt;Today, I am testing MediaPipe Holistic as a way to estimate the body, face, and both hands, then retarget the result to a VRM 1.0 avatar in real time.&lt;/p&gt;

&lt;p&gt;The short answer is that an Apple M1 Max processed a 1280×720 webcam stream at an &lt;strong&gt;effective 17.3 FPS&lt;/strong&gt; using CPU/WASM. Every one of the 141 frames with a detected pose updated the VRM skeleton. Body tracking worked, but this short measurement did not establish stable simultaneous capture of the face and both hands.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is MediaPipe Holistic?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://research.google/blog/mediapipe-holistic-simultaneous-face-hand-and-pose-prediction-on-device/" rel="noopener noreferrer"&gt;MediaPipe Holistic&lt;/a&gt; is a pipeline that Google &lt;strong&gt;announced on December 10, 2020&lt;/strong&gt; for estimating the body, face, and both hands from one camera. A &lt;code&gt;landmark&lt;/code&gt; is a point representing a location such as a shoulder, elbow, or fingertip. The announced version returned 543 points: 33 for the pose, 468 for the face, and 21 for each hand. The bundle used here includes Face Mesh V2 with ten additional iris points, bringing the face output to 478 and the total to 553.&lt;/p&gt;

&lt;p&gt;The 13.7 MB &lt;code&gt;holistic_landmarker.task&lt;/code&gt; used here contains seven TensorFlow Lite model files. Their roles can be summarized as follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pose detector&lt;/td&gt;
&lt;td&gt;Finds the person in the image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pose landmarks detector&lt;/td&gt;
&lt;td&gt;Estimates 33 body points and 3D coordinates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;face detector&lt;/td&gt;
&lt;td&gt;Finds the face region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;face landmarks detector&lt;/td&gt;
&lt;td&gt;Estimates 478 facial points, including the irises&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;face blendshapes&lt;/td&gt;
&lt;td&gt;Produces 52 coefficients such as eye blink and jaw opening&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hand ROI refinement&lt;/td&gt;
&lt;td&gt;Corrects the hand crop for a closer look&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hand landmarks detector&lt;/td&gt;
&lt;td&gt;Estimates 21 points and is shared by the left and right hands&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Holistic first uses the body position to propose approximate face and hand regions. It then crops those areas from the full-resolution input. This multi-stage design avoids trying to read small fingertips from the already-downscaled whole-body image.&lt;/p&gt;

&lt;p&gt;I pinned the float16 task bundle updated on December 21, 2023. Its component documentation dates BlazePose to April 2021, Hand Tracking to October 2021, Face Mesh V2 to September 2022, and Blendshape V2 to November 2022. The model cards for &lt;a href="https://storage.googleapis.com/mediapipe-assets/Model%20Card%20BlazePose%20GHUM%203D.pdf" rel="noopener noreferrer"&gt;BlazePose GHUM 3D&lt;/a&gt;, &lt;a href="https://storage.googleapis.com/mediapipe-assets/Model%20Card%20Hand%20Tracking%20%28Lite_Full%29%20with%20Fairness%20Oct%202021.pdf" rel="noopener noreferrer"&gt;Hand Tracking&lt;/a&gt;, &lt;a href="https://storage.googleapis.com/mediapipe-assets/Model%20Card%20MediaPipe%20Face%20Mesh%20V2.pdf" rel="noopener noreferrer"&gt;Face Mesh V2&lt;/a&gt;, and &lt;a href="https://storage.googleapis.com/mediapipe-assets/Model%20Card%20Blendshape%20V2.pdf" rel="noopener noreferrer"&gt;Blendshape V2&lt;/a&gt; all specify the Apache License 2.0. MediaPipe itself and &lt;code&gt;@mediapipe/tasks-vision&lt;/code&gt; also use Apache License 2.0.&lt;/p&gt;

&lt;p&gt;For the lab's bundled avatar and the measurement, I used &lt;code&gt;Seed-san&lt;/code&gt;, an official sample model by VirtualCast, Inc. Its embedded settings point to the &lt;a href="https://vrm.dev/en/licenses/1.0/" rel="noopener noreferrer"&gt;VRM Public License 1.0&lt;/a&gt; and require credit. Redistribution and redistribution of modified data are allowed; its avatar permission is &lt;code&gt;everyone&lt;/code&gt;, and its commercial-use setting is &lt;code&gt;corporation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The demo video uses the tool's custom-VRM loading feature to display &lt;code&gt;AvatarSample_A&lt;/code&gt;, a VRoid Studio sample model. Its &lt;a href="https://vroid.pixiv.help/hc/en-us/articles/4402394424089-VRoidPreset-A-Z" rel="noopener noreferrer"&gt;official conditions of use&lt;/a&gt; allow free commercial and noncommercial use without attribution. The model is not CC0, and its copyright has not been waived. The &lt;code&gt;@pixiv/three-vrm&lt;/code&gt; and Three.js libraries use the MIT License.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;p&gt;The experiment asks five questions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can the browser version of Holistic Landmarker process a webcam stream in real time?&lt;/li&gt;
&lt;li&gt;Can 33 body landmarks be converted into VRM 1.0 bone rotations?&lt;/li&gt;
&lt;li&gt;Can 21 landmarks per hand drive the wrists and fingers?&lt;/li&gt;
&lt;li&gt;Can facial coefficients be converted into VRM expressions?&lt;/li&gt;
&lt;li&gt;Does smoothing keep the response interactive while reducing jitter?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete code and measurement notes are available in the &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/18/mediapipe-holistic-vrm" rel="noopener noreferrer"&gt;mediapipe-holistic-vrm lab in kiarina/labs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the Lab
&lt;/h2&gt;

&lt;p&gt;You will need &lt;code&gt;mise&lt;/code&gt;, a browser with WebGL and camera-input support, and an internet connection for the first download. &lt;code&gt;mise&lt;/code&gt; installs Node.js 22.22.0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml &lt;span class="se"&gt;\&lt;/span&gt;
  2026/07/18/mediapipe-holistic-vrm
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/18/mediapipe-holistic-vrm run
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/18/mediapipe-holistic-vrm run preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the displayed localhost URL and select &lt;code&gt;カメラを開始&lt;/code&gt; (Start Camera). The initial setup downloads the MediaPipe bundle and Seed-san, then verifies their SHA-256 hashes. Camera input and inference remain inside the browser and are not sent to an external server. You can also load another VRM file with the file picker or by dropping it onto the viewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;webcam (1280×720)
  -&amp;gt; Holistic Landmarker (body, face, and both hands)
  -&amp;gt; landmarks and 52 face blendshapes
  -&amp;gt; MediaPipe-to-Three.js coordinate conversion
  -&amp;gt; directions and body, face, and palm orientation
  -&amp;gt; 34 VRM 1.0 bones and expressions
  -&amp;gt; interpolation against the previous frame to reduce jitter
  -&amp;gt; Three.js rendering
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Retargeting&lt;/code&gt; means transferring an estimated human pose to another skeleton. For an arm or leg, I align the direction between two landmarks, such as shoulder to elbow, with the corresponding bone's rest direction in the VRM. The hips, shoulders, face, and palms use multiple points to build three axes, providing an approximation of torso rotation and palm orientation as well.&lt;/p&gt;

&lt;p&gt;The hand chains map MediaPipe joints to VRM finger bones. For the face, mappings include &lt;code&gt;eyeBlinkLeft&lt;/code&gt; to &lt;code&gt;blinkLeft&lt;/code&gt; and &lt;code&gt;jawOpen&lt;/code&gt; to &lt;code&gt;aa&lt;/code&gt;. This is not speech recognition; it is a simple conversion from facial-shape coefficients to avatar expressions.&lt;/p&gt;

&lt;p&gt;Small changes in estimated points look like shaking when applied directly to an avatar. The implementation therefore holds changes below two degrees for normal bones and below three degrees for wrists and fingers, then interpolates larger movements over time. Leg landmarks below 0.65 visibility are rejected. If the whole pose is lost for more than 0.5 seconds, the avatar returns toward its neutral pose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I connected to the local server from the Codex in-app browser on a MacBook Pro with an Apple M1 Max and 64 GB of memory, running macOS 26.5.2. Inference ran through browser WASM using the CPU XNNPACK delegate.&lt;/p&gt;

&lt;p&gt;The following values came from a 12-second measurement. I did not control the subject to remain fully visible throughout the run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Observed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;1280×720&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processed frames&lt;/td&gt;
&lt;td&gt;207&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective throughput&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.3 FPS&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean inference time&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;49.17 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median inference time&lt;/td&gt;
&lt;td&gt;59.60 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inference time p95&lt;/td&gt;
&lt;td&gt;70.60 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final inference rate&lt;/td&gt;
&lt;td&gt;23 FPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final render rate&lt;/td&gt;
&lt;td&gt;43 FPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pose detected&lt;/td&gt;
&lt;td&gt;141 / 207 frames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pose applied to VRM&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;141 / 141 detected frames&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right hand detected&lt;/td&gt;
&lt;td&gt;1 / 207 frames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Left hand detected&lt;/td&gt;
&lt;td&gt;0 / 207 frames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Face detected&lt;/td&gt;
&lt;td&gt;0 / 207 frames&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The end-to-end path from webcam input through Holistic inference and coordinate conversion to VRM 1.0 loading and bone updates worked. The 49.17 ms mean measures the model call's wall time. It is not glass-to-glass latency including camera exposure and display scanout.&lt;/p&gt;

&lt;p&gt;The demo video shows the tool in operation with VRoid Studio's &lt;code&gt;AvatarSample_A&lt;/code&gt;, rather than the Seed-san model bundled with the lab. The raw camera feed can be hidden without stopping inference, which makes the landmarks and avatar response easier to see.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/-GxEKqtnJdI"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Read the Zero Hand and Face Counts
&lt;/h3&gt;

&lt;p&gt;These zeros are not an accuracy result saying that the model cannot detect hands or faces. During the short whole-body measurement, the face and hands were small and were not kept in a controlled, continuously detectable framing. A separate short run did produce a frame where the body and face were active together.&lt;/p&gt;

&lt;p&gt;All finger-bone and expression conversions are implemented. Synthetic-input tests confirmed finger rotation and the &lt;code&gt;jawOpen -&amp;gt; aa&lt;/code&gt; mapping. However, this experiment does not tell us how stable simultaneous face, hand, and full-body tracking is with a real camera.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Throughput Means
&lt;/h3&gt;

&lt;p&gt;At 17.3 FPS, the pose can be updated about 17 times per second. That is usable for checking movement and interactive demos, but it is not smooth 60 FPS motion capture.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;@mediapipe/tasks-vision&lt;/code&gt; 0.10.35, &lt;code&gt;detectForVideo&lt;/code&gt; is synchronous. Rendering on the same main thread therefore waits during inference. A frame that takes the measured p95 of 70.60 ms also blocks rendering for that time; inference and rendering are not fully isolated.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Plain-Language Reading of the Results
&lt;/h2&gt;

&lt;p&gt;The findings come down to three points.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A normal webcam drove a 3D avatar.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All 141 frames with a detected pose reached the VRM update. The basic path from body movement to a browser avatar worked without a dedicated sensor.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The speed was suitable for a demo, not smooth motion capture.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Effective throughput was 17.3 FPS and mean inference time was 49.17 ms. The response is visible, but fast movement or live-production use may show stutter.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;This was not yet a complete face-and-finger evaluation.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 12-second run was not a controlled framing test. Successful body retargeting and stable simultaneous tracking of the body, face, and both hands are separate claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This retargeter is a lightweight directional approximation. Aligning two points leaves rotation around the bone axis unresolved, so forearm, wrist, and ankle twist is not exact. It also lacks hip translation, foot locking, and floor-contact IK. &lt;code&gt;IK&lt;/code&gt;, or inverse kinematics, calculates joint angles backward from a target hand or foot position. Without it, the feet can slide while the root remains fixed.&lt;/p&gt;

&lt;p&gt;The evaluation is also limited to one M1 Max, 12 seconds, and 207 frames. I did not measure model accuracy, different lighting and backgrounds, multiple people, long-run stability, or full glass-to-glass latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Takeaway
&lt;/h2&gt;

&lt;p&gt;MediaPipe Holistic removed much of the plumbing required to connect separate body, face, and hand estimators. It provided a practical foundation for reaching a VRM avatar entirely inside the browser. Seeing visible body response from CPU/WASM on an M1 Max was more usable than I expected.&lt;/p&gt;

&lt;p&gt;At the same time, the ability to output 553 points does not guarantee stable simultaneous full-body, facial, and finger capture from one monocular camera. A design centered on the body tracking demonstrated here, with face and hand tracking used according to framing and purpose, could work well for lightweight avatar demos or in-browser gesture interfaces.&lt;/p&gt;

</description>
      <category>mediapipe</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Can YAMNet Detect Unseen Sudden Sounds in Real Time? A 48-Stream Evaluation</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Fri, 17 Jul 2026 01:53:43 +0000</pubDate>
      <link>https://dev.to/kiarina/can-yamnet-detect-unseen-sudden-sounds-in-real-time-a-48-stream-evaluation-49d7</link>
      <guid>https://dev.to/kiarina/can-yamnet-detect-unseen-sudden-sounds-in-real-time-a-48-stream-evaluation-49d7</guid>
      <description>&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;There are many situations where we may want to monitor sounds that happen without warning, such as breaking glass or a car horn. Registering every possible sound in advance, however, is not realistic.&lt;/p&gt;

&lt;p&gt;Today, I am testing whether YAMNet can detect that a sound stream has changed, without first specifying which sound it should recognize.&lt;/p&gt;

&lt;p&gt;The short answer is that the best configuration detected 22 of 48 events, for &lt;strong&gt;45.8% recall&lt;/strong&gt;. Processing was easily fast enough, but the results did not support the hypothesis that a simple distance over YAMNet embeddings can reliably detect unseen sudden sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is YAMNet?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tensorflow/models/tree/master/research/audioset/yamnet" rel="noopener noreferrer"&gt;YAMNet&lt;/a&gt; is an audio classification model that Google &lt;a href="https://github.com/tensorflow/models/commit/dfffd623b6be8d1d9744b8e261fbac370d17c46d" rel="noopener noreferrer"&gt;&lt;strong&gt;added to TensorFlow Models on November 21, 2019&lt;/strong&gt;&lt;/a&gt;. It was trained on AudioSet and predicts 521 acoustic event classes, including speech, rain, vehicles, and animals.&lt;/p&gt;

&lt;p&gt;The network uses MobileNet V1, a convolutional architecture designed to reduce computation on mobile hardware. It reads roughly 0.96 seconds of 16 kHz mono audio at a time and returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;scores&lt;/code&gt; (521 dimensions): confidence for each class&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;embedding&lt;/code&gt; (1,024 dimensions): a compact numerical representation of the sound&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spectrogram&lt;/code&gt; (64 bands): frequency content over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This experiment needs the embedding as well as the class scores, so I used the &lt;a href="https://tfhub.dev/google/yamnet/1" rel="noopener noreferrer"&gt;SavedModel version on TensorFlow Hub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The relevant licenses are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/tensorflow/models/blob/master/LICENSE" rel="noopener noreferrer"&gt;TensorFlow Models, including YAMNet&lt;/a&gt;: Apache License 2.0&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tensorflow/tensorflow/blob/master/LICENSE" rel="noopener noreferrer"&gt;TensorFlow&lt;/a&gt;: Apache License 2.0&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/karolpiczak/ESC-50/blob/master/LICENSE" rel="noopener noreferrer"&gt;ESC-50&lt;/a&gt;: Creative Commons Attribution-NonCommercial 3.0 (CC BY-NC 3.0)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ESC-50 includes a noncommercial restriction. The model and dataset have separate licenses, so both need to be considered for redistribution or product use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;p&gt;The goal was to determine whether the detectors could notice a new sound mixed into a normal background, without being told the event label.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether a simple frequency-change detector can catch sudden events&lt;/li&gt;
&lt;li&gt;Whether YAMNet scores or embeddings represent the change more clearly&lt;/li&gt;
&lt;li&gt;Whether distance from the previous sound works better than distance from a memory of normal sounds&lt;/li&gt;
&lt;li&gt;Whether the pipeline can process audio faster than real time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete code and results are available in the &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/17/yamnet-streaming-novelty" rel="noopener noreferrer"&gt;yamnet-streaming-novelty lab in kiarina/labs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the Lab
&lt;/h2&gt;

&lt;p&gt;You will need &lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, FFmpeg, and an internet connection for the first download. The model and data use about 120 MB, while the Python environment uses about 1.3 GB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml &lt;span class="se"&gt;\&lt;/span&gt;
  2026/07/17/yamnet-streaming-novelty
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/17/yamnet-streaming-novelty run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run downloads the pinned YAMNet model and 208 required WAV files from a fixed ESC-50 revision. The task verifies the SHA-256 of the YAMNet archive and writes the results to &lt;code&gt;output/report.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roles and Data Flow
&lt;/h2&gt;

&lt;p&gt;YAMNet is the only AI model in this pipeline. Small detectors for frequency change and vector distance operate around it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;16 kHz mono audio stream
  ├─ frequency change every 32 ms ─────────&amp;gt; spectral flux
  └─ 0.975 s window sent to YAMNet every 0.48 s
       ├─ 521 class scores
       │    ├─ distance from previous frame -&amp;gt; score delta
       │    └─ distance from normal memory -&amp;gt; score kNN
       └─ 1,024-dimensional embedding
            ├─ distance from previous frame -&amp;gt; embedding delta
            └─ distance from normal memory -&amp;gt; embedding kNN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;delta&lt;/code&gt; detectors measure change from the immediately previous sound. The &lt;code&gt;kNN&lt;/code&gt; detectors compare each frame with the five nearest examples in a memory of normal sounds. They use cosine distance, which measures the difference in direction between numerical vectors.&lt;/p&gt;

&lt;p&gt;I also evaluated &lt;code&gt;temporal fusion&lt;/code&gt;, which produces an alert when either score delta or embedding delta fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation Setup
&lt;/h2&gt;

&lt;p&gt;The normal sounds were &lt;code&gt;rain&lt;/code&gt;, &lt;code&gt;sea_waves&lt;/code&gt;, &lt;code&gt;wind&lt;/code&gt;, and &lt;code&gt;clock_tick&lt;/code&gt;. I inserted one of &lt;code&gt;crying_baby&lt;/code&gt;, &lt;code&gt;door_wood_knock&lt;/code&gt;, &lt;code&gt;glass_breaking&lt;/code&gt;, &lt;code&gt;siren&lt;/code&gt;, &lt;code&gt;car_horn&lt;/code&gt;, and &lt;code&gt;fireworks&lt;/code&gt; from 2.0 to 3.0 seconds in the same five-second background clip. There were eight examples of each event, for 48 positive streams in total.&lt;/p&gt;

&lt;p&gt;Event-to-background level was tested at -10, -5, 0, and +5 dB. At -10 dB, the event is substantially quieter than the background; at +5 dB, it is louder. The mixed streams existed only in memory during evaluation and were not saved as files.&lt;/p&gt;

&lt;p&gt;Recordings were separated by purpose:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;ESC-50 fold&lt;/th&gt;
&lt;th&gt;Number of clips&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Normal-sound memory&lt;/td&gt;
&lt;td&gt;1-3&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threshold calibration&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False-alert evaluation&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sudden-event evaluation&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;48 streams&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each threshold was set high enough to produce no alerts on the normal fold-4 calibration streams. I did not tune it against the final fold-5 evaluation set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which Score Ranked Anomalies Best?
&lt;/h3&gt;

&lt;p&gt;I first compared whether positive streams received higher anomaly scores than negative streams without fixing a threshold. AUROC summarizes this ranking: 1.0 is ideal, while a value around 0.5 is close to chance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Detector&lt;/th&gt;
&lt;th&gt;AUROC&lt;/th&gt;
&lt;th&gt;-10 dB&lt;/th&gt;
&lt;th&gt;-5 dB&lt;/th&gt;
&lt;th&gt;0 dB&lt;/th&gt;
&lt;th&gt;+5 dB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;spectral flux&lt;/td&gt;
&lt;td&gt;0.449&lt;/td&gt;
&lt;td&gt;0.393&lt;/td&gt;
&lt;td&gt;0.315&lt;/td&gt;
&lt;td&gt;0.555&lt;/td&gt;
&lt;td&gt;0.534&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;score delta&lt;/td&gt;
&lt;td&gt;0.717&lt;/td&gt;
&lt;td&gt;0.622&lt;/td&gt;
&lt;td&gt;0.672&lt;/td&gt;
&lt;td&gt;0.776&lt;/td&gt;
&lt;td&gt;0.797&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;embedding delta&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.734&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.638&lt;/td&gt;
&lt;td&gt;0.625&lt;/td&gt;
&lt;td&gt;0.815&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.859&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;score kNN&lt;/td&gt;
&lt;td&gt;0.661&lt;/td&gt;
&lt;td&gt;0.484&lt;/td&gt;
&lt;td&gt;0.698&lt;/td&gt;
&lt;td&gt;0.789&lt;/td&gt;
&lt;td&gt;0.672&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;embedding kNN&lt;/td&gt;
&lt;td&gt;0.632&lt;/td&gt;
&lt;td&gt;0.479&lt;/td&gt;
&lt;td&gt;0.581&lt;/td&gt;
&lt;td&gt;0.721&lt;/td&gt;
&lt;td&gt;0.745&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Embedding delta ranked first at 0.734, but it was only 0.018 ahead of score delta. These 80 evaluation streams are not enough to conclude that embeddings are generally better.&lt;/p&gt;

&lt;p&gt;The broader pattern is clearer: distance from the previous frame worked better than distance from the normal-sound memory. For short events like these, asking "did the sound suddenly change?" was more useful than asking "is this sound globally unlike the normal set?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Alerts with Strict Thresholds
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Detector&lt;/th&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;Recall&lt;/th&gt;
&lt;th&gt;F1&lt;/th&gt;
&lt;th&gt;False alerts/hour&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;spectral flux&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;score delta&lt;/td&gt;
&lt;td&gt;0.810&lt;/td&gt;
&lt;td&gt;35.4%&lt;/td&gt;
&lt;td&gt;0.493&lt;/td&gt;
&lt;td&gt;22.5&lt;/td&gt;
&lt;td&gt;0.415 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;embedding delta&lt;/td&gt;
&lt;td&gt;0.765&lt;/td&gt;
&lt;td&gt;27.1%&lt;/td&gt;
&lt;td&gt;0.400&lt;/td&gt;
&lt;td&gt;22.5&lt;/td&gt;
&lt;td&gt;0.415 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;score kNN&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;6.2%&lt;/td&gt;
&lt;td&gt;0.118&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0.895 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;embedding kNN&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;td&gt;16.7%&lt;/td&gt;
&lt;td&gt;0.276&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0.895 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;score delta + embedding delta&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.786&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;45.8%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.579&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;22.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.415 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Precision is the proportion of emitted alerts that were correct. Recall is the proportion of the 48 events that were found. The best temporal-fusion detector found 22 and missed 26.&lt;/p&gt;

&lt;p&gt;There was only one false alert in 160 seconds of normal audio. The reported 22.5 false alerts/hour extrapolates that single event to one hour, so it is a highly uncertain estimate rather than a production false-alert rate. Several hours of continuous audio would be needed for a useful measurement.&lt;/p&gt;

&lt;p&gt;Recall by event level was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event level&lt;/th&gt;
&lt;th&gt;Detected&lt;/th&gt;
&lt;th&gt;Recall&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-10 dB&lt;/td&gt;
&lt;td&gt;4/12&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;td&gt;1.855 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-5 dB&lt;/td&gt;
&lt;td&gt;3/12&lt;/td&gt;
&lt;td&gt;25.0%&lt;/td&gt;
&lt;td&gt;0.415 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0 dB&lt;/td&gt;
&lt;td&gt;7/12&lt;/td&gt;
&lt;td&gt;58.3%&lt;/td&gt;
&lt;td&gt;0.415 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+5 dB&lt;/td&gt;
&lt;td&gt;8/12&lt;/td&gt;
&lt;td&gt;66.7%&lt;/td&gt;
&lt;td&gt;0.415 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By class, the detector found 6/8 glass-breaking events, 5/8 fireworks, and 4/8 car horns. It found only 2/8 crying-baby and 2/8 siren events. Short, sharp changes were relatively easy, while sounds that blended into the background or were weak in the selected one-second segment were harder.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Did Not Work
&lt;/h3&gt;

&lt;p&gt;With a looser threshold, simple spectral flux reached 29.2% recall but produced 967.5 false alerts/hour. Rain and waves naturally contain many frequency changes, so this score could not isolate unusual events.&lt;/p&gt;

&lt;p&gt;Combining spectral flux with embedding kNN also failed to help. Under the strict threshold it matched embedding kNN alone at 16.7% recall. Under the looser threshold it reached 45.8% recall but produced 607.5 false alerts/hour. Adding detectors was not automatically an improvement.&lt;/p&gt;

&lt;p&gt;A high novelty score also does not guarantee a correct YAMNet label. &lt;code&gt;Glass&lt;/code&gt; was the top label for only three of the eight glass-breaking events. At low event levels, background labels such as &lt;code&gt;Water&lt;/code&gt;, &lt;code&gt;Rain&lt;/code&gt;, and &lt;code&gt;Vehicle&lt;/code&gt; often remained on top. Detecting that something changed and explaining what changed are separate problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Processing Speed
&lt;/h3&gt;

&lt;p&gt;I processed 1,040 seconds of audio as sequential windows on a Mac Studio with an Apple M4 Max.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature extraction elapsed: 6.262 s
real-time factor:           0.0060x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A repeat run while writing this article took 6.921 seconds, for a real-time factor of 0.0067x. The detection counts, AUROC values, thresholds, and latencies matched the original run. Including YAMNet inference and spectral flux, both runs processed audio more than 150 times faster than its duration. Compute throughput was not a problem.&lt;/p&gt;

&lt;p&gt;However, YAMNet reads about 0.96 seconds at a time, so the shortest observed detection latency was still 0.415 seconds. Fast inference does not remove the wait introduced by the input window.&lt;/p&gt;

&lt;p&gt;The verification environment was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;machine: Mac Studio (Mac16,9)
chip: Apple M4 Max
OS: macOS 26.5.2, arm64
Python: 3.12.10
TensorFlow: 2.21.0
NumPy: 2.3.5
FFmpeg: 8.1.2
random seed: 20260717
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Plain-Language Reading of the Results
&lt;/h2&gt;

&lt;p&gt;The results come down to three points.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The pipeline was fast enough.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It processed 1,040 seconds of audio in 6.262-6.921 seconds across two runs. The compute throughput needed for real-time monitoring was available.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Detection accuracy was not good enough.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The best method found only 22 of 48 events. Quiet events were especially likely to disappear into the background. This is not ready for a safety-monitoring application.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Recent change mattered more than distance from normal.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Comparing the embedding with a memory of normal sounds was weaker than comparing each frame with the previous one. Local change was the more useful signal for these short events.&lt;/p&gt;

&lt;p&gt;This experiment is limited to 48 synthetic five-second streams, 160 seconds of negative audio, ten selected ESC-50 categories, and one Mac. Continuous microphone input, several-hour false-alert measurements, different recording devices, and environments with multiple simultaneous events remain outside its scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Takeaway
&lt;/h2&gt;

&lt;p&gt;YAMNet was lightweight and easy to use as a foundation because it exposes both class scores and embeddings. Still, an embedding may contain useful acoustic information without its raw distance being a reliable anomaly score.&lt;/p&gt;

&lt;p&gt;In this experiment, recent change worked more directly than storing a large normal-sound memory. A lightweight first stage that proposes sharp events such as glass breaks or fireworks, followed by YAMNet labels or another decision step, looks like a more promising use of this approach.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tensorflow</category>
      <category>audio</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Estimating Surface Orientation and 3D from One Image with MoGe-2 on Apple Silicon</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Thu, 16 Jul 2026 00:47:39 +0000</pubDate>
      <link>https://dev.to/kiarina/estimating-surface-orientation-and-3d-from-one-image-with-moge-2-on-apple-silicon-2n0h</link>
      <guid>https://dev.to/kiarina/estimating-surface-orientation-and-3d-from-one-image-with-moge-2-on-apple-silicon-2n0h</guid>
      <description>&lt;h1&gt;
  
  
  Estimating Surface Orientation and 3D from One Image with MoGe-2 on Apple Silicon
&lt;/h1&gt;

&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;Estimating distance from one image is useful, but knowing surface orientation as well—whether a road faces upward or a wall faces sideways—provides richer information for 3D conversion and relighting.&lt;/p&gt;

&lt;p&gt;Today, I ran MoGe-2 ViT-S Normal on Apple Silicon and tested its surface-normal output, CPU and MPS speed, and consistency with the 3D information produced by the same inference.&lt;/p&gt;

&lt;p&gt;To give the result first, median inference time on an Apple M1 Max was &lt;strong&gt;212.13 ms&lt;/strong&gt; with MPS and &lt;strong&gt;1,259.34 ms&lt;/strong&gt; with the CPU, making MPS &lt;strong&gt;5.94 times faster&lt;/strong&gt;. Surface orientation and object boundaries were visible across a street, tabletop objects, and a crowd. I also turned single images of a street, a cat, and a girl into 2.5D animations with sideways camera movement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MoGe-2?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/microsoft/MoGe" rel="noopener noreferrer"&gt;MoGe-2&lt;/a&gt; is a monocular geometry estimation model released by researchers at Microsoft Research on &lt;strong&gt;June 10, 2025&lt;/strong&gt;. Monocular means that it uses a single RGB image rather than a stereo camera pair. Its paper was submitted to &lt;a href="https://arxiv.org/abs/2507.02546" rel="noopener noreferrer"&gt;arXiv&lt;/a&gt; on July 3 of the same year.&lt;/p&gt;

&lt;p&gt;While the original MoGe focused on relative 3D shape within an image, MoGe-2 adds metric scale, sharper details, and normal estimation in a unified model.&lt;/p&gt;

&lt;p&gt;A surface normal is a three-dimensional vector that tells us which way the surface at each pixel is facing. In the colorful normal maps in this article, the x, y, and z directions are mapped to RGB. The colors represent orientation, not object categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are testing
&lt;/h2&gt;

&lt;p&gt;I used &lt;code&gt;Ruicheng/moge-2-vits-normal&lt;/code&gt;, the smallest official model with normal output, to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether it runs on an Apple M1 Max CPU and MPS without patches&lt;/li&gt;
&lt;li&gt;How much faster MPS is than the CPU with the same FP32 model&lt;/li&gt;
&lt;li&gt;Whether normals are visually readable for a street, tabletop objects, and a crowd&lt;/li&gt;
&lt;li&gt;How closely the directly predicted normals agree with normals calculated from the 3D point map&lt;/li&gt;
&lt;li&gt;Whether the point map can be converted into a textured 3D mesh&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MPS is the PyTorch backend that uses the GPU in a Mac. The target lab is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/16/moge2-surface-normal-apple-silicon" rel="noopener noreferrer"&gt;kiarina/labs/2026/07/16/moge2-surface-normal-apple-silicon&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the environment
&lt;/h2&gt;

&lt;p&gt;You need an Apple Silicon Mac, &lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, and an internet connection for the initial model and shared-image downloads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml 2026/07/16/moge2-surface-normal-apple-silicon
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/16/moge2-surface-normal-apple-silicon run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run downloads a checkpoint at a fixed revision and verifies its SHA-256 hash. To also create a GLB and a four-second Blender video from the street image, install Blender 5.1.2 and FFmpeg, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/16/moge2-surface-normal-apple-silicon run render-video
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Model, mechanism, and licenses
&lt;/h2&gt;

&lt;p&gt;This test uses one pretrained model. It is not a pipeline that calls multiple AI models in sequence. Inside one checkpoint, an encoder reads image features and several heads produce task-specific outputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RGB image
  -&amp;gt; DINOv2 ViT-S encoder: extract global and local image features
  -&amp;gt; shared neck: expand them into multi-resolution features
  -&amp;gt; points head: 3D coordinate at each pixel
     normal head: surface orientation at each pixel
     mask head: reliable pixels
     scale head: metric scale in meters
  -&amp;gt; metric point map / depth / normal / mask / camera intrinsics
  -&amp;gt; optional: point map + source-image texture -&amp;gt; GLB mesh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A head is a small output component that turns shared features into values for one task. A point map assigns a 3D camera-space point to every pixel. Camera intrinsics describe internal camera properties related to focal length and field of view.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Pinned value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;checkpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ruicheng/moge-2-vits-normal/model.pt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;parameters&lt;/td&gt;
&lt;td&gt;35,103,656&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;file size&lt;/td&gt;
&lt;td&gt;140,550,416 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-256&lt;/td&gt;
&lt;td&gt;&lt;code&gt;79a16621928c2bf0ed04659218c55c01075e950507f40bb3332fb4c873d3e1dc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hugging Face revision&lt;/td&gt;
&lt;td&gt;&lt;code&gt;679230677b4d282c6f304189a93e98e14f085902&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoGe repository commit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;07444410f1e33f402353b99d6ccd26bd31e469e8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The licenses are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/microsoft/MoGe#%EF%B8%8F-license" rel="noopener noreferrer"&gt;MoGe code&lt;/a&gt;: MIT License&lt;/li&gt;
&lt;li&gt;DINOv2 code under &lt;code&gt;moge/model/dinov2&lt;/code&gt;: Apache License 2.0&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/Ruicheng/moge-2-vits-normal" rel="noopener noreferrer"&gt;The Hugging Face checkpoint used here&lt;/a&gt;: labeled MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lab does not commit the checkpoint. It downloads the pinned revision at runtime. Check the current official terms again before redistributing the model or embedding it in a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;I resized each input to a short side of 384 pixels and fixed the settings to &lt;code&gt;resolution_level=5&lt;/code&gt;, FP32, and batch size 1. For the street-image benchmark, I measured 10 runs after three warm-up runs. Model loading, image loading, preprocessing, and saving were excluded. Warm-up reduces the effect of one-time setup work on the timing.&lt;/p&gt;

&lt;p&gt;The comparison target for the predicted normals is not ground truth. I calculated a second normal from neighboring 3D points in the point map produced by the same inference, then measured the per-pixel angular difference. This tests internal consistency between two outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;p&gt;The input is on the left, and the directly predicted MoGe-2 normal is on the right.&lt;/p&gt;

&lt;h3&gt;
  
  
  Street
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyhhe50fecgdif66yiuww.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyhhe50fecgdif66yiuww.jpg" alt="Before and after comparison of a street image and its MoGe-2 surface normals" width="800" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The road, buildings on both sides, cars, and trees have distinct orientations. The sky is black because it was marked invalid.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tabletop objects
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8w7ezlqcylmb1xx50igf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8w7ezlqcylmb1xx50igf.jpg" alt="Before and after comparison of tabletop objects and their MoGe-2 surface normals" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Large surfaces on the desk and books have consistent colors, while the colors vary smoothly around the curved cup and bottle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crowd
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqqxhwkkekdd7oahdiooa.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqqxhwkkekdd7oahdiooa.jpg" alt="Before and after comparison of a crowd image and its MoGe-2 surface normals" width="800" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rounded faces and shoulders remain visible, as do boundaries between overlapping people. However, the distant background and small people also show unstable bands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CPU and MPS speed
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;backend&lt;/th&gt;
&lt;th&gt;mean&lt;/th&gt;
&lt;th&gt;median&lt;/th&gt;
&lt;th&gt;min&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;th&gt;std dev&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch CPU&lt;/td&gt;
&lt;td&gt;1,272.74 ms&lt;/td&gt;
&lt;td&gt;1,259.34 ms&lt;/td&gt;
&lt;td&gt;1,244.37 ms&lt;/td&gt;
&lt;td&gt;1,340.16 ms&lt;/td&gt;
&lt;td&gt;28.24 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch MPS&lt;/td&gt;
&lt;td&gt;211.58 ms&lt;/td&gt;
&lt;td&gt;212.13 ms&lt;/td&gt;
&lt;td&gt;208.75 ms&lt;/td&gt;
&lt;td&gt;214.68 ms&lt;/td&gt;
&lt;td&gt;1.88 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By median time, MPS was &lt;strong&gt;5.94 times faster&lt;/strong&gt; than the CPU. About 212 ms corresponds to roughly 4.71 FPS for model inference alone. The normal difference between CPU and MPS on the same input had a mean of 0.0063 degrees, a median of 0 degrees, and a maximum of 0.0396 degrees.&lt;/p&gt;

&lt;p&gt;I ran the same command again while writing this article. Median time was 1,299.96 ms on the CPU and 175.88 ms on MPS, making MPS 7.39 times faster in that run. The model hash, normal differences, and three-image consistency data matched the original measurement. Inference time varies with runtime conditions, so these two runs should be read as a measured range rather than one fixed speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consistency between two normal representations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;image&lt;/th&gt;
&lt;th&gt;valid pixels&lt;/th&gt;
&lt;th&gt;mean&lt;/th&gt;
&lt;th&gt;median&lt;/th&gt;
&lt;th&gt;p90&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;street&lt;/td&gt;
&lt;td&gt;90.80%&lt;/td&gt;
&lt;td&gt;30.75°&lt;/td&gt;
&lt;td&gt;18.69°&lt;/td&gt;
&lt;td&gt;78.55°&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;objects&lt;/td&gt;
&lt;td&gt;100.00%&lt;/td&gt;
&lt;td&gt;14.41°&lt;/td&gt;
&lt;td&gt;7.05°&lt;/td&gt;
&lt;td&gt;35.86°&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crowd&lt;/td&gt;
&lt;td&gt;98.63%&lt;/td&gt;
&lt;td&gt;37.94°&lt;/td&gt;
&lt;td&gt;33.04°&lt;/td&gt;
&lt;td&gt;73.47°&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two representations agreed most closely on the broad surfaces in the tabletop image. Differences increased at object boundaries, thin structures, trees, crowds, and distant details. Normals derived from a point map use differences between neighboring pixels, so they become unstable when those neighbors cross a sudden depth discontinuity. These angular differences should not be interpreted as the accuracy of the directly predicted normals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Converting three images into 3D animations
&lt;/h3&gt;

&lt;p&gt;I used the official CLI to turn each point map into triangles and create GLBs textured with the source images. I then moved a Blender camera sideways from the original viewpoint and rendered four-second animations that show the resulting parallax.&lt;/p&gt;

&lt;p&gt;The APNGs below are reduced to 512x288 at 10 FPS for article display. The lab produces the source MP4 files at 1280x720 and 30 FPS.&lt;/p&gt;

&lt;h4&gt;
  
  
  Street with cars
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuwzix47o59b9tqtv0f4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuwzix47o59b9tqtv0f4a.png" alt="Camera moving sideways through a 2.5D street created from one image with MoGe-2" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Parallax appears between the foreground cars, road, buildings on both sides, and people. Even a small sideways camera movement makes it clear that the single image has been converted into surfaces with depth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vertices: 233,120
triangles: 449,292
dimensions: approximately 30.75 x 114.21 x 21.15 m
estimated horizontal FoV: 60.52°
GLB size: 13,342,320 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Cat character
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5v7zz7011q2oiz86eibk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5v7zz7011q2oiz86eibk.png" alt="Camera moving sideways through a 2.5D cat-character scene created with MoGe-2" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pink cat is placed in front of the trees, bench, and flower bed, producing clear parallax. Its head has gentle curvature, but the ears, hands, bow tie, and body are thin shapes centered on surfaces visible from the source camera.&lt;/p&gt;

&lt;h4&gt;
  
  
  Girl character
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fut9y4qq3gxcaf4a46g6d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fut9y4qq3gxcaf4a46g6d.png" alt="Camera moving sideways through a 2.5D girl-character scene created with MoGe-2" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The girl, bench, streetlight, sign, and background trees are separated into different depths. However, the hair, arms, and legs are not complete independent volumes; they appear as thin surfaces following the visible outlines.&lt;/p&gt;

&lt;p&gt;The measured mesh data for all three scenes is below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;image&lt;/th&gt;
&lt;th&gt;vertices&lt;/th&gt;
&lt;th&gt;triangles&lt;/th&gt;
&lt;th&gt;FoV x / y&lt;/th&gt;
&lt;th&gt;GLB size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;street&lt;/td&gt;
&lt;td&gt;233,120&lt;/td&gt;
&lt;td&gt;449,292&lt;/td&gt;
&lt;td&gt;60.52° / 32.53°&lt;/td&gt;
&lt;td&gt;13,342,320 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cat&lt;/td&gt;
&lt;td&gt;312,105&lt;/td&gt;
&lt;td&gt;599,514&lt;/td&gt;
&lt;td&gt;54.27° / 42.05°&lt;/td&gt;
&lt;td&gt;17,893,416 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;girl&lt;/td&gt;
&lt;td&gt;325,649&lt;/td&gt;
&lt;td&gt;621,216&lt;/td&gt;
&lt;td&gt;60.30° / 47.08°&lt;/td&gt;
&lt;td&gt;18,731,304 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I moved the street camera by ±0.8 m. The cat and girl were estimated closer to the camera, so I reduced their movement to ±0.5 m. These results are &lt;strong&gt;2.5D&lt;/strong&gt;: they look three-dimensional near the original viewpoint but do not reconstruct the hidden backs of objects.&lt;/p&gt;

&lt;p&gt;All three examples support small viewpoint changes, but black holes appear where triangles were removed around the sky, leaves, hair, and object boundaries. Stylized images can therefore be animated as well as realistic ones, but thin outlines and stylized shapes remain difficult to interpret as 3D geometry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned during the test
&lt;/h2&gt;

&lt;p&gt;My first attempt calculated normals only from horizontal and vertical differences in the depth image. The median angular difference was an invalid 113–143 degrees because that method ignored perspective projection and camera intrinsics. Switching to neighboring 3D points in the metric point map produced comparable orientations.&lt;/p&gt;

&lt;p&gt;The official implementation also emitted a warning because FP32 autocast is unsupported on both CPU and MPS. Autocast was disabled automatically, and inference completed without a patch.&lt;/p&gt;

&lt;p&gt;The verification environment was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;machine: MacBook Pro (Apple M1 Max, 64 GB, arm64)
OS: macOS 26.5.2
Python: 3.12.10
PyTorch: 2.13.0
OpenCV: 5.0.0
NumPy: 2.5.1
Blender: 5.1.2
FFmpeg: 8.1.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interpretation
&lt;/h2&gt;

&lt;p&gt;The detailed data is above, but the simpler reading has three main points.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Mac GPU reduced the wait to a practical range&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MPS took about 176–212 ms per image. That is still too slow for real-time video, but practical for image-editing or 3D preprocessing tasks.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;One inference produced several kinds of 3D information&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Metric point maps, depth, normals, masks, and camera intrinsics were produced together. No separate models were needed, making the outputs easy to combine.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Boundaries and hidden surfaces remain difficult&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Normals represented broad planes and curved objects, but thin structures and distant details were unstable. A mesh made from one image is also not a complete 3D space with reconstructed backsides.&lt;/p&gt;

&lt;p&gt;This test was limited to three generated images without ground-truth normals or depth. Timing used only one street image, a 384-pixel short side, FP32, and one M1 Max. I did not test official benchmarks, real photos, ViT-B/L, FP16, Core ML, the Apple Neural Engine, temporal video consistency, power consumption, or accuracy against ground truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts after verification
&lt;/h2&gt;

&lt;p&gt;Getting road orientation, object curvature, and a metric point map from one image in one pass was more convenient than I expected. In particular, the dedicated normal head produced smoother boundaries than the normals calculated naively from the point map, which made the value of direct normal prediction easy to see.&lt;/p&gt;

&lt;p&gt;The 3D mesh is interesting for small viewpoint changes, but it is not a space that can be explored freely.&lt;/p&gt;

&lt;p&gt;This looks like a fun way to add a little camera movement to a single image.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pytorch</category>
      <category>computervision</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Running the Lightweight ZipDepth Model on Apple Silicon</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Wed, 15 Jul 2026 00:44:59 +0000</pubDate>
      <link>https://dev.to/kiarina/running-the-lightweight-zipdepth-model-on-apple-silicon-opf</link>
      <guid>https://dev.to/kiarina/running-the-lightweight-zipdepth-model-on-apple-silicon-opf</guid>
      <description>&lt;h1&gt;
  
  
  Running the Lightweight ZipDepth Model on Apple Silicon
&lt;/h1&gt;

&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;Depth information is useful when we want to distinguish the foreground from the background in a single image. It can support background blur, 3D effects, and perception for robots.&lt;/p&gt;

&lt;p&gt;Today, I test the lightweight &lt;strong&gt;ZipDepth&lt;/strong&gt; model on Apple Silicon and compare PyTorch CPU, PyTorch MPS, and ONNX Runtime CPU.&lt;/p&gt;

&lt;p&gt;The short result is that MPS was the fastest option on an Apple M1 Max, averaging &lt;strong&gt;15.34 ms&lt;/strong&gt;. It was about five times faster than PyTorch CPU with the same standard model. The model also produced clear near-to-far relationships for three different images.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tested
&lt;/h2&gt;

&lt;p&gt;Depth estimation predicts how near or far each pixel is from the camera. This test uses monocular depth estimation, which needs only one RGB image.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether ZipDepth runs on the Apple Silicon CPU and GPU&lt;/li&gt;
&lt;li&gt;Whether the NPU-compatible model can be converted to ONNX and executed&lt;/li&gt;
&lt;li&gt;Inference time and output differences between backends&lt;/li&gt;
&lt;li&gt;Qualitative results for a street, tabletop objects, and a crowd&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the street image used for the benchmark.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2pgp53z1po8c64wvmpng.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2pgp53z1po8c64wvmpng.jpg" alt="Street image before ZipDepth inference" width="799" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lab: &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/15/zipdepth-apple-silicon" rel="noopener noreferrer"&gt;kiarina/labs/2026/07/15/zipdepth-apple-silicon&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the test
&lt;/h2&gt;

&lt;p&gt;You need &lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, an Apple Silicon Mac, and an internet connection for the first model and image download.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml 2026/07/15/zipdepth-apple-silicon
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/15/zipdepth-apple-silicon run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, the task downloads the checkpoints, verifies their SHA-256 hashes, and exports the NPU-compatible model to ONNX opset 18. Benchmarking and result image generation are included.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is ZipDepth?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2607.08771" rel="noopener noreferrer"&gt;ZipDepth&lt;/a&gt; is a monocular depth estimation model whose paper was released in July 2026 and presented at ECCV 2026. It has 6.1 million parameters and requires 3.0 GMACs for a 384x384 input. It targets devices ranging from server GPUs to mobile hardware.&lt;/p&gt;

&lt;p&gt;Its output is relative inverse depth. In the images in this article, brighter pixels are nearer and darker pixels are farther away. It does not return metric distances such as three meters.&lt;/p&gt;

&lt;p&gt;The model was distilled on about 14.07 million images across 17 domains, using pseudo-depth produced by &lt;a href="https://github.com/DepthAnything/Depth-Anything-V2" rel="noopener noreferrer"&gt;Depth Anything V2 Large&lt;/a&gt;. Knowledge distillation trains a small model using predictions from a larger teacher model.&lt;/p&gt;

&lt;p&gt;The data flow is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Training performed by the ZipDepth authors
RGB image -&amp;gt; Depth Anything V2 Large -&amp;gt; pseudo-depth -&amp;gt; train ZipDepth

Inference in this lab
RGB image -&amp;gt; resize short side to 384 -&amp;gt; ZipDepth -&amp;gt; inverse depth map
                                            -&amp;gt; color visualization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depth Anything V2 Large was the training-time teacher. It is not executed by this lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models and licenses
&lt;/h2&gt;

&lt;p&gt;ZipDepth provides two checkpoints that use different methods to restore the output resolution.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checkpoint&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zipdepth_base.pth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard model with &lt;code&gt;Unfold&lt;/code&gt;-based upsampling&lt;/td&gt;
&lt;td&gt;PyTorch CPU / MPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zipdepth_base_npu.pth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Conversion-friendly, unfold-free model&lt;/td&gt;
&lt;td&gt;PyTorch CPU / ONNX Runtime CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MPS is the PyTorch backend for running on the Apple Silicon GPU. Despite the NPU-compatible name, this test runs that model on the CPU, not the Apple Neural Engine.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ZipDepth code and checkpoints: &lt;a href="https://github.com/fabiotosi92/ZipDepth/blob/main/LICENSE" rel="noopener noreferrer"&gt;MIT License&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Commit: &lt;code&gt;a302e5437bc58f15c4efd41d3e8222bf24f7d470&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Standard SHA-256: &lt;code&gt;a55910bb0b99c8c5e641cb9206e810b269690ad94e8a2ef08c827c4679391a65&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;NPU-compatible SHA-256: &lt;code&gt;627c04fda584133ead4310074884a4a037061b4c01ba86e73e492ea30fab570d&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Depth Anything V2 Large teacher is licensed under &lt;a href="https://github.com/DepthAnything/Depth-Anything-V2#license" rel="noopener noreferrer"&gt;CC BY-NC 4.0&lt;/a&gt;. The official project lists Small under Apache-2.0 and Base, Large, and Giant under CC BY-NC 4.0. This lab downloads and runs the MIT-licensed ZipDepth checkpoints; it does not download Depth Anything V2 Large.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;Each image keeps its aspect ratio, with its shorter side resized to 384 pixels. I benchmarked the 768x384 street image for ten runs after three warm-up runs. A warm-up avoids including one-time initialization work in the measurement.&lt;/p&gt;

&lt;p&gt;Model loading, image loading, preprocessing, visualization, and file saving are excluded from inference time. I visually inspected the output for a street, tabletop objects, and a crowd.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;machine: MacBook Pro (Apple M1 Max, 64 GB, arm64)
OS: macOS 26.5.2
Python: 3.12.10
PyTorch: 2.13.0
ONNX Runtime: 1.27.0
input: FP32, batch 1, 768x384 for the benchmark
warm-up: 3 runs
measurement: 10 runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;The input is on the left and the ZipDepth result is on the right. The color range is normalized independently for each image, so colors cannot be compared directly between rows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdq2kftygbjd837d5iye0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdq2kftygbjd837d5iye0.jpg" alt="Before and after ZipDepth results for a street, tabletop objects, and a crowd" width="800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the street, the nearby road and cars are bright, while distant buildings and the vanishing point are dark. On the tabletop, the desk, books, laptop, and background form separate depth levels. In the crowd, foreground people are brighter. Some small distant people merge into smooth regions instead of remaining individually separated.&lt;/p&gt;

&lt;p&gt;These are visual observations, not an accuracy test against ground-truth depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inference speed
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend / model&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Max&lt;/th&gt;
&lt;th&gt;Std. dev.&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch CPU / standard&lt;/td&gt;
&lt;td&gt;77.78 ms&lt;/td&gt;
&lt;td&gt;77.49 ms&lt;/td&gt;
&lt;td&gt;75.75 ms&lt;/td&gt;
&lt;td&gt;80.89 ms&lt;/td&gt;
&lt;td&gt;1.65 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch MPS / standard&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15.34 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15.56 ms&lt;/td&gt;
&lt;td&gt;14.36 ms&lt;/td&gt;
&lt;td&gt;15.89 ms&lt;/td&gt;
&lt;td&gt;0.55 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch CPU / NPU-compatible&lt;/td&gt;
&lt;td&gt;101.49 ms&lt;/td&gt;
&lt;td&gt;100.47 ms&lt;/td&gt;
&lt;td&gt;97.44 ms&lt;/td&gt;
&lt;td&gt;109.55 ms&lt;/td&gt;
&lt;td&gt;3.26 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONNX Runtime CPU / NPU-compatible&lt;/td&gt;
&lt;td&gt;47.08 ms&lt;/td&gt;
&lt;td&gt;47.17 ms&lt;/td&gt;
&lt;td&gt;46.54 ms&lt;/td&gt;
&lt;td&gt;47.46 ms&lt;/td&gt;
&lt;td&gt;0.33 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By median time, MPS was 4.98 times faster than PyTorch CPU with the same standard model. ONNX Runtime CPU was 2.13 times faster than PyTorch CPU with the same NPU-compatible model.&lt;/p&gt;

&lt;p&gt;The MPS and ONNX Runtime outputs were also nearly identical by eye.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3crs0bxrxt7ocps32pe2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3crs0bxrxt7ocps32pe2.jpg" alt="Comparison of ZipDepth outputs from PyTorch CPU, MPS, and ONNX Runtime" width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Numerical output differences
&lt;/h3&gt;

&lt;p&gt;Relative depth can describe the same geometry with a different scale and offset. I therefore aligned scale and shift with least squares before measuring error.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;Aligned MAE&lt;/th&gt;
&lt;th&gt;Aligned RMSE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard: PyTorch CPU vs MPS&lt;/td&gt;
&lt;td&gt;0.00000002&lt;/td&gt;
&lt;td&gt;0.00000002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NPU-compatible: PyTorch vs ONNX Runtime CPU&lt;/td&gt;
&lt;td&gt;0.00000001&lt;/td&gt;
&lt;td&gt;0.00000002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch CPU: standard vs NPU-compatible&lt;/td&gt;
&lt;td&gt;0.00044436&lt;/td&gt;
&lt;td&gt;0.00086735&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Differences between backends using the same checkpoint were at the level of FP32 rounding. The standard and NPU-compatible models were not exactly identical, but the difference was not visible in the street result.&lt;/p&gt;

&lt;h3&gt;
  
  
  ONNX export issues
&lt;/h3&gt;

&lt;p&gt;The first ONNX export failed because &lt;code&gt;onnxscript&lt;/code&gt; was missing. PyTorch 2.13.0's exporter requires it, so adding the dependency fixed the error.&lt;/p&gt;

&lt;p&gt;Requesting opset 17 also failed during conversion from the internally generated opset 18 model. The generated opset 18 model passed the ONNX checker and ran with ONNX Runtime, so the lab uses the actual opset 18 output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interpretation
&lt;/h2&gt;

&lt;p&gt;The detailed data is above, but there are three main points.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MPS reached roughly 65 FPS of model inference&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model averaged 15.34 ms without any compatibility patch. That is promising for local image-processing features on Apple Silicon.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ONNX Runtime roughly halved CPU inference time&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With the same NPU-compatible model, ONNX Runtime took 47.08 ms versus 101.49 ms for PyTorch CPU. The numerical output difference was minimal.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The direct MPS-to-ONNX comparison needs care&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They use checkpoints with different upsampling methods. The roughly three-times speed difference cannot be attributed to the backend alone.&lt;/p&gt;

&lt;p&gt;This test is limited to three generated images, one benchmark image, and one M1 Max. It does not test metric distance accuracy, official benchmarks, temporal stability in video, Core ML, the Apple Neural Engine, quantization, power use, or peak memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;For a model with only 6.1 million parameters, ZipDepth preserved useful object boundaries while producing a clear near-to-far structure. The roughly 15 ms MPS result looks fast enough for experiments such as local background blur or simple 3D effects.&lt;/p&gt;

&lt;p&gt;The output is relative depth, so it cannot directly support applications that require real-world distance. Small distant people also tend to merge, making the model unsuitable as the only perception component in safety-critical systems. Even so, combined with object detection and tracking, it could provide useful context for an LLM-controlled agent to understand the relative position of obstacles or follow a person in real time.&lt;/p&gt;

</description>
      <category>python</category>
      <category>computervision</category>
      <category>onnx</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Comparing YOLO26 Semantic Segmentation with PyTorch and ONNX</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Tue, 14 Jul 2026 05:43:39 +0000</pubDate>
      <link>https://dev.to/kiarina/comparing-yolo26-semantic-segmentation-with-pytorch-and-onnx-20mb</link>
      <guid>https://dev.to/kiarina/comparing-yolo26-semantic-segmentation-with-pytorch-and-onnx-20mb</guid>
      <description>&lt;h1&gt;
  
  
  Comparing YOLO26 Semantic Segmentation with PyTorch and ONNX
&lt;/h1&gt;

&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;Sometimes it is not enough to know that an image contains a car. We also need the pixel-level boundaries of the road and sidewalk.&lt;/p&gt;

&lt;p&gt;Today, I ran a YOLO26n semantic segmentation model on an Apple Silicon CPU and compared its PyTorch and ONNX Runtime outputs and speed.&lt;/p&gt;

&lt;p&gt;To give the result first, the two class maps agreed on &lt;strong&gt;99.3129%&lt;/strong&gt; of all pixels. Mean end-to-end processing time was 27.55 ms with ONNX Runtime and 266.00 ms with PyTorch. Most of the difference came from post-processing rather than inference itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are verifying
&lt;/h2&gt;

&lt;p&gt;Semantic segmentation assigns every pixel in an image to a class. Unlike object detection, which draws boxes, it produces regions that follow the shapes of roads, sky, cars, and other classes.&lt;/p&gt;

&lt;p&gt;I used the official Cityscapes-trained &lt;code&gt;yolo26n-sem.pt&lt;/code&gt; model to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether PyTorch inference and ONNX export work on an Apple M1 Max CPU&lt;/li&gt;
&lt;li&gt;How closely the two class maps agree under the same input conditions&lt;/li&gt;
&lt;li&gt;The time spent on preprocessing, inference, post-processing, and the full call&lt;/li&gt;
&lt;li&gt;How the model divides a generated urban street image into 19 classes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the original test image.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2gysjww8ayquknbieu7p.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2gysjww8ayquknbieu7p.jpg" alt="Urban street image before YOLO26 semantic segmentation" width="799" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Target lab: &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/14/yolo26-semantic-segmentation" rel="noopener noreferrer"&gt;kiarina/labs/2026/07/14/yolo26-semantic-segmentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the environment
&lt;/h2&gt;

&lt;p&gt;You need &lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, and an internet connection for the initial model and shared-image downloads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml 2026/07/14/yolo26-semantic-segmentation
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/14/yolo26-semantic-segmentation run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, the task downloads the checkpoint, verifies its SHA-256 hash, and exports it to ONNX opset 18. It creates four images: the PyTorch output, ONNX output, disagreement view, and an annotated comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model and license
&lt;/h2&gt;

&lt;p&gt;This test uses one pretrained model. PyTorch and ONNX are not two different models here; they are two execution paths for the same checkpoint.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Input and output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yolo26n-sem.pt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assign each pixel to one of 19 Cityscapes classes&lt;/td&gt;
&lt;td&gt;1x3x640x640 image → class logits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exported &lt;code&gt;yolo26n-sem.onnx&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Run the same weights with ONNX Runtime&lt;/td&gt;
&lt;td&gt;1x3x640x640 image → 1x640x640 class IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Class logits are raw scores indicating how likely each class is at each pixel. A class ID is the number of the selected class.&lt;/p&gt;

&lt;p&gt;The data flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1774x887 JPEG
  -&amp;gt; Fit into 640x640 while preserving aspect ratio (letterbox)
  -&amp;gt; Run the same checkpoint through two paths
       PyTorch: class logits -&amp;gt; resize -&amp;gt; class IDs
       ONNX:    640x640 class IDs -&amp;gt; resize
  -&amp;gt; 1774x887 class maps
  -&amp;gt; Compare pixel agreement, per-class IoU, and timing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Letterboxing adds padding to fit an image into a target size without stretching it. IoU measures the overlap between two regions; values closer to 100% mean better agreement.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checkpoint: &lt;a href="https://github.com/ultralytics/assets/releases/tag/v8.4.0" rel="noopener noreferrer"&gt;Ultralytics assets v8.4.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Model size: 3,487,283 bytes&lt;/li&gt;
&lt;li&gt;SHA-256: &lt;code&gt;f3f293cca764de1f93044030d8d5612de9c5ffbf37c9c8ea1b69418b73038999&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ultralytics code and trained models: AGPL-3.0 or Ultralytics Enterprise License&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://www.ultralytics.com/license" rel="noopener noreferrer"&gt;Ultralytics licensing page&lt;/a&gt; states that trained models are covered by AGPL-3.0 by default. Proprietary or commercial integration may require an Enterprise License. Check the official terms for your use case at the time of use. The lab does not commit the checkpoint or exported ONNX file; it downloads and generates them at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;The input is one fixed image containing a road, sidewalks, buildings, traffic signals, cars, people, and bicycles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file: tests/assets/jpg/street_scene_1774x887_287kb.jpg
resolution: 1774x887
SHA-256: d5c865f452599311fbbfd0c132bb4f8b7ade4dd88f0c8ac14ce136490ea53a2e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both paths use &lt;code&gt;imgsz=640&lt;/code&gt;, &lt;code&gt;rect=False&lt;/code&gt;, and the CPU. I measured 10 runs after three warm-up runs. Warm-up reduces the effect of setup work that happens only during the first inference. Model initialization, image loading, and image saving are excluded from the benchmark.&lt;/p&gt;

&lt;p&gt;The model can output these 19 classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;road, sidewalk, building, wall, fence, pole, traffic light, traffic sign,
vegetation, terrain, sky, person, rider, car, truck, bus, train,
motorcycle, bicycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;The left side is the original image. The right side overlays the ONNX class map at 50% opacity. The classes that appeared and their pixel shares are shown below the image.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fynrdi16ix468haa9ftla.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fynrdi16ix468haa9ftla.jpg" alt="Urban street image segmented into pixel-level classes with YOLO26" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both PyTorch and ONNX Runtime ran successfully on the Apple M1 Max CPU.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;backend / stage&lt;/th&gt;
&lt;th&gt;mean&lt;/th&gt;
&lt;th&gt;min&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;th&gt;std dev&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch preprocessing&lt;/td&gt;
&lt;td&gt;0.93 ms&lt;/td&gt;
&lt;td&gt;0.89 ms&lt;/td&gt;
&lt;td&gt;1.04 ms&lt;/td&gt;
&lt;td&gt;0.04 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch inference&lt;/td&gt;
&lt;td&gt;28.83 ms&lt;/td&gt;
&lt;td&gt;26.39 ms&lt;/td&gt;
&lt;td&gt;30.31 ms&lt;/td&gt;
&lt;td&gt;1.02 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch post-processing&lt;/td&gt;
&lt;td&gt;236.07 ms&lt;/td&gt;
&lt;td&gt;233.19 ms&lt;/td&gt;
&lt;td&gt;239.70 ms&lt;/td&gt;
&lt;td&gt;1.67 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch wall time&lt;/td&gt;
&lt;td&gt;266.00 ms&lt;/td&gt;
&lt;td&gt;264.44 ms&lt;/td&gt;
&lt;td&gt;270.02 ms&lt;/td&gt;
&lt;td&gt;1.69 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONNX preprocessing&lt;/td&gt;
&lt;td&gt;1.01 ms&lt;/td&gt;
&lt;td&gt;0.91 ms&lt;/td&gt;
&lt;td&gt;1.24 ms&lt;/td&gt;
&lt;td&gt;0.10 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONNX inference&lt;/td&gt;
&lt;td&gt;25.67 ms&lt;/td&gt;
&lt;td&gt;24.32 ms&lt;/td&gt;
&lt;td&gt;26.91 ms&lt;/td&gt;
&lt;td&gt;0.88 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONNX post-processing&lt;/td&gt;
&lt;td&gt;0.72 ms&lt;/td&gt;
&lt;td&gt;0.60 ms&lt;/td&gt;
&lt;td&gt;1.11 ms&lt;/td&gt;
&lt;td&gt;0.15 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONNX wall time&lt;/td&gt;
&lt;td&gt;27.55 ms&lt;/td&gt;
&lt;td&gt;26.06 ms&lt;/td&gt;
&lt;td&gt;28.82 ms&lt;/td&gt;
&lt;td&gt;0.95 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ONNX inference itself took about 0.89 times the PyTorch time, while its full call took about 0.10 times as long. The main difference was post-processing. PyTorch resizes class logits to the original resolution before choosing class IDs. This exported ONNX model directly returns class IDs, making its post-processing much shorter.&lt;/p&gt;

&lt;p&gt;I ran the lab again while writing this article. Mean wall time was 290.79 ms for PyTorch and 29.76 ms for ONNX. Timings vary between runs, but the roughly 10x gap and 99.3129% pixel agreement were reproduced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pixel and per-class agreement
&lt;/h3&gt;

&lt;p&gt;The two paths produced the same class ID for &lt;strong&gt;99.3129%&lt;/strong&gt; of all pixels and different IDs for &lt;strong&gt;0.6871%&lt;/strong&gt;. The detailed data for the 16 predicted classes is below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;th&gt;PyTorch area&lt;/th&gt;
&lt;th&gt;ONNX area&lt;/th&gt;
&lt;th&gt;IoU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;road&lt;/td&gt;
&lt;td&gt;21.27%&lt;/td&gt;
&lt;td&gt;21.22%&lt;/td&gt;
&lt;td&gt;99.52%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sidewalk&lt;/td&gt;
&lt;td&gt;10.38%&lt;/td&gt;
&lt;td&gt;10.39%&lt;/td&gt;
&lt;td&gt;98.48%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;building&lt;/td&gt;
&lt;td&gt;16.28%&lt;/td&gt;
&lt;td&gt;16.30%&lt;/td&gt;
&lt;td&gt;98.99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wall&lt;/td&gt;
&lt;td&gt;3.05%&lt;/td&gt;
&lt;td&gt;3.05%&lt;/td&gt;
&lt;td&gt;97.16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fence&lt;/td&gt;
&lt;td&gt;4.18%&lt;/td&gt;
&lt;td&gt;4.17%&lt;/td&gt;
&lt;td&gt;98.32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pole&lt;/td&gt;
&lt;td&gt;1.62%&lt;/td&gt;
&lt;td&gt;1.61%&lt;/td&gt;
&lt;td&gt;88.15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;traffic light&lt;/td&gt;
&lt;td&gt;0.03%&lt;/td&gt;
&lt;td&gt;0.04%&lt;/td&gt;
&lt;td&gt;88.03%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;traffic sign&lt;/td&gt;
&lt;td&gt;0.29%&lt;/td&gt;
&lt;td&gt;0.29%&lt;/td&gt;
&lt;td&gt;93.30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vegetation&lt;/td&gt;
&lt;td&gt;26.52%&lt;/td&gt;
&lt;td&gt;26.54%&lt;/td&gt;
&lt;td&gt;98.85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;terrain&lt;/td&gt;
&lt;td&gt;0.02%&lt;/td&gt;
&lt;td&gt;0.02%&lt;/td&gt;
&lt;td&gt;82.92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sky&lt;/td&gt;
&lt;td&gt;8.94%&lt;/td&gt;
&lt;td&gt;8.97%&lt;/td&gt;
&lt;td&gt;99.15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;person&lt;/td&gt;
&lt;td&gt;0.90%&lt;/td&gt;
&lt;td&gt;0.90%&lt;/td&gt;
&lt;td&gt;96.20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider&lt;/td&gt;
&lt;td&gt;0.21%&lt;/td&gt;
&lt;td&gt;0.21%&lt;/td&gt;
&lt;td&gt;94.61%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;car&lt;/td&gt;
&lt;td&gt;5.11%&lt;/td&gt;
&lt;td&gt;5.12%&lt;/td&gt;
&lt;td&gt;98.67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;motorcycle&lt;/td&gt;
&lt;td&gt;0.74%&lt;/td&gt;
&lt;td&gt;0.74%&lt;/td&gt;
&lt;td&gt;95.68%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bicycle&lt;/td&gt;
&lt;td&gt;0.44%&lt;/td&gt;
&lt;td&gt;0.44%&lt;/td&gt;
&lt;td&gt;94.24%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Red pixels show where the two paths produced different class IDs. They are concentrated around object boundaries rather than inside large regions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4974lfi8hk45shvycjj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4974lfi8hk45shvycjj.jpg" alt="Pixels where PyTorch and ONNX class IDs differ, highlighted in red" width="799" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The first failed comparison
&lt;/h3&gt;

&lt;p&gt;In the first attempt, I did not fix the &lt;code&gt;rect&lt;/code&gt; option, and pixel agreement was 98.9648%. PyTorch used minimal padding while the fixed-shape ONNX model used 640x640 padding. The actual inputs therefore differed even with the same &lt;code&gt;imgsz=640&lt;/code&gt; setting.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;rect=False&lt;/code&gt; for both paths raised agreement to 99.3129%. A backend comparison must align preprocessing as well as the model.&lt;/p&gt;

&lt;p&gt;The verification environment was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;machine: MacBook Pro (Apple M1 Max, arm64)
OS: macOS 26.5.2
Python: 3.12.10
Ultralytics: 8.4.95
PyTorch: 2.13.0
ONNX: 1.22.0
ONNX Runtime: 1.27.0
OpenCV: 5.0.0
NumPy: 2.5.1
provider: CPUExecutionProvider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interpretation
&lt;/h2&gt;

&lt;p&gt;The detailed data is above, but the simpler reading has three main points.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Large regions agreed closely&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Road, building, vegetation, and sky all had IoU above 98%. The overall visual results were also nearly identical. Relative differences were larger for thin poles, traffic lights, terrain, and region boundaries.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The output format was the main reason ONNX was faster&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Inference differed by only about 3 ms, while post-processing differed by about 235 ms. That is because this ONNX export returns a class ID map directly. The result does not mean that ONNX is always 10 times faster; it applies to this export and the full processing paths tested here.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The street was segmented, but a distant bus was missed&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Road, sidewalk, buildings, sky, people, cars, and bicycles were assigned to visually reasonable locations. However, a small bus in the distance was not labeled as &lt;code&gt;bus&lt;/code&gt;. Small and distant objects remain difficult.&lt;/p&gt;

&lt;p&gt;This test used one generated image without a ground-truth mask. Therefore, 99.3129% is agreement between PyTorch and ONNX, not prediction accuracy. I did not test Cityscapes validation data, real photos, other images, different resolutions, MPS, CoreML, quantization, or memory usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts after verification
&lt;/h2&gt;

&lt;p&gt;Exporting to ONNX preserved almost the same visual result while reducing CPU processing to about 30 ms per image. That looks useful for locally processing road and sidewalk regions in sequence.&lt;/p&gt;

&lt;p&gt;The important caveat is that most of the speedup came from different post-processing. Performance should not be judged from the backend name alone; preprocessing and output format matter too. Next, I would like to test temporal stability on real road video and compare CoreML performance.&lt;/p&gt;

</description>
      <category>python</category>
      <category>yolo</category>
      <category>onnx</category>
      <category>computervision</category>
    </item>
    <item>
      <title>Removing a Portrait Background with BiRefNet ONNX on CPU</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Mon, 13 Jul 2026 05:11:02 +0000</pubDate>
      <link>https://dev.to/kiarina/removing-a-portrait-background-with-birefnet-onnx-on-cpu-gbj</link>
      <guid>https://dev.to/kiarina/removing-a-portrait-background-with-birefnet-onnx-on-cpu-gbj</guid>
      <description>&lt;h1&gt;
  
  
  Removing a Portrait Background with BiRefNet ONNX on CPU
&lt;/h1&gt;

&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;Background removal is common, but I wanted to see whether a local CPU could preserve thin details such as hair.&lt;/p&gt;

&lt;p&gt;Today, I ran an official BiRefNet ONNX model with ONNX Runtime and removed the background from a portrait.&lt;/p&gt;

&lt;p&gt;To give the result first, it produced a transparent PNG while preserving the main subject and most of the hair. Inference with a 1024x1024 input took about 4.32 seconds on average on an Apple M1 Max CPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are verifying
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ZhengPeng7/BiRefNet" rel="noopener noreferrer"&gt;BiRefNet&lt;/a&gt; separates a high-resolution image into foreground and background. This is called dichotomous image segmentation (DIS), meaning segmentation into two kinds of regions.&lt;/p&gt;

&lt;p&gt;I used the general-purpose Swin-Tiny ONNX model from the official GitHub Release and checked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether it runs from Python with only the ONNX Runtime CPU backend&lt;/li&gt;
&lt;li&gt;Whether it separates a person, white clothing, and thin windblown hair from the background&lt;/li&gt;
&lt;li&gt;How long preprocessing, inference, post-processing, and PNG saving take&lt;/li&gt;
&lt;li&gt;How alpha values are distributed in the output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the original test image.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe2p11sc76gczewh2huq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe2p11sc76gczewh2huq.jpg" alt="Portrait before background removal with BiRefNet" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Target lab: &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/13/birefnet-onnx" rel="noopener noreferrer"&gt;kiarina/labs/2026/07/13/birefnet-onnx&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the environment
&lt;/h2&gt;

&lt;p&gt;You need &lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, and an internet connection for the initial model and shared-image downloads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml 2026/07/13/birefnet-onnx
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/13/birefnet-onnx run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, the task downloads the model after verifying its SHA-256 hash. It then creates &lt;code&gt;output_removed_bg.png&lt;/code&gt;, with the background represented by the alpha channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model and license
&lt;/h2&gt;

&lt;p&gt;This test uses one model.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Input and output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BiRefNet-general-bb_swin_v1_tiny-epoch_232.onnx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Estimate whether each pixel belongs to the foreground or background&lt;/td&gt;
&lt;td&gt;1x3x1024x1024 image → 1x1x1024x1024 logits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Swin-Tiny is the backbone, or the base component that extracts image features. The official model list presents it as a smaller and faster alternative to the larger Swin-Large backbone. The ONNX file used here is 224,005,088 bytes.&lt;/p&gt;

&lt;p&gt;As described in the &lt;a href="https://arxiv.org/abs/2401.03407" rel="noopener noreferrer"&gt;paper&lt;/a&gt;, BiRefNet has a localization module that uses the whole image to locate the subject and a reconstruction module that refers to local image details and edges to recover fine structure. Because this test uses the exported ONNX model, these modules are not invoked separately in the code.&lt;/p&gt;

&lt;p&gt;The data flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1536x1024 JPEG
  -&amp;gt; Resize to 1024x1024, convert to RGB, and normalize
  -&amp;gt; Estimate foreground logits with BiRefNet ONNX
  -&amp;gt; Convert logits to a 0-1 mask with sigmoid
  -&amp;gt; Resize the mask back to 1536x1024
  -&amp;gt; Add it to the original image as an alpha channel
  -&amp;gt; Transparent PNG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logits are the model's raw output values. Sigmoid converts them to a range from 0 to 1. The final image treats 0 as transparent and 1 as opaque.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/ZhengPeng7/BiRefNet" rel="noopener noreferrer"&gt;BiRefNet code&lt;/a&gt;: &lt;a href="https://github.com/ZhengPeng7/BiRefNet/blob/main/LICENSE" rel="noopener noreferrer"&gt;MIT License&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/ZhengPeng7/BiRefNet" rel="noopener noreferrer"&gt;Official Hugging Face model&lt;/a&gt;: labeled MIT&lt;/li&gt;
&lt;li&gt;ONNX weight used in this test: &lt;a href="https://github.com/ZhengPeng7/BiRefNet/releases/tag/v1" rel="noopener noreferrer"&gt;official GitHub Release v1&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before redistributing the model or integrating it into a product, check the current license text, attribution requirements, and dependency terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;The input is one fixed portrait of a person by the sea. It includes white clothing and thin strands of hair extending to the right in the wind.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file: tests/assets/jpg/removebg_1536x1024_141kb.jpg
resolution: 1536x1024
SHA-256: d3d362b876936c57cfaf61eedd0ada05fd4950483ab79502aa5a67ded4a6b910
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenCV resizes the image to 1024x1024 and normalizes it with the ImageNet mean and standard deviation. After ONNX Runtime inference, the mask is resized to the original resolution and saved as an 8-bit alpha channel.&lt;/p&gt;

&lt;p&gt;The timing excludes model download, SHA-256 verification, &lt;code&gt;InferenceSession&lt;/code&gt; initialization, and image loading. The inference benchmark runs 10 times after three warm-up runs. Warm-up reduces the effect of setup work that occurs only during the first inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;This is the generated transparent PNG. Depending on the viewer, transparent areas may appear white or as a checkerboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3xnjtqi39gdokc7tn9k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3xnjtqi39gdokc7tn9k.png" alt="Portrait after background removal with BiRefNet" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These results were recorded on the CPU of a MacBook Pro with an Apple M1 Max.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- One-shot processing time ---
Preprocessing:   23.86 ms
Inference:     4699.83 ms
Postprocessing:  6.80 ms
PNG save:        41.07 ms
Total:         4771.56 ms

--- Inference benchmark (Warmup: 3, Iterations: 10) ---
Average time: 4319.62 ms
Min time:     4206.41 ms
Max time:     4480.35 ms
Std dev:        88.62 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran the same lab again while writing this article. Average inference time was 4302.47 ms, with a minimum of 4179.94 ms and a maximum of 4477.45 ms. Both runs were close to 4.3 seconds, but these timings are reference values that vary between runs.&lt;/p&gt;

&lt;p&gt;The output is a 1536x1024, 8-bit RGBA PNG. The alpha distribution was identical in both runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transparent (alpha=0): 65.92%
Transition (1-254):     9.37%
Opaque (alpha=255):     24.71%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Transition&lt;/code&gt; means pixels that are neither fully transparent nor fully opaque. These values help produce smoother hair and clothing boundaries, but their percentage alone does not measure segmentation accuracy.&lt;/p&gt;

&lt;p&gt;The verification environment was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;machine: MacBook Pro (Apple M1 Max, arm64)
OS: macOS 26.5.1
Python: 3.12.10
ONNX Runtime: 1.27.0
OpenCV: 5.0.0
NumPy: 2.5.1
provider: CPUExecutionProvider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interpretation
&lt;/h2&gt;

&lt;p&gt;The detailed numbers are above, but the simpler reading has three main points.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It preserved the person and most of the hair&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model separated the face, white clothing, and most of the hair from the background. It retained many thin strands extending to the right, and partially transparent pixels kept the boundary from ending abruptly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Some flyaway hair disappeared, and color spill remained&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some especially thin strands above the head and on the right disappeared. A small amount of blue from the original background also remained around the hair and clothing edges. Replacing the background with a different color may require an additional edge-color correction step.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It runs on a CPU, but it is not real-time&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Inference took about 4.3 seconds per image. That is usable for processing a small number of local images one by one, but video or large batches would need further tests with a GPU, a smaller model, or quantization. Quantization reduces the numerical precision used for computation to make a model lighter.&lt;/p&gt;

&lt;p&gt;This test used only one well-lit outdoor portrait. There was no ground-truth mask, so the quality assessment is visual only. I did not test other subjects, complex backgrounds, low light, low resolution, multiple people, or a quality and speed comparison with the Swin-Large model. The current implementation also stretches the input into a square before inference, so alternative resizing methods remain untested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts after verification
&lt;/h2&gt;

&lt;p&gt;I was impressed that a simple ONNX Runtime implementation running only on the CPU could create a transparent image while preserving much of the hair. It seems useful when background removal should stay local instead of sending an image to an external service. This quality should be sufficient for an LLM agent that automatically converts collected portraits into transparent assets for display or image compositing.&lt;/p&gt;

</description>
      <category>python</category>
      <category>onnx</category>
      <category>computervision</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Testing Japanese and English OCR with PP-OCRv6-small and RapidOCR</title>
      <dc:creator>Nariaki Wada</dc:creator>
      <pubDate>Sun, 12 Jul 2026 07:39:21 +0000</pubDate>
      <link>https://dev.to/kiarina/testing-japanese-and-english-ocr-with-pp-ocrv6-small-and-rapidocr-39pd</link>
      <guid>https://dev.to/kiarina/testing-japanese-and-english-ocr-with-pp-ocrv6-small-and-rapidocr-39pd</guid>
      <description>&lt;h1&gt;
  
  
  Testing Japanese and English OCR with PP-OCRv6-small and RapidOCR
&lt;/h1&gt;

&lt;p&gt;Hello, everyone.&lt;/p&gt;

&lt;p&gt;OCR is useful for reading text in images, but it is worth asking how much we can trust a result that merely looks correct.&lt;/p&gt;

&lt;p&gt;Today, I ran PP-OCRv6-small through RapidOCR and tested how well it could read an image containing both Japanese and English on a CPU.&lt;/p&gt;

&lt;p&gt;To give the result first, 12 of 14 representative strings matched exactly, and 13 of 14 were recognizable when a character-form difference was accepted. It also read vertical text, a slanted address, and small email text.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are verifying
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/blog/PaddlePaddle/pp-ocrv6" rel="noopener noreferrer"&gt;PP-OCRv6&lt;/a&gt; is a family of OCR models that finds text regions in an image and converts them into strings. It has tiny, small, and medium tiers. I selected small, with about 7.7 million parameters, as the balanced option between accuracy and size. Parameters are the values a model acquires through training.&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://github.com/RapidAI/RapidOCR" rel="noopener noreferrer"&gt;RapidOCR&lt;/a&gt; 3.9.1 and the ONNX Runtime CPU backend, I checked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the PP-OCRv6-small detection and recognition models can be explicitly selected&lt;/li&gt;
&lt;li&gt;Whether they can read Japanese, English, numbers, symbols, vertical text, and slanted text&lt;/li&gt;
&lt;li&gt;End-to-end OCR pipeline time, excluding model initialization&lt;/li&gt;
&lt;li&gt;Whether confidence scores agree with the actual correctness of recognized text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used the fixed image below for the test. In addition to horizontal text, it contains vertical, small, and slanted text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzjn0084azef8ucjjhbpv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzjn0084azef8ucjjhbpv.jpg" alt="Original image used for the OCR test" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Target lab: &lt;a href="https://github.com/kiarina/labs/tree/main/2026/07/12/pp-ocrv6-small-rapidocr" rel="noopener noreferrer"&gt;kiarina/labs/2026/07/12/pp-ocrv6-small-rapidocr&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the environment
&lt;/h2&gt;

&lt;p&gt;You need &lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, and an internet connection for the initial download of the shared image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kiarina/labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;labs
git sparse-checkout &lt;span class="nb"&gt;set&lt;/span&gt; .gitignore .mise/tasks Makefile mise.toml 2026/07/12/pp-ocrv6-small-rapidocr
mise &lt;span class="nt"&gt;-C&lt;/span&gt; 2026/07/12/pp-ocrv6-small-rapidocr run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command prints all recognized strings, confidence scores, coordinates, and timing data. It also generates &lt;code&gt;output_ocr.jpg&lt;/code&gt; with the detected text regions drawn on the image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models and licenses
&lt;/h2&gt;

&lt;p&gt;I used the following three models included in the RapidOCR wheel.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Detection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PP-OCRv6_det_small.onnx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Find regions that contain text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Orientation classification&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ch_ppocr_mobile_v2.0_cls_mobile.onnx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check and correct the orientation of each cropped region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Recognition&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PP-OCRv6_rec_small.onnx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Convert each corrected region into Japanese or other text&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The data flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input image
  -&amp;gt; Detect text regions
  -&amp;gt; Crop each region
  -&amp;gt; Correct text orientation
  -&amp;gt; Output text and confidence scores
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PP-OCRv6-small handles detection and recognition, while RapidOCR's default model handles orientation classification. All inference ran with ONNX Runtime's &lt;code&gt;CPUExecutionProvider&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/PaddlePaddle/PaddleOCR" rel="noopener noreferrer"&gt;PaddleOCR&lt;/a&gt;: &lt;a href="https://github.com/PaddlePaddle/PaddleOCR/blob/main/LICENSE" rel="noopener noreferrer"&gt;Apache License 2.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/RapidAI/RapidOCR" rel="noopener noreferrer"&gt;RapidOCR&lt;/a&gt;: &lt;a href="https://github.com/RapidAI/RapidOCR/blob/main/LICENSE" rel="noopener noreferrer"&gt;Apache License 2.0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before redistributing models or integrating them into a product, check the current license text, attribution requirements, and dependency terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;The input was one fixed synthetic image of an indoor scene. It contains Japanese and English signs, a whiteboard, vertically printed book spines, a PC screen, small contact details, and a slanted envelope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file: tests/assets/jpg/ocr_1448x1086_242kb.jpg
resolution: 1448x1086
SHA-256: 42d9024588f112ab9fbaf69c0e32a95462613c35b9cdbbb1a9c4bc1ff93ab96e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I selected 14 representative strings from the image and checked for matches after removing spaces and symbols. When a sentence was split across multiple detected regions, it passed if all parts were present.&lt;/p&gt;

&lt;p&gt;For timing, I excluded model initialization and the first inference. After three warm-up runs, I passed the already-loaded image through the full pipeline 10 times. Warm-up runs reduce the effect of setup work that only occurs during the first inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;The left side of the following image shows the detected regions overlaid on the input, while the right side places the recognized strings at their corresponding positions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fslc6h0l2b1qx6labunqw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fslc6h0l2b1qx6labunqw.jpg" alt="Text detection and recognition results from RapidOCR" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These results were measured on the CPU of a Mac Studio with an Apple M4 Max.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;detected lines:       40
representative match: 12/14
mean confidence:      0.984
min confidence:       0.883
max confidence:       1.000

average time:         839.01 ms
min time:             758.89 ms
max time:             890.43 ms
standard deviation:    37.32 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;representative match&lt;/code&gt; is the strict automated check, which treats different character forms as a mismatch. If the hyphen recognized in place of the Japanese prolonged sound mark in &lt;code&gt;ノート&lt;/code&gt; is accepted because the word remains understandable, the practical result is 13/14.&lt;/p&gt;

&lt;p&gt;I ran the same lab again while writing this article. The strict 12/14 match result and confidence scores were unchanged, while timing was 920.47 ms on average, with a minimum of 835.24 ms and a maximum of 1090.61 ms. The 839.01 ms result is a reference measurement that varies between runs, not a fixed performance guarantee.&lt;/p&gt;

&lt;p&gt;A confidence score is the model's own estimate of how reliable a recognition result is. A value near 1.000 is high, but it does not guarantee correctness.&lt;/p&gt;

&lt;p&gt;The representative string checks were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Expected text&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Japanese&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OCR テストルーム&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Please knock before entering&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numbers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;12345&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Japanese and numbers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;在庫確認：ノート12冊／ペン24本&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ACCEPT (character-form difference)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;English and punctuation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Next review: Friday, 3:45 PM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small Japanese&lt;/td&gt;
&lt;td&gt;&lt;code&gt;忘れずに水やり&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small English&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Call Ken at 18:00&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertical Japanese&lt;/td&gt;
&lt;td&gt;&lt;code&gt;日本語の練習&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertical English&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Deep Learning Basics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Japanese&lt;/td&gt;
&lt;td&gt;&lt;code&gt;取扱注意&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MISS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FRAGILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slanted Japanese&lt;/td&gt;
&lt;td&gt;&lt;code&gt;東京都千代田区1-2-3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small email&lt;/td&gt;
&lt;td&gt;&lt;code&gt;test@example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small phone number&lt;/td&gt;
&lt;td&gt;&lt;code&gt;03-1234-5678&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two strings that did not pass the strict automated check were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expected: 在庫確認：ノート12冊／ペン24本
actual:   在庫確認：ノ-ト12冊／ペン24本
score:    0.947

expected: 取扱注意
actual:   取极注意
score:    0.883
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ノ-ト&lt;/code&gt; result uses a different character for the prolonged sound mark, but the word and the full sentence remain understandable, so I count it as recognized in this article. The clear recognition error is &lt;code&gt;取扱注意&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Outside the representative set, &lt;code&gt;Project Alpha&lt;/code&gt; was recognized as &lt;code&gt;Project Alpi&lt;/code&gt;. However, the pencil in the source image covers the final &lt;code&gt;a&lt;/code&gt; area. Because the complete string is not visible, I do not count this as a clear model failure.&lt;/p&gt;

&lt;p&gt;The verification environment was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;machine: Mac Studio (Apple M4 Max, arm64)
OS: macOS 26.5.1
Python: 3.12.10
RapidOCR: 3.9.1
ONNX Runtime: 1.27.0
OpenCV: 5.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interpretation
&lt;/h2&gt;

&lt;p&gt;The detailed numbers are above, but the simpler reading has three main points.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It read many texts despite differences in direction and size&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The matches included vertical Japanese and English, a slanted address, and small email and phone text. This is only one fixed image, but it shows potential beyond plain document scans.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The evaluation rule changes how the result looks&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The strict exact-match result is 12/14, while accepting &lt;code&gt;ノ-ト&lt;/code&gt; as an understandable character-form difference raises it to 13/14. In contrast, recognizing &lt;code&gt;取扱注意&lt;/code&gt; as &lt;code&gt;取极注意&lt;/code&gt; changes the text itself, so I count it as an error. OCR should be evaluated according to whether the application needs exact transcription or only understandable content.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The CPU processed one image in about 0.84 seconds&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the 1448x1086 image, the two recorded averages for detection, orientation classification, recognition, and pre/post-processing were 839.01 ms and 920.47 ms. Bulk processing and real-time use would need additional measurement and optimization, but this felt manageable for processing local images one by one.&lt;/p&gt;

&lt;p&gt;This test used only one readable synthetic image, so it is not a general OCR accuracy benchmark. I did not test handwriting, low light, blur, strong distortion, smaller text, other fonts, the tiny or medium tiers, or GPU backends. Timing also varies with hardware and ONNX Runtime optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts after verification
&lt;/h2&gt;

&lt;p&gt;RapidOCR felt like an accessible way to try ONNX-based OCR because it wraps detection, orientation correction, and recognition into one pipeline. Reading vertical and slanted text was better than I expected.&lt;/p&gt;

&lt;p&gt;A character-form difference such as &lt;code&gt;ノ-ト&lt;/code&gt; should still be useful for search or understanding the content. On the other hand, a character can change entirely, as in &lt;code&gt;取扱注意&lt;/code&gt;. If I integrate this into an LLM agent, I would keep OCR output as one observation rather than confirmed information and add a mechanism to recheck important values.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ocr</category>
      <category>onnx</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
