聞きかじりめも

主にC++やメディア処理技術などに関して気付いたことを書いていきます.ここが俺のメモ帳だ!

倒立振子制作日記(そのいち)

はじめに

急に作りたくなったので.

私は一応,大学で機械工学を修め,ロボコンで実践的なロボット製作にも携わったことがある人間ですが,今ではすっかり情報系という半端モンでございます.研究でバーチャル空間にどっぷり漬かっている間にそろそろ機械工学の知識が風化し始めているので,ここいらでちょっと制御工学の復習をしとこうと思ってロボットの製作を思い立ったというわけであります.丁度夏休みだし.
そこで今回は,制御対象として比較的わかりやすい(であろう)部類であり,困ったときの先達がたーっくさんいる倒立振子を課題として,制御コードや回路から作って完成させようというプロジェクトを立ち上げてみました.勉強が目的なので,今後の制作では先達の情報は大いに参考にしつつも,できるだけ正解は見ないように(パクらないように)したいと思います.

部品集め

主にaitendo,秋月電子千石電商で買いあさりました.それにしてもaitendoはセンサ・IC類がバカ安だな...とはいえ自分で全部揃えると結構かかりました.なるべく安く抑えようと思ったんだけどなー
(注)下表の他に,適当なジャンパ線,ケーブル類,はんだ,ピンヘッダ等が必要です.

品名・型番 購入場所 価格(円) 用途
びんぼうでいいの aitendo 999 制御マイコン
DCジャック aitendo 30 制御マイコン
Arduino用ピンヘッダ aitendo 95 制御マイコン
ATD6050 3軸加速度/ジャイロモジュール aitendo 450 センサ
ATD712-M 高感度電流センサモジュール aitendo 750 * 2 センサ
Arduino用ユニバーサル基板 aitendo 200 基板実装
電池ボックス単3x4本 秋月電子 60 電源
9V電池スナップ-DCプラグ変換アダプタ 千石電商 100 電源
L298N フルブリッジドライバ 秋月電子 350 モータードライバ
ブレッドボード 千石電商 270 回路テスト
MYU-004 50mmタイヤセット(3mm六角シャフト用) 千石電商 270 機体
タミヤ 70097 ツインモーターギヤーボックス 千石電商 790 機体
タミヤ 70157 ユニバーサルプレート2枚セット 千石電商 570 機体
合計 5,684

ちょっと解説

  • 「びんぼうでいいの」とは,aitendo独自のArduino互換機です.ブートローダーが書き込まれていないので,Arduinoを持っていない場合は初めにこれを買うのは止した方がいいと思います.後述しますが結構書き込むの面倒だったので・・・
  • 今回の倒立振子は,現在の角度のみを(ジャイロ+加速度で)センシングして,その情報から駆動輪の出力を決定するフィードバック制御モデルを構成する予定です.
  • ブラシ付DCモータの制御は電流制御入力の方がよいらしい(参考)ので,電流センサをわざわざ購入しました.結構高い・・・実は電流は2~5Ωくらいの抵抗があればオームの法則V=RIで直接計測できるんですが,どうしても抵抗で損失が生じてしまうせいで最大出力が落ちてしまうのが嫌だったので,購入に踏み切りました.実験的に抵抗でもやってみましたが,損失を少なくしようとするとどうしても出力電圧が小さくなります.まあオペアンプ買って電流センサ回路自作しても良かった気がするけど本質じゃないし・・・
  • ギヤボックスのギヤ比は58.2:1を選びました.203.7:1はちょっと遅すぎた.

ハードウェア制作

テスト用回路

あとでアップロードします.

困った所

びんぼうでいいのへのブートローダー書き込み

Arduino IDEにはブートローダー書き込み機能がありますが,これを利用するにはブートローダー書き込み済みArduinoなどの書き込み装置が必要です.殆どの場合Arduino UNOを利用すると思います(ネットのTipsでもUNOばかり)が,生憎手元にはArduino Leonardoしかありませんでした.Leonardoの場合はちょっと特殊な操作が必要で,こちらの記事が参考になります.繋ぎ方はこちらの記事の通りにするとよいです.上記事を参考にする場合は,Arduino IDE 1.6.9では全くうまくいかなかったので,旧バージョンの1.0.6を使うと上手くいきました.参考にしてください.

L298Nのつなぎ方

実はデータシートの等価回路図をよく見ると分かるのですが,電流センサ用ピンのPin1とPin15はGNDに繋がないと動きません! なぜかというと,これらはセンサ用と書いてありますが,実はHブリッジ回路のGNDに接続すべき部分になっているためです(センサと書いてあったので私は内臓センサのアナログ出力Pinだと誤解していました・・・).つまりこれらのピンは各モータの電流の通り道そのものであって,センサを使わないからと言って浮かしてしまうとモータに電流が流れなくなってしまいます.従って,使わない時はGNDに接続し,電流センサに接続するときにも一方をGNDに接続する ことを忘れないようにしましょう.(このことに気づくまでに1日かかってしまったことは内緒)

以上,とりあえず文章だけ挙げておきます.余裕があったら追記していきます.

GLSLでOpenCV歪み補正を実装

OpenCVの歪み補正関数はcv::initUndistortRectifyMap()で予め作成したマッピング画像を使ってcv::remap()で歪んだカメラ画像を処理する,というものですが,このcv::remap()関数はOpenGLにやらせることもできます.それもプログラマブルシェーダGLSLを使った並列演算です.つまり,このテクニックを使えばGLSL上で並列化画像処理を実装することができるというわけです.(ここでは画像表示部をOpenGL/GLFWに任せています)

いやOpenCV使えよ

なぜOpenCVをすでに使っているのにわざわざそんなことをするかって?OK,これには理由が2つある.
まず一つ目は,画像処理を並列化することで実行速度の更なる向上が期待できるからだ.OpenCVの関数は既に強力な最適実装がされているものが多いが,それでもまだまだ最適化されていない実装もあるし,それに自分で作った画素アクセスを含む画像処理をもっと高速化するには並列化がおススメの場合が多い.せっかくOpenGLを使って画像表示しているのなら,プログラマブルシェーダを使ってGPUレベルで画像処理させるのが最も効率的なはず.だからGLSLを使うのだ.
もう一つの理由は,このような特殊なGLSLの使い方をすることでGPGPUによる並列演算テクニックの入り口に立つことができるからだ.まあつまり,GPU並列演算の初歩を学ぶという勉強目的ってこと.リアルタイム処理が必要なAR技術では今後必要になるであろう技術なので,勉強がてらOpenCVの機能を肩代わりするのがどういうことなのかを学んでおくことには価値がある.それにまだGLSLのこと良く知らないし.

というわけで,今回はcv::remap()OpenGLレイヤーで実装することに挑戦しました.

実装

ソースコード

#pragma once

#include <OpenGLHeader.h>
#include <Shader.h>
#include <opencv2\opencv.hpp>

