粗大メモ置き場

個人用,たまーに来訪者を意識する雑記メモ

RICOH THETA_SCで多重露光・合成をしてPixel4みたいにクリアな星空を撮りたい

この記事はたまたま集まった技術&もの作りが好きな人たち Advent Calendar 2019の記事です。

はじめに

それはゴミのような論文を泣く泣く提出した日の夜でした。デスマを終えて見上げた空は信じられないくらい澄んでいてそれはもう最高に晴れやかな気分でした。

『こら撮らな!』と思いたって家からTHETAを引っ張り出してきて撮ったのが以下の写真です。

f:id:ossyaritoori:20191207235116p:plain
オリオンをなぞる

何度かパシャパシャ撮ってから帰りましたが,やっぱり後でビューワーで視るとまだ少し物足りない感じがします。

そこで,Google Pixel4でも使われている多重露光を行って合成してより精細な写真を撮るという手法を試してみたくなりました。

以下がPixel4で撮った写真の一例らしいです。カッコよすぎんか。私も撮りたい。 https://image.itmedia.co.jp/news/articles/1910/17/ki_1609376_pixel4camera01.jpg

戦略

暗い夜空をきれいに取るためには露光時間を長くすればよいのですが,長すぎると星々が動いてしまいます。

Google Pixel4の戦略としては短めの露光を複数回撮って,それらを重ね合わせてノイズを除去しているようです。

japanese.engadget.com

ということで私も以下のような流れをとることにしました。

  • 360°カメラで星空を複数回撮影
  • 複数の画像を合成してノイズを落とす
  • 星空の回転に合わせて画像を補正

諸々の処理のプログラムはPythonで書きます。

Step0: 星空を撮る

撮影に使っているのはRICOHの全天球カメラのTHETA_SCです。

RICOH 360度カメラ RICOH THETA SC (ブルー) 全天球カメラ 910743

RICOH 360度カメラ RICOH THETA SC (ブルー) 全天球カメラ 910743

  • 発売日: 2016/10/28
  • メディア: エレクトロニクス

扱いやすく個人的には360度カメラの入門にはもってこいと思います。 撮像素子はデジカメでは標準的な 1/2.3型,レンズのF値2.0とまぁまぁの明るさなので15秒程度露光すれば都心の夜空は十分に綺麗に撮れます。

都市郊外で空を撮る感覚だと 露光20-30秒 ,ISO:100-160程度が向いていそうです。

当日は小型の三脚に固定して撮影しました。

全天球カメラで撮った画像はEquirectalgular (正距円筒図法)という形式で保存されます。

f:id:ossyaritoori:20191208001928p:plain
スマホのパノラマで一周して撮ったときも同じような画像になりますね。


本稿ではこの画像の中心を原点とします。

横方向:水平方向へは一周で2π,縦方向は緯度方向なので一周でπのスケールがあります。 したがってざっくり画像座標→正距円筒座標の変換を書くと以下のようになります。

hei,wid,_ = image.shape
cx,cy = wid/2,hei/2
Equilecpoints = (np.array(polarpoints)-np.array([cx,cy]))/np.array([wid/2/pi,-hei/pi])

Step1:多重露光を合成する

ということで多重露光をした画像を合成する段階に入ります。 本来なら露光やISOを統一するべきなのですが,合成を思いついたのが撮影会後なので設定がバラバラな画像になってしまいました。

何も考えずにピクセル輝度の平均をとると以下のようにぼやけた画像になってしまいます。

f:id:ossyaritoori:20191210093417p:plain
建物と空の境界が曖昧に白っぽくなってしまう

そこでOpenCVにあるHDR合成(ハイダイナミックレンジ合成)のライブラリを使用してみることにしました。

HDR合成とは簡単に言うと露光条件の異なる画像を組み合わせて白飛び黒飛びを軽減した画像を撮像することです。

丁寧にまとめてくれている方がこちら(文献[1]): [CV]High-Dynamic-Range(HDR) Imagingについて - Qiita

今回は最も楽そうなMergeMertensという手法を用いました。 Pythonで書くなら以下のような感じです。

import cv2
merge=cv2.createMergeMertens()
proc1 =merge.process([img1,img2,img3])

f:id:ossyaritoori:20191210112502p:plain
MergeMertensを用いた複数画像の合成結果


先ほどのようなモヤが生じる問題を回避できましたが,よく見ると星の動きが軌跡となっています。これはこれで乙ですがくっきりとした星空が見たいですね。

実際オリオン座の周辺に着目すると,星空が動いているのがよくわかります。

f:id:ossyaritoori:20191208001147g:plain
短い間にも空は動いている

露光を繰り返した3分強の間にも星々はよく動いていることがわかります。

Step 2 星空の回転を補正する

先程のGIF画像の様に星空は一日で天球を一回転します。回転中心は北極星Polaris)近辺の極北にあたります。

OpenCVの既存関数による位置ずれ補正

OpenCVにもAlignMTBというクラスがあり,calculateShiftという関数で位置ずれを補正できるそうですがプログラムの中身を見た感じこれは微小並進量のみの対応です。

また,一般の平面画像であれば射影変換によって回転ズレを補正できます。 ossyaritoori.hatenablog.com

しかし,Equirectangular画像においては回転操作は非線形変換になります。

Equirectangular画像における三次元回転

回転操作がどの様に影響するか,次の座標変換から導出することができます。

f:id:ossyaritoori:20191210112100p:plain
3D直交座標と球面極座標の関係。文献[2]から引用

逆変換は以下の通りです。


\begin{align}
x = cos \phi cos \theta, \\
y = cos \phi sin \theta, \\
z = sin \phi
\end{align}

星空はrが変化しない無限遠にあるので,変換後のEquirectangular画像の \theta’,\phi’は天球上での三次元回転行列を用いて


\begin{pmatrix}
 x' \\ y' \\ z'
\end{pmatrix}
= R\begin{pmatrix}
 x\\ y \\  z
\end{pmatrix},  \  \theta' = tan^{-1}\frac{x’}{y'} ,\ \phi'=sin^{-1}z'

と計算できます。

実際変形する際には (\theta’,\phi’)=(i,j)に対応する元画像の座標を逆変換から求め,ピクセル値をBilinear補間で求めます。

ロドリゲスの回転行列の計算

回転行列はOpenCVでサポートされているロドリゲスの回転行列を用います。


星空は原点と極北≒北極星を通る回転軸の反時計回りに回転します。1秒あたり1/240度なので微小のように思われますが,先程のGIFのように複数回露光をすると動きとして効いてきます。

変化を補正するために手動で極北の座標を決定した後,画像のExif情報から撮像の間隔を取得して回転変換をかけます。

# polarw :Equirectangularでの極北のtheta,phi
nx = np.cos(polarw[1])*np.cos(polarw[0])
ny = np.cos(polarw[1])*np.sin(polarw[0])
nz = np.sin(polarw[1])

# omega = time_diff/3600/24*2*pi
Rrod,_ = cv2.Rodrigues(omega*np.array([nx,ny,nz]))

このRrodは先程の式における回転行列Rの転置になります。

変換&重ね合わせの結果

ということで重ね合わせの結果です。

f:id:ossyaritoori:20191210161026p:plain
星空が合うようにマッチングしたので建物などがぶれて見える。

重ね合わせが成功して満足してしまいましたが,見えないものが見えるわけではないのでもっと暗い所に行って多重露光合成の威力を試したくなりました。 まずは休暇が欲しい…

STEP3 :自動で極北を抽出(途中)

SIFTやAKAZE特徴点でマッチングした画像は案の定,空と建物の境界にハマります。 星々をマッチングできれば回転を検出できますが,やはり空と建物・地面の分離を事前にやる必要がありそうです。

f:id:ossyaritoori:20191210212145p:plain
案の定有効な特徴点は空との境界に寄るため星空間の対応は難しそう。

夜空部分の抽出

先程のマッチングでは信頼度の低いマッチング結果を除去していました。具体的には距離情報で縛りをかけています。

良いマッチングを得るためにユークリッド距離の比を元に選別するコードがよく書かれています。

for m,n in self.matchings: 
   if m.distance < 0.75 * n.distance:
         #good.append()        

この選別部分をなくした場合のマッチング点(AKAZE)はいくつか空の星々を捉えています。

f:id:ossyaritoori:20191213110200p:plain
青:画像1のAKAZE特徴点,オレンジ:画像2のAKAZE特徴点(マッチング実施後の対応点)

したがって,最初の選別有りのマッチングにて建物と空の協会となるY座標を抽出し,(今回はY=619)Y座標がその閾値以下,すなわちカメラから見てより上方にある箇所を空の領域と仮定しました。 その結果絞り込まれたのが以下の17点の対応です。

f:id:ossyaritoori:20191213112316p:plain
Y座標を元に夜空の部分の特徴点のみを抽出した

3次元座標での回転推定

Equirectangular平面内ではお互いの変換は非線形になってしまうので,一度求めた特徴点の対応を三次元球面極座標写像してから対応する回転行列を求めます。

三次元回転の最適化といえば金谷先生の教科書を参照するのが良いでしょう。

3次元回転: パラメータ計算とリー代数による最適化

3次元回転: パラメータ計算とリー代数による最適化

また,一応英語のテキストが無料で転がっていたりします。

https://igl.ethz.ch/projects/ARAP/svd_rot.pdf

 \sum Rx_i-y_iを最小化する回転Rは対応点x_iとy_iを列方向に並べた行列X,Yを用いて表した

 S=XY^\top特異値分解して得た U\Sigma V^\top=Sから,

 \hat{R} = V diag(1,1,..,det(VU^\top)) U^\top

のように計算できます。なお,ここで推定したRはデータによっては厳密な回転行列にならない場合があります。それを補正するために同様の操作を繰り返します。 こちらは金谷先生の教科書に記述があるのでそちらを確認してください。

numpyでは以下のように書きます。np.linalgのsvdがVを転置の状態で出すことには注意です。

def estimate3dR(X,Y):
    S=np.dot(X,Y.T)
    U,W,V=np.linalg.svd(S)
    Rhat_ = np.dot(V.T,U.T)
    Rhat = np.linalg.multi_dot([V.T,np.diag([1,1,np.linalg.det(Rhat_)]),U.T])
    # additional Step For Optimization
    U,W,V=np.linalg.svd(Rhat)
    Rhat_ = np.dot(V.T,U.T)
    Rhat = np.linalg.multi_dot([V.T,np.diag([1,1,np.linalg.det(Rhat_)]),U.T])
    return Rhat

ランダムサンプルによるロバスト推定

ところで上記の手法でそのまま回転を計算すると,外れ値の影響を受けてしまいます。

この外れ値を除去するためにランダムサンプルによる手法が数多く提案されており,有名なRANSACなどはopenCVでの推定アルゴリズムにも採用されています。

二次文献ですが以下のブログがわかりやすいと思います。

www.sanko-shoko.net

RANSACの完全な実装はなかなかに面倒で,以下のブログを参考にしました。 クラスの継承を有効に使うコードを初めて書いた気がします。これはこれで長いので別記事で。

一目で分かるRANSAC - Qiita

その結果,

  • 手動設定の回転行列
array([[ 0.99989064, -0.00811519, -0.01236295],
       [ 0.00808042,  0.99996327, -0.00285925],
       [ 0.0123857 ,  0.00275904,  0.99991949]])
  • RANSACで推定した回転行列
array([[ 0.99994968, -0.00893819, -0.00455381],
       [ 0.00895058,  0.99995627,  0.00270865],
       [ 0.0045294 , -0.00274927,  0.99998596]])

はい。そこそこまともな値が抽出できてきました。 観測にノイズが混じっているのでこれは厳密には回転行列ではないので,補正が必要になります。(TODO:もう1つSubsectionが必要)

とりあえず,これをこのサイトにて分解して回転軸と回転角(rad)を抽出してみると,

[ -0.2625027, -0.4368636, 0.8603735 ], 0.0103961

これは,約143秒の露光という判定です…うーん,一分ほど短い? 北極星の位置も微妙にずれた検出をしているのでマッチングの精度が悪かったかRANSACの調整ミスですね。(TODO)

