セキュリティキャンプ2019に参加してきました!
どうも、高林です。
今回はセキュリティキャンプに参加した感想を書こうと思います!
自分よりはるかにレベルの高い人たちと一緒に学ぶことができてとても成長できたと感じています!!
ここでは、自分が何の講義を取ったのかを話したいと思います。
自分が選択した講義
A1~3「インシデントレスポンスで攻撃者を追いかけろ」
この講義では、実際にインシデントが発生したときにどう対処していくのかを学習しました。
とはいっても、当日はCTF形式の演習だけで、ツールの使い方などは事前学習で行いました。
事前課題では講師の方がとても丁寧に対応してくれました!
マルウェア感染後の対応などは、独学でやるには限界があるので、ここで教えていただいたことはとても貴重な体験でした。
トラックA「インシデントレスポンスで攻撃者を追いかけろ」では講義内CTFが終了し、順位発表と表彰が行われました。CTFでの考察結果を個人で発表しました。 #seccamp pic.twitter.com/u4Zn0qpJg8
— セキュリティ・キャンプ (@security_camp) August 14, 2019
D4「組込みリアルタイムOSとIoTシステム演習 ~守って!攻めて!ロボット制御バトルで体験する組込みセキュリティ~ 」
この講義では、実際にロボットを使ってセキュリティの重要さを理解する、という講義が行われました。
事前課題にマイコンが配られて、RTOSについて実際に手を動かして学ぶことができました!
トラックDでは松原 豊さんによる「組込みリアルタイムOSとIoTシステム演習 ~守って!攻めて!ロボット制御バトルで体験する組込みセキュリティ~」が始まりました。今日は事前学習で学んだ知識を生かしてグループ競技に取り組みます。 #seccamp pic.twitter.com/BxqCjV3IHh
— セキュリティ・キャンプ (@security_camp) August 15, 2019
B5「体系的に学ぶモダン Web セキュリティ」
この講義では、主にブラウザ側のセキュリティ対策について学びました。
Content-Security-PolicyやSame-Origin-Policyなどのポリシーから、XSSやCSSを用いた攻撃手法などを学びました!
こちらも事前課題がとても充実しており、タイトル通り「体系的に」学ぶことができたと思います。
自分はCTFではrev、pwnをやるのでWebはさっぱりだったのですが、隣に座っていたすごいツヨツヨな方になんども助けてもらいました。
こういうすごい方と知り合えるのもセキュリティキャンプの良いところですね!
トラックB午後の講義は、米内 貴志さんによる「体系的に学ぶモダンWebセキュリティ」です。ブラウザの有するモダンなセキュリティ機構を、その背景にある攻撃手法との関連の中で体系立てて学びます。 XSSやサイドチャネル攻撃、CSSを用いた攻撃などを、手を動かしつつ検討します。 #seccamp pic.twitter.com/XiiFP5N3Yb
— セキュリティ・キャンプ (@security_camp) August 15, 2019
B6「つくって学ぶ、インターネットのアーキテクチャと運用 」
この講義では、実際にPCに仮想のインターネットを構築して、そのインターネットをほかの参加者のインターネットとつなげる、という講義を行いました。
やろうとしていることは理解できましたが、実際に設定をするのに苦労しました
トラックBの講義は、木村 泰司さんによる「つくって学ぶ、インターネットのアーキテクチャと運用」です。 ルータなどの設定を実際に行ってインターネットと同じものを構築します。IPアドレスやルーティング、DNSといった基礎知識やその運用について体系的な知識が身につく事を目指します。 #seccamp pic.twitter.com/VGWL7AyPdH
— セキュリティ・キャンプ (@security_camp) August 16, 2019
E7「実践トラフィック解析」
この講義では、キャンプ期間中のキャンプ内のトラフィックとハニーポットのトラフィックの2種類をwiresharkで解析しました。
ぼくはハニーポットの方のパケットを見ていたのですが、既知の脆弱性を突こうとするようなパケットや、ありがちなパスワードで認証を突破しようとしているパケットが多くあり、セキュリティの重要さが体感できました!
トラックEの最後の講義は、松本 智さん、 石川 大樹さんより「実践トラフィック解析」です。キャンプのライブネットとダークネット双方のトラフィックを手を動かしてキャプチャー、解析します。#seccamp pic.twitter.com/MD9EhmAkKQ
— セキュリティ・キャンプ (@security_camp) August 16, 2019
感想
とても有意義な時間を過ごすことができました!
五日間ということでしたが、実際には二日程度いたかな?くらいの体感速度でした。「あれ?もう終わり?」って感じです。
レベルの高い講義で成長できたのもありますが、やはり大きいのは、キャンプ後も交流したいと思える仲間ができたことですね。
自分は情報系の学科ではなく、周りにセキュリティに興味のある人がいないので、こういう場でできた仲間を大切にしていきたいと思います!
最後に、このような素晴らしい機会を与えてくださった講師の方やセキュリティキャンプ関係者、企業の方々、本当にありがとうございました!
おまけ
バッファオーバーフロー
はい、皆さんこんばんは。
今回は、手元の環境で動作を確認しやすいバッファオーバーフローという脆弱性について述べたい。
バッファオーバーフローとは、メモリ上のバッファに、不正なシェルコードなどの入力をすることで、その実行位置IPを奪う攻撃である。
スタックを用いた攻撃の他に、ヒープや静的領域などを用いた攻撃が存在するが、今回はスタックを用いた攻撃について詳しく述べたい。
スタックベースのバッファオーバーフロー
まずは、スタックを用いたバッファオーバーフローの原理について述べる。
ユーザーの作ったプログラムが実行されるとき、main関数のローカル変数は、スタック上に配置される。スタックにはほかにも、関数を呼び出す際の引数や、その関数を呼び出した後に戻ってくるためのリターンアドレスや、関数プロローグに用いられるebpの値などが積まれている。
これを踏まえて、もしローカル変数があふれた場合にどうなるかについて考える。そのあふれた入力は、そのローカル変数に隣接する無関係の領域に書き込まれてしまう。例えば、入力に使われるローカル変数の近くに、リターンアドレスがある場合、そこに別の関数へのアドレスを書いておくことで、別の処理をさせることができるのである。
ローカル変数の破壊
次に、具体的な例を示す。
まずはローカル変数が破壊できていることを確認してみる。なお、以下ではSSPを無効としてコンパイルする。
コンパイラはgccを用いる。
そのために、まずはbof1.cというプログラムを作成する。
今回は、vimで書く。
bof1.cはバッファのサイズを10バイトとしている。
次に、実行結果を確認する。
実行すると次のようになった。
まず、普通に10バイト以内の入力を入れると、zero変数には、0が値として入っていることがわかる。
次に、10バイト以上の入力を与えると、zero変数には1111638594という値が入っている。
これは、buffer変数からあふれた入力が、隣にあるzero変数の領域に書き込まれたことを示している。
つまり、書き込まれた0xBBBBを十進数に直してintにすると0xBBBB=0x42424242=1111638594となる。
Segmantation faultを起こしているので、gdbでどの部分がエラーになっているか確認してみる。
すると、EIPが0x0となっている。つまり、正しい実行位置にないためSegmentation faultを起こしている。
次に、入力を先ほどよりも多くすると、次のようになった。
この時のEIPは0x565555fbとなっている。
それでは、その部分のアセンブリをradare2を用いてみてみる。
すると、ff invalidとなっている。
X86の公式リファレンスでこの意味を確認すると、オペコードが無効となっていることがわかる。
ここから、やはり実行位置がただしく配置されなかったことがわかる。
このような方法で、変数の値を書き換えるような攻撃に対しては、変数の順序に気を付ける、入力文字数をチェックするなどの対策があげられる。
順序を変えれば、zero変数はbuffer変数より下のアドレスに配置されるため、buffer変数からあふれた入力がzero変数の領域に来ることはなくなる。
リターンアドレスの書き換え
次に、リターンアドレスを書き換える攻撃について述べる。次のようにbof2.cを作成する。
これは、グローバル変数のbuffer変数がstrcpy関数で、local変数からはみ出して上書きされてしまう脆弱性を持ったプログラムである。
リターンアドレスが書き換えられると、ret命令で、espが指しているアドレスに格納されているリターンアドレスをeipレジスタに書き込むときに、書き換えられた値をeipに書き込んでしまうからである。
では、このプログラムに不正な入力である、Aを32文字以上入れてみる。
実行結果は次の通りである。
すると、eipには0x41414141となるはずである。
ところが、実際にgdbで見てみると、eipには0x56555614となっており、ret命令から進んでいないことがわかる。
次に、この原因を探るためにmain関数を逆アセンブルしてみる。
すると、retの前にlea esp,[ecx-0x4]という記述がみられる。
この時、espに書き換えられたリターンアドレスが存在するアドレスが入っていれば、eipを奪えるはずである。
では、ecxについてみてみると、pop ecxが存在する。
さて、実際にバッファオーバーフローを発生させると、スタック上のリターンアドレスに書き込まれるが、その時に同時にpop ecxでespが指し示している値(ecxに代入される値)も書き変わる。
pop ecxでecxに代入される値が異常値になり、最終的なlea esp, [ecx-0x4]で代入されるespの値も異常値になる。
したがって、バッファオーバーフローをSSP無効の状態でも検出することができている。
それでは、プログラムのeipは奪えないのかというと、そんなことはなくて、プログラムをbof2_2.cのように書き換えると、eipを奪うことができた。
Bof2_2は、main関数内の処理を別の関数から呼び出したものである。
このプログラムをgdbで実行すると、図のようになった。
ここから、eipには0x41414141という値が書き込まれていることがわかる。
このプログラムの逆アセンブル結果は次のようになった。
ここでgdbのesp, ecxの値を見ると、先ほどとは違い、有効なアドレスが書き込まれていることがわかる。よってバッファオーバーフローは検出されない。
この理由としては、gccでコンパイルしたため、main関数でほかの関数を呼び出すときにgccがこういうコンパイルの仕方をするから、と考えられる。
それでは、この時にeipを書き換えて実行を操作してみる。
そのためにまずオーバーフローした入力の何文字目がeipに書きこまれるのかを知るために、pedaをgithubからcloneして使う。
Pedaのpattern_create、pattern_offsetを使うことで入力の何文字目かわかる。
ここで実行してみると、eipにはAFAAが書き込まれているため、44文字目から書き込まれていることがわかる。
よって、これ以降にアドレスを書き込めばよい。
リターンアドレスにmain関数のはじめの値を書き込めば、main関数が二回実行されるはずである。
gdbでmain関数のはじめのアドレスを確認すると、0x56555601となっている。
入力するときに、0x01に対応する文字列は存在しないため、echoコマンドを用いて入力する。ここでは、
Echo -e ‘AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x01\x56\x55\56’ | ./bof2_2 を実行した。
しかし、main関数は二回呼び出されず、Segmentation faultとなった。
この理由は、pieが有効になっているからと考えられる。
PIEは、実行されるファイルのアドレスがランダムになるものである。一方、ASLRはスタック領域・ヒープ領域・共有ライブラリが置かれるアドレスがランダムになるものである。
つまり、今回の場合は、main関数の最初のアドレスはプログラムを実行するときに毎回変化していると考えられる。
PIEが有効になっているかどうかは、readelf -a bof2_2 | lessを実行することで確認できるはずである。
これを実行すると、型のところに「DYN(共有オブジェクトファイル)」という記述がみられる。動的リンクは、共有ライブラリを動的に配置するものであるため、ここではファイルを動的に配置している、つまりファイルをランダムに配置していると考えられる。また、これはfileコマンドでも「shared object」という記述があることから確認できる。
また、checksec.shというファイルの持つセキュリティ機構を表示するシェルスクリプトを用いると、次のようになった。
PIEの部分は「Not an ELF FILE」というエラーが出ている。fileコマンドからもELFであることはわかるので、この記述はおかしい。
この原因はおそらくchecksecが壊れているからだと考えられる。
よって、この場合はアドレスのリークをすることで、eipにmain関数の値を書き込むことができる。
その方法としては、別のターミナルでbof2_2を実行した状態で、sudo gdb -q -p ‘pidof bof2_2’を実行して、gdb-peda$x/10xi main を実行すると0x56562665であることがわかる。ここで、ファイルの位置がランダムになっていることがわかる。
しかし、gdb上で入力すると0x9dなどは入力できない。しかし、プログラムを実行させてアドレスをリークさせてから入力する必要がある。
そのため、socatコマンドを用いて、仮想的にサーバー上でプログラムが待機しているようにして、そのポートにnetcatでアクセスすることで実行することを考える。
今回は、ncコマンドの代わりに、pythonなどで、リモートエクスプロイトをするコードを作成することで実現できる。
それでは、リターンアドレスを書き換えるような脆弱性の対策について考える。
先述のアセンブリがバッファオーバーフローを防ぐ機能を持っていたbof2.cはセキュリティが完全かというとそういうわけではない。
今回のアセンブリの場合は、lea esp,[ecx-0x4]のecxの値を正しく書き込めればespが正しくなり、リターンアドレスを書き換えた状態でeipを奪うことができるはずである。
参考資料
https://raintrees.net/projects/a-painter-and-a-black-cat/wiki/CTF_Pwn
https://ja.wikipedia.org/wiki/バッファオーバーラン
http://www.ref.x86asm.net/
https://hiziriai.hatenablog.com/entry/2017/05/17/234505
https://tkmr.hatenablog.com/entry/2017/02/28/030528
https://tkmr.hatenablog.com/entry/2017/02/28/030528)
https://teratail.com/questions/48377
http://warabanshi.hatenablog.com/entry/2013/05/18/231628
「HACKING: 美しき策謀」 Jon Ericckson著、村上雅章 訳 オライリージャパン
「セキュリティコンテストチャレンジブック」碓井利宣 竹迫良範 廣田一貴 保要隆明 前田優人 美濃圭佑 三村聡志 八木橋優 著 マイナビ出版
「セキュリティコンテストのためのCTF問題集」清水祐太郎 竹迫良範 新穂隼人 長谷川千広 廣田一貴 保要隆明 美濃圭佑 三村聡志 森田浩平 八木橋優 渡部裕 著 マイナビ出版
4足歩行ロボットの制作
どうも、こんちゃーす。
今回は、4足歩行ロボットを作ったことについて書こうと思います。
素人なので、あまりわかってないですが…
毎度のことながら、口調は定まっていません。
作ろうと思った理由
4足歩行ロボットを作ろうと思ったきっかけは、ボストンダイナミクスのBigDogという奇妙な歩き方をするロボットを作りたいと思ったからである。
バランスを崩しても自分で修正する姿は、まるで本物の動物のようでとても興味を惹かれた。
そこで、物は試しということでまずは試作機を作ろうと思った。
今回は、歩く仕組みや制御などをすべて自分でやりたいと思ったので、すべて一人で作ることにした。
機体設計、回路設計、加工などもすべて一からやった。
開発環境
まず、使ったマイコンはSTM32F446REという、ST社が出している32bitのマイコンである。
開発環境はSystem Workbench for STM32で、CubeMXというマイコンの回路などの設定をするツールで設定して、それをEclipseに吐き出してEclipse上でプログラミングをした。
使った言語はC。
アルゴリズムについて
実際に歩くアルゴリズムには種類がたくさんある。
例えば、遊脚となるのが一脚だけの「ウォーク」、対角の脚が同期する「トロット」、前脚と後脚がほぼ同期する「ギャロップ」などがあり、それぞれで安定性が異なる。
今回は重心が安定しそうな「ウォーク」を採用した。
実際にロボットを動かすにはサーボモータ(mg996r)を用いることにした。例えば前足を前に10cm前進させようとするとプログラムでサーボモータを何度分回転させるかということに変換して考える必要がある。この方法には、一般的には逆運動学という理論が用いられる。
逆運動学とは、主にロボットの関節を制御する方法として知られるもので、指先やつま先の位置から関節の角度が求まる。
その計算のために必要なものとして、腕の長さ、関節の地面からの高さが必要なので、制御に距離センサーを追加した。
(注意)
後の結果でも述べているが、この逆運動学を用いたアルゴリズムは成功しなかった。
なぜなら、そもそもトルクが足りずにモータが思うような角度を出力できなかったからである。
逆運動学のソースコードを期待している方には申し訳ないですが、これ以降でそのコードは出てきません。
3DCAD
CADはinventor professionalという有料のソフトを用いる。ただし、学生は無料である。
大まかな手順としては、①パーツを作る②組み合わせる➂設計図を印刷する
である。
CADを作る意味はいくつかあるが、大きな意味として、干渉解析をすることができるからである。
実際に作る前に、どのパーツとどのパーツが重なり合ってしまうのかなどを確認できる。
また、穴の位置がずれていないかなども確認できる。
私も、さんざんやらかしてしまい、とてもお世話になった。
今回は、サーボモータを複数使うので、設計が楽になるように、サーボブラケットを3Dプリンターで用意した。
3Dプリンター用のデータもinventorで作ることができる。
部品の選定
- DCモータ…電圧を制御することで動かす。PWMという矩形波の Duty比(ON、OFFの割合)で速度を制御。
ステッピングモーターより速い
- ステッピングモータ…回転角度をパルス数で指定して、そのぶんだけ回転させる。
扱いやすい?
- サーボモーター…小型のものが多く、トルクはあまり大きくない。
PWM波のパルス幅( ONの時間)によって角度を指定する
今回はサーボモータを使う。
サーボモータはサーボモータにはエンコーダと呼ばれる、回転を測る部品が内蔵されていて、
PID制御がされており、加減速が効率よく行われている
マイコンの選定
回路
回路図は「kiCAD」という回路設計用のCADを用いた。
以下に、回路図を一つずつ載せる。
今回は、サーボのPWM信号線が17本、ジャイロセンサーの信号線、デイスプレイの信号線、LSIの信号線、距離センサーの信号線を繋ぎます。
センサー系は、データシート通りに配線します。
サーボのPWMは、PWM波が出力できるピンに配線します。
左上の部分はLチカ(LEDちかちか)させる部分です。
STM32F446RE NUCLEOのデータシートを見ると、電源供給するには、VINに7~12Vを、V5には5Vを供給してくださいと書いてあるので、今回はVINに8.4Vを供給します。
今回は、マイコンとサーボ、センサー類をすべて一つの電池から供給します。
その際、誤動作をしたとき用に緊急停止スイッチを作ります。
単純に、間にスイッチを挟むだけでもいいように思えますが、それだとONのときに流れる電流が大きい場合、スイッチが焼けて壊れてしまいます。
なので、今回はNチャンネルのMOSFETを挟んだにします。スイッチをONにすると、ゲートに電圧がかかって、ドレイン~ソース間に電流が流れるようになります。
LSIは5V駆動のため、レギュレータで8Vを5Vに下げてあげる必要があります。それ以外は、データシート通りに配線します。
一番上の可変抵抗で、音量を調整します。
(追記)注意!これでは正しく動作しません!
よく考えてみると当たり前なんですが、この回路ではいろんな音を鳴らせるわけないです。
スピーカーとは信号を音の振動に変えて出力するものです。その時にどんな音を出すのかは、信号の情報によります。
つまり、交流でないといけません。直流だと音を表現できないです。
今回の場合は、信号(交流の電圧)をスピーカーにつなげばよいのですが、スピーカーに入力として必要な電力を用意する必要があります。
そのため、パワーアンプ回路という、電力を増幅する回路を間に入れる必要があります。
以下の図はパワーアンプ回路の一例です。
これも5V駆動です。
データシート通りに配線します。
これは、8Vを5Vに下げる回路です。上のダイオードは、逆電圧がかかった場合に、レギュレータに流れる電流をダイオードに変わりに流して、レギュレータを保護します。
これも5V駆動です。
データシート通りに配線します
電源には以下を用いた。
実際の回路は次のようになった。
なお、ロボットに配線する際には、配線がわかりやすくなるようにするために、以下のようにした。
しかし、これは後からわかったことであるが、こうすることで損失抵抗が多くなり、電圧降下が2Vほど見られた。
サーボモータを同時に動かすために大電流を流しているからである。
CubeMXの設定について
(追記)最近CubeIDEというものがリリースされましたが、これはそれより前のSW4STM32の設定の方法です。
スライドを作成したので、これを見てほしい。
50Hz = 1秒に50回振動する = 1回の振動には20msかかる
「20msのうち、1.5msだけONにする」=「Duty比100のうち、Duty比7.5のPWM波」
➡50HzでDuty比が7.5のPWM波を作ればよい!
ソースコード
毎度のことながら、乱暴で煩雑なコードです。
このコードは夜中に一日で書いたので、もっと簡潔に書くべきですが、ごり押ししています。(次の日風邪ひいた)
CubeMXのおかげで、細かい設定などのプログラムは自動で生成されるので、ユーザーとして付け足す部分について述べる。
まず、サーボモータの初期位置を記述するために以下のようにした。
また、map関数はDuty比を角度に変換する自作の関数である。
参考文献としては、
blog.livedoor.jp
のしろうさぎさんの記事がとても役に立つので、参考にしてください。
#define SERVO996_LOW 500 #define SERVO996_HIGH 1000 #define SERVO3003_LOW 750 #define SERVO3003_HIGH 950 int8_t PULSE996; int8_t PULSE3003; uint16_t Angle; int8_t PULSEA1 = 0; int8_t PULSEA2 = 0; int8_t PULSEA3 = -30; int8_t PULSEA4 = 0; int8_t PULSEA5 = 0; int8_t PULSEB1 = 0; int8_t PULSEB2 = 0; int8_t PULSEB3 = -20; int8_t PULSEB4 = 0; int8_t PULSEC1 = 0; int8_t PULSEC2 = 0; int8_t PULSEC3 = -10; int8_t PULSEC4 = -10; int8_t PULSED1 = 0; int8_t PULSED2 = -5; int8_t PULSED3 = -20; int8_t PULSED4 = -10; long map(long x, long in_min,long in_max,long out_min,long out_max){ return (x-in_min)*(out_max-out_min)/(in_max-in_min)+out_min; }
以下は、whileの中に書く部分である。こちらもごちゃごちゃなので、ほぼ参考にしないでください…
/* USER CODE BEGIN WHILE */ while (1) { //walking step1 before Angle=map(PULSEC3 -10,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(200); Angle=map(PULSEC2 +15,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim4,TIM_CHANNEL_1,Angle); Angle=map(PULSEC3 ,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSED3 -10,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(200); Angle=map(PULSED2 +15,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim4,TIM_CHANNEL_4,Angle); Angle=map(PULSED3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(1000); //walking step1 Angle=map(PULSEA3 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_4,Angle); HAL_Delay(300); Angle=map(PULSEA4 +40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSEA3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_4,Angle); HAL_Delay(1000); //walking step1 after Angle=map(PULSEC3 -15,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(200); Angle=map(PULSEC2,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim4,TIM_CHANNEL_1,Angle); Angle=map(PULSEC3 ,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSED3 -15,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(200); Angle=map(PULSED2,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim4,TIM_CHANNEL_4,Angle); Angle=map(PULSED3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(1000); //walking move1 Angle=map(PULSEC4 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_2,Angle); //HAL_Delay(1000); Angle=map(PULSED4 +40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim13,TIM_CHANNEL_1,Angle); //HAL_Delay(1000); Angle=map(PULSEB4 +30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_2,Angle); //HAL_Delay(1000); //walking set2(to first) /* //B Angle=map(PULSEB3 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,Angle); HAL_Delay(300); Angle=map(PULSEB4 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSEB3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,Angle); HAL_Delay(1000); */ //C Angle=map(PULSEC3 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSEC4 +40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSEC3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(1000); //before for D //before A Angle=map(PULSEA3 -10,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_4,Angle); Angle=map(PULSEA2 +15,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_3,Angle); HAL_Delay(200); Angle=map(PULSEA3 ,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_4,Angle); HAL_Delay(300); //before B Angle=map(PULSEB3 -10,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,Angle); Angle=map(PULSEB2 +15,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_3,Angle); HAL_Delay(200); Angle=map(PULSEB3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,Angle); HAL_Delay(1000); //D Angle=map(PULSED3 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(300); Angle=map(PULSED4 -40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim13,TIM_CHANNEL_1,Angle); HAL_Delay(300); Angle=map(PULSED3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(1000); //after for D //after A Angle=map(PULSEA3 -10,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_4,Angle); Angle=map(PULSEA2 ,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_3,Angle); HAL_Delay(200); Angle=map(PULSEA3 ,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_4,Angle); HAL_Delay(500); //after B Angle=map(PULSEB3 -10,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,Angle); Angle=map(PULSEB2 ,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_3,Angle); HAL_Delay(200); Angle=map(PULSEB3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,Angle); HAL_Delay(1000); //walking step2 before Angle=map(PULSEC3 -10,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(200); Angle=map(PULSEC2 +15,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim4,TIM_CHANNEL_1,Angle); Angle=map(PULSEC3 ,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSED3 -10,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(200); Angle=map(PULSED2 +15,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim4,TIM_CHANNEL_4,Angle); Angle=map(PULSED3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(1000); //walking step2 Angle=map(PULSEB3 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,Angle); HAL_Delay(300); Angle=map(PULSEB4 -40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSEB3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,Angle); HAL_Delay(1000); //walking step2 after Angle=map(PULSEC3 -10,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(200); Angle=map(PULSEC2,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim4,TIM_CHANNEL_1,Angle); Angle=map(PULSEC3 ,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSED3 -10,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(200); Angle=map(PULSED2,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim4,TIM_CHANNEL_4,Angle); Angle=map(PULSED3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(1000); //walking move2 Angle=map(PULSEA4 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,Angle); // HAL_Delay(1000); Angle=map(PULSEC4 -40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_2,Angle); // HAL_Delay(1000); Angle=map(PULSED4 +30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim13,TIM_CHANNEL_1,Angle); // HAL_Delay(1000); //walking set3(to first) /* Angle=map(-40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(1000); Angle=map(-40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_2,Angle); HAL_Delay(1000); Angle=map(0,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(1000); */ //A forward Angle=map(PULSEA3 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_4,Angle); // HAL_Delay(300); Angle=map(PULSEA4 +40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSEA3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_4,Angle); HAL_Delay(500); //C forward Angle=map(PULSEC3 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); // HAL_Delay(300); Angle=map(PULSEC4 +40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSEC3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim8,TIM_CHANNEL_2,Angle); HAL_Delay(500); //B,D back Angle=map(PULSEB4 +30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_2,Angle); Angle=map(PULSED4 +30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim13,TIM_CHANNEL_1,Angle); HAL_Delay(500); //B forward Angle=map(PULSEB3 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,Angle); // HAL_Delay(300); Angle=map(PULSEB4 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_2,Angle); HAL_Delay(300); Angle=map(PULSEB3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,Angle); HAL_Delay(500); //D forward Angle=map(PULSED3 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); // HAL_Delay(300); Angle=map(PULSED4 -40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim13,TIM_CHANNEL_1,Angle); HAL_Delay(300); Angle=map(PULSED3,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Angle); HAL_Delay(500); //A,C back Angle=map(PULSEA4 -30,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,Angle); Angle=map(PULSEC4 -40,0,120,SERVO996_LOW,SERVO996_HIGH); __HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_2,Angle); HAL_Delay(500);
動画
結果
動画をみると明らかであるが、うまくいかなかった。機体が重すぎて、サーボモータの出力では足りず、ストールしているのがわかる。
つまり、サーボモータは60度を出力していても、実際にはストールして動かず0度のままである、ということが頻繁に起きてしまったのである。
このプログラムは、あらかじめ歩行パターンを作成しておき、それに基づいて歩くように書いたプログラムである。機体の姿勢によって出力できる角度とできない角度があり、それを一つずつ確認していくともっとうまくいくかもしれない。
しかし、それをするのも時間の無駄なきがするので、今回はここで諦めた。
敗因としては、安いサーボモータを使ったことで重量オーバーしてしまったことがあげられるので、今度は重量とトルクには細心の注意を払いたい。
また、音声合成LSIを使ってしゃべらせようとしていたが、それをする余裕がなくなったため、次回に回すことにした。
次回は逆運動学の基づいて歩くように設計したい。
まとめ
4足歩行ロボットを作るのは難しい
画像処理を用いたエアホッケーロボット
今回は、画像処理についての記事です。
pythonで書いてます。動画もあるヨ!
なお、ここからは口調が変えて真面目に書きます。
制御について
ロボットの制御としては、まずカメラでとらえた映像を私のノートパソコンに送り、そこで画像処理をして、パック(エアホッケーで使われる丸い白い円盤)が到達するであろう座標を計算して、マイコンにその計算結果をシリアル通信で送信し、マイコンでエンコーダーを用いたPID制御でロボットを動かした。
開発環境
使用したマイコンはSTM32F446REである。マイコン側の開発環境はSystem Workbench for STM32(通称SW4STM32)で、CubeMXというマイコンの回路などの設定をするツールで設定して、それをEclipseに吐き出してEclipse上でプログラミングをした。これらの開発環境は総称してHALと呼ばれるものである。用いた言語はCである。
一方、ノートパソコンの方では、開発環境はWinpythonで、VScode上でプログラミングした。使用した言語はpythonである。用いたライブラリはserialとnumpyとopencvである。以下、画像処理のプログラムについて詳しく述べる。
ソースコード
以下に実際に使用したソースコードを載せる。
動けばいいや、と思って書いたので、幼稚なプログラムで恐縮です。
皆さんは、参考程度にしてください。関数の名前などは以下の文章と対応しているので、適宜見てほしい。
コメントアウトは特に意味はないので、無視してほしい。
import cv2 import numpy as np import math import time import serial # mode change1 n=0 t=0 count = 0 def morph_and_blur2(img): kernel = np.ones((8,8),np.uint8) m = cv2.GaussianBlur(img,(3,3),0) m = cv2.erode(m,kernel,iterations = 1) # m = cv2.morphologyEx(m,cv2.MORPH_OPEN,kernel,iterations=2) m = cv2.GaussianBlur(m,(5,5),0) return m def binary_threshold2(frame): grayed = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) morph = morph_and_blur2(grayed) under_thresh = 245 upper_thresh = 180 maxvalue = 255 th,drop_back = cv2.threshold(morph,under_thresh,maxvalue,cv2.THRESH_BINARY) th,clarify_born = cv2.threshold(morph,upper_thresh,maxvalue,cv2.THRESH_BINARY_INV) merged = np.maximum(drop_back,clarify_born) return merged def padding_position(x,y,w,h,p): return x-p,y-p,w+p*2,h+p*2 def detect_contour(path, min_size): # contoured = cv2.cvtColor(path,cv2.COLOR_BGR2GRAY) contoured = binary_threshold2(path) forcrop = path global centerx global centery centerx = 0 centery = 0 birds = binary_threshold2(path) birds = cv2.bitwise_not(birds) im2,contours,hierarchy = cv2.findContours(birds,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE) crops = [] for c in contours: if cv2.contourArea(c) < min_size: continue x,y,w,h = cv2.boundingRect(c) x,y,w,h = padding_position(x,y,w,h,5) cropped = forcrop[y:(y+h),x:(x+w)] crops.append(cropped) cv2.drawContours(contoured,c,-1,(0,0,255),3) cv2.rectangle(contoured,(x,y),(x+w,y+h),(0,255,0),3) centerx = int(math.floor(x+w/2)) centery = int(math.ceil(y+h/2)) return contoured, crops,centerx,centery class point: def __init__(self,x,y): self.x = float(x) self.y = float(y) def get_line_intersection(A1,A2,B1,B2): a = A2.y - A1.y b = -B2.y + B1.y c = A2.x - A1.x d = -B2.x + B1.x C1 = B1.y - A1.y C2 = B1.x - A1.x tmp = a*d - b*c if tmp: invMa = d /tmp invMb = -b /tmp invMc = -c /tmp invMd = a /tmp m = invMa*C1 + invMb*C2 n = invMc*C1 + invMd*C2 secx = int(A1.x + m*(A2.x - A1.x)) secy = int(A1.y + m*(A2.y - A1.y)) secx = abs(secx) secy = abs(480-secy) return secx,secy else: secx=205 secy=50 return secx,secy capture = cv2.VideoCapture(0) median = np.full(5,0,dtype="uint16") mediany = np.full(9,0,dtype="uint16") triming1x = 200 triming2x = 30 h = 480 w = 640 widthx = w - (triming1x + triming2x) heighty = 450 tyusin = int(widthx/2) encx = 2000 ency = 1800 ceny = int(h/2) cenx = int(w/2) scale = 1.0 angle = 0.0 ser = serial.Serial('COM7',115200,timeout=1) while(1): #time.sleep(1) start = time.time() ret,frame = capture.read() rotation_matrix = cv2.getRotationMatrix2D((cenx,ceny),angle,scale) img_rot = cv2.warpAffine(frame,rotation_matrix,(w,h),flags=cv2.INTER_CUBIC) dst = img_rot[0:h,0+triming1x:w-triming2x] contoured1, crops1,centerx1,centery1 = detect_contour(dst,1000) ret, frame = capture.read() img_rot = cv2.warpAffine(frame,rotation_matrix,(w,h),flags=cv2.INTER_CUBIC) dst = img_rot[0:h,0+triming1x:w-triming2x] contoured2, crops2, centerx2, centery2 = detect_contour(dst,1000) contoured2 = cv2.circle(contoured2,(centerx1,centery1),10,(0,0,0),-1) contoured2 = cv2.circle(contoured2,(centerx2,centery2),10,(0,0,0),-1) A1 = point(0,h-1) A2 = point(w-1,h-1) B1 = point(centerx1,centery1) B2 = point(centerx2,centery2) cv2.line(contoured2,(centerx1,centery1),(centerx2,centery2),(255,0,0),1) secx,secy = get_line_intersection(A1,A2,B1,B2) print('centery2:'+str(centery2)) #print('secy:'+str(secy)) if centery2>200 and int(t)<60: secx = int(centerx2 / widthx*encx) secy = int(abs(h-centery2) / heighty*ency) secx_str = str(int(secx)) secy_str = str(int(secy)) secx_zero = secx_str.zfill(4) secy_zero = secy_str.zfill(4) value = secx_zero + secy_zero + "s" print("yyyyyyyyyyyyyyyyy") print(int(t)) print(value) print(w,h) print('portstr',ser.portstr) value2=value.encode('utf-8') ser.write(value2) contoured2 = cv2.circle(contoured2, (secx,secy),10,(255,0,0),-1) cv2.line(contoured2,(centerx2,centery2),(secx,secy),(255,0,0),1) cv2.namedWindow('image',cv2.WINDOW_NORMAL) cv2.imshow('image',contoured2) cv2.imshow('image2',dst) elapsed_time = time.time() - start t = t + elapsed_time print("elapsed_time:{0}".format(t)+"[sec]") count = 0 if cv2.waitKey(1) & 0xFF == ord('q'): break continue secx = abs(secx) while(secx>(widthx-1)): a = secx - (widthx-1) secx = abs((widthx-1)-a) median = np.append(median,int(secx)) median = np.delete(median,0) m = np.median(median) m = int(m/widthx*encx) mediany = np.append(mediany,int(secy)) mediany = np.delete(mediany,0) my = np.median(mediany) my = int(my/heighty*ency) secy = int(secy/heighty*ency) if n == m: if count == 0: secx = int(tyusin/ widthx*encx) secy = int( 80/ heighty*ency) secx_str = str(int(secx)) secy_str = str(int(secy)) secx_zero = secx_str.zfill(4) secy_zero = secy_str.zfill(4) value = secx_zero + secy_zero + "s" print('portstr',ser.portstr) value2=value.encode('utf-8') ser.write(value2) count = 1 contoured2 = cv2.circle(contoured2, (secx,secy),10,(255,0,0),-1) cv2.line(contoured2,(centerx2,centery2),(secx,secy),(255,0,0),1) cv2.namedWindow('image',cv2.WINDOW_NORMAL) cv2.imshow('image',contoured2) cv2.imshow('image2',dst) elapsed_time = time.time() - start t = t + elapsed_time print("elapsed_time:{0}".format(t)+"[sec]") if cv2.waitKey(1) & 0xFF == ord('q'): break continue contoured2 = cv2.circle(contoured2, (secx,secy),10,(255,0,0),-1) cv2.line(contoured2,(centerx2,centery2),(secx,secy),(255,0,0),1) cv2.namedWindow('image',cv2.WINDOW_NORMAL) cv2.imshow('image',contoured2) cv2.imshow('image2',dst) count = 0 print('aaaaaaaaaaaaaaa') elapsed_time = time.time() - start t = t + elapsed_time print("elapsed_time:{0}".format(t)+"[sec]") if cv2.waitKey(1) & 0xFF == ord('q'): break continue secy = int(50/heighty*ency) n = m print('only x') secx_str = str(int(m)) secy_str = str(int(secy)) secx_zero = secx_str.zfill(4) secy_zero = secy_str.zfill(4) value = secx_zero + secy_zero + "s" print(value) contoured2 = cv2.circle(contoured2, (m,secy),10,(255,0,0),-1) cv2.line(contoured2,(centerx2,centery2),(m,secy),(255,0,0),1) cv2.namedWindow('image',cv2.WINDOW_NORMAL) cv2.imshow('image',contoured2) cv2.imshow('image2',dst) if cv2.waitKey(1) & 0xFF == ord('q'): break print('portstr',ser.portstr) value2=value.encode('utf-8') ser.write(value2) elapsed_time = time.time() - start t = t + elapsed_time print("elapsed_time:{0}".format(t)+"[sec]") count = 0 # time.sleep(0.05) ser.close() capture.release() cv2.destroyAllWindows()
本題の画像処理
まず、今回のプログラムの仕組みについて簡単に述べる。まずカメラから入手したフレームを2枚用意し、前処理と白黒二値化した画像を用意して、パックかどうか判別する関数を用いてパックの現在地を判別する。
そうして、二つのフレームにおける座標をもとに、直線を作成して、ロボット側の底辺と交わる座標を求める。この時、反射も考慮して、実際の座標に対応する座標を求める。
これによって、近似的にパックの来るである座標を求めることができる。
前処理について
まず生のフレームをカメラから入手したときに前処理をして、判別しやすいように画像データを変換する。
まず、opencvのcv2.cvtColorを用いてRGB画像からGRAY画像に変換する。この理由は、画像をRGB画像として処理するとデータのサイズが大きくなってしまうので、今回のような速さが求められるプログラムでは白黒で処理することにした。
次に、この画像データをcv2.GaussianBlur, cv2.erodeというopencvの関数を用いてぼやけさせたり、対象の周囲をnumpyで作成した配列分ピクセルだけ侵食することでノイズの影響を小さくする。この理由としては、今回はパックを白色、背景となるホッケー台を茶色で行ったが、ロボットを金属で作ったために光の反射などにより白色と誤認識することがあるため、この前処理は必須であった。
次に、cv2.thresholdを用いることでGRAY画像を黒と白の完全な二値画像に変換した。手順としては、白として得たい、パックの部分の画像と、背景として得たいそれ以外の部分をcv2.thresholdで二つ作成しておき、これらをnp.maximumによって結合することで得られる。この時、境界となるGRAYの値を0~255のうち適切なものを選ぶことで光の反射による比較的明るい白と、パックの白をある程度までなら区別することができた。
次に、判別する関数の部分について述べる。ここではcv2.findContoursという関数を用いた。この関数は、白黒の二値画像を入力として、白の部分のピクセルを一つ見つけると、その周囲のピクセルについて白色のピクセルがあるかどうか探索し、あればそのピクセルに移動して再度探索をするというものである。つまり、白色で閉じている部分の面積を求めているのである。
この関数の戻り値を、cv2.boundingRect関数の引数として計算すると、白で閉じている部分のピクセルのうちxy座標が最も小さい座標と、閉じている部分の縦と横の長さが得られる。これによって、パックをより正確に検出するために、ある値の面積を決めておき、それ以上ならパックである、また、縦と横の長さがある値からある値までの範囲をとっていればパックである、というような条件を付けることが可能である。このようにして、画像からあるものを検出することができる。
実際の制御について
まず、前提として、今回のロボットはx、y座標に動く直動機構のロボットである。それぞれのモータがラック上で回転し、それをエンコーダーでフィードバックすることで現在地を把握している。
まず、基本的にロボットは、パックを入れるゴールの中心に待機することにした。そして、ロボット側から見て左右をx軸とすると、パックが来た時はまずx軸上の予想された座標にロボットを動かすことを優先する。
そして、そこにたどり着いた後にy軸とx軸について現在のパックの座標にロボットを動かす。そうすることで、理論上は少なくてもパックに間に合いさえすれば、パックを打ち返せるはずである。
また、y軸について、ラックの長さ以上の座標にロボットを動かすことはできないため、if文で分岐するようにした。
また、パックの予想座標をマイコンに送信するときに、常に値を送信していると、マイコンのバッファを破壊してしまうことになりかねないため、送信する値と前回の値の値にあまり変化がない場合は送信しないようにする、という機能を中央値を取ることで実装した。
結果(まとめ?)
結果としては、画像処理を用いたロボットを自動で動かすことはできたが、反応がかなり遅く、人間に勝てるロボットは到底作れなかった。この遅さは異常で、プログラムが正しく動いていればもっと速く動いているはずであった。
この原因としてはやはり、マイコンのバッファに値を一方的に大量に送り付けすぎていることがあげられる。実際に、値を一つだけ送る場合では問題なくすぐに動いた。
また、なぜかロボットを動かしていると、エンコーダーの値がどんどんずれていくという現象が発生し、最大でも一分半程度しか連続して動かせなかった。それ以上動かすと、ロボットがラックから外れてストールしてしまった。この対策としては、動かす範囲の両端にリミットスイッチを作成し、エンコーダーの値を初期化することがあげられるが、これを実装するとロボットにおかしな挙動がみられたため、やむを得ずデバッグを断念した。
また、画像処理の部分では、照明の明るさなどによってパックや背景の明るさは変化するため、朝と夜では閾値が異なり、定期的なパラメータ調整が必要であった。
また、カメラ自身が揺れることでそもそもの画像にずれが生じてしまうこともあった。このため、画像のトリミングや回転などをその都度調整する必要があった。
改善案?
改善点としては、まずマイコンのバッファの破壊を防ぐために、ノートパソコン側でPID制御も行い、必要な出力だけをマイコンに渡すようにすることである。このためには、エンコーダーの代わりに、ノートパソコン側でパックだけでなくロボットの認識も 行う必要がある。
また、今回のパックの予測ではパックは直線で進み、反射するときは常に反射の法則が成り立つとしていたが、そんなわけはないので、線形回帰を用いた機械学習を導入することがあげられる。画像を特徴量とするのではなく、画像を処理して得られたパックの座標、二つのフレームでのパックの移動量(つまりベクトル)を特徴量として線形回帰を用いることで、パックの位置とタイミングをより正確に、より早く得られるはずである。
また、そもそもpythonの処理が遅いため、C++を用いたopencvによるプログラムにするべきである。
また、カメラの揺れを補正するために、エアホッケー台を自動で検出する機能も付けるべきである。
まとめ(2回目)
画像処理は奥が深いなあ
DCDCコンバータと3端子レギュレータの比較
ご挨拶
どうも、初めまして。花京院です。
技術系のサークルでロボットなどを作っています。
今回は、自分のやったことをとりあえず記録していこうと思ってブログをはじめました。ブログはこれが初めてで、至らぬ文章ですが、読んでもらえるとありがたいです。
DCDCコンバータと3端子レギュレータの比較
さて、自己紹介はこれくらいにして、今回の本題「DCDCコンバータと3端子レギュレータの比較」について話したいと思います。今回は、LTspice上で二つを比較してみたいと思います。
まず、DCDCコンバータ、3端子レギュレータについてですが、この二つの回路は、一定の電圧を得る回路として用いられることが多いです。
3端子レギュレータとは
3端子レギュレータとは、文字通り3端子がVin, Vout, GNDであり、これらを指示通りにつなぐことで3.3Vや5.0Vを得ることができる素子です。秋月電子などで比較的安価で手に入ります。
DCDCコンバータとは
DCDCコンバータとは、コイルやコンデンサなどを用いて一定の電圧を得る回路です。DCDCコンバータは、降圧だけでなく、昇圧や反転なども可能です。
3端子レギュレータの回路図
では、紹介はこれくらいにして実際の回路を見てみましょう。
図1は3端子レギュレータの回路をLTspice上で設計したものです。
これは、5Vの入力電圧から3Vの定電圧を得る回路です。
用いている部品について解説します。
3端子レギュレータの回路の解説
まず、2SC1815は可変抵抗の役割をするトランジスタです。トランジスタは、ベース・エミッタ間に電流を流すことでコレクタ・エミッタ間に電流が流れる素子です。
これはスイッチの機能と捉えることができますが、実際にはベース電流を変化させるとコレクタ電流も変化するため可変抵抗とみなすことができます。
次に、R1,R2は帰還抵抗です。出力電圧をR1,R2で分圧してその電圧を誤差増幅器にフィードバックしています。出力電圧はこの分圧比で決まります。
次に、V2は基準電圧です。誤差増幅器の+端子に接続します。1V前後を出力します。
ちなみに、.tranという文言は、「過渡解析を50ms行う」というものです。
次に、Q1は誤差増幅器です。これは、+端子とー端子の電圧差を増幅した電圧を
出力するオペアンプです。定常状態では+とーの電圧が等しくなります。
つまり数式は(1)のようになります。
Vfb = Vref (1)
Vfb = Vout × R2 / (R1+R2) (2)
(1)を(2)に代入して、
Vref = Vout × R2 / (R1+R2)
よって、 Vout = Vref × (R1+R2) / R2
すなわち、Voutは基準電圧源と分圧比によってのみ決まります。
DCDCコンバータの回路図
次に、図2にDCDCコンバータの回路を示します。
DCDCコンバータの回路図の解説
これは、昇圧DCDCコンバータ回路です。
2SC1815はスイッチの機能として使われているトランジスタです。ベース電圧にDuty比50のパルス波を出力して高速でスイッチングしています。
スイッチがONの時、コイルに電流が流れて時期エネルギーが蓄えられます。スイッチがOFFの時、溜まったエネルギーがコイルからダイオードに流れます。この電流によって、出力コンデンサが充電されて出力電圧は上昇します。
この回路での定常状態は、ONの時のコイル電流の増加量とOFFの時の減少量が等しくなった時です。この時、Vout電圧の上昇が止まります。
スイッチがONの時のコイルに流れる電流の増加量は、
∆I= 1/L ×Vin × Ton (4)
スイッチがOFFの時のコイルに流れる電流の減少量は、
∆I= 1/L ×(Vout-Vin)×Toff (5)
コイルに流れる電流の増加量と減少量が等しいとき、(4)=(5)なので
Vout = (Ton+Toff) / Toff ×Vin (6)
3端子レギュレータの実行結果
次に、これらの回路の実行結果について解説します。
まずは、3端子レギュレータの回路の実行結果です。
図3は図1の回路の実行結果を表しています。
まず、図3を見ると明らかなように、3Vが常に一定に得られました。
この3Vという数値は(3)より、基準電圧と帰還抵抗R1,R2の比によって決まります。
今回の場合は、
Vout= (1k+2k)/1k ×1V
よって、Vout = 3Vが得られます。
Vin=5V, Vout=3Vより、
トランジスタの前後にかかる電圧は、
V= 5V-3V=2V
次に、エネルギー効率の観点から考察したいと思います。
トランジスタを流れる電流の値は、図3より、I = 4.0mAと読み取れます。
よって、
トランジスタで熱として消費されるエネルギーPは、
P=I ×V=8mW
入力時のエネルギーPin,出力時のエネルギーPoutは、
Pin =4.0mA ×5V=20mW
Pout=4.0mA ×3V=12mW
したがって、入力されたエネルギーのうちの40%が熱として消費されてしまっていることがわかります。
DCDCコンバータの実行結果
次に、DCDCコンバータの回路の実行結果について説明します。
図4は、図2の回路を実行した結果です。
Vdが急峻になっているのは、蓄えらえれた電流が、
V=L di/dt
によって、電圧に変換されているからです。この電圧はサージ電圧と呼ばれています。
また、この回路の出力電圧は(6)より、
Vout= (1m+1m)/1m ×1.5V=3.0V
が期待されます。
しかし、実際には図4のように5V付近で定常状態となっています。これはいったいなぜでしょうか?
この理由は、
スイッチがONの時のコイルに流れる電流の増加量の、
∆I= 1/L ×Vin × Ton (4)
と、
スイッチがOFFの時のコイルに流れる電流の減少量の、
∆I= 1/L ×(Vout-Vin)×Toff (5)
が等しくないからです。
「不連続モード」と「連続モード」
ではなぜ等しくないのでしょうか?
この時の、コイルに流れる電流を見てみます。
図5に、コイルに流れる電流を示します。
これを見ると、電流が0になったり、300mAになったりと、不安定な挙動をしています。
しかも、300mAからすぐに0Aになっており、放電がとても短い間に行われています。
このような場合、電流の増加量=減少量の等式は成り立ちません。
このような場合のことを、「不連続モード」と言います。
それでは、この等式が成り立つように、コイルの値を1m[H]から1[H]に変えてみましょう。
実行結果は図6のようになりました。
さっきとは違って、電流が0になっているような箇所はなく、安定しているのがわかります。
このような時を「連続モード」といいます。
それでは、この時の出力電圧を確認してみます。
図7はL=1の時の出力結果です。
これを見ると、出力電圧Voutは、
Vout = 2.4V
の時に、安定になっているのがわかります。
期待される電圧の3.0Vよりも0.6V低い値となっています。
これは、ダイオードによる電圧降下の値です。
実際に、Vdでは3.0V付近で安定になっていることがわかります。
それでは、この時のエネルギー効率について考察してみます。
入力時のエネルギーPin,出力時のエネルギーPoutは、
Pin =5.0mA ×1.5V=7.5mW
Pout=2.4mA ×3V=7.2mW
したがって、入力されたエネルギーのうちの4%が熱として消費されていることがわかります。
多少の誤差はありますが、ほぼ100%のエネルギー効率となっています。
ここで、出力電流は以下の図8から得られました。
まとめ
3端子レギュレータを用いる場合は、一定の電圧を得ることができる。
ただし、その値は抵抗比によって決まるため、任意の値の電圧を得るには抵抗を外付けする必要がある。
エネルギー効率はあまりよくない。
DCDCコンバータを用いる場合は、ある電圧を得ることはできるが、その値はギザギザで一定ではない。
また、不連続モードと連続モードでは、出力電圧を求める式が変わってくる。
連続モードの場合は、エネルギー効率はほぼ100%である。