1カメラ1GPUの時代は来るのか?クラウド上にライブストリーミングのAIフィルタを作ってみた。
こんにちは、テリーです。2023年後半、動画生成AIと関連技術・サービスが各社から新規リリースもしくは大幅改善されて出てきています。他にもたくさんありますが例えば下記があります。
- Stability AI: Stable Video Diffusion
- Meta: Emu Video
- Runway: Gen-2
- Pika labs: Pika
自社サイトのサービスとして利用されており、オープンでない技術がほとんどのようです。オープンソースのものは公開されるたびに動かしているのですが、最新の最高級デスクトップパソコンとGPU以外ではメモリ不足で動かないものが多く、気軽に試したいのに困ったものです。すぐに最新機種が出て世代交代するパソコンに60万円も出せないので、クラウドサービスに時間課金して使用しています。GPU付きのサーバは、GPUなしのVPSに比べるとかなり高いです。1時間100円〜は覚悟が必要です。それでも60万円のパソコンを100で割って、「6000時間使うまではお得、毎日8時間使っても730日(2年)」と割り切って、課金しています。値段が落ち着くまでは納得するしかありません。
今回は、クラウド上にGPUつきサーバを手配して、AI画像処理サーバを構築し、ライブ配信のAIフィルタとして使用するケースを想定したサンプルを紹介します。一部に有料サービスを含みます。
目次
対象読者
リアルタイムのAI画像処理をクラウドで実現したい人
リアルタイムのAI画像フィルタを使ってライブ配信したい人
動作確認環境
- クライアントPC
- macOS 13.5.2
- chrome 120.0.6099.71
- サーバ
- Ubuntu 22.04 LTS
- aiortc 1.6.0
- aiohttp 3.9.1
- nvidia-driver 545.29.06
AIフィルタとは?
LINEやTikTok、facebookにはスマホからライブ配信する際に、AIフィルタを選択する機能がついています。カメラの映像から顔を検出して、動物にキャラ変したり、化粧したり、マスクしたり、髪の色を変えたりなど、オリジナルの映像を元に何かしらの計算を加えて書き換えることをフィルタと呼びます。stable diffusionのベースとなっているdiffusersを使用した画像生成AIはこれまで1枚あたり10〜30秒程度かかっていたため、ライブ配信のフィルタには使用できませんでしたが、現在は10月に出てきたLCMやその他の改善で速度が数十倍速くなり、最新の速いGPUがあれば十分なフレームレートを出せるようになりました。そこでクラウドです。カメラの映像をクラウドに転送し、そこでGPUを使ったAIフィルタを通したものを送り返すことで、ミドルレンジのパソコンやスマホからでも最新AI技術を使った配信を実現することができます。
独自ドメインとSSL証明書を取得
WebRTCを使用するために、DNSが設定可能なドメインと、そのSSL証明書(TLS証明書)が必要です。本記事の趣旨から外れてしまうため、取得方法は省略させていただきます。noip.com と Let's Encrypt でそれぞれ無料で取得できます。本記事では、p2pcamera.hopto.org というドメインを取得し、fullchain.pem、privkey.pem というファイル名のSSL証明書が手元にある前提で進めます。p2pcamera.hopto.org という文字列を全て読者様のドメインに読み替えてください。
VPSサーバを起動・セットアップ
GPU付きのVPS(Virtual Private Server)を取得します。Google Compute Engine、Azure、AWSのどれでもよいです。その他のVPSサービスの場合はUDPポートが解放できるところにしましょう。HuggingFaceなどのPaaSでは、UDPポートが使用できないところが多いようです。1時間でおおよそ100円〜200円が相場のようです。
NVIDIAのドライバが古い場合は更新してください。本記事執筆時点で 545.29.06 が最新です。 こちらで最新バージョンを確認できます。バージョン違いで速度が全く違うので、できれば545以降をインストールしてください。ダウンロードおよびインストールには10分程度かかります。
TCPポート7860とUDPポート全てを外部からアクセス可能に設定してください。
起動したらサーバのIPアドレスをメモし、 p2pcamera.hopto.org のDNSにそのIPを設定します。1分ほど待ったら、pingでパケットの往復時間を調べます。
ping p2pcamera.hopto.org
自宅のLANならば一桁台のミリ秒、インターネット上ならば二桁台のミリ秒でしょう。自宅とクラウドのデータセンターとの物理的な距離によって変わってきます。この数字は小さければ小さいほど映像の遅延が小さくなります。
ブラウザからHTTPS接続の確認
サーバにSSHで接続し、pythonのライブラリをインストールします。Webサーバに相当するaiohttp、WebRTCを処理するaiortc、AIを処理するライブラリをインストールします。この例ではPyTorch、Diffusersをインストールしています。専用サーバなのでvenvやcondaなどのpythonの仮想化は必要ありません。
sudo apt-get install -y python3-pip python3-opencvpip install aiortc aiohttp torch torchvision torchaudio diffusers["torch"] transformers
次にホームディレクトリにフォルダを作成します。
フォルダ名はプロジェクト名 p2pcamera とします。その直下にstaticというフォルダを作成します。p2pcameraフォルダはpythonのプログラムやSSL証明書など、ブラウザから直接アクセスできないファイルを置く場所。staticフォルダはブラウザからアクセスできるファイルを置く場所とします。
cd
mkdir p2pcamera
cd p2pcamera
mkdir static
VSCodeのRemote-SSHでこのサーバに接続し、p2pcameraフォルダを開きます。p2pcameraフォルダに下記のようにfullchain.pemとprivkey.pemファイルを置きます。
hello.py
1 import ssl
2 from aiohttp import web
3
4
5 async def index(request):
6 content = "hello p2pcamera"
7 return web.Response(content_type="text/html", text=content)
8
9
10 def main():
11 cert_file = "fullchain.pem"
12 key_file = "privkey.pem"
13 ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER)
14 ssl_context.load_cert_chain(cert_file, key_file)
15
16 app = web.Application()
17 app.router.add_get("/", index)
18 app.add_routes([web.static("/", "static", show_index=True)])
19
20 web.run_app(
21 app, access_log=None, host="0.0.0.0", port=7860, ssl_context=ssl_context
22 )
23
24
25 if __name__ == "__main__":
26 main()
pythonのプログラムを実行します。
python hello.py
httpsサーバが立ち上がったかのようなメッセージが出ます。
ブラウザで https://p2pcamera.hopto.org:7860/ にアクセスします。このように表示された場合、SSL証明書が有効で、正常なHTTPS接続が確認できました。
SSL証明書とHTTPS接続はWebRTCの根幹で、つながらない場合はこれ以降の作業が無駄になります。ページが表示されない場合、DNSが間違っていないか、ポート7860を外部からアクセスできるようにオープンしているか、SSL証明書が正しいかを確認してください。
カメラの確認
次にWebカメラの映像をシンプルに画面に表示するページを用意します。static フォルダの下に index.html、index.js というファイル名で保存します。
index.html
1 <style>
2 button {
3 margin: 8px;
4 padding: 8px 16px;
5 }
6
7 video {
8 background-color: black;
9 }
10 </style>
11 <div id="video-container">
12 <video id="video" autoplay="true" playsinline="true" muted="true"></video>
13 </div>
14 <div>
15 <button id="start" onclick="start()">Start</button>
16 <button id="stop" style="display: none" onclick="stop()">Stop</button>
17 </div>
18 <script src="index.js"></script>
index.js
1 async function getDeviceId(deviceName) {
2 const devices = await navigator.mediaDevices.enumerateDevices();
3 device = devices.find((device) => device.label.includes(deviceName));
4 return device?.deviceId;
5 }
6
7 async function start() {
8 document.getElementById('start').style.display = 'none';
9 document.getElementById('stop').style.display = 'inline-block';
10 var constraints = {
11 audio: true,
12 video: { width: 640, height: 480 },
13 };
14 try {
15 const deviceId = await getDeviceId('FaceTime');
16 constraints.video.deviceId = deviceId;
17 } catch (e) { }
18 const stream = await navigator.mediaDevices.getUserMedia(constraints);
19 document.getElementById('video').srcObject = stream;
20 }
21
22 function stop() {
23 document.getElementById('stop').style.display = 'none';
24 document.getElementById('start').style.display = 'inline-block';
25 document.getElementById('video').srcObject?.getTracks().forEach(t => t.stop());
26 }
このjavascriptはブラウザでWebカメラの映像を表示する一般的なコードです。index.js 15行目でデバイス名に"FaceTime" を含むカメラを指定しています。他のカメラを指定する場合はこの行を修正します。ブラウザで https://p2pcamera.hopto.org:7860/index.html にアクセスします。
「Start」ボタンを押すと下図のようにカメラ映像が表示されます。「Stop」を押すと閉じます。
p2p接続の確認
次にWebRTCを使用するためのPythonプログラムをサーバに置きます。testp2p.py というファイル名にします。
testp2p.py
1 import asyncio, json, logging, ssl, uuid
2 from aiohttp import web
3 from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription
4 from av import VideoFrame
5
6 logger = logging.getLogger("pc")
7 pcs = set()
8
9
10 class VideoTransformTrack(MediaStreamTrack):
1 kind = "video"
12
13 def __init__(self, track, params):
14 super().__init__()
15 self.track = track
16 self.params = params
17
18 async def recv(self) -> VideoFrame:
19 frame = await self.track.recv()
20 return frame
21
22
23 async def offer(request):
24 params = await request.json()
25 offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
26
27 pc = RTCPeerConnection()
28 pc_id = "PeerConnection(%s)" % uuid.uuid4()
29 pcs.add(pc)
30
31 def log_info(msg, *args):
32 logger.info(pc_id + " " + msg, *args)
33
34 log_info("Created for %s", request.remote)
35
36 @pc.on("datachannel")
37 def on_datachannel(channel):
38 @channel.on("message")
39 def on_message(message):
40 if isinstance(message, str) and message.startswith("ping"):
41 channel.send("pong" + message[4:])
42
43 @pc.on("connectionstatechange")
44 async def on_connectionstatechange():
45 log_info("Connection state is %s", pc.connectionState)
46 if pc.connectionState == "failed":
47 await pc.close()
48 pcs.discard(pc)
49
50 @pc.on("track")
51 def on_track(track):
52 log_info("Track %s received", track.kind)
53
54 if track.kind == "video":
55 pc.addTrack(VideoTransformTrack(track, params))
56
57 @track.on("ended")
58 async def on_ended():
59 log_info("Track %s ended", track.kind)
60
61 await pc.setRemoteDescription(offer)
62 answer = await pc.createAnswer()
63 await pc.setLocalDescription(answer)
64
65 return web.Response(
66 content_type="application/json",
67 text=json.dumps(
68 {"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}
69 ),
70 )
71
72
73 async def index(request):
74 return web.HTTPFound("/index.html")
75
76
77 async def on_shutdown(app):
78 coros = [pc.close() for pc in pcs]
79 await asyncio.gather(*coros)
80 pcs.clear()
81
82
83 def main():
84 logging.basicConfig(level=logging.INFO)
85
86 cert_file = "fullchain.pem"
87 key_file = "privkey.pem"
88 ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER)
89 ssl_context.load_cert_chain(cert_file, key_file)
90
91 app = web.Application()
92 app.router.add_get("/", index)
93 app.router.add_post("/offer", offer)
94 app.add_routes([web.static("/", "static", show_index=True)])
95
96 web.run_app(
97 app, access_log=None, host="0.0.0.0", port=7860, ssl_context=ssl_context
98 )
99
100
101 if __name__ == "__main__":
102 main()
ブラウザ向けのファイルとしてstaticフォルダに p2pcamera.js というファイルを作成します。
p2pcamera.js
1 navigator.mediaDevices.originalGetUserMedia = navigator.mediaDevices.getUserMedia;
2 navigator.mediaDevices.getUserMedia = async (constraints) => {
3 const stream = await navigator.mediaDevices.originalGetUserMedia(constraints);
4 const vt = stream.getVideoTracks()
5 if (vt.length) {
6 const originalVideoTrack = vt[0];
7 const pc = new RTCPeerConnection();
8 const videoTrackPromise = new Promise((resolve, reject) => {
9 pc.addEventListener('track', (evt) => {
10 if (evt.track.kind == 'video') {
11 resolve(evt.track);
12 }
13 });
14 });
15
16 pc.addTransceiver(originalVideoTrack, stream);
17 offer = await pc.createOffer();
18 pc.setLocalDescription(offer);
19 response = await fetch('https://p2pcamera.hopto.org:7860/offer', {
20 body: JSON.stringify({
21 sdp: offer.sdp,
22 type: offer.type,
23 }),
24 headers: { 'Content-Type': 'application/json' },
25 method: 'POST'
26 });
27 answer = await response.json();
28 await pc.setRemoteDescription(answer);
29 const newVideoTrack = await videoTrackPromise;
30 newVideoTrack._stop = newVideoTrack.stop;
31 newVideoTrack.stop = () => {
32 newVideoTrack._stop();
33 pc.getTransceivers()?.forEach(t => { try { t.stop?.() } catch (e) { } });
34 pc.getSenders().forEach(s => s.track.stop());
35 setTimeout(() => { pc.close(); }, 500);
36 }
37 stream.removeTrack(originalVideoTrack);
38 stream.addTrack(newVideoTrack);
39 }
40 return stream;
41 };
index.html から p2pcamera.js を読み込むように、index.html の末尾に追記します。
19 <script src="p2pcamera.js"></script>
以上の準備ができたら testp2p.py を実行し、ブラウザで https://p2pcamera.hopto.org:7860/index.html にアクセスして、startボタンを押します。
python testp2p.py
startボタンを押した時に若干画質が落ちているのが分かるでしょうか? ブラウザとVPSをp2pで接続し、カメラの映像をVPSにストリーミングし、何も加工せずにそのまま送り返したものを表示しています。表示されない場合はブラウザのconsoleを確認してください。
カメラに時計等を表示し、遅延時間を計測することができます。こちらの環境だと0.2秒程度です。
AIフィルタの実装と確認
VPSには yuv420p のフレームが順に届きます。それをAIやOpenCV等の画像処理を加えて送り返しますが、到着する動画のフレーム間隔よりも、画像処理が遅いことがよくあります。ライブ配信の場合は、届いたフレームを全て順に処理するのではなく、キューに貯めて、古くなったものを破棄する行程が必要です。また、AIフィルタは重たい処理なので、マルチプロセスにして、WebRTCの処理に影響が出にくいようにします。それによりp2pもAI処理もそれぞれの持つ最高速度を実現できます。
AI処理のプロセス用の関数を別のPythonスクリプトにします。本記事では、例として detectron2のDensePose を使います。人間の体をセグメンテーションして色付けします。
下記のコマンドをVPS上で実行し、必要なライブラリをインストールします。
pip install
'git+https://github.com/facebookresearch/detectron2@main#subdirectory=projects/DensePose'
git clone https://github.com/facebookresearch/detectron2
p2pで受け取った画像フレームをDensePoseして送り返すPythonスクリプト sub_densepose.pyを作成します。
sub_densepose.py
1 import torch
2 from time import time
3 import numpy as np
4 from queue import Empty
5 from torch.multiprocessing import Queue, RawValue
6
7 from detectron2.config import get_cfg
8 import torch
9 import cv2
10 import numpy as np
11 from detectron2.engine import DefaultPredictor
12 from densepose import add_densepose_config
13 from densepose.vis.extractor import DensePoseResultExtractor
14 from densepose.vis.densepose_results import (
15 DensePoseResultsFineSegmentationVisualizer as Visualizer,
16 )
17
18 app_starttime = time()
19
20
21 def atime(basetime):
22 return int((time() - basetime) * 1000)
23
24
25 def aprint(*args):
26 print(f"{atime(app_starttime):,}:", *args)
27
28
29 def sub_main(input_queue: Queue, output_queue: Queue, processed_count: RawValue):
30 cfg = get_cfg()
31 add_densepose_config(cfg)
32 cfg.merge_from_file(
33 "detectron2/projects/DensePose/configs/densepose_rcnn_R_50_FPN_s1x.yaml"
34 )
35 cfg.MODEL.WEIGHTS = "https://dl.fbaipublicfiles.com/densepose/densepose_rcnn_R_50_FPN_s1x/165712039/model_final_162be9.pkl"
36 predictor = DefaultPredictor(cfg)
37
38 firsttime = None
39 while True:
40 try:
41 image = input_queue.get()
42 except Empty:
43 continue
44 start = time()
45 if processed_count.value <= 1:
46 firsttime = time()
47
48 height, width = image.height, image.width
49 image = np.asarray(image)[:, :, ::-1]
50
51 with torch.no_grad():
52 outputs = predictor(image)["instances"]
53
54 results = DensePoseResultExtractor()(outputs)
55 cmap = cv2.COLORMAP_VIRIDIS
56 arr = cv2.applyColorMap(np.zeros((height, width), dtype=np.uint8), cmap)
57 result = Visualizer(alpha=1, cmap=cmap).visualize(arr, results)[:, :, ::-1]
58
59 processed_count.value += 1
60 count = processed_count.value
61 fps = (count - 1) / (time() - firsttime)
62 aprint(f"{count}, {atime(start):,}ms, {fps:.2f}fps")
63 output_queue.put(result)
この関数 sub_mainはWebRTCのプロセスと別のプロセスで実行されているため、呼び出し元のプロセスと変数の共有はできません。引数に受け取った input_queue、output_queue、processed_count の3変数だけが、双方のプロセスからアクセスできる特別な変数です。
次に testp2p.py を改良し、AI処理のプロセス起動と、p2pで受け取ったフレームをAI処理に投げる処理を追加します。ファイル名を p2pcamera.py とします。
p2pcamera.py
1 import asyncio, json, logging, ssl, uuid
2 from aiohttp import web
3 from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription
4 from av import VideoFrame
5 import numpy as np
6 from queue import Empty
7 from torch.multiprocessing import Process, Queue, RawValue, set_start_method
8
9 try:
10 set_start_method("spawn")
1 except RuntimeError:
12 pass
13
14 ai_process = None
15 input_queue = Queue()
16 output_queue = Queue()
17 processed_count = RawValue("i", 0)
18
19
20 def push_pop(frame):
21 try:
22 while not input_queue.empty():
23 input_queue.get_nowait()
24 except Empty:
25 pass
26 input_queue.put(frame.to_image())
27 try:
28 return output_queue.get_nowait()
29 except Empty:
30 return None
31
32
33 logger = logging.getLogger("pc")
34 pcs = set()
35
36
37 class VideoTransformTrack(MediaStreamTrack):
38 kind = "video"
39
40 def __init__(self, track, params):
41 super().__init__()
42 self.track = track
43 self.params = params
44 processed_count.value = 0
45 while not input_queue.empty():
46 input_queue.get_nowait()
47 img = np.zeros((256, 256, 3), dtype=np.uint8)
48 self.last_img = VideoFrame.from_ndarray(img, format="rgb24")
49
50 async def recv(self) -> VideoFrame:
51 frame = await self.track.recv()
52 img = push_pop(frame)
53
54 if img is None:
55 new_frame = self.last_img
56 new_frame.pts = frame.pts
57 new_frame.time_base = frame.time_base
58 return new_frame
59
60 new_frame = VideoFrame.from_ndarray(img, format="rgb24")
61 new_frame.pts = frame.pts
62 new_frame.time_base = frame.time_base
63 self.last_img = new_frame
64 return new_frame
65
66
67 async def offer(request):
68 params = await request.json()
69 offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
70
71 pc = RTCPeerConnection()
72 pc_id = "PeerConnection(%s)" % uuid.uuid4()
73 pcs.add(pc)
74
75 def log_info(msg, *args):
76 logger.info(pc_id + " " + msg, *args)
77
78 log_info("Created for %s", request.remote)
79
80 @pc.on("datachannel")
81 def on_datachannel(channel):
82 @channel.on("message")
83 def on_message(message):
84 if isinstance(message, str) and message.startswith("ping"):
85 channel.send("pong" + message[4:])
86
87 @pc.on("connectionstatechange")
88 async def on_connectionstatechange():
89 log_info("Connection state is %s", pc.connectionState)
90 if pc.connectionState == "failed":
91 await pc.close()
92 pcs.discard(pc)
93
94 @pc.on("track")
95 def on_track(track):
96 log_info("Track %s received", track.kind)
97
98 if track.kind == "video":
99 pc.addTrack(VideoTransformTrack(track, params))
100
101 @track.on("ended")
102 async def on_ended():
103 log_info("Track %s ended", track.kind)
104
105 await pc.setRemoteDescription(offer)
106 answer = await pc.createAnswer()
107 await pc.setLocalDescription(answer)
108
109 return web.Response(
10 content_type="application/json",
11 text=json.dumps(
12 {"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}
13 ),
14 )
15
16
17 async def index(request):
18 return web.HTTPFound("/index.html")
19
120
121 async def on_shutdown(app):
122 coros = [pc.close() for pc in pcs]
123 await asyncio.gather(*coros)
124 pcs.clear()
125
126
127 @web.middleware
128 async def cors_middleware(request, handler):
129 headers = {
130 "Access-Control-Allow-Headers": "*",
131 "Access-Control-Allow-Methods": "*",
132 "Access-Control-Allow-Origin": "*",
133 }
134 if request.method == "OPTIONS":
135 return web.Response(headers=headers)
136 try:
137 response = await handler(request)
138 for key, value in headers.items():
139 response.headers[key] = value
140 return response
141 except web.HTTPException as e:
142 for key, value in headers.items():
143 e.headers[key] = value
144 raise e
145
146
147 def main():
148 from sub_densepose import sub_main
149
150 global ai_process
151 ai_process = Process(
152 target=sub_main, args=(input_queue, output_queue, processed_count), daemon=True
153 )
154 ai_process.start()
155 logging.basicConfig(level=logging.INFO)
156
157 cert_file = "fullchain.pem"
158 key_file = "privkey.pem"
159 ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER)
160 ssl_context.load_cert_chain(cert_file, key_file)
161
162 app = web.Application(middlewares=[cors_middleware])
163 app.on_shutdown.append(on_shutdown)
164 app.router.add_get("/", index)
165 app.router.add_post("/offer", offer)
166 app.add_routes([web.static("/", "static", show_index=True)])
167
168 web.run_app(
169 app, access_log=None, host="0.0.0.0", port=7860, ssl_context=ssl_context
170 )
171
172
173 if __name__ == "__main__":
174 main()
以上の準備ができたら p2pcamera.py を実行し、ブラウザで https://p2pcamera.hopto.org:7860/index.html にアクセスして、startボタンを押します。
python p2pcamera.py
1回目の起動時は関連するモデルファイルを大量にダウンロードするため数分待たされますが、2回目以降はキャッシュされるのであまり待たずに起動できます。
さて、これまでのカメラ映像と違うものが出てきたでしょうか?フレームレートも明らかに遅くなっていることでしょう。こちらの環境だと11fps程度です。先ほどより0.1秒程度遅延していることにも気づきます。先ほどと比較して増えたのがAI画像処理の時間です。
ビデオ会議サイトへの再配信
AIフィルタを通した映像が手に入ったところで、それを別のビデオ会議サイトに配信してみましょう。ブラウザ側のスクリプト index.js をよく見てください。getUserMediaを呼んでいるだけです。p2p接続してAIフィルタをかける処理は完全に裏方処理として隠蔽されているので、ビデオ会議サイトへの映像配信のプログラムは従来のWebカメラやUSBカメラの配信と何一つ変更する必要がありません。p2pcamera.js を配信用のWebページに<script>タグで読み込むだけで、AIフィルタが適用されます。
ImageFlux Live Streamingのように配信のWebページを自社で実装し、直接修正できる場合は<script>タグを1行追加するだけです。一方、他社サービスのビデオ会議サイトにはスクリプトを追加することはできないので、Chrome拡張を使って挿入します。Chrome拡張の書き方については話が長くなるため、別記事としますが、p2pcamera.js は上で紹介したものから変更がありません。
下図はChrome拡張を使ってp2pcamera.jsをGoogle Meetに読み込ませた例です。
下図はChrome拡張を使ってp2pcamera.jsをMicrosoft Teamsに読み込ませた例です。
まとめ
ブラウザからp2pでクラウドのVPSサーバに映像を転送し、AIフィルタを通した映像を別サイトに再配信するサンプルを紹介しました。セグメンテーションや物体検出、顔検出、その他AIが得意とする処理をクラウド上で実現できます。カメラごとに1台ずつサーバを用意するのがコスト面で大変ですが、1時間100円の維持費がまかなえるようなサービスで今後採用が出てくると思います。GPUの価格も徐々に下がっていくでしょう。diffusersを使ったi2iのAIフィルタも、OpenPoseのAIフィルタも、あらゆるAIフィルタが本記事で紹介したものとほぼ同等のコードで実現可能です。ぜひ挑戦してください。ご質問、ご感想もお待ちしています。