2023/4/20: obs-websocket v5版はこちら
目次
イントロ
みなさんオウチDJ配信してますか?ぼくは(身内向けに)してます。
某音楽配信のあと某絵師の配信に飛び機材を買う流れに乗ってDDJ-400を買ったクチですけども、好きな曲を流しまくってリスナーに好きな曲を叩き込むの楽しいねぇ!
ところで、rekordbox+OBSで配信してるとき配信画面にトラックタイトル出したくなるじゃないですか。無い?あるよね。でもrekordboxには(現状)そういう情報を出力する機能が無い。
仕方ないのでレコボの画面そのまま普通にキャプチャするけど次流そうとしてる曲も見えちゃってこう…… みたいな。
状況に応じて隠したりするといいんだろうけど、VJさんが居るわけでもなし手動で切り替えるの大変じゃないですか。
自動化、したくないですか?
アプローチ
KUVOを経由する方法は偉大なる先人様達がやってますので、レコボの画面キャプチャを状況に応じて切り替えるスタイルで考えます。ラグも少ないしね。
あと2デッキで考えます。3デッキ以上は……まあ……
さて、見せたいのは"今流してる曲"、すなわちMASTERになってるデッキのトラックタイトルというわけです。要するにMASTERが点灯しているデッキがどっちなのか分かればなんとかなるはずです。
幸いにもMASTERが点灯するとレコボ上で"MASTER"のテキスト色が変わるので、これを上手い具合に認識できれば処理できそうですよね。PythonとOpenCVで軽く画像処理してみましょう。
あっバージョン変わってる……
偉大なる先人様
参考にさせて頂いたり部分的にお借りしたりなどしました。
Python + OpenCVによる色情報の取得 - Qiita
ところでQiita(キータ)ってちょっと前[いつ?]まで"チータ"って読むと思ってたんですよね。ワイヤレス充電規格のQi(チー)を先に知ってたのが原因で……。今でもチータって言っちゃう。
物理環境
まずぼくの環境です。実際の接続は頭おかしいことになってますが必要な部分だけ大雑把に抜き出すとだいたいこんな感じです。まあレコボの画面が取得できればいいのでPC1台でやってる人もできます。
今回作るものはすべてメインPC上で動かします。
必要なものなど
- OBS
たいせつ。
- obs-websocket
OBSのプラグインです。導入方法等は割愛しますがココから取ってきて入れましょう。
- Python
面倒なことはPythonにやらせよう。
Anacondaで何も考えず環境建てたら3.7.9でした。
- OpenCV
今回の核です。画像処理はだいたいOpenCVに任せておけばいい。かしこい。
Anaconda Navigatorからポチっとするとnumpy他必要なもの全部勝手に入ります。
- obs-websocket-py
pythonからOBS制御するやつです。デフォルトではAnaconda Navigatorで見つからないのでpipで入れます。
0.5.1が降ってきました。その他必要なモジュールとかも一緒に落ちてくると思います。
pip install obs-websocket-py
映像を切り取って色情報を得るテスト
手始めにそれっぽいのを書きます。
なお切り抜き位置指定はこんな感じです。環境によって変わると思うので気合いで測ってください。
capture = cv2.VideoCapture(1)
print(capture.isOpened())
capture.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
ret, frame = capture.read()
crop1 = frame[ 353 : 370 , 902 : 958]
crop2 = frame[ 353 : 370 , 1862 : 1918]
b1 = crop1.T[0].flatten().mean()
g1 = crop1.T[1].flatten().mean()
r1 = crop1.T[2].flatten().mean()
b2 = crop2.T[0].flatten().mean()
g2 = crop2.T[1].flatten().mean()
r2 = crop2.T[2].flatten().mean()
cv2.imwrite("F:/dev-obs/crop1.png", crop1)
cv2.imwrite("F:/dev-obs/crop2.png", crop2)
print("DECK 1 ##################")
print("B: %.2f" % (b1))
print("G: %.2f" % (g1))
print("R: %.2f" % (r1))
print("DECK 2 ##################")
print("B: %.2f" % (b2))
print("G: %.2f" % (g2))
print("R: %.2f" % (r2))
そうすると次のような2枚の画像と色情報が取れました。
オレンジに点灯するので赤が大きく増えますね。赤が20超えたら点灯判定とかで良さそうです。
実装
おまたせしました、実装です。
1秒ごとに映像を読み取って処理しています。このソースが全てなのでこれを一部調整して適当な名前.pyで保存して実行すれば動くはずです。
エラー処理とか殆ど考えてないので動けばいい人向けですね。
Ctrl+Cで停止します。
トラックタイトル表示の切り替えですが、デッキ1の表示にデッキ2の表示(以下の場合"deck2_track"という名前で用意してあります)を重ねていて、デッキ2のソース表示/非表示を制御することで切り替えています。
import os
import cv2
import time
import datetime
from obswebsocket import obsws, requests
# ======================================
# OBS接続設定
host = "localhost"
port = 4444
password = "password"
# 色のしきい値
threshold = 20
# キャプチャ解像度
capture_width = 1920
capture_height = 1080
# クロップエリア
deck_top = 353
deck_bottom = 370
deck1_left = 902
deck1_right = 958
deck2_left = 1862
deck2_right = 1918
# ======================================
# ソースの表示を切り替える
def setVisible(item, visible):
ws.call(requests.SetSceneItemProperties(item, visible=visible))
# VideoCapture オブジェクトを取得
capture = cv2.VideoCapture(1) # Webカメラが0に割り当てられているのでキャプボを指定する引数は1
print(capture.isOpened())
capture.set(cv2.CAP_PROP_FRAME_WIDTH, capture_width)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, capture_height)
# OBSに接続
ws = obsws(host, port, password)
ws.connect()
# 処理フラグ
switch = 0
try:
while True:
# 映像を取得
ret, frame = capture.read()
# クロップ
deck1_crop = frame[ deck_top : deck_bottom , deck1_left : deck1_right]
deck2_crop = frame[ deck_top : deck_bottom , deck2_left : deck2_right]
# 色情報を取得
red_deck1 = deck1_crop.T[2].flatten().mean()
red_deck2 = deck2_crop.T[2].flatten().mean()
# DECK1がMASTERのとき
if red_deck1 > threshold and (switch == 2 or switch == 0):
os.system('cls')
print(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
print("\nNOW PLAYING: DECK1\n")
setVisible("deck2_spinner", False)
setVisible("deck1_spinner", True)
setVisible("deck2_track", False)
switch = 1
# DECK2がMASTERのとき
elif red_deck2 > threshold and (switch == 1 or switch == 0):
os.system('cls')
print(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
print("\nNOW PLAYING: DECK2\n")
setVisible("deck1_spinner", False)
setVisible("deck2_spinner", True)
setVisible("deck2_track", True)
switch = 2
# よくわからんとき
elif (red_deck1 < threshold and red_deck2 < threshold) or (red_deck1 > threshold and red_deck2 > threshold):
os.system('cls')
print(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
print("\nUndefined\n")
setVisible("deck1_spinner", False)
setVisible("deck2_spinner", False)
setVisible("deck2_track", False)
switch = 0
# 1秒待つ
time.sleep(1)
# Ctrl+Cが押されたら終了
except KeyboardInterrupt:
capture.release()
ws.disconnect()
動作デモ
簡単な動作デモです。イラストの娘はオリキャラのつばきちゃん、要するにぼくの分身です。友人が描いてくれました。
MASTERの点灯に応じてトラックタイトル表示と、つばきちゃんの手元にあるDDJのような何かのエフェクトが(1秒間隔で読む設定にしてあるので瞬時ではないですが)切り替わっているのがわかるかと思います。
問題点など
当然ですが、画面をキャプチャして処理しているのでレコボのレイアウトが変わるような使い方をする人は使えません。
また、キャプチャしている部分にマウスカーソル等が重なるとバグるので注意する必要があります。
おわりに
とりあえず使えるのが出来たのでこれでやっていこうと思います。
画像処理でやってるだけなので色々応用効くと思いますが、機能を増やすとかGUI作るとかのやる気は(今の所)ないので、もしやる気のある人がいればいい感じに改築して頂けると嬉しいです。
特に関係ない宣伝
魔女の旅々をよろしく。めちゃくちゃ好きな作品なので
アウトロ
イントロが流れてくる