class Remapper
{
private:
    GLFWwindow *imgWindow;
    GLuint vao;
    GLuint vbo;
    GLuint imageObj, mapxObj, mapyObj;
    Shader s;
    int vertices;
    // バーテックスシェーダ
    const char *vertexSource =
        "#version 330 core \n"
        "layout(location = 0) in vec4 pv;\n"
        "void main(void)\n"
        "{\n"
        "    gl_Position = pv;\n"
        "}\n";
    // フラグメントシェーダ
    const char *fragmentSource =
        "#version 330 core \n"
        "uniform sampler2DRect image;\n"
        "uniform sampler2DRect map_x;\n"
        "uniform sampler2DRect map_y;\n"
        //"uniform vec2 resolution;\n"
        "layout(location = 0) out vec4 fc;\n"
        "void main(void)\n"
        "{\n"
        "   vec4 mapx = texture(map_x, gl_FragCoord.xy);\n"
        "   vec4 mapy = texture(map_y, gl_FragCoord.xy);\n"
        "    fc = texture(image, vec2(mapx.x, mapy.x));\n"
        "}\n";

public:

    Remapper()
    {
    }
    Remapper(GLFWwindow *window)
    {
        init(window);
    }
    void init(GLFWwindow *window)
    {
        int w, h;
        glfwMakeContextCurrent(window);
        glfwGetWindowSize(window, &w, &h);
        imgWindow = window;

        // 頂点配列オブジェクト
        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);

        // 頂点バッファオブジェクト
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);

        // [-1, 1] の正方形
        static const GLfloat position[][2] =
        {
            { -1.0f, -1.0f },
            { 1.0f, -1.0f },
            { 1.0f, 1.0f },
            { -1.0f, 1.0f }
        };
        vertices = sizeof(position) / sizeof(position[0]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(position), position, GL_STATIC_DRAW);
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
        glEnableVertexAttribArray(0);

        // テクスチャ
        glGenTextures(1, &imageObj);
        // source image について設定
        glBindTexture(GL_TEXTURE_RECTANGLE, imageObj);
        glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGB8, w, h, 0, GL_BGR, GL_UNSIGNED_BYTE, NULL);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        // map_x について設定
        glGenTextures(1, &mapxObj);
        glBindTexture(GL_TEXTURE_RECTANGLE, mapxObj);
        glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_R32F, w, h, 0, GL_RED, GL_FLOAT, NULL);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        // map_y について設定
        glGenTextures(1, &mapyObj);
        glBindTexture(GL_TEXTURE_RECTANGLE, mapyObj);
        glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_R32F, w, h, 0, GL_RED, GL_FLOAT, NULL);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

        // シェーダのロード
        s.initInlineGLSL(vertexSource, fragmentSource);
    }
    void remap(cv::Mat src, cv::Mat &dst, cv::Mat map_x, cv::Mat map_y)
    {
        glfwMakeContextCurrent(imgWindow);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        s.enable();// シェーダプログラムの使用開始
        GLint res[2] = { src.cols, src.rows };

        // srcをテクスチャ0に転送する
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_RECTANGLE, imageObj);
        glTexSubImage2D(GL_TEXTURE_RECTANGLE, 0, 0, 0, res[0], res[1], GL_BGR, GL_UNSIGNED_BYTE, src.data);
        // map_xをテクスチャ1に転送する
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_RECTANGLE, mapxObj);
        glTexSubImage2D(GL_TEXTURE_RECTANGLE, 0, 0, 0, res[0], res[1], GL_RED, GL_FLOAT, map_x.data);
        // map_yをテクスチャ2に転送する
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_RECTANGLE, mapyObj);
        glTexSubImage2D(GL_TEXTURE_RECTANGLE, 0, 0, 0, res[0], res[1], GL_RED, GL_FLOAT, map_y.data);

        glUniform1i(glGetUniformLocation(s.program, "image"), 0);       // uniform変数"image"にテクスチャユニット0(GL_TEXTURE0)を指定
        glUniform1i(glGetUniformLocation(s.program, "map_x"), 1);
        glUniform1i(glGetUniformLocation(s.program, "map_y"), 2);
        //glUniform2fv(glGetUniformLocation(s.program, "resolution"), 1, res);

        glBindVertexArray(vao);// 描画に使う頂点配列オブジェクトの指定
        glDrawArrays(GL_TRIANGLE_FAN, 0, vertices);// 図形の描画
        glBindVertexArray(0);// 頂点配列オブジェクトの指定解除
        
        s.disable();// シェーダプログラムの使用終了
        glBindTexture(GL_TEXTURE_RECTANGLE, 0);

        // 読み込み
        dst = cv::Mat(src.size(), CV_8UC3);
        glReadBuffer(GL_BACK);
        glReadPixels(0, 0, dst.cols, dst.rows, GL_BGR, GL_UNSIGNED_BYTE, dst.data);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    }
    ~Remapper()
    {
    }
};

例によって以前の記事で挙げたShaderクラスを使っています.OpenGLHeader.hにはOpenGLの他にGLFWとGLEW,GLMが含まれています.使い方は前回のGLImageクラスとほぼ同じです.Remapper::remap()のパラメータは順に,ソース画像(8bit,3ch),出力画像(8bit,3ch),マッピング画像(32bit,1ch)2枚,という構成で,ほぼcv::remap()と同様に使えます.

解説

フラグメントシェーダは,次のように書いてます.

 const char *fragmentSource =
        "#version 330 core \n"
        "uniform sampler2DRect image;\n"
        "uniform sampler2DRect map_x;\n"
        "uniform sampler2DRect map_y;\n"
        //"uniform vec2 resolution;\n"
        "layout(location = 0) out vec4 fc;\n"
        "void main(void)\n"
        "{\n"
        "   vec4 mapx = texture(map_x, gl_FragCoord.xy);\n"
        "   vec4 mapy = texture(map_y, gl_FragCoord.xy);\n"
        "    fc = texture(image, vec2(mapx.x, mapy.x));\n"
        "}\n";

やってることは簡単で,map_xmap_yの画素値には浮動小数点で変形先の座標値が格納されているので,texture()で現在座標gl_FragCoord.xyの値(=移動先の画素値)を呼び出し,新たに得たリマップ先の座標値の画素値vec2(mapx.x, mapy.x)を本来の画像テクスチャimageからサンプルする,という実装になっています.ここのテクスチャサンプラsampler2DRect imageはサンプリング時に自動で線形補間するようにRemapper::init()内で設定していますので,これだけですべての画素について滑らかに変形できます.
ちなみに,わざわざ\nで改行するのは,コンパイルエラーが起きた場合に行数を把握しやすくするためです.\nを省略することもできますが,その場合ソースコードを1行で書いたとみなされてしまい,吐き出されるコンパイルエラーも行数が分からなくなってしまいます.

Remapper::remap()では,一度レンダリングしてからSwapせずに,直接Swap前のバッファにアクセスして画像データを読み込み,cv::Matに書き出しています.その後,バッファを全クリアして終了です.つまり,この一連の流れでcv::Mat型からcv::Mat型に変換する画像処理をシェーダレベルで実行したことになります.

