Pythonを使用したビデオ内の深さに配慮したオブジェクトの挿入

Pythonを使用したビデオ内の深さに配慮したオブジェクトの挿入

Pythonを使用した深度認識法を用いたビデオ内に3Dモデルを配置する手順

著者による画像

コンピュータビジョンの分野では、ビデオ内の深度とカメラポーズの一貫した推定が、深度認識オブジェクトの挿入などのより高度な操作に基盤を提供しています。これらの基本的な技術を探求した前回の記事を踏まえ、本記事では深度認識オブジェクトの挿入に焦点を当てています。Pythonベースの計算手法を用いて、深度とカメラの方向データに基づいて既存のビデオフレームにオブジェクトを追加する戦略を概説します。この手法は、編集されたビデオコンテンツのリアリティを高めるだけでなく、ビデオのポストプロダクションにおいて幅広い応用があります。

要約すると、この手法は次の2つの主要なステップから構成されます。まず、ビデオ内の一貫した深度とカメラの位置を推定し、次にメッシュオブジェクトをビデオフレームにオーバーレイします。オブジェクトがビデオの3D空間で静止して見えるようにするために、カメラの移動の反対方向に移動させます。この逆動作により、オブジェクトはビデオ全体で固定されているように見えます。

本記事を通じて参照するために、私のGitHubページでコードを確認することができます。

ステップ1: カメラポーズ行列の生成とビデオの一貫した深度推定

前回の記事では、ビデオの一貫した深度フレームと対応するカメラポーズ行列の推定方法を詳しく説明しました。

今回の記事では、著者が特に選んだ、軸に沿った明瞭なカメラ移動があるストリート上を歩く男性のビデオを使用しました。これにより、挿入されたオブジェクトがビデオの3D空間内で固定された位置を保持しているかどうかを明確に評価することができます。

前回の記事で説明したすべての手順に従って、深度フレームと推定カメラポーズ行列を取得しました。特に、COLMAPによって生成された「custom.matrices.txt」というファイルが必要です。

下記に元の映像とその推定深度ビデオを示します。

(左)Videvo提供のストック映像、www.videvo.netからダウンロード | (右)著者による推定深度ビデオ

最初のフレームに対応するポイントクラウドの可視化を以下に示します。白いギャップは、前景オブジェクトの存在によりカメラの視界から遮られる影領域を示しています。

ビデオの最初のフレームの生成されたポイントクラウド

ステップ2: 挿入するメッシュファイルの選択

次に、ビデオシーケンスに挿入するメッシュファイルを選択します。Sketchfab.comやGrabCAD.comなどのさまざまなプラットフォームでは、幅広い3Dモデルを選ぶことができます。

デモビデオでは、2つの3Dモデルを選びました。各モデルのリンクは以下の画像キャプションに提供されています。

(左)Abby Gancz提供の3Dモデル(CC BY 4.0)、www.sketchfab.comからダウンロード | (右)Renafox提供の3Dモデル(CC BY 4.0)、www.sketchfab.comからダウンロード

私はCloudCompareというオープンソースのツールを使用して、3Dモデルを前処理しました。これは3Dポイントクラウドの操作に使用されます。具体的には、オブジェクトから地面部分を除去して、ビデオへの統合を強化しました。このステップはオプションですが、特定の3Dモデルの側面を変更する場合は、CloudCompareを強くお勧めします。

メッシュファイルの前処理が完了したら、.plyまたは.objファイルとして保存します。(すべての3Dモデルのファイル拡張子が、.stlなどのカラーメッシュをサポートしているわけではないことに注意してください)。

ステップ3:深度認識オブジェクト挿入でフレームを再レンダリングする

これでプロジェクトの中核部分であるビデオ処理に到達します。私のリポジトリでは、2つの重要なスクリプトが提供されています – video_processing_utils.pydepth_aware_object_insertion.py。その名前から推測できるように、video_processing_utils.pyにはオブジェクト挿入のためのすべての重要な関数が含まれており、depth_aware_object_insertion.pyはこれらの関数をループ内の各ビデオフレームに対して実行する主要なスクリプトとして機能します。

depth_aware_object_insertion.pyのメインセクションの抜粋バージョンを以下に示します。入力ビデオのフレーム数だけ実行されるforループ内で、深度計算パイプラインのバッチ情報をロードし、元のRGBフレームとその深度推定値を取得します。その後、カメラポーズ行列の逆行列を計算します。次に、メッシュ、深度、カメラの内部パラメータをrender_mesh_with_depth()という関数に入力します。

for i in tqdm(range(batch_count)):    batch = np.load(os.path.join(BATCH_DIRECTORY, file_names[i]))    # ...(簡潔にするために省略)    # 逆カメラ外部パラメータを使用してメッシュを変換    frame_transformation = np.vstack(np.split(extrinsics_data[i],4))    inverse_frame_transformation = np.empty((4, 4))    inverse_frame_transformation[:3, :] = np.concatenate((np.linalg.inv(frame_transformation[:3,:3]),                                                            np.expand_dims(-1 * frame_transformation[:3,3],0).T), axi    inverse_frame_transformation[3, :] = [0.00, 0.00, 0.00, 1.00]    mesh.transform(inverse_frame_transformation)    # ...(簡潔にするために省略)    image = np.transpose(batch['img_1'], (2, 3, 1, 0))[:,:,:,0]        depth = np.transpose(batch['depth'], (2, 3, 1, 0))[:,:,0,0]    # ...(簡潔にするために省略)    # 変換されたメッシュのカラーと深度バッファをイメージドメインでレンダリング    mesh_color_buffer, mesh_depth_buffer = render_mesh_with_depth(np.array(mesh.vertices),                                                                   np.array(mesh.vertex_colors),                                                                   np.array(mesh.triangles),                                                                   depth, intrinsics)            # メッシュと元の画像の深度認識オーバーレイ    combined_frame, combined_depth = combine_frames(image, mesh_color_buffer, depth, mesh_depth_buffer)     # ...(簡潔にするために省略)

render_mesh_with_depth関数は、頂点、頂点カラー、および三角形で表される3Dメッシュを2D深度フレームにレンダリングします。この関数は、レンダリング出力を保持するために深度とカラーバッファを初期化します。その後、カメラ内部パラメータを使用して3Dメッシュの頂点を2Dフレームに投影します。関数はスキャンラインレンダリングを使用して、メッシュ内の各三角形をループ処理し、2Dフレーム上のピクセルにラスタライズします。このプロセス中、関数は各ピクセルのバリセントリック座標を計算して、深度とカラー値を補間します。これらの補間値は、ピクセルの補間深度が深度バッファ内の既存の値よりもカメラに近い場合にのみ、深度とカラーバッファを更新するために使用されます。最後に、関数はカラーバッファをuint8形式に変換して、画像表示に適した形式でレンダリングされた出力として返します。

