LoginSignup
1
3

More than 3 years have passed since last update.

【Python画像処理】アフィン変換(Affine Transformation)を試す。

Last updated at Posted at 2021-02-23

この投稿は以下の投稿の再現テストです。とりあえずプログラムで動かせたら自分的に敷居が下がるので…
完全に理解するアフィン変換
Python, OpenCVで画像ファイルの読み込み、保存(imread, imwrite)
Pythonのif name == "main" とは何ですか?への回答
matplotlib逆引きメモ(フレーム編)

元画像はこちら。
数学者のイラスト(女性)
suugakusya.jpg
pip で OpenCV のインストール
matplotlibのanimation.FuncAnimationを用いて柔軟にアニメーション作成

Image表示

まずはこんな感じです。
Python, OpenCVで画像ファイルの読み込み、保存

image.png

import cv2
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]
plt.imshow(image)
plt.show()

恒等変換

移動元と移動先が全く同じになるような変換。これだって立派なアフィン変換です。
image.png

image.png

import cv2
import matplotlib.pyplot as plt
import numpy as np

def identity(image):
    h, w = image.shape[:2]
    src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
    affine = cv2.getAffineTransform(src, src)
    return cv2.warpAffine(image, affine, (w, h))

if __name__ == "__main__":
    image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]
    converted = identity(image)
    plt.imshow(converted)
    plt.title("Identity")
    plt.show()

水平移動

単に横にスライドする操作もアフィン変換で書けます。
image.png

添字操作。
【RからPythonへ】ガウス平面上の単位円(Unit Circle)/単位円筒(Unit Cylinder)表示からの再出発
NumPy配列ndarrayの末尾に要素・行・列を追加するappend
[python] スライスでリバース!!(スライスの解説もあるよ!)

import numpy as np
c0=np.arange(0,180,2)
c1=np.append(c0,[180])
c2=c0[::-1]
TimeCode=np.append(c1,c2)
print(TimeCode)

#出力結果
[  0   2   4   6   8  10  12  14  16  18  20  22  24  26  28  30  32  34
  36  38  40  42  44  46  48  50  52  54  56  58  60  62  64  66  68  70
  72  74  76  78  80  82  84  86  88  90  92  94  96  98 100 102 104 106
 108 110 112 114 116 118 120 122 124 126 128 130 132 134 136 138 140 142
 144 146 148 150 152 154 156 158 160 162 164 166 168 170 172 174 176 178
 180 178 176 174 172 170 168 166 164 162 160 158 156 154 152 150 148 146
 144 142 140 138 136 134 132 130 128 126 124 122 120 118 116 114 112 110
 108 106 104 102 100  98  96  94  92  90  88  86  84  82  80  78  76  74
  72  70  68  66  64  62  60  58  56  54  52  50  48  46  44  42  40  38
  36  34  32  30  28  26  24  22  20  18  16  14  12  10   8   6   4   2
   0]

print(len(TimeCode))
#出力結果
181

画像操作。
affine19.gif

import cv2
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

#画面をデフォルトの640*480と設定した場合。
#fig = plt.figure(figsize = (6.4, 4.8))
fig = plt.figure()
#Indx数列を作成。
c0=np.arange(0,180,2)
c1=np.append(c0,[180])
c2=c0[::-1]
indx=np.append(c1,c2)
#元画像読み込み。
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]

def shift_x(image,shift):
    h, w = image.shape[:2]
    src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
    dest = src.copy()
    dest[:,0] += shift # シフトするピクセル値
    affine = cv2.getAffineTransform(src, dest)
    return cv2.warpAffine(image, affine, (w, h));

def update(i):
    plt.cla()
    converted = shift_x(image,indx[i])
    plt.imshow(converted)
    plt.title("Sift "+'x='+str(indx[i]));

ani = animation.FuncAnimation(fig, update, interval=50,frames=len(indx))
ani.save("desktop/python/affine19.gif", writer="pillow")

垂直移動

縦へのスライドもできます。
image.png

画像操作。
affine20.gif

import cv2
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

#画面をデフォルトの640*480と設定した場合。
#fig = plt.figure(figsize = (6.4, 4.8))
fig = plt.figure()
#Indx数列を作成。
c0=np.arange(0,180,2)
c1=np.append(c0,[180])
c2=c0[::-1]
indx=np.append(c1,c2)
#元画像読み込み。
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]

def shift_y(image, shift):
    h, w = image.shape[:2]
    src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
    dest = src.copy()
    dest[:,1] += shift # シフトするピクセル値
    affine = cv2.getAffineTransform(src, dest)
    return cv2.warpAffine(image, affine, (w, h))

