OpenCVでラスタースクロールする

# はじめに
アニメーションGIF大好き。

# 配列（画像）をシフトする　numpy.roll()
numpyには配列をシフトする関数がある。

**numpy.roll(a, shift, axis=None)**

 - a　numpy配列。
 - shift　移動量。整数。shiftとaxisはタプルで表現することで複数方向に同時にシフトすることができる。
 - axis　配列の軸。省略可能だが明記したほうがいいだろう。省略したらどうなるかは君の目で確かめてくれ！

numpyだからOpenCVの画像にも使える。「配列の軸」はカラー画像の場合`0`,`1`,`2`がそれぞれ`縦`,`横`,`カラーチャンネル`を意味することはすぐに分かると思う。
では、たとえば`axis=1`（横移動）で`shift`に正の値を指定したときに右に動くのか左に動くのかは、慣れて覚えるしかない。高校の時、y=f(x)のグラフとy=f(x-a)のグラフを並べて描いてなぜ「そっち」に移動するのかを悩みつつ自分の頭で理解するのと同じだ。

|元画像||
|:-:|:-:|
|![ocean_small.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/22f5f44a-c2b1-65a7-497c-13d78de86f38.jpeg)|   |
|**axis=0に+20シフト**|**axis=0に-20シフト**|
|![ocean_shift_y_plus.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/1916f419-df94-e25d-d5d8-3712547439d9.png)|![ocean_shift_y_minus.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/be50ef9f-76ec-8a97-ccdc-e8aa7f546b9b.png)|
|**axis=1に+100シフト**|**axis=1に-100シフト**|
|![ocean_shift_x_plus.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/ce6839fd-d378-92c9-b1b8-34b3e9c63039.png)|![ocean_shift_x_minus.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/95f33e90-31db-5fe7-c518-42b628c4e915.png)|
|**axis=2に+1シフト**|**axis=2に-1シフト**|
|![ocean_shift_c1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/14413350-7a46-8088-57bf-f30168e4209a.png)<br>青の輝度が高い→緑の輝度が高いにシフト|![ocean_shift_c2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/d425ba17-8ab8-a103-069d-3b17e5ef50a0.png)<br>青の輝度が高い→赤の輝度が高いにシフト|

# スクロールする
無限ループ内で`numpy.roll()`をおこなえば無限スクロールができる。
両端がつながっている画像が望ましいことは言うまでもない。

```Python:scroll.py
import cv2
import numpy as np

filename = "keepout.png"
back_origin = cv2.imread(filename)
a = -2                  # speed
while True:
    back = back_origin.copy()
    back = np.roll(back, a*t, axis=1) 
    cv2.imshow("img", back)
    t += 1
    if cv2.waitKey(1) == ord("q"):
        break
cv2.destroyAllWindows()
```

|元画像|アニメーション|
|:-:|:-:|
|![keepout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/785ae156-35f8-5f84-b8f1-0d16581574cf.png)|![keepout.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/381f9f5c-7821-a3a5-e9bd-ddc2da648ee3.gif)![keepout.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/381f9f5c-7821-a3a5-e9bd-ddc2da648ee3.gif)![keepout.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/381f9f5c-7821-a3a5-e9bd-ddc2da648ee3.gif)|

