Il s'agit d'un article qui tente de simplifier le prétraitement en utilisant la conversion affine par tenseur. Les transformations Affin peuvent être appliquées aux images, mais les mêmes transformations peuvent être appliquées aux annotations telles que Bounding Box. Vous pouvez également l'étendre à un tenseur pour appliquer différentes transformations à un objet en même temps.
La transformation Affin exprime le mouvement des points avec la formule suivante.
Si vous voulez voir une compréhension vague de la conversion affine, veuillez également lire cet article.
Il prend le produit d'une matrice 3x3 et d'une matrice 3x1, où la matrice 3x3 définit la transformation et la matrice 3x1 est le point avant le déplacement.
Lorsque le point (10, 20) est déplacé de 100 dans la direction $ x $ et de 50 dans la direction $ y $, les coordonnées après le mouvement sont (110, 70), qui sont exprimées comme suit.
Vous pouvez penser qu'il n'est pas nécessaire d'utiliser une telle matrice, mais le fait est qu'elle peut être représentée par une matrice. Si vous l'écrivez dans le code, c'est comme suit.
affine = np.array([[1, 0, 100], [0, 1, 50], [0, 0, 1]])
source = np.array([10, 20, 1])[:, None]
dest = np.dot(affine, source)
print(dest)
#[[110]
# [ 70]
# [ 1]]
En supposant que le mouvement dans la direction $ x $ est $ t_x $ et que le mouvement dans la direction $ y $ est $ t_y $, la matrice de conversion pour un mouvement parallèle est la suivante.
La troisième ligne de chaque matrice est un nombre sans signification. 1 est inclus pour faciliter le calcul du produit matriciel.
Lorsque le point (50, 100) est doublé dans la direction $ x $ et 0,8 fois dans la direction $ y $, les coordonnées après le mouvement sont (100, 80), qui sont exprimées comme suit.
affine = np.array([[2, 0, 0], [0, 0.8, 0], [0, 0, 1]])
source = np.array([50, 100, 1])[:, None]
dest = np.dot(affine, source)
print(dest)
# [[100.]
# [ 80.]
# [ 1.]]
En supposant que l'expansion dans la direction $ x $ est $ s_x $ et que l'expansion dans la direction $ y $ est $ s_y $, la matrice de conversion de mise à l'échelle est la suivante.
Puisque la transformation affine est un calcul matriciel, le nombre de points peut être élargi arbitrairement. Lors de la recherche de la transformation affine pour $ N $ points, prenez le produit de la matrice 3x3 et de la matrice 3xN. Le résultat est une matrice 3xN.
La conversion Affin de $ N = 4 $ points entraîne une conversion Affin carrée. L'application de la transformation Affine, qui double $ (2, 3) $ dans la direction $ (x, y) $ et déplace $ (5, -1) $ en parallèle, vers les sommets du quadrilatère est la suivante.
w, h = 2, 1
points = np.array([[0, w, w, 0], [0, 0, h, h], [1, 1, 1, 1]], np.float32) # (3, 4)
affine = np.array([[2, 0, 5], [0, 3, -1], [0, 0, 1]]) # (3, 3)
dest = np.dot(affine, points)
plt.scatter(points[0,:], points[1,:], color="cyan")
plt.scatter(dest[0,:], dest[1,:], color="magenta")
plt.show()
Ceci est un exemple qui peut être utilisé dans le prétraitement pour la détection d'objets. Lorsque vous faites pivoter l'image dans l'augmentation des données de détection d'objet, vous devez également faire pivoter le cadre englobant. Le cadre englobant peut être défini par deux points, le haut à gauche et le bas à droite, mais en prenant les quatre points du haut, vous pouvez facilement calculer le cadre englobant après rotation. En prenant les valeurs minimum et maximum pour x et y après la rotation, les coordonnées supérieure gauche et inférieure droite de la boîte englobante après rotation peuvent être obtenues.
La rotation est également l'une des transformations affines, et la matrice de transformation lors de la rotation de $ \ theta $ dans le sens antihoraire autour de l'origine est
Et lorsque les sommets de la Boîte englobante (carré) avant la rotation se déplacent vers $ (x_1 ', y_1'), \ cdots, (x_4 ', y_4') $ après rotation, un nouveau Bounding circonscrit le carré incliné. Les coordonnées de la boîte sont
Vous pouvez le trouver sur. La raison pour laquelle ce calcul doit être fait est que les sommets d'origine après rotation ne conviennent pas comme boîte englobante (car ils ne sont pas des rectangles parallèles à l'axe $ xy $) et doivent être ajustés. Veuillez voir la vidéo ci-dessous pour plus de détails.
L'intrigue ressemble à ceci: En raison du tracé, 5 points sont déplacés (chevauchant l'origine), mais pour le calcul uniquement, déplacer 4 points est OK.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
def rotate_box():
w, h = 2, 1
max_wh = max(w, h)
points = np.array([[0, w, w, 0, 0], [0, 0, h, h, 0], [1, 1, 1, 1, 0]], np.float32) #Boîte englobante d'origine
for theta in range(0, 360, 10):
rad = np.radians(theta)
rotate_matrix = np.array([
[np.cos(rad), -np.sin(rad), 0],
[np.sin(rad), np.cos(rad), 0],
[0, 0, 1]], np.float32)
dest_points = np.dot(rotate_matrix, points)[:2, :] #Carré après rotation
rectangle = np.concatenate([np.min(dest_points, axis=-1),
np.max(dest_points, axis=-1)]) #Nouvelle boîte englobante
plt.clf()
plt.plot(dest_points[0,:], dest_points[1,:], linewidth=2, marker="o")
ax = plt.gca()
rect = patches.Rectangle(rectangle[:2], *(rectangle[2:] - rectangle[:2]),
linewidth=1, edgecolor="magenta", fill=False)
ax.add_patch(rect)
plt.ylim(-max_wh*2, max_wh*2)
plt.xlim(-max_wh * 2, max_wh * 2)
plt.title("degree = " + str(theta))
plt.show()
Les transformations d'affine peuvent être combinées en prenant un produit matriciel. Lors de la conversion de $ A_1-> A_2 $, on prend le produit de $ A_2A_1P $ ($ P $ est une matrice de points). Notez que l'ordre est inversé. De plus, ** la loi sur les changes ne tient pas **, et si vous modifiez l'ordre, le résultat sera différent.
Par exemple, supposons que $ A_1 $ double $ x, y $, $ A_2 $ se déplace de 50 dans la direction $ x $ et 100 se déplace dans la direction $ y $. À ce stade, $ A_2A_1 (A_1 \ à A_2) $ est
Cependant, $ A_1A_2 (A_2 \ à A_1) $ est
Et la taille du mouvement parallèle change. Cela représente la différence entre «se déplacer en parallèle puis s'étendre, ou s'étendre et se déplacer ensuite en parallèle». Si vous ne connaissez pas la commande, c'est une bonne idée d'essayer un exemple simple comme celui-ci.
Étant donné que la transformation Affin peut être appliquée à n'importe quel nombre de points plus tôt, il est normal d'avoir des milliers de points. Ici, "personne intelligente" (à partir de matériel gratuit)
Pour pointer des données de groupe et tracer tout en synthétisant les transformations affines d'expansion et de rotation. Presque identique à l'exemple 4.
from PIL import Image
def atamanowaruihito():
with Image.open("atamanowaruihito.png ") as img:
img = img.resize((img.width // 2, img.height // 2))
img = img.convert("L").point(lambda x: 255 if x >= 128 else 0) #Grisé, binarisation
points = np.stack(np.where(np.array(img) == 0)[::-1], axis=0) # yx ->xy pour matricer les points
points[1,:] = img.height - points[1,:] #Corrigez l'axe y du positif vers le bas au positif vers le haut(2, 5912)
points = np.concatenate([points, np.ones_like(points[0:1, :])], axis=0) #Ajoutez 1 sur la 3ème ligne(3, 5912)
for theta in range(0, 360, 1):
rad = np.radians(theta)
rotate_matrix = np.array([
[np.cos(rad), -np.sin(rad), 0],
[np.sin(rad), np.cos(rad), 0],
[0, 0, 1]], np.float32) #Matrice de rotation
scale_matrix = np.eye(3, dtype=np.float32) * (1 + theta / 180)
#La synthèse de transformation d'affine est le produit matriciel de la matrice de transformation
# A1->Pour A2, prenez le produit dans l'ordre de A2A1 (notez la commande)
affine = np.dot(scale_matrix, rotate_matrix) #Faire pivoter et développer
dest_points = np.dot(affine, points)[:2, :] #Groupe de points après rotation
rectangle = np.concatenate([np.min(dest_points, axis=-1),
np.max(dest_points, axis=-1)]) #Boîte englobante pour les points
plt.clf()
plt.scatter(dest_points[0,:], dest_points[1,:], s=1)
ax = plt.gca()
rect = patches.Rectangle(rectangle[:2], *(rectangle[2:] - rectangle[:2]),
linewidth=1, edgecolor="magenta", fill=False)
ax.add_patch(rect)
plt.ylim(-750, 750)
plt.xlim(-750, 750)
plt.show()
La boîte englobante peut être calculée de la même manière que l'exemple carré.
De là, le calcul du tenseur Envisagez une méthode d'application de plusieurs transformations Affin au même point en même temps au lieu de synthétiser des transformations Affin. C'est difficile à exprimer dans une expression, alors pensez au code.
S'il n'y avait qu'une seule transformation affine, la matrice de transformation aurait la forme (3, 3) ''. Mais que se passe-t-il s'il y a deux transformations, c'est-à-dire la forme
(2, 3, 3) ''? En d'autres termes,
# points(4 points): (3, 4), affines: (2, 3, 3)
output = np.zeros((2, 3, 4)
for i in range(affines.shape[0]):
output[i] = np.dot(affines[i], points)
Je veux calculer. En fait, il s'agit d'une seule ligne sans utiliser de boucle for
output = np.matmul(affines, points)
Il peut être calculé avec.
Essayons trois transformations affines dans l'exemple 3.
from matplotlib import cm
def rectangle_multi():
w, h = 2, 1
points = np.array([[0, w, w, 0], [0, 0, h, h], [1, 1, 1, 1]], np.float32) # (3, 4)
a1 = np.eye(3) #Conversion égale
a2 = np.array([[2, 0, 5], [0, 3, -1], [0, 0, 1]]) # (3, 3)
a3 = np.array([[3, 0, 10], [0, 1, 2], [0, 0, 1]]) # (3, 3)
affine = np.stack([a1, a2, a3], axis=0) # (3, 3, 3)
dest = np.matmul(affine, points) # (3, 3, 4)
cmap = cm.get_cmap("tab10")
for i in range(3):
plt.scatter(dest[i,0,:], dest[i, 1,:], color=cmap(i))
plt.show()
J'ai pu effectuer trois transformations différentes en un seul calcul. De cette manière, la ** conversion un-à-plusieurs ** peut également être effectuée en utilisant une conversion affine en utilisant un tenseur.
Dans la détection d'objet, le Bounding Box est prédit à partir de chaque point (Anchor) de la sortie du réseau neuronal. La boîte englobante correspond aux (coordonnées) de l'image brute, mais de nombreuses coordonnées apparaissent dans la détection d'objet. Par exemple
En d'autres termes, une conversion de point flexible "de coordonnées en coordonnées" est requise. Cette transformation de coordonnées peut être unifiée par la transformation affine par tenseur.
Considérez maintenant ce qui suit:
(ymin, xmin, ymax, xmax)
, dans Ground Truth
(10, 20, 30, 40), (30, 30, 50, 50) ` Il y en a deux. À ce stade, je veux trouver les coordonnées de la boîte englobante de la vérité au sol vues de chaque boîte d'ancrage. Cependant, chaque ancre est au milieu d'un pixel 1x1 et l'origine de chaque ancre est le point où $ x et y $ sont décalés de 0,5 minute.En règle générale, considérez deux conversions affines.
(3, 3)
.(4, 4, 3, 3)
, avec la conversion affine de (3, 3) '' superposée sur 4x4 vertical et horizontal.
(i, j,:,:)
$ (0 \ leq i, j \ leq 3) La conversion affine à la position de $ est la suivante. Puisqu'il s'agit d'un mouvement parallèle vu de la boîte d'ancrage, le signe sera négatif.Tout ce que vous avez à faire est de combiner les deux, 1 et 2. C'est délicieux. Si vous voulez faire la conversion inverse (ancre → image originale), prenez la matrice inverse du tenseur composite de 1 et 2, c'est-à-dire
inv_transform = np.linalg.inv(combined_affine)
Vous pouvez obtenir le tenseur de conversion inverse simplement en faisant. Si l'entrée de `` np.linalg.inv '' est un tenseur, empilez la matrice inverse pour les deux derniers axes.
En détection d'objet, la notation de $ (y, x) $ est plus pratique que la notation de $ (x, y) $ (car le tenseur de l'image est $ (B, H, W, C) $), donc mouvement parallèle Conversion d'affine de $ t_x, t_y $, mise à l'échelle $ s_x, s_y $
Il est représenté par. Même si les axes sont permutés, cela fonctionne toujours comme une conversion affine.
def anchor_box():
bounding_boxes = np.array([[10, 20, 30, 40], [30, 30, 50, 50]]) # (2, 4)
points = bounding_boxes.reshape(-1, 2).T # (4, 2) -> (2, 4)
points = np.concatenate([points, np.ones_like(points[0:1,:])], axis=0) # (3, 4)
#Pensez en coordonnées yx
a1 = np.array([[0.8, 0, 0], [0, 1.5, 0], [0, 0, 1]]) #0 dans la direction y.8 fois, 1 dans la direction x.5 fois
#ancre
offset_x, offset_y = np.meshgrid(-(np.arange(4) + 0.5), -(np.arange(4) + 0.5)) #Puisque l'origine de l'image d'entrée est vue depuis l'ancre, moins le mouvement parallèle
a2 = np.zeros((4, 4, 3, 3)) + np.eye(3).reshape(1, 1, 3, 3) / 16.0 # (4, 4, 3, 3)Diffuser à
a2[:,:,0,2] = offset_y
a2[:,:, 1, 2] = offset_x
a2[:,:, 2, 2] = 1.0
#Synthèse d'affine
affine = np.matmul(a2, a1)
#Conversion d'affine
raw_dest = np.matmul(affine, points) # (4, 4, 3, 4)
dest = raw_dest.swapaxes(-1, -2)[:,:,:,:2] # (4, 4, 4, 3)Déménagement version Tensol-> (4, 4, 4, 2)
dest = dest.reshape(4, 4, 2, 4)
print("Coordonnées après conversion affine lorsque le cadre englobant de l'image d'origine est visualisé à chaque ancre")
print(dest)
#Conversion inverse et vérification
raw_inv = np.matmul(np.linalg.inv(affine), raw_dest)
inv = raw_inv.swapaxes(-1, -2)[:,:,:,:2]
inv = inv.reshape(4, 4, 2, 4) #Correspond aux cadres de délimitation
print("La conversion inverse des coordonnées converties revient à la valeur d'origine")
print(inv)
#Conversion inverse affine (confirmation)
inv_transoform = np.linalg.inv(affine)
print("Conversion inverse affine (pour le débogage)")
print(inv_transoform)
<détails> Vous pouvez voir que les coordonnées de la boîte englobante d'origine sont restaurées même si l'image d'origine → ancre et ancre → image d'origine sont inversées. Il est fort qu'il n'y ait qu'une seule conversion inverse dans le calcul matriciel. Lorsque vous voulez voir si la conversion d'ancre fonctionne, il est facile de vérifier l'affine de conversion inverse (matrice inverse). Vous pouvez également le faire en tirant parti de la transformation affine par le tenseur. Veuillez regarder la vidéo ci-dessous. Je vais omettre l'explication, mais si vous êtes intéressé, veuillez jeter un œil au code. <détails>
Recommended Posts
Coordonnées après conversion affine lorsque le cadre englobant de l'image d'origine est visualisé à chaque ancre
[[[[ 0. 1.375 1. 3.25 ]
[ 1. 2.3125 2. 4.1875]]
[[ 0. 0.375 1. 2.25 ]
[ 1. 1.3125 2. 3.1875]]
[[ 0. -0.625 1. 1.25 ]
[ 1. 0.3125 2. 2.1875]]
[[ 0. -1.625 1. 0.25 ]
[ 1. -0.6875 2. 1.1875]]]
[[[-1. 1.375 0. 3.25 ]
[ 0. 2.3125 1. 4.1875]]
[[-1. 0.375 0. 2.25 ]
[ 0. 1.3125 1. 3.1875]]
[[-1. -0.625 0. 1.25 ]
[ 0. 0.3125 1. 2.1875]]
[[-1. -1.625 0. 0.25 ]
[ 0. -0.6875 1. 1.1875]]]
[[[-2. 1.375 -1. 3.25 ]
[-1. 2.3125 0. 4.1875]]
[[-2. 0.375 -1. 2.25 ]
[-1. 1.3125 0. 3.1875]]
[[-2. -0.625 -1. 1.25 ]
[-1. 0.3125 0. 2.1875]]
[[-2. -1.625 -1. 0.25 ]
[-1. -0.6875 0. 1.1875]]]
[[[-3. 1.375 -2. 3.25 ]
[-2. 2.3125 -1. 4.1875]]
[[-3. 0.375 -2. 2.25 ]
[-2. 1.3125 -1. 3.1875]]
[[-3. -0.625 -2. 1.25 ]
[-2. 0.3125 -1. 2.1875]]
[[-3. -1.625 -2. 0.25 ]
[-2. -0.6875 -1. 1.1875]]]]
La conversion inverse des coordonnées converties revient à la valeur d'origine
[[[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]]
[[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]]
[[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]]
[[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]
[[10. 20. 30. 40.]
[30. 30. 50. 50.]]]]
Conversion inverse affine (pour le débogage)
[[[[20. 0. 10. ]
[ 0. 10.66666667 5.33333333]
[ 0. 0. 1. ]]
[[20. 0. 10. ]
[ 0. 10.66666667 16. ]
[ 0. 0. 1. ]]
[[20. 0. 10. ]
[ 0. 10.66666667 26.66666667]
[ 0. 0. 1. ]]
[[20. 0. 10. ]
[ 0. 10.66666667 37.33333333]
[ 0. 0. 1. ]]]
[[[20. 0. 30. ]
[ 0. 10.66666667 5.33333333]
[ 0. 0. 1. ]]
[[20. 0. 30. ]
[ 0. 10.66666667 16. ]
[ 0. 0. 1. ]]
[[20. 0. 30. ]
[ 0. 10.66666667 26.66666667]
[ 0. 0. 1. ]]
[[20. 0. 30. ]
[ 0. 10.66666667 37.33333333]
[ 0. 0. 1. ]]]
[[[20. 0. 50. ]
[ 0. 10.66666667 5.33333333]
[ 0. 0. 1. ]]
[[20. 0. 50. ]
[ 0. 10.66666667 16. ]
[ 0. 0. 1. ]]
[[20. 0. 50. ]
[ 0. 10.66666667 26.66666667]
[ 0. 0. 1. ]]
[[20. 0. 50. ]
[ 0. 10.66666667 37.33333333]
[ 0. 0. 1. ]]]
[[[20. 0. 70. ]
[ 0. 10.66666667 5.33333333]
[ 0. 0. 1. ]]
[[20. 0. 70. ]
[ 0. 10.66666667 16. ]
[ 0. 0. 1. ]]
[[20. 0. 70. ]
[ 0. 10.66666667 26.66666667]
[ 0. 0. 1. ]]
[[20. 0. 70. ]
[ 0. 10.66666667 37.33333333]
[ 0. 0. 1. ]]]]
Exemple spécifique 8: Quoi qu'il en soit, une "personne intelligente" qui semble être malade
def atamanowaruihito2():
with Image.open("atamanowaruihito.png ") as img:
img = img.resize((img.width // 2, img.height // 2))
img = img.convert("L").point(lambda x: 255 if x >= 128 else 0) #Grisé, binarisation
points = np.stack(np.where(np.array(img) == 0)[::-1], axis=0) # yx ->xy pour matricer les points
points[1,:] = img.height - points[1,:] #Corrigez l'axe y du positif vers le bas au positif vers le haut(2, 5912)
points = np.concatenate([points, np.ones_like(points[0:1, :])], axis=0) #Ajoutez 1 sur la 3ème ligne(3, 5912)
#Nombre aléatoire pour la matrice de rotation
a = np.random.uniform(1.0, 5.0, size=(4, 4))
b = np.random.uniform(-180, 180, size=a.shape)
#Nombre aléatoire pour la matrice d'expansion
c = np.random.uniform(0.5, 1.5, size=a.shape)
d = np.random.uniform(1.0, 5.0, size=a.shape)
e = np.random.uniform(-180, 180, size=a.shape)
f = np.random.uniform(0.5, 1.0, size=a.shape) + c
#Nombre aléatoire pour un mouvement parallèle
e = np.random.uniform(0, 200, size=a.shape)
g = np.random.uniform(1.0, 5.0, size=a.shape)
h = np.random.uniform(-180, 180, size=a.shape)
for theta in range(0, 360, 10):
#Conversion de rotation
rad = np.radians(a * theta + b)
rotate_tensor = np.broadcast_to(np.eye(3)[None, None,:], (4, 4, 3, 3)).copy()
rotate_tensor[:,:, 0, 0] = np.cos(rad)
rotate_tensor[:,:, 0, 1] = -np.sin(rad)
rotate_tensor[:,:, 1, 0] = np.sin(rad)
rotate_tensor[:,:, 1, 1] = np.cos(rad)
#Conversion d'agrandissement
rad = np.radians(d * theta + e)
scale_tensor = np.broadcast_to(np.eye(3)[None, None,:], (4, 4, 3, 3)).copy()
scale_tensor[:,:, 0, 0] = c * np.sin(rad) + f
scale_tensor[:,:, 1, 1] = c * np.sin(rad) + f
#Mouvement parallèle individuel
rad = np.radians(g * theta + h)
transform_tensor = np.broadcast_to(np.eye(3)[None, None,:], (4, 4, 3, 3)).copy()
transform_tensor[:,:, 0, 2] = e * np.cos(rad)
transform_tensor[:,:, 1, 2] = e * np.sin(rad)
#Mouvement parallèle global
shift_x, shift_y = np.meshgrid(np.arange(4), np.arange(4))
anchor_tensor = np.broadcast_to(np.eye(3)[None, None,:], (4, 4, 3, 3)).copy()
anchor_tensor[:,:, 0, 2] = shift_x * 500
anchor_tensor[:,:, 1, 2] = shift_y * 500
#Rotation → Agrandissement → Mouvement parallèle → Mouvement parallèle global
affine = np.matmul(anchor_tensor, transform_tensor)
affine = np.matmul(affine, scale_tensor)
affine = np.matmul(affine, rotate_tensor)
dest_points = np.matmul(affine, points)[:, :, :2, :] #Groupe de points après rotation
rectangle = np.concatenate([np.min(dest_points, axis=-1),
np.max(dest_points, axis=-1)], axis=-1) #Boîte englobante pour les points
dest_points = dest_points.swapaxes(-1, -2).reshape(-1, 2)
rectangle = rectangle.reshape(-1, 4)
plt.clf()
plt.scatter(dest_points[:, 0], dest_points[:, 1], s=1)
ax = plt.gca()
for i in range(rectangle.shape[0]):
rect = patches.Rectangle(rectangle[i, :2], *(rectangle[i, 2:] - rectangle[i, :2]),
linewidth=1, edgecolor="magenta", fill=False)
ax.add_patch(rect)
plt.ylim(-750, 2500)
plt.xlim(-750, 2500)
plt.title("theta = " + str(theta))
plt.show()