Raspberry Pi Pico WとPicorubyでLチカ

ダウンロード

R2P2のGitHubのReleasesのリンクから最新版をダウンロード。R2P2-FLASHから始まるやつはWやWHが付かない通常のRaspberry Pi Pico用、R2P2_W-FLASHはWやWH用。それぞれgzとzipがあるがどちらでもよい。解凍するとR2P2_W-FLASH_MSC-0.2.1-20240209-5145384.uf2のような名前のファイルがある。

uf2書き込み

Raspberry Pi Pico WHのBOOTSELボタンを押しながらUSBケーブルを繋ぐ。INDEX.HTMなどを含むフォルダが出てきたら成功。

このフォルダに先ほどの.uf2ファイルをドロップする。コピーされて勝手にExplorerウィンドウが閉じてまた開く。以下のようにbin, homeなどが出てくればOK。

Tera Term設定

Tera Term をダウンロードして起動する。現在の最新版は5.2。大昔からある安定したソフトなのでバージョンは気にしなくて多分大丈夫。

「File」→「New Connection」を選び、「Serial」を選択。選択肢がいくつかあるので適当に試す。今回はCOM9だった。

正しくRaspberry Pi Picoと繋がれば以下のような画面になる。

Lチカ(本体内蔵LED)

$> のプロンプトでirbを入力してirbを起動する。Raspberry Pi Pico WやWHの場合は以下を入力すると本体のLEDが光る。

led = CYW43::GPIO.new(CYW43::GPIO::LED_PIN)
led.write 1

Lチカ(外部LED)

Pico Wのピン配置は以下のような感じ。

とりあえずGP5と隣のGNDにLEDを刺す。LEDを使うときは抵抗を入れる必要があるが、ここで使っているのは秋月電子で買った抵抗内蔵のLED。配線の手間が省けるしたいして高くないので(10個で200円)良い感じ。

irbで以下を入力すると光らせることができる。

led = GPIO.new(5, GPIO::OUT)
irb> led.write 1

Arduino Nano互換品を動かす

Arduino Nanoの正規品は秋月電子で見ると1個3560円。Amazonで3個で1999円という激安品があったので買ってみた。しかもピンはんだ付け済み。

https://www.amazon.co.jp/gp/product/B0CQXCCWZT/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&th=1

ただし互換品は正規品と同じ設定ではスケッチを書き込めないことがある。ここを参考にして試したところ、全く同じ手順ではないけどうまくいった。

ジェネリック『Arduino nano』を動かしてみた | ゲームで楽しく学ぶプログラミング教室

上の参考サイトでは以下のことをやっていた。

  • ドライバインストール
  • ボード: Arduino Nano
  • プロセッサ: ATmega168
  • シリアルポート: COM8

今回買ったボードはドライバは入れなくても認識した。ただ認識しないケーブルがあったので、ケーブルの相性はありそう。 USB-SERIAL CH340 (COM8) が認識したボード。

書き込みの設定は以下でできた。

  • ボード: Arduino Nano
  • プロセッサ: ATmega 328P (Old Bootloader)
  • シリアルポート: COM8 (デバイスマネージャーで出てきたものにする)

適当にLチカで試す。

const int led = 11;

void setup() {
  Serial.begin(9600);
  Serial.println("hello");
  pinMode(led, OUTPUT);
}

void loop() {
  digitalWrite(led, HIGH);
  delay(1000);
  digitalWrite(led, LOW);
  delay(1000);
}

Rubyで大きなwavファイルを読む (未完)

訳あってwaveファイルの処理をする必要があったのでRubyを使ってやってみた。やりたいのはwavファイルをNumo::NArrayの形式で読むこと。また、使用したsample.wavはかなり大きい(2時間分、約1.2GB)。

ruby wave file」あたりでググるとだいたい以下の2つのgemが出てくる。

両方を試してみたが、結論から言うとどちらもPythonに比べてかなり時間がかかるため結構つらい。以下詳細。

参考: Pythonの場合

本題のRubyの前に、PythonでwavファイルをNumPyのndarrayで読むには以下のようにする。動作も速く、1.0~1.5秒ほど。 最後にsum()で合計を求めているのは何らかの遅延評価的なもの(読み込んだように見えて実際には読んでおらず、必要になったときにはじめて読む)がないことを確認するため。

#!/usr/bin/env python

import wave
import numpy as np

with wave.open("sample.wav") as wf:
    nframes = wf.getnframes()
    frames = wf.readframes(nframes)
    data = np.frombuffer(frames, dtype=np.int16)
print(np.sum(data))

wavefileの場合

wavファイルを読むコードはだいたい以下のようになる。each_bufferの中身が空なのでファイルを読むだけで何もしていない。これだけでも1分7秒かかった。NArrayに変換するとさらに時間がかかるだろう。

#!/usr/bin/env ruby

require "wavefile"

WaveFile::Reader.new("sample.wav"){|wr|
  wr.each_buffer{|buf|
  }
}

wav-fileの場合

こちらのgemの場合は以下のようになる。readDataChunk()までなら約1秒ほどだが、バイナリデータを取得するためにunpack()まで行うと約30秒かかる。

