こんにちは、Youtaです。
今回は、NumPyの配列を回転させるrot90
をご紹介します。

この前Pythonでリストを回転させたくてさー。
だるかったけど関数自作したんだよねー。
data = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
]
result = 回転させる関数(data)
print(result)
# [[2 5 8]
# [1 4 7]
# [0 3 6]]

ご自身で作られたんですね、素晴らしいです!
ただ、実はNumpyに専用の関数があるんですよ。

まじかー。
「100均に同じのあるよ」的なこと言うじゃん。
この記事を読むと、配列の回転が簡単に実現できます。
ぜひ最後までご覧ください。
Contents
rot90
の基本構文
rot90
は、配列を90°単位で回転 させます。
以下、基本構文です。
numpy.rot90(m, k=1, axes=(0, 1))
rot90
の引数
rot90
の引数は下記の\(3\)つです。
1つずつ見ていきましょう。
m
: 2次元以上の配列
第一引数m
には2次元以上の配列を指定します。
他の引数を指定しない場合、m
を反時計回りに回転させます。
import numpy as np
arr2d = np.array([
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
])
result = np.rot90(arr2d)
print(result)
# [[2 5 8]
# [1 4 7]
# [0 3 6]]
m
には変更は加わりません。
m
を回転した(ように見える)配列が返されるのです(詳細は後述)。
result = np.rot90(arr2d)
print(arr2d)
# [[0 1 2]
# [3 4 5]
# [6 7 8]]
m
はarray_like
なので、numpy.ndarray
に変換できるものは基本何でもいけます。
例えば、下記のようなデータを引数として受け付けます。
- NumPy配列 (
numpy.ndarray
) - Python標準のリスト(
list
)・タプル(tuple
) - Pandasのデータ構造 (
pandas.DataFrame
)
試しにリストやタプルを回転させてみましょう。
Pandasを使える方はpandas.DataFrame
でも試してみてください。
いずれもエラーは出ないはずです。
# リストを使った例
list_data = [
[0, 1, 2],
[3, 4, 5],
]
rotated_list = np.rot90(list_data)
print("リストからの回転:")
print(rotated_list)
# リストからの回転:
# [[2 5]
# [1 4]
# [0 3]]
# タプルを使った例
tuple_data = (
(0, 1, 2),
(3, 4, 5),
)
rotated_tuple = np.rot90(tuple_data)
print("タプルからの回転:")
print(rotated_tuple)
# タプルからの回転:
# [[2 5]
# [1 4]
# [0 3]]
# Pandasを使った例(もし利用可能なら)
import pandas as pd
df = pd.DataFrame([
[0, 1, 2],
[3, 4, 5]],
)
rotated_df = np.rot90(df)
print("Pandas DataFrameからの回転:")
print(rotated_df)
# Pandas DataFrameからの回転:
# [[2 5]
# [1 4]
# [0 3]]
入力のデータ型は違えど、戻り値はすべてnumpy.ndarray
型です。
「rot90
の戻り値」の章で詳しく解説しますね。
print(type(rotated_list))
# <class 'numpy.ndarray'>
print(type(rotated_tuple))
# <class 'numpy.ndarray'>
print(type(rotated_df))
# <class 'numpy.ndarray'>
k
: 90°単位の回転回数
第二引数k
は、90°単位の回転回数を指定します。
デフォルトはk=1
で、反時計回りに90°だけ配列を回します。
import numpy as np
arr2d = np.array([
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
])
result = np.rot90(arr2d, k=1)
print(result)
# [[2 5 8]
# [1 4 7]
# [0 3 6]]
k=1
なら90°、k=2
なら180°、k=3
なら270°、k=4
なら元の形に戻ります。
k
の値は4以上でもOKで、k % 4
回だけ回転したとみなされます。
result = np.rot90(arr2d, k=2)
print(result)
# [[8 7 6]
# [5 4 3]
# [2 1 0]]
逆回転、すなわち時計回りにしたい場合、k
に負の数を指定します。
result = np.rot90(arr2d, k=-1)
print(result)
# [[6 3 0]
# [7 4 1]
# [8 5 2]]
axes
: 回転させる平面
axes
第三引数axes
は、回転させる平面を指定できます。
軸のペアなので、2つの整数を含むタプル(またはarray_like
)を渡します。
例えば、axes=(a, b)
は「a軸とb軸がなす平面上で、a軸からb軸の方向に回転させる」という意味です。
したがって、axes=(a, a)
のように同一軸を指定するとエラーが出ます。
平面を作れないからです。
# np.rot90(arr2d, axes=(1, 1))
# ValueError: Axes must be different.

平面ってどこを指すの?
イメージ湧かないんだけど。

初めはとっつきにくいかも知れません。
次章からは、図解で説明しますね。

2次元配列をrot90
で回転させる例を見てみましょう。
2次元配列の軸は0と1があるので、axes=(0, 1)
とaxes=(1, 0)
を考えます。
以下、話を簡単にするためにk=1
(デフォルト)で統一します。
まずはaxes=(0, 1)
からです。
そもそも「0軸と1軸がなす平面」とはどこを指すのでしょうか?
それは、図の配列後方の青い平面のことです。

