資格勉強のため、更新を一時中断しております。今年中にまた戻って来ます!

【Numpy】配列の回転: rot90を解説

Numpy: 配列を回転させるrot90の使い方

こんにちは、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均に同じのあるよ」的なこと言うじゃん。

この記事を読むと、配列の回転が簡単に実現できます。

ぜひ最後までご覧ください。

rot90の基本構文

rot90は、配列を90°単位で回転 させます。

以下、基本構文です。

numpy.rot90(m, k=1, axes=(0, 1))

rot90の引数

rot90の引数は下記の\(3\)つです。

引数必須/任意処理
marray_like必須入力配列(2次元以上)
kint任意回転回数(90°単位)。デフォルトは1。
正→反時計回り
負→時計回り
axestuple(・array_like)任意回転の対象とする2軸のペア。
デフォルトは (0, 1)。
rot90の引数

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]]

marray_likeなので、numpy.ndarrayに変換できるものは基本何でもいけます。

例えば、下記のようなデータを引数として受け付けます。

rot90に渡せるデータ構造
  • 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は、回転させる平面を指定できます。

軸のペアなので、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次元配列の回転

2D array

2次元配列をrot90で回転させる例を見てみましょう。

2次元配列の軸は0と1があるので、axes=(0, 1)axes=(1, 0)を考えます。

以下、話を簡単にするためにk=1(デフォルト)で統一します。

まずはaxes=(0, 1)からです。

そもそも「0軸と1軸がなす平面」とはどこを指すのでしょうか?

それは、図の配列後方の青い平面のことです。

axes=(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軸の正の向き、つまり時計回りです。

axes=(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次元配列の回転

3D array

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軸の正の向きです。

axes=(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軸の正の向きです。

axes=(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軸の正の向きです。

axes=(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の値をいじることで、画像の回転も自由自在です。

可愛い猫を反時計回りに90°回転
可愛い猫を反時計回りに180°回転
可愛い猫を反時計回りに270°回転
すずか
すずか

可愛い!保存したい!

回転後の画像を保存したい場合は、showsaveメソッドにすれば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()

お粗末なプログラムですが、その気になればテトリスが作れそうです。

実際にキーを押すとコロコロ向きが変わるので面白いですよ。

J-テトリミノ
J-テトリミノを反時計回りに90°回転
J-テトリミノを反時計回りに180°回転
J-テトリミノを反時計回りに270°回転

まとめ

今回はNumpyの配列を90°回転させる関数rot90をご紹介しました。

もう一度内容をおさらいしましょう。

まずは、rot90の基本構文です。

numpy.rot90(m, k=1, axes=(0, 1))

次に、rot90の各引数は下表の通りです。

引数必須/任意処理
marray_like必須入力配列(2次元以上)
kint任意回転回数(90°単位)。デフォルトは1。
正→反時計回り
負→時計回り
axestuple(・array_like)任意回転の対象とする2軸のペア。デフォルトは (0, 1)。

また、rot90の戻り値はnumpy.ndarray型です。

基本的にはビューとして返ってくるのがポイントです。

最後に、rot90を利用した応用例を2つ取り上げました。

この記事が皆様のお役に立てれば幸いです。

ご覧いただきありがとうございました。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA