20210508のPythonに関する記事は30件です。

ADALM PLUTOでFM受信してみる

概要 ソフトウエア無線によるFM受信の技術メモです。RFアンプとAD変換部分は、Analog DevicesのPLUTOを使い、Windows環境のpythonにてFM受信を行ってみました。 想定するアプリケーションと実行環境 想定するアプリケーション:SDR、FM受信、音声処理 実行環境:python, windows10, ADALM PLUTO 他のデバイスでも受信処理とオーディオ出力の部分はそのまま使えるかと。 参考情報 実行環境の準備 PLUTOを使うために、Libiio,python,pyadi-iioをインストールします。 解説 予備知識(遅延検波) まず、ベースバンドIFのFM信号をs(n)=exp(j2πnfT)とする。 1サンプル遅延信号と複素共役信号を積算する s(n) x s(n-1)*= exp(j2πnfT)exp(-j2πnfT+j2πfT)=exp(j2πfT) この角度を求めると2πfTとなり、瞬時周波数f(t)の定数倍の信号が得られる。 予備知識(PyAudio) 参考情報を参照のこと サンプル sdr=SdrInit()にてPlutoの設定を行い、オブジェクトを得る。 sdr.rx()でサンプルを取り出し、demLoop()にて復調処理を行う。 ここで、demLoopで取り出した音声データのサンプル数をpyaudioのチャンクバッファサイズと一致しないため、バッファ(pack)を作成し、一時保存する。 バッファがチャンクサイズより大きくなったら、チャンクサイズごとに取り出し、pyaudio用のバイナリデータに変換してpyaudioのデバイスに(ブロックモードで)書き込む。 PlutoFM.py import adi #pluto import matplotlib.pyplot as plt #graph import numpy as np import scipy.signal # US/DS/Filter import math # pi.sic.cos import pyaudio # System parameters ################################################ #-- application ################################################ SND_FS = 44100 # 48000 SND_CHUNK = 1024*2 SND_CH_OUT = 1 sample_rate_audio = SND_FS ################################################ #-- baseband IQ parameter ################################################ #(i) sample_rate_audio decimate_demod = 5 sample_rate_demod = sample_rate_audio * decimate_demod ################################################ #-- radio interface param ################################################ # PLUTO # LO 325MHz to 3800MHz # BW < 20MHz # FS 521ksps to 61.4Msps # cat spec is 65.2Ksps to 61.4Msps ################################################ #(i) sample_rate_demod decimate_capture = 4 sdrSample_rate = sample_rate_demod * decimate_capture sdrBufSize = 1024*20 sdrFcenter = 82500000 # 中心周波数82.5MHzの場合 #sdrFcenter = 80000000 sdrBandWidth = 400000 ################################################ # Create radio object def SdrInit(): # (i) # (o) radio Object sdr = adi.Pluto() sdr.rx_rf_bandwidth = sdrBandWidth sdr.rx_lo = sdrFcenter sdr.sample_rate = sdrSample_rate sdr.rx_buffer_size=sdrBufSize #sdr.tx_lo = 2000000000 #sdr.tx_cyclic_buffer = True #sdr.tx_hardwaregain = -30 sdr.gain_control_mode = "slow_attack" # Read back properties from hardware #print(sdr.rx_hardwaregain) # Get complex data back sdr.rx_enabled_channels = [0] return sdr #========================================================= # FM 復調 #========================================================= demLastIQ=0 def demLoop(demLastIQ): smp = sdr.rx() decim = scipy.signal.decimate(smp,decimate_capture) bbIQ = np.insert(decim,0,demLastIQ) #bbIQ = decim demLastIQ = decim[-1] #------------------------ # 遅延検波 # -+-[Delay]-- [x]---[ angle ]--> # | ^ # +-[Conj ]----+ #------------------------ dem = np.angle(bbIQ[1:] * np.conj(bbIQ[:-1])) / math.pi # decimate to audio sample rate aud = scipy.signal.decimate(dem,decimate_demod) return aud,demLastIQ if __name__ == "__main__": sdr = SdrInit() #==================================================== #--- setup Audio #==================================================== pyAud = pyaudio.PyAudio() sndStrm = pyAud.open(rate=SND_FS,channels=SND_CH_OUT,format=pyaudio.paInt16,input=False,output=True) #==================================================== #--- setup FIFO #==================================================== pack = np.array([]) #==================================================== #--- FM受信ループ #==================================================== for cnt in range(2000): # while: でもよい #==================================================== #--- チャンクサイズ以上たまったらサウンドデバイスに出力 #==================================================== while pack.size > SND_CHUNK : aBuf = pack[0:SND_CHUNK]*8000 sndBuf = aBuf.astype(np.int16).tobytes() sndStrm.write(sndBuf) pack = pack[SND_CHUNK:] #plt.plot(aBuf,linestyle='None',marker="x") #plt.psd(aBuf,NFFT=1024,Fs=sample_rate_audio , Fc=0) #==================================================== #--- 新たなIF信号を受信し音声データに復調しFIFOにつめる #==================================================== aud,demLastIQ = demLoop(demLastIQ) pack = np.append(pack, aud) #==================================================== # teardown 処理 #==================================================== sndStrm.stop_stream() sndStrm.close() pyAud.terminate() 結言
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Power Automate Desktop「簡易 見守りカメラをつくってみた」

概要 Power AutomateとPower Automate Desktopを連携させ、モバイル端末からローカルPCのWEBカメラで撮影、Teamsに投稿します。 注意事項 Pythonを使います。 アテンド型RPAのユーザーごとのライセンス(試用可)が必要になります。 PA、PAD連携にはWindows 10 pro必須です。 自己責任でお願いいたします。 環境等 Windows 10 pro 20H2 モバイル版Power AutomateおよびTeams 2021年5月時点での最新版PADおよびオンプレミスデータゲートウェイを使用しています。 Python3.8.10 https://www.python.org/ のインストーラーを使用しています。 グローバル環境に以下のライブラリを個別にインストールしてあります。 OpenCV 4.4.0.46 pyperclip 1.8.1 numpy 1.18.5 Pythonについてはhttps://www.python.jp/index.html さまの環境構築ガイドを参考にしていただければと思います。 やること Power AutomateのモバイルアプリからPower Automate Desktopに起動指示 PCのPython環境でpyファイルを実行 Webカメラで撮影 画像を圧縮処理、Base64変換をおこないPower Automate Desktopに返却 Incoming Web hookを利用してTeamsにPOST 1の部分は最後に作ります。 Power Automate Desktopフロー PADのコード内にpythonスクリプトを埋め込んで生成するようになっています。今回のフローではPythonスクリプトにPAD側から変数を渡していないので、pyファイルを作成し任意の場所に置く場合は4、5、6のアクションのみで済みます。 1. 現在の日時を取得します。 Teamsのメッセージに入れたかったので使用しました。 2. Windows環境変数を取得 TEMPフォルダーへのパスを取得しています。 3. テキストをファイルに書き込みます PythonスクリプトをTEMPフォルダーに作成します。 複数行テキストは標準機能で今のところ使えませんので方法は過去記事を参照ください。 Python import cv2 import binascii import pyperclip # Webカメラから入力 cap = cv2.VideoCapture(1) # 画像を取得 ret, img = cap.read() # カメラ解放 cap.release() # リサイズ img2 = cv2.resize(img , (320,240)) # 画像を圧縮 ret, data = cv2.imencode(".jpg", img2,[cv2.IMWRITE_JPEG_QUALITY, 40]) # Base64に変換 base64string = binascii.b2a_base64(data).decode("ascii").strip() # クリップボードに出力 pyperclip.copy(base64string) OpenCVおよびpyperclipはグローバル環境にpipしてあります。 cap = cv2.VideoCapture(1)の1の部分がWebカメラのデバイス番号になるので環境に合わせて書き換えてください。 私のWebカメラのキャプチャ元画像は640X480でした。Teamsにbase64でPostするとき、データが15kbyteを超えたあたりでエラーになるため、320X240にリサイズ後cv2.IMWRITE_JPEG_QUALITYを40にして調整しています。 Base64に変換後余分な空白や改行を削除、Pyperclipを使ってWindows側のクリップボードに保存しています。 4.DOSコマンドの実行 変数は生成していません。TEMPフォルダーに保存したcapture.pyを実行します。 5.クリップボードテキストを取得 クリップボードに格納されているBase64データを%ClipboardText%に代入します。 6.Webサービスを呼び出します このアクションからIncoming Webhook経由でTeamsに画像を投稿します。 Teamasの設定 投稿用のプライベートチームを作成します。メンバー追加は必要ありません。 コネクタを追加します。 incoming webhookを検索して追加します。 再度コネクタを開いてIncoming webhookを構成します。 名前とアイコンを適宜設定して作成します。 作成されるとURLが生成されるのでコピーしておきます。後からでも構成済みから選択すると再コピー可能です。 「Webサービスを呼び出します」アクションの設定 URL部分にTeamsからコピーしたURLを貼り付けます。 メソッド POST コンテンツタイプ application/json 要求本文 カードをJSON形式で投稿しています。imageプロパティにdata URIを使って画像を投稿します。クリップボードから取得したBase64データを挿入しています。 JSON { "@type": "MessageCard", "title": "Image POST %CurrentDateTime%", "text": "キャプチャしました!", "sections": [ { "images": [ { "image": "data:image/jpeg;base64,%ClipboardText%" } ] } ] }     7.ファイルの削除 TEMPフォルダーに作ったpythonスクリプトを削除しています。 確認 テストしてみます。 コード DateTime.Local DateTimeFormat: DateTime.DateTimeFormat.DateAndTime CurrentDateTime=> CurrentDateTime System.GetEnvironmentVariable Name: $'''TEMP''' Value=> EnvironmentVariableValue File.WriteText File: $'''%EnvironmentVariableValue%\\capture.py''' TextToWrite: $'''import cv2 import binascii import pyperclip # Webカメラから入力 cap = cv2.VideoCapture(1) # 画像を取得 ret, img = cap.read() # カメラ解放 cap.release() # リサイズ img2 = cv2.resize(img , (320,240)) # 画像を圧縮 ret, data = cv2.imencode(\".jpg\", img2,[cv2.IMWRITE_JPEG_QUALITY, 40]) # Base64に変換 base64string = binascii.b2a_base64(data).decode(\"ascii\").strip() # クリップボードに出力 pyperclip.copy(base64string)''' AppendNewLine: False IfFileExists: File.IfFileExists.Overwrite Encoding: File.FileEncoding.UTF8NoBOM System.RunDOSCommand DOSCommandOrApplication: $'''%EnvironmentVariableValue%\\capture.py''' Clipboard.GetText Text=> ClipboardText Web.InvokeWebService Url: $'''Your URL''' Method: Web.Method.Post Accept: $'''application/json''' ContentType: $'''application/json''' RequestBody: $'''{ \"@type\": \"MessageCard\", \"title\": \"Image POST %CurrentDateTime%\", \"text\": \"キャプチャしました!\", \"sections\": [ { \"images\": [ { \"image\": \"data:image/jpeg;base64,%ClipboardText%\" } ] } ] }''' ConnectionTimeout: 30 FollowRedirection: True ClearCookies: False FailOnErrorStatus: False EncodeRequestBody: False UserAgent: $'''Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.21) Gecko/20100312 Firefox/3.6''' Encoding: Web.Encoding.AutoDetect AcceptUntrustedCertificates: False ResponseHeaders=> WebServiceResponseHeaders Response=> WebServiceResponse StatusCode=> StatusCode File.Delete Files: $'''%EnvironmentVariableValue%\\capture.py''' Power Automateフローの作成 インスタントクラウドフローを作成します。 「Power Automate Desktopで構築したフローを実行する」で先に作成したPADのフローを選択し、実行モードはライセンスにあわせます。 保存してテストします。 これでPower Automateモバイルアプリから操作可能です。 まとめ モバイル端末からルータ内のPCに難しい設定なくアクセスし画像撮影できました。 撮影した画像をIncoming Webhookを使うことでPADから投稿できました。 クリップボード経由のデータ連携は安定していました。 プライバシーの問題を気にしないなら3000~5000円で見守りカメラは買えるのでネタのひとつとしてお考えください。 「Webサービスを呼び出します」アクションは色々応用できますね! 参考 https://docs.microsoft.com/ja-jp/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using https://teratail.com/questions/288950 https://stackoverflow.com/questions/56853198/teams-invoke-webrequest-sending-base64-string-png-to-teams
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ELM327とRaspberryPiを使って自作で車のメーターを作ってみた。

今回はELM327を用いて、以下のようなメーターを作成した。 1.必要なもの ハードウェア ELM327(V1.5) OBDⅡを介してCAN通信を行うスキャナである。BlueToothでRaspberryPiやスマホなどに、にCAN通信で得られたデータを飛ばすことができる。 V2.1ではELM327自体が正常に動いた事例が少なかったので、v1.5を選択した。 OBDⅡの対応車種(今回はインプレッサスポーツを使用。以下サイトにOBDⅡ対応車種が掲載されているサイトを記載) RasperryPi3 ModelB+以上のRaspberryPi 5インチ以上のTFT液晶 ソフトウェア GIMP2 2.実装方法 Bluetoothにより、ELM327からCANデータを飛ばす。その後、pythonのobdモジュールでデータを整形。それをさらに整形し、メーターに表示させる。 3.実装時注意点 車種次第では一部のデータが取得できない場合があるので、その場合は潔く諦める。 4.実装方法 4.0 ELM327の動作確認 ELM327が使えないと困るので、Androidでインストールできる「Torque lite」をインストールし、回転数などのデータが取得できるか確認する。 ここで取得できない場合には、ELM327を別のものに取り替える。 4-1.pythonのバージョンアップ RaspberryPi 3では、デフォルトでインストールされているpythonが2.7と3.7の2種類があり、デフォルトでは、2.7になっている。 今回は、3.7を使うので、以下記事を参考に、pythonのバージョン変更をする。 4.2 obdモジュールのインストールとBluetooth接続環境のセット このモジュールのインストールは公式サイトでは、pip install obdでインストールすると案内されているが、この方法でインストールすると内部で使用されているloggingモジュールのエラーが発生し、使えない。 そのため、今回は、以下サイトのgithubにアクセスし、そのurlを指定してpipでインストールする。 インストールする際は、以下のコードを打つ pip install git+https://github.com/brendan-w/python-OBD 次に、以下コードを打ち、Bluetooth接続時に必要なライブラリをインストールする。 (コードはnomunomu0504様記事より引用) Bluetooth # bluezを動かすために必要なライブラリ群 $ sudo apt-get install -y libglib2.0-dev libdbus-1-dev libudev-dev libical-dev bluetooth bluez-utils blueman # bluez本体 $ sudo mkdir car_tmp && cd car_tmp $ wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.45.tar.xz $ xz -dv bluez-5.45.tar.xz && tar -xf bluez-5.45.tar $ cd bluez-5.45/ && ./configure --enable-experimental $ make $ sudo make install make時にエラーが出たが、エラーの詳細を失念してしまったので参考にしたサイトを記載する。 4.3 テストコードの実装 obdモジュール・RaspberryPiにおけるBluetoothの接続方法については、nomunomu0504様の記事がわかりやすかったため、こちらを参考にし、説明は割愛する。 以下コード test_obd.py import obd import os #Bluetoothの接続準備 os.system('sudo hcitool scan') os.system('sudo hciconfig hci0 up') os.system('sudo rfcomm bind 0 ELM327のMACアドレス') os.system('sudo rfcomm listen 0 1 &') #OBDⅡコネクションを定義 o = obd.OBD() #回転数を取得 while True: rpm=o.query(obd.commands.RPM) print(rpm) 以下コードが実装できたら、実車とELM327を接続、RaspberryPiを起動する。 起動し、先ほど作ったテストコードを実行する。 すると、余計な文字も含まれているが、ターミナル上に回転数が表示され続ける。 これでobdモジュールが使えることが確認された。 4.4メーター画像の準備 GIMPを用い、メーター画像を制作する。 今回は、ホンダのS2000のメーターパネルを参考に制作したが、画像編集技術がないため、回転数のメモリが曲げられなかった。 細かなgimpの使い方は、以下サイトに詳しく記載されているため、説明を割愛する。 メモリの表現方法として、tkinterで定義したbackgroundの画像を、あらかじ用意した1メモリごとにずらした画像を当てて、一定間隔で画像を更新しメモリがずれているかのように見せる手法を用いた。 完成したメーターの土台画像(メーター内の黄色の文字はtkinterにより当てている) 4.5実装 以下コードのように実装した。 test_obd.py # -*- coding: utf-8 -*- import tkinter as tk import obd import os from time import * import random import math root = tk.Tk() i=1 o = 0 cnt=0 x=0 acc=True speed_v=0 #fuel_amount=0.0 class Display(): def __init__(self): global o os.system('sudo hcitool scan') os.system('sudo hciconfig hci0 up') os.system('sudo rfcomm bind 0 00:1D:A5:08:AC:BE') os.system('sudo rfcomm listen 0 1 &') o = obd.OBD() def window(self): global i i+=1 root.geometry("1920x1080") canvas = tk.Canvas(bg = "black", width=1920, height=1080) canvas.place(x=0, y=0) img = tk.PhotoImage(file =r'/home/pi/Python/elm327_obd2/img/bg/bg3.png') canvas.create_image(960,540,image=img) rpm_file=str(r'/home/pi/Python/elm327_obd2/img/Scales/rpm_')+str(0)+str('.png') rpm_img = tk.PhotoImage(file = rpm_file) rpm_canvas = tk.Canvas(bg = "black", width=1755, height=224) rpm_canvas.place(x=80, y=58) rpm_canvas.create_image(878,113,image=rpm_img) self.clock_display() self.speed_display() self.rpm_display() self.intake_display() self.voltage_display() self.info_display() self.trip_display() self.fuel_display() self.water_t_display() root.mainloop() def rpm_display(self): def set_rpm(): global rpm_file,rpm_img,rpm_canvas,o,x,cnt,acc rpm_str = ('{:.5}'.format(str(o.query(obd.commands.RPM)))) #print(rpm_str) rpm_str2 = ('{:.3}'.format(str(o.query(obd.commands.RPM)))) #Idling_stop if '0.0' in rpm_str: rpm_v=0 rpm_float=0 else: rpm_float = float(rpm_str) #データ整形 if (math.floor(rpm_float)) < 999.999999: rpm_v = ('{:.1}'.format(str(rpm_float))) else: rpm_v = ('{:.2}'.format(str(rpm_float))) if rpm_v==0 and acc==True: if x==0 and acc==True: #ACC-On if x<=80 and cnt==0: x+=1 #up memory if x==80: cnt=1 #reverse x-=1 if x<80 and cnt==1: x-=1 #down memory if x==0: cnt =2 x=0 if x==0 and cnt==2: x=0 acc=False rpm_file=str(r'/home/pi/Python/elm327_obd2/img/Scales/rpm_')+str(x)+str('.png') else: #Ignition-On rpm_file=str(r'/home/pi/Python/elm327_obd2/img/Scales/rpm_')+str(rpm_v)+str('.png') #デフォルトrpm rpm_img = tk.PhotoImage(file = rpm_file) rpm_canvas = tk.Canvas(bg="black",width=1755, height=224) rpm_canvas.place(x=80, y=58) rpm_canvas.create_image(878,113,image=rpm_img) if rpm_v== 0 and acc==True: root.after(400, set_rpm) else: root.after(400, set_rpm) set_rpm() def speed_display(self): speed_status = tk.StringVar() speed_status.set("888") speed_label=tk.Label(root,textvariable=speed_status,font=("DSEG7 Classic",97),background = "#351d18",fg= "orange",anchor="e",width =3) speed_label.place(x=778,y=483) def set_speed(): global o,speed_v speed_str = ('{:.3}'.format(str(o.query(obd.commands.SPEED)))) speed_float = float(speed_str) if (math.floor(speed_float)) < 10: speed_v = ('{:.1}'.format(str(o.query(obd.commands.SPEED)))) if (math.floor(speed_float)) >=10: speed_v = ('{:.2}'.format(str(o.query(obd.commands.SPEED)))) if (math.floor(speed_float))>=100: speed_v = ('{:.3}'.format(str(o.query(obd.commands.SPEED)))) speed_status.set(speed_v) root.after(400, set_speed) set_speed() def clock_display(self): buff = tk.StringVar() buff.set('') clock = tk.Label(root,textvariable=buff,font=("DSEG7 Classic",27),background = "#351d18",fg= "orange") clock.place(x=878,y=730) def show_time(): buff.set(strftime('%H:%M')) root.after(1000, show_time) show_time() def intake_display(self): intake_status = tk.StringVar() intake_status.set("0.88") intake_label=tk.Label(root,textvariable=intake_status,font=("DSEG7 Classic",67),background = "#351d18",fg= "orange",anchor="e",width =3) intake_label.place(x=273,y=438) def set_intake(): global o intake_str = ('{:.3}'.format(str(o.query(obd.commands.INTAKE_PRESSURE)))) intake_float = float(intake_str) intake_status.set(intake_float) root.after(400, set_intake) set_intake() def voltage_display(self): voltage_status = tk.StringVar() voltage_status.set("888") voltage_label=tk.Label(root,textvariable=voltage_status,font=("DSEG7 Classic",65),background = "#351d18",fg= "orange",anchor="e",width =3) voltage_label.place(x=283,y=643) def set_voltage(): global o voltage_str = ('{:.3}'.format(str(o.query(obd.commands.CONTROL_MODULE_VOLTAGE)))) voltage_float = float(voltage_str) voltage_status.set(voltage_float) root.after(300, set_voltage) set_voltage() def info_display(self): info_status = tk.StringVar() info_status.set("888") info_label=tk.Label(root,textvariable=info_status,font=("misaki_mincho",42),background = "#351d18",fg= "orange",anchor="e",width =9) info_label.place(x=260,y=853) def set_info(): info_status.set("S2000-X!") root.after(400, set_info) set_info() def water_t_display(self): water_t_status = tk.StringVar() water_t_status.set("888") water_t_label=tk.Label(root,textvariable=water_t_status,font=("DSEG7 Classic",67),background = "#351d18",fg= "orange",anchor="e",width =3) water_t_label.place(x=1406,y=438) def set_water_t(): global o water_t_str = ('{:.3}'.format(str(o.query(obd.commands.COOLANT_TEMP)))) water_t_float = float(water_t_str) water_t_status.set(water_t_float) root.after(300, set_water_t) set_water_t() def fuel_display(self): fuel_status = tk.StringVar() fuel_status.set("888") fuel_label=tk.Label(root,textvariable=fuel_status,font=("DSEG7 Classic",67),background = "#351d18",fg= "orange",anchor="e",width =3) fuel_label.place(x=1406,y=643) def set_fuel(): global o #Height_of fuel fuel_str = ('{:.3}'.format(str(o.query(obd.commands.FUEL_LEVEL)))) fuel_float = float(fuel_str) #max=100 fuel_amount= ((fuel_float)/100.0)*55.0 fuel_status.set(fuel_amount) root.after(400, set_fuel) set_fuel() def trip_display(self): trip_status = tk.StringVar() trip_status.set("888") trip_label=tk.Label(root,textvariable=trip_status,font=("DSEG7 Classic",37),background = "#351d18",fg= "orange",anchor="e",width =10) trip_label.place(x=696,y=798) def set_trip(): global o trip_str = (str(o.query(obd.commands.DISTANCE_W_MIL))) if '0.0' in trip_str: trip_v=0 trip_float=0 if (math.floor(trip_float)) < 10: trip_v = ('{:.2}'.format(str(trip_str))) if (math.floor(trip_float)) < 100: trip_v = ('{:.3}'.format(str(trip_str))) trip_status.set("TRIP: "+str(trip_v)) root.after(400, set_trip) set_trip() d = Display() d.window() データの整形は、取得されたデータをstring型に変換し、必要な文字を切り詰め、それをfloat型に変換した。 実際のメーターの動作風景を以下に載せる。 5.制作してみて OBDモジュールのインストールでてこずった。自分の画像編集技術のなさに失望した。 質問に的確に答えてくださったLINEのPythonのオープンチャットの皆様、大変感謝しております。 次回改良するときがあったら、車検に通るくらいの精度が高く起動が速いメーターを作りたい。 6.参考サイト メーター制作の際に、使用モジュールなどを参考にさせていただいた記事です。丁寧なコメントありがとうございました。 小数点以下を切り上げる際に参考にさせていただいたサイトです。いつもお世話になっております。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DjangoでviewsからHTML(templates)への値の渡し方

