Ryosuke Nakagawaのブログ

まなびをシェア

HUB75規格を深掘りしてみる

前回記事の紹介

前回の記事でLEDマトリックスディスプレイのデモを紹介しました. ryosukeeeee.hatenablog.com

このLEDディスプレイに使われているHUB75という規格について解説します.

HUB75規格

HUB75には16本のピンがあります. 説明のためにピンを3つのグループにわけます.

グループ名 ピン
Addr ピンA,ピンB,ピンC,ピンD
LED ピンR1,ピンG1,ピンB1,ピンR2,ピンG2,ピンB2
Utils ピンCLK,ピンLAT,ピンOE,ピンGND※

※GNDのみ3本ある

Addrはセクションの指定に用います.セクションについては後述します.

LEDは該当するLEDの色を指定するために用います.

Utilはその他の命令を送るために用います.もちろん,ピンGNDは接地です.

説明用の図

私が購入したこの商品を例にとりあげて説明します. 縦横のLEDの数は商品によって異なります. ディスプレイを使うときはデータシートを確認しましょう.

この商品は横64列,縦32行のLEDが並んでいます. 説明のためにすべてのLEDに下の図のように番号を振ります. 例えば,上から5行目,左から3列目のLEDは5-3と表記することにします. f:id:ryosukeeeee:20190429221535p:plain

Fig 1. 64列32行のLEDマトリックスディスプレイ

Addrグループの役割

すべてのLEDを同時に点灯することはできません. なのでいくつかのセクションに分割し,人間の眼では認識できないほどの短い時間で点滅させるセクションを切り替えることでディスプレイ全体に映像を映し出します. すなわち,同時に点灯しているのは1つのセクションです.

この商品は2行分のLEDを1つのセクションを構成します. 全部で32行あるので16つのセクションに分けられます. 具体的には,

1,17行でセクション1,

2.18行でセクション2,

3.19行でセクション3,

・・・

16,32行でセクション16 といった具合です.

Addrグループのピンは点灯させるセクションを選択するために用いらます.

ABCDの4ビットでセクションを指定します.

LEDグループの役割

フルカラーのディスプレイなので,RGBのLEDのON・OFFを指定する必要があります.

R1,G1,B1で上半分側のLEDの色を指定します.

R2,G2,B2で下半分側のLEDの色を指定します.

加法混合になるので,RGB全部点灯すると白になります.

ja.wikipedia.org

Utilグループの役割

ピンCLK,ピンLAT,ピンOEの役割は実際のコードを見てもらったほうがわかりやすいのでCのコードをみてください

gpio.h

/* -*- coding: utf-8 -*- */
#ifndef GPIO
#define GPIO

// ピン番号に名前をつける
#define pinA 22
#define pinB 23
#define pinC 24
#define pinD 25
#define pinR1 11
#define pinR2 8
#define pinG1 27
#define pinG2 9
#define pinB1 7
#define pinB2 10
#define pinClock 17
#define pinOE 18
#define pinStrobe 4

// アドレスから特定のビットをとりだすビットマスク
#define addressA 0b0001
#define addressB 0b0010
#define addressC 0b0100
#define addressD 0b1000

void setup(void);

void allPinLow(void);

#endif

gpio.c

/* -*- coding: utf-8 -*- */
#include "gpio.h"
#include <stdio.h>
#include <wiringPi.h>

// GPIOの初期設定.すべてのピンを出力で使う.
void setup(void) {
  pinMode(pinA, OUTPUT);
  pinMode(pinB, OUTPUT);
  pinMode(pinC, OUTPUT);
  pinMode(pinD, OUTPUT);
  pinMode(pinR1, OUTPUT);
  pinMode(pinR2, OUTPUT);
  pinMode(pinG1, OUTPUT);
  pinMode(pinG2, OUTPUT);
  pinMode(pinB1, OUTPUT);
  pinMode(pinB2, OUTPUT);
  pinMode(pinClock, OUTPUT);
  pinMode(pinOE, OUTPUT);
  pinMode(pinStrobe, OUTPUT);

  printf("setup done\n");
}

