オシロスコープで静止画を描画する

## １．はじめに
### 使用したもの
・オシロスコープ
・ノートパソコン（Pythonの開発環境）
・NIダック（型番:NI USB-6212）

### 開発環境
・Python 3.8.6
・ライブラリ
　nidaqmx（0.8.0）
　[nidaqmxのダウンロード](https://nidaqmx-python.readthedocs.io/en/latest/)
　matplotlib（3.7.3）
　numpy（1.24.4）
　cv2
 　time

### 使用したソフト
・VScode editor
・NI MAX（2023Q4）
　[NI MAXのダウンロード](https://www.ni.com/ja/support/downloads/drivers/download.ni-daq-mx.html#521556)

## ２．仕組み
オシロスコープで静止画を表示するために、今回はラスター方式というものを用いた。
これはテレビにも用いられる方式で、水平方向をX座標、垂直方向をY座標とし、ある画素の位置を (x, y) のように表現するものである。
今回は座標(x, y)に対応する電圧をNIダックを用いて出力し、それをオシロスコープに接続することで静止画を描画している。NIダックはPythonによって動作させるため、専用のライブラリが必要となる。

## ３．画像変換プログラム
まず、カラー画像を白黒に変換するプログラムを作成した。
```
img = cv2.imread('example.png', 0)
ret, bi_img = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY)

cv2.imshow("Binary", bi_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

b_img = np.array(bi_img)

x0 = []
y0 = []

for i in range (b_img.shape[0]):
    for j in range (b_img.shape[1]):
        if b_img[i, j] == 0:
            x0.append(j/100)
            y0.append(-i/100)
```
このコードの example.png に変換したいファイル名を入れる。
２行目の180という数字は閾値を表しており、これを大きくすることでより多くの領域が白色（255）に変換される。
最後にx0, y0というリストを作成し、ここに変換した画像のうち黒色（0）の部分の座標を格納していく。この時、描画する画像が上下反転せず正常に表示されるようにするため、y座標の符号を逆転させている。また座標を1/100倍することで大きさを調整している。（任意で変更可）
実行したときに変換画像が表示され、閾値の調整がうまくいっているか確認できるようにした。

参考にしたサイト

 https://www.tech-teacher.jp/blog/opencv-binarization/

## ４．画像描画プログラム
画像描画プログラム全体は以下のようになった。
```
import cv2
import time
from nidaqmx.constants import AcquisitionType
import numpy as np
import nidaqmx


'''画像変換プログラム'''
img = cv2.imread('example.png', 0)
ret, bi_img = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY)

cv2.imshow("Binary", bi_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

b_img = np.array(bi_img)

x0 = []
y0 = []

for i in range (b_img.shape[0]):
    for j in range (b_img.shape[1]):
        if b_img[i, j] == 0:
            x0.append(j/100)
            y0.append(-i/100)


'''initiation(初期設定)'''
global tTask1
tTask1=nidaqmx.Task() #アナログ出力用のタスク
tTask1.__init__("main_VC_controller")#タスク名の割り当て


'''サンプル波形のパラメータ'''''''''''''''''''''''''''''''''''''''''''''
samps_rate = 100000 #サンプリングレート
N = 1000 #バッファサイズ
n = 1 #三角関数の周期を変えるための整数
data_size = 1000 #1サンプルの配列データ要素数(データサイズ)
required_value = 100 #コールバック関数をトリガするための端末読み取りデータ数

#初期波形の定義
t= np.linspace( -2*np.pi, 2*np.pi, num=data_size, endpoint=True )
Vx = 2*np.cos(t)
Vy = 2*np.sin(t)


'''出力するサンプル波形*(numpyの1次元ndarray配列によって指定)'''
sample1 = np.append( Vx, np.zeros(1))
sample2 = np.append( Vy, np.zeros(1))
sample_2D = np.array([sample1, sample2])
sample_2D__ = np.array([ Vx, Vy ])

on_off=True
global samp_Writer
k = 0
j = 0


'''コールバック関数の定義'''''''''''''''''''''''''''''''''''''''''''''
def askUser():
    global on_off
    input("Press return to stop")
    on_off = False

def callback1(task_handle, status, callback_data=1):
    print('コールバックしました')
    return 0

def callback2(task_idx, event_type, num_samples, callback_data):
    global Vx
    global Vy
    global sample_2D__
    global k
    global j
    #波形の出力

    while True:
        print('コールバック関数に移動しました。')
        Vx = x0
        Vy = y0
        sample_2D__ = np.array([Vx, Vy], dtype=float)
        samp_Writer.write_many_sample(sample_2D__)
        #print("kの値:",k)
        j = j + 1
        

'''アナログ出力信号のタスクを構成(DAQ内の構成)'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
tTask1.ao_channels.add_ao_voltage_chan( physical_channel='Dev1/ao0:1',
                                        name_to_assign_to_channel="AO-code", 
                                        min_val=-10, max_val=10 )#タスク1
tTask1._out_stream.output_onbrd_buf_size = 4095
tTask1.output_buf_size = N #バッファサイズ
tTask1.timing.cfg_samp_clk_timing(rate=samps_rate,
                                  sample_mode=AcquisitionType.CONTINUOUS,
                                  samps_per_chan=N)#有限サンプルの出力タイミング


'''波形の生成(PC→DAQの構成)'''''''''''''''''''''
#波形出力の自動スタート、つまり、垂れ流し状態。生成されたそばから出力している
samp_Writer = nidaqmx.stream_writers.AnalogMultiChannelWriter(tTask1.out_stream, auto_start=False)

#第一引数に指定したサンプル数がPCバッファ→DAQデバイスに書き込まれたときに受信するコールバック関数を登録する
tTask1.register_every_n_samples_transferred_from_buffer_event(required_value, callback2)
#tTask1.register_every_n_samples_transferred_from_buffer_event(1000, callback2)

#これを行うとき、明示的なタスクの開始でなくてはならず、auto_startは使用不可である
samp_Writer.write_many_sample(sample_2D__)


'''実行フェーズ'''
print('開始準備中...')
time.sleep(3)

print('タスクを開始')
tTask1.start()#タスク1の開始

while k < 10:
    Vx = 0.5*k*np.cos(t)
    Vy = 0.5*k*np.sin(t)
    sample_2D__ = np.array([Vx, Vy])
    samp_Writer.write_many_sample(sample_2D__)
    print('連続出力')


'''停止フェーズ'''

#print(tTask1.is_task_done())#タスクが終わっているかどうかをTure or Falseで値を返す
print('タスクの停止まで待機しています...')
tTask1.wait_until_done(timeout=5)#タスクの停止までの待機時間(有限サンプルモードのときにして指定する)


#print(tTask1.is_task_done())
print('タスクの停止')
tTask1.stop()#タスクを停止

print('タスク1をクリアしました')
tTask1.close()#生成したタスクをクリア
```
重要なのは「コールバック関数の定義」の部分だ。ここで
```
Vx = x0
Vy = y0
sample_2D__ = np.array([Vx, Vy], dtype=float)
samp_Writer.write_many_sample(sample_2D__)
#print("kの値:",k)
```
このように指定することで、(x, y)座標を電圧として出力できるようになっている。
また、このコードでは最初に初期波形（今回は円）が表示されるようになっているが、この部分を消去しようとしてもうまくいかなかったため、残していただきたい。

## ５．実際に動作させる
・PCとNIダックをUSB接続し、NI MAXに認識されていることを確認する。
・NIダックとオシロスコープを接続する。
　※この時、今回のコードではAO0、AO1が(x, y)出力に対応していることに注意。
・オシロスコープのスケールを適切な値に調整する。例えば画像サイズ256×256の場合、±5V程度がちょうどよい。
・Pythonを実行し波形を観察する。

最初に円が表れ、次に自分の変換した画像が表示されれば成功。
色が薄く見えてしまう場合、輝度を上げると改善される可能性あり。