電子工作」カテゴリーアーカイブ

M-JPEG streamerとOpenCV(CV2)でタイムラプス動画を作成

10秒間隔で画像を400枚 jpgファイルとして保存

事前にM-JPEG streamerがインストールされ、アドレスxxx.xxx.xxx.xxx:8080で稼働している環境で、ファイル名 T0000.jpgからT0399.jpgとして画像を保存します。

import time
import cv2

# VideoCapture オブジェクトを取得します
URL = "http://xxx.xxx.xxx.xxx:8080/?action=stream"
capture = cv2.VideoCapture(URL)
for n in range(400):
    ret, frame = capture.read()
    name = "T" + '{:04d}'.format(n)+".jpg"
    print(name)
    cv2.imwrite(name, frame)
    time.sleep(10)

print("Done!")

jpgファイルからmp4動画を生成

import glob
import cv2

img_array = []
for filename in sorted(glob.glob("*.jpg")):
    print(filename)
    img = cv2.imread(filename)
    height, width, layers = img.shape
    size = (width, height)
    img_array.append(img)

name = 'project.mp4'
out = cv2.VideoWriter(name, cv2.VideoWriter_fourcc(*'mp4v'), 5.0, size)

for i in range(len(img_array)):
    out.write(img_array[i])
out.release()

生成したmp4動画の編集(不要部分の削除、回転、再生速度の調整など)には、Windows10標準のソフト「フォト」が便利。

作例

Raspberry Pi3BでRepetier-Serverを利用する

Raspberry Pi3Bの環境

 $ uname -a
Linux ps2 5.10.52-v7+ #1440 SMP Tue Jul 27 09:54:13 BST 2021 armv7l GNU/Linux

$ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 10 (buster)"
NAME="Raspbian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"

ダウンロードサイトからLinux 32bit ARM(armfh)をダウンロードしてインストール

$ sudo dpkg -i  Repetier-Server-1.1.2-Linux.deb

インストールしたら、ブラウザで http://xxx.xxx.xxx.xxx:3344 をアクセスすると、プリンターの設定画面が表示されるので、マニュアルに従ってプリンターの追加・設定を行います

Webcam のインストール

ここを参照して、Webcamを設定

Repetier-ServerでWebcamの機能を利用するには、有料のプロバージョンが必要なため、 次のような画面となって、サーバーでは表示できません。

ただし、インストールしたWebcamのストリーミング機能は動作しているようで、ブラウザーで http://xxx.xxx.xxx.xxx:8080 をアクセスすると次のようにストリーミング映像を見ることができました。(カメラは窓の外向けに設置)

Start, Stop and Restart Repetier-Server

# Start server
sudo service RepetierServer start
sudo /etc/init.d/RepetierServer start
# Stop server
sudo service RepetierServer stop
sudo /etc/init.d/RepetierServer stop
# Restart server
sudo service RepetierServer stop
sudo /etc/init.d/RepetierServer restart

streamlitを試す

streamlitはpythonコードだけで、Webブラウザーからアクセスできるアプリを公開できる優れものです。磁気センサーのオフセット可視化で試してみました。jupyterのコードへ多少手を加えるだけでOK。デバッグはjupyterで動作確認を行い、完成したら必要に応じてstreamlit化するのが良さそう。streamlitはコマンドラインから次のように実行し、表示されたurlをブラウザーでアクセスします。

$ treamlit run mag-offset.py

  You can now view your Streamlit app in your browser.

  Network URL: http://192.168.68.122:8501
  External URL: http://203.165.226.42:8501

ブラウザーでアクセスした様子

mag-offset.py

import matplotlib

import os
import pandas as pd
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import math
import numpy as np

import streamlit as st
st.title('Magnetic compass offset plot')
### データの読み込み
df = pd.read_csv('BMX055/data4.csv')
#print(df)
a_x=np.average(df['Mag_x'])
a_y=np.average(df['Mag_y'])
a_z=np.average(df['Mag_z'])
#print(round(a_x,2),round(a_y,2),round(a_z,2))
# ここからグラフ描画