// すべてのピンをLowにする
void allPinLow(void){
  digitalWrite(pinA, 0);
  digitalWrite(pinB, 0);
  digitalWrite(pinC, 0);
  digitalWrite(pinD, 0);
  digitalWrite(pinR1, 0);
  digitalWrite(pinR2, 0);
  digitalWrite(pinG1, 0);
  digitalWrite(pinG2, 0);
  digitalWrite(pinB1, 0);
  digitalWrite(pinB2, 0);
  digitalWrite(pinClock, 0);
  digitalWrite(pinOE, 0);
  digitalWrite(pinStrobe, 0);

  printf("all pin low\n");
}

main.c

/* -*- coding: utf-8 -*- */
#include <wiringPi.h>
#include <stdio.h>
#include "gpio.h"

 // ディスプレイの横幅
#define DISPLAY_WIDTH 64
 // for文を繰り返す回数.点灯時間に相当する.大きい値にすれば点灯する時間が長くなる
#define LOOPSIZE 1000000

int main(void) {
  int i, j, address;

  if(wiringPiSetupGpio() == -1) return 1;
  setup(); //GPIOの初期設定をする
  allPinLow(); //一度すべてのピンをLowにする

  for (j = 0; j < LOOPSIZE; j++) {
    address = j % 16; // jを16で割ったときの余り.0から15までの値をとる.

 // ディスプレイの横幅と同じ回数繰り返す
    for (i = 0; i < DISPLAY_WIDTH; i++) {
      // R1, G1, B1, R2, G2, B2に値を設定する.
      // iは列に相当する.

      //  試しに上半分は赤にする
      digitalWrite(pinR1, 1);
      // digitalWrite(pinG1, 1);
      // digitalWrite(pinB1, 1);

      // 下半分は緑にする
      // digitalWrite(pinR2, 1);
      digitalWrite(pinG2, 1);
      // digitalWrite(pinB2, 1);

      // pinClockをHighにすると,そのときのグループLEDの値がキャプチャされる
      digitalWrite(pinClock, 1);
      // 次に備えてLowにする
      digitalWrite(pinClock, 0);
    }
    
    // 前のセクションから移るためにLEDをオフにする
    digitalWrite(pinOE, 1);
    
    // どのセクションにデータを書き込むか設定する.
    digitalWrite(pinA, address & addressA);
    digitalWrite(pinB, address & addressB);
    digitalWrite(pinC, address & addressC);
    digitalWrite(pinD, address & addressD);

    // データがそろったのでLEDを光らせる
    digitalWrite(pinStrobe, 1);
    
    // 次に備えてLowにする
    digitalWrite(pinStrobe, 0);

    //  次に備えてLowにする
    digitalWrite(pinOE, 0);
  }

  allPinLow();
  return 0;
}

wiringPiを使ってるので-lwiringPiをつけてコンパイルします.

gcc -o main main.c gpio.c -lwiringPi

以下のコマンドで実行すると,ディスプレイが点灯します.

sudo ./main

ドット欠けしてますが,こんな風になるはず. f:id:ryosukeeeee:20190511152042j:plain

特定の列のLEDだけを点灯してみます. main.cを一部変更します.

main.c(一部抜粋)

 // ディスプレイの横幅と同じ回数繰り返す
    for (i = 0; i < DISPLAY_WIDTH; i++) {
      // R1, G1, B1, R2, G2, B2に値を設定する.
      // iは列に相当する.
      if (i == 1 || i == 5 || i == 35) {
        digitalWrite(pinR1, 1);
        digitalWrite(pinG2, 1);
      } else {
        digitalWrite(pinR1, 0);
        digitalWrite(pinG2, 0);
      }

      // Clock High
      digitalWrite(pinClock, 1);
      // Clock Low
      digitalWrite(pinClock, 0);
    }