また,回転中心の位置推定もやはり手動には敵いません。これはなかなか手ごわいです。

f:id:ossyaritoori:20191214002406p:plain
白:本来の北極星,黄色:自動推定した空の回転中心


とまぁこれ以上詳細に書くとめちゃ長くなるので別記事にします。 とりあえず,

  • 星空の領域抽出
  • 星空の運動の推定

がネックとなるため最初持っていたイメージよりかは簡単には自動化できないという感覚を得ました。 手動で北極星さえ指定すればExif情報で基本的に推定できるのでこの頑張りがそもそも必要かというのも少し怪しげですが気力があれば続きも記事にします。

TODO

  • 空の領域とそうでない領域の分離:雑にDone
  • 回転中心の自動抽出(特徴点マッチングを用いる):雑にDone
  • 三次元回転推定まわりを整形:多分やる
  • 公開ようにプログラムを整形:やる?

おまけプログラム's

PILでgif画像を保存する関数

from PIL import Image
def saveasGif(images,filename='out.gif',duration=500,loop=0):
    imlist = []
    for img in images:
        img_array = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        imlist.append(Image.fromarray(img_array))
        
    imlist[0].save(filename, save_all=True, append_images=imlist[1:], duration=duration,loop=loop)

Exifデータから撮影間隔を取得する関数

from PIL.ExifTags import TAGS
from datetime import datetime

# Exif データを取得
def get_exif_of_image(file):
    """Get EXIF of an image if exists.
    指定した画像のEXIFデータを取り出す関数
    @return exif_table Exif データを格納した辞書
    """
    im = Image.open(file)

    try:
        exif = im._getexif()
    except AttributeError:
        return {}

    exif_table = {}
    for tag_id, value in exif.items():
        tag = TAGS.get(tag_id, tag_id)
        exif_table[tag] = value
    return exif_table

def get_diff_time(file1,file2):
    """Calculate capture interval of images from  EXIF
    指定した2つの画像ファイル間の撮影時間の間隔を計算する
    """
    FMT = '%H:%M:%S'
    exif1=get_exif_of_image(file1)['DateTime']
    exif2=get_exif_of_image(file2)['DateTime']
    dtime = datetime.strptime(exif2[-8:], FMT) - datetime.strptime(exif1[-8:], FMT)
    return min(3600*24-dtime.seconds,dtime.seconds)

多くなりすぎたのでStep3に関しては別記事に移行する気がします。

参考文献等

[1] HDRなどに関して:[CV]High-Dynamic-Range(HDR) Imagingについて - Qiita

[2] 球面極座標に関する図表などの引用:360度パノラマ画像から平面画像への変換手法 - LASTMILE WORKS / DYNAMO TECH - R&D Project - Medium

[3] パノラマ画像から矩形画像を切り取るプログラム:GitHub - NitishMutha/equirectangular-toolbox: Handy tool for equirectangular images

他余裕でき次第追記

OpenCVで使われる座標系の作法メモ

OpenCVの座標系

座標系というやつはどう定義されているかが非常に重要です。 従って参照にすべきはQiitaでもなく,このブログでもなく公式APIリファレンスです。

これに書いてあることが全てです。でもなかなか探しづらいのでここにメモを書きます。

前提of前提

超前提知識ですが,OpenCVでは以下の前提があります。

  • 右手系
  • カメラの座標系において,左上が原点,横方向がX(右が正),画像の縦方向がY(下が正),奥行き方向がZ(奥が正)

OpenCVで使われる回転の作法

回転の記述法でメジャーどころと言えば以下の3つが挙げられます。

OpenCVではちょくちょく回転を表す3×1のベクトルを算出しますが,これは全てロドリゲスの回転公式に基づいています。

公式のcv2.Rodrigues()のレファレンスを覗くと

f:id:ossyaritoori:20190302003143p:plain

とあります。

日本語でいうと,「ベクトルのノルムが回転角,正規化されたベクトルが回転軸」を表します。

X軸[1,0,0]まわりに45度回転させたい場合は以下のように書けばいいわけです。

import cv2
import numy as np

R, Jacob = cv2.Rodrigues(np.array([math.pi/4,0,0]))
print(R)
#R = array([[ 1.        ,  0.        ,  0.        ],
#        [ 0.        ,  0.70710678, -0.70710678],
#        [ 0.        ,  0.70710678,  0.70710678]])

なお,cv2.Rodriguesは返り値を2つ返すので注意。

従って以下のようなオイラー角のDCM変換を手で書いている人はOpenCV上では使うのをやめた方がいいです。 ぱっと書きやすいので私は好きなんですが…

# Calculates Rotation Matrix given euler angles.
def eulerAnglesToRotationMatrix(theta) :
     
    R_x = np.array([[1,         0,                  0                   ],
                    [0,         math.cos(theta[0]), -math.sin(theta[0]) ],
                    [0,         math.sin(theta[0]), math.cos(theta[0])  ]
                    ])
         
         
                     
    R_y = np.array([[math.cos(theta[1]),    0,      math.sin(theta[1])  ],
                    [0,                     1,      0                   ],
                    [-math.sin(theta[1]),   0,      math.cos(theta[1])  ]
                    ])
                 
    R_z = np.array([[math.cos(theta[2]),    -math.sin(theta[2]),    0],
                    [math.sin(theta[2]),    math.cos(theta[2]),     0],
                    [0,                     0,                      1]
                    ])
                     
                     
    R = np.dot(R_z, np.dot( R_y, R_x ))
 
    return R

# Checks if a matrix is a valid rotation matrix.
def isRotationMatrix(R) :
    Rt = np.transpose(R)
    shouldBeIdentity = np.dot(Rt, R)
    I = np.identity(3, dtype = R.dtype)
    n = np.linalg.norm(I - shouldBeIdentity)
    return n < 1e-6
 
 
# Calculates rotation matrix to euler angles
# The result is the same as MATLAB except the order
# of the euler angles ( x and z are swapped ).
def rotationMatrixToEulerAngles(R) :
 
    assert(isRotationMatrix(R))
     
    sy = math.sqrt(R[0,0] * R[0,0] +  R[1,0] * R[1,0])
     
    singular = sy < 1e-6
 
    if  not singular :
        x = math.atan2(R[2,1] , R[2,2])
        y = math.atan2(-R[2,0], sy)
        z = math.atan2(R[1,0], R[0,0])
    else :
        x = math.atan2(-R[1,2], R[1,1])
        y = math.atan2(-R[2,0], sy)
        z = 0
 
    return np.array([x, y, z])

OpenCVでの座標変換計算の数式

前節でOpenCVがどのような形で座標を扱うことを想定しているかがわかりました。次はそれがどのように使われることを想定されているかです。

例えばOpenCVではカメラの位置を図るような関数がいくつか存在します。既知の3次元点と投影された画像の点との対応からカメラの姿勢を計算するPNP問題を解く場合が特にそうです。

心当たりがあるのは

  • SolvePNP系統
  • arucoモジュールのEsitmatePose系

でしょうか。

rvecやtvecの扱い

これらでは得られたカメラの位置として返り値にrvecsとtvecsという3×1のベクトルを返してきます。 気になるのはこれらは果たしてどの座標系に属しているのかということです。(都合等をよく考えたらカメラ座標系にきまっているんですが)

以前の記事でもカメラ変位をカメラtoグローバルなのかグローバルtoカメラなのかで扱いが変わることを書いたと思います。 ossyaritoori.hatenablog.com

結論から言うとrvecsやtvecsはカメラのLocal座標系から見た値で表されています。 従って,

OpenCVで測ったカメラ姿勢R_c=Rodrigues(rvec_c)^\topから測った回転物の姿勢R_o=Rodrigues(rvec_o)^\topはグローバル座標系では


^g R_{o} = R_c R_o

と計算できるわけです。

検証用投影プログラム

自分はちょっと疑い深いので実際に計算して確かめました。

三次元の点(グローバル座標)をカメラの視点に投影するcv2.projectPoints()という関数があります。 imgpts, jac = cv2.projectPoints(3Dpoints, rvecs, tvecs, K, dist)のように使えば3次元点を2次元に投影することができます。

f:id:ossyaritoori:20190302005627p:plain
親の顔よりみた式

この中身をDistortionを含めないで書くならこういう風になります。

tc=tvecs
Rc,_=cv2.Rodrigues(rvecs)
def camProjection(points,Rc,tc,K=np.eye(3),campose_is_global=0):
    '''
    Input: CameraPose, Camera Matrix, Object Points in Global Coordinate
    Output: Projected Points
    '''
    h,w = points.shape
    if h != 3:
        print("Input do not match points style!")
    if campose_is_global:#assume Rc,tc is from global frame
        Proj = K.dot(np.concatenate([Rc.T, -np.dot(Rc.T,tc)], axis=1))
    else:#assume Rc,tc is from camera frame( opencv default)
        Proj = K.dot(np.concatenate([Rc, tc], axis=1))        
    pts = np.vstack((points,np.ones((1,w))))
    converted_pts= Proj.dot(pts)
    return Projection(converted_pts)

def Projection(points):
    h,w = points.shape
    if h != 3:
        print("Input do not match points style!")
    Dep = points[2,:]
    Depth = np.tile(Dep,(2,1))
    return points[0:2,:]/Depth

これを使った検証の結果,やはりOpenCVではrvecsやtvecsはカメラのLocal座標系から見たものに適切に変換して使う必要があります。

Homography行列の分解 OpenCV Python

OpenCVは便利なんですが不十分な情報や古い情報,ニセの情報がネット上に多すぎます. 基本的に公式のサイトを見ような.公式っぽいけど違うみたいな奴多すぎ. これがまともだったので公式と信じる.

はじめに:Homography 行列の推定とか

findHomographyにぶち込みます. 昔書いたテンプレート抽出のコードを参考にどうぞ.

キャリブレーションはがんばりましょう.decomposeはエラーにめっちゃ敏感です. ROSを使えばかなり簡単にキャリブレーションできます。100枚程度あれば結構いいスコアを出せるので頑張って。

過去にもエピポーラ幾何でなんかやってますね,この時なキャリブレーションが甘かったので結果をやり直してみたい. ossyaritoori.hatenablog.com

cv2.decomposeHomographyMat

公式?によると使い方は以下の通り.

retval, rotations, translations, normals   =   cv.decomposeHomographyMat(  H, K[, rotations[, translations[, normals]]]    )

生で吐くとこんな値になります.

(4, [array([[ 0.99069192, -0.0745761 ,  0.1138768 ],
       [ 0.07708256,  0.99686649, -0.01776175],
       [-0.11219536,  0.02637434,  0.99333609]]), array([[ 0.99069192, -0.0745761 ,  0.1138768 ],
       [ 0.07708256,  0.99686649, -0.01776175],
       [-0.11219536,  0.02637434,  0.99333609]]), array([[ 0.8447263 ,  0.02653746,  0.53454021],
       [-0.1212945 ,  0.98227444,  0.14291458],
       [-0.52127259, -0.18556049,  0.8329719 ]]), array([[ 0.8447263 ,  0.02653746,  0.53454021],
       [-0.1212945 ,  0.98227444,  0.14291458],
       [-0.52127259, -0.18556049,  0.8329719 ]])], [array([[ 0.30840663],
       [-0.09586909],
       [-0.81377852]]), array([[-0.30840663],
       [ 0.09586909],
       [ 0.81377852]]), array([[ 0.67210665],
       [ 0.17138383],
       [-0.53426701]]), array([[-0.67210665],
       [-0.17138383],
       [ 0.53426701]])], [array([[ 0.92347388],
       [ 0.23136337],
       [-0.30605063]]), array([[-0.92347388],
       [-0.23136337],
       [ 0.30605063]]), array([[ 0.64092667],
       [-0.04427804],
       [-0.76632399]]), array([[-0.64092667],
       [ 0.04427804],
       [ 0.76632399]])])

そもそもSVDを使ったdecompositionでは4つの解が出ます. したがって4つずつあるこの回転Rと並進T, 法線ベクトルnのペアから一番まともなペアを選びます.

ネコと学ぶ...さんの超わかりやすいサイトより,以下の画像を拝借.

この場合(a)のパターンが正しいです.

これの解き方は一般にわかっている点群を投影して,すべての距離が正になるかで判断します.

全容を理解するのにはInriaの2008年の文献がわかりやすそうです.(ただし長い)

解を絞る条件

要約すると

  • カメラ平面から見て同じ側に有る:8点から4点へ(ここまではOpenCVがやる)
  • Reference Point Visibility:4点から2点へ
  • のこりは...?:normal vectorからなどでやる。

ということみたいで,事前情報なしで絞れるのはせいぜい2つまでで,それ以降は例えばテンプレートのnormalベクトルがどっちを向いているかなどの条件を使います。

例えば,どちらもこちら側にあるかというのは変換前とあとのnormalベクトルがどちらもカメラ側を向いているという条件をつけることで2つに絞れますし, テンプレートが真正面から撮ったものであればnormalベクトルは(0,0,1)になっているはずなのでそれとの内積を計算して大きさを評価するなどが挙げられます。

n_ref = np.float32([0,0,1]).reshape(3,1)
for n in normals:
    print(np.dot(n.T,n_ref))

こんなかんじでやればいいですね。

サンプルコード

Gistにnotebookを上げておきました。多分これで本番環境でも行ける。 キャリブレーションはしっかりしておきましょう。 gist.github.com

解の絞り方,その他の手法などについて

連続的なフレームからの抽出の場合動作に制約をつけて式を組み直したり<例>,その他のセンサとの情報統合からこれを解くなどのアプローチがあります。

anacondaやpipでSIFTやSURFを使えるようにする。Contribパッケージの入れ方。

Windows側のAnacondaで環境を改変したのでメモ。

OpenCVでSIFTが使えない問題

出るよね。こういうエラー。

module 'cv2' has no attribute 'xfeatures2d' anaconda

OpenCV3以降になってからSIFTやSURF等のライセンスが必要な奴らはcontribというExtensionみたいなフォルダに分離され,普通のインストールでは入らなくなっています。
解決法はcontribを自分でコンパイルするか,誰かがコンパイルしたのをいただく事。

なお,2017年12月のROS Kinetic のモジュールにはきちんと含まれています。
なのでウチのUbuntuPCでは大丈夫。
問題はWindowsのPCでおこりました。

セットアップ内容は以下を参照
ossyaritoori.hatenablog.com

Anacondaでの解決法

以下を参照。contribを含むパッケージを探します。
OpenCV 3(core + contrib)をWindows & Python 3の環境にインストール&OpenCV 2とOpenCV 3の違い&簡単な動作チェック - Qiita

以下の通りでダウングレードはしましたが無事使えるようになりました。

conda install -c https://conda.anaconda.org/menpo opencv3

pip 使いのための解決法

普段pipを使っている人はこちら。

pipとAnaconda併用するとわけわからない事になりがち。
どうもVerの新しい方が優先されるっぽい?

contribも別で配布しているので両方入れるには以下の様にやります。

pip install opencv-python
pip install opencv-contrib-python

以下の記事もあったりしてどれがいいかはちょっとよくわかっていないです。
qiita.com

どう使うか

マッチングのやり方とかは公式OR以下の例を参照。
ossyaritoori.hatenablog.com

結論

Anacondaを使っている以上はできるだけconda使うべきかもしれない。

ROSでGPUを有効にしたOpenCVを使用する

前回の記事でOpencvをcudaのサポート込でコンパイルしたわけですが今度はこれをROSで使えるように教えこまないといけません.

ここで問題になるのがOpencv with cudaとRosのOpencv3パッケージの競合です.
自分でコンパイルした方を使いたいのですが,ROSの方にはcv_bridgeを始めとする超重要パッケージもあるので片方を消すわけにも行かない...

以下の記事にもあるように割と根深い問題です.
qiita.com

Step1. findpackageで正しいpathを指定する。

はじめにfindpackageの部分をいじって、OPENCV_LIB等に正しいPATHを認識させます。

cmakelistのfindpackageがどこを見ているのかチェック

ガチの初心者&雰囲気で理解しているので間違っていたらコメントください.
CMakeListでははじめに
findpackage()を呼んでインクルードしたいパッケージのPathを探すようです.
その後,そのPathを用いて目的のソースとライブラリを関連付けます.

${FUGA}の中身を確認するには?

次のような文をprintfよろしく書き連ねることでcmakeをするときにWarning文として出力できます.
多分正攻法ではない気がしますがとにかく動きます.WARNINGをつけているのは該当箇所に黄色い見出しがついて見やすくなるからです。

MESSAGE(WARNING "prefix ${CMAKE_PREFIX_PATH}")    
MESSAGE(WARNING "version ${OpenCV_VERSION}")
MESSAGE(WARNING "install path ${OpenCV_INSTALL_PATH}") 
MESSAGE(WARNING "config path ${OpenCV_CONFIG_PATH}") # look at the output of this message
MESSAGE(WARNING "include dirs ${OpenCV_INCLUDE_DIRS}")
MESSAGE(WARNING "libs ${OpenCV_LIBS}")

OpenCVのPathを指定する

私はcmakeとかLinuxとかガチガチの初心者でしたのでcmakeの中身についてメモをば.
以下のような文を足すことで確かにCmakeにOpencvのデフォルトパッケージの位置を教えることができます.

find_package(OpenCV 3 REQUIRED
NO_MODULE 
PATHS /usr/local 
NO_DEFAULT_PATH)

cmakeとcatkin_makeで結果が違うんだけど!?

そうして試してみると,ある時cmakeでコンパイルした時とcatkin_makeでコンパイルした時で結果が違うことにきづきました.

どうもcatkin_makeでは前回のBuildに依存しているようで,一度Buildとdevelフォルダを消し飛ばす必要があります.
How to tell Catkin_make to use opencv with gpu? - ROS Answers: Open Source Q&A Forum

develをまるごと消すとdarknet_rosとかに不都合が生じるのできちんとバックアップをとっておくように!

そうして再コンパイルしてこの事件は解決...かにおもわれました.

STEP2. catkin_LIBRARIESとの競合解決

問題提起

上記の方法で、
結局ソースコードと正しいOpencvとを関連付けることができませんでした.

原因は最後のLinkに有ると思っています.

target_link_libraries(my_function ${OpenCV_LIBS} ${catkin_LIBRARIES})

このCatkin_librariesがある$catkin_develを見たところ,以下のpathが紛れておりバッチリROSのOpencvを参照していたからです.

/opt/ros/kinetic/include/opencv-3.2.0-dev

ということは,現状次の解決策はROSのOpencvとBuildしたOpencvのうち,被っている箇所のどちらかを吹き飛ばすか,先にBuildした方のOpencvのPathを先に解決させることが考えられます.(小学生並みの希望的観測)

が,どっちもやり方がよくわかんないので誰かおしえてください〜〜〜ってなっているのが現在の状況.

追記:catkin_librariesからROSのopencvを追い出す

cmakeの変数はどれも文字列として扱う事ができるので、そこからROSのOpencvに相当するところだけを頑張って置換する事ができます。

書き方の例は

string(REPLACE "StringYouWantReplace" "ReplacedString" LIBafter "${LIBbefore}" )

こんな感じ。

私の場合、catkin_LIBRARIESからOpencv関連を全てを消し飛ばすのが最も有効でした。
なお、cv_bridge等のパッケージはOpencv2準拠で別枠らしいのでこの方法では支障をきたすことはありません。

string(REPLACE "/opt/ros/kinetic/lib/libopencv_calib3d3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_core3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_features2d3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_flann3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_highgui3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_imgcodecs3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_imgproc3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_ml3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_objdetect3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_photo3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_shape3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_stitching3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_superres3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_video3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_videoio3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_videostab3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_viz3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_aruco3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_bgsegm3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_bioinspired3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_ccalib3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_cvv3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_datasets3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_dpm3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_face3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_fuzzy3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_hdf3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_line_descriptor3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_optflow3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_phase_unwrapping3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_plot3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_reg3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_rgbd3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_saliency3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_stereo3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_structured_light3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_surface_matching3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_text3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_xfeatures2d3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_ximgproc3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_xobjdetect3.so.3.2.0;/opt/ros/kinetic/lib/libopencv_xphoto3.so.3.2.0;" "" catkin_LIBRARIES "${catkin_LIBRARIES}")

フォーラムでクソ長い質問をしたかいがありました。
How to tell Catkin_make to use opencv with gpu? - ROS Answers: Open Source Q&A Forum

テスト用ソース、サンプルプロジェクト

cmakelistsをどう書くと動いたり動かなかったりするのか調べるのに使ったサンプルプロジェクト。良ければどうぞ。
GitHub - YoshiRi/ros_opencv_conflict: Immitate ROS opencv version confliction

未解決問題

  • 何故か色々いじっているうちにimage_pipelineのstereo_image_procが激遅になってしまって非常に悲しい。
  • OpenCVをCudaでコンパイルしたバージョンがROSのバージョンよりも遅い現象がある。OpenCLを有効化していないせい?

user profile overview - OpenCV Q&A Forum

小技集

catkin_make -DOpenCV_INCLUDE_DIRS=/opt/opencv/include

みたいな感じでBuild時に教えることができますが,雑に試した結果はうまく行っていません.

あと、知らなかったんですが

catkin_make --only PROJECT

ってやれば一つのプロジェクトだけコンパイルできるんですね...
これ使えば他との競合を考えずにコンパイルできたかもしれない。

OpenCV バージョン指定

cmakelistで

SET(OCV_VERSION "3.1")
find_package(OpenCV ${OCV_VERSION})

と書けば該当するOpencvを指定できますね。
これも有効な方法でしょう。

checkPackage("OPENCV_CORE" "Error statement")
と打つことで、該当パッケージが入っているのか確認できるのもグッドポイント。

保存用データ

ros-kinetic-oepncv3のライブラリ

opencv_calib3d;opencv_core;opencv_features2d;opencv_flann;opencv_highgui;opencv_imgcodecs;opencv_imgproc;opencv_ml;opencv_objdetect;opencv_photo;opencv_shape;opencv_stitching;opencv_superres;opencv_video;opencv_videoio;opencv_videostab;opencv_viz;opencv_aruco;opencv_bgsegm;opencv_bioinspired;opencv_ccalib;opencv_cvv;opencv_datasets;opencv_dpm;opencv_face;opencv_fuzzy;opencv_hdf;opencv_line_descriptor;opencv_optflow;opencv_phase_unwrapping;opencv_plot;opencv_reg;opencv_rgbd;opencv_saliency;opencv_stereo;opencv_structured_light;opencv_surface_matching;opencv_text;opencv_xfeatures2d;opencv_ximgproc;opencv_xobjdetect;opencv_xphoto

ビルドしたライブラリ

opencv_xphoto;opencv_xobjdetect;opencv_ximgproc;opencv_xfeatures2d;opencv_tracking;opencv_text;opencv_surface_matching;opencv_structured_light;opencv_stereo;opencv_sfm;opencv_saliency;opencv_rgbd;opencv_reg;opencv_plot;opencv_optflow;opencv_line_descriptor;opencv_hdf;opencv_fuzzy;opencv_face;opencv_dpm;opencv_dnn;opencv_datasets;opencv_cvv;opencv_ccalib;opencv_bioinspired;opencv_bgsegm;opencv_aruco;opencv_videostab;opencv_videoio;opencv_video;opencv_superres;opencv_stitching;opencv_shape;opencv_photo;opencv_objdetect;opencv_ml;opencv_imgproc;opencv_imgcodecs;opencv_highgui;opencv_flann;opencv_features2d;opencv_cudev;opencv_cudawarping;opencv_cudastereo;opencv_cudaoptflow;opencv_cudaobjdetect;opencv_cudalegacy;opencv_cudaimgproc;opencv_cudafilters;opencv_cudafeatures2d;opencv_cudacodec;opencv_cudabgsegm;opencv_cudaarithm;opencv_core;opencv_calib3d

Build OpenCV for GPU version in Jetson TX1

容量に関する問題

OpenCVソースコードからフルでコンパイルするには容量が4GB必要になります。
16GBの容量のTX1にそれは非常に厳しい...

外部メモリ上でのビルド

似たような状況がRaspberryPiにもあるようで、そちらの記事を参照しました。
PkLab - Install OpenCV 3.2 Python/C++ on Raspberry PI

USBをLinuxファイルシステムとして認識させてその上でコンパイルします。

USBの中身のフォーマット

とにかくUSBの中身をLinuxで見れるext2等のフォーマットへと変える必要があります。

sudo fdisk -l

で中身を確認しながらパーティションをクリアできるとありましたが、
私の場合うまく行かなかったので以下のサイトに準拠してフォーマットしました。
How to format a USB flash drive? - Ask Ubuntu


マウント

私の場合/dev/sda1が対象のUSBのデバイス名なので

sudo mkfs -t ext2 /dev/sda1

これでLinuxファイルシステムとしてフォーマットして
次のコマンドでUSBメモリの中身をLinuxファイルシステムから確認する事ができるようになります。

mkdir ~/usbmem
sudo mount /dev/sda1 ~/usbmem

Opencvのダウンロードとコンパイル

大部分はOpenCV: Building OpenCV for Tegra with CUDAを参考にしています。

ソースのダウンロードと所々の修正は[参考文献1]を。
その後contrib moduleをダウンロードするところは[参考文献2]を参照しています。
SIFTとかも使いたいですからね。

git clone https://github.com/opencv/opencv.git
git cherry-pick 10896
git cherry-pick cdb9c
git cherry-pick 24dbb
cd opencv-3.1.0
git clone --depth 1 https://github.com/Itseez/opencv_contrib.git opencv_contrib
cd opencv_contrib
git fetch origin --tags --depth 1
git checkout 3.1.0

Dependencyの解決

依存するパッケージをインストールします。

sudo apt-add-repository universe
sudo apt-get update
sudo apt-get install \
    libglew-dev \
    libtiff5-dev \
    zlib1g-dev \
    libjpeg-dev \
    libpng12-dev \
    libjasper-dev \
    libavcodec-dev \
    libavformat-dev \
    libavutil-dev \
    libpostproc-dev \
    libswscale-dev \
    libeigen3-dev \
    libtbb-dev \
    libgtk2.0-dev \
    pkg-config

準備していない人はPython環境もちゃんと準備しましょう。(下は2.7用)

sudo apt-get install python-dev python-numpy python-py python-pytest

Cmake と make

追記:完全に参考文献1に準拠し直しました。

cmake .. -DWITH_OPENGL:BOOL=ON -DWITH_QT:BOOL=ON -DWITH_CUDA:BOOL=ON -DCUDA_ARCH_BIN="5.2" -DCUDA_ARCH_PTX="5.2" -DCMAKE_BUILD_TYPE=RelWithDebugInfo -DCMAKE_INSTALL_PREFIX=/usr/local -DBUILD_TESTS:BOOL=OFF -DBUILD_PERF_TESTS:BOOL=OFF -DWITH_FFMPEG:BOOL=OFF -DENABLE_NEON:BOOL=ON -DBUILD_EXAMPLES:BOOL=ON -DINSTALL_C_EXAMPLES:BOOL=OFF -DINSTALL_PYTHON_EXAMPLES:BOOL=ON -DOPENCV_EXTRA_MODULES_PATH=../opencv_contrib/modules
以下古いバージョン(不具合あり)

多分どれかがおかしい...
Cmakeですが、参考文献1と2をあわせた手法を取っています。>||
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DBUILD_PNG=OFF \
-DBUILD_TIFF=OFF \
-DBUILD_TBB=OFF \
-DBUILD_JPEG=OFF \
-DBUILD_JASPER=OFF \
-DBUILD_ZLIB=OFF \
-DBUILD_EXAMPLES=OFF \
-DBUILD_opencv_java=OFF \
-DBUILD_opencv_python2=ON \
-DBUILD_opencv_python3=OFF \
-DENABLE_PRECOMPILED_HEADERS=OFF \
-DWITH_OPENCL=OFF \
-DWITH_OPENMP=OFF \
-DWITH_FFMPEG=ON \
-DWITH_GSTREAMER=OFF \
-DWITH_GSTREAMER_0_10=OFF \
-DWITH_CUDA=ON \
-DWITH_GTK=ON \
-DWITH_VTK=OFF \
-DWITH_TBB=ON \
-DWITH_1394=OFF \
-DWITH_OPENEXR=OFF \
-DCUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda-8.0 \
-DCUDA_ARCH_BIN=5.3 \
-DCUDA_ARCH_PTX="" \
-DINSTALL_C_EXAMPLES=OFF\
-DINSTALL_TESTS=OFF \
-DOPENCV_EXTRA_MODULES_PATH=../opencv_contrib/modules
|

  • DCMAKE_INSTALL_PREFIX を /sur/localにするか迷う...(よくわかっていない)


次にmakeします。

make -j4 VERBOSE=1

このVERBOSE=1を入れることでエラーが出た時の詳細が出て進めやすくなります。
(エラー出る前提)
よく出たエラーについては下を参照。

make install

最後のこのコマンドでmakeした結果の実行ファイルなどをローカルのPATHの通ったLibなどにコピーします。
(間違っていたら教えてください)

make install

時々sudoでやっているサイトを見るのだけれどどっちが正しいのだろう?うまく行かなかったらsudoで。

sudo ldconfigを行う場合もあるそうな。

エラー集

当然出まくるエラー。

hdf5.h : No such file or directory

もしhdf5.hがあるのにこのエラーが出るなら
Pythonがhdf5を探せていないのが原因。

modules/python/common.cmake の最後の行に以下の2文を追加。

find_package(HDF5)
include_directories(${HDF5_INCLUDE_DIRS})

can't find hdf5.h when build caffe · Issue #156 · NVIDIA/DIGITS · GitHub
build error for python bindings with opencv_contrib modules · Issue #6016 · opencv/opencv · GitHub

