PyScriptでOpenCVを使ってWebで画像処理AI

# PyScriptとは
PyScriptとは、Webブラウザ上でPythonコードを実行するためのフレームワークです。
Anaconda社が中心となって開発を進めており、Pythonをフロントエンドの開発に統合することを目的としているようです。

Hello Worldのサンプルは以下です。
PyScript出始めの時に触った方からしたら、ちょっと違和感あるかもしれません（ちょいちょい開発は行われていて、たまに破壊的な変更もあったりなかったり。。。）
※破壊的変更が怖いので、個人的にはjsとcss読み込む際は、`latest`指定ではなく`releases/2024.11.1`のようにバージョン固定で読み込むことをおすすめします
```html
<!DOCTYPE html>
<html>
    <head>
        <title>Hello World</title>
        <link rel="stylesheet" href="https://pyscript.net/releases/2024.11.1/core.css">
        <script type="module" src="https://pyscript.net/releases/2024.11.1/core.js"></script>
    </head>
    <body>
        <script type="py">
            from pyscript import display
            from datetime import datetime
            now = datetime.now()
            display("Hello World: " + now.strftime("%Y/%m/%d %H:%M:%S"))
        </script>
    </body>
```
GitHub Pagesにも公開しているので、動作している様子を確認いただけます↓

https://kazuhito00.github.io/Qiita-AdventCalendar-20241217-OpenCV/01-01.hello_world(PyScript)/

JavaScriptの代わりに、Pythonで直接DOM操作やWeb APIの操作が可能だったりするので、気になる方は公式ドキュメントを眺めてみると良いかもしれません。
公式ドキュメント：https://docs.pyscript.net

また、一部ではありますが、opencv-python、numpy、pandasなどのPythonのパッケージも使用することができます。
PyScriptは内部的にはPyodide(CPythonをWebAssembly/Emscriptenに移植したもの)を使用していて、Pyodideがサポートしているパッケージを使用することができます。
Pyodideサポートパッケージ：https://pyodide.org/en/stable/usage/packages-in-pyodide.html

ちなみにPyodideで上記のサンプルと同じようなことをしようとすると、以下のようなコードになります。
当たり前と言えば当たり前ですが、PyScriptのほうが簡易で使いやすいです。
```html
<!doctype html>
<html>
    <head>
        <title>Hello World</title>
        <script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
    </head>
    <body>
        <div id="text"></div>

        <script type="text/javascript">
            async function main() {
                let pyodide = await loadPyodide();
                const code = `
                    from js import document 
                    from datetime import datetime
                    now = datetime.now()
                    document.getElementById('text').innerHTML = "Hello World: " + now.strftime("%Y/%m/%d %H:%M:%S")
                `;
                pyodide.runPython(code);
            };

            main();
        </script>
    </body>
</html>
```

こちらも、GitHub Pagesに公開しています↓

https://kazuhito00.github.io/Qiita-AdventCalendar-20241217-OpenCV/01-02.hello_world(Pyodide)/

# さあ、画像処理AIやろうぜと思ったけど、、、
さあ、画像処理AIやろうぜと思って、[Pyodideサポートパッケージ](https://pyodide.org/en/stable/usage/packages-in-pyodide.html) を眺める👀

opencv-python、numpy、pandas、scikit-learnにscikit-imageはあるけど、、、
さすがに、TensorFlow、PyTorch、onnxruntimeは無いんですよねー。。。
いや、こいつらのビルドの面倒くささ考えると、opencv-pythonあるだけでも、ありがたいのですが、、、🙄

そもそも、パッケージ名とバージョンだけが羅列してあって、何のパッケージか分かりにくいんだよなー、、、
元リンクくらい貼ってくれよ。と思いますが。。。😇
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140207/7ce351de-6d3a-2e13-ed64-8388f4c88696.png)

TensorFlow、PyTorch、onnxruntime が無いので、軽く諦めかけてかけていましたが、、、
我々には、OpenCV の <b>dnnモジュール</b>と言う強い味方がいます🦔

# と言うわけで、YOLOX(Tiny)動かそう

![01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140207/63884aa3-4e48-c5b5-135f-660cde557ad4.gif)

上記のデモは、GitHub Pagesに公開しています↓

https://kazuhito00.github.io/Qiita-AdventCalendar-20241217-OpenCV/02.yolox/

ソースコードに興味ある方は、以下の中身を眺めてみてください👀
推論はOpenCVのdnnモジュールを利用しています。

https://github.com/Kazuhito00/Qiita-AdventCalendar-20241217-OpenCV/tree/main/02.yolox

# せっかくなので、PINTOさんの[YOLOv9-Wholebody25](https://github.com/PINTO0309/PINTO_model_zoo/tree/main/459_YOLOv9-Wholebody25)も動かしてみよう

![02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140207/fb95a92a-6e6e-c127-841b-14586d1d3126.gif)

上記のデモは、GitHub Pagesに公開しています↓

https://kazuhito00.github.io/Qiita-AdventCalendar-20241217-OpenCV/03.yolov9wholebody25/

ソースコードに興味ある方は、以下の中身を眺めてみてください👀
こちらも、推論はOpenCVのdnnモジュールを利用しています。
もともとPINTO_model_zooではdnnモジュールは利用していないので、NMSのポストプロセスが統合されていないONNXを使う処理をザーッと書きました（バグってたらコソっと教えてください。。。）

https://github.com/Kazuhito00/Qiita-AdventCalendar-20241217-OpenCV/tree/main/02.yolox

# 課題
上のデモを見て「YOLOX-Tinyを使ってるわりに何か遅くねえ？」と感じた方もいると思いますが、、、
その感覚は正しいですが、推論自体はそんなに遅くないです。
ただ、キャンバスへの反映が500ms前後かかっており、そこで遅くなっています。

解決できなくて放置していたのですが、、、
いったんアドベントカレンダーに公開します（誰か知見持っているかもですし）

main.pyを眺めていただくと、以下のような処理があります。
コメントアウトされていない処理が、上記のデモで動作していた処理です。
コメントアウトされている方の処理だとキャンバスへの反映は速いのですが、じゃんじゃんメモリリークしていきます。
ちょっと調べていたのですが、メモリリークが解決できず妥協した感じです。
何かご存じの方がいればコメントいただけると助かります。
```python:main.py
        #### ToDo：メモリリーク箇所
        # # メモリリークするけど早い処理
        # # NumPy 配列のバッファを使用して Uint8ClampedArray を作成
        # buffer = image.flatten().tobytes()
        # bytes_proxy = create_proxy(buffer)
        # try:
        #     # 新しい ImageData オブジェクトを作成
        #     bytes_buffer = bytes_proxy.getBuffer("u8clamped").data
        #     new_image_data = js.ImageData.new(bytes_buffer, canvas.width, canvas.height)
        #     context.putImageData(new_image_data, 0, 0)
        # finally:
        #     # キャンバスに画像データを描画
        #     bytes_proxy.destroy()
        #     del bytes_proxy, bytes_buffer, new_image_data

        # メモリリークしないけど遅い処理
        height, width, _ = image.shape
        buffer = image.flatten().tobytes()
        js_image = ImageData.new(Uint8ClampedArray.new(buffer), width, height)
        context.putImageData(js_image, 0, 0)
        ####
```

以上。