なお,OpenGLOpenCVでは画像テクスチャのY軸が逆ですが,ここでは反転処理を書いていません.なぜなら,今回の処理ではOpenGLに描かせた後にまたOpenCVに渡すので,Y軸が2回反転して結局元に戻るからです.もちろん,反転処理を入れればそれだけ遅くなります.GLSL上で反転させればそんなに速度変わらんかもしれんけど・・・

実装で苦労したところ

なぜかmap_xmap_yテクスチャが転送できないという問題に陥りました.ほぼ前回のGLImageと同じだから,正しくソースコードが書けているはずなのに・・・
しかし,原因は(やっぱりって感じだけど)glTexImage2D()及びglTexSubImage2D()の設定にありました.本来ならば

     glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_R32F, w, h, 0, GL_RED, GL_FLOAT, NULL);
        glTexSubImage2D(GL_TEXTURE_RECTANGLE, 0, 0, 0, res[0], res[1], GL_RED, GL_FLOAT, map_x.data);

と書くべきところを,

     glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_R, w, h, 0, GL_R, GL_FLOAT, NULL);
        glTexSubImage2D(GL_TEXTURE_RECTANGLE, 0, 0, 0, res[0], res[1], GL_R, GL_FLOAT, map_x.data);

とか

     glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_LUMINANCE, w, h, 0, GL_LUMINANCE, GL_FLOAT, NULL);
        glTexSubImage2D(GL_TEXTURE_RECTANGLE, 0, 0, 0, res[0], res[1], GL_LUMINANCE, GL_FLOAT, map_x.data);

とか書いていたわけです.間違えどころはinternal format(正しいコードでGL_R32F)とformat(正しいコードでGL_RED)の値の組み合わせで,推奨される正しい組み合わせはOpenGLのリファレンスに書いてあります.GL_RとかGL_LUMINANCEとかはここには使えないくせに,こんなGL_REDとほぼ同じ意味のグローバル定数を別の値として用意しているOpenGLに俺は文句を言いたい.更に言うと,3chの画像でGL_RGB2chの画像(某掲示板の事じゃないゾ!)GL_RGって書くなら普通1chならGL_Rかなーとか思いこんじゃうでしょ…

くそが!!!!!

ふぅ,すっきりした.

結果

補正前

f:id:Mzawa2:20160809155714p:plain

OpenCVで補正(cv::remap())

f:id:Mzawa2:20160809155757p:plain

OpenGL(GLSL)で補正(Remapper::remap())

f:id:Mzawa2:20160809155838p:plain

おお!ちゃんとできてる!これで並列化した高速画像処理演算ができるように…ん?

OpenCV: 0.014ms / 71.6fps

OpenGL: 0.016ms / 61.1fps

OpenCVの方が圧倒的に速いじゃんッッッ!!!

というわけで,まる2日かけて書いた今回の実装は完全に徒労に終わりました.でもきっと,今回の経験は将来の役に立つでしょう…

参考文献

床井研究室 - GLSL で画像処理 (1) 画像を取り込む

床井研究室 - GLSL で画像処理 (3) ワーピング

実は今回の記事は↑の丸パクリリファインです.床井先生いつも参考にさせてもらってます.ありがとうございます.

追記(2016/09/07)

この記事によるとどうやらglPixelRead()がめちゃ遅いらしく,PixelBufferObject(PBO)を活用するともっと高速化できそうです.PBOは4K映像も30FPSいけるくらい速いらしいし,今度試してみよう.

OpenGLでカメラ画像を表示

OpenCVは好きなんだけど標準の描画GUIであるHighGUIはWindowsマシンでは60fps以上出ない(過去記事参照)ので,私は最近では,画像処理だけOpenCVでやらせて描画ウィンドウはGLFWを使ってOpenGLで描画させる,という方法を採用しています.この方が圧倒的に速いだけでなく,簡単に3DモデルをオーバーラップできたりとARとの親和性が良く,非常に便利です.HighGUIの貧弱なトラックバーなんかもOpenGLGUIライブラリで改良できますし,シェーダ言語GLSLを上手く使えばCUDAやOpenCLなんて使わなくても並列化画像処理まで実装できます(これはちょっと難しいけど).

ただ問題は,OpenGLで画像を扱うのがOpenCVに比べてちょっと難しいという事です.OpenGLはかなりプリミティブなところを触るAPIなので,ウィンドウに画像を描画するだけでも一苦労です.そこで今回は,GLFWのウィンドウに簡単にOpenCVの画像を描画できるクラスを作りました.

OpenCV画像のOpenGL描画用クラス

ソースコード

/********************************************************
OpenGL Image with OpenCV
GLFWでOpenCVのcv::Matを背景描画するためのクラス

How to Use:
1. メインループに入る前にGLImageを生成
2. 描画したいGLFWwindowを与えてGLImageを初期化
3. メインループ内で次の様に書く(ex. mainWindowの背景にframeImgを描画)

// Change Current Window
glfwMakeContextCurrent(mainWindow);
// Clear Buffer Bits
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Draw Image
glImg.draw(frameImg);      //  <- Only rendering
// Clear Depth Bits (so you can overwride CG on frameImg)
glClear(GL_DEPTH_BUFFER_BIT);
// Draw your CG
// End Draw
glfwSwapBuffers(mainWindow);

Change 20160119:
・コンストラクタで初期化できるようにした
・コメント大幅追加
・GLSLをインライン化して外部ファイルを不要にした

*********************************************************/

#pragma once

#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "glew32.lib")
#pragma comment(lib, "glfw3.lib")
#include <stdio.h>
#include <stdlib.h>

#include <GL\glew.h>
#include <GL/glfw3.h>    
#include "Shader.h"
#include <opencv2\opencv.hpp>

class GLImage
{
private:
    GLFWwindow *imgWindow;
    GLuint vao;     // 頂点配列オブジェクト
    GLuint vbo;     // 頂点バッファオブジェクト
    GLuint image;   // テクスチャオブジェクト
    GLuint imageLoc;// オブジェクトの場所
    Shader s;       // シェーダ
    // バーテックスシェーダ
    const char *vertexSource = 
        "#version 330 core \n" 
        "layout(location = 0) in vec4 pv;\n" 
        "void main(void)\n" 
        "{\n" 
        "    gl_Position = pv;\n" 
        "}\n";
    // フラグメントシェーダ
    const char *fragmentSource =
        "#version 330 core \n"
        "uniform sampler2DRect image;\n"
        "layout(location = 0) out vec4 fc;\n"
        "void main(void)\n"
        "{\n"
        "    fc = texture(image, gl_FragCoord.xy);\n"
        "}\n";
    int vertices;

public:
    GLImage()
    {
    }
    GLImage(GLFWwindow *window)
    {
        init(window);
    }
    void init(GLFWwindow *window)
    {
        int w, h;
        glfwMakeContextCurrent(window);
        glfwGetWindowSize(window, &w, &h);
        imgWindow = window;

        // 頂点配列オブジェクト
        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);

