rekordboxで流れている曲をOBSに表示するツールを作ってみた

こんにちは、私は東北でボカロ曲のDJをしています。現地でDJをするほか、自分のtwitchでも配信しています。今回はその配信の際に役立つツールを作成しましたので、その概要をここに書きます。

目的

配信中では、様々な曲を流しています。現在再生中の曲のタイトルやアーティストの名前は、視聴者にとって重要な情報です。これにより、視聴者が好きな曲を見つけたときに、簡単に検索できるようになります。

最初はOBSのシーン切り替えで対応していましたが、切り替えミスにより、今後流す曲が事前にバレてしまう可能性がありました。そのため、曲の表示切り替えはPythonにお任せすることにしました。

配信システムの概要

図1に私が運用している配信環境を示します。

fig1.配信システムの構造

図1に、配信システムの構造を示します。DJ用と配信用の2つのパソコンを使用しています。音声信号の流れは黒い矢印で示されており、映像データの流れは赤い矢印で示されています。曲のタイトルはrekordboxの画面をキャプチャして配信用のパソコンに送られています。

OBSでは、NDIで受け取った画面をシーンとして登録し、そのシーンをOBSの仮想カメラの機能を使用してPythonのプログラムに送っています。曲名が表示されている部分を切り取り、DECK1とDECK2それぞれのシーンを作成しています。図2と図3にそれぞれ示します。

fig2.DECK1のシーン


fig3.DECK2のシーン

このシーンの切り替えをpythonで制御します。

実行環境

OS Windows10
Python 3.9.12
numpy 1.23.1
obs-websocket-py 1.0
opencv-python 4.8.0.76

プログラムの処理内容

fig4にプログラムのフローチャートを示します。

fig4.プログラムのフローチャート

OBSとプログラムを接続するために、obs-websocketを使用しています。OBSから出力された仮想カメラの画像データをOpenCVで取得します。rekordboxでは、マスタートラックをオレンジ色で表示されるため、どちらがオレンジ色かでトラックを判定します。

マスタートラックになっている方のDECKにシーンを切り替える命令をOBSに送ります。そして、この一連の流れを5秒ごとに繰り返すことで、配信と追従を行っています。

fig5.マスタートラックの点灯位置

プログラムの説明

使用する前の準備

このプログラムにはいくつか数値を事前に設定しておく必要があります。fig5のオレンジ色の枠で囲んだところの切り取る座標はimshowで見ながら決めていきます。判定のしきい値は行目のコードで出した数値のうち低いほうを採用します。

ライブラリのインポート

以下のライブラリをインポートします。

import cv2
import numpy as np
import time
import math

mathモジュールは実際に動かすときは使いませんが数値の準備に使用します。

OBSとの接続

#OBSの設定
from obswebsocket import obsws,requests

host = 'localhost'
port = 4455
password = "secret"

ws = obsws(host, port, password)
ws.connect()

obswebsocketをインポートします。
hostとportとpasswordはOBSのツールタブから確認できます。

仮想カメラの読み込み

# カメラの読込み
cap = cv2.VideoCapture(5)

cv2.VideoCaptureメソッドで仮想カメラを読み込みます。カメラIDはしらみつぶしに0からさがします。0は内臓のwebカメラが割り当てられていることが多いです。

import cv2
import numpy as np
import time
import math
from obswebsocket import obsws,requests

#シーンのリストの取得
host = 'localhost'
port = 4455
password = "secret"

ws = obsws(host, port, password)
ws.connect()

#シーンの取得
scenes = ws.call(requests.GetSceneList())
print(scenes)

ws.disconnect()

i = 0

#カメラを探す
while( i<=10):
    cap = cv2.VideoCapture(i)
    frag = cap.isOpened()
    print("ID: %d is"% i, frag)
    i += 1
    if i == 11 :
            break 
#True のiDに変えて確認

別のファイルにこんな感じのコードを用意して実行するとシーンの取得やカメラを探すのがちょっと簡単になります。

変数の宣言

top = 180
bottom= 210
deck1_left= 250
deck1_right= 300
deck2_left =600
deck2_right =640

switch = 0#切り替え用の変数


各変数は画面を切り取るのに使用します。これはimshowで確認しながら決めていきます。switchはシーンを切り替えるのに使います。

master_ditect関数