def render_mesh_with_depth(mesh_vertices, vertex_colors, triangles, depth_frame, intrinsic):    vertex_colors = np.asarray(vertex_colors)        # 深度とカラーバッファを初期化    buffer_width, buffer_height = depth_frame.shape[1], depth_frame.shape[0]    mesh_depth_buffer = np.ones((buffer_height, buffer_width)) * np.inf        # 3D頂点を2D画像座標に変換    vertices_homogeneous = np.hstack((mesh_vertices, np.ones((mesh_vertices.shape[0], 1))))    camera_coords = vertices_homogeneous.T[:-1,:]    projected_vertices = intrinsic @ camera_coords    projected_vertices /= projected_vertices[2, :]    projected_vertices = projected_vertices[:2, :].T.astype(int)    depths = camera_coords[2, :]    mesh_color_buffer = np.zeros((buffer_height, buffer_width, 3), dtype=np.float32)        # 各三角形をレンダリングするためにループ処理    for triangle in triangles:        # 三角形の頂点に対する2Dポイントと深度を取得        points_2d = np.array([projected_vertices[v] for v in triangle])        triangle_depths = [depths[v] for v in triangle]        colors = np.array([vertex_colors[v] for v in triangle])                # スキャンラインレンダリングのためにy座標で頂点をソート        order = np.argsort(points_2d[:, 1])        points_2d = points_2d[order]        triangle_depths = np.array(triangle_depths)[order]        colors = colors[order]        y_mid = points_2d[1, 1]        for y in range(points_2d[0, 1], points_2d[2, 1] + 1):            if y < 0 or y >= buffer_height:                continue                        # 現在のスキャンラインの開始および終了x座標を決定            if y < y_mid:                x_start = interpolate_values(y, points_2d[0, 1], points_2d[1, 1], points_2d[0, 0], points_2d[1, 0])                x_end = interpolate_values(y, points_2d[0, 1], points_2d[2, 1], points_2d[0, 0], points_2d[2, 0])            else:                x_start = interpolate_values(y, points_2d[1, 1], points_2d[2, 1], points_2d[1, 0], points_2d[2, 0])                x_end = interpolate_values(y, points_2d[0, 1], points_2d[2, 1], points_2d[0, 0], points_2d[2, 0])                        x_start, x_end = int(x_start), int(x_end)            # スキャンライン内の各ピクセルをループ処理            for x in range(x_start, x_end + 1):                if x < 0 or x >= buffer_width:                    continue                                # ピクセルのバリセントリック座標を計算                s, t, u

変換されたメッシュのカラーバッファとデプスバッファは、元のRGB画像と推定されたデプスマップとともにcombine_frames()関数に入力されます。この関数は、2つのセットの画像とデプスフレームを結合するために設計されています。深度情報を使用して、元のフレームのどのピクセルがレンダリングされたメッシュフレームの対応するピクセルで置き換えられるべきかを決定します。具体的には、各ピクセルについて、関数はレンダリングされたメッシュの深度値が元のシーンの深度値よりも小さいかどうかをチェックします。もし小さい場合、そのピクセルはレンダリングされたメッシュフレームでカメラに「近い」と見なされ、カラーフレームとデプスフレームの両方のピクセル値がそれに応じて置き換えられます。この関数は、結合されたカラーフレームとデプスフレームを返し、深度情報に基づいてレンダリングされたメッシュを元のシーンにオーバーレイします。

# 深度情報に基づいて元のフレームとメッシュでレンダリングされたフレームを結合するdef combine_frames(original_frame, rendered_mesh_img, original_depth_frame, mesh_depth_buffer):    # メッシュが元の深度よりも近い位置にあるマスクを作成する    mesh_mask = mesh_depth_buffer < original_depth_frame        # 結合されたフレームを初期化する    combined_frame = original_frame.copy()    combined_depth = original_depth_frame.copy()        # マスクがTrueの場所でメッシュの情報を結合されたフレームに更新する    combined_frame[mesh_mask] = rendered_mesh_img[mesh_mask]    combined_depth[mesh_mask] = mesh_depth_buffer[mesh_mask]        return combined_frame, combined_depth

以下は、mesh_color_buffermesh_depth_buffer、およびcombined_frameが最初のオブジェクトである象のように見える方法です。象のオブジェクトはフレーム内の他の要素によって隠されないため、完全に表示されます。異なる配置では、遮蔽が発生します。

(左)象のメッシュの計算されたカラーバッファ | (右)象のメッシュの計算されたデプスバッファ | (下)結合されたフレーム

それに応じて、2番目のメッシュである車を道路の路肩に配置しました。また、それがそこに駐車されているように見えるように、初期の向きも調整しました。以下のビジュアルは、このメッシュの対応するmesh_color_buffermesh_depth_buffer、およびcombined_frameです。

(左)車のメッシュの計算されたカラーバッファ | (右)車のメッシュの計算されたデプスバッファ | (下)結合されたフレーム

