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を使うのはオーバーキルですね。

ThinkPad X1 Carbon Gen5 (2017モデル) で4K 60Hz出力

ThinkPad X1 Carbon Gen5 (2017モデル) にはHDMI端子があるが4K出力では30Hzになる。ゲームをするのでなければそんなに気にならない、かと思いきや、実際に使ってみるとマウス移動のカクカク感が結構気になる。

とここで「USB Type C→HDMI変換ケーブルを使えば60Hz出力が可能」という情報を見たのでやってみた。使ったのはAnker PowerExpand+ USB-C & HDMI 変換アダプタ 【4K (60Hz) 対応】というやつで、Amazonで2000円だった。

試したところ無事4K 60Hzで動作した。これで快適になった。

PC本体のHDMIに繋げた場合(30Hzまでしか選択できない) f:id:wagavulin:20220103001025p:plain

Type C→HDMI変換ケーブルを経由した場合(60Hzまで選択できる) f:id:wagavulin:20220103001132p:plain

Yolov5による物体検出 (2): 環境構築と訓練済みモデルを使った動作確認

Yolov5による物体検出 記事一覧

  • (1): 概要
  • (2): 環境構築と訓練済みモデルを使った動作確認 ← イマココ
  • (3): 少量のデータによる訓練と動作確認 ← 後で書く

環境を構築したらまずは訓練済みのデータで動作を確認します。いきなり自前のデータで訓練すると、うまく動かなかったときの原因究明が面倒だからです。それができたら自前のデータで訓練するわけですが、まずは少量のデータ(ある程度は検出可能になるくらい)で訓練してみます。今回はCOCOというパブリックなデータをダウンロードして、その一部を使います。ラベルデータもすでにあるわけですが、ラベルファイルや訓練データのディレクトリ構造など、COCOとYolov5ではいくつか異なるところがあるため、Yolov5が想定している構成になるよう変換するスクリプトを用意してやる必要があります。そのスクリプトに間違いがあると当然うまく動かないので、「スクリプト作成」→「訓練」→「結果確認」→「スクリプト修正」...というループを何度か回すことになります。最初から大量のデータを入れると1ループに時間がかかるので、まずは少量のデータでやる方が良いです。

動作環境

以下の2つで動作を確認しています。

  • Ubuntu-20.04 (WSL2)
    • CPUのみ
    • Python-3.9.4をpyenvで入れる。
    • 必要なパッケージはpipで入れる。virtualenvも使用。
  • Google Colaboratory (ハードウェアアクセラレータ: GPU)
    • Python-3.7.10 (現在のColaboratoryのデフォルト環境)
    • 必要なパッケージはpipで入れる

環境構築 - Ubuntu

Pythonはpyenvでインストールしたものを使います。バージョンは手元に入っていた3.9.4。Pythonのバージョンについてはそれほど強いバージョン制約はなさそう。Colaboratoryの方では3.7.10で動いているのでその辺りのバージョンなら多分動くでしょう。

パッケージはpipで入れますが、virtualenvも使うことにします。ディープラーニングでよく使われるtensorflow, pytorchはバージョンの制約がそれなりあるので環境を分けておいた方が無難です。

Pythonインストール

# Python本体インストール
$ sudo apt install liblzma-dev
$ pyenv install 3.9.4

注意点としてはliblzma-devパッケージをPython本体ビルド前にインストールしておきます。lzmaというモジュールがPython本体に同梱されていますが、liblzma-devパッケージが入っていないとそれが有効化されずエラーになってしまいます。手元にすでにビルド済みのPythonがあり、かつまだliblzma-devをインストールしていないなら、そのPythonは多分使えません。一度アンインストールして再ビルドしましょう。

Yolov5ダウンロード

ディレクトリ構成は以下のような感じにします。

$HOME/yolov5/
        +- dataset/ # 学習用のデータセットを置く(後で作る)
        +- yolov5/  # Yolov5スクリプト

virtualenvの設定とパッケージインストール

$ $HOME/yolov5
$ pyenv virtualenv 3.9.4 yolov5
$ echo "yolov5" > .python-version
$ cd yolov5
$ pip install -r requirements.txt

環境構築 - Colaboratory編

あとで書く。

訓練済みモデルによる検出

まずは訓練済みのモデルを使って動くかどうか確かめてみます。単にpython detect.pyを実行すれば自動的にモデルのダウンロードを行うのですが、そういうのにあまり頼るとブラックボックスになってしまうので、ここでは手動でダウンロードします。モデルはgithubのリリースページからダウンロードできるので、とりあえずyolov5s.ptをダウンロードします。置き場所はどこでもよいですが、yolov5の直下に置きました。

あとは検出を試す画像ですが、data/images以下にbus.jpgzidane.jpgが含まれているのでこれを使います。

モデルと画像が準備できたら、以下のコマンドで実行します。

$ python detect.py --weights yolov5s.pt --source data/images --device cpu

検出結果はruns/detect/exp以下にできるので、確認してみましょう。なお、以下の例外が出た場合はlzmaモジュールができていないので、上の説明を参考にしてPythonをビルドし直して再度試してください。

    from _lzma import *
ModuleNotFoundError: No module named '_lzma'

train.pyのオプション

train.pyのオプションのうちよく使いそうなものを以下に挙げます。

オプション 意味
--weights PATH モデル (ptファイル)
--source PATH 検出する画像を含むディレクト
--conf-thres THRESH 検出する閾値。デフォルトは0.25
--device DEVICE CUDAデバイス0, 0,1,2,3など。CPUの場合はcpu
--save-txt 検出結果のbboxなどをテキストファイルとして保存する
--save-conf 上記テキストファイルに検出結果のconfidenceも出力する
--project PROJECT 出力結果の保存先。PROJECT/NAMEに保存される
--name NAME 出力結果の保存先。PROJECT/NAMEに保存される

次回以降に続く。