        // 頂点バッファオブジェクト
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);

        // [-1, 1] の正方形
        static const GLfloat position[][2] =
        {
            { -1.0f, -1.0f },
            { 1.0f, -1.0f },
            { 1.0f, 1.0f },
            { -1.0f, 1.0f }
        };
        vertices = sizeof(position) / sizeof (position[0]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(position), position, GL_STATIC_DRAW);
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
        glEnableVertexAttribArray(0);

        // テクスチャ
        glGenTextures(1, &image);
        glBindTexture(GL_TEXTURE_RECTANGLE, image);
        glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGB, w, h, 0, GL_BGR, GL_UNSIGNED_BYTE, NULL);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

        // シェーダのロード
        s.initInlineGLSL(vertexSource, fragmentSource);
        imageLoc = glGetUniformLocation(s.program, "image");
    }
    void draw(cv::Mat frame)
    {
        glfwMakeContextCurrent(imgWindow);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // 切り出した画像をテクスチャに転送する
        cv::flip(frame, frame, 0);
        glBindTexture(GL_TEXTURE_RECTANGLE, image);
        glTexSubImage2D(GL_TEXTURE_RECTANGLE, 0, 0, 0, frame.cols, frame.rows, GL_BGR, GL_UNSIGNED_BYTE, frame.data);

        // シェーダプログラムの使用開始
        s.enable();

        // uniform サンプラの指定
        glUniform1i(imageLoc, 0);

        // テクスチャユニットとテクスチャの指定
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_RECTANGLE, image);

        // 描画に使う頂点配列オブジェクトの指定
        glBindVertexArray(vao);

        // 図形の描画
        glDrawArrays(GL_TRIANGLE_FAN, 0, vertices);

        // 頂点配列オブジェクトの指定解除
        glBindVertexArray(0);

        // シェーダプログラムの使用終了
        s.disable();
    }
};

中身の解説

中でやっていることは,インラインのシェーダをコンパイルさせ,その後ビューポートと同じ大きさのポリゴンを用意し,OpenCV画像データをテクスチャとしてポリゴンと一緒にシェーダに転送する,という流れです.注意すべきところは,OpenCVのY軸とOpenGLのY軸は向きが逆だったり,BGR順じゃなくてRGB順だったりと,OpenCVOpenGLで画像データの扱いが多少異なるということです.このクラスではそこを気にしなくても良いようになっていて,OpenCVのBGR順のカラー画像をHighGUIと同様にそのまま出力できるようになってます.

なお,この画像描画クラスではOpenGL1.1の固定シェーダ機能を使っておらず,プログラマブルシェーダを利用しています.通常ではWindows環境はOpenGL1.1の機能しか使えないように制約しているので,GLFWとOpenGL, OpenCVの他に,その制約を取っ払うためのライブラリであるGLEWも使用しています.WindowsでもモダンOpenGLをプログラミングできるようにするためのライブラリにはglewとgl3wという2つのライブラリがあるようですが,どちらを使っても構わないと思います.

また,ここで用いているShader.hというのは私が作ったシェーダコンパイル用クラスです.中身はこちら.

/********************************************************
Shader Class
GLSLシェーダを管理するライブラリ
参考:
Modern OpenGL Tutorial http://www.opengl-tutorial.org/
床井研究室    http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20090827

Change 20160119:
シェーダのソースコード文字列を直接読む仕様を追加
*********************************************************/

#pragma once

#include "OpenGLHeader.h"

class Shader
{
protected:
    GLuint vertexShader, fragmentShader;        // シェーダオブジェクト
    const char *vertexFileName, *fragmentFileName;        // シェーダファイル名
    void readShaderCompile(GLuint shader, const char *file); // .shaderのコンパイル
    void readInlineShaderCompile(GLuint shader, const char *source); // シェーダのインラインソースをコンパイル
    void link(GLuint prog);        // コンパイルしたshaderをリンクする
public:
    GLuint program;         // シェーダプログラム
    Shader();               // コンストラクタ
    ~Shader();              // デストラクタ
    Shader &operator=(Shader &_s) // コピーコンストラクタ
    {
        initGLSL(_s.vertexFileName, _s.fragmentFileName);
        return *this;
    }
    // 初期化
    // フラグメントシェーダーの有無で分ける
    void initGLSL(const char *vertexFile);
    void initGLSL(const char *vertexFile, const char *fragmentFile);
    void initInlineGLSL(const char *vertexSource, const char *fragmentSource);
    // 有効化
    void enable(){ glUseProgram(program); }
    void disable(){ glUseProgram(0); }

};
/********************************************************
Shader Class
GLSLシェーダを管理するライブラリ
参考:
Modern OpenGL Tutorial http://www.opengl-tutorial.org/
床井研究室    http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20090827

Change 20160119:
シェーダのソースコード文字列を直接読む仕様を追加
*********************************************************/

#include "Shader.h"

Shader::Shader()
{
}

Shader::~Shader()
{
}

void Shader::readShaderCompile(GLuint shader, const char *file)
{
    FILE *fp;
    char *buf;
    GLsizei size, len;
    GLint compiled;
    // ファイルを開く
    fopen_s(&fp, file, "rb");
    if (!fp) printf("ファイルを開くことができません %s\n", file);
    //ファイルの末尾に移動し現在位置を得る
    fseek(fp, 0, SEEK_END);
    size = ftell(fp);//ファイルサイズを取得
    // ファイルサイズのメモリを確保
    buf = (GLchar *)malloc(size);
    if (buf == NULL){
        printf("メモリが確保できませんでした \n");
    }
    // ファイルを先頭から読み込む
    fseek(fp, 0, SEEK_SET);
    fread(buf, 1, size, fp);
    //シェーダオブジェクトにプログラムをセット
    glShaderSource(shader, 1, (const GLchar **)&buf, &size);
    //シェーダ読み込み領域の解放
    free(buf);
    fclose(fp);
    // シェーダのコンパイル
    glCompileShader(shader);
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (compiled == GL_FALSE){
        printf("コンパイルできませんでした!!: %s \n ", file);
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &size);
        if (size > 0){
            buf = (char *)malloc(size);
            glGetShaderInfoLog(shader, size, &len, buf);
            printf(buf);
            free(buf);
        }
    }
}

void Shader::readInlineShaderCompile(GLuint shader, const char *source)
{
    char *infolog;
    GLsizei size, infolen;
    GLint compiled;
    //printf(source);
    size = strlen(source);
    // シェーダオブジェクトにプログラムをセット
    glShaderSource(shader, 1, (const GLchar**)&source, &size);

    // シェーダのコンパイル
    glCompileShader(shader);
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (compiled == GL_FALSE){
        printf("コンパイルできませんでした!!: InlineShader \n ");
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &size);
        if (size > 0){
            infolog = (char *)malloc(size);
            glGetShaderInfoLog(shader, size, &infolen, infolog);
            printf(infolog);
            free(infolog);
        }
    }
}

void Shader::link(GLuint prog)
{
    GLsizei size, len;
    GLint linked;
    char *infoLog;
    glLinkProgram(prog);
    glGetProgramiv(prog, GL_LINK_STATUS, &linked);
    if (linked == GL_FALSE){
        printf("リンクできませんでした!! \n");
        glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &size);
        if (size > 0){
            infoLog = (char *)malloc(size);
            glGetProgramInfoLog(prog, size, &len, infoLog);
            printf(infoLog);
            free(infoLog);
        }
    }
}

