OSSのノーコード・ローコード開発ツール「プリザンター」 Advent Calendar 2024への参加記事です。
プリザンターにて、QRコードを読み込むスクリプトをつくってみました!
OpenCVをJavaScriptで使えるOpenCV.jsを使用します。
スクリプトを使う準備
環境
- windows11 IIS
- SQLServer
- プリザンター ver1.4.9.2
使用ライブラリ
- OpenCV.js
-
opencv-4.10.0-docs.zip
にあるopencv.js
を使用 - もしくはhttps://docs.opencv.org/4.10.0/opencv.js を使う(バージョン番号を変える)
- 他にはjsQRというのも有名なようです
jsファイルの読み込み
プリザンターの管理画面から追加。
body script bottom
として以下を入れる。
main.js
:処理する内容
opencv.js
:上記でダウンロードしたもの
「新規作成」・「編集」の画面で使いたいのでチェック
<script src="/scripts/extentions/opencv/opencv.js"></script>
<script src="/scripts/extentions/opencv/main.js"></script>
カメラ表示用htmlの追加
プリザンターの拡張htmlとかで入れておいたらいいと思いますが、今回はhtmlファイルをmain.jsから読み込むようにしています。
<!DOCTYPE html>
<html>
<button id="videoButton">onoff</button>
<video id="videoInput"
autoplay="true"
style="display: none"></video>
<canvas id="canvasOutput"
style="display: none"></canvas>
</html>
jsの追加
main.jsの全体
let video;
let canvas;
let targetElement = document.getElementById("Issues_IssueIdField");
fetch("/scripts/extentions/opencv/camera.html")
.then((response) => response.text())
.then((data) => {
targetElement.insertAdjacentHTML("beforebegin", data);
document.getElementById("videoButton").addEventListener("click", videoLoad);
video = document.getElementById("videoInput");
video.addEventListener("loadeddata", qrReader);
canvas = document.getElementById("canvasOutput");
canvas.onmouseover = () => (canvas.style.cursor = "pointer");
canvas.onmouseout = () => (canvas.style.cursor = "default");
canvas.addEventListener("click", qrClick);
document.addEventListener("visibilitychange", visibilityChange);
});
function videoLoad() {
// onになってたら停止
if (video.srcObject !== null) {
video.srcObject.getTracks().forEach((track) => track.stop());
video.srcObject = null;
video.style.display = "none";
canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
canvas.style.display = "none";
canvas.textContent = "";
console.log("videoを停止");
return;
}
// カメラを起動
const constraints = {
video: { width: 320, height: 240, facingMode: "environment" },
audio: false,
};
navigator.mediaDevices
.getUserMedia(constraints)
.then((stream) => {
video.srcObject = stream;
video.style.display = "inline";
canvas.style.display = "inline";
console.log("videoを開始");
})
.catch(function (err) {
console.log(err.name + ": " + err.message);
alert("カメラを許可しないと使えません");
});
}
let frame;
let gray;
let points;
let straight_qrcode;
// videoのオンオフで呼び出される
function qrReader() {
if (video.srcObject == null) {
frame.delete();
gray.delete();
points.delete();
straight_qrcode.delete();
return;
}
let FPS = 30;
let begin = Date.now();
let cap = new cv.VideoCapture(video);
video.height = video.videoHeight;
video.width = video.videoWidth;
frame = new cv.Mat(video.videoHeight, video.videoWidth, cv.CV_8UC4);
cap.read(frame);
// グレー
gray = new cv.Mat();
cv.cvtColor(frame, gray, cv.COLOR_RGBA2GRAY);
// // QRコードのオブジェクト
// let qrDecoder = new cv.QRCodeDetector();
let qrDecoder = new cv.QRCodeDetectorAruco();
// バーコードの検出
// let qrDecoder = new cv.barcode_BarcodeDetector();
points = new cv.Mat();
straight_qrcode = new cv.Mat();
// QRの検出とデコード
let qrCodeString = qrDecoder.detectAndDecode(gray, points, straight_qrcode);
if (qrCodeString) {
console.log(qrCodeString);
// qrコードに枠線を引く
let n = points.total();
for (let i = 0; i < n; i++) {
let start = new cv.Point(
points.data32F[i * 2],
points.data32F[i * 2 + 1]
);
let end = new cv.Point(
points.data32F[(i * 2 + 2) % (n * 2)],
points.data32F[(i * 2 + 3) % (n * 2)]
);
cv.line(frame, start, end, [0, 255, 0, 255], 3);
console.log(`線を描画: (${start.x}, ${start.y}) -> (${end.x}, ${end.y})`);
}
canvas.textContent = qrCodeString;
} else {
// console.log("qrなし");
}
// cv.line(frame, { x: 10, y: 20 }, { x: 100, y: 300 }, [255, 0, 255, 255], 3);
// 画像内にQR文字列を入れる
// canvas.textContent = "abcdefg";
// canvas.textContent = "https://qiita.com/advent-calendar/2024/pleasanter";
cv.putText(
frame,
canvas.textContent,
{ x: 10, y: 50 },
cv.FONT_HERSHEY_SIMPLEX,
0.5,
[255, 0, 255, 255],
2
);
// canvasへの表示
cv.imshow("canvasOutput", frame);
let delay = 1000 / FPS - (Date.now() - begin);
setTimeout(qrReader, delay);
}
// qr読み込んだ画像をクリックした時の処理
// ページを開くかタイトルへ反映させて、カメラをオフ
function qrClick(e) {
let qrText = e.target.textContent;
console.log("qr読み込み:" + qrText);
if (qrText.startsWith("https")) {
window.open(qrText, "_blank");
} else {
document.getElementById("Issues_Title").value = qrText;
videoLoad();
}
}
// ページを離れた時にカメラをオフにする
function visibilityChange() {
if (document.visibilityState === "hidden" && video.srcObject !== null) {
videoLoad();
}
}
読み込んだ時の処理
イベント追加など。
htmlファイルからカメラ表示を追加。
let video;
let canvas;
let targetElement = document.getElementById("Issues_IssueIdField");
fetch("/scripts/extentions/opencv/camera.html")
.then((response) => response.text())
.then((data) => {
targetElement.insertAdjacentHTML("beforebegin", data);
document.getElementById("videoButton").addEventListener("click", videoLoad);
video = document.getElementById("videoInput");
video.addEventListener("loadeddata", qrReader);
canvas = document.getElementById("canvasOutput");
canvas.onmouseover = () => (canvas.style.cursor = "pointer");
canvas.onmouseout = () => (canvas.style.cursor = "default");
canvas.addEventListener("click", qrClick);
document.addEventListener("visibilitychange", visibilityChange);
});
ビデオを開始・終了する処理
ビデオ使用方法のチュートリアル
navigator.mediaDevices.getUserMedia
はhttpsかlocalhostでないと使えないので注意。
スマホからも試したかったので、自己証明書でhttpsにしました。
function videoLoad() {
// onになってたら停止
if (video.srcObject !== null) {
video.srcObject.getTracks().forEach((track) => track.stop());
video.srcObject = null;
video.style.display = "none";
canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
canvas.style.display = "none";
canvas.textContent = "";
console.log("videoを停止");
return;
}
// カメラを起動
const constraints = {
video: { width: 320, height: 240, facingMode: "environment" },
audio: false,
};
navigator.mediaDevices
.getUserMedia(constraints)
.then((stream) => {
video.srcObject = stream;
video.style.display = "inline";
canvas.style.display = "inline";
console.log("videoを開始");
})
.catch(function (err) {
console.log(err.name + ": " + err.message);
alert("カメラを許可しないと使えません");
});
}
QRコード取得の処理
QRコードがあったら枠を囲って、その文字列を画面に表示します。
文字は一度読んだらフォーカス外れても消えないようにしています。
let frame;
let gray;
let points;
let straight_qrcode;
// videoのオンオフで呼び出される
function qrReader() {
if (video.srcObject == null) {
frame.delete();
gray.delete();
points.delete();
straight_qrcode.delete();
return;
}
let FPS = 30;
let begin = Date.now();
let cap = new cv.VideoCapture(video);
video.height = video.videoHeight;
video.width = video.videoWidth;
frame = new cv.Mat(video.videoHeight, video.videoWidth, cv.CV_8UC4);
cap.read(frame);
// グレー
gray = new cv.Mat();
cv.cvtColor(frame, gray, cv.COLOR_RGBA2GRAY);
// // QRコードのオブジェクト
// let qrDecoder = new cv.QRCodeDetector();
let qrDecoder = new cv.QRCodeDetectorAruco();
// バーコードの検出
// let qrDecoder = new cv.barcode_BarcodeDetector();
points = new cv.Mat();
straight_qrcode = new cv.Mat();
// QRの検出とデコード
let qrCodeString = qrDecoder.detectAndDecode(gray, points, straight_qrcode);
if (qrCodeString) {
console.log(qrCodeString);
// qrコードに枠線を引く
let n = points.total();
for (let i = 0; i < n; i++) {
let start = new cv.Point(
points.data32F[i * 2],
points.data32F[i * 2 + 1]
);
let end = new cv.Point(
points.data32F[(i * 2 + 2) % (n * 2)],
points.data32F[(i * 2 + 3) % (n * 2)]
);
cv.line(frame, start, end, [0, 255, 0, 255], 3);
console.log(`線を描画: (${start.x}, ${start.y}) -> (${end.x}, ${end.y})`);
}
canvas.textContent = qrCodeString;
} else {
// console.log("qrなし");
}
// cv.line(frame, { x: 10, y: 20 }, { x: 100, y: 300 }, [255, 0, 255, 255], 3);
// 画像内にQR文字列を入れる
// canvas.textContent = "abcdefg";
// canvas.textContent = "https://qiita.com/advent-calendar/2024/pleasanter";
cv.putText(
frame,
canvas.textContent,
{ x: 10, y: 50 },
cv.FONT_HERSHEY_SIMPLEX,
0.5,
[255, 0, 255, 255],
2
);
// canvasへの表示
cv.imshow("canvasOutput", frame);
let delay = 1000 / FPS - (Date.now() - begin);
setTimeout(qrReader, delay);
}
canvasをクリックした時の処理
読み込んだQRの内容をタイトルへ反映させます。
もしくはページを開く。
// qr読み込んだ画像をクリックした時の処理
// ページを開くかタイトルへ反映させて、カメラをオフ
function qrClick(e) {
let qrText = e.target.textContent;
console.log("qr読み込み:" + qrText);
if (qrText.startsWith("https")) {
window.open(qrText, "_blank");
} else {
document.getElementById("Issues_Title").value = qrText;
videoLoad();
}
}
ページを離れた時の処理
ページを離れたらカメラをオフにします。
function visibilityChange() {
if (document.visibilityState === "hidden" && video.srcObject !== null) {
videoLoad();
}
}
完成!
QRコードを読み取れるようになりました!
左がカメラそのままで、右がopencv.jsで加工されたもの。
これはページ飛んでますけど、プリザンターへのデータ反映とか色々組み合わせられますね!
その他
QRCodeDetectorAruco
cv.QRCodeDetector()
よりもcv.QRCodeDetectorAruco()
の方が、速くて検出精度もいいようです。
これを変えただけでも[Violation]'setTimeout' handler took xxms
が出なくなるくらいのはやさになりました。
こちらの方が(Python・OpenCV)で検証されていました。
バーコードの読み取り
let qrDecoder = new cv.QRCodeDetectorAruco();
の所を、new cv.barcode_BarcodeDetector();
にすればバーコードでも読み取れるようです。
detectAndDecode
の内容は同じように使えます。
httpsにする
自己証明書でのhttpsとしました。
こちらやこちらを参考にpowershellで作成。
これでスマホからでもカメラ使えますが、ブラウザの上の警告表示はでます。
New-SelfSignedCertificate -Subject localip -TextExtension @("2.5.29.17={text}DNS=hostnameee&IPAddress=192.168.xxx.xxx") -NotAfter (Get-Date).AddYears(10) -CertStoreLocation cert:\LocalMachine\My
videoとcanvasのサイズ
videoとcanvasのサイズを合わせないと、pcで出来ていてもスマホでサイズ/比率がおかしくなったりします。
video: { width: 320, height: 240}
で指定してサイズを合わせておきます。
video.height = video.videoHeight;
video.width = video.videoWidth;
let frame = new cv.Mat(video.videoHeight, video.videoWidth, cv.CV_8UC4);
ブラウザとかスマホ機種によったらサイズおかしくなるかもしれない。
こちらも参考。