概要 djangoの値をhtmlに渡すために必要なviewsの書き方を調べてきたので、よければお役に立ててください。また、ある程度djangoを触ってる人向けに書いてあります。 開発環境 Mac djagno=3.2.1 python=3.9.1 きっかけ djangoからhtmlに値を渡す際に、querysetが表示されてしまって、なかなか自分の思い通りにならなかったので、忘備録兼、記事にしたいと思います。 views.pyの書き方 まずは、関数での値の渡し方 ()の中の数字はそれと同じものを指しています。 views.py def hello(request): hello(①) = "Hello World" context = { 'hello'(②): hello(①) } return render(request, 'hello.html', context) hello.html <!DOC... <body> <h1>{{ hello(②) }}</h1> </body> このcontextというのに入れるのが基本系だと思います。また、辞書型で渡さないとエラーになってしまうため、このような書き方をしています。公式ドキュメント通りだとこの書き方だと思います。また、models.pyを作って、簡単にadmin.pyで表示、編集。それの値を受け取り、関数を使って、値を渡してみます。 admin.pyで保存したのは id = 1 = Hello World id = 2 = Goodbye World の2つです。1つしか表示しない場合はfor文はいらないですが、せっかくなので、同時に紹介したいと思います。 models.py class Greetings(models.Model): greeting = models.CharField(max_length=20) views.py def greeting(request): greetings = Greetings.objects.all() context = { 'greetings'(①): greetings, } return render(request, 'greeting.html', context) hello.html {% for greeting(②) in greetings(①) %} <h1>{{ greeting(②) }}</h1> 次は汎用ビューでの値の渡し方です。greeting.htmlの中身は上のものと一緒です。 models.py class Greetings(models.Model): greeting = models.CharField(max_length=20) views.py class GreetingView(TemplateView): model = Greetings template_name = 'greeting.html' #この関数が、classからhtmlに値を渡すおまじないになります。2行目まではほとんど一緒だと思います。 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['greetings'] = Greetings.objects.all() return context こうすると、Hello World と GoodBye World が一緒に表示されます。 また、汎用ビューでもDetailViewを使う際は、id毎に別々のgreetingを呼ぶことができます。 自分でも確認ができてよかったです。次は、modelのフィールドについて話してみようと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReperとTouchDesignerでアルゴリズム作曲がしたい

やりたいこと アルゴリズム作曲といえばMax/MspやPure Dataなどを思い浮かべる人が多いと思いますが、基本的にこれらのツールはリアルタイムで音楽を生成することに長けているかわりにタイムライン的に音を配置していくような作曲は少々やりずらく感じます。 そこでReperというDAWを使って、より一般的なDAWっぽい操作感でアルゴリズム作曲をしたいと考えました。 Reperについて ReaperはオープンソースのDAWであり、無料でも使えるため、DTM初心者がとりあえず手を出すDAWソフト、という印象を持っている方も多いと思います。ですが実際は非常に多機能かつ拡張性のあるソフトなんです! 中でもReascriptという機能はlua, eel, python用に用意されているAPIを用いることで書き出しの自動化やマクロの作成、エフェクトの作成などができるようになります。 このReascriptをPythonで使って簡単なアルゴリズム作曲をしてみます。 CSVファイルの準備 今回はアルゴリズム作曲というか現代音楽でよくありがちなデータマッピングで音楽を作る手法をやってみます。音楽におけるデータマッピングについては松本昭彦さんのこのページ http://akihikomatsumoto.com/maxmsp/guido.html が非常にわかりやすいと思います。 今回は芸術的な云々ということはおいておき、CSVファイルの情報からMIDIノートを生成してみます。 今回使わせていただいたデータは http://www2.obirin.ac.jp/tsubota/eq/menu3.html ここで公開されている地震発生時刻と震源の深さ、発生地点の移動経度などがまとめられているのデータです。 まずはこのデータをTouchDesignerを使って扱いやすくしていきます。 今回はなるべくシンプルにするために発生時刻、発生地点の緯度、震源の深さの3つデータを発音タイミング、ピッチ、ベロシティにマッピングすることにします。 まず、TouchDesignerに先ほどのCSVを読み込みます。 TouchDesignerでは難しいことはやらない(というか使わなくてもいいけど視覚的にデータ処理がしやすいから使う)ので細かい説明は省きます。 発生時間の時間単位を掻けたり足したりしてhourにまとめて一年間を一列の時単位にまとめます。 発生時間と緯度と震源の深さをそれぞれ発生タイミング、ピッチ、ベロシティに最適な値となるようにレンジを調節し、ピッチ、ベロシティに関しては0-127の整数にしておきます(MIDI規格の範囲)。発生タイミングに関しては今回書くプログラムでは秒指定になるので、そのつもりでレンジを調節します。 fileout datでCSVファイルとして書き出します。 3列目(深度のデータ)は計測されていない部分もあるので欠けていますが問題ありません。 Reaperの準備 Reaperで準備することはトラックを一つ作って任意の名前を付け、media item(MIDIノートを入力できるボックス)を一つ作っておくだけです。media itemはトラック上でctrlを押しながらドラックすると作成できます。これらもReascriptで自動化することはできますが今回はMIDIノートの生成だけのプログラムを書きます。 Reascript Reascriptでpythonを使う場合は設定が必要になります。 Options > Preferenecs > ReaScriptのpythonの有効化にチェック入れ、python.dllのパスと名前を入れることでpythonでreascriptが使えるようになります。 tomoさんのnoteにわかりやすくのっています。 https://note.com/suntomo/n/nac439dbafb66  次にメニューバーのActionsからShow action listをクリック→開いたウィンドウの右下のNew actionをクリック→New reascriptを選択し.pyで任意の名前で保存します。 するとこのようなウィンドウが出るので 以下のコードを貼り付け、トラック名とcsvのパスを入れます。 csv_for_reaper.py from csv import reader search = "自分でつけたトラック名" notelength = 3 ppq = 960 with open(r'csvデータのパス', 'r') as csv_file: csv_reader = reader(csv_file, delimiter = '\t') notelist = list(csv_reader) notenum = len(notelist) tracknum = RPR_CountTracks(0) for index in range(tracknum): track = RPR_GetTrack(0,index) name = RPR_GetSetMediaTrackInfo_String(track, "P_NAME","", False)[3] if name == search: trackindex = index item = RPR_GetMediaItem(0, trackindex) take = RPR_GetMediaItemTake(item, 0) for addnote in range(notenum): startsec = float(notelist[addnote][0])*ppq endsec = startsec + (notelength*ppq) pitch = int(notelist[addnote][1]) velocity = int(notelist[addnote][2]) RPR_MIDI_InsertNote(take, True, False, startsec, endsec, 0, pitch, velocity, False) 保存すると同時に実行されるはずなので、うまくいけばこのようにmidiノートが生成されます。 うまくいかなかった場合は ・csvライブラリが入ってない ・トラック名が間違ってる ・csvのパスが間違ってる などが考えられると思います。 エラーメッセージも出るのでそれを読んでみてください。 pythonプログラムについて軽く説明すると csv_for_reaper.py with open(r'csvデータのパス', 'r') as csv_file: csv_reader = reader(csv_file, delimiter = '\t') notelist = list(csv_reader) notenum = len(notelist) ここでcsvを読み込んでリスト化し、リストの数を取得しています。 csv_for_reaper.py tracknum = RPR_CountTracks(0) for index in range(tracknum): track = RPR_GetTrack(0,index) name = RPR_GetSetMediaTrackInfo_String(track, "P_NAME","", False)[3] if name == search: trackindex = index ここで任意の名前のトラックを探し、 csv_for_reaper.py item = RPR_GetMediaItem(0, trackindex) take = RPR_GetMediaItemTake(item, 0) for addnote in range(notenum): startsec = float(notelist[addnote][0])*ppq endsec = startsec + (notelength*ppq) pitch = int(notelist[addnote][1]) velocity = int(notelist[addnote][2]) RPR_MIDI_InsertNote(take, True, False, startsec, endsec, 0, pitch, velocity, False) ここでリスト化したcsvのデータを取り出してstartsec(発音タイミング),pitch,velocityに代入するのをリストの数ぶんforでくりかえしています。 あたまにRPMがついているのはreaperのAPI functionなので詳しくは https://www.reaper.fm/sdk/reascript/reascripthelp.html こちらを参照してください。 基本は以上です! かなりいろんなことができそうなので、これからもちょくちょくやっていこうと思ってます! 私はコードベースのプログラミングが得意ではないのでおかしい部分などもあると思いますが、もしよかったら教えていただけると幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReaperとTouchDesignerでアルゴリズム作曲がしたい

やりたいこと アルゴリズム作曲といえばMax/MspやPure Dataなどを思い浮かべる人が多いと思いますが、基本的にこれらのツールはリアルタイムで音楽を生成することに長けているかわりにタイムライン的に音を配置していくような作曲は少々やりずらく感じます。 そこでRaeperというDAWを使って、より一般的なDAWっぽい操作感でアルゴリズム作曲をしたいと考えました。 Reaperについて ReaperはオープンソースのDAWであり、無料でも使えるため、DTM初心者がとりあえず手を出すDAWソフト、という印象を持っている方も多いと思います。ですが実際は非常に多機能かつ拡張性のあるソフトなんです! 中でもReascriptという機能はlua, eel, python用に用意されているAPIを用いることで書き出しの自動化やマクロの作成、エフェクトの作成などができるようになります。 このReascriptをPythonで使って簡単なアルゴリズム作曲をしてみます。 CSVファイルの準備 今回はアルゴリズム作曲というか現代音楽でよくありがちなデータマッピングで音楽を作る手法をやってみます。音楽におけるデータマッピングについては松本昭彦さんのこのページ http://akihikomatsumoto.com/maxmsp/guido.html が非常にわかりやすいと思います。 今回は芸術的な云々ということはおいておき、CSVファイルの情報からMIDIノートを生成してみます。 今回使わせていただいたデータは http://www2.obirin.ac.jp/tsubota/eq/menu3.html ここで公開されている地震発生時刻と震源の深さ、発生地点の移動経度などがまとめられているのデータです。 まずはこのデータをTouchDesignerを使って扱いやすくしていきます。 今回はなるべくシンプルにするために発生時刻、発生地点の緯度、震源の深さの3つデータを発音タイミング、ピッチ、ベロシティにマッピングすることにします。 まず、TouchDesignerに先ほどのCSVを読み込みます。 TouchDesignerでは難しいことはやらない(というか使わなくてもいいけど視覚的にデータ処理がしやすいから使う)ので細かい説明は省きます。 発生時間の時間単位を掻けたり足したりしてhourにまとめて一年間を一列の時単位にまとめます。 発生時間と緯度と震源の深さをそれぞれ発音タイミング、ピッチ、ベロシティに最適な値となるようにレンジを調節し、ピッチ、ベロシティに関しては0-127の整数にしておきます(MIDI規格の範囲)。発音タイミングに関しては今回書くプログラムでは秒指定になるので、そのつもりでレンジを調節します。 fileout datでCSVファイルとして書き出します。 3列目(深度のデータ)は計測されていない部分もあるので欠けていますが問題ありません。 Reaperの準備 Reaperで準備することはトラックを一つ作って任意の名前を付け、media item(MIDIノートを入力できるボックス)を一つ作っておくだけです。media itemはトラック上でctrlを押しながらドラックすると作成できます。これらもReascriptで自動化することはできますが今回はMIDIノートの生成だけのプログラムを書きます。 Reascript Reascriptでpythonを使う場合は設定が必要になります。 Options > Preferenecs > ReaScriptのpythonの有効化にチェック入れ、python.dllのパスと名前を入れることでpythonでreascriptが使えるようになります。 tomoさんのnoteにわかりやすくのっています。 https://note.com/suntomo/n/nac439dbafb66 次にメニューバーのActionsからShow action listをクリック→開いたウィンドウの右下のNew actionをクリック→New reascriptを選択し.pyで任意の名前で保存します。 するとこのようなウィンドウが出るので 以下のコードを貼り付け、トラック名とcsvのパスを入れます。 csv_for_reaper.py from csv import reader search = "自分でつけたトラック名" notelength = 3 ppq = 960 with open(r'csvデータのパス', 'r') as csv_file: csv_reader = reader(csv_file, delimiter = '\t') notelist = list(csv_reader) notenum = len(notelist) tracknum = RPR_CountTracks(0) for index in range(tracknum): track = RPR_GetTrack(0,index) name = RPR_GetSetMediaTrackInfo_String(track, "P_NAME","", False)[3] if name == search: trackindex = index item = RPR_GetMediaItem(0, trackindex) take = RPR_GetMediaItemTake(item, 0) for addnote in range(notenum): startsec = float(notelist[addnote][0])*ppq endsec = startsec + (notelength*ppq) pitch = int(notelist[addnote][1]) velocity = int(notelist[addnote][2]) RPR_MIDI_InsertNote(take, True, False, startsec, endsec, 0, pitch, velocity, False) 保存すると同時に実行されるはずなので、うまくいけばこのようにmidiノートが生成されます。 うまくいかなかった場合は ・csvライブラリが入ってない ・トラック名が間違ってる ・csvのパスが間違ってる などが考えられると思います。 エラーメッセージも出るのでそれを読んでみてください。 pythonプログラムについて軽く説明すると csv_for_reaper.py with open(r'csvデータのパス', 'r') as csv_file: csv_reader = reader(csv_file, delimiter = '\t') notelist = list(csv_reader) notenum = len(notelist) ここでcsvを読み込んでリスト化し、リストの数を取得しています。 csv_for_reaper.py tracknum = RPR_CountTracks(0) for index in range(tracknum): track = RPR_GetTrack(0,index) name = RPR_GetSetMediaTrackInfo_String(track, "P_NAME","", False)[3] if name == search: trackindex = index ここで任意の名前のトラックを探し、 csv_for_reaper.py item = RPR_GetMediaItem(0, trackindex) take = RPR_GetMediaItemTake(item, 0) for addnote in range(notenum): startsec = float(notelist[addnote][0])*ppq endsec = startsec + (notelength*ppq) pitch = int(notelist[addnote][1]) velocity = int(notelist[addnote][2]) RPR_MIDI_InsertNote(take, True, False, startsec, endsec, 0, pitch, velocity, False) ここでリスト化したcsvのデータを取り出してstartsec(発音タイミング),pitch,velocityに代入するのをリストの数ぶんforでくりかえしています。 あたまにRPRがついているのはreaperのAPI functionなので詳しくは https://www.reaper.fm/sdk/reascript/reascripthelp.html こちらを参照してください。 基本は以上です! かなりいろんなことができそうなので、これからもちょくちょくやっていこうと思ってます! 私はコードベースのプログラミングが得意ではないのでおかしい部分などもあると思いますが、もしよかったら教えていただけると幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ドラッグアンドドロップでxlsファイルを読み込む

ddExcel.py import pandas as pd import os import io import sys def MojibakeTaisaku(): # 出力の文字化け対策 sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') def main(wbName): MojibakeTaisaku() print(pd.__version__) # pyファイルのあるフォルダに移動する os.chdir(os.path.dirname(os.path.abspath(__file__))) df = pd.read_excel(wbName, index_col=0) print(df) if __name__ == '__main__': # ドラッグアンドドロップでxlsファイルを読み込む wbName = sys.argv[1] main(wbName) input() # ターミナルが勝手に閉じるの防止用 何か入力すれば閉じる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python 環境構築 obd

obdのマシンでの練習のための備忘録 (1) $ conda create -n py37_GAN python=3.7 anaconda (2) $ conda activate py37_GAN (py37_GAN) $ conda install -c anaconda chainer Collecting package metadata (current_repodata.json): done Solving environment: done ==> WARNING: A newer version of conda exists. <== current version: 4.9.2 latest version: 4.10.1 Please update conda by running $ conda update -n base -c defaults conda ## Package Plan ## environment location: /home/[user_name]/.pyenv/versions/anaconda3-5.2.0/envs/py37_GAN added / updated specs: - chainer The following packages will be downloaded: package | build ---------------------------|----------------- ca-certificates-2020.10.14 | 0 128 KB anaconda chainer-6.3.0 | py_0 671 KB anaconda libprotobuf-3.13.0.1 | hd408876_0 2.3 MB anaconda openssl-1.1.1h | h7b6447c_0 3.8 MB anaconda protobuf-3.13.0.1 | py37he6710b0_1 700 KB anaconda typing-3.7.4.3 | py37_0 12 KB anaconda ------------------------------------------------------------ Total: 7.6 MB The following NEW packages will be INSTALLED: chainer anaconda/noarch::chainer-6.3.0-py_0 libprotobuf anaconda/linux-64::libprotobuf-3.13.0.1-hd408876_0 protobuf anaconda/linux-64::protobuf-3.13.0.1-py37he6710b0_1 typing anaconda/linux-64::typing-3.7.4.3-py37_0 The following packages will be SUPERSEDED by a higher-priority channel: ca-certificates pkgs/main --> anaconda openssl pkgs/main --> anaconda Proceed ([y]/n)? yy Invalid choice: yy Proceed ([y]/n)? y Downloading and Extracting Packages typing-3.7.4.3 | 12 KB | ########################################################################################## | 100% ca-certificates-2020 | 128 KB | ########################################################################################## | 100% libprotobuf-3.13.0.1 | 2.3 MB | ########################################################################################## | 100% openssl-1.1.1h | 3.8 MB | ########################################################################################## | 100% protobuf-3.13.0.1 | 700 KB | ########################################################################################## | 100% chainer-6.3.0 | 671 KB | ########################################################################################## | 100% Preparing transaction: done Verifying transaction: done Executing transaction: done (3) $ conda install -c anaconda chainer
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GhPythonで平面直角座標を緯度経度に変換する

