2024年6月9日日曜日

Photo Scanner を大幅に改善した

スマホをスキャナーにできるアプリ Photo Scanner を大幅に改善しました。 気付けばスキャンアプリも色々できていたので、改めて色々考え直してすべて再実装しました。 もはやコンセプト以外は原形を留めてませんが、まあ仕方ない。



1. 自動矩形探索の廃止→手動設定の追加

リアルタイムに画像からプリントを探索する機能、 具体的には cv.findContours() → 最大領域を四角形として自動判定する機能は、 精度が残念なのでやめました。 脳死して cv.findContours() だけだと、ちょっとの歪みや折れがあると安定した矩形探索はできません。 自動認識ではどうやっても意図しない動作が生まれます。 実装を頑張ったとしても、白黒領域が混在したプリントや、 カラフルなプリントに対応するのは、かなり難しいと言えます。 ちなみに Object Detection はモデルがデカすぎて使いにくいです。 そこで自動認識にすべてを委ねるのはやめて、手動で矩形を指定してもらい射影変換することにしました。

2. 矩形予測機能を追加

手動で矩形を指定してもらうのは手間なので、撮影後にプリントの予測点を提示することはやっています。 矩形点の予測は、Canny Edge Detection を使う方法などもありますが、単純な方法ではやはりうまくいきません。 ただ色々やっているうちに、cv.findContours() を使う場合、cv.medianBlur() などのノイズ処理が大きく精度に影響するとわかりました。 また普通に考えると、プリントをスキャンしたい場合は画質の問題から一つずつ撮影します。 そのため画像内には 1枚のプリントしかない仮定を置いてもまず大丈夫のはずです。 その場合 cv.findContours() →最大領域を cv.minAreaRect() が安定しそうなので実装してみました。 実際に動作は安定しており精度も良いので、割と満足です。ただ完璧とは思わないので前述の通り、手動設定できるようにしました。 他の方法では Canny Edge Detecttion と HoughLines を組み合わせる方法を見つけました。 未確認ですが複数のプリントを読み込む場合には良いかも知れません。

ところで OpenCV 4.10.0 から findContours() の処理結果が以前とは変わり、 cv.RETR_EXTERNAL の領域最大サイズが画面全体を示すようになりました。 画面内で1番大きな領域を取得できなくなったので、 cv.RETR_LIST などですべての輪郭を取得し、2番目に大きな領域を取得するのが良さそうでした。 このへんの動作も以前とは異なるはずです。何もしないのに壊れて割とハマりました。

3. Wasm SIMD/Threads 対応

OpenCV 4.10.0 から SIMD/Threads のビルドが簡単になったので、 SIMD/Threads に対応しています。速度面ではもうネイティブアプリと大差ないはずです。 GitHub Pages だとデフォルトでは動作しないので、cross-origin isolation で動くようにしています。 Wasm の様々なビルドオプションに対応するのが手間で、デバッグと Service Worker の設定が面倒なので、もう少し簡単に動いて欲しい。

速度面ではまったく不満がなくなりましたが、現状ではファイルサイズの課題があります。 おそらくその原因は一つの関数に大量のオプション実装が含まれているからです。 cv.findContours() → 最大領域を cv.minAreaRect() するだけでも 1.4MB 必要です。 OpenCV.js を使うのが癪で JavaScript での実現も一瞬だけ考えましたが、 画像処理は Wasm のほうが明らかに高速なのでつらい。 今回は諦めポイントの 1つですが、この記事を参考にして C/C++ でコードを書いて Wasm 化すると、 コンパイラが非常に強力なこともあって、インライン展開による最適化が期待できそうです。

4. Deep Denoise の廃止→様々な手動加工に対応

撮影後に自動的に深層学習を使った簡易的な影の除去処理を加えていましたが、 遅いので撮影後に編集モードで手動処理してもらうことにしました。 深層学習ではないアルゴリズム的なノイズ除去やフィルター処理もたくさん付けて、 高機能な加工ができるアプリになりました。2値化やシャープネスは割と使えると思います。 2値化は矩形領域の探索にも使っている処理の応用ですが、 いざブラウザ上でリアルタイムに実行できるように実装すると、 どのパラメータが重要か一目でわかって非常に良かったです。

たとえば cv.adaptiveThreshold() は C が超重要なことも今さら知りました。 その結果を活かして矩形探索の処理も安定化させたので、何事も作って見るものです。 それ以外のフィルター処理は glfx.js を利用しているだけなので、実のところ実装は簡単でした。 glfx.js は前から知ってはいたのですが、こんなに便利だとは気付いていなかった。 深層学習による影の除去はまた取り組んでみたい気持ちはありますが、 みんな脳死で深層学習なので、最近は深層学習しないほうが面白そうと思ってます。

5. Modal 廃止→画像編集ソフトっぽく

撮影後のプレビューは Modal だとわかりにくかったので、 カメラ表示していた部分に画像と操作ボタンを表示するようにしました。 実利用ではタブレットよりスマホのほうがよく使うと考えられますが、 スマホで Modal はもはや画面いっぱいに最大表示をしているのと一緒なので、 Modal は使い勝手が良くないです。 この変更によって画像編集エディタとしても割と使いやすくなった気がします。

他の細かな改善点を上げるとキリがないですが、 実装をコンポーネント化したり、高速化したり、細かなバグフィックスをしたり、 設定項目を削減して代わりに手動設定できるようにしたり、 かなり色々やりました。実装もほとんどやり直して、わかりやすいものにしました。 今回は Web Components はあまり使ってないですが、 すぐに切り替えられるような Class として実装しています。 これくらい処理が複雑なアプリになってくると、 Class 化によるコンポーネント化は行ったほうが明らかに楽ですね。 はるか昔に作ったアプリだったので色々残念なところがありましたが、 だいぶ満足のいく実装になりました。

UI もスマホのカメラアプリに似せて使いやすくしました。 スマホで Canvas を使ったらだいたい position:absoulte; などでコンポーネントを被せていく感じになり、割と難しいです。 スマホに寄せて実装すると PC の UI がシンプルすぎて使いにくい気もしたので、ちまちま修正していくつもりです。 PC だとやっぱり画面のシンプルさより、編集ツールとしての使いやすさのほうが欲しいです。 まだ実装したいことは色々ありますが、アプリとして利用できるレベルに到達したのでひとまず公開です。

0 件のコメント: