z軸周りに回転された2つの姿勢のクォータニオンのSlerpと角度のLerpの関係性について
問題
z軸まわりの回転角 と
によって得られる2つの姿勢を考える。この2つの姿勢を内分比
で球面線形補間(Slerp)によって内分した姿勢は、角度を線形補間(Lerp)した姿勢と一致するか?
考察
2つの姿勢をクォータニオンで表すとそれぞれ次のようになる。
この2つの姿勢の間を (
)の割合で内分する姿勢を考える。まず、Slerpで補間した姿勢の
成分は次のように表される。
次に、ヨー角を線形補間した姿勢の 成分は以下のように表される。
次のように定義する。
問題は が成り立つかどうかに帰着される。
簡単化のために、以下の定義を行う
この定義のもとで と
を展開すると、次のようになる。
よって が成り立つので、クォータにオンの
成分が一致することが示された。
次に、z成分に関しても同様の確認を行う。
この および
を展開すると次のようになる。
よって も成り立ち、クォータニオンのz成分も一致することが示された。
結論
z軸まわりの回転においては、クォータニオンのSlerpによって得られる姿勢と、ヨー角を線形補間(Lerp)した姿勢は一致する。すなわち、この特定の軸回りの回転において、SlerpとLerpは同一の補間結果を与える。
SegmentTreeはモノイドを扱えるデータ構造という話について
TL;DR
SegmentTreeはモノイドが扱えるデータ構造だという理解をしていたが、モノイドではなく半群を扱えるデータ構造だと理解した方が良いのではないかと考えている。
前提知識
半群とモノイドについて
まず半群とモノイドについて整理する。
半群
結合法則を満たす群構造のことを半群と呼ぶ。結合法則とは、任意の群の要素、
、
について
が成立することを言う。
モノイド
結合法則を満たしかつ単位元が存在する群構造のことをモノイドと呼ぶ。ある群の要素とは単位元であるとは、任意の群の要素
について
が成立することを言う。
すなわち、モノイドと半群の違いは単位元が存在するかどうかである。
SegmentTreeについて
AC libraryによると
モノイド 、つまり
- 結合律
- 単位元の存在
を満たす代数構造に対し使用できるデータ構造です。
長さNのSの配列に対し、
- 要素の1点変更
- 区間の要素の総積の取得
をO(logN) で行うことが出来ます。
と説明されている。コンストラクタは2種類あり、 a. 群の集合、要素間の演算、単位元、SegmentTreeサイズ b. 群の集合、要素間の演算、単位元、初期配列
の2つの与え方がある。
背景
ABC254-F問題をやっていた時に、正の整数列のある区間の最大公約数を高速で求めるためにSegmentTreeが使えないか検討していた。そのとき、SegmentTreeを使う条件として結合則の成立と単位元の存在が要求されるが、結合則の成立は問題なさそうだが、単位元については少し手が止まった。数学的には、あらゆる正の整数を掛け合わせたものすごい大きい整数をが単位元になりそうだが、実装する上ではoverflowが起きてしまうのでそのような単位元は実装上は使えない。そこで、0をそのものすごく大きい数の代用に使おうと考えていたが、ここでふと「あれ、単位元の定義がなぜ必要なのか」と思い始めた。0を単位元として定義するにしても、正の整数列の区間取得を行う限りにおいては0が登場することはなく、なぜ「0を単位元にします」という宣言を実装しないといけないのか疑問に感じ、SegmentTreeのデータ構造の一般性を失わないために、「SegmentTreeは半群に対して使えるデータ構造」という理解をした方がよいのではないかと考えた。
SegmentTreeに単位元を与える必要があるか
SegmentTreeの実装において単位元は次の2ケースで使われるようである。
1.について、ノードをどの値で初期化することに関してはユーザーの責務とする考え方も自然に感じる。単位元で初期化することにより部分的にノードの値が更新されたときにも区間取得クエリが正しく動くため便利という考え方もあるかもしれないが、部分的にノードの値を更新するような操作を許さず、初期化されていないノードを含む区間取得クエリに対しては返す値を保証しない(未定義動作)ということでも良いのではないか。
2.について、これは完全に実装をシンプルにするためだけの都合なので、それを理由にユーザーに単位元を要求してほしくない。
以上から、一般的に「SegmentTreeは半群を扱えるデータ構造」という理解をしたい。ただし、あるライブラリが実装の都合で単位元を要求しその結果として「このSegmentTreeはモノイドを扱えるデータ構造」というように使い方を限定すること自体は問題ないと思っている。
C++クラスのpublic、protected、privateの挙動確認
派生クラスから基底クラスの要素にアクセスできるか
| 基底クラスの指定 \ 派生クラスの継承の仕方 | public | protected | private |
|---|---|---|---|
| public | ◯ | ◯ | ◯ |
| protected | ◯ | ◯ | ◯ |
| private | × | × | × |
クラス外から基底クラスの要素にアクセスできるか
| Baseクラスの指定 \ 派生クラスの継承の仕方 | public | protected | private |
|---|---|---|---|
| public | ◯ | × | × |
| protected | × | × | × |
| private | × | × | × |
派生クラスで基底クラスの要素がどう扱われるか
| Baseクラスの指定 \ 派生クラスの継承の仕方 | public | protected | private |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | no access | no access | no access |
まとめ
クラス外からはpublicのみアクセス可能。派生クラスからは(規定クラスをどのような指定子で継承したかによらず)publicとprotectedがアクセス可能。
派生クラスで規定クラスの要素がどう扱われるかは、「基底クラスの指定子」と「継承指定子」の厳しい方となる。
きたまさ法
概要
数列について、隣接
項間の漸化式および初期条件(第
項までの数列の値)が与えられた時に
を高速に求める方法。言い換えると、
を
で表現する方法。
内容
前提の確認
任意のについて、隣接
項間の漸化式が
で与えられているとする。このとき、
となる係数を、
で表すことが最終的なゴールである。ここで、任意の0以上の整数
について
となることを言及しておく。
(A)
の関係
さて、について考えていくと
となるから、
という関係式を導くことができた。
(B)
の関係
次にについて考えてみると
となるから
という関係式を導くことができた。
解法
(A)と(B)の漸化式を使い、を
で表すために、以下のようなステップで
の計算量で求めることができる。ここで、基本的には現在の値が奇数の時は(A)、偶数の場合は(B)を使って遷移させることになるが、値が
を下回った場合は常に(A)を使って遷移させることに注意。

、
の場合の遷移の例
計算量
(A)の1回あたりの計算量はであり、(B)の1回あたりの計算量は
である。
から
まで減らしていくステップで、(A)は
回、(B)は
回なので、合わせて
である。
参考
画像特徴量
特徴量
SIFT
Scale Invariant Feature Transformの略。2004年発表。Difference of Gaussian (DoG)の極値を探すことでエッジ点を検出。回転、拡大縮小、限定アフィン変換に対して頑強だが、計算コストがかかる。
SURF
Speed Up Robust Featureの略。2008年発表。回転、拡大縮小に対して頑強だが、アフィン変換に対して弱い。SIFTと比べると高速。
KAZE
2012年発表。回転、拡大縮小、限定アフィン変換に対して頑強。
AKAZE
Accelerated-KAZE。2013年発表。回転、拡大縮小、限定アフィン変換に対して頑強。
BRIEF
Binary Robust Independent Elementary Featuresの略。 特徴点の記述に使われ、検出には使えない。
ORB
Oriented FAST and Rotated BRIEF の略。2013年発表。回転、拡大縮小、限定アフィン変換に対して頑強。
BRISK
Binary Robust Invariant Scalable Keypointsの略。2011年発表。
FAST
Features from Accelerated Segment Testの略。
Harris
コーナー検出。1988年
http://www.bmva.org/bmvc/1988/avc-88-023.pdf
参考
Harrisコーナー検出。
を適当な変位とする。画像上のある点
がコーナーであるかを判定するために、点
周りのみ0でない値を持つ窓関数
を用いて
を使い、この値が大きい点でコーナーと判定する。

を変形すると
となる。ここで行列を
と対角化したとすると
と変形できる。これは、という新しい直行軸
を取ったときに、それぞれの方向にどれだけ変化するかを表しており、その変化係数がそれぞれの固有値である。。新しい直行軸は変化の主成分を表しているため、以下のことが言える。
具体例
コーナーの場合
窓関数領域において、方向と
方向がそれぞれ独立に大きな微分
、
を取るとする。そうすると
や
は大きな値をとるが
はある程度互いに独立になり0に近い値になるので、行列
は対角行列に近くなり、固有ベクトルの方向も
軸と
軸に平行になる。

の計算例
辺の場合
例えばx軸に平行な辺の場合、である。
は
と比べると大きくなり、
は互いにキャンセルして0に近くなる。
も小さい値であるが常に2乗で足していっているので
の項よりは大きくなると思うと、やはり行列
は対角行列となり、第1固有値は
軸方向、第2固有値は
軸方向となる。

参考
SIFT
画像ピラミッド
画像ピラミッドとは、重み関数を定義し、この重みに基づいて圧縮した画像をピラミッドのように並べたものである。元の画像のサイズを
とし、
とすると、下から
層目のピラミッドの画像は
であり、
と表される。重み関数としてはガウシアンなどが使われる。またこのように画像サイズを圧縮することをOctaveと呼ぶ

