OpenCVで誰もが一度は絶望する「Assertion failed (size.width>0)」を徹底解剖。動画フレーム消失の深淵と、プロフェッショナルが辿り着く堅牢な実装術

Python

さて、画像処理やコンピュータビジョンの世界に足を踏み入れた者が、必ずと言っていいほど遭遇し、そして数時間を無駄にする「呪いの呪文」があります。それがこれです。

cv2.error: OpenCV(4.x.x) /io/opencv/modules/imgproc/src/color.cpp:182: error: (-215:Assertion failed) !_src.empty() in function 'cvtColor'

または

Error: Assertion failed (size.width>0 && size.height>0)

このエラー、一見すると「あ、画像が読み込めなかったんだな」という単純なものに見えます。しかし、その裏側には、OSのファイル記述子の限界、ネットワークプロトコルの揺らぎ、ビデオコーディングの複雑な仕様、そしてC++とPythonのブリッジにおけるメモリ管理の絶妙なパワーバランスが隠されています。

本日は、この「サイレント・フレーム・ドロップ」がなぜ起こるのか、そしてプロフェッショナルな現場で求められる「絶対に落ちない動画処理パイプライン」の構築方法について、コンピュータサイエンスの深淵まで潜り込んで解説します。

1. そもそも「Assertion failed」とは何なのか?:C++層からの咆哮

まず、なぜPythonを使っているのに、こんな不親切なエラーメッセージが出るのかという点から解説します。OpenCVの本質は、高度に最適化されたC++ライブラリです。Pythonの import cv2 は、単なるラッパー(Wrapper)に過ぎません。

C++の世界において、assert(アサーション)は「この前提条件が満たされていないなら、これ以上処理を続けてもセグメンテーションフォールトを起こすか、デタラメな値を返すだけだ。だから今すぐプログラムを緊急停止させろ」という、開発者からの断固たる命令です。

size.width > 0 && size.height > 0 という条件は、OpenCVの cv::Mat(行列オブジェクト)が、物理的にメモリ領域を確保していない、つまり「空っぽ」であることを指しています。空っぽのメモリ領域に対して、グレースケール化(cvtColor)やリサイズ(resize)を行おうとすれば、プログラムは存在しないアドレスにアクセスしようとし、OSによってプロセスが殺されます。それを防ぐための「最後の防波堤」がこのエラーなのです。

歴史的背景:OpenCVの進化と「空のMat」

古のOpenCV 1.x系では、IplImage という構造体が使われていました。この時代、メモリ管理は手動で、プログラマが明示的に cvReleaseImage を呼ばなければメモリリークが発生していました。しかし、2.x系以降、cv::Mat というスマートポインタに近い挙動をするクラスが導入されました。これにより、参照カウンタによってメモリは自動管理されるようになりましたが、「宣言はされているがデータが入っていない状態」が許容されるようになりました。

動画処理において、cv2.VideoCaptureread() を呼び出した際、デコーダが何らかの理由でフレームをデコードできなかった場合、OpenCVは例外を投げません。代わりに False という戻り値と、空の Mat オブジェクトを返します。これをチェックせずに次の処理に渡すことが、このエラーの正体です。

2. なぜフレームは消失するのか?:物理層とデコーダの不都合な真実

「動画ファイルはあるのに、なぜフレームが取れないことがあるのか?」という疑問に答えましょう。ここにはコンピュータ・サイエンスにおける、複数のレイヤーでの「遅延と欠損」が関係しています。

(1) ビデオコーデックの構造:GOP(Group of Pictures)の罠

H.264やH.265といった現代のビデオコーデックは、すべてのフレームを画像として保持しているわけではありません。 – Iフレーム(Intra-coded): 単体で画像として完結している(キーフレーム)。 – Pフレーム(Predicted): 前のフレームとの差分情報。 – Bフレーム(Bi-directional predicted): 前後のフレームから予測される情報。

