I have a 3d numpy array describing a polycube (imagine a 3d tetris piece). How can I calculate all 24 rotations?
Numpy\'s array manipulation routines includes a rot90 me
I encountered a similar problem when trying to randomly rotate a 3D one hot encoded numpy array, just before feeding into my neural network. I will demonstrate here for a 3D array, but this also works with a 4th dimension (when using OHE).
>>> m = np.reshape(np.arange(8),(2,2,2))
>>> m
array([[[0, 1],
[2, 3]],
[[4, 5],
[6, 7]]])
Next we rotate the array 3 times, each time in a different direction. Repeat 24,000 times to see distribution (expecting 1000 counts for each unique rotation):
>>> rot_list = []
>>> for _ in range(24000):
a = np.rot90(m,np.random.randint(0,4),axes=(0,np.random.randint(1,3)))
b = np.rot90(a,np.random.randint(0,4),axes=(np.random.randint(1,3),0))
c = np.rot90(b,np.random.randint(0,4),axes=(1,2)) # or axes=(2,1)
rot_list.append(c)
>>> unique_rotation_matrices, counts = np.unique(np.asarray(rot_list),axis=0, return_counts=True)
>>> len(unique_rotation_matrices)
24
So we see we get all the 24 possible rotations. Let's look at their distribution:
>>> counts
[1062 946 917 982 1096 978 1153 936 939 907 1183 932 958 932 1122
926 1115 954 933 932 1135 924 1138 900]
Distribution looks quite even, but rerunning this a number of times reveals it is slightly biased.
We would start off with the intention of getting all 48
combinations so that we get the general idea about solving it for a n-dim array. Later on we would filter out the unwanted 24
ones.
The idea to solve for a generic case would be to basically do two things - flip along every axis and permute axes with all combinations for the given number of axes.
Flip : To flip, we would use the stepsize parameter for slicing, i.e. array[::stepsize]. So, to flip, it would be : [::-1]
and without flipping, simply : [::1]
. That stepsize
could be assigned as a variable varying between 1
and -1
for the two combinations simpl. For a ndarray, simply extend this to all axes.
Permute axes : To achieve this, we can use np.transpose and specify the required permuted order as the axes parameter with it. We will generate all possible orders with itertools.permutations
.
That's all there is! Let's implement it with a
as the input 3D
array -
import itertools
def rotations48(a):
# Get all combinations of axes that are permutable
n = a.ndim
axcomb = np.array(list(itertools.permutations(range(n), n)))
# Initialize output array
out = np.zeros((6,2,2,2,) + a.shape,dtype=a.dtype)
# Run loop through all axes for flipping and permuting each axis
for i,ax in enumerate(axcomb):
for j,fx in enumerate([1,-1]):
for k,fy in enumerate([1,-1]):
for l,fz in enumerate([1,-1]):
out[i,j,k,l] = np.transpose(a[::fx,::fy,::fz],ax)
return out
We could simplify for the flipping nested loops with one loop -
def rotations48(a):
n = a.ndim
axcomb = list(itertools.permutations(range(n), n)) # all axes combinations
pcomb = list(itertools.product([1,-1], repeat=n)) # all permuted orders
out = np.zeros((6,8,) + a.shape,dtype=a.dtype) # Initialize output array
for i,ax in enumerate(axcomb): #loop through all axes for permuting
for j,(fx,fy,fz) in enumerate(pcomb): # all flipping combinations
out[i,j] = np.transpose(a[::fx,::fy,::fz],ax)
return out
So, this gets us all the 48
combinations.
Extend to more dimensions : If we want to extend this to a 4D
array, simply edit the initialization part to extend by 2
and slice along one more axis.
24
rotationsNow, as OP is claiming to have a working solution to get the desired 24
combinations, we need to filter out
from our proposed solution. I couldn't find a generic pattern for the filtering, but got the indices required for indexing -
idx = np.array([ 0, 3, 5, 6, 9, 10, 12, 15, 17, 18, 20, 23, 24, \
27, 29, 30, 32, 35, 37, 38, 41, 42, 44, 47])
If you care about the order to have the output same output as with rotations24, we would have -
idx = np.array([ 0, 10, 3, 9, 5, 15, 6, 12, 41, 27, 42, 24, 44, \
30, 47, 29, 18, 35, 17, 32, 20, 37, 23, 38])
Hence, get the required 24
ones with indexing
-
final_out = out.reshape(48,-1)[idx]
This works for 3D
arrays with any generic lengths.
Sample run for verification
# From https://stackoverflow.com/a/33190472/ @Colonel Panic
def rotations24_array(a):
out0 = np.zeros((6,2,2,2,) + a.shape,dtype=a.dtype)
p = [list(i) for i in rotations24(a)]
out0 = np.zeros((6,4,m,m,m),dtype=a.dtype)
for i in range(6):
for j in range(4):
out0[i,j] = p[i][j]
return out0
Verify -
In [486]: # Setup
...: np.random.seed(0)
...: m = 3
...: a = np.random.randint(11,99,(m,m,m))
...:
...: # Verify results
...: idx = np.array([ 0, 10, 3, 9, 5, 15, 6, 12, 41, 27, 42, 24, 44, \
...: 30, 47, 29, 18, 35, 17, 32, 20, 37, 23, 38])
...: out1 = rotations24_array(a).reshape(-1,m**3)
...: out2 = rotations48(a).reshape(48,-1)[idx]
...: print np.allclose(out1, out2)
True
There are 24 rotation matrices at the bottom of this page: http://www.euclideanspace.com/maths/algebra/matrix/transforms/examples/index.htm .
If a tetris piece were represented by an array of coordinate triples, to apply a rotation to the piece you would just matrix multiply the matrix by each of the triples in the array. You would do that 24 times to get the 24 different pieces.
For example, if your array of boxes is (1,2,5), (1,2,4), (1,3,4)
And the rotation you are considering at the moment is for example
1 0 0
M = 0 0 -1
0 1 0
Then the rotated figure has representation
(1,2,5).M, (1,2,4).M, (1,3,4).M
where the . represents matrix multiplication. Do that for each M in the list and you've got all 24 rotations. (You can either pre- or post- multiply by the matrices, it doesn't matter if you want the whole set of rotations in no particular order.)
So that is very straightforward.
Now, to get the data out of your data structure and into the array structure I have used above, you would have to do something like (sorry I don't know Python)
for i = 0 to 2
for j = 0 to 2
for k = 0 to 2
if polycube(i,j,k)==1 then push(i-1,j-1,k-1) onto end of newArray
or something like that. Finally, to go from the newArray representation to the polycube you would do something like
polycube = all zeros
foreach point (i,j,k) in newArray
polycube(i+1,j+1,k+1) = 1
For a 2x2x2 polycube, you would do
for i = 0 to 1
for j = 0 to 1
for k = 0 to 1
if polycube(i,j,k)==1 then push(i-0.5,j-0.5,k-0.5) onto end of newArray
and the corresponding inverse function.