2列目,6列目,36列目のLEDが点灯してます. f:id:ryosukeeeee:20190511152036j:plain

特定の行のLEDだけを点灯してみます.

main.c(一部抜粋)

 // ディスプレイの横幅と同じ回数繰り返す
    for (i = 0; i < DISPLAY_WIDTH; i++) {
      if (address == 3 || address == 15) {
        digitalWrite(pinR1, 1);
        digitalWrite(pinG2, 1);
      } else {
        digitalWrite(pinR1, 0);
        digitalWrite(pinG2, 0);
      }
      // Clock High
      digitalWrite(pinClock, 1);
      // Clock Low
      digitalWrite(pinClock, 0);
    }

4行目,16行目,20行目,32行目のLEDが点灯してます. f:id:ryosukeeeee:20190511152028j:plain

ソースコードGithubにあげました. github.com

参考資料

LEDドットマトリクスパネル HUB75規格について調べてみた - Qiita

https://cdn-learn.adafruit.com/downloads/pdf/32x16-32x32-rgb-led-matrix.pdf

https://learn.adafruit.com/32x16-32x32-rgb-led-matrix/how-the-matrix-works

RaspberryPiで電光掲示板を制御したい

ことのはじまり

友人からとあるアイデアをいただきまして,電光掲示板を制御することになりました. いろいろ勉強したので記事にまとめます.

買ったもの

Amazonでこちらの商品を買いました. [asin:B0749KSQ5J:detail] RGBのフルカラーです.縦64×横32個×3色なので6144個のLEDが一枚のパネルに収まってます.

  • 外箱

大きさの比較のために500mlのペットボトルを置いてます. f:id:ryosukeeeee:20190429160150j:plain

  • 中身

HUB75規格の端子に接続するケーブル1個と電源用のケーブルが1個,磁石つきのネジが4個が同梱されています. f:id:ryosukeeeee:20190429160232j:plain

  • ディスプレイ裏面 f:id:ryosukeeeee:20190429160342j:plain

  • ディスプレイ裏面接写

この16本のピンに信号を送ってLEDを制御します. f:id:ryosukeeeee:20190429161207j:plain

デモを動かす

raspberryPi 3 model Bを使って,このライブラリのデモを動かすことを目標にします. github.com

このプロジェクトのwiring.mdに接続方法が書いてあるのでそれに従います.

電源ケーブルの接続方法

まずはディスプレイに電源を繋ぎましょう. といってもコンセントにさすようなタイプではないので工夫が必要です. 付属の電源ケーブルはこうなってます.

f:id:ryosukeeeee:20190429183010j:plain:w300

家庭のコンセントから給電するならこれら2つを組み合わせると良さげです. https://www.marutsu.co.jp/contents/shop/marutsu/img/goods/020/1319370/1319370_2.jpg https://www.marutsu.co.jp/contents/shop/marutsu/img/goods/020/594178/594178_2.jpg

電源アダプタの選び方ですが,5Vのものを使ってください. amazonの商品紹介をみると消費電力は80Wと記載されていますが信用なりません. 秋葉原で買える類似品では12Wです. 私は5V2Aの電源で駆動しています.

フラットケーブルの接続方法

ディスプレイの裏面をみてもらうと,この端子が左右2箇所についてます. 裏面をみたときに左にある端子に付属のケーブルを接続します. 右の端子は複数枚のLEDディスプレイを制御するときに使うみたいです.

https://raw.githubusercontent.com/hzeller/rpi-rgb-led-matrix/master/img/hub75-other.jpg

ケーブルをこのように接続する.

f:id:ryosukeeeee:20190429161851j:plain:w300

ケーブルのもう片方はraspberry Piのピンに接続します. オス-メスのジャンパワイヤが16本必要です.

ELEGOO 50 PCS オスメスジャンパーワイヤ200mm (無料 170 タイポイント ブレッドボード)

ジャンパワイヤを16本差し込むとこうなります.

f:id:ryosukeeeee:20190429162714j:plain:w300

私が使ったraspberry Pi 3 model Bのピンは右のタイプです.

https://raw.githubusercontent.com/hzeller/rpi-rgb-led-matrix/master/img/raspberry-gpio.jpg

ケーブルの反対側は接続時に左右を間違えやすいので気をつけてください.

https://raw.githubusercontent.com/hzeller/rpi-rgb-led-matrix/master/img/idc-hub75-connector.jpg

この表のとおりに,raspberry Piのピンと接続してください.

接続先 ピン番号 ピン番号 接続先
1 2
3 4
5 6 GND
strobe 7 8
9 10
clock 11 12 OE
G1 13 14
A 15 16 B
17 18 C
B2 19 20
G2 21 22 D
R1 23 24 R2
25 26 B1
27 28
29 30
31 32
33 34
35 36
37 38
39 40

raspberry Pi側はこんな感じになります.

f:id:ryosukeeeee:20190429162707j:plain:w300

raspberry Piでの操作

OSはRaspbian Liteが推奨されています. 初期設定が終わったら

git clone https://github.com/hzeller/rpi-rgb-led-matrix.git
cd rpi-rgb-led-matrix
make -C examples-api-use

を実行してください.これでデモが動かせるようになりました. ではデモを実行してみましょう.

sudo examples-api-use/demo -D0

はい.表示が乱れてます.荒ぶってます. デフォルトだと32×32のディスプレイになってしまうので,パラメータを渡して64×32に設定します.するとどうでしょう.

sudo examples-api-use/demo -D0 --led-rows=32 --led-cols=64

これもだめです.いろいろとパラメータを試行錯誤してうまくいきました.これです.

sudo examples-api-use/demo -D0 --led-rows=32 --led-cols=64 --led-pwm-lsb-nanoseconds=300

やっとデモが動きました. ほかにもデモが用意されているので試してみてください.

rpi-rgb-led-matrix/examples-api-use at master · hzeller/rpi-rgb-led-matrix · GitHub

RaspberryPiに赤外線LEDを組み合わせて自宅の照明をiPhoneから操作する 1.赤外線受信編

必要は発明の母

4月から新社会人になりまして2月に引っ越ししました.
新居のリビングの照明はリモコンor壁のスイッチで操作します.
リモコンスタンドが壁に備え付けられているので,普段はリモコンを挿しっぱなしにしてます.
ところが,これでは寝るときに不便です.布団に入るまえにリモコンホルダーからリモコンを持ち出しておかないと,
いざ寝ようとしたときにリモコンが遠くて布団をでなくてはいけまん.
この状況を打開するべく,iPhoneから照明を操作できるようにしました.

どんなものを作ったか

こんな感じ↓
f:id:ryosukeeeee:20190421222850p:plain

これから数回に分けて投稿していきます.

ロードマップ

  1. 照明のリモコンから送信される赤外線信号を解析する.
  2. 解析した赤外線信号を赤外線LEDから送信する.
  3. Node.jsとExpressで照明操作用APIを作る.
  4. iOSアプリでAPIを叩く

使ったデバイス

https://www.raspberrypi.org/app/uploads/2017/05/Raspberry-Pi-Model-B-1-462x322.jpg
もともと家で常時起動させてたラズパイがあったので,そいつを使いました.OSはRaspbianです.
なお,raspberry Piの初期設定の方法はここでは説明しません.

http://akizukidenshi.com/img/goods/C/I-00872.jpg
赤外線センサです.受信した赤外線の強度に応じて電圧が変化します.

http://akizukidenshi.com/img/goods/C/I-12612.jpg
次回以降の記事で使います.赤外線の送信ができます.赤外線は目に見えないのでデバッグ用に普通の(可視光領域の)LEDもあると便利です.

http://akizukidenshi.com/img/goods/C/I-09669.jpg
次回以降の記事で使います.GPIOから出力できる弱い電流で回路をオン・オフするために使います.

http://akizukidenshi.com/img/goods/C/K-05148.JPG
次回以降の記事で使います.raspberry Piの5Vピンの最大出力電流が足りなかったので,電源を追加するために使います.


リモコンの赤外線信号を受信する.

まずは赤外線を受信します.


赤外線送受信用の有名なライブラリにlircがあります.
lircについて知りたい方はこの記事がわかりやすくておすすめです.
nomunomu.hateblo.jp

lircは高機能なのですが設定が難しいです.無事にインストールして動かすことはできましたが,この記事のプログラムがシンプルでつかいやすかったのでこちらを採用しました.


赤外線リモコン受信モジュールのデータシートの4ページでピン配置を確認します.
Vcc, GND, Voutをraspberry PiのGPIOと接続します.raspberry PiのGPIOの配置は下の画像のとおりです.
https://www.bigmessowires.com/wp-content/uploads/2018/05/Raspberry-GPIO.jpg


私の環境では,raspberry Piのピンに直接つないでしまうとノイズが入ってうまく受信できませんでした.
こちらの記事を参考にコンデンサを追加したところノイズがなくなりました.
というわけで完成した回路がこちらです.
f:id:ryosukeeeee:20190421222853p:plain


このように回路を組んだらこの記事のプログラムをscan.cというファイル名で保存します.

//
//  scan.c
//  Copyright © 2018 Hiroki Kawakami. All rights reserved.
//

#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include<wiringPi.h>

struct timeval sharedTimeval;
double current() {
    gettimeofday(&sharedTimeval, NULL);
    return sharedTimeval.tv_sec * 1e6 + sharedTimeval.tv_usec;
}

int main(int argc, char **argv) {

    // wiring pi setup
    if (wiringPiSetupGpio() < 0) {
        fprintf(stderr, "Failed to setup wiring pi\n");
        return 1;
    }

    // obtain read pin number
    int pin = 20;
    if (argc > 1) {
        pin = atoi(argv[1]);
    }
    fprintf(stderr, "GPIO Pin Number : %d\n", pin);

    // obtain wait interval (us)
    double wait = 4e4;
    if (argc > 2) {
        wait = atoi(argv[2]);
    }

    // set gpio pin mode
    pinMode(pin, INPUT);

    double buffer[1024];
    double currentTime;
    int currentState, lastState;
    int offset = 0;

    lastState = digitalRead(pin);

    fprintf(stderr, "Scanning begin\n");

    while(1) {
        currentTime = current();
        if (offset && currentTime > buffer[offset - 1] + wait) {
            break;
        }

        currentState = digitalRead(pin);
        if (currentState != lastState) {
            buffer[offset++] = currentTime;
            lastState = currentState;
        }
    }

    fprintf(stderr, "Scanning end\n");

    fprintf(stderr, "Output begin\n");

    int i;
    for (i = 1; i < offset; i++) {
        printf("%.0lf%s", buffer[i] - buffer[i - 1], (i & 1) ? "\t" : "\n");
    }

    fprintf(stderr, "Output end\n");

    return 0;
}

ファイルを保存したらコンパイルします.忘れずにwiringPiをリンクしましょう.

$ gcc scan.c -o scan -lwiringPi


まだwiringPiをインストールしてない方はここを参考にしてください.
tool-lab.com



コンパイルできたら,試しに以下のコマンドで実行してみます.
実行したら,赤外線リモコン受信モジュールに向けて適当なリモコンから信号を送ってください.

$ ./scan 20 > lightOn


うまくいけば,同じディレクトリにlightOnというファイルが作成されているはずです.



今回はここまで.次回は赤外線送信編です.

SuperColliderのrecordNRTでOSCコマンドから音を生成する

SuperColliderとは

SuperColliderは音響合成やアルゴリズム作曲のための統合開発環境です.
オープンソースであり,Windows, macOS, Linuxで使えます.

