業界最安値のGPUaaS「GPUSOROBAN」を実際に試してみた(4)~Dockerコンテナを使用し機械学習プロト環境を作る~【オンプレでリアルタイム顔検出編】
皆さん、こんにちは。
本ブログは最新のGPUが使える上に、とても安いと噂の「GPUSOROBAN」がどんな感じのものなのか、Dockerコンテナを使用し機械学習プロト環境を作成し、最終的にはオンプレ環境とクラウド環境でリアルタイムに顔検出をおこなう工程を実際に試してみたブログです。
今回は、「オンプレでリアルタイム顔検出編」と題し、raspberry PIからカメラ映像の配信を行う工程から始まり、リアルタイム顔検出をする様子についてお伝えします。
なお、リアルタイム顔検出実行時のエラーについては顔検出を行った際に発生し得るエラー対策のヒントについてお伝えしていますので、ご参考になれば幸いです。
それでは、始めましょう。
目次
Raspberry PIからカメラ映像を配信
少々古いですが、以下のイメージをインストールしたRaspberry PI 4にmjpg_streamerをインストールする手順です。特殊なものは使っていないのでPI2や3、PCでも動かせると思います。ネットワーク等の設定は環境に合わせて設定してください。
# パッケージをインストール
sudo apt-get install subversion libjpeg-dev imagemagick
# 作業用ディレクトリを作成
sudo chmod 777 /opt
mkdir -p /opt/mjpg-streamer2
cd /opt/mjpg-streamer2
# ソースコードを取得
git clone https://github.com/jacksonliam/mjpg-streamer.git
# ビルド
cd mjpg-streamer/mjpg-streamer-experimental
make
# インストール
sudo make install
ここにカメラを接続したRaspberry PIの写真を入れます。
Raspberry PIでカメラ映像を配信します。
mjpg_streamer -i "input_uvc.so -n -fps 30 -r 640x480 -u -q 80" \
-o "output_http.so -w www -p 40080"
mjpeg_streamerの主なオプションです。
-fps:フレームレート
-r:解像度
-q:映像品質
-p:送信ポート番号
PCやスマホのブラウザで以下のURLにアクセスするとカメラの映像が表示されます。
表1-1 mjpg_streamer再生URL
No. | 項目 | URL | |
---|---|---|---|
No.1 | 1 |
再生URL |
http://[Raspberry PIのIPアドレス]:40080/?action=stream |
図1 mjpg_streamer受信画面
配信の停止はctrl-cです。
配信を停止して再開した場合は、ブラウザをリロードしてください。
リアルタイム顔検出
顔検出のコードとデータ一式をダウンロードして、ホストPCからコンテナにscpコマンド等でコピーしてください。
顔検出コード一式:
soroban3-20220815a.tar.bz2
scp -P 40022 soroban3-202208xx.tar.bz2 $USER_NAME@$HOST_IP:/work/
私の環境では以下のコマンドでコピーしました。
scp -P 40022 soroban3-20220815a.tar.bz2 $USER_NAME@$HOST_IP:/work/
別途用意した動画ファイルを入れたフォルダのアーカイブもコピーしました。
scp -P 40022 movie-xeon-20220616a.tar.bz2 $USER_NAME@$HOST_IP:/work/
コンテナにログインしてコードを展開します。
cd /work/
tar xvfj soroban3-202208xx.tar.bz2
コード一式のディレクトリは以下の構成になっています。
kirin@docker_CUDA:/work$ ls -la soroban3
total 24
drwxrwxr-x 4 kirin kirin 4096 Aug 15 13:50 .
drwxr-xr-x 5 kirin kirin 4096 Aug 17 04:52 ..
-rw-rw-r-x 1 kirin kirin 4059 Aug 15 13:44 detectMultiScale_cpu.py # CUDAを使用しない顔検出スクリプト
-rw-rw-r-x 1 kirin kirin 3450 Aug 15 13:45 detectMultiScale_cuda.py # CUDAを使用する顔検出スクリプト
lrwxrwxrwx 1 kirin kirin 16 Jun 10 19:00 input.mp4 -> ../movie/xxx.mp4 # 動画ファイル
drwxr-xr-x 2 kirin kirin 4096 Feb 19 2021 lbpcascades # 学習済データ
drwxrwxr-x 2 kirin kirin 4096 Aug 15 13:51 www # 検出結果表示用HTML
コード一式に動画ファイルは含んでいませんが、input.mp4はmovie-xeon-20220616a.tar.bz2の中のxxx.mp4にリンクました。以下の仕様の動画ファイルであれば使用できると思います。
コンテナ:mp4
映像:H.264 AVC
オーディオ:AAC
解像度:フルHD以下
動画ファイルはファイル名をinput.mp4にして/work/soroban3のinput.mp4と置き換えます。
Raspberry PIで配信を開始して以下のコマンドを実行するとブラウザからのアクセス待ちになります。
### 以下はsshでログインしたコンテナ内で実行します。
cd /work/soroban3
export RASPI_IP=[RASP PIのIPアドレス]
python3 detectMultiScale_cpu.py http
私の環境では以下のコマンドを実行しました。
### 以下はsshでログインしたコンテナ内で実行します。
cd /work/soroban3
export RASPI_IP=192.168.0.143
# CUDAを使用するコンテナの場合
python3 detectMultiScale_cuda.py http
# CUDAを使用しないコンテナの場合
python3 detectMultiScale_cpu.py http
Raspberry PIを使わない場合や顔検出速度を測定する場合は、以下のようにスクリプトの引数のhttpを削除することで、先ほど用意した動画ファイル(input.mp4)を入力として顔検出を行います。
cd /work/soroban3
# CUDAを使用するコンテナの場合
python3 detectMultiScale_cuda.py
# CUDAを使用しないコンテナの場合
python3 detectMultiScale_cpu.py
ホストPC、またはホストPCと同じネットワーク内のブラウザで以下のURLにアクセスすると顔検出を開始します。
表1-2 顔検出開始URL
No. | 項目 | URL | |
---|---|---|---|
No.1 | 1 |
顔検出URL |
http://[ホストPCのIPアドレス]:40090/ |
私は以下のURLにアクセスしました。
http://192.168.0.90:40090/
CUDAを使ってカメラ映像から顔検出したときのブラウザの表示です。
CUDAを使わずにカメラ映像から顔検出したときのブラウザの表示です。
どちらもカメラのフレームレートの30fpsに追従できていることが確認できました。
CUDAを使って動画ファイルから顔検出したときのブラウザの表示です。
CUDAを使わずに動画ファイルから顔検出したときのブラウザの表示です。
CUDA(GTX 1650)は平均で115.6fps、CPU(xeon 1380 V3)は17.0fpsとなりました。GPUはCPUの6.8倍となっていることからCUDAが機能していることを確認できました。
スクリプトの停止はCTRL-cです。
リアルタイム顔検出実行時のエラー
顔検出を行った際に発生し得るエラーの対策のヒントのメモです。
動画ファイルの読み込みエラー
動画ファイルが読み込めないとブラウザでアクセスした際に以下のエラーが発生します。
kirin@docker_CPU:/work/soroban3$ python3 detectMultiScale_cpu.py
play input.mp4
play input.mp4
Bottle v0.12.23 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:9090/
Hit Ctrl-C to quit.
192.168.0.126 - - [17/Aug/2022 07:07:42] "GET / HTTP/1.1" 304 0
[ WARN:0@2.724] global /work/opencv_cuda/opencv/modules/videoio/src/cap_gstreamer.cpp (1127) open OpenCV | GStreamer warning: Error opening bin: unexpected reference "input" - ignoring
[ WARN:0@2.724] global /work/opencv_cuda/opencv/modules/videoio/src/cap_gstreamer.cpp (862) isPipelinePlaying OpenCV | GStreamer warning: GStreamer: pipeline have not been created
[ERROR:0@2.724] global /work/opencv_cuda/opencv/modules/videoio/src/cap.cpp (597) open VIDEOIO(GSTREAMER): raised OpenCV exception:
OpenCV(4.5.5) /work/opencv_cuda/opencv/modules/videoio/src/cap_gstreamer.cpp:1936: error: (-215:Assertion failed) fps > 0 in function 'open'
Frame: 1 301659.2 fps
done
192.168.0.126 - - [17/Aug/2022 07:07:42] "GET /video_feed HTTP/1.1" 200 0
ファイル名やパスを確認します。
映像ストリームの受信エラー
Raspberry PIで配信が行われていない、もしくは何等かの理由でコンテナが映像を受信できていない場合は、ブラウザでアクセスした際に以下のエラーが発生します。
kirin@docker_CUDA:/work/soroban3$ python3 detectMultiScale_cuda.py http
play http://192.168.0.143:40080/?action=stream
play http://192.168.0.143:40080/?action=stream
Bottle v0.12.23 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:9090/
Hit Ctrl-C to quit.
192.168.0.126 - - [17/Aug/2022 05:35:55] "GET / HTTP/1.1" 304 0
[tcp @ 0x2d3dec0] Connection to tcp://192.168.0.143:40080 failed: Connection refused
[ WARN:0@9.005] global /work/opencv_cuda/opencv/modules/videoio/src/cap_gstreamer.cpp (2402) handleMessage OpenCV | GStreamer warning: Embedded video playback halted; module source reported: Could not establish connection to server.
[ WARN:0@9.005] global /work/opencv_cuda/opencv/modules/videoio/src/cap_gstreamer.cpp (1356) open OpenCV | GStreamer warning: unable to start pipeline
[ WARN:0@9.005] global /work/opencv_cuda/opencv/modules/videoio/src/cap_gstreamer.cpp (862) isPipelinePlaying OpenCV | GStreamer warning: GStreamer: pipeline have not been created
[ERROR:0@9.006] global /work/opencv_cuda/opencv/modules/videoio/src/cap.cpp (166) open VIDEOIO(CV_IMAGES): raised OpenCV exception:
OpenCV(4.5.5) /work/opencv_cuda/opencv/modules/videoio/src/cap_images.cpp:253: error: (-5:Bad argument) CAP_IMAGES: can't find starting number (in the name of file): http://192.168.0.143:40080/?action=stream in function 'icvExtractPattern'
[ERROR:0@9.006] global /work/opencv_cuda/opencv/modules/videoio/src/cap.cpp (597) open VIDEOIO(GSTREAMER): raised OpenCV exception:
OpenCV(4.5.5) /work/opencv_cuda/opencv/modules/videoio/src/cap_gstreamer.cpp:1936: error: (-215:Assertion failed) fps > 0 in function 'open'
Frame: 1 319796.5 fps
done
192.168.0.126 - - [17/Aug/2022 05:35:56] "GET /video_feed HTTP/1.1" 200 0
ホストPC、コンテナで下記を実行してどこで受信できなくなっているか特定して対処します。
wget http://[Ras PIのIP]:40080/?action=stream -O tmp.mp4
学習データ読み込みエラー
学習データ(lbpcascades)が読み込めないとブラウザでアクセスした際に以下のエラーが発生します。
kirin@docker_CUDA:/work/soroban3$ python3 detectMultiScale_cuda.py http
play http://192.168.0.143:40080/?action=stream
Traceback (most recent call last):
File "detectMultiScale_cuda.py", line 21, in <module>
classifier = cv2.cuda.CascadeClassifier_create(cascPath)
cv2.error: OpenCV(4.5.5) /work/opencv_cuda/opencv_contrib/modules/cudaobjdetect/src/cascadeclassifier.cpp:155: error: (-217:Gpu API call) NCV Assertion Failed: NcvStat=4, file=/work/opencv_cuda/opencv_contrib/modules/cudalegacy/src/cuda/NCVHaarObjectDetection.cu, line=2363 in function 'NCVDebugOutputHandler'
以下の学習データが存在することを確認します。
kirin@docker_CUDA:/work/soroban3$ ls -la lbpcascades/
total 348
drwxr-xr-x 2 kirin kirin 4096 Feb 19 2021 .
drwxrwxr-x 4 kirin kirin 4096 Aug 17 05:42 ..
-rw-r--r-- 1 kirin kirin 138705 Feb 19 2021 lbpcascade_frontalcatface.xml
-rw-r--r-- 1 kirin kirin 51856 Feb 19 2021 lbpcascade_frontalface.xml
-rw-r--r-- 1 kirin kirin 54039 Feb 19 2021 lbpcascade_frontalface_improved.xml
-rw-r--r-- 1 kirin kirin 47015 Feb 19 2021 lbpcascade_profileface.xml
-rw-r--r-- 1 kirin kirin 47027 Feb 19 2021 lbpcascade_silverware.xml
コードの説明
CPUで動作したコードにGPUを使用する宣言等を追加することでフレームワークがCPUからGPUに切り替えてくれるのではないかと想像していたのですが違っていました。OpenCVの場合、CPUとGPUではAPIが異なるため、コードを書き換える必要がありました。
以下にGPU用コード(detectMultiScale_cuda.py)を示します。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import cv2, time
import bottle
import os
app = bottle.Bottle()
if len(sys.argv) > 1:
raspi_ip = os.environ['RASPI_IP']
url = "http://"+raspi_ip+":40080/?action=stream"
else:
url = "input.mp4"
print("play "+url)
cascPath = './lbpcascades/lbpcascade_frontalface.xml'
classifier = cv2.cuda.CascadeClassifier_create(cascPath)
def gen():
cap=cv2.VideoCapture(url)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
width_mosaic = int(width / 48)
height_mosaic = int(height / 48)
disp_scale = 1/4;
gpu_frame = cv2.cuda_GpuMat()
gpu_frame_mosaic = cv2.cuda_GpuMat()
fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
writer = cv2.VideoWriter('./output.mp4', fmt, fps, (int(width*disp_scale), int(height*disp_scale)))
first_time = time.perf_counter()
last_time = time.perf_counter()
disp_last_time = time.perf_counter()
disp_fps_str = ""
frame_count = 0
while True:
current_time = time.perf_counter()
fps_measurement = round(1 / (current_time - last_time), 1)
last_time = current_time
frame_count +=1
print("Frame: "+ str(frame_count)+" "+str(fps_measurement)+" fps")
ret, frame = cap.read()
if not ret:
print("done")
cap.release()
writer.release()
cv2.destroyAllWindows()
break
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gpu_gray_frame = cv2.cuda_GpuMat(gray_frame)
gpu_result = classifier.detectMultiScale(gpu_gray_frame)
result = gpu_result.download()
gpu_frame.upload(frame)
gpu_frame_mosaic = cv2.cuda.resize(gpu_frame, (width_mosaic, height_mosaic))
gpu_frame_mosaic = cv2.cuda.resize(gpu_frame_mosaic, (int(width*disp_scale), int(height*disp_scale)))
disp_frame_mosaic = gpu_frame_mosaic.download();
if result is not None:
for item in result[0]:
(x, y, w, h) = item
cv2.rectangle(disp_frame_mosaic, (int(x*disp_scale), int(y*disp_scale)), (int((x+w)*disp_scale), int((y+h)*disp_scale)), (0, 255, 0), 2)
if (current_time - disp_last_time) > 0.2:
disp_fps_str = str(fps_measurement)+"fps"+"/"+str(round(frame_count / (current_time - first_time), 1))
disp_last_time = current_time
cv2.putText(disp_frame_mosaic, disp_fps_str, (1,21), fontFace = cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale = 1, color = (255,255,255))
cv2.putText(disp_frame_mosaic, disp_fps_str, (0,20), fontFace = cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale = 1, color = (0,255,0))
writer.write(disp_frame_mosaic)
flag, frame_jpeg = cv2.imencode('.jpg', disp_frame_mosaic)
yield b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + bytearray(frame_jpeg) + b'\r\n\r\n'
# cv2.imshow("Camera GPUTest", disp_frame_mosaic)
# if cv2.waitKey(1) & 0xFF == ord('q'):
# break
@app.route('/')
def main():
return bottle.static_file('gpu.html', root='./www/')
@app.route('/video_feed')
def video_feed():
bottle.response.content_type = 'multipart/x-mixed-replace;boundary=frame'
return gen()
app.run(host='0.0.0.0', port=9090, reloader=True, debug=True)
以下にCPU用コード(detectMultiScale_cpu.py)を示します。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import cv2, time
import bottle
import os
app = bottle.Bottle()
if len(sys.argv) > 1:
raspi_ip = os.environ['RASPI_IP']
url = "http://"+raspi_ip+":40080/?action=stream"
else:
url = "input.mp4"
print("play "+url)
cascPath = './lbpcascades/lbpcascade_frontalface.xml'
#gpu classifier = cv2.cuda.CascadeClassifier_create(cascPath)
classifier = cv2.CascadeClassifier(cascPath)
def gen():
cap=cv2.VideoCapture(url)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
width_mosaic = int(width / 48)
height_mosaic = int(height / 48)
disp_scale = 1/4;
#gpu gpu_frame = cv2.cuda_GpuMat()
#gpu gpu_frame_mosaic = cv2.cuda_GpuMat()
fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
writer = cv2.VideoWriter('./output.mp4', fmt, fps, (int(width*disp_scale), int(height*disp_scale)))
first_time = time.perf_counter()
last_time = time.perf_counter()
disp_last_time = time.perf_counter()
disp_fps_str = ""
frame_count = 0
while True:
current_time = time.perf_counter()
fps_measurement = round(1 / (current_time - last_time), 1)
last_time = current_time
frame_count +=1
print("Frame: "+ str(frame_count)+" "+str(fps_measurement)+" fps")
ret, frame = cap.read()
if not ret:
print("done")
cap.release()
writer.release()
cv2.destroyAllWindows()
break
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
#gpu gpu_gray_frame = cv2.cuda_GpuMat(gray_frame)
#gpu gpu_result = classifier.detectMultiScale(gpu_gray_frame)
#gpu result = gpu_result.download()
result = classifier.detectMultiScale(gray_frame)
#gpu gpu_frame.upload(frame)
#gpu gpu_frame_mosaic = cv2.cuda.resize(gpu_frame, (width_mosaic, height_mosaic))
#gpu gpu_frame_mosaic = cv2.cuda.resize(gpu_frame_mosaic, (int(width*disp_scale), int(height*disp_scale)))
#gpu disp_frame_mosaic = gpu_frame_mosaic.download();
frame_mosaic = cv2.resize(frame, (width_mosaic, height_mosaic))
disp_frame_mosaic = cv2.resize(frame_mosaic, (int(width*disp_scale), int(height*disp_scale)))
#gpu if result is not None:
#gpu for item in result[0]:
#gpu (x, y, w, h) = item
#gpu cv2.rectangle(disp_frame_mosaic, (int(x*disp_scale), int(y*disp_scale)), (int((x+w)*disp_scale), int((y+h)*disp_scale)), (0, 255, 0), 2)
if len(result)>0: # >0
for rect in result:
x,y,w,h=rect
p1,p2=(int(x*disp_scale),int(y*disp_scale)),(int((x+w)*disp_scale),int((y+h)*disp_scale))
cv2.rectangle(disp_frame_mosaic,p1,p2,color=(0,255,0),thickness=4)
if (current_time - disp_last_time) > 0.2:
disp_fps_str = str(fps_measurement)+"fps"+"/"+str(round(frame_count / (current_time - first_time), 1))
disp_last_time = current_time
cv2.putText(disp_frame_mosaic, disp_fps_str, (1,21), fontFace = cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale = 1, color = (255,255,255))
cv2.putText(disp_frame_mosaic, disp_fps_str, (0,20), fontFace = cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale = 1, color = (0,255,0))
writer.write(disp_frame_mosaic)
flag, frame_jpeg = cv2.imencode('.jpg', disp_frame_mosaic)
yield b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + bytearray(frame_jpeg) + b'\r\n\r\n'
# cv2.imshow("Camera GPUTest", disp_frame_mosaic)
# if cv2.waitKey(1) & 0xFF == ord('q'):
# break
@app.route('/')
def main():
return bottle.static_file('cpu.html', root='./www')
@app.route('/video_feed')
def video_feed():
bottle.response.content_type = 'multipart/x-mixed-replace;boundary=frame'
return gen()
app.run(host='0.0.0.0', port=9090, reloader=True, debug=True)
記事のボリュームが大きいと責められているので詳しい解説は省きますが、当初課題としていた項目の簡単にまとめます。
- ・
- 映像ストリームの受信、デコード方法
受信については以下のAPIの引数をURLにするだけでした。カメラであればデバイスファイル、ネットワークであればURLと柔軟に対応してくれる便利なAPIでした。
cv2.VideoCapture()
デコードも自動認識でした。バックグラウンドでgstreamerが動いているらしいです。
- ・
- 映像ストリームのエンコード、送信方法
エンコードは以下のAPIでできました。
cv2.imencode()
送信はbottleを使うことでできました。
bottleはwebサーバとして動いてくれるので、webアプリの開発と同じく、スクリプトにエラーが発生すると何が起きているか分かりにくいところに手間がかかりました。
これはbottoleに限らずwebアプリ共通の問題なので便利なデバッガが欲しいところです。
- ・
- 映像ストリームのファイル保存
以下のAPIでできました。
cv2.VideoWriter_fourcc()
フォーマットにmp4形式を指定するだけでmp4にエンコードしてくれました。
- ・
- 推論のCPUとGPU処理の切り替え方法
GPU用のAPIにコードを書き換える必要がありました。
GPUのメモリへの転送や結果の取得にはcv2.cuda_GpuMat()を使うということもあって、APIを置き換えるだけでは済みませんでした。単純に置き換えられたのは表4-3の2つのAPIのみでした。
表1-3 OpenCVのCUDA API
No. | CPU用API | GPU用API | |
---|---|---|---|
No.1 | 1 |
cv2.CascadeClassifier() |
cv2.cuda.CascadeClassifier_create() |
No.2 | 2 |
cv2.resize() |
cv2.cuda.resize() |
解析処理にあたるcv2.detectMultiScale()は変更不要でした。
色空間を変換するcv2.cvtColor()は、CUDA用のcv2.cuda.cvtColorが存在するようですが、今回使用したOpenCVには実装されていないようで使えませんでした。OpenCV 4.0以上であれば使えるという情報もありましたが原因不明です。
出来上がったコンテナのイメージ作成
ホストPCでコンテナをイメージ化します。このイメージがあればコンテナを復元することができるので、他のPCやクラウドに同じ環境を展開する際やバックアップとして活用できます。
ホストPCで以下のコマンドを実行します。
sudo mkdir $WORK_DIR/docker_images
sudo chown $USER_NAME.$GROUP_NAME $WORK_DIR/docker_images
cd $WORK_DIR/docker_images
# CUDAを使用するコンテナの場合
docker stop kirin_cuda_test01 # コンテナが起動している場合は停止する
time docker commit kirin_cuda_test01 kirin/cuda11_4_ubuntu18.04__01
time docker save kirin/cuda11_4_ubuntu18.04__01 | gzip > kirin_cuda11_4_ubuntu18.04__$REVISION.tar.gz
# CUDAを使用しないコンテナの場合
docker stop kirin_cpu_test01 # コンテナが起動している場合は停止する
time docker commit kirin_cpu_test01 kirin/cpu_ubuntu18.04__01
time docker save kirin/cpu_ubuntu18.04__01 | gzip > kirin_cpu_ubuntu18.04__$REVISION.tar.gz
このコンテナのイメージを使ってクラウドでも実験してみたいと
おわりに
いかがでしたでしょうか。
最後までご覧いただきありがとうございました!
次回もよろしくお願いします。