OpenCVを使って何千枚もの画像データを一気にリサイズして、学習用データセットを作ろう!for img_path in file_list: のループを回し、進捗バーが順調に進むのをコーヒーを飲みながら眺めていると、進捗95%のところで突如プロセスが完全停止し、残酷なエラーがターミナルに出力されます。
AttributeError: 'NoneType' object has no attribute 'shape'
「えっ!?なんでNoneなの?フォルダには間違いなく画像ファイルが入っているのに!」
本記事では、世界中で最も多くのプログラマーを発狂させてきたであろうOpenCV特有の「サイレント・フェイラー(無言の失敗)」という欠陥仕様と、実運用に耐えうる「絶対に落ちない(防弾化された)画像処理パイプライン」の構築方法を解説します。
1. すべての元凶は cv2.imread() の恐るべき仕様
Pythonの常識では、例えば open('存在しないファイル.txt') と実行すれば、瞬時に FileNotFoundError という例外(エラー)が発生し、プログラムはその場で停止します。
どこのパスが間違っていたのか一目瞭然ですね。
ところが、OpenCVの根幹である画像読み込み関数 cv2.imread() は、C++のレガシーな設計思想を引き継いでおり、Pythonの例外機構を一切使用しません。
ファイルが存在しなかったり、画像データが破損していて開けなかった場合でも、「エラーを出さずに、ただ黙って Pythonの None(空っぽ)を返す」という、極めて初心者殺しの仕様になっています。
Noneが返ってきたことに気づかないまま、次の行で画像の高さや幅を取得しようと h, w = img.shape を実行すると、「空っぽの何もない物体(NoneType)に対して .shape という属性を要求するな!」とPythonのインタープリタが逆ギレし、あのエラーが発生するわけです。
import cv2
# 本当は "dataset/" に入っているのに、パスを間違えたとする
img_path = "data/dog.jpg"
# OpenCVはパスが間違っていても絶対にエラーを吐かない。
# ただ不気味な微笑みを浮かべて img に None を代入する。
img = cv2.imread(img_path)
# ここに来てようやく大爆発する!
# OpenCVではなく、Pythonから「NoneTypeにshapeはないぞ!」と怒られる
h, w, c = img.shape # => AttributeError !!2. 日本語パスという最凶のトラップ
さらに悲惨なことに、パスが100%正しく、画像が間違いなく存在していても None が返ってくるケースがあります。
それが、Windows環境などで「ファイルのパス(フォルダ名など)に日本語が含まれている現象」です。
OpenCVの標準の imread は、Shift-JISなどのマルチバイト文字(日本語のパス)を正しく解釈できず、内部で「ファイルが見つからない」と判断し、またしても黙って None を返します。
(例: C:\Users\太郎\画像フォルダ\犬.jpg)
3. 防弾仕様(Bulletproof)の自作関数でパイプラインを守る
プロダクション(業務開発やAPIサーバーなど)において、このようなサイレントな失敗はあってはならない大罪です。
この問題を恒久的に解決するためには、生の cv2.imread() を直接叩くことを直ちにやめ、「日本語パスに対応」かつ「読み込み失敗時には明確にエラーを投げる(フェイルファスト)」自作のラッパー関数を作るのがベストプラクティスです。
import cv2
import numpy as np
import os
from pathlib import Path
def safe_imread(file_path_str: str) -> np.ndarray:
"""
1. 日本語パスでも確実に読み込む
2. ファイル不在・破損時は即座に明示的な例外を投げる
最強の防弾仕様画像ローダー
"""
path = Path(file_path_str)
# そもそもファイルがOS上に存在するか?
if not path.is_file():
raise FileNotFoundError(f"[エラー] 画像ファイルが存在しません: {path}")
try:
# np.fromfile で生のバイナリとして読み込む(日本語パス回避の最強テク)
buffer = np.fromfile(str(path), np.uint8)
# バイナリデータからOpenCVの画像形式(BGR)にデコード
img = cv2.imdecode(buffer, cv2.IMREAD_COLOR)
# デコード失敗(画像ファイルが壊れている等)のチェック
if img is None:
raise ValueError(f"[エラー] 画像が破損しているか未対応形式です: {path}")
return img
except Exception as e:
raise RuntimeError(f"画像読み込み時の致命的エラー {path}: {str(e)}")
# ==================================
# 使用例
# ==================================
try:
img = safe_imread("全角フォルダ/猫の画像.png")
# ここに到達した時点で、img は 100% 完全な画像テンソルであることが保証される!
height, width = img.shape[:2]
except Exception as e:
# 壊れた画像があってもプロセス全体を落とさず、スキップして次に行ける
print(e)まとめ:エラーの「発生地点」を手前にズラす技術
NoneType アクセス・エラーの真の恐ろしさは、「本当の原因(読み込み失敗)」と「システムが落ちる地点(shape要求)」が離れていることにあります。
エラーは、システムの奥深くで爆発させるのではなく、データの入り口(入口の関数)で即座に検知して爆発させる「フェイルファスト(Fail Fast)」という思想が重要です。
Pythonを使った堅牢なコーディング(ガード節の実装など)や、品質を維持するための Defensive Programming(防御的プログラミング)について書かれた技術書は、データサイエンティストにとっても非常に有用なバイブルとなります。


コメント