ウェスタンブロット定量 — NumPyイメージ配列からバンド強度を再現性よく測定
このトピックを終えると
教科書で学んだ NumPy配列、相関、例外処理を組み合わせて、ウェスタンブロットゲル画像からバンド強度を自動で測定し、ローディングコントロールで正規化するツールを自分で作成できるようになります。ImageJのクリック操作をPythonスクリプトに置き換えることで、再現性を確保します。
この記事は、教育用一般的な例です。実際のデンシトメトリーでは、画像の自動バンド検出、背景補正、飽和検出など、より高度な処理が求められます。
「ImageJで100回クリック」—手作業による定量化の落とし穴
あなたが大学院3年次に最初にウェスタンブロッティングの定量化を学んだ方法は、おそらく以下のようなものでしょう。
- ImageJで画像を開く
- Rectangleツールでバンドを囲む
Analyze > Measureをクリック- 結果ウィンドウからIntDenの値(積分値)をコピー
- Excelに貼り付ける
- 次のバンド...を繰り返す
1つのゲルに8つのレーン×3つの抗体=24回。ゲルが10枚あれば240回。手作業なので、時間がかかり、間違いも起こりやすい。
この手法の実際の問題点:
問題1: 再現性。あなたが描いた長方形の位置が正確に記録されません。「このバンドの強度をどのように測定しましたか?」と査読者が質問した場合、答える方法がありません。
問題2: 背景補正の恣意性。背景をどこにするかによって、最終的な強度が大きく異なります。ImageJの背景除去オプションを毎回同じ値で使用するのは困難です。
問題3: 規模。論文の締め切り前日に、20枚のゲルの画像が追加された場合、手作業では対応できません。
真の解決策は、Pythonスクリプトで定量化パイプラインを作成することです。各レーンの位置を座標で明示し、バンド領域のピクセル値をnumpyで計算し、ローディングコントロールで正規化し、結果をCSVに保存します。完全に同じスクリプトで、完全に同じ結果を再現できます。
ブラックボックスからパーツへ
パーツ 1: 画像 = NumPy 2D配列
ゲル画像は、各ピクセルが明るさの値を持つ2次元配列です。Pythonでは、PILやimageioで読み込み、NumPy配列に変換します。
import numpy as npfrom PIL import Image
def load_blot_image(path: str) -> np.ndarray: img = Image.open(path).convert("L") # グレースケールに変換 return np.array(img)
image = load_blot_image("western_blot_01.png")print(image.shape) # 例: (400, 800) — 高さ400、幅800ピクセルprint(image.dtype) # uint8 — 0〜255の明るさ注意: 8ビット画像は、明るさ0〜255です。暗いバンドは低い値になります。デンスィトメトリーでは通常、反転させて使用します。つまり、明るさの代わりに密度 = 255 - 明るさを使用します。
density = 255 - imageパーツ 2: ROI(関心領域)の選択
バンドがある長方形の領域を座標で指定します。
def measure_band(density: np.ndarray, roi: tuple[int, int, int, int]) -> float: """ roi = (top, left, bottom, right) 返り値:領域内のピクセル密度の合計(積分密度) """ top, left, bottom, right = roi region = density[top:bottom, left:right] return float(region.sum())
band_intensity = measure_band(density, (100, 200, 130, 260))print(f"バンド強度: {band_intensity}")この関数は、ImageJのIntDen(積分密度)と同じ値を正確に計算します。
パーツ 3: 背景補正
バンドの近くにある背景領域も測定し、減算します。
def measure_band_with_background( density: np.ndarray, band_roi: tuple[int, int, int, int], bg_roi: tuple[int, int, int, int]) -> float: band = measure_band(density, band_roi) bg_area = (bg_roi[2] - bg_roi[0]) * (bg_roi[3] - bg_roi[1]) band_area = (band_roi[2] - band_roi[0]) * (band_roi[3] - band_roi[1]) bg_density_per_pixel = measure_band(density, bg_roi) / bg_area background_contribution = bg_density_per_pixel * band_area return band - background_contributionこの関数は、バンド領域の総密度から、同じサイズの背景の予想密度を引きます。背景はバンドよりも広く設定できるため、ピクセルあたりの密度で正規化した後、掛けます。
パーツ 4: ローディングコントロールによる正規化
ウェスタンブロットでは、**ローディングコントロール(通常はGAPDHまたはβ-アクチン)**の強度で正規化します。これは、各レーンにロードされた総タンパク質の量のわずかな違いを補正するためです。
def normalize_to_control(target: float, control: float) -> float: if control == 0: raise ValueError("Loading control cannot be zero") return target / control複数のレーンを一度に処理する:
def quantify_gel( density: np.ndarray, target_rois: list[tuple[int, int, int, int]], control_rois: list[tuple[int, int, int, int]], background_roi: tuple[int, int, int, int]) -> list[float]: """ 各レーンごとにターゲット強度とコントロール強度を測定し、 正規化された値を返します。 """ if len(target_rois) != len(control_rois): raise ValueError("Target and control ROIs must have same length") normalized = [] for target_roi, control_roi in zip(target_rois, control_rois): target = measure_band_with_background(density, target_roi, background_roi) control = measure_band_with_background(density, control_roi, background_roi) normalized.append(normalize_to_control(target, control)) return normalized実戦検証 — ローディングコントロールとの相関関係
正規化が適切に行われたかどうかを確認する方法があります。ローディングコントロールの強度がお互いに大きく異なる場合、各レーンのロード量が大きく異なっていることを示します。正規化を行わずにターゲット強度をそのまま使用すると、この違いが結果を歪めてしまいます。
def check_loading_uniformity(control_intensities: list[float]) -> dict: arr = np.array(control_intensities) mean = float(arr.mean()) std = float(arr.std()) cv = std / mean if mean > 0 else float("inf") return { "mean": mean, "std": std, "cv": cv, # 変動係数 "acceptable": cv < 0.2 # 20%以下が安全範囲 }CV(変動係数)が20%以上の場合、ゲルのロードが大きく不均一であったことを示します。この場合、実験をやり直すか、正規化後も結果の解釈に注意が必要です。
ターゲットとコントロールの相関関係も確認します。強い正の相関関係がある場合、ロード量の違いがターゲットの測定値を支配していることを意味し、これは正規化の必要性を正当化します。
def check_target_control_correlation( targets: list[float], controls: list[float]) -> float: t_arr = np.array(targets) c_arr = np.array(controls) return float(np.corrcoef(t_arr, c_arr)[0, 1])値が0.7以上の場合、強い正規化効果があります。値が低い場合、ロード量の違いが大きくなく、正規化が結果に大きな影響を与えません。
フェーディング — 埋めるべき3つの空白
空白1:自動レーン検出
レーンの位置を手動で指定する代わりに、画像の下端を走査して明るさプロファイルからレーンを自動検出します。
def detect_lanes(density: np.ndarray, num_lanes: int) -> list[int]: """ 画像の各縦位置の密度合計を計算し、 ピーク位置をレーンの中心として返します。 """ column_profile = density.sum(axis=0) # TODO: 上位のnum_lanes個のピークを見つけて、ソートされた位置のリストを返します # ヒント: scipy.signal.find_peaksを使用できます passヒント: from scipy.signal import find_peaks; peaks, _ = find_peaks(column_profile, distance=min_lane_spacing).
空白2:バンド飽和の検出
バンドが強すぎてピクセルの値が255に達した場合、実際の強度がどの程度であるかを把握できません。
def check_saturation( image: np.ndarray, band_roi: tuple[int, int, int, int], saturation_threshold: float = 0.05) -> bool: """ ROI内のピクセルの中で、値が0(=完全に暗いバンド、反転すると255)である割合。 閾値を超えた場合、飽和していると判断します。 """ top, left, bottom, right = band_roi region = image[top:bottom, left:right] # TODO: region内で値が0以下であるピクセルの割合を計算し、 # 閾値と比較してブール値を返します。 passヒント: saturated_pixels = (region <= 0).sum(); ratio = saturated_pixels / region.size.
空白3:例外安全な定量化パイプライン
複数のゲル画像をバッチ処理する場合、1つの画像でエラーが発生しても、残りの画像は処理を継続する必要があります。
def batch_quantify( image_paths: list[str], roi_config: dict) -> dict: """ 各画像を定量化します。失敗した場合は、エラーとともに記録します。 返り値: {"results": [...], "errors": [...]} """ results = [] errors = [] for path in image_paths: try: # TODO: 画像をロードし、定量化し、結果をresultsにappendします。 pass except Exception as e: # TODO: どのファイルの、どのようなエラーが発生したかをerrorsに記録します。 pass return {"results": results, "errors": errors}ヒント: except (FileNotFoundError, ValueError) as e: で複数の例外の種類を捕捉するか、上位の Exception を捕捉し、詳細なログを出力します。
考察 — 実用的なデンシトメトリーツールとの違い
サブピクセル精度: 実用的なツールは、バンドの中心をサブピクセル精度で決定します。一方、あなたのROIは整数ピクセル単位です。
曲線的な背景: ゲルの画像は通常、均一ではなく、グラデーションがあります。実用的なツールは、ローリングボール背景除去などのアルゴリズムを使用して、曲線的な背景を除去します。これは、ImageJの基本的なオプションの一つです。
飽和の自動検出: あなたのアプローチは単純な閾値に基づいています。一方、実用的なツールは、ヒストグラム分析を使用して、より高度に飽和を判定します。
定量的なダイナミックレンジ: 化学発光ウェスタンブロットは、ダイナミックレンジが狭く、定量的な信頼区間は数倍程度です。実用的なツールは、標準曲線とともにロードし、各バンドがこの信頼区間内にあるかどうかを確認します。
蛍光ウェスタンブロット: LI-COR Odysseyのような蛍光システムは、より広いダイナミックレンジと正確な定量を提供します。原理は同じですが、画像の特徴が異なります。
拡張プロジェクト
1. matplotlibによる自動可視化: 各画像にレーンROIを重ねて保存。事後検証用。
2. 標準曲線: 既知の濃度の組換えタンパク質をいくつかのレーンにロードした後、標準曲線を作成。未知試料の絶対量を推定。
3. Streamlitアプリ: 既存のパイプラインをWeb UIでラップ。画像アップロード → ROI調整 → 結果ダウンロード。
4. 複数の抗体処理: 同じブロットを洗浄し、複数の抗体で再プローブしたデータ。各抗体ごとに個別のファイルを作成し、それらをマッチングさせて総合レポートを作成。
この章の構成要素
- [F] NumPy 2次元配列: 画像 = 配列。スライスでROIを抽出し、
sum()で強度を計算。 - [F] 相関関係:
np.corrcoefでターゲットとコントロールの相関を確認。正規化の必要性を検証。 - [F] 例外処理: バッチ処理中に失敗したファイルを隔離。try-except の実戦的な使用。
- [W] ファイル I/O · matplotlib: 画像のロードと結果の可視化(完成スクリプト)。
[F] = 自分で実装 / [W] = 完成コードとして提供。