# グラフの入れ物を用意する。
fig = plt.figure()
#ax = Axes3D(fig)     <--- warningとなるので、次の行に書き換え
ax = fig.add_subplot(111, projection='3d')
# 軸のラベルを設定する。
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
st.table(pd.DataFrame({
    'Center X': [a_x],
    'Center Y': [a_y],
    'Center Z': [a_z]
}))
# グラフを表示する。
ax.scatter3D(df['Mag_x'],df['Mag_y'],df['Mag_z'],color="blue")
ax.scatter3D(a_x,a_y,a_z,color="red")
#plt.show()       <--- plt.show()を st.write(fig)へ置き換える。
st.write(fig)

ESP32/DCモータ/Websocket UIで倒立振子

PID制御のパラメータと自立のための目標角度(TARGET)をWebsocket UIでスマホから設定します。今のところ、パラメータの値が適切でないようで、硬い床面上では短時間で倒れます。やわらかい素材の上では倒れないので、この状態で適切なパラメータを試行錯誤で探索中、、、WebベースのUIのためか、細かなパラメータ設定の操作性がいまいちなので、改善が必要かもしれない。


倒立振子のコードは、https://qiita.com/Google_Homer/items/3897e7ffef9d247e2f56 を参考にしています。

Websocketのコードは、https://github.com/mgo-tec/ESP32_SPIFFS_EasyWebSocket を参考にしています。

#include <WiFi.h>
#include <WiFiMulti.h>
#include "ESP32_SPIFFS_EasyWebSocket.h" //beta ver 1.60

#include <MadgwickAHRS.h>
Madgwick MadgwickFilter;
#include "I2Cdev.h"
#define MPU6050_PWR_MGMT_1   0x6B
#define MPU_ADDRESS  0x68

#include "Wire.h"
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET     4 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

//PIDパラメータ調整
//PID係数
//#define TARGET            0.4
//#define KP                100.0
//#define KI                2.0
//#define KD                2.0

//Motor
#define LED_PIN            27                      // 内蔵LED
#define MOTOR_PIN_F        A4                      // to DC Motor Driver FIN
#define MOTOR_PIN_R        A18                      // to DC Motor Driver RIN
#define MOTOR_PWM_F        0                       // PWM CHANNEL
#define MOTOR_PWM_R        1                       // PWM CHANNEL
#define MOTOR_POWER_MIN    50
int MOTOR_POWER_MAX = 200;

#define DPS                1000                    // Gscale [deg/s]

const char* ssid = "*******"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "******"; //ご自分のルーターのパスワードに書き換えてください

const char* HTM_head_file1 = "/EWS/LIPhead1.txt"; //HTMLヘッダファイル1
const char* HTM_head_file2 = "/EWS/LIPhead2.txt"; //HTMLヘッダファイル2
const char* HTML_body_file = "/EWS/dummy.txt"; //HTML body要素ファイル(ここではダミーファイルとしておく)
const char* dummy_file = "/EWS/dummy.txt"; //HTMLファイル連結のためのダミーファイル

ESP32_SPIFFS_EasyWebSocket ews;
WiFiMulti wifiMulti;

IPAddress LIP; //ローカルIPアドレス自動取得用

String ret_str; //ブラウザから送られてくる文字列格納用
String txt = "text send?"; //ブラウザから受信した文字列を ESP32から再送信する文字列

int PingSendTime = 10000; //ESP32からブラウザへPing送信する間隔(ms)

long ESP32_send_LastTime;
int ESP32_send_Rate = 300;
byte cnt = 0;

//PID
float power = 0, I = 0, preP = 0, preTime;
float now = 0, Duty = 0, pitch, roll, yaw;
float KP = 100.0, KI = 2.0, KD = 2.0, TARGET = 0.0;
boolean f_disp = false;

