【Python】画像の上下反転と半画素ずれを直す

PythonのTopに戻る

結論:extentには画素中心ではなく画像の外側の境界を渡す

測定画像へ実座標の点を重ねて半画素ずれるなら、imshowのextentを調べよう。指定順は左・右・下・上で、画像全体が占める境界である。等間隔の画素中心が分かっている場合、最初の中心から半間隔を引き、最後の中心へ半間隔を足す。最初と最後の中心をそのまま境界にすると、画像が内側へ縮んで重ねた点と一致しない。

もう一つ確認したいのがoriginである。配列の行番号0を物理座標の下側に置くならorigin="lower"とする。上側から取得した画像ならupperが適切な場合もある。見た目が上下逆だからと配列反転・origin変更・縦軸反転を次々に重ねると、どの操作で直ったのか分からなくなる。取得時の座標規約を一つ決めてから表示へ対応させよう。

そのまま動かせる例

この例は3行4列の画像を使い、横の画素中心を10・12・14・16 mm、縦の中心を20・23・26 mmとする。間隔は横2 mm、縦3 mmなので、正しいextentは9〜17 mm、18.5〜27.5 mmである。白い丸は既知の画素中心を表す。左は中心を境界にした誤り、中央は上下の規約違い、右が意図した対応である。

NumPyとMatplotlibが使える環境で例全体を実行すると、作業フォルダーへimage-coordinates.pngを保存する。同名画像は上書きされる。比較条件をそろえるため、3枚とも同じ色範囲、同じ軸範囲、同じ物理的な縦横比を使っている。コード中の丸は画像へ描き込んだ印ではなく、Axes上の実座標に置いた独立の点である。

import numpy as np
import matplotlib.pyplot as plt

a = np.arange(12).reshape(3, 4)
x_centers = np.array([10., 12., 14., 16.])
y_centers = np.array([20., 23., 26.])
dx, dy = 2., 3.
extent = (x_centers[0] - dx / 2, x_centers[-1] + dx / 2,
          y_centers[0] - dy / 2, y_centers[-1] + dy / 2)
wrong = (x_centers[0], x_centers[-1], y_centers[0], y_centers[-1])
fig, axs = plt.subplots(1, 3, figsize=(11, 4.1), layout="constrained")
try:
    settings = [("Wrong: center as boundary", "lower", wrong),
                ("Wrong: first row at top", "upper", extent),
                ("Correct: centers align", "lower", extent)]
    for ax, (title, origin, box) in zip(axs, settings):
        im = ax.imshow(a, origin=origin, extent=box, cmap="viridis",
                       vmin=0, vmax=11, interpolation="nearest", aspect="equal")
        xx, yy = np.meshgrid(x_centers, y_centers)
        ax.scatter(xx, yy, s=35, facecolors="none", edgecolors="white")
        ax.set(title=title, xlabel="x (mm)", ylabel="y (mm)",
               xlim=(8.5, 17.5), ylim=(18, 28), xticks=x_centers, yticks=y_centers)
    fig.colorbar(im, ax=axs.tolist(), label="Pixel value", shrink=0.8)
    recovered_x = extent[0] + (np.arange(a.shape[1]) + 0.5) * dx
    recovered_y = extent[2] + (np.arange(a.shape[0]) + 0.5) * dy
    np.testing.assert_allclose(recovered_x, x_centers)
    np.testing.assert_allclose(recovered_y, y_centers)
    print("extent:", extent)
    print("pixel [0, 0] center:", (recovered_x[0].item(), recovered_y[0].item()))
    fig.savefig("image-coordinates.png", dpi=160)
finally:
    plt.close(fig)
print("saved: image-coordinates.png")

実行結果

extent: (np.float64(9.0), np.float64(17.0), np.float64(18.5), np.float64(27.5))
pixel [0, 0] center: (10.0, 20.0)
saved: image-coordinates.png
画素中心を境界にした誤り、originの上下違い、正しいextentの3枚比較
白丸は既知の実座標。右図では行0が下側にあり、各画素中心と丸が一致する。

行・列とx・yの対応を読む

配列へのアクセスは通常a[row, column]の順である。画像表示では行が縦方向、列が横方向になる。実座標のxとyを渡す順番と配列添字の順番が逆なので、添字のペアをそのまま散布図へ渡さないようにしたい。右図で左下の画素が値0、その上が4、その右が1になっているかを見ると、上下の規約と行列の取り違えを確認できる。

正しいextentから中心を逆算すると、左端に半画素分を足した位置から等間隔に並び、元の中心配列と一致する。点が画像の境界上に乗っていないかだけでなく、全ての中心に対応することを確かめる。実際の測定では既知の目印や校正点を複数置き、一つの点だけが合う状態で満足しないようにしたい。

等間隔でない座標には別の表現を使う

extentは四角い領域へ画像を割り当てる指定であり、不等間隔の各画素の位置を個別に設定する機能ではない。中心座標の間隔が一定でない測定を同じ方法で描けば、途中の画素が実際と異なる位置へ置かれる。セル境界が分かる場合はpcolormeshなどを検討し、表示方法に合わせてデータを無意識に等間隔とみなさないことが必要である。

aspect="equal"はx方向1 mmとy方向1 mmを同じ長さで描く指定で、この例の画素は縦長に見える。各画素を正方形へ見せることとは違う。autoにするとAxesの枠に合わせて伸縮するので、形状・距離・角度を比較する画像では注意したい。座標の単位が異なるなら、同じ縦横比に物理的な意味があるかも検討する。

補間や軸反転で座標の誤りを隠さない

ここではinterpolationをnearestにして画素の境界を見やすくした。滑らかな補間を選んでも、座標の対応自体が正しくなるわけではない。小さな検証配列で境界を確認してから、最終的な解像度や補間方法を選ぶとよい。写真のような画像と離散的な測定セルでは、適した表示も異なる。

extentを明示した後で軸範囲を逆向きに設定すると、画面上の向きはさらに反転する。既定のoriginも設定ファイルなどで変わり得るため、再現したい図では明示するのが分かりやすい。画像の形、中心座標、画素間隔、originを保存しておけば、別の処理へ渡しても位置を追いやすい。見た目の上下だけではなく、座標値がどちらへ増えるかまで確認しておこう。

確認環境と参考資料

例はLinux・CPython 3.12.14・NumPy 2.3.5・Matplotlib 3.10.8(Agg)で実行した。掲載した出力はこの環境での結果である。公式資料のstable版や最新版は更新されるため、手元のバージョンと対応する仕様も確認してほしい。

関連項目:複数のヒートマップで同じ色を同じ値にする / 測定配列を形と型を保って保存する

PythonのTopに戻る