参考
画像フィルターと微分の関係
画像の微分はフィルターをかけることと同値である。
1階微分
となるから、
となるから、これはというフィルターを適用したのと同等である。
2階微分
となるから、
となり、というフィルターを適用したのと同等である。2次元に拡張すれば、2階微分の和であるラプラシアンは
というフィルターとなる。
エッジとラプラシアンの関係
エッジ位置というのは2階微分が0でありかつ1階微分の絶対値が大きいところとして定義できる。したがってラプラシアンのフィルタをかけこの値の絶対値が小さいところをエッジ点とすればよい。

参考
https://www.cse.psu.edu/~rtc12/CSE486/lecture11.pdf
LoGとDoGの関係
まず、ガウシアンフィルターのラプラシアンは、ガウシアンのラプラシアンのフィルターになる。すなわち、をガウシアンフィルタ
とし、ピクセルの値をとすると、
となる。
証明
1次元の場合は、
となるので、
一方、異なる標準偏差のガウシアンを引き算すると、ガウシアンの2階微分の形に近くなる。

の異なるガウシアンフィルターを使って同じ画像サイズの画像をいくつか生成し、その差分をとることでラプラシアンフィルターの近似を得て、その絶対値が小さいところとしてエッジを検出する。また、その時に、1)同じ
のフィルターをかけた画像内での周辺8ピクセルおよび2)前後の
のフィルターをかけた画像内での周辺および自分の位置9ピクセルを使って、異なる
のフィルターをかけた画像においてもエッジとなっているかを確認したのち正式な特徴点として登録される。