新しいオブジェクトにより発生した新しい遮蔽領域により、点群の可視化ではより多くの白い隙間が導入されます。

最初のフレームに挿入されたオブジェクトの生成されたポイントクラウド

各ビデオフレームのオーバーレイ画像を計算した後、ビデオをレンダリングする準備が整いました。

ステップ4:処理されたフレームからビデオをレンダリングする

depth_aware_object_insertion.pyの最後のセクションでは、render_video_from_frames関数を使用してオブジェクトが挿入されたフレームからビデオを単純にレンダリングします。このステップで出力ビデオのfpsも調整できます。以下にコードを示します:

video_name = 'depth_aware_object_insertion_demo.mp4'save_directory = "depth_aware_object_insertion_demo/"frame_directory = "depth_aware_object_insertion_demo/"image_extension = ".png"fps = 15 # オーバーレイされたフレームのビデオをレンダリングするrender_video_from_frames(frame_directory, image_extension, save_directory, video_name, fps)

こちらは私のデモ動画です:

(左)Videvoから提供されたストック映像、www.videvo.netからダウンロード | (右)2つのオブジェクトが挿入されたストック映像

このアニメーションの高解像度版はYouTubeにアップロードされています。

全体的に、オブジェクトの整合性はよく保たれています。たとえば、シーン中の街灯のポールによって車のオブジェクトが説得力を持って隠されています。ビデオ全体で車の位置にわずかな揺れが感じられますが、おそらくカメラの位置推定の不完全さによるものです。ワールドロックのメカニズムは一般的にデモンストレーションビデオで期待される通りに機能します。

ビデオ内へのオブジェクトの挿入という概念は新しいものではありませんが、After Effectsなどの既存のツールが特徴追跡に基づく手法を提供しているため、これらの従来の手法はビデオ編集ツールに慣れていない人々にとって非常に困難で費用がかかることがよくあります。ここで、Pythonベースのアルゴリズムの可能性が発揮されるのです。機械学習と基本的なプログラミング構造を活用することで、これらのアルゴリズムは、限られた経験を持つ個人でも高度なビデオ編集タスクを民主化する可能性があります。したがって、技術が進化し続ける中、ソフトウェアベースの手法は強力なエンエーブラーとして機能し、ビデオ編集における創造的な表現の新たな可能性を開拓するでしょう。

素晴らしい一日をお過ごしください!

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

機械学習

もし芸術が私たちの人間性を表現する方法であるなら、人工知能はどこに適合するのでしょうか?

MITのポストドクターであるジヴ・エプスタイン氏(SM '19、PhD '23)は、芸術やその他のメディアを作成するために生成的AIを...

データサイエンス

「Seerの最高データオフィサーであるDr. Serafim Batzoglouによるインタビューシリーズ」

セラフィム・バツォグルはSeerのチーフデータオフィサーですSeerに加わる前は、セラフィムはInsitroのチーフデータオフィサー...

人工知能

Aaron Lee、Smith.aiの共同設立者兼CEO - インタビューシリーズ

アーロン・リーさんは、Smith.aiの共同創業者兼CEOであり、AIと人間の知性を組み合わせて、24時間365日の顧客エンゲージメン...

人工知能

「コマンドバーの創設者兼CEO、ジェームズ・エバンスによるインタビューシリーズ」

ジェームズ・エバンズは、CommandBarの創設者兼CEOであり、製品、マーケティング、顧客チームを支援するために設計されたAIパ...

人工知能

「LeanTaaSの創設者兼CEO、モハン・ギリダラダスによるインタビューシリーズ」

モーハン・ギリダラダスは、AIを活用したSaaSベースのキャパシティ管理、スタッフ配置、患者フローのソフトウェアを提供する...

人工知能

「アナコンダのCEO兼共同創業者、ピーターウォングによるインタビューシリーズ」

ピーター・ワンはAnacondaのCEO兼共同創設者ですAnaconda(以前はContinuum Analyticsとして知られる)を設立する前は、ピー...