rot90
は、配列が乗っている平面自体をぶん回すイメージです。
回転方向は、0軸の正の向きから1軸の正の向きであり、axes=(0, 1)
はデフォルトです。
これこそが、rot90
が配列を反時計回りに回転させる理由です。
import numpy as np
arr2d = np.array([
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
])
# 反時計回り
result = np.rot90(arr2d, axes=(0, 1))
print(result)
# [[2 5 8]
# [1 4 7]
# [0 3 6]]
次にaxes=(1, 0)
です。
回転方向は、1軸の正の向きから0軸の正の向き、つまり時計回りです。

実際に確かめてみましょう。
import numpy as np
arr2d = np.array([
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
])
# 時計回り
result = np.rot90(arr2d, axes=(1, 0))
print(result)
# [[6 3 0]
# [7 4 1]
# [8 5 2]]

なるほどねー。
そーいやk
の正負でも回す方向変わったよね?

鋭いですね。
実は、k
だけでなくaxes
でも回転方向を操作できるのです。
k<0
は「時計回り」と誤解されがちですが、正しくは「逆回転」と捉えるべきです。
何に対して「逆」かというと、axes
で定まる回転方向の「逆」ということです。
つまり、rot90(m, k=1, axes=(1,0))
は rot90(m, k=-1, axes=(0,1))
と同じです。
print(np.array_equal(
np.rot90(arr2d, k=1, axes=(1, 0)),
np.rot90(arr2d, k=-1, axes=(0, 1)),
))
# True
※ array_equal
は2配列の形状・要素がすべて一致するかを判定します。

3次元配列の軸は0・1・2があります。
よって、axes
の組み合わせは(0, 1)
・(1, 2)
・(2, 0)
の3通りあります。
(1, 0)
・(2, 1)
・(0, 2)
は逆回転なだけなので、上記3つを理解すれば十分です。
なお、話を簡単にするために以下はk=1
(デフォルト)で統一します。
下記の3次元配列を用意します。
import numpy as np
arr3d = np.array([
[[0, 1],
[2, 3]],
[[4, 5],
[6, 7]],
])
まずはaxes=(0, 1)
の回転から。
配列は0軸と1軸がなす平面に乗っており、この平面を回します。
回転方向は0軸の正の向きから1軸の正の向きです。

よって、コードを実行すると下記の結果が得られます。
import numpy as np
arr3d = np.array([
[[0, 1],
[2, 3]],
[[4, 5],
[6, 7]],
])
result = np.rot90(arr3d, axes=(0, 1))
print(result)
# [[[2 3]
# [6 7]]
# [[0 1]
# [4 5]]]
お次はaxes=(1, 2)
の回転です。
配列は1軸と2軸がなす平面に乗っており、この平面を回します。
回転方向は1軸の正の向きから2軸の正の向きです。

(本来平面は配列の手前に来るのが自然ですが、図が見づらくなるので奥に描きました。)
よって、コードを実行すると下記の結果が得られます。
import numpy as np
arr3d = np.array([
[[0, 1],
[2, 3]],
[[4, 5],
[6, 7]],
])
# 回転後
result = np.rot90(arr3d, axes=(1, 2))
print(result)
# [[[1 3]
# [0 2]]
# [[5 7]
# [4 6]]]
最後はaxes=(2, 0)
の回転です。
配列は2軸と0軸がなす平面に乗っており、この平面を回します。
回転方向は2軸の正の向きから0軸の正の向きです。

(本来平面は配列の上部に来るのが自然ですが、図が見づらくなるので下部に描きました。)
よって、コードを実行すると下記の結果が得られます。
import numpy as np
arr3d = np.array([
[[0, 1],
[2, 3]],
[[4, 5],
[6, 7]],
])
# 回転後
result = np.rot90(arr3d, axes=(2, 0))
print(result)
# [[[4 0]
# [6 2]]
# [[5 1]
# [7 3]]]
rot90
の戻り値
rot90
の戻り値はnumpy.ndarray
型の配列です。
import numpy as np
arr2d = np.array([
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
])
result = np.rot90(arr2d)
print(type(result))
# <class 'numpy.ndarray'>
ただし、この配列は第一引数m
とメモリを共有します(ビュー)。
print(np.shares_memory(arr2d, result))
# True
※ numpy.shares_memory
は2配列がメモリを共有するかを判定します。
つまり、どちらかを変更すると、もう一方も自動で変更されます。
import numpy as np
arr2d = np.array([
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
])
result = np.rot90(arr2d)
print(result)
# [[2 5 8]
# [1 4 7]
# [0 3 6]]
# resultの2を9に変更
result[0, 0] = 9
print(result)
# [[9 5 8]
# [1 4 7]
# [0 3 6]]
# arr2dの2も9も自動変更
print(arr2d)
# [[0 1 9]
# [3 4 5]
# [6 7 8]]
メモリの話が出てきて少し難しいかも知れません。
そういった方は、取り敢えず「一方の変更が他方にも影響を及ぼす」と理解してください。