onlineMIL.hpp でのエラー

In file included from /home/ubuntu/usbmem/opencv/opencv_contrib/modules/tracking/include/opencv2/tracking/tracker.hpp:48:0,
                 from /home/ubuntu/usbmem/opencv/build/modules/python2/pyopencv_generated_include.h:49,
                 from /home/ubuntu/usbmem/opencv/modules/python/src2/cv2.cpp:12:
/home/ubuntu/usbmem/opencv/opencv_contrib/modules/tracking/include/opencv2/tracking/onlineMIL.hpp:57:23: error: expected unqualified-id before ‘>’ token
 #define  sign(s)  ((s > 0 ) ? 1 : ((s<0) ? -1 : 0))

この場合、同様の定義が他のcppファイルで使われているのが原因。
error building ```opencv_contrib/modules/tracking/include/opencv2/tracking/tracker.hpp``` · Issue #618 · opencv/opencv_contrib · GitHub

onlineMIL.hppにある次の文をカットして
src/omlineMIL.cppへとペーストする。

#define  sign(s)  ((s > 0 ) ? 1 : ((s<0) ? -1 : 0))

cuda_gl_interop.hに関するエラー

/usr/local/cuda-8.0/includes にある同盟ファイルの GL/gl.hのインクルード周りを次のように変更。

//#if defined(__arm__) || defined(__aarch64__)
//#ifndef GL_VERSION
//#error Please include the appropriate gl headers before including cuda_gl_interop.h
//#endif
//#else
#include <GL/gl.h>
//#endif

Webカメラから画像が取れない?

謎。v4l2周りのエラーということはわかっているのだが...。謎。

テスト

Python

python

import cv2
cv2.__version__
print(cv2.getBuildInformation())

Cpp

以下の通りでコンパイル可能。

g++ -ggdb XXXX.cpp -o XXXX `pkg-config --cflags --libs opencv`

ソースの削除について

基本的にsudo make installを済ませたらもとのOpencvのフォルダに用はないです。
jetsonは容量がカツカツなので中でコンパイルした人はデリート推奨。
私は怖いのでまるっとドライブにバックアップしました。
stackoverflow.com

実行の際のコツ(最初に空の関数を呼び出す)

Opencvでは初回の関数呼び出し時にGPUを初期化する的な動作があるため,
実行する際にはとても時間がかかるという問題があります.

実際にOpenCVのフォーラムに問い合わせたところ,
以下のように空のcuda関数を呼び出すことで初期化を済ませることができるという回答を得ました.

cv::cuda::GpuMat test;
test.create(1, 1, CV_8U);

answers.opencv.org

にゃーんAAジェネレータ

にゃーん。

f:id:ossyaritoori:20170929174031p:plain

にゃ?

ーーーーーーーーーーーー                                          んん                      にんに      ーー
ーーーーーーーーーーーー      に  にゃににににに            にににゃゃゃにに    にににににににに  ーーーーー
ーーーーーーーーーーーー      にーゃ        ににゃににゃに    ー  にゃゃににに  ににゃに    にに  に    ーー
ーーーーーーーーーーーー      に  にに              にゃに  ん      ににゃゃに        にゃにゃにに      ーー
ーーーーーーーーーーーー        に  ゃ                    ににににに      に      にににににゃにに      ーー
ーーーーーーーーーーーーー        にゃに                  ににににに              ににににに  ゃ      ーーー
ーーーーーーーーーーーーーーー      にゃ                      に            ん          に      に        ー
ーーーーーーーーーーーーーー          にに                  ゃゃゃゃ                  ゃゃゃに    に      ー
ーーーーーーーーーーーーーーー      に          ん          ゃゃゃに                  ゃゃゃ      に      ー
ーーーーーーーーーーーーーーー      ゃ      ににんににに              ん    にゃゃゃゃに            に    ー
ーーーーーーーーーーーーーーーーーーにににににに    にににに                ゃゃゃゃゃゃ            に    ー
ーーーーーーーーーーーーーーー      ゃ  にゃゃゃんゃゃゃに                    にゃゃに          に  に    ー
ーーーーーーーーーーーーーーー      ににに                に                                    に  ー    ー
ーーーーーーーーーーーーーーー        に                          に      ゃゃゃゃに      ん      に      ー
                      ーーーー          に          ー              ににに        にににに      に        ー
                            ーー          にに                                              に          ーー
  に      ににに                ー        ににゃゃに              んん  ん          ににゃゃに          ーー
  に  ににににゃににに                  に        ににゃゃゃににににんににににゃゃゃにに      にに          
      ににににに  にに  に            に                にゃゃにににににゃゃ            に        にに      
                  ゃに    に          に                  ゃ          にゃゃゃににんににーに          に    
ーーー                に    ゃ      にーに              にゃ    ににゃに          ににゃに          ーに    
ーーーーーー            に    に    ゃ  にゃ              にににに                ゃに                ゃ    
ーーーーーーーー        ゃ    ゃ    に    にゃに                              にゃに              にゃ      
ーーーーーーーーーー    に    に    に        にゃに        ん          にゃゃにににににににににゃにゃーー  
ーーーーーーーーー        に    に  に            ににゃににににににゃにに                ん        ゃ    ー
ーーーーーーーーー        ゃ    に  ゃ                      にに                  ー                に    ー
ーーーーーーーーーー        に    にゃ                                  ーーーーーーーー          に      ー
ーーーーーーーーーー          にに  にににに      ーーーーーーーーーーーーーーーーー            に        ー
ーーーーーーーーーーー                にゃに      ーーーーーーーーーーーーーー                に          ー
ーーーーーーーーーーーーー                にー    ーー                                  ににゃに        ーー
ーーーーーーーーーーーーーーーー      ー  に      ー          に  ん        ににににゃゃに    に        ーー
ーーーーーーーーーーーーーーーーー          ー  ー        にに  んんんん      ゃに          ーーー      ーー
ーーーーーーーーーーーーーーーーー      に    ー        に                      にに          に      ーーー
ーーーーーーーーーーーーーーーーー        に        にに          ーーー            にに    に        ーーー


にゃ

にゃーん