def update(i):
    plt.cla()
    converted = shift_y(image,indx[i])
    plt.imshow(converted)
    plt.title("Shift "+'y='+str(indx[i]))

ani = animation.FuncAnimation(fig, update, interval=50,frames=len(indx))
ani.save("desktop/python/affine20.gif", writer="pillow")

ランダムシフト

水平と垂直移動を組み合わせて任意の座標移動ができます。ディープラーニングのData Augmentationで使う「ランダムクロップ」もアフィン変換で再現できます。移動元、移動先の座標はnp.float32にしないと怒られるので注意しましょう。

添字操作
NumPy, randomで様々な種類の乱数の配列を生成

ind = []
for i in range(10):
        rand = (np.random.randint(-40,40),np.random.randint(-40,40))
        ind.append(rand)
indx=np.array(ind)
print(indx)

出力結果
[[  7 -12]
 [-13  20]
 [ 29   9]
 [ 18 -24]
 [ 11  32]
 [  7 -30]
 [ -6  24]
 [-28 -34]
 [-29  22]
 [ -8  33]]

画像操作
affine21.gif

import cv2
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

#画面をデフォルトの640*480と設定した場合。
#fig = plt.figure(figsize = (6.4, 4.8))
fig = plt.figure()
#Indx数列を作成。
ind = []
for i in range(10):
        rand = (np.random.randint(-40,40),np.random.randint(-40,40))
        ind.append(rand)
indx=np.array(ind)
#元画像読み込み。
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]

def random_shift(image, shifts):
    h, w = image.shape[:2]
    src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
    dest = src + shifts.reshape(1,-1).astype(np.float32)
    affine = cv2.getAffineTransform(src, dest)
    return cv2.warpAffine(image, affine, (w, h))

def update(i):
    plt.cla()
    converted = random_shift(image,indx[i])
    plt.imshow(converted)
    plt.title("Shift "+'='+str(indx[i]))

ani = animation.FuncAnimation(fig, update, interval=50,frames=len(indx))
ani.save("desktop/python/affine21.gif", writer="pillow")

拡大・縮小(Scale)

移動系では移動先の座標を足し算で計算していましたが、これを掛け算にかえると拡大・縮小になります。拡大縮小に限った話ではありませんが、補間法も通常のresizeと同様に指定できます。今回は最高品質のLANCZOS法を使ってみました。デフォルトだと線形補間(INTER_LINEAR)になります。
image.png

添字操作

import numpy as np

c0=np.arange(0,2,0.1)
c1=np.append(c0,[2.0])
c2=c0[::-1]
indx=np.append(c1,c2)
print(indx)

#出力結果
[0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.  1.1 1.2 1.3 1.4 1.5 1.6 1.7
 1.8 1.9 2.  1.9 1.8 1.7 1.6 1.5 1.4 1.3 1.2 1.1 1.  0.9 0.8 0.7 0.6 0.5
 0.4 0.3 0.2 0.1 0. ]

画像操作
affine22.gif

import cv2
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

#画面をデフォルトの640*480と設定した場合。
#fig = plt.figure(figsize = (6.4, 4.8))
fig = plt.figure()
#Indx数列を作成。
c0=np.arange(0,2,0.1)
c1=np.append(c0,[2.0])
c2=c0[::-1]
indx=np.append(c1,c2)
#元画像読み込み。
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]

def expand(image, ratio):
    h, w = image.shape[:2]
    src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
    dest = src * ratio
    affine = cv2.getAffineTransform(src, dest)
    return cv2.warpAffine(image, affine, (2*w, 2*h), cv2.INTER_LANCZOS4) # 補間法も指定できる

def update(i):
    plt.cla()
    converted = expand(image,indx[i])
    plt.imshow(converted)
    plt.title("Expand "+'='+str(indx[i]))

ani = animation.FuncAnimation(fig, update, interval=50,frames=len(indx))
ani.save("desktop/python/affine22.gif", writer="pillow")

剪断(Shear)

平行四辺形型の変換ををせん断(Shear)といいます。まずは横方向の歪みを作ります。考え方は水平移動と同じでxx座標を足していくのですが、yy座標ごとに足していくxxを少しずつ変えていくと「水平方向のせん断」の変形になります。
image.png

添字操作
Pythonでスライスを使ってリストの要素を抽出する方法
Pythonのリストからの要素の取り出し(抽出)方法のまとめ
リストのインデックスを複数指定する(Python)
[python] リストから複数のインデックスを指定して値を取得・削除するまとめ