void setup() {
  Wire.begin();
  Serial.begin(38400);
  Wire.beginTransmission(MPU_ADDRESS);
  Wire.write(MPU6050_PWR_MGMT_1);  //MPU6050_PWR_MGMT_1レジスタの設定
  Wire.write(0x00);
  Wire.endTransmission();

  Serial.print(F("Connecting to "));
  Serial.println(ssid);

  wifiMulti.addAP(ssid, password);

  Serial.println(F("Connecting Wifi..."));
  if (wifiMulti.run() == WL_CONNECTED) {
    Serial.println("");
    Serial.println(F("WiFi connected"));
    Serial.println(F("IP address: "));
    LIP = WiFi.localIP(); //ESP32のローカルIPアドレスを自動取得
    Serial.println(WiFi.localIP());
  }
  ews.EWS_server_begin();
  Serial.println(); Serial.println("Initializing SPIFFS ...");

  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS failed, or not present");
    return;
  }

  Serial.println("SPIFFS initialized. OK!");
  MadgwickFilter.begin(100); //100Hz
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;); // Don't proceed, loop forever
  }
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);
  pinMode(MOTOR_PIN_F, OUTPUT);
  pinMode(MOTOR_PIN_R, OUTPUT);

  ledcSetup(MOTOR_PWM_F, 312500, 8); //CHANNEL, FREQ, BIT
  ledcSetup(MOTOR_PWM_R, 312500, 8);
  ledcAttachPin(MOTOR_PIN_F, MOTOR_PWM_F);
  ledcAttachPin(MOTOR_PIN_R, MOTOR_PWM_R);
  // 初期化

  delay(10);

  // Clear the buffer
  display.clearDisplay();
  // テキストサイズを設定
  display.setTextSize(1);
  // テキスト色を設定
  display.setTextColor(WHITE);
  display.setCursor(20, 5);
  display.println("Start...");
  display.setCursor(20, 25);
  display.println(WiFi.localIP());
  display.display();

  delay(10); // Pause for 2 seconds
  preTime = micros();
  //  TaskHandle_t th; //ESP32 マルチタスク ハンドル定義
  //  xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 5, &th, 0); //マルチタスク core 0 実行

  ESP32_send_LastTime = millis();
}

void loop() {
  websocket_handshake();

  if (ret_str != "_close") {
    if (millis() - ESP32_send_LastTime > ESP32_send_Rate) {
      if (cnt > 3) {
        cnt = 0;
      }
      websocket_send(cnt, txt);
      cnt++;
      ESP32_send_LastTime = millis();
    }
    ret_str = ews.EWS_ESP32CharReceive(PingSendTime);
    if (ret_str != "\0") {
      Serial.println(ret_str);
      if (ret_str != "Ping") {
        if (ret_str[0] != 't') {
          int ws_data = (ret_str[0] - 0x30) * 100 + (ret_str[1] - 0x30) * 10 + (ret_str[2] - 0x30);
          switch (ret_str[4]) {
            case '!':
              ESP32_send_Rate = ws_data;
              break;
            case 'B':
              TARGET = float(5.0 - ws_data / 20.0);
              break;
            case 'G':
              KP = float(ws_data / 1.8) ;
              break;
            case 'R':
              KI = float(ws_data / 50.0);
              break;
            case '_':
              KD = float(ws_data / 50.0);
              break;
            case 'A':
              f_disp = ! f_disp;
              break;
            case 'O':
              KP = 100, KI = 2.0, KD = 2.0, TARGET = 0.0;
              break;
          }
        } else if (ret_str[0] == 't') {
          txt = ret_str.substring(ret_str.indexOf('|') + 1, ret_str.length() - 1);
          Serial.println(txt);
        }
      }
    }
  } else if (ret_str == "_close") {
    ESP32_send_LastTime = millis();
    ret_str = "";
  }
  PID();
}