def master_ditect(img):
    """画像からオレンジ色を検出

    Args:
        img (配列): 画像の配列データ

    Returns:
        配列: マスクの配列データ(二値画像)
    """
    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    hsv_min = np.array([0, 100, 10])
    hsv_max = np.array([50, 255, 255])
    mask_img = cv2.inRange(img_hsv, hsv_min, hsv_max)
    return mask_img

この関数は画像データからマスターのオレンジ色を検出するための関数です。RGB画像をHSVに変換して、オレンジ色の箇所のみマスクで取り出します。こうすることでオレンジ色は1,それ以外は0の二値画像データを取得することができます。図で表すと下のような処理を行っています。

これを
こう


frame_average関数

def frame_average(img):
    """二値画像の平均値を出して30以上ならTrueを返す
    Args:
        img (配列): 画像の配列データ

    Returns:
        ブール値
    """
    imgavg = img.flatten().mean()
    if imgavg >= 13:
        return True
    else:
        return False

二値画像データは二次元のNumpy配列です。それを扱いやすいようにflattenメソッドで一次元の配列データにして、配列の平均値を求めます。二値画像データなので白い領域(=1)が大きいほどこの値は大きくなります。

このとき平均値が13より大きければTrue,小さければFalseを返します。先述のmaster_ditect関数と組み合わせてオレンジ色が濃ければTrue,薄ければFalseを返します。これでマスターがついてるかどうかを判定します。

フレームの切り取り

 #フレームの切り取り
    deck1_frame = frame[top:bottom, deck1_left:deck1_right]
    deck2_frame = frame[top:bottom, deck2_left:deck2_right]

ここではフレームをDECKごとに切り取る処理をしています。切り取ったデータは変数deckn_frameに格納します。

色の検出

    #マスタートラックの色検出
    # mask = master_ditect(frame)
    mask1 = master_ditect(deck1_frame)
    mask2 = master_ditect(deck2_frame)
    avg1 = frame_average(mask1)
    avg2 = frame_average(mask2)
    

deckn_frameをmaster_ditect関数とframe_average関数で処理します。これでdeckが点灯すればTrueが返ってくるようになります。

DECKの切り替え

    #deck切り替え用
    if avg1 ==False and avg2 == False:
        switch = 0
        
    elif avg1 ==True and avg2 == False:
        switch = 1
        ws.call(requests.SetCurrentProgramScene(sceneName="DECK1"))
    elif avg1 ==False and avg2 == True:
        switch = 2
        ws.call(requests.SetCurrentProgramScene(sceneName="DECK2"))
    elif avg1 ==True and avg2 == True:
        switch = 0

if文でどのシーンにするかを決めています。変数switchはターミナルでモニターするときにprint関数で出力します。rekordboxの初期状態はどちらもマスターが点灯しておらず灰色になっています。

この状態のときavg1もavg2もFalseとなっています。switchには0を入れてシーンの切り替えは行いません。また、通常では起こりえない挙動ですがどちらもTrueになった場合も同様にシーンの切り替えは行いません。avg1,2のいずれかがTrueのとき、websocketのcallメソッドのSetCurrentProgramSceneを使って現在のシーンを対応するものに切り替えます。引数はsceneName ="シーン名"とします。


結果の表示

 # # 結果の表示
    # cv2.imshow("m1",deck1_frame)
    # cv2.imshow("m2",deck2_frame)
    # a1 = mask1.flatten().mean()
    # a2 = mask2.flatten().mean()
    # print(a1,a2)
    # print(switch)

ここでは動作確認に必要なコードが書かれています。通常はコメントアウトしておきます。3~5行目はしきい値を決めるのに使います。a1,a2どちらか一方の小さい方を設定してあげるとうまくいきます。

処理の終わり

   time.sleep(5)
    
    # qキーでループを抜ける
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 終了処理
cap.release()
ws.disconnect()
cv2.destroyAllWindows()

timeメソッドで5秒の待機時間を設けます。5秒くらいがPCの負荷とスムーズな切り替えのちょうどいいところだと思います。break文でループを抜けたらOpenCVとOBSとの接続を切ります。


コード全体

import cv2
import numpy as np
import time
import math

#OBSの設定
from obswebsocket import obsws,requests

host = 'localhost'
port = 4455
password = "secret"