import numpy as np

c0=np.arange(0,180,3)
c1=np.append(c0,[180])
c2=c0[::-1]
c3=np.append(c1,c2)
c4=np.arange(-180,0,3)
c5=c4[::-1]
c6=np.append([0],c5)
c7=np.append(c6,c4[1:len(c4)])
indx=np.append(c7,c3)
print(indx)

#変換結果
[   0   -3   -6   -9  -12  -15  -18  -21  -24  -27  -30  -33  -36  -39
  -42  -45  -48  -51  -54  -57  -60  -63  -66  -69  -72  -75  -78  -81
  -84  -87  -90  -93  -96  -99 -102 -105 -108 -111 -114 -117 -120 -123
 -126 -129 -132 -135 -138 -141 -144 -147 -150 -153 -156 -159 -162 -165
 -168 -171 -174 -177 -180 -177 -174 -171 -168 -165 -162 -159 -156 -153
 -150 -147 -144 -141 -138 -135 -132 -129 -126 -123 -120 -117 -114 -111
 -108 -105 -102  -99  -96  -93  -90  -87  -84  -81  -78  -75  -72  -69
  -66  -63  -60  -57  -54  -51  -48  -45  -42  -39  -36  -33  -30  -27
  -24  -21  -18  -15  -12   -9   -6   -3    0    3    6    9   12   15
   18   21   24   27   30   33   36   39   42   45   48   51   54   57
   60   63   66   69   72   75   78   81   84   87   90   93   96   99
  102  105  108  111  114  117  120  123  126  129  132  135  138  141
  144  147  150  153  156  159  162  165  168  171  174  177  180  177
  174  171  168  165  162  159  156  153  150  147  144  141  138  135
  132  129  126  123  120  117  114  111  108  105  102   99   96   93
   90   87   84   81   78   75   72   69   66   63   60   57   54   51
   48   45   42   39   36   33   30   27   24   21   18   15   12    9
    6    3    0]

画像操作
affine10.gif

import cv2
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

#画面をデフォルトの640*480と設定した場合。
#fig = plt.figure(figsize = (6.4, 4.8))
fig = plt.figure()
#Indx数列を作成。
c0=np.arange(0,180,3)
c1=np.append(c0,[180])
c2=c0[::-1]
c3=np.append(c1,c2)
c4=np.arange(-180,0,3)
c5=c4[::-1]
c6=np.append([0],c5)
c7=np.append(c6,c4[1:len(c4)])
indx=np.append(c7,c3)
#元画像読み込み。
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]

def shear_X(image, shear):
    h, w = image.shape[:2]
    src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
    dest = src.copy()
    dest[:,0] += (shear / h * (h - src[:,1])).astype(np.float32)
    affine = cv2.getAffineTransform(src, dest)
    return cv2.warpAffine(image, affine, (w, h))

def update(i):
    plt.cla()
    converted = shear_X(image,indx[i])
    plt.imshow(converted)
    plt.title("Shear "+'X='+str(indx[i]))

ani = animation.FuncAnimation(fig, update, interval=50,frames=len(indx))
ani.save("desktop/python/affine10.gif", writer="pillow")

同様に垂直方向のせん断を行います。
image.png

affine11.gif

import cv2
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

#画面をデフォルトの640*480と設定した場合。
#fig = plt.figure(figsize = (6.4, 4.8))
fig = plt.figure()
#Indx数列を作成。
c0=np.arange(0,180,3)
c1=np.append(c0,[180])
c2=c0[::-1]
c3=np.append(c1,c2)
c4=np.arange(-180,0,3)
c5=c4[::-1]
c6=np.append([0],c5)
c7=np.append(c6,c4[1:len(c4)])
indx=np.append(c7,c3)
#元画像読み込み。
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]

def shear_Y(image, shear):
    h, w = image.shape[:2]
    src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
    dest = src.copy()
    dest[:,1] += (shear / w * (w - src[:,0])).astype(np.float32)
    affine = cv2.getAffineTransform(src, dest)
    return cv2.warpAffine(image, affine, (w, h))

def update(i):
    plt.cla()
    converted = shear_Y(image,indx[i])
    plt.imshow(converted)
    plt.title("Shear "+'Y='+str(indx[i]))

ani = animation.FuncAnimation(fig, update, interval=50,frames=len(indx))
ani.save("desktop/python/affine11.gif", writer="pillow")

垂直方向のせん断(起点違い)。せん断の起点を右上ではなく左上においた変換です。
image.png