//************* 倒立振り子 ****************************************
void PID() {
  float  P, D, dt, Time;
  static float  powerI = 0;

  Wire.beginTransmission(0x68);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(0x68, 14, true);
  while (Wire.available() < 14);
  int16_t axRaw, ayRaw, azRaw, gxRaw, gyRaw, gzRaw, Temperature;

  axRaw = Wire.read() << 8 | Wire.read();
  ayRaw = Wire.read() << 8 | Wire.read();
  azRaw = Wire.read() << 8 | Wire.read();
  Temperature = Wire.read() << 8 | Wire.read();
  gxRaw = Wire.read() << 8 | Wire.read();
  gyRaw = Wire.read() << 8 | Wire.read();
  gzRaw = Wire.read() << 8 | Wire.read();

  // 加速度値を分解能で割って加速度(G)に変換する
  float acc_x = axRaw / 16384.0;  //FS_SEL_0 16,384 LSB / g
  float acc_y = ayRaw / 16384.0;
  float acc_z = azRaw / 16384.0;

  // 角速度値を分解能で割って角速度(degrees per sec)に変換する
  float gyro_x = gxRaw / 131.0;  // (度/s)
  float gyro_y = gyRaw / 131.0;
  float gyro_z = gzRaw / 131.0;

  /*
    //c.f. Madgwickフィルターを使わずに、PRY(pitch, roll, yaw)を計算
    double roll  = atan2(acc_y, acc_z) * RAD_TO_DEG;
    double pitch = atan(-acc_x / sqrt(acc_y * acc_y + acc_z * acc_z)) * RAD_TO_DEG;
  */

  //Madgwickフィルターを用いて、PRY(pitch, roll, yaw)を計算
  MadgwickFilter.updateIMU(gyro_x, gyro_y, gyro_z, acc_x, acc_y, acc_z);

  //PRYの計算結果を取得する
  roll  = MadgwickFilter.getRoll();
  pitch = MadgwickFilter.getPitch();
  yaw   = MadgwickFilter.getYaw();
  now     = TARGET - roll                 ; // 目標角度から現在の角度を引いて偏差を求める
  if (f_disp) {
    display.clearDisplay();
    display.setCursor(20, 5);
    display.println( roll);
    display.setCursor(20, 25);
    display.println( now);
    display.display();
  }
  if (-20 < now && now < 20) {
    Time    = micros()                    ;
    dt      = (Time - preTime) / 1000000  ; // 処理時間を求める
    preTime = Time                        ; // 処理時間を記録
    P       = now / 90                    ; // -90~90→-1.0~1.0
    I      += P * dt                      ; // 偏差を積分する
    D       = (P - preP) / dt             ; // 偏差を微分する
    preP    = P                           ; // 偏差を記録する
    power  += KP * P + KI * I + KD * D    ; // 出力を計算する
    if (power < -1) power = -1            ; // →-1.0~1.0
    if (1 < power)  power =  1            ;
    //Motor駆動
    Duty = (int)((MOTOR_POWER_MAX - MOTOR_POWER_MIN) * abs(power) + MOTOR_POWER_MIN);
    ledcWrite( MOTOR_PWM_F, (power < 0 ?    0 : Duty) );
    ledcWrite( MOTOR_PWM_R, (power < 0 ? Duty :    0) );
    digitalWrite(LED_PIN, HIGH);
    if (f_disp) {
      display.clearDisplay();
      display.setCursor(20, 5);
      display.println( roll);
      display.setCursor(20, 25);
      display.println( Duty);
      display.setCursor(20, 45);
      display.println( power);
      display.display();
    }
  } else {                                                  // 転倒したら停止
    ledcWrite(MOTOR_PWM_F, 0);
    ledcWrite(MOTOR_PWM_R, 0);
    power = 0;
    I = 0;
    digitalWrite(LED_PIN, LOW);
  }

}