ws = obsws(host, port, password)
ws.connect()

# scenes = ws.call(requests.GetSceneList())
# print(scenes)

# カメラの読込み
# 内蔵カメラがある場合、下記引数の数字を変更する必要あり
cap = cv2.VideoCapture(5)

# height =cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
# width =cap.get(cv2.CAP_PROP_FRAME_WIDTH)
top = 180
bottom= 210
deck1_left= 250
deck1_right= 300
deck2_left =600
deck2_right =640

t_0 = time.time()
i = 0#カウント変数

switch = 0#切り替え用の変数



def master_ditect(img):
    """画像からオレンジ色を検出

    Args:
        img (配列): 画像の配列データ

    Returns:
        配列: マスクの配列データ(二値画像)
    """
    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    hsv_min = np.array([0, 100, 10])
    hsv_max = np.array([50, 255, 255])
    mask_img = cv2.inRange(img_hsv, hsv_min, hsv_max)
    return mask_img


def frame_average(img):
    """二値画像の平均値を出して30以上ならTrueを返す
    Args:
        img (配列): 画像の配列データ

    Returns:
        ブール値
    """
    imgavg = img.flatten().mean()
    if imgavg >= 13:
        return True
    else:
        return False
    

while(cap.isOpened()):
    # 1フレームの読み込み
    ret, frame = cap.read()
     #フレームの切り取り
    deck1_frame = frame[top:bottom, deck1_left:deck1_right]
    deck2_frame = frame[top:bottom, deck2_left:deck2_right]
     
    #マスタートラックの色検出
    # mask = master_ditect(frame)
    mask1 = master_ditect(deck1_frame)
    mask2 = master_ditect(deck2_frame)
    avg1 = frame_average(mask1)
    avg2 = frame_average(mask2)
    

    
    #deck切り替え用
    if avg1 ==False and avg2 == False:
        switch = 0
        
    elif avg1 ==True and avg2 == False:
        switch = 1
        ws.call(requests.SetCurrentProgramScene(sceneName="DECK1"))
    elif avg1 ==False and avg2 == True:
        switch = 2
        ws.call(requests.SetCurrentProgramScene(sceneName="DECK2"))
    elif avg1 ==True and avg2 == True:
        switch = 0
    
    # # 結果の表示
    # cv2.imshow("m1",deck1_frame)
    # cv2.imshow("m2",deck2_frame)
    # a1 = mask1.flatten().mean()
    # a2 = mask2.flatten().mean()
    # print(a1,a2)
    # print(switch)
    time.sleep(5)
    
    # qキーでループを抜ける
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 終了処理
cap.release()
ws.disconnect()
cv2.destroyAllWindows()

#####参考にしたサイト#####

 #OBS websocketの使い方
#obs-websocket-pyの使い方ほか - Mokerの徒然日記2.0
#https://mokerdiary.hatenablog.com/entry/2019/09/03/000000

#OBSとrekordboxの連携
#rekordbox+OBSでトラックタイトルの表示を自動切り替えする ...
#https://flyan495.tumblr.com/post/635969448088567808/rekordbox-obs%E3%81%A7%E3%83%88%E3%83%A9%E3%83%83%E3%82%AF%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%82%92%E8%87%AA%E5%8B%95%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88%E3%81%99%E3%82%8Bpython

#仮想カメラの読み込み
#【 Python OpenCV 】カメラで撮影した動画を、リアルタイムで ...
#https://zero-cheese.com/8946/


動作結果

まとめ

なかなかにぐちゃぐちゃなコードですがとりあえず動くものができました。
じつはプログラミングは専門外で大学の授業でC++をやらされたくらいしか経験がありません。pythonも独学で色々なサイトや動画を見て学びました。
自分と同じような悩みを持っている方に少しでも力になれれば幸いです。

参考文献

このコードを作成するにあたって参考にしたサイトです。この場を借りてお礼申し上げます。


obs-websocket-pyの使い方ほか - Mokerの徒然日記2.0

rekordbox+OBSでトラックタイトルの表示を自動切り替えする@Python - log.flyan.net

【 Python OpenCV 】カメラで撮影した動画を、リアルタイムで表示させる – Windows, Mac, Linux 対応 –|Zero-Cheese


この記事が気に入ったらサポートをしてみませんか?