SuperColliderでリアルタイム録音する

SuperColliderを使っていると生成された音を録音したくなります.
リアルタイム録音がしたいときは,以下のようにs.recordで録音できます.

// 以降はコメント

s.boot; // サーバーを起動する

// 適当なsynthを定義する
(
SynthDef("bubbles", { |out|
    var f, sound;
    f = LFSaw.kr(0.4, 0, 24, LFSaw.kr([8,7.23], 0, 3, 80)).midicps; // glissando function
    sound = CombN.ar(SinOsc.ar(f, 0, 0.04), 0.2, 0.2, 4); // echoing sine wave
    Out.ar(out, sound);
}).add;
)

x = Synth.new("bubbles");

s.prepareForRecord; // 録音の準備をさせる.このコマンドは無くても録音できる.

s.record; // 録音を開始する

s.pauseRecording; // 録音を一時停止する

s.record // 録音を再開する

s.stopRecording; // 録音を終了する

x.free; // 音を止める

この方法の欠点は,長さが10分の音を録音したかったら,実時間で10分かかる点です.

リアルタイムではない録音をする

私は研究でSuperColliderを使っているのですが,周波数を100Hz~2000Hzの範囲で100Hz刻みにして音を数種類作りたいみたいなことがしばしばあります.
上記の方法で録音すると,1個の音が5分としたら20個の音を録音するのに100分かかります.
そういうときはrecordNRTを使いましょう.NRTはNon-RealTimeを意味します.

公式のヘルプから引用します.

// 以降はコメント

// 適当なsynthを定義する
(
SynthDef("NRTsine", { |out, freq = 440|
	Out.ar(out,
		 SinOsc.ar(freq, 0, 0.2)
	)
}).writeDefFile; // 必ずwriteDefFileをすること.recordNRTするときはaddはNG.
)
(
var f, o;
g = [
	[0.1, [\s_new, \NRTsine, 1000, 0, 0, \freq, 440]], // 開始から0.1秒後にNRTsineというsynthをfreq=440で生成する.
	[0.2, [\s_new, \NRTsine, 1001, 0, 0, \freq, 660]], // 開始から0.2秒後にNRTsineというsynthをfreq=660で生成する.
	[0.3, [\s_new, \NRTsine, 1002, 0, 0, \freq, 220]], // 開始から0.3秒後にNRTsineというsynthをfreq=220で生成する.
	[1, [\c_set, 0, 0]] // 開始から1秒後に録音をやめる.
	];
o = ServerOptions.new.numOutputBusChannels = 1; // モノラルで録音する.
Score.recordNRT(g, "help-oscFile.osc", "helpNRT.aiff", options: o); // helpNRT.aiffというファイル名で録音される
)

これで時間の大幅な短縮ができる.複数ファイルを作りたいときはコードの後半を下のように書き換えてやればOKです.

(
for(1,20,{
	arg i; // Iは繰り返しの回数
	var f, o;
	g = [
		[0.1, [\s_new, \NRTsine, 1000, 0, 0, \freq, 440]], // 開始から0.1秒後にNRTsineというsynthをfreq=440で生成する.
		[0.2, [\s_new, \NRTsine, 1001, 0, 0, \freq, 660]], // 開始から0.2秒後にNRTsineというsynthをfreq=660で生成する.
		[0.3, [\s_new, \NRTsine, 1002, 0, 0, \freq, 220]], // 開始から0.3秒後にNRTsineというsynthをfreq=220で生成する.
		[1, [\c_set, 0, 0]] // 開始から1秒後に録音をやめる.
		];
	o = ServerOptions.new.numOutputBusChannels = 1; // モノラルで録音する.
	Score.recordNRT(g, "help-oscFile.osc", "helpNRT-"++i.asString++".aiff", options: o); // helpNRT-1.aiff, helpNRT-2.aiff ... というファイル名で録音される
})
)

