Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Tauri v2で作る⼀括画像ブラー / Bulk image blur created wi...
Search
Atom
August 10, 2026
Programming
14
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Tauri v2で作る⼀括画像ブラー / Bulk image blur created with Tauri v2
https://github.com/yAtomtom/batch-blur
Atom
August 10, 2026
More Decks by Atom
See All by Atom
YouTubeのチャット欄の配置変更 / Changing the layout of the YouTube chat field
roraidolaurent
0
39
文献紹介 / Structure-based Knowledge Tracing: An Influence Propagation View
roraidolaurent
0
130
文献紹介 / Knowledge Tracing with GNN
roraidolaurent
0
110
文献紹介 / Non-Intrusive Parametric Reduced Order Models withHigh-Dimensional Inputs via Gradient-Free Active Subspace
roraidolaurent
0
73
ニューラルネットワークのベイズ推論 / Bayesian inference of neural networks
roraidolaurent
2
2.9k
Graph Convolutional Networks
roraidolaurent
0
270
文献紹介 / A Probabilistic Annotation Model for Crowdsourcing Coreference
roraidolaurent
0
100
文献紹介Deep Temporal-Recurrent-Replicated-Softmax for Topical Trends over Time
roraidolaurent
0
150
文献紹介/ Bayesian Learning for Neural Dependency Parsing
roraidolaurent
0
150
Other Decks in Programming
See All in Programming
tsc.rip を支える技術 / Kyoto.なんか #8
susisu
0
4.1k
リアルな遅延を測る仕様
kota_yata
1
130
承認済みなのに差戻しできてしまうバグ、型で潰せます
shinchit
0
130
Detecting Compromised CI with eBPF and Cilium Tetragon
lizrice
0
280
Press start. Python's next generation.
willingc
PRO
3
290
AI Readyの正体はデータマネジメントだ メダリオン2.0の最前線
freee
PRO
0
490
自分的「カンファレンスの楽しみ方」
syumai
0
160
VibeCodingからAgenticWorkflowへ
starfish719
0
1k
仕様駆動開発の消費期限
watany
20
9.2k
KotlinConf Extended South Korea 2026 Keynote
l2hyunwoo
0
130
【DroidKaigi 2026】「アクセシビリティを利用するとき、 アクセシビリティもまたこちらを利用している」 〜マルウェアによる攻撃と防衛について〜
halunoyo
0
110
新人はどこまで自力でやり、どこからAIに頼るべきか/エンジニア育成に向き合う_先輩たちの悩みと知見共有会
toppan_digital_dev
1
300
Featured
See All Featured
Art, The Web, and Tiny UX
lynnandtonic
304
22k
The World Runs on Bad Software
bkeepers
PRO
72
12k
Technical Leadership for Architectural Decision Making
baasie
3
550
How to Build an AI Search Optimization Roadmap - Criteria and Steps to Take #SEOIRL
aleyda
1
2.2k
How Fast Is Fast Enough? [PerfNow 2025]
tammyeverts
3
850
The agentic SEO stack - context over prompts
schlessera
0
880
It's Worth the Effort
3n
188
29k
Collaborative Software Design: How to facilitate domain modelling decisions
baasie
1
300
16th Malabo Montpellier Forum Presentation
akademiya2063
PRO
0
370
svc-hook: hooking system calls on ARM64 by binary rewriting
retrage
2
550
Designing Powerful Visuals for Engaging Learning
tmiket
1
520
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
6.1k
Transcript
batch-blur Tauri v2 で作る一括画像ブラー 吉澤亜斗武 1
なぜ作ったか(1) 社内勉強会のために作った資料を公開しようとすると修正が必要なことがある。 社内の機密情報 他社のプラットフォーム / プロダクトの情報 クライアント周りの情報 画像の場合は具体的なコンテンツを隠しつつ、表示の大まかなイメージやレイアウト 構成は伝えたい。そのためにぼかし(ブラー)を適用する。 2
なぜ作ったか(2) 画像が多い場合だと結構大変。 一般的な画像編集ソフトは、一括処理は苦手(バッチ機能がない/あっても手順 が煩雑で、1枚ずつ処理すると手間がかかる) CLI ツールなら一括適用できるが、フィルターの強さが適切かプレビューできな い。そもそもCLIは非技術者にはハードルが高く、使える人が限られる。 3
成果物 複数画像を読み込み → ブラー設定 → 一括書き出し するデスクトップアプリ https://github.com/yAtomtom/batch-blur 4
アジェンダ の技術選定 2. アーキテクチャ(責務分割・IPC 境界・レイヤリング) 3. WYSIWYG プレビューの設計 4. 画像処理アルゴリズム(書くボックスと合成するモザイク)
1. Tauri v2 5
1. Tauri v2 の技術選定 6
技術選定の評価軸 処理速度は個人で使うスケールでは大きな問題にならず、必要に応じて改善 → 優先度の高い軸で選定し、速度は決め手にしない 評価軸(候補) 配布容易性(単一バイナリ・小サイズ) 保守性(WYSIWYG・単一実装・テスト容易) 安全性(メモリ安全・権限モデル) メモリ(実行時フットプリント) 処理速度(ピクセル処理の速さ)
本アプリでの優先度 高 高 高 中 低(想定規模では決め手にならない) 7
なぜ Tauri v2 か 軽量(バイナリ小・メモリ小) Rust バックエンド(型安全・ネイティブなピクセル処理) OS 標準 WebView(Chromium
を同梱しない) Skia FW 言語 描画 アイドルRAM バンドル Electron JS Chromium ~150–300MB ~85–150MB Tauri v2 Rust OS WebView ~30–80MB ~0.6–3MB Wails v2 Go OS WebView ~10MB* ~15MB* Flutter Dart Skia ~20–35MB ~18MB+ Avalonia C# Skia C++ ネイティブ 数MB〜 Qt 低 数十MB 同梱 ~50–70MB =アプリが自前でピクセルを描く 2D 描画エンジン(全 OS で同一の見た目)/ネイティブ=OS 標準ウィジェットで描画(OS 純正の見た目・挙動) 8
メモリ優位の前提と限界 は WebView2 = Chromium → 実行時RAMは Electron と大差なく、計測 法次第で逆転もある
差が明確なのは macOS / Linux(OS が WebView を共有し専有コピーを持たない 分、実効メモリが小さい) 代表ベンチは測定法依存かつ N=1(実アプリ1本の単機計測)で、一般化には注意 Windows 代表実測: Electron ~200–300MB / Tauri ~30–40MB(最小〜小規模アプリのアイドル時, OS 標準ツール計測)。N=1 出典=levminer(実アプリ Authme を単機計測)。測定法の争 点=Tauri #5889(RSS が Chromium 共有メモリを二重計上、PSS/USS で結果が変わる) 9
Tauri v1 → v2: 何が変わったか モバイル対応(iOS/Android)が目玉 — 本アプリは未使用だが v2 の中核
コア機能のプラグイン化 — 本アプリの tauri-plugin-dialog も v2 プラグイン セキュリティ刷新: v1 の allowlist → Capabilities / Permissions ( capabilities/default.json ) IPC 強化: Channel でストリーミング(進捗 ExportProgress に活用) 設定 tauri.conf.json 再編・Rust API 変更で v1 とは非互換(移行作業が必要) 10
プラグイン化・権限・IPC — 何がうれしいか 概念 プラグイン化 Capabilities / Permissions (Inter-Process Communication/
プロセス間通信) IPC 何か コア機能を本体から切り出し、必要分だけ導 入する仕組み 「どの window が どの API を呼べるか」を宣 言的に制限する権限モデル Web(JS) ↔ Rust の別プロセスを繋ぐ呼び出 し・データ交換 うれしさ 使う機能だけ=軽い・攻撃面が小/本体と独 立に更新・拡張 最小権限を強制/WebView が触れるネイテ ィブ API を限定・監査可能 重い/危険な処理を Rust に隔離/型付き境 界・ Channel で逐次配信 プラグイン化=機能を crates/npm のように付け外し。使わない機能を同梱しない→軽く安全、コミュニティ拡張が容易/Capabilities/Permissions=v1 の allowlist(全体 ON/OFF)を window×API 単位の細粒度 ACL に(permission=個々の許可、capability=束ねて window に付与) 。XSS/サプライチェーン被害を限定、JSON で監査可/IPC= WebView と Rust は別プロセス。invoke() で呼び出し、Channel/Event で逆方向。v2 の Channel は1件ずつ stream(逐次UI更新に活用。キャンセルは Channel の機能ではなく cancel_export +AtomicBool フラグで実現) 11
ピクセル処理の言語・ライブラリ選定 (ブラウザ標準の描画API)でも描けるが、JS(プレビュー)と Rust(書き出 し の二重実装は見た目がズレる → Rust に一本化、同一コードで 見たまま出力(WYSIWYG) 型安全・ネイティブ速度・単一バイナリも両立。各言語のライブラリは?
Canvas ) エコシステム 主ライブラリ C/C++ OpenCV / libvips / Skia Node sharp(libvips) / jimp Python OpenCV / Pillow Rust image / imageproc Go imaging / bimg(libvips) 種別 ネイティブSIMD native / 純JS ネイティブbinding 純Rust 純Go / native .NET ImageSharp / SkiaSharp managed / native Java Java2D ConvolveOp managed 速度帯 最速(gold standard) sharp最速 / jimp ~40–50×遅 速い 中(SIMD薄い) まちまち 中 低(分離実装なし) 12
技術選定の妥当性と弱み 妥当な点: メモリ安全+単一バイナリ(libjpeg 等 C 依存を同梱せず配布容易)+ プレビュー/書き出しで同一実装(WYSIWYG) 弱み: 最速ではない —
事実上の標準は OpenCV/libvips。 imageproc は rayon 並列 はあるが SIMD 前面設計ではない(特定条件で数倍改善の報告あり) なぜ今 imageproc か: image crate と統合され依存最小・実績十分で安定。速度が 問題化した時の差し替え先は用途別の特化ライブラリ — ブラーは libblur 、縮 小は fast_image_resize (YAGNI) 並列化の2軸 — rayon=処理を複数コアに分散(Rust のデータ並列ライブラリ。 par_iter() 等)/SIMD(Single Instruction, Multiple Data)=1命令で複数画素を同時計算(1 コア内)。OpenCV/libvips は SIMD も駆使、 imageproc は rayon 中心 13
最終的な技術選定(結論) 結論: 本アプリの要件では Tauri v2 がベスト 決め手は処理速度ではなく「配布・安全・WYSIWYG」 想定規模では速度が問題になりにくく、一般層への単一バイナリ配布が効く 速度がボトルネックになったら Rust
圏内で差し替え( imageproc → libblur ) もし要件が… 超大規模・巨大画像で低メモリ サーバ / CLI バッチ GPU・リアルタイム映像 中規模・配布重視の GUI(=本件) より適する選択 libvips 系(sharp / bimg / NetVips) Python+OpenCV / Go+bimg C++/Skia / wgpu Tauri v2 × Rust 14
アーキテクチャ Rust ⇔ Web の責務分割・IPC 境界・レイヤリング 2. 15
責務分割: Rust ⇔ Web 境界の判断軸は「ピクセル/ファイルシステム(FS)に触るか」— 触る処理はすべて Rust へ集約 ピクセル実装は Rust
に1つだけ → プレビューと書き出しが構造的に一致 (WYSIWYG) Rust (pixels / FS) ║ blur / decode / encode ║ repository: FS I/O / cancel ║ domain: filter/save/preset ║ Web (UI / state) orchestration / history keybindings / i18n / save domain: EditHistory/SaveConfig 境界は IPC の 1 本(Web = React 18 + Vite 5) 16
IPC 境界 Web (JS) Rust #[command] │ invoke("export_batch", {args}) │
│─────────────────────────────────>│ camelCase→snake_case・spawn_blocking │ ExportProgress {done,total} │ │<╌╌╌╌╌╌╌╌╌╌╌╌ Channel ╌╌╌╌╌╌╌╌╌╌╌╌│ 1 ファイルごとに逐次 │ Result<ExportOutcome> │ │<─────────────────────────────────│ 完了数・キャンセル・失敗一覧を返す コマンド load_images / generate_preview / export_batch / cancel_export — pixel は imaging(codec/blur)、ストレージ I/O は repository(DI) へ委譲 境界の仕事は実行手順の組み立てと共有状態の管理 — spawn_blocking への退 避、キャンセルフラグ、プレビューキャッシュ、進捗の逐次送出 ファイル単位の失敗・キャンセルはエラーではなく結果 — ExportOutcome が生エ ラーを保持し(握り潰さない)、 Err は前提検証とインフラ障害のみ 17 4
spawn_blocking — なぜ UI が固まらないか 重い同期処理を専用スレッドへ逃がす関数 ( tauri::async_runtime , tokio)
直接実行だと async ワーカーを占有し IPC/イベントが詰まる → 退避すれば即解放 され、プレビュー更新・キャンセルなど UI が応答し続ける spawn_blocking = 直接実行: async worker |==== blur / encode / write ====| spawn_blocking: async worker |-> await 即解放(他の IPC を処理) block thread |==== blur / encode / write ====| IPC/イベント滞留 → UI 停止 完了後に結果を返す =Rust で最も広く使われる非同期ランタイム。 async タスクのスケジューリングとワーカースレッド管理を担い、 spawn_blocking 用のブロッキング専用スレッドプール も提供する(Tauri が内部で採用)。 tokio 18
IPC 境界を外殻とするレイヤリング(テスタビリティ) ┌──────────────────────────────────────────────────┐ │ commands / types │ │ imaging(codec/blur)
repository(port+local_fs) │ │ domain (filter/save/preset) │ └──────────────────────────────────────────────────┘ 外殻: IPC 境界(repository を DI) アダプタ: 純粋コーデック/ストレージ・ポート 内核: 純粋コア(image/FS/Tauri 非依存) 依存は内向き 1 方向(外側 → 内核 domain )、逆はない domain は image/FS/Tauri 非依存 → #[cfg(test)] で同ファイル完結 repository が FS を隔離 → imaging も純粋コーデックに(in-memory で差し替 えテスト) [lib] 分離で GUI 起動なしに domain/imaging/repository をテスト #[cfg(test)] テスト可) =テスト時だけコンパイルする Rust の印(実装と同ファイルに書く)/ [lib] =アプリをライブラリとして切り出す Cargo.toml 設定(GUI 本体と別にビルド・ 19
外殻・アダプタ・内核のディレクトリ構成 Rust バックエンド src-tauri/src/ ├ main.rs, lib.rs ├ commands.rs, types.rs
├ domain/ ├ imaging/ └ repository/ Web 起動・層宣言・IPC ハンドラ登録 IPC 境界(調整+契約型・repository を DI) 純粋コア: filter, save, preset 純粋コーデック+カーネル: codec, blur ストレージ・ポート+アダプタ: mod(ImageRepository), local_fs フロントエンド src/ ├ app/ ├ ipc/ ├ domain/ ├ features/ └ shared/, i18n/ オーケストレーション(App.tsx) IPC 境界ミラー: commands, types 純粋コア(TS): EditHistory, SaveConfig 機能: asset-catalog, editor, batch 横断: keybindings, 翻訳 20
プレビューの設計 単一実装(imaging/blur)とスケール補正 3. WYSIWYG 21
課題: プレビューが「別物」になる 2 つのズレ 実装のズレ: JS(Canvas) でプレビュー・Rust で書き出し → 1章の
Rust 一本化で解 消済み 解像度のズレ: プレビューは縮小・書き出しはフル → 同一実装でも同じ半径で結 果が変わる 同じ radius=20 を当てると… 書き出し 4000px の画像に r=20 → 画像幅の 0.5% プレビュー 1600px の画像に r=20 → 画像幅の 1.25%(2.5 倍ぼける) 本章の WYSIWYG = プレビューで決めた強度が、フル出力でも同じ見え方になる こと 22
プレビュー 1 回の経路 Web slider → debounce 130ms → invoke
generate_preview{…, maxDim, reqId} Rust ├ cache hit → 縮小ベースを再利用 └ cache miss → read → decode → downscale(長辺を 1600px へ) → cache へ格納 → apply_stack_scaled(base, stack, scale) → PNG → base64 data URL Web 応答を現在の選択と照合(古い応答は捨てる)→ <img src={dataUrl}> 重い前処理はキャッシュ、毎回走るのはブラー+PNG、UI 側は間引きと採否だけ 以降のスライドで、上記の図の工夫した点を1つずつ — 競合制御 / スケール補正 / キャッシュ =連続入力を間引き、最後の入力から一定時間経ってから 1 回だけ送る仕組み。スライダーを端から端まで動かしても、送信は指を止めた 1 回だけ。130ms は「即応 と感じる 0.1 秒前後」かつ連打をまとめられる帯/1600px=プレビュー基準画像の長辺上限。上げるほど忠実・下げるほど毎入力が軽い。元が 1600px 以下なら縮小せず等倍 (拡大はしない) debounce 23
間引きと採否 — 非同期 UI の競合制御 要求A invoke ──────────────> 応答A 要求B
invoke ────> 応答B effect は cleanup 済み → stale で破棄 現行 effect の要求 → 採用 で間引き、採否は effect クロージャの stale フラグが持つ cleanup が旧要求を無効化し、結果は render 時に path 照合 → 旧画像を出さない in-flight は止めない — プレビューは軽く副作用もないので捨てる方が単純 DEBOUNCE_MS = 130 =送信済みでまだ応答が返っていないリクエスト。処理が debounce の 130ms より長引くと要求が重なり、複数が同時に in-flight になる。 invoke は発行後に取り消 せず 側は最後まで走る = 受け取ってから捨てる in-flight Rust 24
単一実装 — Rust の 2 経路が blur_rgba に収束 preview export
apply_stack_scaled(img, stack, scale) ┐ max(1, round(radius × scale)) ├→ blur_rgba(img, kind, r) apply_stack(img, stack) ┘ radius そのまま 上がプレビュー(縮小画像)、下が書き出し(フルサイズ)の経路 ブラーを掛けるコードは blur_rgba ただ 1 つ。2 経路の違いは「半径をスケール するか」だけ デコードも decode_rgba の共通経路 → EXIF 向きの正規化も両経路で一致する → ズレうる箇所が構造的に 1 行に閉じる(レビューで守り切れる粒度) 25
半径のスケール補正 preview_radius = max(1, round(radius × scale)) , 1600 scale
= min (1, ) max(w, h) 半径はピクセル単位の長さ → 画像を scale 倍に縮めれば、同じ見え方になる半 径も scale 倍 補正しないと縮小画像が過剰にぼける(前ページの 0.5% → 1.25%) PREVIEW_MAX_DIM = 1600 (長辺) 。元が 1600 以下なら scale = 1.0 で拡大はしな い 上式が効くのは正の半径のみ — radius = 0 は下限を適用せず 0 のまま(=恒 等) 元画像 scale UI radius 4000×3000 0.40 20 8 4000×3000 0.40 1 1 round preview_radius ( は 0 → 下限 1) 26
スケール補正の限界 — プレビューだけ順序が逆 プレビューは縮小 → ブラー、揃えたい相手はブラー → 縮小(フル出力を画面で 見た姿) この
2 つは可換でない → 一致は保証ではなく実用上の近似 なぜ厳密な「ブラー → 縮小」にしないか: 毎入力でフル解像度のブラーと縮小を やり直す → 画素数 6.25 倍(4000×3000) 。縮小を初回で済ませる今の順序が LRU(1) キャッ シュの前提 27
近似が残すズレ — 丸めと表示側の縮小 丸めの量子化: 4000px の画像で radius=1 は round(1 ×
0.4) = 0 = 恒等になっ ていた → プレビュー素通し・出力はぼけるという WYSIWYG 破れ → 下限 1 で修正済み 下限で「ぼけの存在」は保証されるが、弱ブラー × 強縮小では過剰側に ±1 画素 相当の誤差が残る 表示側でもう一段縮む: CSS の max-width/max-height + object-fit: contain 残る誤差は実用上ほぼ使わない領域 → 問題化したら詰める(YAGNI) =寸法の上限を親( .canvas-viewport )に制限。 <img> は固有の縦横比を持つので、上限に当たると比を保ったまま縮む/object-fit: contain= 箱の大きさが決まった後、中の画像をどう収めるかの指定。比を保って収まる最大で描き、余りは余白 max-width / max-height: 100% 28
縮小ベースの 1エントリキャッシュ(LRU(1)) キー = (ResourceLocation, max_dim, fingerprint) = どの画像を・どこまで縮め たか・どの内容か
値 = ブラー前の縮小画像 + scale(+原本の ICC。結果でないので設定変更でも ヒット) 1 件で足りる: プレビューは常に選択中の 1 枚=ミスは切替時と内容更新時だけ ミス時の実費 = read → decode → downscale(画素数比例、12MP で 300ms) → N 件の是非は切替の待ち時間次第。1 件 約 6〜10MB で有界(縦横比で変動) =大きい画像ほど得 → 毎入力で走るのは ブラー + PNG encode。それを速くする話が次章 LRU(1) =容量 1 の LRU。直近の 1 件だけ保持し、別のキーが来たら捨てる(容量 1 では追い出し戦略が退化し、実装は Option<PreviewBase> の上書きだけ) 29
画像処理アルゴリズム 書くボックスと合成するモザイク — 速さ・端・正し さの決め方 4. 30
4 章の要点 — 書くか合成するかで、核と格子の扱いは逆になる 同じ「隠す」でも、ボックスとモザイクは判断が逐一反転する ボックス(核で混ぜる) モザイク(格子で潰す) 作り方 型が合わず書く resize
2 回の合成 O(n) の作り方 状態の再利用 出力の縮小で相殺 端の解き方 仕様として受け取る clamp を選んで解く 残る課題: モザイクの格子だけ WYSIWYG が届かない(プレビューと書き出しでセ ルがズレる) 核(カーネル)=出力 1 画素を決めるとき、周囲の画素に掛ける重みの表。形がぼけの質を決める/格子=入力に依らず位置だけで決まるセルの区切り。モザイクはこの中を 1 色に潰す 31
核と格子の前提 — 重みで混ぜるか、位置で潰すか ガウス: 中心ほど重い釣鐘型の加重平均 → 滑らかなぼけ ボックス: 重みが一律の単純平均 →
O(n) 化の余地がある(n=画素数) モザイク: 核を持たずセル(1 辺 b 画素)単位で 1 色に潰す=区分一定 モザイクだけ平行移動不変でない — 格子に位相がある(章末で効く) 重み ガウス核 │ █ █ █ │ █ █ █ █ █ │ █ █ █ █ █ █ █ █ █ └──────────────────────→ x in : 1 2 4 7 8 6 3 1 2 5 8 6 out : 2 2 2 7 7 7 2 2 2 6 6 6 重み ボックス核 │ █ █ █ █ █ █ █ █ █ ← すべて同じ重み │ █ █ █ █ █ █ █ █ █ │ █ █ █ █ █ █ █ █ █ └──────────────────────→ x ← モザイクだけ核を持たない(格子で潰す) ←[--b=3--] セル内は同値。格子は入力に依らず固定 32
混ぜ方も潰し方も半径 1 本 — スライダーからの固定変換 r σ= , 2
d = 2r + 1, b=r+1 は radius(r)1 本。種別ごとの強度へ固定変換で接続する 変換は 1 関数に固定( radius_to_sigma / radius_to_block )— 変えるなら 1 行 radius=0 で imageproc は assert!(sigma > 0) で panic → 早期 return で恒等を返し、防御を境界 1 箇所に置く 効き幅は揃えていない(b = r + 1 < d = 2r + 1)=モザイクが弱い UI = の radius(ぼかす半径の画素数)/σ=ガウスの広がり幅(重みの約95%が ±2σ)。σ = r/2 は実効範囲 ±2σ をボックスの ±r に揃えた取り決めで等価変換ではない/d= ボックスが平均する窓幅(画素数)。奇数なのは窓の中心を 1 画素に定めるため(偶数だと半画素ずれる)/b=モザイクのセル 1 辺の画素数 r UI 33
固定変換の先で詰まる — ボックスは型が合わずライブラリを使え ない ガウス: imageproc::gaussian_blur_f32 に委譲 — ジェネリックで RgbaImage
をそ のまま渡せる ボックス: imageproc::box_filter は GrayImage 専用 → アルゴリズムでなく型で 弾かれる image の blur / fast_blur もガウス系のみ。ボックス平均そのものは提供なし 「無い」の判定は 3 段階: ①そのまま使える ②型を合わせれば使える ③本当に無 い ボックスは②(分解4回+再合成)を経て③扱い → 「無い車輪」だけ作る ジェネリック=画素型を差し替えられる関数の書き方( <P: Pixel> )。 gaussian_blur_f32 は RGBA でも Gray でも呼べるが、 box_filter は引数の型が GrayImage に固定され ている 34
型が合わないので自前で書く — 素朴な 2D は O(n ⋅ r2 ) 各画素で
d × d の窓を総和して平均 → 仕事量 = n × d2 半径2倍で4倍遅い: 12MP・r=20 → 窓 41²=1681 → 約200億サンプル/チャネル 磨いても消えない構造 → 窓の「形」に注目する(次ページ) 出力1画素ごとに窓 d×d を丸ごと読み直す(r=1, d=3 → 9 画素) ┌──┬──┬──┬──┬──┐ ┌──┬──┬──┬──┬──┐ │▓ │▓ │▓ │ │ │ │ │▓ │▓ │▓ │ │ • = 出力画素 │▓ │• │▓ │ │ │ → │ │▓ │• │▓ │ │ ▓ = 読む近傍(d² = 9) │▓ │▓ │▓ │ │ │ │ │▓ │▓ │▓ │ │ 1 画素進むと 9 画素を読み直し └──┴──┴──┴──┴──┘ └──┴──┴──┴──┴──┘ 記法=入力が大きくなったとき計算量が「どう増えるか」の形だけを比べる記法(定数倍は無視)。ここでは n=画素数、r=ブラー半径。O(n ⋅ r2 )=「画素数に比例、か つ半径の2乗に比例」 O 35
2D の窓は縦横に分離できる — d2 → 2d 重みが一律 → 1/d2 を
1/d × 1/d の外積に分解できる(分離可能) 「d × d の平均」=「横 → 縦の 1D 平均」の2段。読みは 1681 → 82 実装も 水平 src→tmp / 垂直 tmp→out の 2 パス。ランク1核なら効く定石 2D 窓を一括(読み d² = 9) ┌───┬───┬───┐ │ ▓ │ ▓ │ ▓ │ │ ▓ │ • │ ▓ │ = │ ▓ │ ▓ │ ▓ │ └───┴───┴───┘ 水平パス src→tmp ▓ ─ • ─ ▓ ∘ 垂直パス tmp→out ▓ │ • │ ▓ 分離可能性(separability)=2D カーネルが「横1本 ⊗ 縦1本」の外積に分解できる性質。ボックス核なら Kbox = k ⊗ k, k = d1 [1, … , 1] で、d × d の読みが d + d に減る (分解できない例: 円形の窓) 36
分離の次は r を消す — running-sum で O(n) let mut sum:
i64 = (-r..=r).map(sample).sum(); sum += sample(x + r) - sample(x - 1 - r); tmp[i] = ((sum + half) / d) as u8; // 初期和: 行・列の先頭で1回だけ O(r) // 差分更新: 入る1画素を足し、出る1画素を引く // 固定除数 d の整数四捨五入(half = d/2) 窓を1つ右へ → 総和は作り直さない。更新は d に依存せず約4更新/画素 u8 → i64 昇格(負の座標と桁あふれ)+ (sum+half)/d の四捨五入 整数の加減算は正確 → 移動和に誤差が蓄積しない(パス間の u8 丸めは別)。状態 は sum 1変数 ガウス不可(重みが位置で変わる)→ 分離止まり。ボックスだけ 1681 → 82 → 約 4 running-sum (移動和)=直前の窓の合計を使い回し、端の差分だけで次の合計を得る技法。総和の再計算 O(d) が O(1) になる 37
running-sum の端 — ゼロ埋めは端を暗くする 方式 端の見え方 running-sum との相性 ゼロ埋め 端が黒ずむ(0
が混ざる) ◦ そのまま使える 除数を実画素数に縮小 暗くならない × 端分岐+除数の再計算で単純さが崩れる ミラー(折り返し) clamp とほぼ同等 △ 添字の折り返し計算が複雑になるだけ エッジ複製(採用) 暗くならない ◦ clamp 1行で済む エッジ複製=画像の外側を最も近い端の画素が続いているとみなす境界規則 端を複製しても除数 d は縮めない → 窓に 0 が混ざらず端が暗くならない 境界の読み出しはクロージャ 1 つに閉じ込め、ループ本体は端を意識しない 一様画像は端まで不変の性質テスト1本で境界バグを捕まえる 38
ボックスは端まで自前、モザイクは合成で済む に pixelate / mosaic は無い — ボックスと同じ「無い」 だがモザイクは既にあるもので書ける: resize
を 2 回呼ぶだけ(依存追加ゼロ・ 実体 13 行) ボックス=書くしかない(型が合わない)/モザイク=合成で済む 実装方針: まず合成を探し、合成できない時だけ書く — 「無い」=「合成でも作 れない」 image / imageproc fn mosaic_rgba(img: &RgbaImage, block: u32) -> RgbaImage { if block <= 1 { return img.clone(); } // 恒等 let (w, h) = img.dimensions(); let small = resize(img, w.div_ceil(block), h.div_ceil(block), Triangle); // 縮小=平均 resize(&small, w, h, Nearest) // 拡大=複製 } 39
合成の中身 — 縮小 Triangle・拡大 Nearest の非対称 (三角)=重み 1 − ∣x∣
の加重平均。混ぜる幅は縮小率ぶん伸びる Nearest(最近傍)=入力を 1画素だけ読み、値をそのまま使う(混ぜない) セル辺長は半径から決まる b = r + 1、セル数は ⌈w/b⌉ × ⌈h/b⌉(切り上げ= 端が半端でもセルを捨てない) 縮小で情報を捨て、拡大で戻さない — この非対称がモザイクの本体 Triangle 縮小 \ 拡大 Nearest(複製・採用) Triangle(補間) Triangle(平均・採用) モザイク(セルが立つ) ただのぼかし(セルが溶ける) Nearest(点サンプル) 隠蔽にならない(代表1画素が原寸で復活) 弱いぼかし 左下が事故: 縮小を Nearest にすると代表 1 画素が原寸で戻り、文字の芯が読める ⌈x⌉ (天井関数)=x 以上の最小の整数。コードでは u32::div_ceil が対応/block=コード上の引数名(本文の b)。UI の radius とは変換関数 1 つだけで接続する 40
縮小の Triangle は「厳密なブロック平均」ではない の ratio = 入力幅 ÷ 出力幅。カーネルはこの ratio
倍に引き伸ばされる 幅 7 画素・b=3 なら 7 → 3 セルの縮小 → ratio = 7/3 ≈ 2.3=タップは ±2.3 隣の block まで届く → セル値は「block 内 b 画素の平均」ではなくテント加重平 均 厳密なブロック平均なら thumbnail が実在する — 型制約と整数比前提があり不 採用 resize pixel : 1 2 3 4 5 weight: 0.06 0.24 0.41 0.24 0.06 block :[------------][-------------------] sum : 0.29 0.71 ← 出力セル 1(自分の block = 画素 3,4,5) 中心は画素 3、±2.3 の外の 0 と 6 は重み 0 左=隣の block / 右=自分の block ← 約 29% が隣の block から来る タップ=1 出力を作るために読む入力画素とその本数(加重平均の項数)。Nearest は 1 タップ、Triangle は引き伸ばした幅のぶんだけ増える/ thumbnail = image にあるサム ネイル生成用の縮小関数。 resize と違い、ブロックを矩形のまま厳密に平均する 41
厳密でなくても O(n) — b が相殺する計算量の会計 会計の作法: コスト = 出力画素数 ×
タップ数。縮小は出力 1/b2 ・タップ b2 倍で 相殺 素朴 2D は bn (2b)2 + n = 5n、分離すると 3n + 3n/b(b=21 で 3.1) 2 パス 出力画素数 タップ コスト(総計) ① 縮小・垂直 w × h/b 2b 2n ② 縮小・水平 w/b × h/b 2b 2n/b ③ 拡大・垂直 w/b × h 1 n/b ④ 拡大・水平 w × h 1 n オーダーに r が残るかは「出力数がどう変わるか」だけで決まる 42
計算量が同じでも中身は逆 — 状態の再利用 vs 出力の縮小 窓の大きさ 1画素あたり・素朴 2D 1画素あたり・分離後 r
を上げると オーダー(総計) 中間バッファ ボックス モザイク d = 2r + 1 ( ) 2d ≈ 4r(r=20: 82) r に比例して増える b=r+1 d2 ≈ 4r2 r=20: 1681 O(n ⋅ r) → running-sum u8 1 枚 = 48MB 固定 5 ( ) 減る(4.5 → 3 に漸近) O(n+n/r) = O(n)(1 ≤ r ≤ n で有界) f32 = 96MB(b=2)〜9.2MB(b=21) 3 + 3/(r + 1) r=20: 3.1 で O(n) 読み回数は近いが中身が違う: ボックスは整数の加減算、モザイクは f32 の乗加算 メモリは逆: モザイクは弱いほど重い(縮小率が小さいほど中間が大きい) モザイクは自前で書く方が速い(1画素あたり 2 対 3.1)— それでも合成=自前の コードを増やさず、端と正規化も任せられる 43
逆なのは端の扱いも — resize 任せとエッジ複製の対比 端専用の分岐は 1 つも無い — 切り上げでセル数を決め、 Nearest
が幅を割り振 るだけ 7px を 3 セルに割ると 3/3/1 ではなく 2/3/2 — 中心座標で選ぶ結果、均等に再配 分される 端が暗くならないのも resize 任せ(重みを正規化)— ボックスがエッジ複製で 選んで解いた問題を、モザイクはライブラリの仕様として受け取っている 代償: image がサンプル位置を変えればセル境界も静かに変わる → 気づく手立て が次ページ out : 0 1 2 3 4 5 6 center: 0.21 0.64 1.07 1.50 1.93 2.36 2.79 floor : 0 0 1 1 1 2 2 cell :[----------][----------------][----------] w = 7 → セル 3(ratio = 3/7) ← 幅 2/3/2。3/3/1 にはならない 44
resize 任せの代償 — golden でなく性質で縛る (期待画像との完全一致)は書けるが、 image の実装に過剰固定され依存 更新で落ちる 代わりに入力によらず成り立つ関係=性質だけを並べ、「壊れ方」を捕まえる網に
する golden テスト radius_zero_is_identity mosaic_preserves_dimensions_on_non_multiples mosaic_is_piecewise_constant_on_exact_grid mosaic_bounds_distinct_colors_on_non_multiples mosaic_uniform_image_is_unchanged mosaic_preview_never_collapses_to_identity 何を固定するか radius 0 は恒等(3 種別共通) 寸法不変(7×5, b=3) 整数倍寸法ならセル内一定(8×6, b=2) 色数 ≤ セル数(境界の丸め実装に依存しない上界) 一様画像は不変(重みの正規化) 縮小率 0.1 でも素通しにならない 45
性質でも縛れないもの — b = r + 1 はスケールと可換でない round(rs) +
1 実装(スケール → 変換) = round(s(r + 1)) = 順序を直した案 s(r + 1) 理想(整数でない) =プレビューの縮小率(3章の scale 。4000px → 1600px なら s = 0.4) プレビューだけ半径を rs にして丸めてから変換へ(書き出しは r のまま) ガウスの σ = r/2 は線形 → 丸めを除けばスケールと可換 モザイクの b = r + 1 は +1 のオフセット → 丸めを除いても可換でない しかも b は整数( u32 )→ 一致する理想 s(r + 1) はそもそも表現できず、順序 を直しても round の誤差が残る s 46
可換でないズレの実際 — 書き出しとプレビューでセル数が食い違 う export : 4000px / b =
20+1 = 21 → ceil(4000/21) = 191 セル preview: 1600px / b = round(20*0.4)+1 = 9 → ceil(1600/9) = 178 セル ideal : 1600px / b = 0.4*(20+1) = 8.4 → ceil(1600/8.4) = 191 セル(書き出しと一致) round : 1600px / b = round(8.4) = 8 → ceil(1600/8) = 200 セル プレビューのセルが約 7% 大きく、格子の位相もずれる 理想の 8.4 なら一致するが b は整数 → 丸めた 8 では今度は 200 セルになる ぼかしのズレは滲みの差、モザイクのズレは数えられる格子の差 → 目につきやす い それでもプレビューが素通しになることは無い: 丸めた半径の下限が 1 → b = r + 1 ≥ 2 = セルは必ず 2 画素以上(b = 1 なら恒等=素通し) 性質テストが縛るのは素通しの回避だけ — 書き出しとの一致は縛れない 47
ズレを詰める 4 案 — どれも別のものと引き換え 改良案 効果 代償 変換→block の順でスケール
200 セル=ズレ 7%→5% 一致しない。種別ごとの分岐 プレビューも原寸で計算 ズレゼロ 画素数 6.25 倍。可視領域だけ評価する基盤 sb が整数になる縮小率 格子も位相も一致 寸法が半径依存=縮小キャッシュ無効 「等倍でだけ正確」と明示 実装コストゼロ WYSIWYG を UI 側に降ろす 既存ソフト 規約あり 既定で採用 例なし UI で採用 採っているのは現状(7% 大きい)— 隠す用途では見え方が伝われば足りる 「規約あり」=縮小レベルをフィルタに渡して追従させる(未追従はバグ扱い) 前例が無いのは 3 案目だけ — 縮小して計算するのは既知の天秤 48
まとめ 技術選定: Tauri v2、ピクセル処理は Rust に一本化 — 自前はボックスだけ 2. アーキテクチャ:
Rust=ピクセル/FS、Web=UI/状態 — 境界は IPC 1 本 3. WYSIWYG プレビュー: 書き出しと同一経路 — ズレはスケール補正に集約 4a. ボックス(書くしかない): 型が合わず自前 → 分離+running-sumで O(n ⋅ 1. r2 ) → O(n) 端は clamp を選んで解き、境界処理は 1 箇所に閉じる 4b. モザイク(合成で済む): resize 2 回・13 行 — 端はライブラリ仕様として受 け取る — 正しさは golden でなく性質で縛る/格子のズレは詰め切れず残る — 49