void Shader::initGLSL(const char *vertexFile)
{
    // GPU,OpenGL情報
    printf("VENDOR= %s \n", glGetString(GL_VENDOR));
    printf("GPU= %s \n", glGetString(GL_RENDERER));
    printf("OpenGL= %s \n", glGetString(GL_VERSION));
    printf("GLSL= %s \n", glGetString(GL_SHADING_LANGUAGE_VERSION));
    //シェーダーオブジェクトの作成
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    //シェーダーの読み込みとコンパイル
    readShaderCompile(vertexShader, vertexFile);
    // シェーダプログラムの作成
    program = glCreateProgram();
    // シェーダオブジェクトをシェーダプログラムに関連付ける
    glAttachShader(program, vertexShader);
    // シェーダオブジェクトの削除
    glDeleteShader(vertexShader);
    // シェーダプログラムのリンク
    link(program);
}

void Shader::initGLSL(const char *vertexFile, const char *fragmentFile)
{
    //glewの初期化
    GLenum err = glewInit();
    if (err != GLEW_OK){
        printf("Error: %s\n", glewGetErrorString(err));
    }
    memcpy(&vertexFileName, &vertexFile, sizeof(vertexFile) / sizeof(char));
    memcpy(&fragmentFileName, &fragmentFile, sizeof(fragmentFile) / sizeof(char));
    // GPU,OpenGL情報
    printf("VENDOR= %s \n", glGetString(GL_VENDOR));
    printf("GPU= %s \n", glGetString(GL_RENDERER));
    printf("OpenGL= %s \n", glGetString(GL_VERSION));
    printf("GLSL= %s \n", glGetString(GL_SHADING_LANGUAGE_VERSION));
    //シェーダーオブジェクトの作成
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    //シェーダーの読み込みとコンパイル
    readShaderCompile(vertexShader, vertexFile);
    readShaderCompile(fragmentShader, fragmentFile);
    // プログラムオブジェクトの作成
    program = glCreateProgram();
    // シェーダオブジェクトをシェーダプログラムに関連付ける
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    // シェーダオブジェクトの削除
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    // シェーダプログラムのリンク
    link(program);
}

void Shader::initInlineGLSL(const char *vertexSource, const char *fragmentSource)
{
    //glewの初期化
    GLenum err = glewInit();
    if (err != GLEW_OK){
        printf("Error: %s\n", glewGetErrorString(err));
    }
    // 擬似的にシェーダファイル名を作成
    char vertexFile[] = "InlineVertex", fragmentFile[] = "InlineFragment";
    memcpy(&vertexFileName, &vertexFile, sizeof(vertexFile) / sizeof(char));
    memcpy(&fragmentFileName, &fragmentFile, sizeof(fragmentFile) / sizeof(char));
    // GPU,OpenGL情報
    printf("VENDOR= %s \n", glGetString(GL_VENDOR));
    printf("GPU= %s \n", glGetString(GL_RENDERER));
    printf("OpenGL= %s \n", glGetString(GL_VERSION));
    printf("GLSL= %s \n", glGetString(GL_SHADING_LANGUAGE_VERSION));
    //シェーダーオブジェクトの作成
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    //シェーダーの読み込みとコンパイル
    readInlineShaderCompile(vertexShader, vertexSource);
    readInlineShaderCompile(fragmentShader, fragmentSource);
    // プログラムオブジェクトの作成
    program = glCreateProgram();
    // シェーダオブジェクトをシェーダプログラムに関連付ける
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    // シェーダオブジェクトの削除
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    // シェーダプログラムのリンク
    link(program);
}

使用方法

コメント文に書いてある通りですが,使用する疑似コードは次の通りです.疑似コードなのでこのままコピペしても動きません(笑).

#include "GLImage.h"

GLImage glImg;

int main(void)
{
  setup();    //mainWindowの用意
  //  Initialize GLImage and set context
  glImg.init(mainWindow);
  cv::VideoCapture cap(0);
  while(1){
    //  get frame Image
    cv::Mat frameImg;
    cap >> frameImg;
    // Change Current Window
    glfwMakeContextCurrent(mainWindow);
    // Clear Buffer Bits
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // Draw Image
    glImg.draw(frameImg);       // <- Only rendering
    // Clear Depth Bits (so you can overwride CG on frameImg)
    glClear(GL_DEPTH_BUFFER_BIT);
    // Draw your CG
    draw();
    // End Draw
    glfwSwapBuffers(mainWindow);
  }
}

大事なところは,ARでCGを描画する場合はglImg.draw()の直後にglClear(GL_DEPTH_BUFFER_BIT)を書き加えてデプスバッファを消去してやる,という事です.GLImageはビューポートと同じ大きさのポリゴンにテクスチャを貼りつけるという作業をするため,glImg.draw()を実行するとデプスバッファが平面に上書きされてしまいます.GLImageで描いた画像を背景画像にするには,デプスバッファを一旦消去してからCGを描画して,最後にglfwSwapBuffers()を実行します.

出力結果

こんな感じです(めちゃめちゃ暗い…).画像ではGUIも書き込まれていますが,カメラ画像をこのように映すことができます.なお,GUIにはimgui(ocornut/imguiの方)を使用しています.

f:id:Mzawa2:20160804205627p:plain

デルタ型3Dプリンタキット「Zonestar D810」を組み立てた(そのさん)

もはやプリントできている状態なので組み立てたと言うタイトルはふさわしくない気もするのですが…それにしても自宅に3Dプリンタがある生活っていいですよね!動きを眺めているだけで休日が終わってしまった(笑)

さて,今回はZonestar D810で任意のSTLデータをプリントするのにチャレンジします.3Dプリンタに詳しくない人も読んでるかもしれないので一応説明しておくと,3Dプリンタは3Dモデルをそのまま読み込んで出力するわけではありません.(おそらく)どの造形(or切削)方式であれ,3DモデルのSTLデータをプリントするのに具体的にどうプリンタを動かすか(例えばヘッドの軌跡)を命令するためのファイル形式であるGコードを生成する過程が必要です.STLデータからGコードを生成するソフトウェアを3Dプリンタ界隈ではスライサーと呼んでいます.いかに3Dプリンタの性能が良くても,このスライサーの性能如何で仕上がりが全く異なってしまいます.

そこで今回は,前回までにテスト用のGコードからプリントできることを確認したので,D810用にスライサーの設定を変えながら試してみることにします.

今回のテスト用STLデータは…

f:id:Mzawa2:20160719023637j:plain

前回割れたこいつ(キャリッジパーツ)です(笑)

ホストアプリケーション

今回は安定性のため,オフラインでSDカード内のGコードからプリントすることにします.PC-プリンタ間の接続は行いません.なので,今回のタスクはGコードを出力して保存するところまでです.

今回は3Dプリンタ用ホストアプリケーションとしてRepetier-Hostを使いました.こいつはスライサーにSlic3rを利用しています.