Raspberry Pi 3を無線LANのブリッジにする

無線LANルーターが壊れる

無線LANルーターが壊れました.メモリの書き込み回数の上限がきたみたいです.約6年働いてくれました.ありがとう.
修論の締切が近づくと物が壊れるってほんとですね.これがマーフィーの法則ってやつなのか.

Macの有線用アダプタは持ってるから問題ないけど,iPadがネットに繋げなくなってしまいました.
先月は予想外の出費があり,今すぐ新しい無線LANルーターを買うのは厳しい.
手持ちの機器で解決できないだろうかと知恵を絞りました.

Macをアクセスポイントにする

まず考えたのはMacをアクセスポイントにする方法.
MacをWiFiアクセスポイントとして使う方法
Mojaveでも使えました.とはいえMacを常時起動していないと無線LANが使えないのは不便.

Raspberry Pi 3を無線LANのブリッジにする

家にあまってるRaspberry Pi 3があった.こいつをブリッジにして無線LAN環境を構築できるんじゃないか?
Raspberry Piは写真にあるようなマイコンです.画像は公式サイトからお借りしました.
f:id:ryosukeeeee:20181202231156j:plain
Raspberry Piは3から無線LANをサポートしています..

このページに沿って実行したらすんなりいきました.
singyestarday.hatenablog.com

2ヶ月後の引っ越しまでRaspberry Pi無線LANルーターで耐えしのげるのか?!

ベジェ曲線の長さを求める

ベジェ曲線の長さを求める

背景

趣味でアプリを作ってるときにベジェ曲線の長さが必要になりました.ベジェ曲線用のクラスは用意されているものの,長さは自分で計算する必要がありました.色々調べたので記事にまとめることにします.この記事はこのページを参考に執筆しました.

ベジェ曲線とは

CGの分野で曲線はベジェ曲線を用いて表されることが多いです.

イラレのペンツールで書ける曲線はベジェ曲線です.

下のページではベジェ曲線がわかりやすく紹介されています.解説用のアニメーションが非常にわかりやすいです.

postd.cc

 

ベジェ曲線の数学的表現

3次ベジェ曲線上の点を媒介変数 t, 0 \leq t \leq 1を用いて P(t)と表すことにします.
このときの始点,制御点1,制御点2,終点を P_0, P_1, P_2, P_3とすれば, P(t) = (1-t)^3P_0 + 3t(1-t)^2 P_1 + 3t^2(1-t) P_2 + t^3 P_3と書けます.

各点の x座標, y座標を P(t).x P(t).yと書くことにします.構造体のイメージです.

このあとの計算のために P(t)のかっこを展開して tの次数で整理しておきます.係数を A, B,C,Dとすれば,

\begin{align}P(t) &= (-P_0+3P_1-3P_2+P_3)t^3 + (3P_0-6P_1+3P_2)t^2 + (-3P_0 + 3P_1)t + (P_0) \\ &= At^3 + Bt^2 + Ct + D \end{align}

となります.

ここで注意してほしいのは,この関数は xyについてそれぞれ成り立っています.すなわち,

 P(t).x = (A.x)t^3 + (B.x)t^2 + (C.x)t + D.x

 P(t).y = (A.y)t^3 + (B.y)t^2 + (C.y)t + D.y

ということです.

曲線の長さの求め方

一般に,曲線が媒介変数 t x = f(t), y = g(t)とと表現されているとき,区間 \alpha \leq t \leq \betaの曲線の長さは

 \displaystyle{ L = \int_{\alpha}^{\beta} \sqrt{\left( \frac{dx}{dt} \right)^2 + \left(\frac{dy}{dt} \right)^2} dt }

で求められます.

先のベジェ曲線にあてはめれば,

 \displaystyle{ \frac{dx}{dt} = \frac{d}{dt}P(t).x }

 \displaystyle{ \frac{dy}{dt} = \frac{d}{dt}P(t).y }

です.