参考
https://www.cse.psu.edu/~rtc12/CSE486/lecture11.pdf
Descriptorの計算
特徴点の周辺を領域に分割し、各領域ごとに
ごとに合計8方向の特徴ベクトルを用意する。したがってSIFTの1つの特徴点は合計128次元のベクトルである。
マルチスレッド
マルチスレッドの例
#include <thread> void thread1() { printf("thread1\n"); } void thread2() { printf("thread2\n"); } int main() { std::thread th1(thread1); std::thread th2(thread2); // joinを呼ぶと、そのスレッドが終了するまで現在のコンテクストが中断する th1.join(); th2.join(); return 0; }
import threading
def thread1():
print("thread1")
def thread2():
print("thread2")
t1 = threading.Thread(target=thread1)
t2 = threading.Thread(target=thread2)
print("start")
t1.start()
t2.start()
t1.join()
t2.join()
print("end")
グローバル変数を複数スレッドから書き込む失敗例
#include <thread> int cnt = 0; void thread1() { printf("thread1 start cnt = %d\n", cnt); for (int i=0; i<100000; i++) { cnt++; } printf("thread1 end cnt = %d\n", cnt); } void thread2() { printf("thread2 start cnt = %d\n", cnt); for (int i=0; i<100000; i++) { cnt++; } printf("thread2 end cnt = %d\n", cnt); } int main() { std::thread th1(thread1); std::thread th2(thread2); // joinを呼ぶと、そのスレッドが終了するまで現在のコンテクストが中断する th1.join(); th2.join(); return 0; }
出力例
thread1 start cnt = 0 thread2 start cnt = 422 thread2 end cnt = 77428 thread2 end cnt = 115025
排他処理を入れてグローバル変数を更新する例
#include <thread> #include <mutex> int cnt = 0; std::mutex mtx; void add_count() { std::lock_guard<std::mutex> lock(mtx); cnt++; } void thread1() { printf("thread1 start cnt = %d\n", cnt); for (int i=0; i<100000; i++) { add_count(); } printf("thread1 end cnt = %d\n", cnt); } void thread2() { printf("thread2 start cnt = %d\n", cnt); for (int i=0; i<100000; i++) { add_count(); } printf("thread2 end cnt = %d\n", cnt); } int main() { std::thread th1(thread1); std::thread th2(thread2); // joinを呼ぶと、そのスレッドが終了するまで現在のコンテクストが中断する th1.join(); th2.join(); return 0; }
出力例
thread1 start cnt = 0 thread2 start cnt = 0 thread2 end cnt = 195262 thread1 end cnt = 200000
最終的にグローバル変数が意図通りの値になっている。
import threading
import time
lock = threading.Lock()
value = 0
def thread(n):
lock.acquire()
global value
value = value + n
value = value - n
lock.release()
print(f"start value={value}")
ts = []
N = 4
for i in range(N):
t = threading.Thread(target=thread, args=(i,))
t.start()
ts.append(t)
for i in range(N):
ts[i].join()
print(f"end value={value}")
セマフォ
import threading
import time
# Semaphoreというオブジェクトもある
# Semaphore: acquireせずにreleaseして残セマフォ数を初期値以上に増やすことができる
# BoundedSemaphore: qcquireしないとreleaseできない
s = threading.BoundedSemaphore(2)
def thread(n):
s.acquire()
time.sleep(5)
print(f"thread: {n}")
s.release()
# こちらの獲得方法も可能
def thread2(n):
with s:
time.sleep(5)
print(f'thread2: {n}')
print("start")
for i in range(10):
t = threading.Thread(target=thread, args=(i,))
t.start()
print("end")
とやると、文字が2つずつ画面に出力される。
実行されるタイミングの確認
std::threadをコールした段階でマルチスレッドが立ち上がりスレッド処理が始まる。joinをコールした段階で開始するわけではないことに注意。
#include <thread> #include <mutex> #include <unistd.h> int cnt = 0; std::mutex mtx; void add_count() { std::lock_guard<std::mutex> lock(mtx); cnt++; } void thread1() { printf("thread1 start cnt = %d\n", cnt); for (int i=0; i<100000; i++) { add_count(); } printf("thread1 end cnt = %d\n", cnt); } void thread2() { printf("thread2 start cnt = %d\n", cnt); for (int i=0; i<100000; i++) { add_count(); } printf("thread2 end cnt = %d\n", cnt); } int main() { std::thread th1(thread1); printf("cnt before sleep = %d\n", cnt); usleep(1000000); // 1 sec printf("cnt after sleep = %d\n", cnt); std::thread th2(thread2); // joinを呼ぶと、そのスレッドが終了するまで現在のコンテクストが中断する th1.join(); th2.join(); return 0; }
出力例
cnt before sleep = 0 thread1 start cnt = 0 thread1 end cnt = 100000 cnt after sleep = 100000 thread2 start cnt = 100000 thread2 end cnt = 200000
usleep直後の段階ではまだth1.join()は呼んでいないが、thread1の関数の処理が実行完了していることがわかる。
マルチスレッドで実行する関数の返り値を設定する
PythonではThreadを継承したクラスを作りrunメソッドをオーバーロードすればよい
import threading
import time
def myfunc(n):
ans = 0
for i in range(n):
ans += i
return ans
class MyThread(threading.Thread):
def __init__(self, func, args):
# 親クラスのコンストラクタを呼ぶことを忘れない
super().__init__()
self.func = func
self.args = args
def run(self):
self.result = self.func(*self.args)
def get(self):
return self.result
t1 = MyThread(myfunc, args=(10,))
t2 = MyThread(myfunc, args=(100,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"result1={t1.get()}")
print(f"result2={t2.get()}")
参考
Pythonマルチスレッドの戻り値を取得する2つの方法 - JPDEBUG.COM
デッドロック
import threading
import time
l1 = threading.Lock()
l2 = threading.Lock()
def func1():
l1.acquire()
time.sleep(2) # 時間のかかる処理を模擬
g1 = 10
l2.acquire()
g2 = 10
l2.release()
l1.release()
def func2():
l2.acquire()
time.sleep(2) # 時間のかかる処理を模擬
g2 = 10
l1.acquire()
g1 = 10
l1.release()
l2.release()
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
t1.start()
t2.start()
t1.join()
t2.join()
デッドロック対策
- プロセスが利用するリソースをまとめて1つのロックで管理するようにし、実行開始前にリソース全体のロックを取得する。これによりリソースの利用率は低下する。
- リソースのロックをかける順番を揃える。
参考
コメント
- 複数スレッドから同じ変数の値を書き出す時だけでなく、読み込む時にむ排他処理を入れた方が良い。読み込み中に他スレッドによって値が更新されると値がundefinedになることがある。
参考
高速フーリエ変換
問題概要
、
が与えられたとき、
を高速に求めたい。普通に計算するとの計算量となるが、これを
に高速化することを目指す。
問題の詳細は以下を参照 atcoder.jp
フーリエ変換
ある次多項式
があったとして、この多項式の
個の
に対する値
がわかれば係数が決定できるはずである。適当に
を選んでしまうと
と
の間の関係式は単純にならないが、1の
乗根を選ぶと直行性により綺麗な関係になるということである。
と定義すると
が成立する。
証明
より、
よって、の異なる
この点で関数
の値が一致することが示されたので、この変換の妥当性が示された。
関数の値の効率の良い求め方。
は
次式なので、
の
この値について
を普通に計算しようとするとの計算量となる。これを効率よく計算するため部分問題に分割する。
の項を偶数部分
と奇数部分
に分割すると、それぞれ
の次数になるので、
したがって、元の定義と見比べれば
と表されることがわかる。
計算量の評価
について、
]を求めるのにかかる計算量を
とする。上で説明した解法により、サイズが半分になった問題
を2つ解き、それを
について足し合わせればよいので
という漸化式が成立する。ここから