はじめに 「緯度経度と平面直角座標の相互変換をPythonで実装する」を参考に Grasshopper の GhPython で平面直角座標を緯度経度に変換してみた。@sw1227 さん大変参考になりました!ありがとうございます! 変えたところ GhPython では numpy を使えないので・・・  ・math で使える関数に置き換えた  ・np.array をリストに置き換えた  ・np.dot を for文で書き直した といったあたりを変更 GhPythonで作成 Type hint はすべて float にする コードはこちら import math """ 平面直角座標を緯度経度に変換する - input: (x, y): 変換したいx, y座標[m] (phi0_deg, lambda0_deg): 平面直角座標系原点の緯度・経度[度](分・秒でなく小数であることに注意) - output: lat: 緯度[度] lon: 経度[度] """ # 平面直角座標系原点をラジアンに直す phi0_rad = math.radians(phi0_deg) lambda0_rad = math.radians(lambda0_deg) # 補助関数 def A_array(n): A0 = 1 + (n**2)/4. + (n**4)/64. A1 = - (3./2)*( n - (n**3)/8. - (n**5)/64. ) A2 = (15./16)*( n**2 - (n**4)/4. ) A3 = - (35./48)*( n**3 - (5./16)*(n**5) ) A4 = (315./512)*( n**4 ) A5 = -(693./1280)*( n**5 ) return [A0, A1, A2, A3, A4, A5] def beta_array(n): b0 = None b1 = (1./2)*n - (2./3)*(n**2) + (37./96)*(n**3) - (1./360)*(n**4) - (81./512)*(n**5) b2 = (1./48)*(n**2) + (1./15)*(n**3) - (437./1440)*(n**4) + (46./105)*(n**5) b3 = (17./480)*(n**3) - (37./840)*(n**4) - (209./4480)*(n**5) b4 = (4397./161280)*(n**4) - (11./504)*(n**5) b5 = (4583./161280)*(n**5) return [b0, b1, b2, b3, b4, b5] def delta_array(n): d0 = None d1 = 2.*n - (2./3)*(n**2) - 2.*(n**3) + (116./45)*(n**4) + (26./45)*(n**5) - (2854./675)*(n**6) d2 = (7./3)*(n**2) - (8./5)*(n**3) - (227./45)*(n**4) + (2704./315)*(n**5) + (2323./945)*(n**6) d3 = (56./15)*(n**3) - (136./35)*(n**4) - (1262./105)*(n**5) + (73814./2835)*(n**6) d4 = (4279./630)*(n**4) - (332./35)*(n**5) - (399572./14175)*(n**6) d5 = (4174./315)*(n**5) - (144838./6237)*(n**6) d6 = (601676./22275)*(n**6) return [d0, d1, d2, d3, d4, d5, d6] def show_angle(deg): """ 小数点の角度[deg]を度,分,秒で表記 """ d = int(math.floor(deg)) m = int(math.floor((deg%1) * 60)) s = ( ((deg%1)*60) % 1 ) * 60 return """ {0}°{1:02d}'{2}" """.format(d, m, s) # 分は10の位を0埋めする # 定数 (a, F: 世界測地系-測地基準系1980(GRS80)楕円体) m0 = 0.9999 a = 6378137. F = 298.257222101 # (1) n, A_i, beta_i, delta_iの計算 n = 1. / (2*F - 1) A_array = A_array(n) beta_array = beta_array(n) delta_array = delta_array(n) # (2), S, Aの計算 A_ = ( (m0*a)/(1.+n) )*A_array[0] S_ = ( (m0*a)/(1.+n) ) * (A_array[0]*phi0_rad + sum([A_array[j]*(math.sin(2*phi0_rad*j)) for j in range(1,6)])) # (3) xi, etaの計算 xi = (x + S_) / A_ eta = y / A_ # (4) xi', eta'の計算 xi2 = xi - sum([beta_array[j]*math.sin(2*xi*j)*math.cosh(2*eta*j) for j in range(1,6)]) eta2 = eta - sum([beta_array[j]*math.cos(2*xi*j)*math.sinh(2*eta*j) for j in range(1,6)]) # (5) chiの計算 chi = math.asin( math.sin(xi2)/math.cosh(eta2) ) # (6) 緯度(latitude), 経度(longitude)の計算 latitude = chi + sum([delta_array[j] * math.sin(2*chi*j) for j in range(1,7)]) longitude = lambda0_rad + math.atan( math.sinh(eta2)/math.cos(xi2) ) lat = show_angle(math.degrees(latitude)) lon = show_angle(math.degrees(longitude)) 参考 緯度経度と平面直角座標の相互変換をPythonで実装する 緯度経度と平面直角座標の相互変換を実装するための数式
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RDkitをインストールできなかった時の対処法

AnacondaをインストールしChemoinformaticsの勉強をしていましたが、どうしてもRDkitをインストールできなかったので、その対処法を忘備録として残します。 明治大の金子先生のホームページを参考にしました。https://datachemeng.com/rdkit_install_import/ 環境 Windows10 Anaconda(Anaconda3-2020.11-Windows-x86_64) RDkitをインストールできなかった原因 金子先生のホームページ情報によると、RDkitの最新バージョンである 2020.09.1.0 は、Python 3.8 以上に対応していないようです。私はAnaconda3-2020.11-Windows-x86_64.exeをインストールしたので、Pythonは3.8.6でした。Pythonのバージョン そこでPythonのバージョンをダウングレードすることにしました。Anaconda Promptで以下のように入力します。 conda install -y python=3.7.9 ダウンロードやファイルの解凍は行われているようでしたが、以下のメッセージが出てダウングレードできませんでした。 EnvironmentNotWritableError: The current user does not have write permissions to the target environment. environment location: C:\ProgramData\Anaconda3 対処法 どうやらC:\ProgramData\下のディレクトリでは書き換えが許可されていないようです。Anacondaをアンインストールし Cドライブ直下のC:Anaconda3に再インストールしました。 その後、再度以下のコマンドでPythonのダウングレードしたところ、うまくダウングレードできました。 conda install -y python=3.7.9 conda listで3.7.9バージョンを確認しました。 RDkitのインストールは以下のコマンドで行います。 conda install -y -c rdkit rdkit まとめ Anacondaのインストールは標準のC:\Programfile\ではなくCドライブ直下にした方が良い。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonによるリアルタイムオーディオ出力

概要 windows版のpythonで、実時間で作成orMIXした音声データを出力するための技術メモです。 想定するアプリケーションと実行環境 想定するアプリケーション:音声合成、音声処理 実行環境:python, windows10 参考情報 実行環境の準備 pyaudioのインストールは、pip install pyaudioで行えますが、python 3.7以降では使用できません。Windwos環境であれば、UCIのLaboratory for Fluorescence Dynamics講座のChristoph Gohlkeさんが、作成された非公式パッケージがあります。こちらをインストールするには、https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio から該当するものをダウンロードし、pip install <ダウンロードしたファイル>とします。自己責任ですが。 解説 予備知識(処理の同期) リアルタイムデータを扱うソフトウェアでは、「データを処理/生成する部分」と「データを入力/出力する部分」のデータ処理速度が釣り合っていないと、データの欠損や処理の異常停止などが発生し、期待される処理が行えなくなります。このため、生成部分と消費部分の処理タイミングを調整するメカニズムが必要になります。 ブロッキングモードでは、「データを処理/生成する部分」が、データの入力/出力処理を行った場合、「データを入出力する処理を完了する」まで処理を停止させることで、「データを処理/生成する部分」の処理速度を調整します。 コールバックモードでは、「データを入出力する処理が完了した」ときに、あらかじめ登録した「データを処理/生成する部分」の処理を呼び出すことで、「データを処理/生成する部分」の処理速度を調整します。呼び出されるまでは他の処理ができるのが利点です。 音声出力プログラムの例 ブロッキングモード データを作っては出力APIsndStrm.write(チャンクデータ)を呼び出すことを繰り返す。出力バッファがいっぱいになると処理が一時停止することで、処理が同期する。 コールバックモード playAudioオブジェクトを作成するときに、コールバックする関数をパラメータ指定して登録するstrm = pyAud.open(..., stream_callback=コールバック関数)。準備が整ったところで、処理開始を伝えるstrm.start_stream()ことで、入出力処理がスタートし、データが完成/必要となったときに自動的にコールバック関数が呼び出させる。最後に停止処理を行う。 サンプルソース サンプル import pyaudio import numpy as np import time SND_FS = 44100 # 48000 SND_CHUNK = 1024 SND_CH_OUT = 1 # CHUNK time = CNUNK/FS 23.22 ms (43.07Hz) #========================================================= # テスト用サウンドデータを作る # チャンクあたり10個のSIN波データを作る(232Hz @ 44100sps) #========================================================= def procSound(dtype=np.int16): x = np.linspace(0, 20*np.pi, SND_CHUNK+1) y=np.sin(x[0:SND_CHUNK])*256*10 #----参考 ざっくりかくならこれ(歪みあり) ---- #x = np.linspace(0, 20*np.pi, SND_CHUNK) #y=np.sin(x)*256*10 # array to buffer sndBuf = y.astype(dtype).tobytes() return sndBuf # test #buf = procSound() #print(buf) #========================================================= # ブロックモードの利用 #========================================================= def demoBlockingMode(): pyAud = pyaudio.PyAudio() sndStrm = pyAud.open(rate=SND_FS,channels=SND_CH_OUT,format=pyaudio.paInt16,input=False,output=True) for cnt in range(100): obuf = procSound() if sndStrm.is_active : sndStrm.write(obuf) # blocking mode. blocks until all the give frames have been played sndStrm.stop_stream() sndStrm.close() pyAud.terminate() #========================================================= # callbackモード #========================================================= def sndCallback(in_data, frame_count, time_info, status): #data = wf.readframes(frame_count) data = procSound() return (data, pyaudio.paContinue) def demoCallbackMode(): pyAud = pyaudio.PyAudio() sndStrm = pyAud.open(rate=SND_FS,channels=SND_CH_OUT,format=pyaudio.paInt16,input=False,output=True, stream_callback=sndCallback) sndStrm.start_stream() time.sleep(3) sndStrm.stop_stream() sndStrm.close() pyAud.terminate() #========================================================= # 実行 #========================================================= #demoBlockingMode() demoCallbackMode() 結言 まぁ、コールバックとかみなさまご存じでしょうが念のため。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

投稿テスト(タイトル)

はじめに 文章文章。... 2行目文章..... 3行目行間無し sub ・ 箇条書き1 ・ 箇条書き2 sub2 サイトリンク グーグル ヤフー 赤* 青 緑 水色 紫 橙* senntence00 a senntence1 senntence2 code テスト import python #説明 A="a" #説明2 >>> import re #説明3 >>> test(r"\b[]|\¥")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS CloudFormationでS3アーティファクトを使用したLambdaを構築しよう

はじめに AWS CloudFormationを利用してS3アーティファクトを使用したLambda構築のテンプレートのサンプルです。 テンプレートの概要が分からない場合は、はじめてのAWS CloudFormationテンプレートを理解するを参考にしてください。 コードはGitHubにもあります。 今回は、akane というシステムの dev 環境を想定しています。 同じ構成で違う環境を作成する場合は、{環境名}-parameters.jsonを別途作成します。 ディレクトリ構成 akane (システム) ├── lambda (スタック) │ ├── code │ │ └── getTiAmo.py (S3アーティファクトソース) │ ├── code-lambda-getTiAmo.zip (S3アーティファクト) │ ├── delete_artifact.dev.sh (S3アーティファクト削除シェル) │ ├── dev-parameters.json (dev 環境のパラメータ) │ ├── lambda.yml (CFnテンプレート) │ ├── mkzip.sh (S3アーティファクト作成シェル) │ └── upload_artifact.dev.sh (S3アーティファクト削除シェル) └─ s3 (スタック) ├─ s3.yml (CFnテンプレート) └─ all-parameters.json (all 環境のパラメータ) AWS リソース構築内容 lambdaスタック Lambdaロール Lambda s3スタック s3バケット (akane-all-s3-artifacts) バケットポリシー (s3:GetObject) 実行環境の準備 AWS CloudFormationを動かすためのAWS CLIの設定を参考にしてください。 AWS リソース構築手順 下記を実行してスタックを作成 ./create_stacks.sh 下記を実行してLambdaの動作を確認 ./test_lambda.sh 下記を実行してスタックを削除 ./delete_stacks.sh 構築テンプレート 1. s3スタック s3.yml AWSTemplateFormatVersion: 2010-09-09 Description: S3 For Akane # Metadata: Parameters: SystemName: Type: String AllowedPattern: '[a-zA-Z0-9-]*' EnvType: Description: Environment type. Type: String AllowedValues: [all, dev, stg, prod] ConstraintDescription: must specify all, dev, stg, or prod. # Mappings # Conditions # Transform Resources: # S3 Bucket作成 akaneS3Bucket: Type: AWS::S3::Bucket Properties: AccessControl: Private BucketName: !Sub - ${SystemName}-${EnvType}-s3-artifacts - {SystemName: !Ref SystemName, EnvType: !Ref EnvType} Tags: - Key: Name Value: !Sub - ${SystemName}-${EnvType}-s3-artifacts - {SystemName: !Ref SystemName, EnvType: !Ref EnvType} - Key: SystemName Value: !Ref SystemName - Key: EnvType Value: !Ref EnvType # S3 BucketPolicy作成 akaneS3BucketPolicy: Type: AWS::S3::BucketPolicy DependsOn: akaneS3Bucket Properties: Bucket: !Ref akaneS3Bucket PolicyDocument: Statement: - Action: - s3:GetObject Effect: Allow Resource: !Join - '' - - 'arn:aws:s3:::' - !Ref akaneS3Bucket - /* Principal: AWS: '*' Outputs: akaneS3Bucket: Value: !Ref akaneS3Bucket Export: Name: !Sub - ${SystemName}-${EnvType}-s3-artifacts - {SystemName: !Ref SystemName, EnvType: !Ref EnvType} all-parameters.json { "Parameters": [ { "ParameterKey": "SystemName", "ParameterValue": "akane" }, { "ParameterKey": "EnvType", "ParameterValue": "all" } ] } 2. lambdaスタック lambda.yml AWSTemplateFormatVersion: 2010-09-09 Description: Lambda For Akane # Metadata: Parameters: SystemName: Type: String AllowedPattern: '[a-zA-Z0-9-]*' EnvType: Description: Environment type. Type: String AllowedValues: [all, dev, stg, prod] ConstraintDescription: must specify all, dev, stg, or prod. ArtifactS3Bucket: Type: String LambdaName: Type: String LambdaMemorySize: Type: Number LambdaRuntime: Type: String GirlName: Type: String # Mappings # Conditions # Transform Resources: # ロール作成 akaneRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Description: !Sub - ${SystemName}-${EnvType}-role-lambda-${AWS::Region} - {SystemName: !Ref SystemName, EnvType: !Ref EnvType} ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Path: / RoleName: !Sub - ${SystemName}-${EnvType}-role-lambda-${AWS::Region} - {SystemName: !Ref SystemName, EnvType: !Ref EnvType} Tags: - Key: Name Value: !Sub - ${SystemName}-${EnvType}-role-lambda-${AWS::Region} - {SystemName: !Ref SystemName, EnvType: !Ref EnvType} - Key: SystemName Value: !Ref SystemName - Key: EnvType Value: !Ref EnvType # Lambda作成 akaneLambdaGetTiAmo: Type: AWS::Lambda::Function Properties: Code: S3Bucket: !Ref ArtifactS3Bucket S3Key: !Sub - code-lambda-${LambdaName}.zip - {LambdaName: !Ref LambdaName} Description: !Sub - ${SystemName}-${EnvType}-lambda-${LambdaName} - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaName: !Ref LambdaName} Environment: Variables: GIRL_NAME: !Ref GirlName FunctionName: !Sub - ${SystemName}-${EnvType}-lambda-${LambdaName} - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaName: !Ref LambdaName} Handler: !Sub - ${LambdaName}.handler - {LambdaName: !Ref LambdaName} MemorySize: !Ref LambdaMemorySize Role: !GetAtt akaneRole.Arn Runtime: !Ref LambdaRuntime Tags: - Key: Name Value: !Sub - ${SystemName}-${EnvType}-lambda-${LambdaName} - {SystemName: !Ref SystemName, EnvType: !Ref EnvType} - Key: SystemName Value: !Ref SystemName - Key: EnvType Value: !Ref EnvType Timeout: 900 TracingConfig: Mode: PassThrough Outputs: akaneLambdaGetTiAmoArn: Value: !GetAtt akaneLambdaGetTiAmo.Arn Export: Name: !Sub - ${SystemName}-${EnvType}-lambda-arn - {SystemName: !Ref SystemName, EnvType: !Ref EnvType} dev-parameters.json { "Parameters": [ { "ParameterKey": "SystemName", "ParameterValue": "akane" }, { "ParameterKey": "EnvType", "ParameterValue": "dev" }, { "ParameterKey": "ArtifactS3Bucket", "ParameterValue": "akane-all-s3-artifacts" }, { "ParameterKey": "LambdaName", "ParameterValue": "getTiAmo" }, { "ParameterKey": "LambdaMemorySize", "ParameterValue": "128" }, { "ParameterKey": "LambdaRuntime", "ParameterValue": "python3.8" }, { "ParameterKey": "GirlName", "ParameterValue": "akane" } ], "Capabilities": [ "CAPABILITY_NAMED_IAM" ] } 3. 実行ファイル create_stacks.sh #!/bin/sh cd `dirname $0` SYSTEM_NAME=akane create_stack () { ENV_TYPE=$1 STACK_NAME=$2 aws cloudformation create-stack \ --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME} \ --template-body file://./${SYSTEM_NAME}/${STACK_NAME}/${STACK_NAME}.yml \ --cli-input-json file://./${SYSTEM_NAME}/${STACK_NAME}/${ENV_TYPE}-parameters.json aws cloudformation wait stack-create-complete \ --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME} } create_stack all s3 ./akane/lambda/mkzip.sh ./akane/lambda/upload_artifact.dev.sh create_stack dev lambda exit 0 test_lambda.sh #!/bin/sh cd `dirname $0` LAMBDA_NAME=akane-dev-lambda-getTiAmo OUTPUT_FILE=response.json aws lambda invoke --function-name ${LAMBDA_NAME} --log-type Tail ${OUTPUT_FILE} --query 'LogResult' --output text | base64 -D cat ${OUTPUT_FILE} exit 0 delete_stacks.sh #!/bin/sh cd `dirname $0` SYSTEM_NAME=akane delete_stack () { ENV_TYPE=$1 STACK_NAME=$2 aws cloudformation delete-stack \ --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME} aws cloudformation wait stack-delete-complete \ --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME} } delete_stack dev lambda ./akane/lambda/delete_artifact.dev.sh delete_stack all s3 exit 0 4. アーティファクト関連ファイル mkzip.sh #!/bin/sh cd `dirname $0` ENV_TYPE=dev LAMBDA_NAME=$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "LambdaName").ParameterValue') FILE=../code-lambda-${LAMBDA_NAME}.zip cd ./code ls . | grep -v -E ".gitignore|${LAMBDA_NAME}.py" | xargs rm -rf rm -f ${FILE} zip -r ${FILE} ./* exit 0 upload_artifact.dev.sh #!/bin/sh cd `dirname $0` BASENAME=$(basename $0) FILENAME=${BASENAME%.*} ENV_TYPE=${FILENAME##*.} BUCKET_NAME=$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "ArtifactS3Bucket").ParameterValue') ARTIFACT_NAME=code-lambda-$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "LambdaName").ParameterValue') aws s3 cp ${ARTIFACT_NAME}.zip s3://${BUCKET_NAME}/${ARTIFACT_NAME}.zip exit 0 delete_artifact.dev.sh #!/bin/sh cd `dirname $0` BASENAME=$(basename $0) FILENAME=${BASENAME%.*} ENV_TYPE=${FILENAME##*.} BUCKET_NAME=$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "ArtifactS3Bucket").ParameterValue') ARTIFACT_NAME=code-lambda-$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "LambdaName").ParameterValue') aws s3 rm s3://${BUCKET_NAME}/${ARTIFACT_NAME}.zip exit 0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python 文字列の中で変数を展開する formatとfstring