ストリーミング再生において、最初の数フレームが「Iフレーム」でない場合、デコーダは画像を描画できません。そのため、cap.read() は成功しているように見えて、中身が空のフレームを返すことがあります。これが「動画開始直後のクラッシュ」の主な原因です。

(2) ネットワーク・ジッターとパケットロス(RTSP/RTMPの場合)

監視カメラなどのIPカメラから映像を取得する場合、通信には主にUDPが使われます。UDPはパケットが届く保証がありません。パケットが一部欠損すると、デコーダ(FFmpeg等)はフレームを再構成できず、そのフレームを「ドロップ」します。OpenCVはこのドロップを「無」として扱い、Python側に空のオブジェクトを渡します。

(3) OSのバッファとハードウェア・アクセラレーション

GPUを使ってデコードしている場合、ビデオメモリ(VRAM)からメインメモリ(RAM)への転送に遅延が生じることがあります。また、OS側のファイル記述子が上限に達していたり、バックエンドのFFmpegがビジー状態だったりすると、サイレントに読み込みがスキップされることがあります。

3. 凡人が書くコード vs プロフェッショナルが書くコード

では、具体的なコードを見ていきましょう。まずは、ネット上のチュートリアルにありがちな、「いつか必ず死ぬコード」です。

import cv2

cap = cv2.VideoCapture("rtsp://admin:password@192.168.1.100/stream")

while True:
    ret, frame = cap.read()
    # retのチェックを怠っている、あるいは不十分
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # ここで爆死する
    cv2.imshow('frame', gray)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
cap.release()

このコードは、ネットワークが少しでも瞬断した瞬間に Assertion failed でプロセスが終了します。24時間稼働のシステムでこれを行えば、深夜に呼び出されること間違いなしです。

プロフェッショナルによる「防御的プログラミング」の実装

プロフェッショナルは、ret の戻り値だけでなく、frame の実体もダブルチェックし、さらに例外処理でラップします。また、ネットワークカメラの場合は「リコネクト(再接続)ロジック」を組み込むのが常識です。

import cv2
import time
import logging

def get_stream(url):
    cap = cv2.VideoCapture(url)
    if not cap.isOpened():
        logging.error("ストリームを開けませんでした")
        return None
    return cap

def main():
    url = "rtsp://example.com/live"
    cap = get_stream(url)
    
    retry_count = 0
    max_retries = 5

    while True:
        try:
            ret, frame = cap.read()

            # 1. 戻り値チェック
            if not ret:
                logging.warning("フレームが取得できません。再試行します...")
                cap.release()
                time.sleep(2)
                cap = get_stream(url)
                continue

            # 2. フレームの形状(空でないか)を物理的にチェック
            if frame is None or frame.size == 0:
                logging.error("空のフレームを検知しました。スキップします。")
                continue

            if frame.shape[0] == 0 or frame.shape[1] == 0:
                logging.error("無効な解像度のフレームです。")
                continue

            # 3. 本来の処理
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            
            # --- 処理の継続 ---

        except cv2.error as e:
            logging.error(f"OpenCV内部エラー: {e}")
            continue
        except Exception as e:
            logging.error(f"予期せぬエラー: {e}")
            break

    cap.release()

if __name__ == "__main__":
    main()

4. アーキテクチャレベルでの解決:Producer-Consumerパターンの導入

さらに高度なシステム(例えばAIによるリアルタイム物体検知など)では、メインスレッドで read()processing を交互に行うのは悪手です。処理が重くなると、OpenCVの内部バッファが溢れ、結果として古いフレームや壊れたフレームを掴まされることになるからです。

ここで登場するのが、コンピュータ・サイエンスの古典的パターンである「Producer-Consumer(生産者・消費者)パターン」です。

  • Producer(スレッド1): VideoCapture.read() だけをひたすら回し、最新のフレームを Queue に叩き込む。
  • Consumer(スレッド2): Queue からフレームを取り出し、重いAI処理や画像変換を行う。