affine12.gif

import cv2
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

#画面をデフォルトの640*480と設定した場合。
#fig = plt.figure(figsize = (6.4, 4.8))
fig = plt.figure()
#Indx数列を作成。
c0=np.arange(0,180,3)
c1=np.append(c0,[180])
c2=c0[::-1]
c3=np.append(c1,c2)
c4=np.arange(-180,0,3)
c5=c4[::-1]
c6=np.append([0],c5)
c7=np.append(c6,c4[1:len(c4)])
indx=np.append(c7,c3)
#元画像読み込み。
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]

def shear_Y(image, shear):
    h, w = image.shape[:2]
    src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
    dest = src.copy()
    dest[:,1] += (shear / w * src[:,0]).astype(np.float32)
    affine = cv2.getAffineTransform(src, dest)
    return cv2.warpAffine(image, affine, (w, h))

def update(i):
    plt.cla()
    converted = shear_Y(image,indx[i])
    plt.imshow(converted)
    plt.title("Shear "+'Y='+str(indx[i]))

ani = animation.FuncAnimation(fig, update, interval=50,frames=len(indx))
ani.save("desktop/python/affine12.gif", writer="pillow")

反転(Flip)

座標を水平方向に反転させるように定義すれば水平反転になります。これもアフィン変換で定義できます。
image.png

image.png

import cv2
import matplotlib.pyplot as plt
import numpy as np

def horizontal_flip(image):
    h, w = image.shape[:2]
    src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
    dest = src.copy()
    dest[:,0] = w - src[:,0] 
    affine = cv2.getAffineTransform(src, dest)
    return cv2.warpAffine(image, affine, (w, h))

if __name__ == "__main__":
    image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]
    converted = horizontal_flip(image)
    plt.imshow(converted)
    plt.title("Horizontal flip")
    plt.show()

水平反転の軸を変えると垂直反転になります。
image.png

image.png

import cv2
import matplotlib.pyplot as plt
import numpy as np

def vertical_flip(image):
    h, w = image.shape[:2]
    src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
    dest = src.copy()
    dest[:,1] = h - src[:,1] 
    affine = cv2.getAffineTransform(src, dest)
    return cv2.warpAffine(image, affine, (w, h))

if __name__ == "__main__":
    image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]
    converted = vertical_flip(image)
    plt.imshow(converted)
    plt.title("Vertical Flip")
    plt.show()

回転(Rotate)

さていよいよ回転です。左上を原点として反時計回りにθだけ回転する操作を考えます。こうしてみると、回転というのはアフィン変換におけるほんの一部でしかないのがわかります。回転は「原点を固定して、三角形の辺の長さが維持されるという縛りをおいたアフィン変換」ということになりますね。
image.png

添字操作

import numpy as np

c0=np.arange(0,90,3)
c1=np.append(c0,[90])
c2=c0[::-1]
c3=np.append(c1,c2)
c4=np.arange(-90,0,3)
c5=c4[::-1]
c6=np.append([0],c5)
c7=np.append(c6,c4[1:len(c4)])
indx=np.append(c7,c3)
print(indx)

#出力結果
[  0  -3  -6  -9 -12 -15 -18 -21 -24 -27 -30 -33 -36 -39 -42 -45 -48 -51
 -54 -57 -60 -63 -66 -69 -72 -75 -78 -81 -84 -87 -90 -87 -84 -81 -78 -75
 -72 -69 -66 -63 -60 -57 -54 -51 -48 -45 -42 -39 -36 -33 -30 -27 -24 -21
 -18 -15 -12  -9  -6  -3   0   3   6   9  12  15  18  21  24  27  30  33
  36  39  42  45  48  51  54  57  60  63  66  69  72  75  78  81  84  87
  90  87  84  81  78  75  72  69  66  63  60  57  54  51  48  45  42  39
  36  33  30  27  24  21  18  15  12   9   6   3   0]

画像操作
affine15.gif

import cv2
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

#画面をデフォルトの640*480と設定した場合。
#fig = plt.figure(figsize = (6.4, 4.8))
fig = plt.figure()
#Indx数列を作成。
c0=np.arange(0,90,3)
c1=np.append(c0,[90])
c2=c0[::-1]
c3=np.append(c1,c2)
c4=np.arange(-90,0,3)
c5=c4[::-1]
c6=np.append([0],c5)
c7=np.append(c6,c4[1:len(c4)])
indx=np.append(c7,c3)
#元画像読み込み。
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]