上に掲げたソースは画面上では動くがアニメGIFを作ってくれるわけではない。アニメGIFの作り方はこちらを参照のこと。
　　[OpenCVでコマ撮りカメラを作る　～PILとOpenCVの画像変換＆PILでアニメGIF作成～](https://qiita.com/mo256man/items/a2bfdf2393953314fa39)


# ラスタースクロールする
画像全体でなく行ごとに異なる量の`numpy.roll()`をおこなえばラスタースクロールができる。

## 上半分のみスクロールさせる　～流れる雲～
元画像の雲は厳密には左右でつながっていないのだが、動きがあるためそれほど気にならない。これもいらすとやの人徳といったところか。

|元画像|アニメーション|
|:-:|:-:|
|![lavender.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/11c35685-238e-a543-6142-0e5c050fea9d.jpeg)|![lavender.gif](https://raw.githubusercontent.com/mo256man/komono/main/lavender.gif)|

ソースは略。


## 一行ずつシフト量を変える　～揺らめく海底～
20世紀末のテレビゲームでよく見ることができた演出。
実際にそれらがどのような処理をしていたのかは知らない。

|元画像　ocean.jpg|アニメーション|
|:-:|:-:|
|![ocean.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/21eb6179-0584-8598-7a48-e8ecd6d2e6ad.jpeg)|![ocean.gif](https://raw.githubusercontent.com/mo256man/komono/main/ocean.gif)|
ソースはこちら。

```Python:ocean.py
import cv2
import math
import numpy as np

back_origin = cv2.imread("ocean.jpg")
imgH, imgW = back_origin.shape[:2]

# ↓を有効にしておくと動きがよくわかる
# cv2.line(back_origin, (imgW//2,0), (imgW//2,imgH), (0,0,255), 3)
a = imgW//16            # 振幅
b = 2*math.pi/imgH      # 周波数
c = 0                   # 位相のずれ
step = 1                # 計算のステップ 波の粗さ・計算量に関係
tt = 16                 # 周期
t=0
while True:
    back = back_origin.copy()
    for y in range(0, imgH, step):
        x = int(a*math.sin(2*math.pi*(t-c)/tt+b*y))
        back[y:y+step] = np.roll(back[y:y+step], x, axis=1)
    cv2.imshow("", back)
    t += 1
    if cv2.waitKey(1) == ord("q"):
        break

cv2.destroyAllWindows()
```

この上に背景が透過処理されている魚のイラストを重ね合わせればダライアスのできあがりだ。

|ダライアス|
|:-:|
|![darius.gif](https://raw.githubusercontent.com/mo256man/komono/main/darius.gif)|


## 一行ずつシフトする方向を変える　～分身の術～

|元画像　ninja.jpg|アニメーション|
|:-:|:-:|
|![ninja.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/c88d1e1c-8bb1-1d7f-a43b-218227240dd5.jpeg)|![ninja.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/0750107c-0216-5c0d-52ec-cdca9a108706.gif)|
ソースはこちら。

```Python:ninja.py
import cv2
import math
import numpy as np

back_origin = cv2.imread("ninja.jpg")
imgH, imgW = back_origin.shape[:2]

a = imgW//4             # 振幅
c = 0                   # 位相のずれ
step = 1                # 計算のステップ 波の粗さ・計算量に関係
tt = 32                 # 周期
t=0
while True:
    back = back_origin.copy()
    for y in range(0, imgH, step):
        sign = 1 if y%2==0 else -1
        x = int(sign*a*math.sin(2*math.pi*(t-c)/tt))
        back[y:y+step] = np.roll(back[y:y+step], x, axis=1)
    cv2.imshow("", back)
    t += 1
    if cv2.waitKey(1) == ord("q"):
        break

cv2.destroyAllWindows()
```

## 一行ずつシフト量と方向を変える　～旅の扉～
あらためて調べると、ドラクエの旅の扉って作品ごと機種ごとに演出が違うのな。
ラスタースクロールと無関係な処理が多いのでソースは略。

|アニメーション|
|:-:|
|![tabinotobira.gif](https://raw.githubusercontent.com/mo256man/komono/main/tabinotobira.gif)|

## 投影変換と組み合わせる　～3Dレースゲーム～

### 疑似3Dの実装
`numpy.roll()`はデータのシフト（平行移動）なので変形はできない。投影変換で長方形を台形に変形することで疑似3Dを実装できる。
投影変換についてはこちらを参照のこと。
　　[投影変換で画像を変形する　～モニタ画面をハックする～](https://qiita.com/mo256man/items/27d0a44071aee5f06933)

|元画像 road.jpg|投影変換を無駄にアニメ化|
|:-:|:-:|
|![road.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/3604eb9f-f967-36e8-0e8e-5d644d2c44a9.png)<br>アメリカのルート66をイメージしたのに<br>左側通行になってしまった。|![road_anim_2.gif](https://raw.githubusercontent.com/mo256man/komono/main/road_anim_2.gif)<br>幅が縮小されるだけでなく<br>高さ方向にも変形するのか。|
|**numpy.roll()**|**numpy.roll()+投影変換**|
|![road_anim_1.gif](https://raw.githubusercontent.com/mo256man/komono/main/road_anim_1.gif)|![road_anim_3.gif](https://raw.githubusercontent.com/mo256man/komono/main/road_anim_3.gif)|

### カーブの実装
カーブを実装するにはもう一つ手間が必要だ。
上底をオフセットするだけではこうなってしまう。

|上底オフセットなし|上底オフセット|
|:-:|:-:|
|![road1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/1f3567c9-5e8b-4ae5-0f83-36f587ce5e13.png)|![road02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/62e389e4-2856-029d-3985-7077c65432d2.png)|

なんとかうまいことカーブを描けないか。こんな方法で実装してみた。
まず、目指すカーブの曲線を決める。下底では垂直（まっすぐ）で、次第にカーブし上底でオフセット量に達する曲線。円弧でも[クロソイド](https://www.musashino-u.ac.jp/academics/faculty/engineering/mathematical_engineering/math_class/003.html)でもいいが、ここでは計算が楽な二次関数とした

|上底オフセット（再）|赤が二次関数|
|:-:|:-:|
|![road02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/62e389e4-2856-029d-3985-7077c65432d2.png)|![road03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/10ca8fd7-1ea7-7411-7363-d3dbd72761e0.png)|

これまでの例では1行ずつオフセットさせたので、ここでは台形であることを利用して少し大雑把な変形をさせてみよう。

|投影変換＋赤の曲線|画像を投影変換＋赤の曲線|
|:-:|:-:|
|![road_anim_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/812a93a1-3fc6-c32b-22cb-f0c58cf1caac.gif)|![road_anim_5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/fd53d824-495b-b9ab-821a-178bb60c4228.gif)|

6分割もすれば十分にカーブに見えることが分かった。
`numpy.roll()`した画像に対しこの分割投影変換をおこなえばカーブのできあがりだ。

|カーブのアニメ|
|:-:|
|![road_anim_6.gif](https://raw.githubusercontent.com/mo256man/komono/main/road_anim_6.gif)|

こいつにアレとコレとソレを合体させればハングオンのできあがりだ。
こういうことがあるからスプライト関数には回転機能が必要なのだ。
　　[OpenCVでスプライトを回転させる #3　～人任せにせず自分で計算せよ～](https://qiita.com/mo256man/items/d89c53a39c8e82b9c889)

|ハングオン|
|:-:|
|![road_anim_8.gif](https://raw.githubusercontent.com/mo256man/komono/main/road_anim_8.gif)|


# 終わりに
毎度のことながら、いらすとやさんにはお世話になっています。
「上底」「下底」という用語を台形の面積の公式以外で使ったのは初めてだ。