この構成にすることで、万が一デコードに失敗しても、スレッド全体が停止することなく、次の正常なフレームを待つことが可能になります。

Pythonによるマルチスレッド・ビデオキャプチャの実装例

import cv2
import threading
from queue import Queue

class VideoStreamer:
    def __init__(self, src):
        self.cap = cv2.VideoCapture(src)
        self.queue = Queue(maxsize=128) # バッファサイズ
        self.stopped = False

    def start(self):
        t = threading.Thread(target=self.update, args=())
        t.daemon = True
        t.start()
        return self

    def update(self):
        while True:
            if self.stopped:
                return
            
            ret, frame = self.cap.read()
            
            if not ret or frame is None:
                continue # またはリコネクト処理

            if not self.queue.full():
                self.queue.put(frame)

    def read(self):
        return self.queue.get()

    def stop(self):
        self.stopped = True
        self.cap.release()

# 使い方
stream = VideoStreamer("video.mp4").start()
while True:
    frame = stream.read()
    # ここで重い処理をしても、キャプチャスレッドは止まらない
    cv2.imshow("Robust Stream", frame)
    if cv2.waitKey(1) == ord("q"):
        break

5. コンピュータ・サイエンスの視点:なぜ 0x0 の画像が存在し得るのか?

少し理論的な話をしましょう。画像データとは、メモリ上では単なる1次元または3次元の配列(Array)です。 Address = BaseAddress + (y * Width + x) * Channels

この数式を見てわかる通り、Width が 0 の場合、どのアドレスを参照しても BaseAddress に戻ってしまいます。これは数学的な特異点です。また、多くの画像処理アルゴリズム(特にフーリエ変換や畳み込み演算)は、データのサイズが2の累乗であることを期待したり、少なくとも0でないことを前提とした「ゼロ除算」を内包しています。

OpenCVのソースコード(C++層)を覗くと、CV_Assert(ssize.width > 0 && ssize.height > 0) という記述が至る所にあります。これは、SIMD(Single Instruction Multiple Data)命令セット(SSEやAVX)を使用して高速化を図る際、レジスタにデータを詰め込む前段階で、無効なメモリ空間を読み込んでCPUが例外を吐くのを防ぐための、低レイヤでの「防衛策」なのです。

6. まとめ:エンジニアとしての「品格」はエラー処理に宿る

Assertion failed (size.width>0 && size.height>0) というエラーは、単なるバグではありません。それは、「現実世界の不確実性(不安定なネットワーク、壊れたファイル、遅延するハードウェア)」と「厳密な数学的世界(行列演算、アルゴリズム)」の衝突地点なのです。

このエラーを単に try-except で握りつぶすのではなく、 – なぜフレームが欠落したのか?(ネットワークか、デコーダか) – 欠落した際にどうリカバリするか?(再接続か、スキップか) – 処理性能は追いついているか?(スレッド化が必要か) を思考できるかどうかが、初学者とプロフェッショナルの分水嶺となります。

次にこのエラーに遭遇したときは、舌打ちするのではなく、「おっと、デコーダとの対話が必要なようだな」と不敵に笑えるエンジニアになってください。あなたの書くコードが、より堅牢で、より美しいものになることを願っています。


今回のような「画像データ」の扱いから、さらに一歩進んで、それらを統計的に処理し、機械学習のパイプラインへと昇華させたいと考えている方には、こちらの書籍がバイブルとなります。OpenCVで得た「多次元配列」をどのように科学的に分析するか、そのすべての基礎が詰まっています。

特に第2版になり、最新のライブラリ環境にアップデートされた本書は、実務でデータサイエンスを扱う人間にとって、ググるよりも早く正確な答えに辿り着ける「地図」のような存在です。デスクに一冊置いておくだけで、解決できるエラーの質が変わるはずです。

コメント

タイトルとURLをコピーしました