完全に別物だと思って戻り値を変更しまくっていたら、
実は引数も変わっていた…という事態はバグの原因になります。
「完全に別物として扱いたい!」という場合は配列のコピーを作成しましょう。
引数をコピーするか、戻り値をコピーするかはお好みです。
import numpy as np
arr2d = np.array([
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
])
# arr2dのコピーをrot90に入れる
result = np.rot90(np.copy(arr2d))
print(result)
# [[2 5 8]
# [1 4 7]
# [0 3 6]]
# resultの2を9に変更
result[0, 0] = 9
print(result)
# [[9 5 8]
# [1 4 7]
# [0 3 6]]
# arr2dは変更されない
print(arr2d)
# [[0 1 2]
# [3 4 5]
# [6 7 8]]
※ numpy.copy
は配列のコピーを作成します。
ただし、引数のデータ構造によっては必ずしもビューが返されるわけでもなさそうです。
例えば、第一引数m
の章で見たようなデータ構造をrot90
に入れた結果が下記です。
リストやタプルの例ではメモリの共有がされておらず、pandas.DataFrame
の例ではされています。
# リストを使った例
list_data = [
[0, 1, 2],
[3, 4, 5],
]
rotated_list = np.rot90(list_data)
print(np.shares_memory(list_data, rotated_list))
# False
# タプルを使った例
tuple_data = (
(0, 1, 2),
(3, 4, 5),
)
rotated_tuple = np.rot90(tuple_data)
print(np.shares_memory(tuple_data, rotated_tuple))
# False
# Pandasを使った例(もし利用可能なら)
import pandas as pd
df = pd.DataFrame([
[0, 1, 2],
[3, 4, 5]],
)
rotated_df = np.rot90(df)
print(np.shares_memory(df, rotated_df))
# True
恐らく、可能ならビューを返し、無理そうならコピーを返す仕様なのでしょうね。
rot90
の応用例
rot90
の応用例を2つご紹介しましょう。
画像の回転
rot90
のメジャーな応用例といえば、やはり画像の回転です。
試しに猫の画像を回転させます。

皆さんは、下記コードの"cutie_cat.png"
に好きな画像のパスを指定してください。
(Pillowライブラリが入っていることを前提とします。)
import numpy as np
from PIL import Image
img = Image.open("cutie_cat.png")
arr = np.array(img)
rotated_arr = np.rot90(arr)
rotated_img = Image.fromarray(rotated_arr)
rotated_img.show()
k
の値をいじることで、画像の回転も自由自在です。




可愛い!保存したい!
回転後の画像を保存したい場合は、show
をsave
メソッドにすればOKです。
# rotated_img.show()
rotated_img.save("保存先のパス.拡張子")
テトリスのブロック回転
rot90
はゲーム開発にも応用できそうです。
例えば、テトリスやパズルゲームでのブロックの回転操作に使えます。
ブロックを回転させるだけの簡単なプログラムを作ってみました。
「→」キーでブロックが時計回りに、「←」キーで反時計回りに回ります。
import numpy as np
import tkinter as tk
# データ定義
CLOCKWISE = -1
COUNTERCLOCKWISE = 1
FRAME_SIZE = 100
block = np.array([
[0, 0, 1],
[1, 1, 1],
[0, 0, 0],
])
# 内部処理
def rotate(direction):
global block
block = np.rot90(block, direction)
reset()
show()
def show():
for row, col in zip(*block.nonzero()):
frame = root.grid_slaves(row=row, column=col)[0]
frame.config(bg="blue", bd=FRAME_SIZE/5)
def reset():
for frame in root.grid_slaves():
frame.config(bg="black", bd=0)
# GUI作成
root = tk.Tk()
row, col = block.shape
for i in range(len(block.ravel())):
frame = tk.Frame(root, width=FRAME_SIZE, height=FRAME_SIZE, relief="raised")
frame.grid(row=i%row, column=i//col)
reset()
show()
# イベント設定
root.bind("<Right>", lambda e: rotate(CLOCKWISE))
root.bind("<Left>", lambda e: rotate(COUNTERCLOCKWISE))
# 表示
root.mainloop()
お粗末なプログラムですが、その気になればテトリスが作れそうです。
実際にキーを押すとコロコロ向きが変わるので面白いですよ。




まとめ
今回はNumpyの配列を90°回転させる関数rot90
をご紹介しました。
もう一度内容をおさらいしましょう。
まずは、rot90
の基本構文です。
numpy.rot90(m, k=1, axes=(0, 1))
次に、rot90
の各引数は下表の通りです。
また、rot90
の戻り値はnumpy.ndarray
型です。
基本的にはビューとして返ってくるのがポイントです。
最後に、rot90
を利用した応用例を2つ取り上げました。
この記事が皆様のお役に立てれば幸いです。
ご覧いただきありがとうございました。