読者です 読者をやめる 読者になる 読者になる

聞きかじりめも

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

OpenGLでカメラ画像を表示

OpenCV ソースコード 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プリンタ

もはやプリントできている状態なので組み立てたと言うタイトルはふさわしくない気もするのですが…それにしても自宅に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」を組み立てた(そのに)

3Dプリンタ

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

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プリンタ

前々からマイ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

以前にも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の透視投影行列を導くことができました.

FlyCapture2 SDKがVS2015で動かなくなった件

OpenCV ソースコード

生存報告も兼ねて.

もう半年以上放置していたのは,しゅーろんとかいうクソイベとDの新生活が思っていた以上に忙しかったためです.

PointGray社のFlyCapture2 SDKにバグがあったのでその報告と対策.

何が起こったのか

【以前】 Windows 8.1(64bit)環境で,VisualStudio2013で開発していたときには何も問題なくビルドができていた.

【今回】 同じ環境で,VisualStudio2015にアップグレードしたら以前のコードに大量のエラー. なんかFlyCapture2::Cameraとか重要なやつに赤い波線が…

クラス宣言の外側に無効な型指定子があります.

namespace"FlyCapture2"にメンバー"Camera"がありません.

ぶっちゃけありえな~い!

原因

FlyCapture2::Camera等の型指定子FLYCAPTURE2_APIの定義は FlyCapture2Platform.h内のマクロで決められているが,こいつがVS2015以降に仕事しなくなったのだ.

// 前略
#if defined(WIN32) || defined(WIN64)
// Windows 32-bit and 64-bit
// 中略
#elif defined(MAC_OSX)

// Mac OSX

#else
// Linux and all others
// 後略

元々,VisualStudioの定義済みマクロは_WIN32_WIN64など,_が前に付いているのが普通であり, 今までWIN32とかWIN64が使えていたほうが不思議だったらしい. FlyCapture2 SDKは,この謎のマクロに依存した書き方をしていたため,VS2015以降の仕様変更(?)で動かなくなったのだろう.

参考:

定義済みマクロ

visual studio - C++ MSVC14.0 での WIN32マクロについて - スタック・オーバーフロー

対策

FlyCapture2.hをインクルードする前にWIN32をdefineしてしまえばいい.たとえば次の様に.

#ifdef _WIN32
#define WIN32
#endif
#ifdef _WIN64
#define WIN64
#endif

#include <FlyCapture2.h>
#include <FlyCapture2GUI.h>

以上.

この件については公式サポートに問い合わせメールを出しておきました.

cv::waitKey()の処理時間を計測してみた

OpenCV ソースコード

ニコ動っぽいタイトルになっちゃったけど嫌いな人は許して(笑)

なんか遅くね?

PointGray社のFlea3は最大120fpsまで出る素晴らしい高速カメラですが,OpenGLを使わずOpenCVだけと連携させたところ,どうしても64fps以上出ないという問題にぶち当たりました.これではこのカメラを使う意味がないのでどうにか80fps以上は出せるようにしたい.そこで処理時間を計測してみたところ,どうやらcv::waitKey()で相当時間をかけているようなので,実際に計測してみました.

計測用コード

画像取得の部分に使ってるのは自作クラスですが,これについては以前の記事を参照のこと.

#include "FlyCap2CVWrapper.h"

using namespace FlyCapture2;

int main(void)
{
    FlyCap2CVWrapper cam;
    int count = 0;
    double time100 = 0;
    int waitparam = 1;
    // capture loop
    char key = 0;
    while (key != 'q')
    {

        // Get the image
        cv::Mat image = cam.readImage();
        flip(image, image, 1);
        cv::imshow("image", image);

        double f = 1000.0 / cv::getTickFrequency();
        int64 time = cv::getTickCount();

        key = cv::waitKey(waitparam);

        if (count == 1000)
        {// TickCountの変化を[ms]単位で表示 1000回平均
            std::cout << "param = " << waitparam << ", time = " << time100 / (count+1) << " [ms]" << std::endl;
            time100 = 0;
            count = 0;
            waitparam++;
        }
        else
        {
            count++;
            time100 += (cv::getTickCount() - time)*f;
        }
    }

    return 0;
}