D810のファイル内に付属のKISSlicerというのもありましたが,インターフェースが古臭い以外にも,何回か試してみたところあんまりスライサーの性能が良くなさそうだったのでやめました(Macに対応してないし).もう一つ推奨っぽいやつにCura(元々はUltimaker2専用アプリ)というのもありましたが,こちらは最新バージョンだと細かい設定のいじり方が良くわからなくなってたのでボツ.性能がいいと評判のUltimaker2のスライサーだから使ってみたかったんだけど…

あ,ちなみに環境はMac OSX EI Capitanです.多分Winでも設定方法は同じだと思う.

アプリケーションの設定

f:id:Mzawa2:20160723173816p:plain

直ぐにプリントできるわけではなく,最初に起動した時には色々と細かい設定が必要です.キャプチャ画像は既に設定済みですが,最初はSTLデータもなければプリントエリアの形状も円柱状ではありません.まずは右上の歯車"Printer settings"をクリック.

f:id:Mzawa2:20160723175002p:plain

出てくるポップアップウィンドウがこちら.DimensionタブでPrinter typeを画像のように"Delta Printer with Cylindric Print Shape"を選択.こうすることでアプリケーションのプリントエリアが円柱状になります.次にDiameter, HeightをD810の公称値のΦ200×300に設定します.

f:id:Mzawa2:20160723175256p:plain

次がBehaviourタブで基本的な動作設定.今回はDefault Extruder TemperatureをPLAにしてはちょっと高めの210℃に設定し,あとはデフォルトのままにしておきました.これにてOK.

Slic3r(スライサー)の設定

その後,メインウィンドウのSlicerタブでSlic3r - Configurationボタンをクリックすると,Slic3rの細かい設定ができます.これからする設定は多いですが重要です.

f:id:Mzawa2:20160723175726p:plain

まずLayer heightです.これはZ軸精度を表すのですが,最初のレイヤーだけは厚みを別に設定できます.これが便利で,何しろデルタ型3Dプリンタだとその構造上ビルドプレートに水平に動いてくれるとは保証できないわけで,少し初期レイヤーに厚みを持たせることである程度は水平誤差を吸収してくれます.今回は初期レイヤーは0.3mm,その後のレイヤーは0.1mm(D810公称最小値)としました.その他は初期値のまま.

f:id:Mzawa2:20160723180402p:plain

次にInfill.ソリッドの中身をどう埋めるかを規定します.今回はプリンタ部品に使うので,密度は多めに40% (dencity = 0.4),Fill patternはhoneycomb(蜂の巣),Top/bottom fill patternはconcentricに設定しました.ハニカム構造は同じ平面なら最も線の長さ(=フィラメントの量)が短い構造と言われています.concentricは渦巻き状に平面を埋めていく設定といえばわかると思います.

f:id:Mzawa2:20160723181420p:plain

Sppedは…デフォルトのままにしました.プリント速度が気になるようだったら変えようと思います.

f:id:Mzawa2:20160723181523p:plain

Skirt and brimは,初期レイヤーの作り方のオプションです.

Skirtは初期レイヤーの周囲に一度ループを描く設定です.いきなり本体のプリントを始めるとホットエンドを温める段階で出てきたフィラメントが邪魔になったりして,初期レイヤーがうまくプレートにくっつかずに剥がれてしまうことが本当によくあります.そこでSkirtを描いてやることで,これを軽減したり異常を早く察知してプリントに入る前に中止できたりします.1周分じゃ不安だったのでLoops = 2とし,Distance from object = 8mmとして,デフォルト設定よりも少し離してやりました.

Brimは初期レイヤーのみプリント物を周囲に大きく描く設定で,プリント物の周囲にピラピラしたものを付け足します(名前的にはこっちの方がスカートっぽい…).こうすることでプリント物とビルドプレートの接触面積を大きくでき,よりプリント中に剥がれにくくなることを期待できます.様子を見るためにBrim width = 3mmにしました.

f:id:Mzawa2:20160723182745p:plain

Support materialではサポート材の造形方法,Raft(捨てる初期レイヤー)の設定をします.今回はフィラメント節約のため両方使わないので設定しません.Notes以降の設定はD810には関係ないと思うので未設定です.

f:id:Mzawa2:20160723183054p:plain

ここまでできたら,設定を保存しましょう.設定は上のタブ毎に保存する必要があります.設定を変更すると(modified)と出てくるので,横の保存ボタンをクリックします.

f:id:Mzawa2:20160723183234p:plain

保存用ポップアップで設定名をリネームしてOK.

f:id:Mzawa2:20160723183319p:plain

次に隣タブのFilament Settingに移ります.Diameterはフィラメント直径です.D810の場合1.75mmを使います.デフォルト設定は3mmの方になっているので注意しましょう.Temparatureは初期レイヤーとその他のレイヤーで別々に設定できますが,同じにしておいた方がいいです.初期レイヤーをプリントし終えた直後にその場に留まって加熱or冷却し始めるので,その間にどんどんフィラメントが垂れてきてそこだけこんもりとフィラメントが盛り上がり,プリント失敗の原因になります(実体験).

Cooling設定はそのままでもいいと思います.後,ここで次のタブに移る前にちゃんと設定を保存しておきましょう.

f:id:Mzawa2:20160723183638p:plain

Printer Settingタブでは,プリンタ自体の設定を決定します.Print centerは(x,y)=(0,0)にしました.Bed sizeが直交座標になっていますが,無視しましょう.ちゃんと円柱状に設定できてますので.

FirmwareはD810の場合Zonestarのオリジナルですが,Marlinベースと考えていいでしょう.RepRap (Marlin/Sprinter)を選択してうまく動いてくれています.

f:id:Mzawa2:20160723184605p:plain

次のCustom G-codeは結構特殊な設定です.開始時と終了時に付け加えるGコードを直接編集できます.D810の推奨設定として,開始時GコードのG28(ホームポジションに戻る)の直後にG29(オートレベリング)を加える,というものがあります.こうすると開始時にオートレベリングを実行してからプリントを始めるので,プリント実行前に自分で水平調整をしなくて良くなります.

f:id:Mzawa2:20160723185901p:plain

最後はExtruder 1です.Nossle diameterはD810の場合0.4mmです.別のノズルをつけた場合はそれに合わせてください.Positionは複数のExtruderがある場合の話なので無視します.

Retractionはフィラメントの退避設定です.飛び地に移動する際,Lengthで設定した分だけフィラメントを巻き戻してやります.デフォルト設定だと退避しなさすぎて移動時にもフィラメントがこぼれてしまい,プリント後に大変なことになったりプリント失敗したりします.私の場合,画像の設定でうまくいきました.

最後に,忘れずに設定を保存しておきましょう.

f:id:Mzawa2:20160723173816p:plain

元の画面に戻って,Print Settingsなどの空白の設定欄に先ほど保存した設定を選択して入れておきます.

以上の設定が完了したら,スライサーの準備は完了です.STLファイルを読み込んでプリントしたいモデルを配置したら,Slice with Slic3rでGコードを生成します.

f:id:Mzawa2:20160723190858p:plain

しばらくすると,自動的にこの画面に遷移します.下のログに色々と情報が出てきます.例えば,この3Dモデルの生成には10m程度のフィラメントが必要みたいです.このGコードを保存し,SDカードに移してやれば後はプリントするだけです.