f = "hoge" l = "ホゲ男" # "{}"置換フィールドに入れる文字列を指定 print('First={} Last={}'.format(f,l)) # First=hoge Last=ホゲ男 # 2 "{}" 置換フィールドを引数番号指定 # .formatカッコ内は左から(0,1,2,3....)という順番になる。 print('First={0} Last={1}'.format(f,l)) # First=hoge Last=ホゲ男 # 引数番号は自由に変更が可能 print('First={1} Last={0}'.format(f,l)) # First=ホゲ男 Last=hoge # 3 変数を指定した展開 print('First={first} Last={last}'.format(first=f,last=l)) # First=ホゲ男 Last=hoge # 4 新しいバージョン python3.6以降 処理が早い print(f'First={f} Last={l}') # First=hoge Last=ホゲ男 参考にした記事 Pythonの文字列の中で変数を展開する方法
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

株価分析(MACD)

はじめに 前回の記事、株価分析(RSI) の続編です。今回はMACDを使用します。MACDは移動平均を使った指標です。前にSMA(単純移動平均線)についての記事を書いていますが、MACDではEMA(指数平滑移動平均線)を使用しています。MACDについての詳しい説明は他に譲るとして1、早速使っていきたいと思います。 MACDの戦略は下記のようにしました。 MACDとMACDシグナルのゴールデンクロスで買い MACDとMACDシグナルのデッドクロスで売り MACDがマイナスからプラスになったら買い MACDがプラスからマイナスになったら売り 今回は下記のような流れで進めていきたいと思います。 2011年〜2018年の8年間で最適な長期、短期、シグナル期間の組み合わせを見つける。 上記の最適な組み合わせを使用して、2019年〜2020年のシミュレートを行う。 2011年〜2020年の10年間の最適な長期、短期、シグナル期間の組み合わせを見つける。 MACD Strategyクラス MACD Strategyクラスは下記のように実装しました。 MACDの計算はいつも通りTA-Libで行っています。 期間はTA-Libのデフォルト値と同じ値にしています。 class MacdStrategy(Strategy): ''' MACD Strategy ''' fastperiod = 12 slowperiod = 26 signalperiod = 9 def init(self): close = self.data['Adj Close'] self.macd, self.macdsignal, _ = self.I( talib.MACD, close, fastperiod=self.fastperiod, slowperiod=self.slowperiod, signalperiod=self.signalperiod) def next(self): ''' MACDとMACDシグナルのゴールデンクロスで買い MACDとMACDシグナルのデッドクロスで売り MACDがマイナスからプラスになったら買い MACDがプラスからマイナスになったら売り ''' if crossover(self.macd, self.macdsignal): self.buy() elif crossover(self.macd, self.macdsignal): self.sell() elif crossover(self.macd, 0): self.buy() elif crossover(0, self.macd): self.sell() このデフォルト値を使った状態で2011年〜2020年の日経平均株価でをシミュレートしてみるとリターンは177%でした。年率で考えると10.7%です。前回求めたRSIの最適値でのリターンが214%(年率13.5%)だったことを考えると、期待できそうな予感がします。 2011年〜2018年の8年間の最適な組み合わせを見つける 日経平均株価を使用して、2011年〜2018年の8年間の最適な組み合わせを見つけていきます。 各期間の範囲は下記のようにしました。 また、EMA長期期間 > EMA短期期間 となるように制約も入れています。 期間 最小値 最大値 EMA短期 5 50 EMA長期 5 50 MACDシグナル期間 5 50 bt = Backtest( df, MacdStrategy, cash=INIT_CASH, trade_on_close=False, exclusive_orders=True ) stats, heatmap = bt.optimize( fastperiod=range(5, 51), slowperiod=range(5, 51), signalperiod=range(5, 51), return_heatmap=True, constraint=lambda p: p.fastperiod < p.slowperiod) この期間の最適な組み合わせでのリターンは216%でした。8年間で216%なので、年率で考えると15.4%くらいになります。 Start 2011-01-04 00:00:00 End 2018-12-28 00:00:00 Duration 2915 days 00:00:00 Exposure Time [%] 97.1007 Equity Final [$] 3.16202e+06 Equity Peak [$] 3.6104e+06 Return [%] 216.202 Buy & Hold Return [%] 92.4849 Return (Ann.) [%] 15.9004 Volatility (Ann.) [%] 23.5399 Sharpe Ratio 0.675464 Sortino Ratio 1.12638 Calmar Ratio 0.599543 Max. Drawdown [%] -26.5209 Avg. Drawdown [%] -3.45087 Max. Drawdown Duration 484 days 00:00:00 Avg. Drawdown Duration 32 days 00:00:00 # Trades 145 Win Rate [%] 62.7586 Best Trade [%] 18.9492 Worst Trade [%] -10.4943 Avg. Trade [%] 0.799388 Max. Trade Duration 84 days 00:00:00 Avg. Trade Duration 20 days 00:00:00 Profit Factor 1.79997 Expectancy [%] 0.889616 SQN 2.0116 _strategy MacdStrategy(fas... _equity_curve ... _trades Size Entry... 最適な期間の組み合わせは、EMA短期=9日、EMA長期=48日、シグナル=5日 となりました。 MacdStrategy(fastperiod=9,slowperiod=48,signalperiod=5) 2019年〜2020年の2年間でシミュレートしてみる では、上記の最適値を使って2019年〜2020年をシミュレートしてみます。 MacdStrategyクラス内の期間を下記のように変更してプログラムを実行します。 class MacdStrategy(Strategy): ''' MACD Strategy ''' fastperiod = 9 slowperiod = 48 signalperiod = 5 (以下省略) 下記が実行結果です。リターンは54%と少なく感じますが、2年間なので年率に換算すると24%くらいになりかなり良い結果です。2019年〜2020年の間にはコロナによる株価の暴落もあったのですが、それもうまく乗り越えられたようです。 Start 2019-01-04 00:00:00 End 2020-12-30 00:00:00 Duration 726 days 00:00:00 Exposure Time [%] 87.3706 Equity Final [$] 1.54643e+06 Equity Peak [$] 1.54694e+06 Return [%] 54.643 Buy & Hold Return [%] 40.2936 Return (Ann.) [%] 25.5397 Volatility (Ann.) [%] 22.5301 Sharpe Ratio 1.13358 Sortino Ratio 2.23523 Calmar Ratio 1.62558 Max. Drawdown [%] -15.7111 Avg. Drawdown [%] -3.0758 Max. Drawdown Duration 194 days 00:00:00 Avg. Drawdown Duration 25 days 00:00:00 # Trades 27 Win Rate [%] 51.8519 Best Trade [%] 16.1861 Worst Trade [%] -6.4028 Avg. Trade [%] 1.63782 Max. Trade Duration 64 days 00:00:00 Avg. Trade Duration 24 days 00:00:00 Profit Factor 2.82347 Expectancy [%] 1.77853 SQN 1.72547 _strategy MacdStrategy _equity_curve ... _trades Size EntryB... 2010年〜2020年の10年間の最適な組み合わせを見つける では、10年間を通した最適な組み合わせはどうなるのでしょうか? 結果だけを載せます。 Start 2011-01-04 00:00:00 End 2020-12-30 00:00:00 Duration 3648 days 00:00:00 Exposure Time [%] 97.7134 Equity Final [$] 5.74037e+06 Equity Peak [$] 5.74225e+06 Return [%] 474.037 Buy & Hold Return [%] 163.934 Return (Ann.) [%] 19.7 Volatility (Ann.) [%] 23.7815 Sharpe Ratio 0.828377 Sortino Ratio 1.4699 Calmar Ratio 0.702859 Max. Drawdown [%] -28.0284 Avg. Drawdown [%] -2.88859 Max. Drawdown Duration 925 days 00:00:00 Avg. Drawdown Duration 32 days 00:00:00 # Trades 227 Win Rate [%] 55.9471 Best Trade [%] 22.0555 Worst Trade [%] -10.5505 Avg. Trade [%] 0.775317 Max. Trade Duration 80 days 00:00:00 Avg. Trade Duration 16 days 00:00:00 Profit Factor 1.92129 Expectancy [%] 0.856358 SQN 2.87072 _strategy MacdStrategy(fas... _equity_curve ... _trades Size Entry... なんとリターンは474%となりました。年率に換算すると19%です。 最適な期間の組み合わせは、EMA短期=5日、EMA長期=47日、シグナル=6日 となりました。 MacdStrategy(fastperiod=5,slowperiod=47,signalperiod=6) 最後に 時期や銘柄によって最適な組み合わせは変化すると思いますが、このMACDは活用できそうです。 MACDは人気のある指標だと言われていますが、その理由がわかったような気がします。 これまでの結果を下記にまとめておきます。 指標 リターン(10年) リターン(年率) 売買戦略 SMA 140% 9% 短期と長期のゴールデンクロス/デッドクロスで売買。 RSI 214% 12% 基準となる上限/下限を超えたときに売買。 MACD 474% 19% MACDとMACDシグナルのゴールデンクロス/デッドクロスで売買。MACDの符号が変わったときに売買。 ※年率は複利換算。 プログラムソースはGitHubに置いています。 MINKABのページ「MACDとは?特徴・使い方を基礎から応用まで徹底解説」の説明が詳しいですね。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ESP8266入門】ServoとMPU6050センサーで追跡カメラを制御する♪

Raspi Picoで先日同様な記事を書いたが、IoTをググっていると、esp8266が非常に多い印象を受ける。 そして、元々このあたりの計測にはesp8266などで十分らしいことがうすうす分かってきた。 しかも、今回は実施しないがWifiなども利用できるらしい。 ということで、今回esp8266でpicoでやった同じことをやってみたので、記録しておこうと思う。 ※因みに、esp32も同様なboard(より機能的)ということでどちらを使うべきかは以下の参考⓪を参照  また、ウワンも同じようにesp32をインストールしようとしたが、2個ともflash read error 1000が出てここでは比較できないのが残念です。 ※esp32においても、MicroPythonのインストールに成功したのでおまけに追記しました ちなみに、参考⓪は分かり易いサイトで初心者でも参考になるが、日本語サイトは「ほんとに初めてesp8266を触る人」にやさしいサイトが無い。 とはいえ、この記事は以下のサイトを参照した。 普通は、esp8266でプログラミングする場合は、参考①のようにArdiunoIDEを利用して、Ardiuno(C,C++like)言語で実装するようだ。 しかし、今回はPythonでプログラミングしたいので、Raspi4に参考②の方法でestoolをインストールし、直にbinを利用して書き込む方法でMicroPythonをインストールした。 ※この方法はMicroPython本家でもこの方法を示しているので、正統派と考えていいと思います 【参考】 ⓪ESP32 vs ESP8266 – Pros and Cons(長所と短所)@May 18, 2020 By Sara Santos ①Installing ESP8266 Board in Arduino IDE (Windows, Mac OS X, Linux) Arduino 日本語リファレンス ②esp8266でMicroPythonを動かす やったこと ・環境構築 ・MPU6050センサー利用とServo利用 ・追跡カメラを制御する ・環境構築 RasPi4環境 ESP8266に新しい環境を構築するためにbinをダウンロードしてきてesptoolでファームウェアを書き込みます。 そのために、esptoolを以下でインストールします。 $ pip install esptool ※esptoolはpython3環境でも動きました。 次に、ESP8266の古いファームウェアを消去します。 $ sudo esptool.py --port /dev/ttyUSB0 erase_flash esptool.py v3.0 Serial port /dev/ttyUSB0 Connecting.... Detecting chip type... ESP8266 Chip is ESP8266EX Features: WiFi Crystal is 26MHz MAC: ************** Uploading stub... Running stub... Stub running... Erasing flash (this may take a while)... Chip erase completed successfully in 2.5s Hard resetting via RTS pin... 以下のMicroPython.orgのサイトから、esp8266-20210418-v1.15.bin (elf, map)(03/05/21現在の安定最新MicroPythonバージョン)をダウンロードしてpiディレクトリ配下に配置します。 【参考】 ③Firmware for Generic ESP8266 module そして、以下のコマンドでesp8266に書き込みます。 $ sudo esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash --flash_size=32m 0 esp8266-20210418-v1.15.bin WARNING: Flash size arguments in megabits like '32m' are deprecated. Please use the equivalent size '4MB'. Megabit arguments may be removed in a future release. esptool.py v3.0 Serial port /dev/ttyUSB0 Connecting.... Detecting chip type... ESP8266 Chip is ESP8266EX Features: WiFi Crystal is 26MHz MAC: *************** Uploading stub... Running stub... Stub running... Changing baud rate to 460800 Changed. Configuring flash size... Flash params set to 0x0040 Compressed 632632 bytes to 415633... Wrote 632632 bytes (415633 compressed) at 0x00000000 in 9.6 seconds (effective 525.9 kbit/s)... Hash of data verified. Leaving... Hard resetting via RTS pin... ※単位をmegabitsじゃなくて、4MBとしてねとおこられていますが、無事に書き込めています シリアルモニタを開いて確認する。 $ sudo screen /dev/ttyUSB0 115200 esp8266のresetボタンを押すと、以下が表示されます。 ... MicroPython v1.15 on 2021-04-18; ESP module with ESP8266 Type "help()" for more information. >>> これで、通常のpythonと同じようにコマンドを入力すれば、逐次実行ができます。 >>> import time >>> print(time.time()) 310 >>> micropythonの開発環境であるThonnyをinstall Thonnyの開発環境は、いろいろなOs上で動くようにそれぞれ提供されています。 ※インストール方法は先日の記事のとおりからダウンロードしてインストールします(今回はWindows版を利用してみました)。 インストールして立ち上げたら、右下の言語選択からMicroPython(esp8266)を選択します。これでMicroPython deviceとして認識されて、deviceのディレクトリにアクセスできるようになります。(図の背景はご容赦下さい) ※ここで、ウワンがやった範囲だと、接続は以下の手順が確実なようです。 ①Thonnyを立ち上げる ②USBでboardを接続する ③openする、または新たなPGを作成して、saveする これで、接続確認と動くことが確認できると思います。 なお、installの状況により、Raspi4上のThonnyでは動くけど、Windowsでは動かないesp8266がありました。 Lチカをやってみます。 14番PinとLEDのプラス側、GNDをマイナス側に結線し、以下のコードを実行します。 import machine import time pin=machine.Pin(14, machine.Pin.OUT) while True: pin.on() time.sleep(1) pin.off() time.sleep(1) ・MPU6050センサー利用とServo利用 ここまで来るとあとは、picoでやったこととほとんど同じです。 異なるのは、以下のI2Cの定義でidが不要なようです。 self.i2c = I2C(scl = self.scl, sda = self.sda, freq = 100000) 一方、Servoモータも同じようなコードですが、本家に以下のような解説があります。 PWM can be enabled on all pins except Pin(16). There is a single frequency for all channels, with range between 1 and 1000 (measured in Hz). The duty cycle is between 0 and 1023 inclusive. 【参考】 PWM (pulse width modulation)@Quick reference for the ESP8266@MicroPython1.15 ということで、周波数は100Hzとし、動作範囲を見て、以下のように duty = int(150 - pitch) duty1 = int(150 - yaw) としています。pitchとyawは台座のx-軸とz-軸周りの回転角度です。 ・追跡カメラを制御する 今回は、回転角pitch,rollを加速度センサー、yaw(z軸周りの回転角)を角速度センサーから算出することとしました。 ※今回はpitchのみ利用(roll側の回転では動かない)し、yawの補正は原点補正のみとして、それ以外の積分補正も無く、動くかな程度のものです。 【参考】 6軸センサーから3軸回転の傾き角度算出とドリフト補正方法 コード from machine import Pin from machine import I2C from machine import PWM import time import math as mt class mpu6050: def __init__(self, scl, sda): self.scl = scl self.sda = sda self.i2c = I2C(scl = self.scl, sda = self.sda, freq = 100000) slv = self.i2c.scan() self.slvAddr = 104 # レジスタをリセットする self.writeByte(0x6B,0x80) time.sleep(0.1) # PWR_MGMT_1をクリア self.writeByte(0x6B,0x00) time.sleep(0.1) def readXYZ(self): data = self.readByte(0x3B ,6) x = (2.0 / 0x8000) * u2s(data[0] << 8 | data[1]) y = (2.0 / 0x8000) * u2s(data[2] << 8 | data[3]) z = (2.0 / 0x8000) * u2s(data[4] << 8 | data[5]) return (x,y,z) def readTemp(self): data = self.readByte(0x41 ,2) raw = data[0] << 8 | data[1] # 上位ビットが先 # Temperature in degrees C = (TEMP_OUT Register Value as a signed quantity)/340 + 36.53 temp= u2s(raw)/340 + 36.53 return temp def readGyro(self): data = self.readByte(0x43 ,6) x = (250 / 0x8000) * u2s(data[0] << 8 | data[1]) y = (250 / 0x8000) * u2s(data[2] << 8 | data[3]) z = (250 / 0x8000) * u2s(data[4] << 8 | data[5]) return (x,y,z) def writeByte(self, addr, data): d = bytearray([data]) self.i2c.writeto_mem(self.slvAddr, addr, d) def readByte(self, addr, num): s = self.i2c.readfrom_mem(self.slvAddr, addr, num) return s #unsignedを、signedに変換(16ビット限定) def u2s(unsigneddata): if unsigneddata & (0x01 << 15) : return -1 * ((unsigneddata ^ 0xffff) + 1) return unsigneddata def get_angle(rawX, rawY, rawZ): pitch = mt.degrees(mt.atan2(rawX, mt.sqrt(rawY*rawY+rawZ*rawZ))) roll = mt.degrees(mt.atan2(rawY, rawZ)) return pitch, roll if __name__ == "__main__": start = time.time() scl = Pin(5) sda = Pin(4) servo1 = PWM(Pin(0)) servo2 = PWM(Pin(12)) servo1.freq(100) servo2.freq(100) led_onboard = Pin(14, Pin.OUT) snsr = mpu6050(scl, sda) f = open('write1.txt', 'wb') f.write('t: pitch: zg_: ' + ' \n') xg_,yg_,zg_ = 0,0,0 x0,y0,z0 = snsr.readXYZ() z0 = z0 - 1 try: while True: t = time.time()- start #print(t) x1,y1,z1 = snsr.readXYZ() x, y, z = x1-x0,y1-y0,z1-z0 xg,yg,zg = snsr.readGyro() pitch, roll = get_angle(x,y,z) xg_ += xg yg_ += yg zg_ += zg #print('xg_,yg_,zg_=',xg_,yg_,zg_) print(zg,zg_,pitch) duty = int(150 - pitch) duty1 = int(150 - zg_) #int(roll) #print(duty, duty1) servo1.duty(duty1) servo2.duty(duty) led_onboard.on() f.write('{}'.format(t)+ ' ') f.write('{0:.2f} {1:.2f}'.format(pitch,zg_)+'\n') time.sleep(0.8) led_onboard.off() time.sleep(0.2) except KeyboardInterrupt: pass finally: f.close() pass 実行結果は以下のようになります。 そして、標準出力は以下のような数値になっています。 ※このうち、'write1.txt'に't: pitch: zg_: 'を保存しています。 標準出力 zg,    zg_,   pitch -0.160217 -0.160217 9.06498 0.556946 0.396729 7.57075 0.183105 0.579834 8.03654 0.213623 0.793457 8.50789 0.518799 1.31226 8.20365 0.457764 1.77002 7.50442 -0.0305176 1.7395 8.26609 -5.77545 -4.03595 8.01375 -0.167847 -4.2038 7.727 -6.23322 -10.437 6.7226 -0.816345 -11.2534 8.37546 0.236511 -11.0168 11.0136 0.259399 -10.7574 7.455 10.9787 0.221252 6.81365 12.085 12.3062 8.67728 7.18689 19.4931 8.86063 16.6397 36.1328 6.88407 15.7547 51.8875 6.88363 -13.0081 38.8794 7.73767 -11.8866 26.9928 7.83815 0.0457764 27.0386 8.67745 -0.0915527 26.947 27.1415 0.976563 27.9236 25.942 1.97601 29.8996 39.4848 -0.816345 29.0833 32.9303 0.709534 29.7928 19.0487 -4.15039 25.6424 14.0403 -0.411987 25.2304 9.2747 -1.51825 23.7122 -6.38299 -0.37384 23.3383 7.11945 -3.31879 20.0195 7.52914 -0.50354 19.516 1.76985 28.4348 47.9507 2.62209 7.71332 55.6641 2.18427 -14.7858 40.8783 3.15388 -1.30463 39.5737 5.20048 -9.89532 29.6783 5.69588 0.144958 29.8233 7.59914 0.25177 30.0751 6.01024 0.434875 30.5099 5.92602 0.274658 30.7846 5.6169 0.236511 31.0211 6.05784 -0.114441 30.9067 6.0632 0.579834 31.4865 6.41652 0.221252 31.7078 6.27217 0.328064 32.0358 6.28518 0.434875 32.4707 6.14895 0.152588 32.6233 6.21468 0.62561 33.2489 6.30875 0.350952 33.5999 6.10206 0.350952 33.9508 6.30138 この程度のものでも、一応ゆっくり動かせば、どうにかカメラが追跡して同一方向を向いてくれました。 まとめ ・esp8266にMicroPythonをインストールして環境構築を実施した ・PWMでServoを動かせた ・MPU6050を利用できた ・カメラの乗っている基盤のpitchとyawに基いて、追跡カメラが出来た ・RasPi4, RasPi Pico, esp8266が出来たので、esp32とRasPi zeroでも同様に実施してそれぞれの特徴を明らかにしようと思う ・倒立振り子を完成したいと思う おまけ ESP32のMicroPythonインストールもできました。 本家コマンドのとおりです. $ esptool.py --port /dev/ttyUSB0 erase_flash esptool.py v3.0 Serial port /dev/ttyUSB0 Connecting.... Detecting chip type... ESP32 Chip is ESP32-D0WDQ6 (revision 1) Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None Crystal is 40MHz MAC: ************** Uploading stub... Running stub... Stub running... Erasing flash (this may take a while)... Chip erase completed successfully in 15.7s Hard resetting via RTS pin... $ esptool.py --chip esp32 --port /dev/ttyUSB0 write_flash -z 0x1000 esp32-20210418-v1.15.bin esptool.py v3.0 Serial port /dev/ttyUSB0 Connecting...... Chip is ESP32-D0WDQ6 (revision 1) Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None Crystal is 40MHz MAC: ************** Uploading stub... Running stub... Stub running... Configuring flash size... Compressed 1469216 bytes to 953244... Wrote 1469216 bytes (953244 compressed) at 0x00001000 in 84.9 seconds (effective 138.5 kbit/s)... Hash of data verified. Leaving... Hard resetting via RTS pin... Thonny-MicroPython上で以下が出てきます. MicroPython v1.15 on 2021-04-18; ESP32 module with ESP32 Type "help()" for more information. >>>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シグモイド関数をグラフにしてみる

