こんにちは、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つ取り上げました。
この記事が皆様のお役に立てれば幸いです。
ご覧いただきありがとうございました。