プリンタ本体のトラブル

ホットエンドからフィラメントが溢れ出てしまう

高確率でノズルのネジが緩んでいます.そんな時は一旦フィラメントを抜いてホットエンド内を掃除しましょう.掃除は樹脂が溶ける温度までホットエンドを温めたまま作業しますので,火傷の危険があります.注意して作業しましょう.

まずホットエンドを温め,ホットエンドやノズルの周りの溶けた樹脂をドライバーか何かでこそぎ落とします.次にノズルをペンチを使って外し,ネジの部分にこびりついた樹脂もこそぎ落とします.ノズルは非常に熱いです.

その後,フィラメント挿入口からパーツクリーナーやシリコンスプレーを吹き付け,ホットエンド内を洗います.最後にノズルをしっかりと緩まないように取り付け,ノズルから十分な量が出てくるまでフィラメントを挿入します.これでOKだと思います.

なお,この現象が起きる原因はフィラメントの射出不良です.例えばZ軸キャリブレーションがうまくいっておらず,初期レイヤー成型時にノズルがビルドプレートにぴったりくっついてしまってフィラメントが十分押し出せない時間が長く続く,というような場合にこのような現象が起きやすいです.今後起きないようにするためには,Z軸キャリブレーションをしっかりと行うことが重要です.

すぐ初期レイヤーが剥がれる

主にZ軸キャリブレーションができていないことが原因です.何度もZ軸キャリブレーションを繰り返して,再現性が得られてからプリントを始めると良いでしょう.

あと,機械的な理由もあります.D810のオートレベリングはキャリッジに結構負担がかかるので,キャリッジが弱いとバネのようになってうまく水平出しができないです.その状態だと,オートレベリングしたのに初期レイヤーの厚みが場所によって違うなんてことになり,最悪の場合ビルドプレートをガリっと削ってしまうことになります.この場合,初期レイヤーの厚みをちょっと厚めにしておくと良いと思います.

ヒートベッドを使う場合,温度設定を60〜80℃くらいの間で色々と試してみるといいと思います.実は最初組み立てている時には,ヒートベッドだけ独立で別電源とか頭悪い設計だなーと思ってたのですが,いざ使ってみるとGコードを変えずにヒートベッドの温度を独立に変えられるという仕組みはなかなかに使い心地が良かったりします.でも電源プラグはメインボードと統一して欲しかった…その他の制御系が上にある関係上困難なのはわかるけどさぁ…

結果

f:id:Mzawa2:20160723193359j:plain

f:id:Mzawa2:20160723193447j:plain

15回くらい失敗してようやくうまくいきました.それでもちょっと底の部分が反り上がってしまいましたが,これでもキャリッジとしては機能するでしょう.まあ,そもそも壊れたキャリッジを使った3Dプリンタでプリントしてるのでプリント性能はこれが限度でしょう.

f:id:Mzawa2:20160723193622j:plain

ゴミを取り除くとこんな感じです.なかなかいいんじゃないでしょうか.

 

デルタ型3Dプリンタキット「Zonestar D810」を組み立てた(そのに)

 前回は到着まで書いたので,ここから組み立てに入ります(記事作成時点で動いてます).

f:id:Mzawa2:20160718223807j:plain

とは言っても殆どマニュアル通りなのでそんなに書くことはないです.中華3Dプリンタの場合はマニュアルが途中で終わっているとか部品表がないなんて事もあるらしいですが,D810ではそんな事はありませんでした.動画解説もあるのでイメージしやすいし,丁寧な方でしょう.ここではつまづいたところを雑記しておきます.

1.部品が足りない

・・・まあある程度予想はしてましたが,自分の場合テフロンチューブと細かなネジ類が足りませんでした.致命的な部品ではないので良しとしましょう.ちなみにテフロンチューブについて問い合わせたら,

Hello ,Dear 
Could you pls send more photos ? we had checked very carefully before shipment ,it should be included ,teflon tube is a white tube ,it looks the same color with the packaging ,pls double check and advise ,thanks ~ 

 と返ってきました.Zonestarは丁寧な業者ですし確かに入れたはずと言っているので,それを信じるならば輸入の際の開封確認作業時に失くされた可能性が高いです.テフロンチューブだけもう一回送ってよこせなんて時間かかってアホらしいので,近くのホームセンターでΦ4のエアチューブを購入して中にシリコンスプレーを吹き付けることで代用しました.ついでに足りないネジ類も購入し,先方にメッセージを送信.

Hello, seller. The picture is the rest of hardware setup D810 documentation slides. I cannot find teflon tube, but it's OK. Maybe it was lost while checking goods imported. I'll buy another tube.And I have finished hardware installation. Thanks a lot! 

f:id:Mzawa2:20160719032602j:plain

 これにて事なきを得ました.

2.3Dプリントパーツが割れる

D810はパーツの一部を3Dプリントで作ってありますが,3Dプリント品は積層方向への荷重に弱く,またプリント不良があるとそこから一気に割れます.自分の場合,キャリッジのアームをネジ止めする部分が割れちゃいました.(写真は割れてないやつ)

f:id:Mzawa2:20160719023637j:plain

精度に非常に関係する部分なので焦りましたが,瞬間接着剤で固めたらなんとかなりました.但し,プリント不良の部分がバネみたいになってしまったせいでキャリブレーションに影響が出てしまっています.このままでは問題なので,送ってもらったSTLファイルを早くプリントして交換する予定です.

3.平面出しができない

電装系も組みあがり,さてキャリブレーションしようと動かしたところ,ちょんちょんとビルドプレートをクリックする間隔が明らかに前後で異なり,ついには動かす段階でガツンとプレートにぶつかってキャリブレーションが失敗しました.明らかにホットエンドに対してプレートが斜めになっているのが原因ですが,プレートのネジ調整の範囲では直りません.

真の原因は支柱にありました.上部のコーナーパーツの奥までしっかりと挿入されていなかったため,そこでベルトの長さが変わってしまったようです.ただでさえキツキツな作りなのに上部コーナーパーツはちゃんと差さっているか見えないため,奥まで入り込んでいないことに気付きづらいです.もうちょっと隙間広くして入れやすくしてくれてもいいのに・・・

4.フィラメントが送れない

キャリブレーションが正常に動くようになり,プリントしないテスト用のGコードもうまく動いてくれたので,次はエクストルーダをチェックしたところ全く動きません.X軸モータとケーブル差し込み位置を入れ替えたところちゃんと動いたので,モータやケーブルが原因ではないようです.マルチメータで測ったところ,どうやらメイン基板の時点で出力信号が出ていないようです.ヤバい.壊れてるのか・・・?

youtu.be

これ以上AliExpress担当に技術的トラブルを報告するのも良くないので,テクニカルサポートエンジニアにメールを送りました.すると,

Hi Friend,Do you have heat the hotend before you load the filament?? In order to protect the extruder motor, the printer will prohibit rotate extruder motor when the hotend temperature less than 150 degree.
 な,なんだってー!(画像略)