Jupyter を使ってやってみます。 import numpy as np import matplotlib.pyplot as plt def func_sigmoid(x): # シグモイド関数 return 1/(1+np.exp(-x)) def func_derivative_sigmoid(x): # シグモイド関数の導関数 y = func_sigmoid(x) return (1-y)*y x = np.linspace(-5, 5) y = func_sigmoid(x) y_derivative = func_derivative_sigmoid(x) plt.plot(x, y, label="sigmoid") plt.plot(x, y_derivative, label="derivative") plt.legend() plt.xlabel("x", size=14) plt.ylabel("y", size=14) plt.show()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

均一な乱数と正規分布に従う乱数をグラフにしてみる

Jupyter を使ってやってみます。 均一な乱数 import numpy as np import matplotlib.pyplot as plt n = 500 # サンプル数 x = np.random.rand(n) y = np.random.rand(n) plt.scatter(x, y) plt.grid() plt.show() 正規分布に従う乱数 import numpy as np import matplotlib.pyplot as plt n = 500 # サンプル数 x = np.random.randn(n) y = np.random.randn(n) plt.scatter(x, y) plt.grid() plt.show()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kivyMDチュートリアル其の什陸 Components - Card篇

みなさん、おはこんばんは。 GW君の勢いも大分後退してきましたが、いかがだったでしょうか。まだ、終わってないぞ! という方もいらっしゃるかもしれませんね。私のGW君はとっくに置き手紙を残し、どこかに 消え去りました。おそらく、来年あたりに帰ってくるのですかね。 まぁそういうことは一旦置いておいて、GW企画も幸いにして第3弾を迎えることが出来ました! ありがとうございます。誰に向かってお礼を言っているか分からないですが、とりあえず感謝を 述べておきます。 今日のお題は前回述べた通りで、Card篇となっています。これまでもいくつかのウィジェットを 扱ってきましたが、今日のテーマに関して言うと、とても協力な武器になり得るものです。GW企画 の3テーマで簡単なアプリが出来てしまうのではないでしょうか。RPGだと、中ボスくらいは倒せ そうな勢いです。 まぁ、そんな嘘かホントか分からない話は置いておいてさっそく取り掛かることにします。 Card 最初のマテリアルデザインはさることながら、概要が記載されています。 Cards contain content and actions about a single subject. あまり自信の精度の低い解釈を載せてしまうとダメなので、こちらも依頼をしてみます。 カードには、一つのテーマに関するコンテンツやアクションが含まれています。 あ、今回から「Google翻訳」君を取りやめて「DeepL」君に依頼先を変更しました。 だって、翻訳精度がいいって言うから... とまぁ、どうでもよいことは置いておいて、こちらに関しては馴染みが深みかもしれません。 Googleの検索結果の横方向にコンテンツが並べられたり、Google Mapの店の表示に使われたりで 枚挙にいとまがありません。こんな以外なところにも使われているよ!というのがありましたら 教えて下さいね。 で、KivyMDからは次のようなクラスらを提供するよということも書かれています。 MDCard MDCardSwipe で、さらにInfoとして次のようなことも触れられています。 MDCard inherited from BoxLayout. You can use all parameters and attributes of the BoxLayout class in the MDCard class. MDCardはBoxLayoutから継承しています。MDCardクラスでは、BoxLayoutクラスのすべての パラメータと属性を使用することができます。 結構大胆な翻訳をするなぁと思っていることは置いておいて、BoxLayoutクラスのように使える らしいです。このあたりはこのあとのコードの方を見てみましょうか。 今日については少しコードの列挙が多いので、いつもと進行を変えコード → 結果というように 記載していきます。ではレッツゴ。 MDCard xvi/card.py from kivy.lang import Builder from kivymd.app import MDApp KV =''' MDScreen: MDCard: size_hint: None, None size: "280dp", "180dp" pos_hint: {"center_x": .5, "center_y": .5} ''' class TestCard(MDApp): def build(self): return Builder.load_string(KV) TestCard().run() うーん、実にシンプル。確かに、BoxLayoutでも使用できるsize_hintやpos_hintなどが 使われています。特に目新しいものはないので、一旦結果の方を見てみます。 結果 存在感がありますね。では、さくさく次に進んでみましょう。 MDCard(add content) 次はカードにコンテンツを注入したものになります。依存性注入(DI)ではありません。 xvi/card_content.py from kivy.lang import Builder from kivymd.app import MDApp KV = ''' MDScreen: MDCard: orientation: "vertical" padding: "8dp" size_hint: None, None size: "70dp", "45dp" pos_hint: {"center_x": .5, "center_y": .5} MDLabel: text: "Title" theme_text_color: "Secondary" size_hint_y: None height: self.texture_size[1] MDSeparator: height: "1dp" MDLabel: text: "Body" ''' class TestCard(MDApp): def build(self): return Builder.load_string(KV) TestCard().run() さっきよりか、BoxLayoutの特徴が顕著ですね。orientationとか出るともう出ちゃって ますねーと言いたくなります。やってよかった、Layout篇。なんだか良く分からんと言う方は GW企画の中のLayout篇を一読願います。というかKivyのBoxLayoutのマニュアル見た方が 早いかも。 kivyMDチュートリアル其の什肆 Components - Layout篇 とくにここも目新しいものは少ないですが、sizeというプロパティがありますね。これは 見ての通りということと、マニュアルの記載がないので詳細は省略します。というか見ての 通りしかないですね。 あとは、いままでほったらかしにしてきたMDSeparatorに触れ込みたいと思います。仕様 としては以下になります。なぜかCardに紛れ込んで、記載されています。謎しかない。。 class kivymd.uix.card.MDSeparator(**kwargs) A separator line. color  Separator color in rgba format.  color is a ColorProperty and defaults to None. on_orientation(self, *args) ただ線を引くだけでなく、色も選べるのですね。あとは、on_orientationというメソッド? というものもありますね。なんにせよ、思わぬ武器が手に入りました。 結果 さて、結果の方を見てみましょう。 ただカードを表示するだけでは面白みがないので、サイズを1/4ほど縮めます。 # いやそれでも面白くねーよというツッコミは受け付けておりません 小っさっ!と言いたくなるほどですね。これはダメです。ダメよ〜ダメダメ。 懐かしいギャグをしたところで、ダメということを強調しておきます。というのもMaterial- -Designの仕様としてもこんな仕様は認めていません。NGパターンにもこんな例はあるはずも なく、コントリビューターが見ていたら開いた口が塞がらない状態に陥りそうです。どうか このページを見ていませんように。 意味はないかもしれませんが、念のため正規パターンも載せておきます。これはマニュアル 通りのコードです。 んー、ばっちし! MDCardSwipe こちらは、タイトルにある通りCardをスワイプしたり削除できたりするものになります。 使用方法と仕様が簡潔にマニュアルに記載されていますね。 To create a card with swipe-to-delete behavior, you must create a new class that inherits from the MDCardSwipe class: どうやら、このカードを使う場合はクラス宣言とkv側の両方を書かないとダメらしいです。 具体的な使用方法はマニュアル通りですが、以下みたいです。 <SwipeToDeleteItem>: size_hint_y: None height: content.height MDCardSwipeLayerBox: MDCardSwipeFrontBox: OneLineListItem: id: content text: root.text _no_ripple_effect: True class SwipeToDeleteItem(MDCardSwipe): text = StringProperty() クラス宣言から言うと、textプロパティはクラス側でもkv側でも用意した方が良い ということですね。理由としては後述のコードを見てもらった方がいいかもです。 kv側というと、なんかこのパターン見たことあるぞ!という方はKivyMDマスターです。(何様だ) そうです、そうなのです。Componentsのいきなり最初で洗礼を浴びせられたBackdrop篇です! そんなの知らねーよという方はマニュアルを見直すか以下リンクを参照ください。 kivyMDチュートリアル其の漆 Components - Backdrop篇 そのときと同様に、フロント・バックレイヤーを2つ用意してフロントの方にコンテンツを 仕込みます。バックレイヤーの方は、この後でも出てきますが削除ボタンなどを配置する こともあるようですね。とりあえず、良く分からんという方はこの形を覚えてもらえれば と思います。 で、あれこれ言ってもどうしようもないのでさっそくコードに入っていきましょう。 なんの変哲もないですが、マニュアルそのままのコードです。 xvi/card_swipe.py from kivy.lang import Builder from kivy.properties import StringProperty from kivymd.app import MDApp from kivymd.uix.card import MDCardSwipe KV = ''' <SwipeToDeleteItem>: size_hint_y: None height: content.height MDCardSwipeLayerBox: # Content under the card. MDCardSwipeFrontBox: # Content of card. OneLineListItem: id: content text: root.text _no_ripple_effect: True MDScreen: MDBoxLayout: orientation: "vertical" spacing: "10dp" MDToolbar: elevation: 10 title: "MDCardSwipe" ScrollView: scroll_timeout : 100 MDList: id: md_list padding: 0 ''' class SwipeToDeleteItem(MDCardSwipe): '''Card with `swipe-to-delete` behavior.''' text = StringProperty() class TestCard(MDApp): def __init__(self, **kwargs): super().__init__(**kwargs) self.screen = Builder.load_string(KV) def build(self): return self.screen def on_start(self): '''Creates a list of cards.''' for i in range(20): self.screen.ids.md_list.add_widget( SwipeToDeleteItem(text=f"One-line item {i}") ) TestCard().run() import 特に触れなくても良いかと思っていましたが、新たな発見というか当たり前の話ですが 上記のSwipeTo~クラス内で、以下のようにクラスを使うときというか以下は継承を使用 するときですが、import文を追加する必要があります。すみません、言わずもがなという ところかもです。 (略) from kivymd.uix.card import MDCardSwipe (略) class SwipeToDeleteItem(MDCardSwipe): '''Card with `swipe-to-delete` behavior.''' (略) kv側 本来ここも目新しいものは少なく、SwipeToDeleteItemのレイアウトも先述通りなので 取り上げる必要はないのですが、ポツンと見慣れないScrollViewが姿を現しています。 MDScreen: MDBoxLayout: (略) ScrollView: scroll_timeout : 100 (略) このscroll_timeoutってなんだろうなと思いマニュアルも見ていたのですが、以下リンク の解説がめちゃ×2分かりやすかったので参照しておきます。 # ありがとうございます、gotta_dive_into_pythonさん! Kivy小ネタ集(随時更新) - Scroll関係 念のため公式マニュアル(Kivy)の定義も触れておきます。 Timeout allowed to trigger the scroll_distance, in milliseconds. If the user has not moved scroll_distance within the timeout, the scrolling will be disabled, and the touch event will go to the children. scroll_timeout is a NumericProperty and defaults to 55 (milliseconds) according to the default value in user configuration. ScrollView https://kivy.org/doc/stable/api-kivy.uix.scrollview.html クラス側 SwipeToDeleteItemも先述の通りなので、再掲はしません。TestCardについては注目 するべき箇所は以下になります。 class TestCard(MDApp): (略) def on_start(self): '''Creates a list of cards.''' for i in range(20): self.screen.ids.md_list.add_widget( SwipeToDeleteItem(text=f"One-line item {i}") ) まぁ、これも絶対見たことありそうですが再掲をしました。kv側で宣言をしたmd_listが on_startメソッドで使用されています。今まで触ってこなかったですが、add_widgetに ついてもマニュアルから仕様を再確認したいと思います。 add_widget(self, widget, index=0, canvas=None) Add a new widget as a child of this widget.  Parameters widget: Widget          Widget to add to our list of children. ※ 他にもindexとかcanvasとかもありますが元バージョンではないということなので  省略しています 要はこれさえ記載されていれば何でも?ウィジェットを追加できるようですね。 コードの方でもカスタムクラスのSwipeToDeleteItemを生成してウィジェットを 20個分入れ込んでいます。 結果 ということでこれも結果を見てみましょう。 うーん、問題なさそうです。 思いきりスワイプをしても、現時点では削除が出来ません。 先述できていないところですが、以下のように記載を加えるとスワイプする方向を 変更できるようです。 <SwipeToDeleteItem>: # By default, the parameter is "left" anchor: "right" MDCardSwipe(Removing) コンテンツを残しておく前提であれば問題ないと思いますが、消去したい場合はどう しましょうか。まぁ、こんな遠い言い回ししなくても答えは出てますけどね。そう、 公式マニュアルならね。 すみません、調子乗りました。。大人しくマニュアルから使用方法など抜粋します。 Removing an item using the type_swipe = "auto" parameter The map provides the MDCardSwipe.on_swipe_complete event. You can use this event to remove items from a list: <SwipeToDeleteItem>: on_swipe_complete: app.on_swipe_complete(root) def on_swipe_complete(self, instance): self.screen.ids.md_list.remove_widget(instance) 上記のように指定すれば良いとのことです。on_swipe_completeでのコールバックメソッドの 引数にrootウィジェットごと渡していますね。この辺はmd_listが持っているremove_widget メソッドがごにょごにょやっているんだなと思うしかありません。 で、さっそくサンプルコードに入っていきたいですが、すべてコードを載せても重複している ところばかりなので先程のcard_swipe.pyからの差分を載せたいと思います。どこを変更 すれば良いのか分かりやすくなるかと思われます。 xvi/card_swipe_delete.py (略) KV = ''' <SwipeToDeleteItem>: size_hint_y: None height: content.height + type_swipe: "auto" + on_swipe_complete: app.on_swipe_complete(root) (略) def build(self): return self.screen + def on_swipe_complete(self, instance): + self.screen.ids.md_list.remove_widget(instance) def on_start(self): for i in range(20): self.screen.ids.md_list.add_widget( SwipeToDeleteItem(text=f"One-line item {i}") ) (略) まぁ、これも先述の使用方法通りですね。1つ変わっているのはtype_swipeプロパティが 追加されていることくらいですかね。これは任意プロパティ?と思われるかもですが、どちらか というとあった方がいいかもしれません。これをなくすと(type_swipe: "hand"に戻る)どう なるかというと、スワイプを途中で止めてカードを離すと逆方向に戻ってしまいます。ちょっと、 動作としては不自然に見えます。 [補足] 変更部分は他でもありますが、コメントの削除とかなので差分として出すのは本質ではない と思い、省略しています。ご了承のほどを。 結果 ではどうなるか見てみましょう。 初期画面は問題なさそうです。 スワイプする様子がないのは申し訳ないですが、スワイプしても問題なく消去できます。 上記画面としては忘備録に近い内容ですが、スワイプを途中で止めて下の方向にずらすと カードが途中で止まってしまいます。これもスマホ・タブレットとかだとどうなるか、 興味がある点になりますね。 MDCardSwipe(Add content to the bottom layer of the card) ちょっと、疲れてきたw うーん、後少しなのでがんばりマッスル。 という冗談を挟みながらですが、先述したバックレイヤーのほうにウィジェットを追加する方法です。 ここでは削除ボタンが題材ですが、SNSに共有など色々選択肢があるところです。 使用方法は以下になります。 To add content to the bottom layer of the card, use the MDCardSwipeLayerBox class. <SwipeToDeleteItem>: MDCardSwipeLayerBox: padding: "8dp" MDIconButton: icon: "trash-can" pos_hint: {"center_y": .5} on_release: app.remove_item(root) うーん、簡単ですね。バックレイヤーにボタンを追加するだけですね。ボタンもGW企画で 触っているしでバッチリですね!知らないという方は以下リンクを参照! kivyMDチュートリアル其の什伍 Components - Button篇 サンプルコードもcard_swipe_delete.pyから微小たる変更になります。 card_swipe_delete_trashicon.py (略) KV = ''' <SwipeToDeleteItem>: size_hint_y: None height: content.height - type_swipe: "auto" - on_swipe_complete: app.on_swipe_complete(root) MDCardSwipeLayerBox: padding: "8dp" + MDIconButton: + icon: "trash-can" + pos_hint: {"center_y": .5} + on_release: app.remove_item(root) (略) class TestCard(MDApp): def __init__(self, **kwargs): super().__init__(**kwargs) self.screen = Builder.load_string(KV) - def on_swipe_complete(self, instance): + def remove_item(self, instance): self.screen.ids.md_list.remove_widget(instance) (略) 必要がないということからtype_swipeなどプロパティがなくなっています。あとは ボタンの追加も先述通りですね。最後の差分としては、メソッドのリネームだけ。。 以上になります。 結果 では、結果の方を見てみましょう。 初期画面は問題ありません。 スワイプしても途中で止まったりすることはありません。 アイコンボタンを見事に消去されます。止まったりする心配事をなくすという意味では こちらの方がいいかもしれませんね。止まることがなければ選択基準はフラットになり ますが。 Focus behavior こちらについては体力の限界が見えてきたので(大げさ)、おまけということにしたいと 思います。コードの方も著作権がめんどくさいということでフリー画像に変更している くらいで特にそれ以外は変更していません。詳細を確認したい方はGitHubの方にて。 →card_starbutton.py 結果 実行結果は以下になります。 動かすためには、なにがしか画像ファイルを用意する必要があります。ないと動作しません。 うーん、可愛い。猫はいつみても可愛いですね。 少し画面サイズが大きいのは、スマホサイズにすると途端に文字などが読みにくくなる ためです。なのでスマホ基準に考えると、設計を注意しなければいけません。 あとは、Ripple behaviorが動作しなかったことも注意が必要です。 今後に期待ということでしょうか。それとも私が動作の仕方わかってないだけだったりして。 補足 最後のまとめに入る前に、全然マニュアル最下部の仕様に触れてなかったので、使用 したものについてはここで触れておきます。 class kivymd.uix.card.MDCard(**kwargs) focus_behavior Using focus when hovering over a card. focus_behavior is a BooleanProperty and defaults to False. ripple_behavior Use ripple effect for card. ripple_behavior is a BooleanProperty and defaults to False. class kivymd.uix.card.MDCardSwipe(**kw) on_swipe_complete Called when a swipe of card is completed. anchor Anchoring screen edge for card. Available options are: ‘left’, ‘right’. anchor is a OptionProperty and defaults to left. type_swipe Type of card opening when swipe. Shift the card to the edge or to a set position max_opened_x. Available options are: ‘auto’, ‘hand’. type_swipe is a OptionProperty and defaults to auto. まとめ んーーー、今日も長かったぁー! というどうでもよいことは良くて、いかがだったでしょうか。 少し予定よりもページが長くなったので、読み応えがあろうかと思われます。 せっかく身についたのですから、これから積極的に使っていきたいですね。 ということで、今日はこれにて終わろうと思います!また次回にて。 次回はDialogの続きのDropdown Itemに入っていく予定です。 それでは、ごきげんよう。 参照 Components » Card https://kivymd.readthedocs.io/en/latest/components/card/ 猫ジャーナル https://nekojournal.net/?p=2436 ScrollView https://kivy.org/doc/stable/api-kivy.uix.scrollview.html Kivy小ネタ集(随時更新) - Scroll関係
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python で数学グラフを表示してみる