//**************************************************************
void LED_PWM(byte Led_gr, byte channel, int data_i) {
  Serial.println(data_i);
}
//*********************************************
void websocket_send(uint8_t count, String str_txt) {
  String str, tmp;
  //※WebSocketへのテキスト送信は110 byte 程度なので、全角35文字程度に抑えること
  tmp = "AGL=" + String(roll) + ",TGT=" + String(TARGET) + ",P=" + String(KP) +  ",I=" + String(KI) + ",D=" + String(KD);
  switch (cnt) {
    case 0:
      //str = str_txt;
      str = tmp;
      break;
    case 1:
      str = tmp;
      break;
    case 2:
      str = tmp;
      break;
    case 3:
      str = tmp;
      break;
  }

  ews.EWS_ESP32_Str_SEND(str, "wroomTXT"); //ブラウザに文字列を送信
}
//************************* Websocket handshake **************************************
void websocket_handshake() {
  if (ews.Get_Http_Req_Status()) { //ブラウザからGETリクエストがあったかどうかの判定
    String html_str1 = "", html_str2 = "", html_str3 = "", html_str4 = "", html_str5 = "", html_str6 = "", html_str7 = "";

    //※String変数一つにEWS_Canvas_Slider_T関数は2つまでしか入らない
    html_str1 += "<body style='background:#000; color:#fff;'>\r\n";
    html_str1 += "<font size=3>\r\n";
    html_str1 += "ESP-WROOM-32(ESP32)\r\n";
    html_str1 += "<br>\r\n";
    html_str1 += "ESP32_SPIFFS_EasyWebSocket Beta1.60 Sample\r\n";
    html_str1 += "</font><br>\r\n";
    html_str1 += ews.EWS_BrowserSendRate();
    html_str1 += "<br>\r\n";
    html_str1 += ews.EWS_ESP32_SendRate("!esp32t-Rate");
    html_str1 += "<br>\r\n";
    html_str1 += ews.EWS_BrowserReceiveTextTag2("wroomTXT", "from ESP32 DATA", "#555", 20, "#00FF00");
    html_str1 += "<br>\r\n";
    html_str1 += ews.EWS_Status_Text2("WebSocket Status", "#555", 20, "#FF00FF");
    html_str1 += "<br><br>\r\n";

    html_str2 += ews.EWS_TextBox_Send("txt1", "Hello Easy WebSocket Beta1.60", "送信");
    html_str2 += "<br><br>\r\n";
    html_str2 += "SETTING \r\n";
    html_str2 += ews.EWS_On_Momentary_Button("ALL", "ALL-ON", 80, 25, 15, "#000000", "#AAAAAA");
    html_str2 += ews.EWS_On_Momentary_Button("OUT", "DEFALT", 80, 25, 15, "#FFFFFF", "#555555");
    html_str2 += "<br>\r\n";

    html_str3 += "<br>TGT_ :\r\n";
    html_str3 += ews.EWS_Canvas_Slider_T("BLUE", 240, 40, "#777777", "#0000ff"); //CanvasスライダーはString文字列に2つまでしか入らない
    html_str3 += "<br>PID P:\r\n";
    html_str3 += ews.EWS_Canvas_Slider_T("GREEN", 250, 40, "#777777", "#00ff00"); //CanvasスライダーはString文字列に2つまでしか入らない

    html_str4 += "<br>PID I:\r\n";
    html_str4 += ews.EWS_Canvas_Slider_T("RED", 250, 40, "#777777", "#ff0000"); //CanvasスライダーはString文字列に2つまでしか入らない
    html_str4 += "<br>PID D:\r\n";
    html_str4 += ews.EWS_Canvas_Slider_T("_RGB", 250, 40, "#777777", "#ffff00");

    html_str7 += "<br><br>\r\n";
    html_str7 += ews.EWS_WebSocket_Reconnection_Button2("WS-Reconnect", "grey", 200, 40, "black" , 17);
    html_str7 += "<br><br>\r\n";
    html_str7 += ews.EWS_Close_Button2("WS CLOSE", "#bbb", 150, 40, "red", 17);
    html_str7 += ews.EWS_Window_ReLoad_Button2("ReLoad", "#bbb", 150, 40, "blue", 17);
    html_str7 += "</body></html>";

    //WebSocket ハンドシェイク関数
    ews.EWS_HandShake_main(3, HTM_head_file1, HTM_head_file2, HTML_body_file, dummy_file, LIP, html_str1, html_str2, html_str3, html_str4, html_str5, html_str6, html_str7);
  }
}

サーボモータを利用したPan/Tiltカメラ台の制御(pigpioの利用)

こんな記事を発見:RPi.GPIOよりもpigpioの方が精度が良いらしい。

「RPi.GPIO と pigpio のパルス幅の精度を測定」

テストのコード

#!/usr/bin/python
# -*- coding: utf-8 -*-
import time
import pigpio
PAN=27
GP = pigpio.pi()
GP.set_mode(PAN, pigpio.OUTPUT)
GP.set_servo_pulsewidth(PAN, 500)   # gpio18   500us