def rotate(image, angle):
    h, w = image.shape[:2]
    affine = cv2.getRotationMatrix2D((0,0), angle, 1.0)
    return cv2.warpAffine(image, affine, (w, h))

def update(i):
    plt.cla()
    converted = rotate(image,indx[i])
    plt.imshow(converted)
    plt.title("Rotate ="+str(indx[i]))

ani = animation.FuncAnimation(fig, update, interval=50,frames=len(indx))
ani.save("desktop/python/affine15.gif", writer="pillow")

左上を原点とすると画像が大きく切れてしまうので、中心を原点にして回転してみます。ほとんどコードは一緒です。内部的には回転と平行移動の合成変換をやっています。
image.png

画像操作
affine16.gif

import cv2
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

#画面をデフォルトの640*480と設定した場合。
#fig = plt.figure(figsize = (6.4, 4.8))
fig = plt.figure()
#Indx数列を作成。
c0=np.arange(0,90,3)
c1=np.append(c0,[90])
c2=c0[::-1]
c3=np.append(c1,c2)
c4=np.arange(-90,0,3)
c5=c4[::-1]
c6=np.append([0],c5)
c7=np.append(c6,c4[1:len(c4)])
indx=np.append(c7,c3)
#元画像読み込み。
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]

def rotate_center(image, angle):
    h, w = image.shape[:2]
    affine = cv2.getRotationMatrix2D((w/2.0, h/2.0), angle, 1.0)
    return cv2.warpAffine(image, affine, (w, h))

def update(i):
    plt.cla()
    converted = rotate_center(image,indx[i])
    plt.imshow(converted)
    plt.title("Rotate ="+str(indx[i]))

ani = animation.FuncAnimation(fig, update, interval=50,frames=len(indx))
ani.save("desktop/python/affine16.gif", writer="pillow")

中心を原点にして回転しても微妙にはみ出してしまいます。完全にはみ出さない方法は、アフィン変換の合成を意識して使います。

添字操作

import numpy as np

indx=np.arange(0,360,3)
print(indx)

#出力結果
[  0   3   6   9  12  15  18  21  24  27  30  33  36  39  42  45  48  51
  54  57  60  63  66  69  72  75  78  81  84  87  90  93  96  99 102 105
 108 111 114 117 120 123 126 129 132 135 138 141 144 147 150 153 156 159
 162 165 168 171 174 177 180 183 186 189 192 195 198 201 204 207 210 213
 216 219 222 225 228 231 234 237 240 243 246 249 252 255 258 261 264 267
 270 273 276 279 282 285 288 291 294 297 300 303 306 309 312 315 318 321
 324 327 330 333 336 339 342 345 348 351 354 357]

画像操作
affine18.gif

import cv2
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

#画面をデフォルトの640*480と設定した場合。
#fig = plt.figure(figsize = (6.4, 4.8))
fig = plt.figure()
#Indx数列を作成。
indx=np.arange(0,360,3)
#元画像読み込み。
image = cv2.imread("desktop/python/suugakusya.jpg")[:,:,::-1]

def rotate_fit(image, angle):
    h, w = image.shape[:2]
    # 回転後のサイズ
    radian = np.radians(angle)
    sine = np.abs(np.sin(radian))
    cosine = np.abs(np.cos(radian))
    tri_mat = np.array([[cosine, sine],[sine, cosine]], np.float32)
    old_size = np.array([w,h], np.float32)
    new_size = np.ravel(np.dot(tri_mat, old_size.reshape(-1,1)))
    # 回転アフィン
    affine = cv2.getRotationMatrix2D((w/2.0, h/2.0), angle, 1.0)
    # 平行移動
    affine[:2,2] += (new_size-old_size)/2.0
    # リサイズ
    affine[:2,:] *= (old_size / new_size).reshape(-1,1)
    return cv2.warpAffine(image, affine, (w, h))

def update(i):
    plt.cla()
    converted = rotate_fit(image,indx[i])
    plt.imshow(converted)
    plt.title("Rotate ="+str(indx[i]))

ani = animation.FuncAnimation(fig, update, interval=50,frames=len(indx))
ani.save("desktop/python/affine18.gif", writer="pillow")

おや? この操作には見覚えが…
数理考古学】ピタゴラスの定理あるいは三平方の定理からの出発
20190918061420.gif
現段階の私に食い切れるのはここまでの様です。そして、こうした処理をマスク処理と組み合わせるとこんな感じに…
【Python画像処理】スプライト・アニメーションを試す。
sprite04.gif
sprite08.gif
sprite06.gif
そんな感じで、以下続報…

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3