Jupyter を使ってやってみます。 べき乗 $y=x^5$ import numpy as np import matplotlib.pyplot as plt def exponentiation(x): a = 5 return x**a # xのa乗 x = np.linspace(0, 10) y = exponentiation(x) # y = f(x) plt.plot(x, y) plt.xlabel("x", size=14) plt.ylabel("y", size=14) plt.grid() plt.show() 平方根 $y=\sqrt x$ import numpy as np import matplotlib.pyplot as plt def square_root(x): return np.sqrt(x) # xの正の平方根 x = np.linspace(0, 100) y = square_root(x) # y = f(x) plt.plot(x, y) plt.xlabel("x", size=14) plt.ylabel("y", size=14) plt.grid() plt.show() 多項式 $y=4x^3+3x^2+2x+1$ import numpy as np import matplotlib.pyplot as plt def polynomial(x): return 4*x**3 + 3*x**2 + 2*x +1 x = np.linspace(-10, 10) y = polynomial(x) # y = f(x) plt.plot(x, y) plt.xlabel("x", size=14) plt.ylabel("y", size=14) plt.grid() plt.show() 三角関数 $y=\sin x$ $y=\cos x$ $y=\tan x$ import numpy as np import matplotlib.pyplot as plt def sin(x): return np.sin(x) # sin(x) def cos(x): return np.cos(x) # cos(x) def tan(x): return np.tan(x) # tan(x) x = np.linspace(-2*np.pi, 2*np.pi) # xの範囲を指定 y_sin = sin(x) y_cos = cos(x) y_tan = tan(x)/np.pi/10 plt.plot(x, y_sin, label="sin") plt.plot(x, y_cos, label="cos") plt.plot(x, y_tan, label="tan") plt.legend() plt.xlabel("x", size=14) plt.ylabel("y", size=14) plt.grid() plt.show()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djangoのas_viewについて

今回やること 前回TemplateViewの流れをまとめた中でas_viewあたりが分かりにくかったのでまとめてみた。 実装を見てみる as_viewのところのおおまかな実装 django.views.generic.base.py ... @classonlymethod def as_view(cls, **initkwargs): ... def view(request, *args, **kwargs): self = cls(**initkwargs) self.setup(request, *arg, **kwargs) return self.dispatch(request, *args, **kwargs) return view classonlymethodというのが意味不明のため、 まずこのclassonlymethodというデコレータから追ってみる。 classonlymethodとは? 名前からするとクラスのみのメソッド?のようだが実装から見てみる。 django.utils.decoraters.py class classonlymethod(classmethod): def __get__(self, instance, cls=None): if instance is not None: raise AttributeError('...') return super().__get__(instance, cls) 実装はこのようになっており、getメソッドが呼ばれた時にinstanceが空じゃなかったらエラーを投げるようになっているだけ。 じゃあこのgetメソッドについて公式ドキュメントから引用してみる。 object.get(self, instance, owner=None) Called to get the attribute of the owner class (class attribute access) or of an instance of that class (instance attribute access). The optional owner argument is the owner class, while instance is the instance that the attribute was accessed through, or None when the attribute is accessed through the owner. 所有者クラスの属性にアクセスされた時か、そのクラスのインスタンスを取得する時に呼ばれる。引数のownerは所有者クラス。 もう1つの引数のinstanceは、インスタンスからアクセスされた時は所有者クラスが入り、所有者クラスからアクセスされた時はNoneが入る。 インスタンス束縛 オブジェクトインスタンスへ束縛すると、a.xは呼び出し、type(a).dict['x'].get(a, type(a))に変換されます。 クラス束縛 クラスへ束縛すると、A.xは呼び出し、A.dict['x'].get(None, A)に変換されます。 つまり、インスタンスからアクセスした時には、第2引数に所有者クラスが入ってしまうため、上記のclassonlymethodではエラーを投げ、所有者クラスからのアクセスのみ通すような実装になっている。 つまり、なんちゃらView.as_view()といった呼び出し方だけを通すようにするデコレータだと思われる。一応挙動を確認してみた。 view.py class classonlymethod(classmethod): def __get__(self, instance, cls=None): print('self', self) print('instance', instance) print('cls', cls) if instance is not None: raise AttributeError('Error') return super().__get__(instance, cls) class View: @classonlymethod def as_view(cls, **initkwargs): pass class TestView(View): pass print('クラスからアクセス') TestView.as_view() print('インスタンスからアクセス') t = TestView() t.as_view() # クラスからアクセス # self <__main__.classonlymethod object at ...> # instance None # cls <class '__main__.TestView'> # インスタンスからアクセス # self <__main__.classonlymethod object at ...> # instance <__main__.TestView object at ...> # cls <class '__main__.TestView'> # AttributeError: Error といった感じの動作でメソッド名の通り、クラスからアクセスした時のみ通るようになっていた。 ではas_viewの中身を見ていく。 django.views.generic.base.py ... @classonlymethod def as_view(cls, **initkwargs): # バリデーションとか ... def view(request, *args, **kwargs): self = cls(**initkwargs) self.setup(request, *arg, **kwargs) if not hasattr(self, 'request'): raise AttributeError('...') return self.dispatch(request, *args, **kwargs) # 属性らセット ... return view といったようにas_viewは、viewメソッドを返すエンクロージャーになっているみたい。 viewメソッドでは以下の処理が実装されている。 1. インスタンス化 2. setup 3. 検証 4. dispatchメソッドを返す 1. インスタンス化 classonlymethodによって、クラスからしかアクセスさせないため、ここには呼び出したクラスがそのまま入り、そのクラスをインスタンス化している。 TemplateView.as_view()だったら、TemplateViewがそのまま入ってきてself = TemplateView(**initkwargs)というようにインスタンス化する。 2. setup リクエストと引数をインスタンスにセットしている。 django.views.generic.base.py def setup(self, request, *args, **kwargs): is hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.request = request self.args = args self.kwargs = kwargs 3. 検証 setupメソッドによってインスタンスにrequestがセットされなかった場合、AttributeErrorが投げられる。 4. dispatchメソッドを返す django.views.generic.base.py http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] ... def dispatch(self, request, *args, **kwargs): if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs) def http_method_not_allowed(self, request, *args, **kwargs): ... return HttpResponseNotAllowed(self._allowed_methods()) def _allowed_methods(self): return [m.upper() for m in self.http_method_names if hasattr(self, m)] 許容しているHTTPメソッドリストに存在するか、そのメソッドをViewが持っているか確認し、OKの場合はそのメソッドを返す。NGの場合はエラーを投げる。 TemplateViewだったらgetだったら許容リストに存在し、実装もされているためhandlerはgetメソッドになり、getメソッドが返る。 urls.pyでas_viewを書いてからの流れ ① urls.pyでas_viewで宣言 urls.py ... path('', views.TemplateView.as_view(), name='index'), ... と宣言する事で、TemplateViewを元にas_viewが動き検証と属性類をセットし、view関数により、渡ってきたリクエストのメソッド名と同じメソッドが呼ばれるhandlerが指定される。 ② リクエストが来てから そして、いざgetメソッドでリクエストが渡ってきたら、許容されているメソッドか、TemplateViewに実装されているかなどを確認した後、OKであればgetメソッドが動き、レスポンスが返る。 まとめ まとめてみようとする事で、改めて理解が深まった気がする?・・・ 作った人尊敬します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python boot camp day11

Mission>> Black Jack game <Rules> ※難易度調整のため一部改変 1. 山札は無限大 (同じカードを重複して引くことがある) 2. ジョーカーはなし 3. "A"は1or11,Jack/Queen/Kingは10としてカウントする 4. cards = [11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10] 5. 山札からカードを引いてもデッキから削除されることなく、常に同じカードが山札に存在する BJ.py #from art import logo import random from replit import clear def deal_card(): """returns a random card from a deck""" #deal_card()のDocstring cards = [11,2,3,4,5,6,7,8,9,10,10,10,10] card = random.choice(cards) return card def cal_score(cards): """check whether BlackJack and Ace card then returns total score""" if sum(cards) == 21 and len(cards) == 2: return 0 #score=0をBlackJackとする if 11 in cards and sum(cards) > 21: #"A"(=11)を引いた際にover21なら1に変換 cards.remove(11) cards.append(1) return sum(cards) def compare(user_score,dealer_score): """compare score and returns a message""" if user_score == dealer_score: return "DRAW!(^^)!" elif dealer_score == 0: return "Lose, opponent has BLACKJACK" elif user_score == 0: return "You win with a BLACKJACK." elif user_score > 21: return "You are over 21...You lose!!" elif dealer_score > 21: return "You Win!! Opponent is over 21." elif user_score > dealer_score: return "You win!!" else: return "You lose..." def play_game(): """controls game""" #print(logo) user_cards = [] dealer_cards = [] is_game_over = False #gameから抜ける for i in range(2): user_cards.append(deal_card()) dealer_cards.append(deal_card()) while not is_game_over: #for the user loop user_score = cal_score(user_cards) dealer_score = cal_score(dealer_cards) if len(user_cards) < 3: if user_score == 0: #このままだとBlackJackの時にCurrent scoreに0が入ってしまうので、 user_score = 21 #21に変更しておく print(f"\nNOW...\nYour cards: {user_cards}\t\tCurrent score: {user_score}\nDealer's first card:{dealer_cards[0]}") else: print(f"\n>> You added {user_cards[-1]}\n\nNOW...\nYour cards: {user_cards}\t\tCurrent score: {user_score}\nDealer's first card: {dealer_cards[0]}") if user_score == 21 or dealer_score == 0 or user_score > 21: is_game_over = True else: again = input("HIT?? [y/n] ") if not again == "n": user_cards.append(deal_card()) else: is_game_over = True while dealer_score != 21 and dealer_score < 17: #for dealer loop dealer_cards.append(deal_card()) dealer_score = cal_score(dealer_cards) if dealer_score == 0: #ここも、BlackJackの際にCurrent scoreに0ではなく21を入れる dealer_score = 21 print(f"\nDealer final hand:{dealer_cards}\t\tDealer's final score:{dealer_score}\n\nRESULT...") result = compare(user_score,dealer_score) line = "+"*len(result) print(f"++++{line}\n| {result} |\n{line}++++") while not input("\nDou you want to play a BLACKJACK?? [y/n] ") == "n": clear() play_game() else: print("Bye...") 出力結果 NOW... Your cards: [10, 10] Current score: 20 Dealer's first card:10 HIT?? [y/n] y >> You added 8 NOW... Your cards: [10, 10, 8] Current score: 28 Dealer's first card: 10 Dealer final hand:[10, 6, 8] Dealer's final score:24 RESULT... ++++++++++++++++++++++++++++++++ | You are over 21...You lose!! | ++++++++++++++++++++++++++++++++ Dou you want to play a BLACKJACK?? [y/n]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

有向非巡回グラフ(Directed Acyclic Graph)

概要 有向非巡回グラフ(Directed Acyclic Graph: DAG)とは、閉路のない有向グラフのことである。サイクルが存在しないため、ある頂点から同じ頂点に戻ってこれないという特徴がある。 有向非巡回グラフは要素の因果関係や物事の依存関係をモデル化するのに有効である。例えばあるタスクを行う前に別のタスクを完了させておく必要がある場合、タスクを頂点、順序の制約を有向辺とみなすと、各タスクの依存関係を有向非巡回グラフで表現できる。トポロジカルソートを使うと、制約条件に従ってタスクを完了させるための適当な順序を得ることができる。ただしジャンケンのグー・チョキ・パーのように依存関係(強さの順序)がサイクルを形成するような場合、有効非巡回グラフではなくなるためトポロジカルソート(順序付け)を行うことはできない。 例題 prerequisites[i] = [a, b]が与えられたとき、コースaを受講するには事前にコースbを受講しなければならないとします。例えば[0, 1]のペアは、コース0を受講するためには、まずコース1を受講しなければならないことを示しています。与えられた制約条件に従って全てのコースを終了することが可能な場合はtrueを、不可能な場合はfalseを返してください。 出典:https://leetcode.com/problems/course-schedule/ 考察 この問題は一種のグラフ探索問題としてモデル化することができます。ここでは各コースをグラフの頂点とみなし、コース間の依存関係を2つの頂点間の有向エッジとしてモデル化します。そして、すべての依存関係(制約条件)を満たす有効なコースのスケジュールを構築できるかどうかを判断するということは、対応するグラフがDAG(Directed Acyclic Graph)であるかどうか、つまりグラフにサイクルが存在しないかどうかを判断することと等価です。 グラフ探索問題の典型的な戦略に、DFS(深さ優先探索)の一種であるバックトラックがあります。バックトラックは制約充足問題を解くのによく使われる一般的なアルゴリズムで、解の候補を段階的に構築し、その候補が有効な解をもたらさないと判断した時点で候補を放棄する(バックトラックする)というものです。 各コースの環状依存性のチェックは、バックトラックによって行うことができます。ここでは、受講可能なコースがなくなるか、以前受講したはずのコースが再び現れる(サイクルを検出する)まで、グラフを段階的に探索していきます。 実装例 from collections import defaultdict class Solution(object): def canFinish(self, numCourses, prerequisites): """ :type numCourses: int :type prerequisites: List[List[int]] :rtype: bool """ def hasCycle(node, visited): """ :param node: 現在訪れているノード :param visited: 過去に訪れたノードの集合 :return: bool (閉路が存在するか否か) """ # すでに保存された結果があればそれを返す if node in checked: return checked[node] # すでに訪れたnodeに再会(閉路を検出) if node in visited: return True else: visited.add(node) result = False # 隣接するノードに対して再帰的に探索を行う for adj in g[node]: if hasCycle(adj, visited): result = True break checked[node] = result return checked[node] g = defaultdict(list) checked = {} # 効率化のため、結果を保存しておく # 有向グラフ(隣接リスト)を生成 for course in prerequisites: a, b = course[0], course[1] g[b].append(a) # グラフの各頂点から探索を開始し、閉路が存在するかどうか調べる for node in list(g): if hasCycle(node, set()): return False return True 補足 アルゴリズムの全体的な構造は、以下の3つのステップで構成されています。 Step 1 与えられたコースの依存関係のリストから、グラフデータ構造を構築します。ここでは、グラフを表現するために隣接リストを採用しています。これはハッシュマップや辞書で実装することができます。隣接リストのkeyは各コースを表し、そのコースを終了後に受講できるコースのリストを値として保持します。 Step 2 次に構築されたグラフの各ノード(コース)を列挙し,そのノードから探索を開始した際に閉路を検出できるかを確認します。 Step 3 以前探索した経路を再度探索するのは無駄なので、動的計画法の要領で結果をcheckedに保存しておき、探索済みの場合はそれを返すようにします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

有向非巡回グラフ(DAG)とトポロジカルソート

有向非巡回グラフ(DAG) 有向非巡回グラフ(Directed Acyclic Graph: DAG)とは、閉路のない有向グラフのことである。サイクルが存在しないため、ある頂点から同じ頂点に戻ってこれないという特徴がある。 有向非巡回グラフは要素の因果関係や物事の依存関係をモデル化するのに有効である。例えばあるタスクを行う前に別のタスクを完了させておく必要がある場合、タスクを頂点、順序の制約を有向辺とみなすと、各タスクの依存関係を有向非巡回グラフで表現することができる。また後述するトポロジカルソートを使用することで、制約条件に従ってタスクを完了させるための適当な順序を得ることができる。ただしジャンケンのグー・チョキ・パーのように依存関係(強さの順序)がサイクルを形成するような場合、巡回グラフとなるため順序付け(トポロジカルソート)を行うことはできない。 トポロジカルソート トポロジカルソート(Topological sort)は、有向非巡回グラフ(DAG)の各ノードを順序付けして、各ノードをその出力辺の先のノードより前にくるように並べることである。グラフがトポロジカルソート可能であることは、そのグラフが有向非巡回グラフであることと等価である。トポロジカルソートの活用例としては、Job scheduling、論理合成、Makefile等でのファイルのコンパイル順序の決定、リンカのシンボル依存関係の解決などが挙げられる。 例題 1 prerequisites[i] = [A, B]が与えられたとき、コースAを受講するには事前にコースBを受講しなければならないとします。例えば[0, 1]のペアは、コース0を受講するためには、まずコース1を受講しなければならないことを示しています。与えられた制約条件に従って全てのコースを終了することが可能な場合はtrueを、不可能な場合はfalseを返してください。 出典:https://leetcode.com/problems/course-schedule/ 考察 この問題は一種のグラフ探索問題としてモデル化することができます。ここでは各コースをグラフの頂点とみなし、コース間の依存関係を2つの頂点間の有向エッジとしてモデル化します。そして、すべての依存関係(制約条件)を満たす有効なコースのスケジュールを構築できるかどうかを判断するということは、対応するグラフがDAG(Directed Acyclic Graph)であるかどうか、つまりグラフにサイクルが存在しないかどうかを判断することと等価です。 グラフ探索問題の典型的な戦略に、DFS(深さ優先探索)の一種であるバックトラックがあります。バックトラックは制約充足問題を解くのによく使われる一般的なアルゴリズムで、解の候補を段階的に構築し、その候補が有効な解をもたらさないと判断した時点で候補を放棄する(バックトラックする)というものです。 各コースの環状依存性のチェックは、バックトラックによって行うことができます。ここでは、受講可能なコースがなくなるか、以前受講したはずのコースが再び現れる(サイクルを検出する)まで、グラフを段階的に探索していきます。 実装例 from collections import defaultdict class Solution(object): def canFinish(self, numCourses, prerequisites): """ :type numCourses: int :type prerequisites: List[List[int]] :rtype: bool """ def hasCycle(node, visited): """ :param node: 現在訪れているノード :param visited: 過去に訪れたノードの集合 :return: bool (閉路が存在するか否か) """ # すでに保存された結果があればそれを返す if node in checked: return checked[node] # すでに訪れたnodeに再会(閉路を検出) if node in visited: return True else: visited.add(node) result = False # 隣接するノードに対して再帰的に探索を行う for adj in g[node]: if hasCycle(adj, visited): result = True break checked[node] = result return checked[node] g = defaultdict(list) checked = {} # 効率化のため、結果を保存しておく # 有向グラフ(隣接リスト)を生成 for course in prerequisites: a, b = course[0], course[1] g[b].append(a) # グラフの各頂点から探索を開始し、閉路が存在するかどうか調べる for node in list(g): if hasCycle(node, set()): return False return True 補足 アルゴリズムの全体的な構造は、以下の3つのステップで構成されています。 Step 1 与えられたコースの依存関係のリストから、グラフデータ構造を構築します。ここでは、グラフを表現するために隣接リストを採用しています。これはハッシュマップや辞書で実装することができます。隣接リストのkeyは各コースを表し、そのコースを終了後に受講できるコースのリストを値として保持します。 Step 2 次に構築されたグラフの各ノード(コース)を列挙し,そのノードから探索を開始した際に閉路を検出できるかを確認します。 Step 3 以前探索した経路を再度探索するのは無駄なので、動的計画法の要領で結果をcheckedに保存しておき、探索済みの場合はそれを返すようにします。 例題 2 prerequisites[i] = [A, B]が与えられたとき、コースAを受講するには事前にコースBを受講しなければならないとします。例えば[0, 1]のペアは、コース0を受講するためには、まずコース1を受講しなければならないことを示しています。与えられた制約条件を満たしつつ、全てのコースを受講可能な順序を返してください。複数の解が存在する場合は、そのうちのひとつを返すものとします。条件を満たす順序が存在しない場合、空の配列を返してください。 出典: https://leetcode.com/problems/course-schedule-ii/ 考察 入力辺(In-degree)を利用したトポロジカルソート トポロジカルソート順の最初のノードは、入力辺を持たないノードになります。基本的に、入力辺(in-degree)の数が0のノードは、トポロジカルソートされた順序の最初のノードになり得ます。このようなノードが複数ある場合、その相対的な順序は問題にならず、どれから始めても構いません。 まず、in-degreeが0のノード(コース)をすべて受講します。これは、前提条件となるコースがないコースを真っ先に受講することを意味します。次にこれらのコースをグラフから削除し、それらから発生している辺も削除すると、次に処理されるべきコースを見つけることができます。これもまた、入力辺が0のノードです。すべてのコースが処理されるまで、この作業を続けます。 実装例 from collections import deque, defaultdict class Solution(object): def findOrder(self, numCourses, prerequisites): # 入力辺、出力辺のそれぞれに対してグラフを用意する in_degrees, out_degrees = defaultdict(list), defaultdict(list) arr, s = [], set(list(range(numCourses))) for edge in prerequisites: dst, src = edge[0], edge[1] out_degrees[src].append(dst) in_degrees[dst].append(src) # 入力辺を持つノードを除外 if dst in s: s.remove(dst) # 入力辺を持たないノードをqに入れる q = deque(list(s)) # 入力辺を持たないノードがなくなるまで繰り返す while q: node = q.popleft() arr.append(node) # 隣接ノードを走査 for adj in list(out_degrees[node]): # nodeからの入力辺を削除 in_degrees[adj].remove(node) # 隣接ノードの入力辺の数が0になった場合、qに追加する if len(in_degrees[adj]) == 0: q.append(adj) # nodeをグラフから削除 del out_degrees[node] # グラフから全てのノードが削除されていればトポロジカルソート完了 if len(out_degrees) == 0: return arr # グラフに入力辺を持つノードが残っている場合、閉路が存在する(トポロジカルソート不可) else: return [] 方針 入力辺の数が0のノードをキューに追加 キューから先頭ノードを取り出し、ソート順序に追加 先頭ノードの隣接ノードを走査し、先頭ノードからの入力辺を取り除く 隣接ノードが持つ入力辺の数が0になった場合、その隣接ノードをキューに追加 先頭ノードをグラフから削除 キューの中身がなくなるまで2~5を繰り返す 上記の操作の完了後、 グラフから全てのノードが取り除かれていればトポロジカルソート成功、グラフにノードが残っていればトポロジカルソート失敗となる。 補足 実装例ではキュー(deque)を用いたが、Stackを用いても構いません。その場合トポロジカルソート順は異なるものになりますが、いずれにせよ制約を満たす順序が得られるはずです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Lambdaのログのタイムゾーンをいい感じにしたい