time.sleep(60)
GP.stop()

実測結果

標準偏差が348nSと、OSのオーバーヘッドがないArduinoと比較すると2桁程大きいが、サーボモータの制御には十分な精度が得られそう、、、

サーボモータを利用したPan/Tiltカメラ台の制御(GPIOライブラリーでは正確な制御は無理?)

サーボモータでPan/Tiltカメラ台を制御する実験をやってみたが、意図した方向から少し外れては正しい方向へカメラ台が回転する現象が頻発している。ステッピングモータの時には、このような現象に遭遇した記憶がない。サーボモータは、制御信号のパルス幅に対応した角度まで回転するように作られているので、意図しない回転の原因は、次のように推定できる。

  • 制御信号のパルス幅がバラついている
  • サーボモータの精度が悪い(制御信号のパルスに正確に追従していない)
  • その他?(上記の両方、またはその他の原因)

安価なサーボモータを使っているので、最初はサーボモータの精度の問題と思ったりしたが、制御信号のパルス幅をオシロで観測してみることにした。オシロには測定した値の統計情報を表示する機能があり、パルス幅の評価にはこれを利用することにした。

サーボモータの制御は、次のPythonスクリプトでRaspberry pi4bのGPIOからPWM信号を発生させて行っている。500HzでDuty50%の矩形波を生成。

#GPIOの初期設定
import RPi.GPIO as GPIO
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
PAN=27
GPIO.setup(PAN, GPIO.OUT)
PWM_pan = GPIO.PWM(PAN, 500)
PWM_pan.start(0.0)

PWM_pan.ChangeDutyCycle(50)
while True:
    pass

オシロで観測した波形と統計情報

赤枠部分に表示された情報を信用すると、パルス幅の平均値が1.08mS、最大値が2.34mS、標準偏差が88.96μなどどなっている。ここで、利用したサーボモータは0.5mSから2.5mSの制御信号のパルス幅に対応して、270度の範囲で回転することになっている。回転角度を1度変更するにために必要なパルス幅の変化は、約4.7μS(=2mS/270)。ところが、観測したパルス幅の標準偏差が88.96μということなので、サーボモータが制御信号に忠実に反応した場合、回転角度が数度以上変動することになる。実際に、この制御信号をサーボモータへ与えると、モータは静止せずに微動を繰り返す状態となった。

制御パルス生成にArduino Microを用いた場合

PIN6からPWM信号を生成

int PAN = 6;

void setup() {
  pinMode(PAN, OUTPUT);
  analogWrite(PAN,128);
}

void loop() {
}

オシロで観測した様子

なんと標準偏差が1桁のnSオーダー。Raspberry Pi4の1/1000以下となりました。

サーボモーターを用いたPan/Tiltカメラマウント

以前、ステッピングモータでPan/Tilt制御可能なカメラマウントを作ってみましたが、今回はサーボモータを利用してみました。サーボモータは、手軽にドライブできる利点はありものの、角度の情報を取り出す仕組がないという利用目的次第では致命的な欠点もあることに気が付きました。特にTilt方向が問題で、気を付けないとマウントに搭載しているカメラが座台と干渉し、カメラなどを破損する可能性があります。

ステッピングモータでPan/Tiltの制御し、画像の重心を追尾する実験の様子。

Pan/Tiltの台座

Pan/Tiltの台座を購入してみました。ネジが曲がっていたり数が合わないのは想定内?パーツの不具合を補うため、3Dプリンターでパーツを作る。

組み立ての途中の様子。組み立ててみたら回転の動きが少し怪しい。ベアリングの精度の問題か?この上に、Tilt制御用のサーボを搭載する予定。

ToF(Time of Flight)デバイス

ToF(Time of Flight)デバイスVL530Xを利用して、超近接レーダもどきを作ってみた。

主要なパーツ

  • VL53L0X     距離センサモジュール
  • Arduino nano  CPU
  • A4998     ステッピングモータドライバー
  • Stepping motor 
  • Slip ring

