25
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OSSのノーコード・ローコード開発ツール「プリザンター」 Advent Calendar 2024への参加記事です。

プリザンターにて、QRコードを読み込むスクリプトをつくってみました!
OpenCVをJavaScriptで使えるOpenCV.jsを使用します。

スクリプトを使う準備

環境

  • windows11 IIS
  • SQLServer
  • プリザンター ver1.4.9.2

使用ライブラリ

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から読み込むようにしています。

camera.html
<!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の全体
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ファイルからカメラ表示を追加。

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);
  });

ビデオを開始・終了する処理

ビデオ使用方法のチュートリアル
navigator.mediaDevices.getUserMediaはhttpsかlocalhostでないと使えないので注意。
スマホからも試したかったので、自己証明書でhttpsにしました。

main.js
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コードがあったら枠を囲って、その文字列を画面に表示します。
文字は一度読んだらフォーカス外れても消えないようにしています。

main.js
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の内容をタイトルへ反映させます。
もしくはページを開く。

main.js
// 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();
  }
}

ページを離れた時の処理

ページを離れたらカメラをオフにします。

main.js
function visibilityChange() {
  if (document.visibilityState === "hidden" && video.srcObject !== null) {
    videoLoad();
  }
}

完成!

QRコードを読み取れるようになりました!
左がカメラそのままで、右がopencv.jsで加工されたもの。
これはページ飛んでますけど、プリザンターへのデータ反映とか色々組み合わせられますね!

qrのgif3.gif

その他

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);

ブラウザとかスマホ機種によったらサイズおかしくなるかもしれない。
こちらも参考。

参考

25
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?