はじめに
スマホで撮った書類やレシートの写真は、たいてい台形に歪んでいたり、背景の机や床が写り込んでいたりします。それをまっすぐな1枚の紙として扱えるように加工しようとすると、輪郭検出・射影変換といった画像処理の知識が必要になり、自前で実装するにはハードルが高い処理です。
Jscanifyは、この「写真の中から紙を見つけて、まっすぐに補正する」という処理を、ブラウザ上のJavaScriptだけで完結させてくれるドキュメントスキャナライブラリです。内部ではOpenCV.jsを使って輪郭検出と射影変換を行っており、サーバーに画像をアップロードすることなく、クライアントサイドだけでスキャンアプリのような機能を実現できます。
Jscanifyがどんな見た目で動くのか、まずは触ってみるのが早いと思います。書類の写真をアップロードすると、その場で輪郭がハイライト表示されるサンプルを用意したので、先に挙動を見たい方はこちらからどうぞ。
Jscanifyとは
Jscanify(GitHub: puffinsoft/jscanify、旧ColonelParrot/jscanify)は、「The Javascript document scanning library」を掲げるオープンソースのドキュメントスキャナライブラリです。MITライセンスで公開されており、ブラウザとNode.jsの両方で動作します。内部の画像処理はOpenCV.jsに依存しており、Cannyエッジ検出や輪郭抽出、射影変換(warpPerspective)といったOpenCVの機能を、扱いやすいAPIにまとめて提供しています。
主な特徴
- 用紙の検出とハイライト表示 - 写真の中から四角い紙を見つけ出し、その輪郭を線で強調表示できます
- 歪み補正付きの抽出 - 検出した紙を、指定した幅・高さの正面向きの画像として切り出せます
- グレア抑制と多色紙対応 - v1.3.0以降では、照明の反射(グレア)が強い写真や、白以外の色の紙にも対応できるよう検出精度が改善されています
- 低レベルAPIも公開 - 輪郭検出(
findPaperContour)と四隅の座標計算(getCornerPoints)が個別のメソッドとして公開されているため、ハイライト表示や抽出以外の独自処理にも組み込めます - ブラウザ・Node.js両対応 - フロントエンドではCDN経由のOpenCV.jsと組み合わせて使い、Node.jsでは
canvasとjsdomを使ったサーバーサイド処理も可能です
インストール
npm経由でインストールできます。
npm install jscanify
注意点として、jscanifyパッケージのデフォルトエントリーポイント(import jscanify from "jscanify")はNode.js向けのビルドで、内部でcanvasとjsdomを利用します。Webpack・ViteなどでブラウザバンドルにJscanifyを組み込む場合は、ブラウザ向けのサブパスを明示的にimportしてください。
import jscanify from "jscanify/client";
このブラウザ向けビルドは、グローバルのcv(OpenCV.js)を前提に動作します。そのため、あらかじめOpenCV.jsを読み込んでおく必要があります。もっとも手軽なのはCDN経由で両方をscriptタグで読み込む方法で、この記事のサンプルもすべてこの構成で動いています。
<script src="https://docs.opencv.org/4.7.0/opencv.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jscanify@1.4.3/src/jscanify.js"></script>
Jscanifyのサンプルを動かす
highlightPaper()は、写真の中から紙を検出し、その輪郭を線で強調表示したHTMLCanvasElementを返すメソッドです。下のサンプルでは、書類やレシートを撮った写真をアップロードすると、Jscanifyが検出した紙の輪郭がその場でハイライト表示されます。枠線の色と太さもhighlightPaper()のoptions引数として渡しているので、選択に応じて表示が変わります。
const scanner = new jscanify();
fileInput.addEventListener("change", () => {
const img = new Image();
img.onload = () => {
const canvas = scanner.highlightPaper(img, {
color: colorSelect.value,
thickness: Number(thicknessInput.value),
});
resultEl.replaceChildren(canvas);
};
img.src = URL.createObjectURL(fileInput.files[0]);
});
実際に動かせるサンプルが下です。背景と紙のコントラストがはっきりした写真(白い机の上に置いた書類など)ほど、きれいに検出されます。逆に紙と背景の色が近い写真を選ぶと、輪郭がずれたり検出できなかったりする様子も確認できます。
highlightPaper()はあくまで「どこを紙として認識したか」を確認するためのメソッドで、元画像に線を重ねて描画するだけです。実際に紙だけを切り出したい場合は、次の章で紹介するextractPaper()を使います。
基本的な使い方
Jscanifyの基本的な流れは、jscanifyのインスタンスを作り、画像(<img>や<canvas>)を渡してメソッドを呼び出すだけです。
// OpenCV.jsとjscanify.jsをscriptタグで読み込み済みの前提
const scanner = new jscanify();
const image = document.getElementById("myImage");
image.onload = function () {
// 紙の輪郭をハイライト表示
const highlighted = scanner.highlightPaper(image);
document.body.appendChild(highlighted);
// 紙を500x1000pxの正面向き画像として抽出
const extracted = scanner.extractPaper(image, 500, 1000);
document.body.appendChild(extracted);
};
どちらのメソッドも、戻り値は描画済みのHTMLCanvasElementなので、appendChild()でそのままページに追加できます。ただし、OpenCV.jsはWebAssemblyの初期化に時間がかかるため、実際にはcv["onRuntimeInitialized"]のコールバックが発火してからnew jscanify()を呼び出す必要があります(サンプルのコードを参照してください)。
実践的なユースケース
歪んだ写真をまっすぐな紙として抽出する
書類をスキャンアプリのように保存したい場合は、extractPaper(image, resultWidth, resultHeight)を使います。検出した紙の四隅を使って射影変換(warpPerspective)を行い、指定したサイズの正面向き画像として切り出してくれるメソッドです。紙が検出できなかった場合はnullが返るため、その分岐も必要です。
const scanner = new jscanify();
let sourceImage = null;
function extract() {
if (!sourceImage) return;
const canvas = scanner.extractPaper(
sourceImage,
Number(widthInput.value),
Number(heightInput.value)
);
if (!canvas) {
messageEl.textContent = "紙の輪郭を検出できませんでした。";
return;
}
resultEl.replaceChildren(canvas);
}
下のサンプルでは、出力の幅と高さをスライダーで変えながらextractPaper()の結果を確認できます。同じ写真でも、幅と高さの比率を変えると紙の縦横比が変わって見えるので、実際のA4用紙やレシートの比率に近づける感覚がつかめます。
検出した四隅の座標を独自処理に使う
highlightPaper()やextractPaper()は便利な反面、内部の処理がブラックボックスになりがちです。もっと細かく制御したい場合は、輪郭を返すfindPaperContour(img)と、その輪郭から四隅の座標を計算するgetCornerPoints(contour)を直接使うと、検出結果を自分の描画処理やデータ加工に組み込めます。
const mat = cv.imread(img);
const contour = scanner.findPaperContour(mat);
if (contour) {
const corners = scanner.getCornerPoints(contour);
// { topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner }
console.log(corners.topLeftCorner.x, corners.topLeftCorner.y);
}
mat.delete();
下のサンプルでは、findPaperContour()とgetCornerPoints()で計算した4つの座標に赤い丸を描画し、座標の値そのものもJSONとして表示しています。写真を変えるたびに、topLeftCornerやbottomRightCornerなどのキー名と数値がどう変化するかを確認できます。
まとめ
Jscanifyは、OpenCV.jsが持つ輪郭検出や射影変換の機能を、highlightPaper()・extractPaper()という2つの高レベルAPIにまとめることで、複雑な画像処理を意識せずに「写真の中の紙をまっすぐ補正する」機能を実装できるライブラリです。さらにfindPaperContour()とgetCornerPoints()という低レベルAPIも公開されているため、既製のメソッドでは物足りない場面でも、検出結果を自分の処理に組み込む余地が残されています。
サーバーに画像を送らずクライアントサイドだけで完結する点は、プライバシーの観点でも、通信コストの観点でも大きなメリットです。名刺管理アプリやレシート整理ツール、フォームへの書類添付機能など、「ブラウザで完結する簡易スキャナ」が欲しい場面では、まず試してみる価値があるライブラリだと思います。