VL53L0X Time-of-Flight 距離センサモジュールの概要は、例えばこちらのページを参照。

arduino nanoでステッピングモータを回転させながら、ToFデバイスで光を反射する物体までの距離を測定します。モータの回転角度と距離をUSB I/FでPCへ転送し、PC側でPython(Jupyter notebook)で可視化しています。

Jupyter notebookで可視化した様子。

Jupyter notebookで可視化

# -*- coding: utf-8 -*-
import sys
import  glob

import serial
import asyncio, sys
import cv2
import numpy as np
import pygame
import math
from pygame.locals import *

def main():
    hist=350
    buf={}
    (x,y) = (1024,1024)   # 画面サイズ

    pygame.init()       # pygame初期化
    pygame.display.set_mode((x, y), 0, 32)  # 画面設定
    pygame.display.set_caption('ToF VL53L0X ranging...')
    screen = pygame.display.get_surface()
    fontFace =cv2.FONT_HERSHEY_SIMPLEX
    dev=glob.glob("/dev/ttyUSB*")[0]
    dev=dev.replace('[','')
    dev=dev.replace(']','')
    ser =serial.Serial(dev, 115200) 
    
    for i in range(5):
        line=ser.readline()
        
    font = pygame.font.Font(None, 55)
    #print(buf)
    while (1):
        for i in range(1,3):
            try:
                deg,distance=ser.readline().decode().split(',')
                deg=int(deg)
                distance=int(distance)
                buf[deg]=distance
                # ゴミデータ対策
                for k in range(4):
                    tmp=deg-k
                    if  tmp<0:
                        tmp=tmp+360
                    buf[deg-k]=distance
            except:
                continue


        # レーダー画面の背景描画
        pygame.draw.circle(screen, (0, 200, 0), (int(x/2), int(y/2)), int(x/2), 1)
        pygame.draw.circle(screen, (0, 200, 0), (int(x/2), int(y/2)), int(x/4), 1)
        pygame.draw.line(screen, (0, 200, 0), (0, int(y/2)), (x, int(y/2)))
        pygame.draw.line(screen, (0, 200, 0), (int(x/2), 0), (int(x/2), y))
        # レーダービームの軌跡描画
        dx = x/2 + x/2 * math.cos(math.radians(deg))
        dy = y/2 + x/2 * math.sin(math.radians(deg))
        pygame.draw.line(screen, (0, 255, 0), (int(x/2), int(y/2)), (int(dx), int(dy)))
        for k in range(hist):
            tmp = deg-k
            if tmp<0:
                tmp=tmp+360
            if tmp in buf:
                x0 = x/2 + buf[tmp]*0.8*math.cos(math.radians(tmp))
                y0=  y/2 + buf[tmp]*0.8*math.sin(math.radians(tmp))
                blip=int(255*(hist-k)/hist)
                pygame.draw.line(screen, (blip, 0, 0), (int(x/2), int(y/2)), (int(x0), int(y0)))
                pygame.draw.circle(screen, (blip, 0, 0), (int(x0), int(y0)), 4, 2)
                
        pygame.display.update()     # 画面更新
        pygame.time.wait(10)        # 更新時間間隔
        screen.fill((0, 20, 0, 0))  # 画面の背景色

        # イベント
        for event in pygame.event.get():
            if event.type == QUIT:      # 閉じるボタンが押されたら終了
                pygame.quit()           # Pygameの終了(画面閉じられる)
                ser.close()
                sys.exit()
                
if __name__ == "__main__":
        main()

Arduinoで(ステッピングモータと距離センサーの制御)

#include <vl53l0xTOFA.h>
#include <Wire.h>

#include <Stepper.h>
//#define PIN_xDIR   D3 //direction
//#define PIN_xSTEP  D4 //step
#define PIN_xDIR   PD3 //direction
#define PIN_xSTEP  PD2 //step
#define PIN_MODE   A1 // MODE
//#define LED       A2
#define ST        4
VL53L0xTOFA sensor;

// for your motor
int val = 0;
// initialize the stepper library on pins 8 through 11:
//Stepper myStepper(stepsPerRevolution, D4, D3);