背景 Lambda Powertoolsを使ってログ出力していたが、タイムゾーンがUTCになっていたのでこれをどうにかしたい、という要望が出た。 どうにかしたい、という微妙な言い回しになっているのは、 タイムゾーンをJSTに変更する UTCのままでタイムゾーン情報を追加で出力する という2パターンが選択肢として存在したため。最終的には後者で終えた。理由は後段で述べる。 Lambda Powertools の簡単な使い方 前提知識としてざっくり書いておく。以下の様にデコレータをつけると詳細なログを出してくれる。 from aws_lambda_powertools import Logger logger = Logger() @logger.inject_lambda_context def handler(event, context): logger.info("This is an information") ... logger.error("This is an error") 出力される情報は、関数名、メモリサイズ、コールドスタートかどうか、などなど。 詳しくはドキュメント参照。 タイムゾーン情報を付加して出力する方法 前述したようにタイムゾーンはUTCのまま、タイムゾーン情報を付加する形で落ち着いた。 実装方法は、loggerの初期化部分で以下のようにするだけ。 logger = Logger(datefmt="%Y-%m-%d %H:%M:%S %z") こうすることで、出力の形式は以下のようになる。 "2021-05-08 15:50:22 +0000" 一応タイムゾーンを変更する方法もあったのだが(後述)、内部変数をいじるような形になってしまったため今回は不採用。 とりあえず、プロジェクトに関わる人の中で合意が取れるところに落とし込んだ。 Timezoneを変更する方法 以下のようにすれば変更できる。pytzはインストール済みの想定。 from datetime import datetime from aws_lambda_powertools import Logger from pytz import timzeon def custome_time(*arg): return datetime.now(timezone("Asia/Tokyo")).timetuple() logger = Logger() logger._handler.formatter.converter = custome_time 前述の通り内部変数をいじる形になってしまう。 (他にいい方法あるのだろうか...?) Lambda Powertoolsを使わない場合 単純にloggingライブラリを使う場合は以下のようにできる。 今回はLambda Powertoolsの詳細なログを捨ててまでタイムゾーン変更をする強い要望はなかったのでこちらの方法は採用していない。 from datetime import datetime import logging import pytz def custome_time(*arg): return datetime.now(timzeone("Asia/Tokyo")).timetuple() logger = logging.getLogger(__name__) handler = logging.StreamHandler() formatter = logging.Formatter() formatter.converter = custome_time handler.setFormatter(formatter) logger.addhandler(handler) 前述したLambda Powertoolsでタイムゾーン変更はこの手順を無理矢理やっているだけ。 ちなみに環境変数TZをいじるという方法もあるが... この記事によるとTZを変更するのは非推奨らしい。 参考リンク
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Raspberry Piで困ったことを書いていく

はじめに 気温とか気圧とか色々記録したいなぁなどと 漠然とした理由から色々な製品を調べていたのですが 自分の用途にあった丁度良い製品が見当たらない。無いなら作ればよい! ということで今更ですがRaspberry Piをはじめてみました。 本記事ではRaspberry Piを触っていて困ったことを書き溜めていきます。 (困ったことが発生した際に本記事に追加更新していきます。) 使用している環境 Raspberry Pi 4 MODEL B SUNFOUNDER Sensor Kit V2.0 for Raspberry Pi Python3 以下困ったこと集 Barometer BMP180のサンプルプログラム(31_barometer.py)がエラーで実行できない1 事象 以下のエラーが発生する $ sudo python3 31_barometer.py Barometer begins... Traceback (most recent call last): File "31_barometer.py", line 42, in <module> loop() File "31_barometer.py", line 26, in loop sensor = BMP085.BMP085() File "/usr/local/lib/python3.7/dist-packages/Adafruit_BMP-1.5.0-py3.7.egg/Adafruit_BMP/BMP085.py", line 66, in __init__ File "/usr/local/lib/python3.7/dist-packages/Adafruit_GPIO-1.0.4-py3.7.egg/Adafruit_GPIO/I2C.py", line 66, in get_i2c_device File "/usr/local/lib/python3.7/dist-packages/Adafruit_GPIO-1.0.4-py3.7.egg/Adafruit_GPIO/I2C.py", line 99, in __init__ File "/usr/local/lib/python3.7/dist-packages/Adafruit_PureIO-1.1.8-py3.7.egg/Adafruit_PureIO/smbus.py", line 125, in __init__ File "/usr/local/lib/python3.7/dist-packages/Adafruit_PureIO-1.1.8-py3.7.egg/Adafruit_PureIO/smbus.py", line 150, in open FileNotFoundError: [Errno 2] No such file or directory: '/dev/i2c-1' 原因 I2Cが有効化されていない 対応 I2Cを有効化してOSを再起動する Raspberry Piのコンフィグを開く $ sudo raspi-config 3 Interface Options > P5 I2Cを選択する。1 Would you like the ARM I2C interface to be enabled? で<はい>を選択する。 The ARM I2C interface is enabled で<了解>を選択する。 <Finish>で閉じる。 Raspberry Piを再起動する。 $ sudo reboot Barometer BMP180のサンプルプログラム(31_barometer.py)がエラーで実行できない2 事象 以下のエラーが発生する $ sudo python3 31_barometer.py Barometer begins... Traceback (most recent call last): File "31_barometer.py", line 42, in <module> loop() File "31_barometer.py", line 27, in loop temp = sensor.read_temperature() # Read temperature to veriable temp File "/usr/local/lib/python3.7/dist-packages/Adafruit_BMP-1.5.0-py3.7.egg/Adafruit_BMP/BMP085.py", line 144, in read_temperature TypeError: unsupported operand type(s) for >>: 'float' and 'int' 原因 BMP085.pyで式の要素の型が合っていない 対応 BMP085.pyを修正(式の要素に型を指定)する エラーの内容より実行しているファイルが /usr/local/lib/python3.7/dist-packages/Adafruit_BMP-1.5.0-py3.7.egg/Adafruit_BMP/BMP085.pyであることがわかります。 こちらはeggファイルなので、このままですと修正するのが面倒くさいため Adafruit_BMP-1.5.0-py3.7.eggを展開し配下のAdafruit_BMPフォルダを 以下のようにサンプルプログラムと同じフォルダにコピーします。 こうすることでPythonは親スクリプト(サンプルプログラム)の実行パスと同一パスのファイルから 優先して読み込まれるためコピー後のBMP085.pyが読み込まれます。 [/home/pi/SunFounder_SensorKit_for_RPi2/Python]  ├31_barometer.py  └[Adafruit_BMP]   ├__init__.py   └BMP085.py つぎにコピーしたBMP085.pyを修正します。 エラーの内容では144行目がエラーであるとなっていますがこれはあくまでも 一番はじめにヒットしたエラー箇所であり 実は同種のエラー箇所が複数あります。 そのため以下のように複数行修正する必要があります。 いずれも明示的に変数をint型にするよう修正しています。 行数 修正前 修正後 144 temp = ((B5 + 8) >> 4) / 10.0 temp = (int(B5 + 8) >> 4) / 10.0 164 X1 = (self.cal_B2 * (B6 * B6) >> 12) >> 11 X1 = (self.cal_B2 * int(B6 * B6) >> 12) >> 11 165 X2 = (self.cal_AC2 * B6) >> 11 X2 = (self.cal_AC2 * int(B6)) >> 11 169 X1 = (self.cal_AC3 * B6) >> 13 X1 = (self.cal_AC3 * int(B6)) >> 13 170 X2 = (self.cal_B1 * ((B6 * B6) >> 12)) >> 16 X2 = (self.cal_B1 * (int(B6 * B6) >> 12)) >> 16 180 X1 = (p >> 8) * p >> 8) X1 = (int(p) >> 8) * (int(p) >> 8) 182 X2 = (-7357 * p) >> 16 X2 = (-7357 * int(p)) >> 16 自分はPythonを少しかじっていたのでわかりましたが初心者殺しだよなぁと^^; それではよきラズパイライフを! Raspberry Piのバージョンによりコンフィグのメニューが微妙に違います。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Macでpython3系の環境構築を数分で完了させるためのメモ

標準pythonのバージョン確認 macにはpythonが標準でインストールされている。 $ python --version で確認するとversionが表示される。 Python 2.7.16 しかしこのpythonは2系であるため今回は最新の3系pythonをインストールしていく。 homebrewインストール $ brew -v これでhomebrewが入っていることが確認できる。 エンジニアなら入っている人が多数のイメージ。 もし入っていない場合は $ /usr/bin/ruby -e "$(curl -fsSLhttps://raw.githubusercontent.com/Homebrew/install/master/install)" このコマンドでインストールし、確認してみる pyenvのインストール これはpython触る人以外インストールしていないのでは? バージョン確認してみる。 $ pyenv -v ない場合はインストールしたhomebrewを利用し、 $ brew install pyenv インストール完了。もう一度-vで確認する。 pyenvの設定変更 このままではpyenvが正常に動かないため設定を変更する $ env | grep SHELL このコマンドでenv(環境変数)からSHELLの記述がある部分を探す SHELL = /bin/bash と表示されていれば $vi ~/.bash_profile で~/.bash_profileに移動し"i"コマンドで編集する。 exportの並んでいるあたりで export PYENV_ROOT=”$HOME/.pyenv” export PATH=”$PYENV_ROOT/bin:$PATH” eval “$(pyenv init -)” を挿入し、環境変数を変更する。 python3系のインストール ここでpyenvを使ってインストール可能なpythonのバージョンを確認する。 $ pyenv install --list リストで表示された中から好きなversionのpythonをインストール $ pyenv install 3.7.3 ただこの状態ではまだmacで3.7.3のpythonが選択されていない。 pythonコマンドが見ている先のパスを変更する pyenvでのバージョン管理をしているpythonを使用したい場合は、 コマンドの向き先のパスをpyenvに変更する必要がある。 $ which python /usr/bin/python $ eval "$(pyenv init -)" $ which python ~/.pyenv/shims/python 3系を標準にする $ pyenv versions * system (set by /Users/"名前"/.pyenv/version) 3.7.3 これでpyenvに3.7.3があることが確認できる。 これを標準とするために $ pyenv global 3.6.5 とし、最後にバージョン確認を行う。 完了だ。 コードの実行 ①デスクトップでファイルを作成 ②テキストエディタで print("test") と記述。 ターミナルを起動し、 $ python ファイル名 で実行され、 test と表示される。 参考 MacにPythonをインストールする3つの方法!それぞれの利点と手順を紹介 https://kredo.jp/media/mac-python-installation/ pyenvを使ってpythonのバージョンが変更できない時の原因 https://qiita.com/taketyan/items/869e2a0ae144eaa702e0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リストの中身を逆順にする2つの方法 (今日のPython Day8)

0. はじめに  『入門Python3 第2版』の各章の最後には復習課題があります。だいたいの問題は本を読み直せば解けると思うのですが、なぜか第5章 文字列の復習課題(p102-103)は初心者にとってはかなり難しいと思います。今日の問題は初心者でも解ける文字列の問題をつくりました。 1. 問題 li = ["one", "two", "three"]の中身を逆順にして['three', 'two', 'one']としてください。 2. 解答 解答その1 li = ["one", "two", "three"] print(li[::-1]) 解答その2 li = ["one", "two", "three"] li.reverse() print(li) 3. 解説 リストの中身を逆順にするにはli[::-1]かli.reverse()のいずれかを用います。 4. おまけトーク  Qiita トラブルシューティング・失敗集 【(半)自動更新: 2021年05月07日】に自分の記事がリンクされたのですが、これは自分の記事がトラブルシューティングに関する記事だったのか、それとも記事そのものが失敗だったのか、どちらなんですかね?後者でないことを祈っています。最後まで読んでいただき本当にありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PyTorch TensorDatasetについて

PyTorchでTensorDatasetを作成するときのメモ。 TensorDatasetは入力データと教師データが入ったデータセット。 torch.utils.data(公式ドキュメント) numpyからTensorDataset 1. インポート import numpy as np import torch.utils.data 2. 入力データと教師データを準備 例として入力データと教師データをランダムに生成。 # 入力データ x = np.random.randn(100, 5) # 教師データ t = np.random.randn(100, 1) 3. torch.tensor型に変換 tensorのデータ型(公式ドキュメント) x = torch.tensor(x, dtype=torch.float32) t = torch.tensor(t, dtype=torch.int64) 4. torch.utils.data.TensorDatasetを生成 dataset = torch.utils.data.TensorDataset(x, t) TensorDatasetについて # 1つ目のデータを確認 print('入力データ:', dataset[0][0]) print('教師データ:', dataset[0][1]) pandasのDataFrameからTensorDataset DataFrameから直接TensorDatasetへの変換はできない。 一度、numpyに変換してからTessorDatasetへ変換する。 1. DataFrame 例として以下のようなパラメタ3つのデータで説明。 df = pd.DataFrame( columns = ['p1', 'p2', 'p3', 'target'], data = [[1.0, 2.0, 3.0, 0], [4.0, 5.0, 6.0, 1], [7.0, 8.0, 9.0, 0]], ) 2. numpyに変換 values属性または、to_numpy()メソッドでnumpyに変換できる。 入力データ ```python values属性を使う x = df.iloc[: , : -1].values to_numpy()メソッドを使う x = df.iloc[: , : -1].to_numpy() **教師データ** python values属性を使う t = df.iloc[: , 3: ].values numpyへの変換ではないが、Series型はTensorDatasetの引数に指定できる。 t = df['target'] ``` 3. torch.tensor型に変換して、TensorDatasetを生成 numpy変換後は、上記と同じ # torch.tensor型に変換 x = torch.tensor(x) t = torch.tensor(t) # torch.utils.data.TensorDatasetを生成 dataset = torch.utils.data.TensorDataset(x, t) おまけ: データセットを訓練データ、テストデータに分ける 訓練データ数、テストデータ数を決める n_train = 80 n_test = 20 訓練データ、テストデータに分ける d_train, d_test = torch.utils.data.random_split(dataset, [n_train, n_test])
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ARの中で物体認識してワードクラウドを表示する