1000回ループしたときの平均時間を表示したらcv::waitKey()の引数を1ずつ上昇させていくコードです. 風の噂によるとcv::getTickCount()は1msくらいの精度はあるみたいなので使っています.本当かどうか良く分かりませんが...

ちなみに計測環境は,Windows8.1 64bit,Intel Xeon X5675,12GB RAMです.

結果

f:id:Mzawa2:20151228175054j:plain

これはひどい

なんと,cv::waitKey()はせいぜい15ms程度の精度しかないことが分かりました.

ちなみにカメラ入力を描画するまでの処理は1.2msでした.

原因

これはcv::waitKey()の実装を見ればわかります.sources\modules\highgui\src\window_w32.cppの1900行目にあります.

CV_IMPL int
cvWaitKey( int delay )
{
    int time0 = GetTickCount();

    for(;;)
    {
        CvWindow* window;
        MSG message;
        int is_processed = 0;

        if( (delay > 0 && abs((int)(GetTickCount() - time0)) >= delay) || hg_windows == 0 )
            return -1;

        if( delay <= 0 )
            GetMessage(&message, 0, 0, 0);
        else if( PeekMessage(&message, 0, 0, 0, PM_REMOVE) == FALSE )
        {
            Sleep(1);
            continue;
        }

        for( window = hg_windows; window != 0 && is_processed == 0; window = window->next )
        {
            if( window->hwnd == message.hwnd || window->frame == message.hwnd )
            {
                is_processed = 1;
                switch(message.message)
                {
                case WM_DESTROY:
                case WM_CHAR:
                    DispatchMessage(&message);
                    return (int)message.wParam;

                case WM_SYSKEYDOWN:
                    if( message.wParam == VK_F10 )
                    {
                        is_processed = 1;
                        return (int)(message.wParam << 16);
                    }
                    break;

                case WM_KEYDOWN:
                    TranslateMessage(&message);
                    if( (message.wParam >= VK_F1 && message.wParam <= VK_F24)       ||
                        message.wParam == VK_HOME   || message.wParam == VK_END     ||
                        message.wParam == VK_UP     || message.wParam == VK_DOWN    ||
                        message.wParam == VK_LEFT   || message.wParam == VK_RIGHT   ||
                        message.wParam == VK_INSERT || message.wParam == VK_DELETE  ||
                        message.wParam == VK_PRIOR  || message.wParam == VK_NEXT )
                    {
                        DispatchMessage(&message);
                        is_processed = 1;
                        return (int)(message.wParam << 16);
                    }

                    // Intercept Ctrl+C for copy to clipboard
                    if ('C' == message.wParam && (::GetKeyState(VK_CONTROL)>>15))
                        ::SendMessage(message.hwnd, WM_COPY, 0, 0);

                    // Intercept Ctrl+S for "save as" dialog
                    if ('S' == message.wParam && (::GetKeyState(VK_CONTROL)>>15))
                        showSaveDialog(window);

                default:
                    DispatchMessage(&message);
                    is_processed = 1;
                    break;
                }
            }
        }

        if( !is_processed )
        {
            TranslateMessage(&message);
            DispatchMessage(&message);
        }
    }
}

あの精度が低いことで有名なGetTickCount()を内部で使っているんですね.メディア処理プログラムでは御法度レベルなんですがこれ...Intelさんなんで未だにリアルタイム画像処理ライブラリでこんなの使ってんの?

ただし,cv::waitKey()は内部的にOpenGLでいうglutSwapBuffers()に相当する処理も兼ねてるらしいので,HighGUIを使う限りこいつからは逃れられません.もっとFPSを上げたい場合は画像処理だけOpenCVにやらせてあとは別の描画ライブラリを使うようにしましょう.

とはいえこれはwindows環境での話なので,別のOSならまた結果は違うかもしれませんね.Linuxだともっと早いかも.