【画像処理】「AttributeError: ‘NoneType’ has no attribute ‘shape’」とOpenCVの防弾化

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(防御的プログラミング)について書かれた技術書は、データサイエンティストにとっても非常に有用なバイブルとなります。

コメント

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