モータの安全のため150℃以上になるまで動かないとかいうFAQにも書いてない仕様があるらしい.試しに180℃に設定して同じく"Change Filament"コマンドを実行するとちゃんと動きました.

 5.プリントしようとするとファームウェアが落ちる

上記と同時に判明した問題です.プリント時のオートレベリングまでは問題なく動くのですが,その直後に強制終了してファームウェアが再起動してしまう問題にぶち当たり,途方に暮れてしまいました.

youtu.be

見た感じ,ベルト用モータとエクストルーダ用モータとホットエンドを同時に起動した時に落ちるっぽいので,電力不足が原因に思えました.なぜかというと,ホットエンドやエクストルーダを使用しないテスト用Gコードはちゃんと動くからです.メインボード電源は12V5Aなので,試しに12V10Aのヒーテッドベッド用電源をメインボードに挿すという危険極まりない行為をしてみたところ,ちゃんとプリントできるようになりました.

でもこれをやっちゃうと,下のヒーテッドベッドが使えなくなっちゃうんだよね・・・どうしよう.

結果

youtu.be

f:id:Mzawa2:20160719032155j:plain

だいぶいい感じです.側面ががたついてますが,多分これは割れたキャリッジのせいでしょう.ついでに花びんも作ってみた.

youtu.be

次はキャリッジパーツを作っておこう・・・

デルタ型3Dプリンタキット「Zonestar D810」を組み立てた(そのいち)

前々からマイ3Dプリンタが欲しかった(自作したかった)のですが,最近やっとお金が貯まったので思い切ってキットを買っちゃいました.格安になるように自分で一から設計しようかとも思ったのですが,流石に本体からフィラメントまで諸々が揃っていながら2万円台ってのを見るとそっちのがトータルコスト安いよね,ってことで香港から直輸入しました.それでも本当にバラバラなところから作るので,ちゃんと自作感があります.中学英語が読み書きできるならオススメ.

実は今まで研究室の3Dプリンタ(Repricator2)を実効支配してたのでマイ3Dプリンタは不要だったのですが,最近そいつのホットエンドに樹脂がべったりこびりついて使えなくなっちゃいました.それも購入に踏み切ったひとつの理由です.

はじめてのAliExpress

買うなら絶対デルタ型!って思ってたので(だって超カッコいいし),デルタロボットタイプの3Dプリンタが格安で売っていることで有名なAliExpressでの輸入に踏み切りました.RepRapプロジェクトのKosselコピー品がズラリと並んでいて,しかもどれも安いのです.Kosselは廉価を目的としてるので当たり前ですけど.

私の購入条件

以下の条件で購入を検討しました.

  • 予算3万円未満(送料・関税込なので実質$250位が目安)
  • デルタロボット型(カッコいいだろう!?(ギャキィ))
  • ヒーテッドベッド付属(ABSを使いたい)
  • オートレベリング機能あり(デルタ型ならあるとめちゃくちゃ便利)
  • フィラメント・SDカード等備品一式が付属(楽だし)
  • 最小積層ピッチ0.1mm以下(最近のだったらこれくらい普通)
  • 液晶は4段もしくはグラフィック液晶(2段キャラクタは流石に見づらい)
  • ビルド範囲がなるべく広い(安いのはやたら小さかったりする)
  • 2色刷り可能タイプ(多分無理だけど可能であれば…)
  • 店の評価が高いこと(中国のショップなのでこれが重要)

ぶっちゃけこんな構成で3万未満とかできるのか?って思うような超我儘な構成ですが,なんとお眼鏡に適うものがちゃんとあるんですね.流石中国.しかも送料無料だし.

Buy Products Online from China Wholesalers at Aliexpress.com

2色刷りタイプもありましたが,そっちは予算オーバーだったので除外.(最初は$200以下じゃんやったー!と思ってたのですが,実はその値段で買えるのはフィラメントだけで,本体を買うにはもう$250必要だったというオチでした.)

電装系が頭の方にあるので安定性がちょっと気になりますが,黒ボディに赤パーツという厨二心をくすぐるマシンだったのでまあいいやって感じで購入ボタンポチーッ!

注文から到着まで

幸いにしてZonestarは対応が丁寧で,平易な英語でスムーズなやり取りができました.

実はヒーテッドベッドが付属するのはOption 3を選んだときだけだったのですが,注文時点でOption 3が選択できなかったのとOptionの意味がよくわかっていなかったため,Option 2を注文していました.そこで「Option 2買っちゃったけどヒーテッドベッドが欲しい.買える?」と先方に伝えた所,「いいよ!じゃあ追加の$31をここから買ってね!」と柔軟に対応してくれました.これはありがたい.

FedExで送ってもらいましたが,通関処理のところで「コマーシャルインボイスの記入が不十分です」とか言われて止まってしまいました.これはコマーシャルインボイス(送付状のようなもの)の記載が不十分のため通関できないということです.私の場合,宛先に自分の名字が書かれていなかったのでNGを食らったみたいです.FedExカスタマーサービスに電話口で修正することで事なきを得ました.

そして到着.注文から1週間で追跡番号の連絡,それから4営業日で届きました.海外からなのに意外と早い気がする.

開封

f:id:Mzawa2:20160717141909j:plain

キターーーーーーーーーーー!!来た来た来たーーーーーーーーーーーッ!

否が応でもテンション上がる瞬間ですよね!!

f:id:Mzawa2:20160717142246j:plain

これがキットの中身全部.中国輸入だと壊れてるとか足りないとかザラらしいので,注意深く見ていきたいと思います.(そのにへ続く?)

OpenCVの内部パラメータでOpenGLの透視投影行列を作成(そのに)

以前にもOpenCVの内部パラメータでOpenGLの透視投影行列(フラスタム行列)を作成するコードを掲載しましたが,あれが数学的に何をやっているのかちょこっと解説します.元はパワポですが,SlideShareにアップロードするまでもない量と内容なので画像でスライドを掲載します.

OpenCVOpenGLの座標系

f:id:Mzawa2:20160612200757p:plain

OpenCVのデフォルトの座標系は左図,OpenGLの座標系は右図の通りです.OpenGLの方では暗黙的に,主点(光軸中心)が原点でZ軸と光軸が完全に一致しています.ですので,OpenCVOpenGLを渡り歩くときには,この図に従って適切に座標変換する必要があります.OpenCVのカメラモデルでは画素の剪断変形はありませんが,画素自体のアスペクト比が考慮されているため,fx=fyとは限りません.

透視投影行列

f:id:Mzawa2:20160612201114p:plain

さて,透視投影行列とは,空間の3次元位置を正規化デバイス座標系([-1,1]の3次元空間)にうまく圧縮する行列です.透視投影では視体積が錐体の頭を切り取った形をしているので,このような複雑な行列で変換する必要があります.なお,Z軸は出力写像が入力の反比例の関係になるように定められています.

OpenCV内部パラメータ表示

f:id:Mzawa2:20160612201704p:plain

導出と考え方は画像の通りです.znのあたりは透視投影行列の導出方法と合わせて読むと理解できると思います.

以上より,OpenCVのパラメータからOpenGLの透視投影行列を導くことができました.