
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、少し前に私にとって少し前に驚くべき事実を知ることになりました。
それは・・・・・
画像処理パッケージ「OpenCV」がブラウザでも使える
というものです。
OpenCV
とは、Python
などでは定番の画像処理パッケージのことで、PHP
で言うところのGD
のような存在です。
しかし、GD
と比べるとより専門的な処理ができるので、数学的な知識が多少必要な分、できることもより多くなっていたりします。
ということで、今回はこのJavaScript
版のOpenCV
を使って、「ななめに写ってしまった文書」を自動的に修正して画像にピッタリになるように修正してみたいと思います。
つまり、こういうことですね。
↓↓↓
これが自動で・・・
ピッタリ
では、実際にやってみましょう。
「ブラウザだけで
OpenCVってすごいですね」
開発環境: OpenCV.js 4.5
目次 [非表示]
準備する
OpenCV.js
にはCDN
が用意されているので直接読み込むのがシンプルでいいです。ただ、ファイルサイズが大きいので、読み込みに時間がかかる場合があります。
そのため、もしサイトに設置し使いたい場合は、以下のページから「opencv-(バージョン番号)-docs.zip」をダウンロードし、展開した中にあるopencv.js
を/js/opencv.js
として使ってください。
ダウンロードURL: https://github.com/opencv/opencv/releases
以下はバージョン4.5.2の場合です。
※ なお、後でServiceWorker
による高速化を紹介していますが、その場合は上記の手順で実装してください。
歪みを修正する部分をつくる
では、いきなりメイン「歪んだ文書をピッタリさせる」部分です。
<html>
<head>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="p-3">
<div v-if="!initialized">
opencvを準備中です...しばらくお待ちください。
</div>
<div v-else>
<input type="file" accept="image/*" @change="onFileChange">
<div class="pt-3" v-if="imageData">
<img id="image" class="float-left" :src="imageData" v-if="imageData">
<canvas id="outputCanvas"></canvas>
</div>
<div class="pt-3 clear-both" v-if="imageData">
<button type="button" class="bg-indigo-500 text-indigo-50 p-2 rounded mb-3" @click="transform">歪みを修正する</button>
</div>
</div>
</div>
<script src="https://unpkg.com/vue@3.0.2/dist/vue.global.prod.js"></script>
<script src="https://docs.opencv.org/4.5.2/opencv.js"></script>
<script>
Vue.createApp({
data() {
return {
initialized: false,
imageData: null,
MIN_CONTOURS_SCALE: 20, // 最低元画像比率
THRESHOLD: 170, // モノクロのしきい値
}
},
methods: {
onFileChange(e) {
const files = e.target.files;
if(files.length > 0) {
const file = files[0];
const reader = new FileReader();
reader.onload = (e) => {
this.imageData = e.target.result;
};
reader.readAsDataURL(file);
}
},
// opencv
transform() {
const imageElement = document.querySelector('#image');
const im = cv.imread(imageElement);
const pts = this.getContoursPoints(im);
if(pts) {
const transformedIm = this.getTransformedImage(im, pts);
cv.imshow('outputCanvas', transformedIm);
console.log('Done!');
} else {
console.log('Failed...');
}
im.delete();
},
getContoursPoints(im) {
// Image area
const imRectArea = im.cols * im.rows // 元画像の面積
// Grayscale
let im_gray = new cv.Mat();
cv.cvtColor(im, im_gray, cv.COLOR_RGBA2GRAY);
// Threshold
let threshold_im = new cv.Mat();
cv.threshold(im_gray, threshold_im, this.THRESHOLD, 255, cv.THRESH_BINARY);
// Contours
let contours = new cv.MatVector();
let hierarchy = new cv.Mat();
cv.findContours(threshold_im, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);
hierarchy.delete();
let pts = null;
let maxCntArea = 0
for(let i = 0; i < contours.size(); ++i) {
let cnt = contours.get(i);
const cntArea = cv.contourArea(cnt)
const maxRectScale = parseInt(cntArea / imRectArea * 100) // 元画像と比べてどれぐらいの大きさか(%)
if(maxRectScale >= this.MIN_CONTOURS_SCALE) { // 元画像との比率でフィルター
if(cntArea > maxCntArea) { // より大きいものをキープ
let approx = new cv.Mat();
const epsilon = 0.02 * cv.arcLength(cnt, true)
cv.approxPolyDP(cnt, approx, epsilon, true)
if(approx.size().height === 4) { // 四角形ならキープ
maxCntArea = cntArea;
pts = approx // 切り抜くべき四角形の座標(4点)
}
}
}
}
contours.delete();
im_gray.delete();
threshold_im.delete();
pts.convertTo(pts, cv.CV_32FC2);
return pts;
},
getTransformedImage(im, fromPts) {
let transformedIm = new cv.Mat();
const rows = im.rows;
const cols = im.cols;
let dsize = new cv.Size(cols, rows);
const toPts = cv.matFromArray(4, 1, cv.CV_32FC2, [
cols, 0, 0, 0, 0, rows, cols, rows
]);
const M = cv.getPerspectiveTransform(fromPts, toPts); // 変形の行列
cv.warpPerspective(im, transformedIm, M, dsize);
fromPts.delete();
toPts.delete();
return transformedIm;
}
},
mounted() {
cv['onRuntimeInitialized'] = () => {
this.initialized = true;
};
}
}).mount('#app');
</script>
</body>
</html>
この中で文書の歪みを修正している手順は次のとおりです。
- 画像をモノクロに変換
- しきい値を使ってより文書の部分を明確にする
- 全ての輪郭を取得し「形が四角形で最大のもの(=つまり文書)」を取得する
- 取得した位置情報から「遠近法ワープ」で画像を変形
- 完成!
つまり画像でいうとこうなります。
1.元画像
2.モノクロへ変換
3.しきい値を適用
4.輪郭から位置を取得
5.遠近法ワープで元画像の歪みを修正
なお、今回は「A4縦サイズ」を想定していますがパラメータを使って横向きや他のサイズへ対応することもできるでしょう。
デモページを用意しました
今回のコードを少しだけ改変したものをデモページとして公開することにしました。ぜひOpenCV.js
を試してみてください。
デモページ: ななめに写った文書をピッタリにするサンプル
ちなみに:ServiceWokerによる高速化
デモページを見ていただくとお気づきかもしれませんが、OpenCV.js
本体のファイルサイズが大きいので、少し待たないといけません。
次回からはブラウザのキャッシュが効いてくれるのでそれほど気にする必要はありませんが、キャッシュは完全ではありませんし、ユーザービリティとしてあまりいいとは言えませんよね。
そこで、今回はせっかくですので「Service Worker」を使って事前に/js/opencv.js
をキャッシュするようにしてみます。
例えば、流れとしては以下のようになります。
- ユーザーがログインする
- ログインした時点で /js/opencv.js をキャッシュする(バックグランドで独立して動きます)
- /js/opencv.js が必要なページへ移動
- すでにファイルはキャッシュされているので高速でページ表示できる!
つまり、キャッシュ・ファイルをローカルに保存するので高速化できるというわけですね。(HTMLなどもキャッシュできるので、オフラインでも操作できるようになります)
では、実際のコードです。
まず、Service Worker
本体です。
/service-worker.js
const CACHE_NAME = 'opencv-cache-v1';
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll([
'/js/opencv.js'
]);
})
);
});
self.addEventListener('fetch', function(e) {
e.respondWith(
caches.match(e.request).then(response => {
if(response) {
return response;
}
return fetch(e.request);
})
);
});
※ なお、Service Worker
はファイルが存在するフォルダ以下で有効になります。そのため、/js/service-worker.js
に設置としてしまうと、サイト全体で有効にならなくなるため、今回はルートフォルダに設置しています。
そして、このService Worker
を登録するコードです。
これは事前に/js/service-worker.js
をキャッシュしたいページと、実際に/js/service-worker.js
を使うページで実行するといいでしょう。
<html>
<body>
Service Worker を準備中
<script>
window.onload = e => {
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(
registration => {
console.log('ServiceWorker registration successful', registration.scope);
},
err => {
console.log('ServiceWorker registration failed: ', err);
}
);
}
}
</script>
</body>
</html>
これでopencv.js
がService Worker
経由で読み込まれるので、次回からは高速でページ表示ができます
おまけ:Python版
もともと今回の記事は、「AWS LambdaでPythonを実行する」というのがテーマだったんですが、Lambda
でOpenCV
を使うには自分でOpenCV
を同梱しないといけないということが分かり「うーん…」となって、方向転換をしました。
(当然と言えば当然ですが、有名パッケージだから大丈夫だろうなんて考えでした…)
しかし、せっかくPython
版もつくったのにもったいないなということで「おまけ」として公開することにしました。(ボツコード供養 )
準備する
今回はPython
コードの中以下のパッケージを使います。
もしインストールされていない場合は先にインストールしておいてください。
- opencv-python 4.5
- numpy 1.19
インストールはこちら
pip3 install opencv-python
そして、
pip3 install numpy
※おそらく上のコマンドで依存パッケージとしてインストールされると思いますが念のため。
歪みを修正する部分をつくる(Python)
では、以下のようなコマンドで画像から文書の位置を取得し、歪みを修正するコードを書いていきます。
python3 transform_document.py (元画像のパス) (修正した画像のパス)
実際のコードはこちら
#!/usr/bin/env python3
# command example: python3 transform_document.py ./images/original/document.jpg ./images/transformed/document.jpg
import cv2
import numpy as np
import sys
MIN_CONTOURS_SCALE = 20 # 最低元画像比率
THRESHOLD = 170 # モノクロのしきい値
if len(sys.argv) - 1 != 2:
print('You need to TWO arguments at least!')
sys.exit()
INPUT_IMAGE_PATH = sys.argv[1]
OUTPUT_IMAGE_PATH = sys.argv[2]
def get_contours_points(im):
rows, cols = im.shape[:2] # 元画像の縦、横を取得
im_rect_area = cols * rows # 元画像の面積
im_gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(im_gray, THRESHOLD, 255, 0)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
pts = None
max_cnt_area = 0
for cnt in contours:
cnt_area = cv2.contourArea(cnt)
max_rect_scale = int(cnt_area / im_rect_area * 100) # 元画像と比べてどれぐらいの大きさか(%)
if max_rect_scale < MIN_CONTOURS_SCALE: # 元画像との比率でフィルター continue if cnt_area > max_cnt_area: # より大きいものをキープ
epsilon = 0.02 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
if len(approx) == 4: # 四角形ならキープ
max_cnt_area = cnt_area
pts = approx # 切り抜くべき四角形の座標(4点)
return pts
def get_transformed_image(im, from_pts):
rows, cols = im.shape[:2]
to_pts = np.float32([[cols,0], [0,0], [0,rows], [cols,rows]])
M = cv2.getPerspectiveTransform(from_pts, to_pts) # 変形の行列
return cv2.warpPerspective(im, M, (cols, rows))
im = cv2.imread(INPUT_IMAGE_PATH)
pts = get_contours_points(im)
if pts is not None:
transformed_im = get_transformed_image(im, np.float32(pts))
cv2.imwrite(OUTPUT_IMAGE_PATH, transformed_im)
print('Done!')
else:
print('Failed...')
企業様へのご提案
今回の技術を使えば、サーバーやAWSなどクラウド環境がなくても写真の歪みを修正できます。(つまりコストを減らすことができます)
また、文書の撮影にスキャナーを購入する必要はなく、手持ちのスマートフォンで問題ありません。
※ なお、Google Cloud Vision API
などを使えば日本語の読み取り機能も追加することができます。
ぜひ、興味がございましたらご連絡ください。
おわりに
ということで、今回はブラウザでOpenCV.js
を使って遠近法ワープを実装してみました。
なお、「おまけ」でも書きましたがLambda
でPython
パッケージを使う場合は同梱してアップしないといけないというのは想定外でした
・・・ただ、そうなると「Lambda
ってどんな使い方してるんだろう」という疑問も浮かんできました。
Python
を使うメリットの一つに「パッケージが豊富」というのが私の中にはあって、それがデフォルトで使えないとなると・・・というカンジです。(もしかすると「パッケージを同梱するなんて当然でしょ!」みたいなカンジなのかもしれません)
とにかく、そんなことがあったので今回は逆に「クラウドなんて不要です」をテーマに記事を書いてみました。
ぜひ皆さんも試してみてくださいね。
ではでは〜
「A5ランクの肉、
うまかったな〜」