OpenCV cv2.imwrite で発生する「_img.empty()」エラーと「動画安定化」による解決法

こんにちは!
画像処理や動画解析の現場で広く利用されている OpenCV。
しかし実務で動画処理を行っていると、時折以下のようなエラーに遭遇することがあります。
cv2.error: OpenCV(4.11.0) /io/opencv/modules/imgcodecs/src/loadsave.cpp:929:
error: (-215:Assertion failed) !_img.empty() in function 'imwrite'
このエラーは、cv2.imwrite()
に渡された画像が空(None またはサイズ0) の場合に発生します。 一見単純に見える問題ですが、背後には「入力動画の不安定さ」や「並列処理の競合」といった要因が潜んでいることが少なくありません。
本記事では、このエラーの発生原因を掘り下げ、実務で効果のある解決策として 「動画の安定化(正規化)」 を紹介します。
TL;DR
OpenCVの動画処理で頻発する !_img.empty()
エラーは、入力動画(mp4)の不安定さが根本原因であることがけっこう多い
不安定な動画→安定化するためのffmpegコマンドは
ffmpeg で動画を安定化
ffmpeg -y -fflags +genpts -i INPUT.mp4 \
-vf "fps=30,format=yuv420p" \
-c:v libx264 -preset slow -crf 20 -g 90 -sc_threshold 0 \
-movflags +faststart -an OUTPUT_stable.mp4
最終手段
それでもダメなら、 連番画像に展開してから処理
目次
- なぜ「空画像」が紛れ込むのか?
- エラーを防ぐ基本的な工夫
- 動画を「安定化」してから処理する
- さらに堅牢にするレシピ
- 実務で役立つ環境上の注意点
- デバッグ方法
- まとめ
なぜ動画に「空画像」が紛れ込むのか?
1. 入力動画の不安定さ
- 可変フレームレート(VFR) 一部のカメラやスマートフォンは VFR で撮影しており、処理パイプラインによっては「抜けフレーム」や「タイムスタンプ不整合」が発生します。
- 特殊なピクセルフォーマット 10bit、4:2:2、4:4:4 など、標準的でないフォーマットはライブラリによって扱いが不安定。
- GOP 構造の揺らぎ I/P/B フレームの参照構造が複雑な動画は、特定フレームだけ取り出しに失敗するケースがあります。
2. 中間処理での破損
- 一時的に PNG などへ展開して並列処理を行うと、I/O が競合し
libpng error: IDAT: CRC error
が出ることがあります - たとえば、 Windows + WSL 環境で
/mnt/c
を経由すると、I/O レイテンシが大きく、破損の可能性が増したりします
3. アルゴリズム側の仕様
- 顔検出やセグメンテーションなどの処理で「対象が見つからない場合に None を返す」実装になっていると、そのまま
imwrite
に渡されて落ちます。
4. 環境要因
- ディスクの空き容量不足やメモリ不足による一時的なフレーム生成失敗。
- 並列度が高すぎて一時ファイルの読み書きが衝突するケース。
エラーを防ぐ基本的な工夫
imwrite 前のチェック
まずは cv2.imwrite
を呼ぶ前に、画像が空でないか確認するのが基本です。
if img is None or getattr(img, "size", 0) == 0:
# 空の場合はスキップやフォールバック処理を行う
continue
cv2.imwrite(path, img)
安全なラッパー関数の例
リトライやフォールバックコピーを組み合わせるとさらに堅牢になります。
import cv2, os, shutil, time
def safe_imwrite(path, img, retries=2, sleep=0.05):
if img is None or getattr(img, "size", 0) == 0:
return False
for _ in range(retries + 1):
try:
if cv2.imwrite(path, img):
return True
except cv2.error:
pass
time.sleep(sleep)
return False
def write_with_fallback(out_path, img, src_path_for_fallback=None):
os.makedirs(os.path.dirname(out_path), exist_ok=True)
if safe_imwrite(out_path, img):
return True
if src_path_for_fallback and os.path.exists(src_path_for_fallback):
shutil.copy2(src_path_for_fallback, out_path)
return True
return False
動画を「安定化」してから処理する
根本的な解決策として有効なのが、
入力動画を事前に正規化(安定化)すること
です。
これがけっこう効きます
具体的には以下のような ffmpeg コマンドを利用します。
標準的な安定化(最初に試すべきレシピ)
ffmpeg -y -fflags +genpts -i INPUT.mp4 \
-vf "fps=30,format=yuv420p" \
-c:v libx264 -preset slow -crf 20 -g 90 -sc_threshold 0 \
-movflags +faststart \
-an \
OUTPUT_stable.mp4
オプションの詳細説明
オプション | 説明 | 効果 |
---|---|---|
-fflags +genpts |
タイムスタンプ生成 | 欠損したPTSを補完 |
-vf fps=30 |
FPS固定 | VFR→CFR変換で安定化 |
-vf format=yuv420p |
ピクセルフォーマット統一 | 最も互換性の高い8bit 4:2:0に |
-g 90 |
GOPサイズ指定 | 30fpsなら3秒ごとにIフレーム |
-sc_threshold 0 |
シーンチェンジ無効化 | GOP構造の完全な安定化 |
-crf 20 |
品質設定 | 品質とファイルサイズのバランス |
-movflags +faststart |
メタデータ配置最適化 | ストリーミング再生の高速化 |
-an |
音声除去 | 映像処理に特化 |
さらに堅牢にするレシピ
I フレームのみ(All-I)
ffmpeg -i INPUT.mp4 \
-vf "fps=30,format=yuv420p" \
-c:v libx264 -crf 18 -g 1 \
-an \
OUTPUT_allI.mp4
→ すべて I フレーム化するため、ランダムアクセス時の不具合が激減。ただしファイルサイズは増加。
ProRes 422HQ(中間コーデック)
ffmpeg -i INPUT.mp4 \
-vf "fps=30,format=yuv422p10le" \
-c:v prores_ks -profile:v 3 \
-an \
OUTPUT_prores422hq.mov
→ 編集用途でもよく使われる高品質・高安定コーデック。
MJPEG(軽量で堅牢)
ffmpeg -i INPUT.mp4 \
-vf "fps=30,format=yuvj420p" \
-c:v mjpeg -q:v 3 \
-an \
OUTPUT_mjpeg.avi
→ I フレームのみ、デコードも軽い。検証・実験用に便利。
画像連番に展開(最強の安定策)
mkdir -p frames
ffmpeg -i INPUT.mp4 -vf "fps=30" -qscale:v 2 frames/%06d.jpg
→ PNG より JPG の方が CRC エラーを避けやすく、大量並列処理に強い。
実務で役立つ環境上の注意点
- WSL を使う場合 一時ファイルは
/mnt/c
ではなく/home/...
側(ext4)に置く方が安定。 - ディスクの空き容量
df -h
で確認し、余裕を持たせる。容量不足は破損の温床。 - 並列度の調整 ワーカー数を減らすと I/O 衝突や PNG CRC エラーを減らせる。
- "未検出=原画返し"の仕様に統一 処理関数が None を返さず、検出できなければ元のフレームを返す設計にする。
デバッグ方法
問題の切り分けには、どのフレームで失敗しているかを特定することが重要です。
フレーム読み込みの検証
def debug_frame_read(video_path):
"""どのフレームで読み込みに失敗しているか特定"""
cap = cv2.VideoCapture(video_path)
frame_count = 0
failed_frames = []
while True:
ret, frame = cap.read()
if not ret:
break
if frame is None or frame.size == 0:
failed_frames.append(frame_count)
print(f"Frame {frame_count}: Empty or corrupted")
frame_count += 1
cap.release()
if failed_frames:
print(f"\n問題のあるフレーム数: {len(failed_frames)}")
print(f"最初の10個: {failed_frames[:10]}")
else:
print(f"全 {frame_count} フレーム正常に読み込み可能")
return failed_frames
動画の整合性チェック
def verify_video_integrity(video_path):
"""動画ファイルの基本情報と整合性を確認"""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"エラー: {video_path} を開けません")
return False
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"動画情報:")
print(f" FPS: {fps}")
print(f" フレーム数: {frame_count}")
print(f" 解像度: {width}x{height}")
# 実際に読み込めるフレーム数をカウント
actual_count = 0
while True:
ret, _ = cap.read()
if not ret:
break
actual_count += 1
cap.release()
if actual_count != frame_count:
print(f"警告: メタデータのフレーム数({frame_count})と")
print(f" 実際のフレーム数({actual_count})が一致しません")
return False
return True
まとめ
cv2.imwrite
の_img.empty()
エラーは 空画像が渡された結果 であり、背景には 入力動画の不安定さや I/O の揺らぎ がある。- 解決にはの二段構えが効果的。
- ガード処理を入れる(空画像をスキップ or フォールバック)
- 動画を ffmpeg で安定化する(CFR化・ピクセルフォーマット統一・GOP安定化)
- さらに堅牢化するなら、All-I / ProRes / MJPEG / 画像連番 といった手段も検討可能。
- デバッグ時は、問題のあるフレームを特定することで原因の切り分けが容易になる。
こうした工夫により、実務の動画処理パイプラインはより堅牢で信頼性の高いものになります。
なにはともあれ、
まずは後段で泣かないために入力となる動画の品質をあげましょう!
それでは、お読みいただきありがとうございました!