まずは \displaystyle{ \frac{dx}{dt}} \displaystyle{ \frac{dy}{dt} }を求めるために P(t)微分してみます.

 \displaystyle{ \frac{d}{dt}P(t) = 3At^2 + 2Bt + C }

これを2乗すると,

 \displaystyle{ \left( \frac{d}{dt}P(t) \right)^2 = 9At^4 + 12ABt^3 + (6AC + 4B^2)t^2 + 4BCt + C}

 求めたいベジェ曲線の長さを Lとすると,

\begin{align} L &= \int_{0}^{1} \sqrt{\left( \frac{dx}{dt} \right)^2 + \left(\frac{dy}{dt} \right)^2} dt. \\ &= \int_{0}^{1} \sqrt{\left( \frac{d}{dt}P(t).x \right)^2 + \left(\frac{d}{dt}P(t).y \right)^2} dt  \\ &= \int_{0}^{1}  \sqrt{(9(A.x)t^4 +...) + (9(A.y)t^4 + ...)}  dt \end{align}

原始関数を求めるのは難しそうです.

であるならば近似解を求める作戦でいきましょう.

この分野には疎いのですが,調べてみたかんじでは数値積分とか数値解析がキーワードのようです.

今回はオイラー法とルンゲクッタ法,シンプソン法を試しました.

ルンゲクッタ法のなかでも4次ルンゲクッタ法と3次ルンゲクッタ法を試しました.

後でわかったことだが,オイラー法とルンゲクッタ法は常微分方程式の近似解を求める手法でした.

Python + matplotlibでベジェ曲線を描く 

普段から使い慣れてるPythonでまずはベジェ曲線を描いてみる.

matplotlibの機能だけではベジェ曲線が描けなかったのでモジュールを追加した.

pypi.org

 

 pipでインストールできます.

 

始点,制御点1,制御点2,終点が 

\begin{align*}P_0 &= (0.0,0.0)\\P_1& = (0.5, 3.0)\\ P_2 &= (10.0, -2.0)\\P_3& = (12.0, 4.0)\end{align*}

であるときの3次ベジェ曲線がこちらです.

f:id:ryosukeeeee:20181201190234j:plain

手法を比較する

手法を比較・評価するときの基準となる"正解"を決めましょう.

自分なりに考えた結果,ベジェ曲線を細かく区切り,その極小区間を直線で近似して三平方の定理で長さを求め,その和を"正解"としました.

シンプソン法(Simpson's rule),オイラー法(Euler method),3次ルンゲクッタ法(3rd-order Runge-Kutta method),4次ルンゲクッタ法(4th-order Runge-Kutta method)を,分割数を10から100まで5ずつ増やして比較してみました.

f:id:ryosukeeeee:20181201190232j:plain

3次のルンゲクッタ法と4次のルンゲクッタ法はかぶってます.

被積分関数が高々2次であることが関係してそう)

 

結論

3次のルンゲクッタ法が一番収束も早くて精度がありそうです.

 

 

PythonのコードをGithubで公開しています.

ベジェ曲線クラスが定義されているので,4点を与えれば長さを計算してくれます.

github.com

 

最初の記事だからか気合が入って長くなってしまいました.

お付き合いいただきありがとうございました.

 

はじめまして

はじめまして.Ryosukeです.
いまは情報系の大学院で研究をしてます.来年度からは就職してエンジニアとして仕事します.
これまでとこれからの自分の学びを世界に発信していきたいと考え,ブログを始めました.
わかりやすい文章の書き方の練習も兼ねています.

使用言語ですが,PythonとSwiftをメインで使っています.
研究のなかでSound Programmingする機会があり,SuperColliderCSoundを書いた経験もあります.
CSoundは日本語の解説資料がほとんど無くて苦労したので,CSoundチュートリアル記事とか書きたいです.
学部生の頃にはMatlabも触ってました.

このブログが誰かの役に立つことを祈ります.
よろしくおねがいします.