#!/usr/bin/env ruby

require "wav-file"

f = open("sample.wav")
format = WavFile::readFormat(f)
dataChunk = WavFile::readDataChunk(f)
f.close
wavs = dataChunk.data.unpack("s*")

OpenCVとNumo::NArrayのバインディング

OpenCVは行列を表すクラスを独自に持っていますが(cv::Matクラス)、Pythonバインディングではこれをnumpyのndarrayに置き換えています。opencvrはcv::MatRubyにバインドさせたものを使っていましたが、試しにNumo::NArrayにバインドさせてみました(opencvrとは何か、とかOpenCVの行列ライブラリの話の詳細は以前に書いたこの記事を参照)。

具体的にはopencvr内で以下の2つを行います。

  • OpenCVの関数の戻り値がcv::Matのときは、その戻り値をNumo::NArrayに変換してRuby側に返す
  • OpenCVの関数の引数がcv::Matのときは、Ruby側から受け取った引数はNumo::NArrayであると想定し、それをcv::Matに変換してからC++の関数を呼び出す

試しに実装しただけなので不完全なところはあるでしょうが一応動きました。以下サンプル。

元画像

f:id:wagavulin:20220105051019j:plain

読み込みと行列の情報の表示

imread()の戻り値がNumo::UInt8になっています。

#!/usr/bin/env ruby

require 'numo/narray'
require_relative '../opencvr/cv2'

img = CV2::imread("sample1.jpg")
p img.class
p img.shape
p img.size
# 実行結果
$ ./test.rb
Numo::UInt8
[533, 800, 3]
1279200

減色処理

Numo::NArrayのAPIで画像を加工してみます。画素値を整数値で割ると小数点以下が切り捨てられ、また同じ値を掛けると値が離散的になり、結果として減色されます(もっと良いアルゴリズムがあるだろうけど)。

また、hstack()で行列を結合させることで画像の結合も可能です。

#!/usr/bin/env ruby

require 'numo/narray'
require_relative '../opencvr/cv2'

img = CV2::imread("sample1.jpg")
img2 = img / 32 * 32
img3 = Numo::NArray::hstack([img, img2])
CV2::imwrite("subtract.jpg", img3)

f:id:wagavulin:20220105051049j:plain

画像をグレースケールにしてヒストグラムを作成

画像読み込み時にIMREAD_GRAYSCALEを指定するとグレースケールとして読み込み、結果は2次元UInt8行列になります。0~255がそれぞれ何ピクセルあるかヒストグラムで表示します。Numo::GSLとNumo::Gnuplotを使ったヒストグラムの作成はこちらの記事を参考にしました。

#!/usr/bin/env ruby

require 'numo/narray'
require_relative '../opencvr/cv2'
require 'numo/gnuplot'
require 'numo/gsl'

def gen_hist(img, fname)
  h = Numo::GSL::Histogram.new(256)
  h.set_ranges_uniform(0, 255)
  img_flat = img.flatten

  img_flat.each do |x|
    h.increment x
  end
  freq = h.bin
  center_of_range = h.range.to_a.each_cons(2).map{|a,b| (a+b)/2.0}

  Numo.gnuplot do
    set terminal: :png
    set output: fname
    set style:[:fill, :solid]
    unset :key
    plot center_of_range, freq, w: :boxes
  end
end

img_gray = CV2::imread("sample1.jpg", CV2::IMREAD_GRAYSCALE)
gen_hist(img_gray, "hist-rb.png")

f:id:wagavulin:20220105051755p:plain

なお、Pythonならもっと短いコードで同じような結果が得られます。Rubyももっと短くしたいところですが。

#!/usr/bin/env python

import cv2
import matplotlib.pyplot as plt

img = cv2.imread("sample1.jpg", cv2.IMREAD_GRAYSCALE)
img2 = img.flatten()
plt.hist(img2, bins=256)
plt.savefig('hist-py.png')

f:id:wagavulin:20220105052427p:plain

ベタ塗り画像に文字を書く

今度はimread()で画像ファイルを読み込むのではなく、Numo::NArrayのAPIで行列データを作成します。カラー画像を作成するには3次元のUInt8型の行列を作ります。幅800, 高さ600ピクセルの画像なら(600, 800, 3)です。このうち青成分のみ255にすれば青ベタ塗りの画像ができるので、それにputText()で文字を書きこみます。なおOpenCVの画像データはRGBではなくBGRなので、青成分は0番です。

#!/usr/bin/env ruby

require 'numo/narray'
require_relative '../opencvr/cv2'

img = Numo::UInt8.zeros(600, 800, 3)
img[0..-1, 0..-1, 0] = 255
CV2::putText(img, "Hello OpenCV with Numo::NArray", [100, 300], CV2::FONT_HERSHEY_DUPLEX, 1, [255, 255, 255])
CV2::imwrite("out.png", img)

f:id:wagavulin:20220105193950p:plain

ちなみに

画像をNumo::NArrayで取得するだけならすでにmagroというライブラリがあるのでそっちを使った方が良いです。あと単なる減色処理や画像の結合についてももっと良い既存のライブラリがあるはずです。この程度の処理にOpenCVを使うのはオーバーキルですね。