float dist_TOF(int pos, int n) {
  float sum = 0;
  for ( int j = 0; j < n; j++) {
    sensor.readTOFA();
    sum += sensor.tofa.distancemm;
  }
  int distance = int(sum / n);
  int deg = int(pos *  36.0 / 40.0);
  //  Serial.print(pos);
  //  Serial.print(',');
  Serial.print(deg);
  Serial.print(',');
  Serial.println(distance);
}

void setup() {
  pinMode(PIN_xDIR, OUTPUT);
  pinMode(PIN_xSTEP, OUTPUT);
  //  pinMode(LED, OUTPUT);
  pinMode(PIN_MODE, INPUT_PULLUP);
  // set the speed at 60 rpm:
  //  myStepper.setSpeed(60);
  // initialize the serial port:
  Serial.begin(115200);
  delay(1000);
  Wire.begin();
  delay(1000);

  sensor.setTimeout(500);
  if (!sensor.init())
  {
    Serial.println("Failed to detect and initialize sensor!");
    while (1) {}
  }

  sensor.startContinuous(5);

  Serial.println("Radar Start");
}

void rot(int dir, int n) {
  for (int i = 0; i < n; i++) {
    for (int j = 0; j < 2 * ST; j++) {
      digitalWrite(PIN_xSTEP, !(digitalRead(PIN_xSTEP)));
      delayMicroseconds(800);
    }
    dist_TOF(i, 1);
    delayMicroseconds(800);
  }
}

void loop() {
  if (digitalRead(PIN_MODE) == LOW) {
    digitalWrite(PIN_xDIR, LOW);
    rot(1, 400);
  } else {
    digitalWrite(PIN_xDIR, HIGH);
    rot(0, 400);
  }
}

ArduinoでEFレンズを制御する実験(2)

注文していたマクロアダプターが追跡情報の更新がないまま届きました。13/21/31mmの3個がセットとなっています。左側がアダプター、右側がZWOのCCDカメラで、色が良く似ています。

 本来、レンズとカメラボディの間に取り付けるアダプターなので、CCDカメラと接続するには工夫が必要です。13mmと21mmのアダプターを使って、CCDカメラと接続してみたところ、望遠端付近ではピントが出ました。広角側ではピントが出ませんでした。

強度などの問題があるかもしれませんが、3DプリンターでマクロアダプターとCCDカメラを接続するアダプターを作ってみました。2つのパーツで構成し、M3ネジで接続しています。CCDカメラとは3Dプリンターで作成したM42,0.75Pのネジで接続しています。CADソフトはホビー用途であれば無料で利用できるFusion360を使っています。

CCDカメラ側のパーツを取り付けるまえに、マクロアダプターの接点にリード線を半田づけして引き出し、レンズの制御と電源を供給するコネクタと接続します。マクロアダプターとこのパーツを固定するために、6mmのネジで締め付けています。何回か試作を繰り返した結果、青のフィラメントが無くなったので、手持ちの白にチェンジ!

EFレンズに接続した様子と、フォーカスのハンド・コントローラ。
ソースコードの最新版は、こちら

組み立ててはみたものの、残念ながら動作が不安定です。今のところ、EF-S 18-55mmと同ISは、それなりに制御できますが、EF 35-80mmとEF70-200 1:28Lはまったく動きません。ロジックアナライザーでデコードしたSPIのデータ観測すると、どのレンズでも同じように見えるのですが、、、
 本来、SPIの信号はボード内の短い距離の伝送用なので、コントローラの線が長すぎるのかもしれません。今後、配線の短縮またはバッファーやレベル変換を追加して、改善するかどうか確認したいと思っています。
(追記)ケーブルを約70cmから約25cmに変更したら、手持ちのレンズ全てが一応動くようになりました。ただし、まだ不安定な面もあるので、さらに短くする必要がありそうです。SPIの信号を生成するマイクロプロセッサー(Arduino nano)を、レンズに近接して配置できるよう取り付け方法の変更が必要かもしれません。この場合、ピントの調整を操作するSW付きのロータリーエンコーダの部分だけ、ケーブルで接続した別の箱に格納した方が良さそうです。