作ったもの こういうものを作りました。 カメラに写っているものを物体認識して、その名称や関連語を空間内に散りばめて表示するものです。 ソースコードはこちらになります。 https://github.com/shibuiwilliam/ARWithWord 前置き ARアプリではスマホやグラスを通して現実空間に視覚的な演出を表示、操作しますが、そのほとんどが画像やエフェクトに限られているように思います。たとえばスマホでGoogle検索した動物の一部をARのオブジェクトとして仮想現実内に表示してみることができます。 もちろんこうした視覚的な効果は面白いのですが、ARの使い途はモノに限られないと感がています。モノそのもの以外にも言葉や文字を空間に表示して意味づけする用途があっても良い気がします。というわけで、ARにワードクラウドを表示するシステムを作ってみました。 ワードクラウドとは ワードクラウドは自然言語処理で使われるデータの可視化手法の一つです。テキストデータを解析して頻出単語やキーワードを2次元画像に表示します。以下の画像がその例ですが、テキストデータを分析して頻出語を表示することで、そのテキストの内容や重要な単語を可視化します。 簡単なワードクラウドは以下のサイトで作ることができます。 ワードクラウドは基本的には2次元で画像として作りますが、ARと組み合わせて3次元にしても良いと思いたって、今回アプリを作ってみました。 どう作るか AR ARアプリはUnityで作ります。AR Foundationでカメラを通した仮想現実のプレーンを認識し、文字を表示します。 今回はアプリはAndroidで動かしますが、Unity+AR FoundationなのでiOSでも動くと思います(未検証)。 物体認識 物体認識はUnity Barracudaにtiny YOLO v3を載せて使っています。本当はUnity Perceptionを試してみたかったのですが、ライブラリインストールが安定せず、Barracudaを使いました。 モデルはONNXによるtiny YOLO v3の学習済みモデルで、学習データはCOCOデータセットです。ですので認識できるものはここにある80カテゴリのみです。 自然言語処理 認識した物体に関連する単語はfastTextで検索しています。fastTextはFacebookが開発した単語をベクトル化する仕組みで、特定の単語を入力すると、その単語に近い関連語を得ることができます。今回はWikipediaでクローリングして作られた学習済みモデルを使っています。 単語分散表現のモデルは容量が大きい(数GB・・・)になるため、アプリには組み込まずクラウド側にREST APIサーバを作って関連語をリクエストできるようにします。 サイズは仕方ないにしても、word2vecやfastTextのような単語分散表現をONNX変換できるようになってほしいです。 全体像 全体像は以下になります。 UnityでARアプリのベースを開発し、その中でONNXベースのtiny YOLO v3の物体認識を実行します。認識した物体の関連語をKubernetesに作ったfastTextのREST APIにリクエストし、結果をAR Foundationでアンカーを差して表示するというものです。というわけで、システムとしてはUnityによるAndroidアプリとKubernetesに乗せるREST APIを開発しています。 アプリサイドとサーバサイド両方に、AR、Edge AI、サーバサイドのAIを組み合わせた仕組みになります。いろいろと組み合わさっていますが、基本的には既存のモデルとライブラリを組み合わせれば作れます。 もうちょい詳しく Unity Unityによるアプリ開発を説明します。 Unityアプリのコードは以下にあります。 https://github.com/shibuiwilliam/ARWithWord/tree/main/ARWithWord Unityでは最初に3Dプロジェクトを作ります。 作成したプロジェクトにはAR FoundationもBarracudaも入っていないので、Package Managerで必要なパッケージを追加します。 AR Foundation:Package Managerで検索してインストール。 Barracuda:ここ参照。Package Managerで見つからない場合は Add package from git URL で com.unity.barracuda を入力すればインストールできます。 Android Logcat:UnityでAndroidアプリのログを表示するツールです。 パッケージをインストールしたら必要なリソースを追加します。 AR Session Originを作成し、そのコンポーネントとして AR Raycast Manager, AR Anchor Manager, AR Plane Managerを Add Component します。加えて物体認識のコンポーネント(ObjectDetector)と関連語検索のクライアント(SimilarWordClient)を作っておきます。 AR Session Originにセッション中の実行コード(Spawn Manager Script)を実装します。コード全文は長いので省略しますが、以下のような内容になります。 // SpawnManager.cs using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEngine; using UnityEngine.UI; using UnityEngine.XR.ARFoundation; using UnityEngine.XR.ARSubsystems; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Barracuda; public class DetectionTarget { public Texture2D CurrentTexture2D; public Pose HitPose; } public class Detected { public Pose HitPose; public IList<ItemDetected> ItemsDetected; } public class DetectedSimilarWords { public Pose HitPose; public SimilarWords SimilarWords; } public class SpawnManager : MonoBehaviour { [SerializeField] GameObject goText; [SerializeField] ARCameraManager m_arCameraManager; public ARCameraManager arCameraManager { get { return m_arCameraManager; } set { m_arCameraManager = value; } } [SerializeField] GameObject m_goObjectDetector; public GameObject goObjectDetector { get { return m_goObjectDetector; } set { m_goObjectDetector = value; } } private ObjectDetector objectDetector; [SerializeField] GameObject m_goSimilarWordClient; public GameObject goSimilarWordClient { get { return m_goSimilarWordClient; } set { m_goSimilarWordClient = value; } } private SimilarWordClient similarWordClient; public float shiftX = 0f; public float shiftY = 0f; public float scaleFactor = 1; private ARRaycastManager arRaycastManager; private List<ARRaycastHit> hits = new List<ARRaycastHit>(); private static Texture2D _texture; private bool isDetecting = false; Texture2D m_Texture; Queue<DetectionTarget> detectionTargetQueue = new Queue<DetectionTarget>(); Queue<Detected> detectedQueue = new Queue<Detected>(); Queue<DetectedSimilarWords> detectedSimilarWordsQueue = new Queue<DetectedSimilarWords>(); private List<Color32> colors = new List<Color32>() { new Color32(255, 115, 200, 255), new Color32(241, 233, 137, 255), new Color32(108, 109, 101, 255), new Color32(128, 244, 222, 255), new Color32(134, 222, 249, 255), new Color32(228, 178, 249, 255) }; private void OnEnable() { // アプリ起動時処理 Debug.Log("initialize AR camera manager frame received"); m_arCameraManager.frameReceived += OnCameraFrameReceived; _texture = new Texture2D(1, 1); _texture.SetPixel(0, 0, new Color(0.3843137f, 0, 0.9333333f)); _texture.Apply(); } void Start() { // AR起動時処理 this.arRaycastManager = GetComponent<ARRaycastManager>(); // 物体認識 this.objectDetector = goObjectDetector.GetComponent<TinyYolo3Detector>(); this.objectDetector.Start(); // 関連語REST APIクライアント this.similarWordClient = goSimilarWordClient.GetComponent<SimilarWordClient>(); this.similarWordClient.Start(); } void Update() { // 画面タッチでカメラに写っているものを物体認識する if (Input.touchCount > 0) { Touch touch = Input.GetTouch(0); if (touch.phase == TouchPhase.Began) { if (arRaycastManager.Raycast(touch.position, hits, TrackableType.PlaneWithinPolygon)) { Pose hitPose = hits[0].pose; var detectionTarget = new DetectionTarget { CurrentTexture2D = this.m_Texture, HitPose = hitPose }; this.detectionTargetQueue.Enqueue(detectionTarget); Debug.Log($"touched ({hitPose.position.x}, {hitPose.position.y}, {hitPose.position.z})!"); } } } // 関連語の表示 if (this.detectedSimilarWordsQueue.Count() > 0) { var detectedSimilarWord = this.detectedSimilarWordsQueue.Dequeue(); Debug.Log($"similar word for {detectedSimilarWord.SimilarWords.word} has {detectedSimilarWord.SimilarWords.predictions.Count()} predictions"); AllocateItem(detectedSimilarWord.SimilarWords.word, detectedSimilarWord.HitPose.position, detectedSimilarWord.HitPose.rotation); int i = 0; foreach (var prediction in detectedSimilarWord.SimilarWords.predictions) { Debug.Log($"{i} process {prediction.similar_word} with {prediction.similarity}"); var hitPoseRandom = new Vector3(detectedSimilarWord.HitPose.position.x, detectedSimilarWord.HitPose.position.y, detectedSimilarWord.HitPose.position.z); float rX = UnityEngine.Random.Range(-1.0f, 1.0f); float rY = UnityEngine.Random.Range(-1.0f, 1.0f); float rZ = UnityEngine.Random.Range(-1.0f, 1.0f); hitPoseRandom.x += rX; hitPoseRandom.y += rY; hitPoseRandom.z += rZ; AllocateItem(prediction.similar_word, hitPoseRandom, detectedSimilarWord.HitPose.rotation); } } RequestSimilarWord(); } private void AllocateItem(string word, Vector3 hitPose, Quaternion hitRotation) { Debug.Log($"allocate {word}"); this.goText.GetComponent<TextMesh>().text = word; int colorIndex = UnityEngine.Random.Range(0, colors.Count()); this.goText.GetComponent<TextMesh>().color = this.colors[colorIndex]; int characterSize = UnityEngine.Random.Range(6, 18); this.goText.GetComponent<TextMesh>().characterSize = characterSize; Instantiate(goText, hitPose, hitRotation); Debug.Log($"allocated {word} on {hitPose.x}, {hitPose.y}, {hitPose.z}"); } // ARカメラの画像を取得 unsafe void OnCameraFrameReceived(ARCameraFrameEventArgs eventArgs) { if (!arCameraManager.TryAcquireLatestCpuImage(out XRCpuImage image)) { return; } var conversionParams = new XRCpuImage.ConversionParams { inputRect = new RectInt(0, 0, image.width, image.height), outputDimensions = new Vector2Int(image.width, image.height), outputFormat = TextureFormat.RGBA32, transformation = XRCpuImage.Transformation.None }; int imageSize = image.GetConvertedDataSize(conversionParams); var buffer = new NativeArray<byte>(imageSize, Allocator.Temp); image.Convert(conversionParams, new IntPtr(buffer.GetUnsafePtr()), buffer.Length); image.Dispose(); this.m_Texture = new Texture2D( conversionParams.outputDimensions.x, conversionParams.outputDimensions.y, conversionParams.outputFormat, false ); this.m_Texture.LoadRawTextureData(buffer); this.m_Texture.Apply(); buffer.Dispose(); Detect(); } // 物体認識 private void Detect() { if (this.isDetecting) { return; } if (this.detectionTargetQueue.Count() == 0) { return; } var detectionTarget = this.detectionTargetQueue.Dequeue(); this.isDetecting = true; StartCoroutine( ProcessImage( this.objectDetector.IMAGE_SIZE, detectionTarget.CurrentTexture2D, picture => { StartCoroutine( this.objectDetector.Detect( picture, itemsDetected => { if (itemsDetected.Count > 0) { var detected = new Detected { HitPose = detectionTarget.HitPose, ItemsDetected = itemsDetected, }; this.detectedQueue.Enqueue(detected); } Resources.UnloadUnusedAssets(); this.isDetecting = false; } ) ); } ) ); } private IEnumerator ProcessImage(int inputSize, Texture2D texture2D, Action<Color32[]> callback) { Coroutine croped = StartCoroutine( TextureTools.CropSquare( texture2D, TextureTools.RectOptions.Center, snap => { var scaled = Scale(snap, inputSize); var rotated = Rotate(scaled.GetPixels32(), scaled.width, scaled.height); callback(rotated); } ) ); yield return croped; } private Texture2D Scale(Texture2D texture, int imageSize) { Texture2D scaled = TextureTools.scaled(texture, imageSize, imageSize, FilterMode.Bilinear); return scaled; } private Color32[] Rotate(Color32[] pixels, int width, int height) { Color32[] rotate = TextureTools.RotateImageMatrix(pixels, width, height, 90); return rotate; } // 関連語をリクエスト private void RequestSimilarWord() { if (this.detectedQueue.Count()==0) { return; } var detected = this.detectedQueue.Dequeue(); StartCoroutine( this.similarWordClient.SimilarWordAPI( detected.ItemsDetected[0].PredictedItem.Label, 20, results => { Debug.Log($"result {results}"); var detectedSimilarWords = new DetectedSimilarWords { HitPose = detected.HitPose, SimilarWords=results, }; this.detectedSimilarWordsQueue.Enqueue(detectedSimilarWords); } ) ); } } コードの中でやっているフローは以下のようになります。 各処理はQueueに入れてCoroutineで非同期に進めていきます。メモリは消費しますが、物体認識やRESTリクエストの遅延でUIに影響を与えない工夫です。 ARカメラに写っている画像は OnCameraFrameReceived メソッドで取得します。この方法は公式ドキュメントで説明されているものです。 画面タッチ時に OnCameraFrameReceived で取得した最新の画像をtiny YOLO v3で物体認識します。この時点で認識した物体の名称と位置を取得し、キューに溜めます。 物体認識のコードは以下のようなものになります。 // TinyYolo3Detector.cs using System; using UnityEngine; using Unity.Barracuda; using System.Linq; using System.Collections; using System.Collections.Generic; public class Parameters { public int ROW_COUNT; public int COL_COUNT; public int CELL_WIDTH; public int CELL_HEIGHT; public Parameters(int ROW_COUNT, int COL_COUNT, int CELL_WIDTH, int CELL_HEIGHT) { this.ROW_COUNT = ROW_COUNT; this.COL_COUNT = COL_COUNT; this.CELL_WIDTH = CELL_WIDTH; this.CELL_HEIGHT = CELL_HEIGHT; } } public class TinyYolo3Detector : MonoBehaviour, ObjectDetector { public NNModel modelFile; // ラベルの言語。英語or日本語 public enum LabelLanguages { EN, JP }; public LabelLanguages labelLanguage; public string inputName; public string outputNameL; public string outputNameM; private const int IMAGE_MEAN = 0; private const float IMAGE_STD = 255.0F; private const int _IMAGE_SIZE = 416; public int IMAGE_SIZE { get => _IMAGE_SIZE; } public float minConfidence = 0.25f; private IWorker worker; private Model model; public Parameters paramsL = new Parameters(13, 13, 32, 32); public Parameters paramsM = new Parameters(26, 26, 16, 16); public const int BOXES_PER_CELL = 3; public const int BOX_INFO_FEATURE_COUNT = 5; private int classLength; private string[] labels; public void Start() { switch(this.labelLanguage) { case LabelLanguages.EN: this.labels = Constants.cocoLabelEN; break; case LabelLanguages.JP: this.labels = Constants.cocoLabelJP; break; default: this.labels = Constants.cocoLabelEN; break; } this.classLength = this.labels.Length; this.model = ModelLoader.Load(this.modelFile); this.worker = GraphicsWorker.GetWorker(this.model); Debug.Log($"Initialized model and labels: {this.classLength} classes"); } public IEnumerator Detect(Color32[] picture, Action<IList<ItemDetected>> callback) { // Coroutineで物体認識を実行 Debug.Log("Run detection"); using (var tensor = TransformInput(picture, this.IMAGE_SIZE, this.IMAGE_SIZE)) { var inputs = new Dictionary<string, Tensor>(); inputs.Add(this.inputName, tensor); yield return StartCoroutine(this.worker.StartManualSchedule(inputs)); var outputL = this.worker.PeekOutput(this.outputNameL); var outputM = this.worker.PeekOutput(this.outputNameM); List<ItemDetected> results = ParseOutputs(outputL, outputM, this.paramsL, this.paramsM); Debug.Log($"yielded {results.Count()} results"); callback(results); } } public static Tensor TransformInput(Color32[] pic, int width, int height) { float[] floatValues = new float[width * height * 3]; for (int i = 0; i < pic.Length; ++i) { Color32 color = pic[i]; floatValues[i * 3 + 0] = (color.r - IMAGE_MEAN) / IMAGE_STD; floatValues[i * 3 + 1] = (color.g - IMAGE_MEAN) / IMAGE_STD; floatValues[i * 3 + 2] = (color.b - IMAGE_MEAN) / IMAGE_STD; } return new Tensor(1, height, width, 3, floatValues); } private List<ItemDetected> ParseOutputs(Tensor yoloModelOutputL, Tensor yoloModelOutputM, Parameters parametersL, Parameters parametersM) { var itemsInCenter = new List<ItemDetected>(); for (var box = 0; box < BOXES_PER_CELL; box++) { for (int cy = 0; cy < parametersL.COL_COUNT; cy++) { for (var cx = 0; cx < parametersL.ROW_COUNT; cx++) { var result = Parse(cx, cy, box, yoloModelOutputL); if (result != null) { itemsInCenter.Add(result); } } } for (int cy = 0; cy < parametersM.COL_COUNT; cy++) { for (var cx = 0; cx < parametersM.ROW_COUNT; cx++) { var result = Parse(cx, cy, box, yoloModelOutputM); if (result != null) { itemsInCenter.Add(result); } } } } return itemsInCenter; } private ItemDetected Parse(int cx, int cy, int box, Tensor yoloModelOutput) { var channel = (box * (this.classLength + BOX_INFO_FEATURE_COUNT)); float confidence = GetConfidence(yoloModelOutput, cx, cy, channel); if (confidence < this.minConfidence) { return null; } float[] predictedClasses = ExtractClasses(yoloModelOutput, cx, cy, channel); var (topResultIndex, topResultScore) = GetTopResult(predictedClasses); var topScore = topResultScore * confidence; if (topScore < this.minConfidence) { return null; } var itemInCenter = new ItemDetected { PredictedItem = new Prediction { Label = labels[topResultIndex], Confidence = topScore, } }; return itemInCenter; } private float Sigmoid(float value) { var k = (float)Math.Exp(value); return k / (1.0f + k); } private float[] Softmax(float[] values) { var maxVal = values.Max(); var exp = values.Select(v => Math.Exp(v - maxVal)); var sumExp = exp.Sum(); return exp.Select(v => (float)(v / sumExp)).ToArray(); } private float GetConfidence(Tensor modelOutput, int x, int y, int channel) { return Sigmoid(modelOutput[0, x, y, channel + 4]); } public float[] ExtractClasses(Tensor modelOutput, int x, int y, int channel) { float[] predictedClasses = new float[this.classLength]; int predictedClassOffset = channel + BOX_INFO_FEATURE_COUNT; for (var predictedClass = 0; predictedClass < this.classLength; predictedClass++) { predictedClasses[predictedClass] = modelOutput[0, x, y, predictedClass + predictedClassOffset]; } return Softmax(predictedClasses); } private ValueTuple<int, float> GetTopResult(float[] predictedClasses) { return predictedClasses .Select((predictedClass, index) => (Index: index, Value: predictedClass)) .OrderByDescending(result => result.Value) .First(); } private List<ValueTuple<int, float>> GetOrderedResult(float[] predictedClasses) { return predictedClasses .Select((predictedClass, index) => (Index: index, Value: predictedClass)) .OrderByDescending(result => result.Value) .ToList(); } } 物体認識では TransformInput で画像をテンソルに変換、リサイズします。PeekOutでtiny YOLO v3から2種類の推論結果を取得します。ONNXモデルはUnity画面内で内部のレイヤーや入出力を見ることができるのですが、以下のとおりOutputが2個(いずれも物体のバウンディングボックスとラベル)出力されます。 認識した物体名はREST APIクライアントで関連語をリクエストします。REST APIクライアントにはUnityのUnityWebRequestを使っています。実装は以下のとおりです。 やはりCoroutineで起動する仕組みになっています。リクエストもレスポンスもJSONを使います。 // SimilarWordClient.cs using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking; [Serializable] public class SecretJson { public string url; public string secret; } [Serializable] public class PostData { public string word; public int topn; } [Serializable] public class SimilarWord { public string similar_word; public float similarity; } [Serializable] public class SimilarWords { public string word; public SimilarWord[] predictions; } public class SimilarWordClient : MonoBehaviour { public TextAsset secretFile; private string secretString; private SecretJson secretJson; private string similarWordUrl; public void Start() { this.secretString = Resources.Load<TextAsset>(this.secretFile.name).ToString(); this.secretJson = SecretJson.Deserialize(this.secretString); this.similarWordUrl = $"{this.secretJson.url}/similar-word/"; } public IEnumerator SimilarWordAPI(string word, int topn, Action<SimilarWords> callback) { using (var request = new UnityWebRequest(this.similarWordUrl, "POST")) { Debug.Log($"Request {word} for {topn}"); PostData postData = new PostData(); postData.word = word; postData.topn = topn; string postJson = PostData.Serialize(postData); byte[] byteData = System.Text.Encoding.UTF8.GetBytes(postJson); request.uploadHandler = (UploadHandler)new UploadHandlerRaw(byteData); request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); request.SetRequestHeader("accept", "application/json"); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("X-API-KEY", this.secretJson.secret); yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError) { Debug.Log("Error POST request"); Debug.Log(request.error); } else { Debug.Log($"status code [{request.responseCode}]"); if (request.responseCode==200) { Debug.Log("POST request succeeded"); string json = request.downloadHandler.text; SimilarWords similarWords = SimilarWords.Deserialize(json); callback(similarWords); } else { Debug.Log("POST request failed"); } } } } public void Update() { } } Kubernetesと関連語REST APIサーバ 続いてサーバサイドの実装です。サーバサイドのコードは以下にあります。 https://github.com/shibuiwilliam/ARWithWord/tree/main/backend fastTextは既存のモデルを使いますが、1ファイルが圧縮状態で4GB、解凍して7GB、メモリに展開すると15GBが必要になるというデカいモデルです。金があれば64GBメモリのVMを使って起動しても良いですが、モデルの取得やメモリロードで15分以上かかって使い勝手がとても悪いです。なので単語表現の次元数を減らします。標準の次元数は300ですが、サイズと精度を測って100次元に減らします。これでモデルサイズは2GB、メモリに展開して4GBというサイズになりました。モデルロードも5分以内で済みます。 fastTextの次元削減方法は公式ドキュメントにあります。単語分散表現の次元数は各単語を表す数値の数になります。これが多いほうが単語を詳細に表現できますが、容量が大きく計算も重くなります。減らしても実用上問題なければ減らしても良いでしょう。 REST APIはFastAPIを使います。PythonのWeb APIはいろいろ試しましたが、FastAPIが一番安定して構造的に書けて使いやすいです。 REST APIの実装は以下のようになります。 import os from typing import List, Tuple, Dict import shutil import gzip from urllib.request import urlopen import fasttext import fasttext.util from gensim.models.fasttext import load_facebook_model from google.cloud import storage from fastapi import APIRouter, HTTPException, Security from fastapi.security.api_key import APIKeyHeader from starlette import status from logging import getLogger from src.constants import LANGUAGE_ENUM from src.data.schema import Prediction, Predictions, PredictionRequest logger = getLogger(__name__) # 関連語推論クラス class SimilarWordPredictor(object): def __init__( self, bucket_name: str, model_directory: str = "/opt/", language: LANGUAGE_ENUM = LANGUAGE_ENUM.ENGLISH, model_dimension: int = 100, threshold: float = 0.6, ): self.bucket_name = bucket_name self.client = storage.Client() self.bucket = self.client.get_bucket(self.bucket_name) self.model_directory = model_directory self.language = language self.model_dimension = model_dimension if self.model_dimension not in [100, 300]: raise ValueError("model dimension must be one of 100 or 300") self.file_path = self.download_model(force_download=False) self.fasttext_predictor = load_facebook_model(self.file_path) logger.info(f"loaded {self.file_path}") self.threshold = threshold # local cache self.cache: Dict[str, Predictions] = {} def predict( self, word: str, topn: int = 20, ) -> Predictions: logger.info(f"predict {word}") key = f"{word}_{topn}" if key in self.cache.keys(): return self.cache[key] results = self.fasttext_predictor.wv.most_similar( word, topn=topn, ) _predictions = [] for r in results: if r[1] < self.threshold: continue if repr(r[0]).startswith("'\\u"): continue _predictions.append( Prediction( similar_word=r[0], similarity=r[1], ) ) logger.info(f"{word} prediction: {_predictions}") predictions = Predictions( word=word, predictions=_predictions, ) self.cache[key] = predictions return predictions def download_model( self, force_download: bool = False, ): file_name = f"cc.{self.language.value}.{self.model_dimension}.bin" file_path = os.path.join(self.model_directory, file_name) logger.info(f"retrieve model {file_name}") if os.path.exists(file_path): if not force_download: logger.info(f"model {file_name} exists") return file_path blob = self.bucket.blob(file_name) blob.download_to_filename(file_path) logger.info(f"retrieved model {file_name}") return file_path similar_word_predictor = SimilarWordPredictor( bucket_name=os.environ["BUCKET_NAME"], model_directory=os.getenv("MODEL_DIRECTORY", "/opt/"), language=LANGUAGE_ENUM[os.getenv("LANGUAGE", "ENGLISH").upper()], model_dimension=int(os.getenv("MODEL_DIMENSION", 100)), threshold=float(os.getenv("THRESHOLD", 0.6)), ) router = APIRouter() api_key_header_auth = APIKeyHeader( name="X-API-KEY", auto_error=True, ) def get_api_key(api_key_header: str = Security(api_key_header_auth)): if api_key_header != os.environ["PASSPHRASE"]: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key", ) # 関連語API @router.post( "/", response_model=Predictions, dependencies=[Security(get_api_key)], ) def predict( prediction_request: PredictionRequest, ): predictions = similar_word_predictor.predict( word=prediction_request.word, topn=prediction_request.topn, ) return predictions # サンプル用API @router.get( "/sample/", response_model=Predictions, dependencies=[Security(get_api_key)], ) def predict_sample(): predictions = similar_word_predictor.predict( word="ネコ", topn=20, ) return predictions システムはGCPのGKE(Kubernetes)で動かす想定です。モデルファイルはGCPストレージに入れてあり、必要に応じてダウンロードします。 関連語は検索するごとに辞書に登録していき、2度目以降の検索は辞書からレスポンスすることで高速化を図ります。 これでDockerビルドしてKubernetesに乗せればAPIの完成です。 作ったもの 最初に書いたとおり、カメラに写っているものを認識して、その関連語をワードクラウドのように空間内に散りばめて表示します。 おわりに ご覧のとおり、これでなにかの課題を解決するものではありません。 しかしARで現実空間にエフェクトを出すとき、空間内に存在する物体の名称とその意味を認識し、次のアクションを共起するような使い方ができると思い、その試作品として作ってみました。 AIとARは相性が良い割りには組み合わせたプロダクトを作っている例はあまり見ないですし、さらに自然言語処理を組み込んでいるのを見たことがないので、ひとまず作ってみたシステムになります。 次はImage to TextやGPT-3を組み合わせて空間の状況に説明を表示するARを作りたいです(時間があれば)。 まあまあ楽しかったです。緊急事態宣言でGW中引きこもっていた暇潰しになりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む