最初に言っておきたいのは自分も機械学習やロボットやVisionについてまったく初心者で、大学も出てない自分が差を少し少なくしようと日々勉強してる香港人です。今回の記事でTwinCAT UserやRoboDK Userが自分のシステムにEdge Devicesを導入するには意外とかんたんだと感じていただければ嬉しいです。
ちなみに、今回の目標を達成するにはいくつかの小さなゴールが必要です。
- RoboDKからStationセットアップ
- RaspberryのTPUセットアップ
- TwinCATとRoboDKのHandshake
- TwinCATからRaspberry piのImages転送
- TwinCATとRaspberryのHandshake
- TwinCATからの結果を可視化(TF1800)
- 移転学習
- Handshakeで使用するOPCUA Protocol
- TwinCATでOPCUA Server構築
- RoboDK、RaspberryPIのOPCUA Client構築
- DUT構造体をまるごと読み書き
一気にすべてをクリアするのは難しいので、実際いくつかの小さな記事にも分けておき、途中でLINKを貼りますので参考にしてきてください。
もちろん記事の最後にソースコードをDownloadできますので、よかったら見てください。
Transfer-learning?
Mathlabから引用します。
転移学習 (Transfer Learning) とは、あるタスク向けに学習したモデルを、類似したタスクを実行するモデルの開始点として使用するディープラーニングの手法です。転移学習によるネットワークの更新と再学習は通常、ゼロからネットワークを学習させるよりもはるかに高速で簡単です。この手法は、物体検出や画像認識、音声認識などのアプリケーションで一般的に使用されています。
https://jp.mathworks.com/discovery/transfer-learning.html
今回は1000種類のObjectのClassificationのModelを流用します。
Configuration
こちらは今回の構成です。TwinCATにTF6100のOPCUA Serverを使用し、RoboDK側とRaspberry側はOPCUA ClientとしてServerのデータをアクセスします。
RoboDKが取り終わった写真をTwinCATがFTP 経由でRaspberryに転送。Raspbserry piがCoral USB でClassificationのProcessを大幅に加速し、結果をTwinCATに伝えます。
最後にTwinCATがTF1800を使用し結果を表示します。
TwinCATとRoboDKは同じHostにインストールされています。
Operation Concept
プロジェクトは主にManual ModeとAuto Modeに分かれています。
Manual Mode
Flowは以下のように、画面からClassificationボタンを押すと、いま選んでる画像をFTP経由でRaspberry PI4のほうに転送します。Raspberry PI4からImageデータをもらい、そしてStartコマンドが来たらClassificationを始め、Busy Flagを返します。最後に完了とClassificationデータを一緒にServerに返します。
Auto Mode
自動ModeはManual ModeとHandShakeの部分は変わりなく、RoboDKからのProgram End Flagを待ちます。Program End Flagが来たら、TwinCATはRobo_*.jpgが入っているFileを取り出しリスト化します。そしてリストに沿ってImage Fileを順次Raspbserry piへ転送、Resultを待ちます。
TwinCAT Side
TwinCAT側で主にセットアップするのはTF6300 FTP Client・TF6100 OPCUA Server・あとは操作画面です。その3つのセットアップについても別の記事に説明したことありますので、よかったら参考にしてください。
FTP Client Setup
以下の記事を参考にしてください。
OPCUA Server Setup
以下の記事を参考にしてください。
Allow to access With Structure
変数の上に以下のAttributeを追加してください。
{attribute ‘OPC.UA.DA.StructuredType’ := ‘1’} |
Visualization
以下の記事を参考にしてください。TwinCAT側がどうやってDirectoryのFileを検索しリストを作成するかと画面構成も書いています。
Manual Screen
Auto Screen
Code
DUT
DUT_OPCUA_PI4_ClassificationResult
Imageの判断結果が格納されています。
TYPE DUT_OPCUA_PI4_ClassificationResult : STRUCT Label:STRING; Score:REAL; END_STRUCT END_TYPE |
DUT_OPCUA_PI4_TPUConfigurations
いまは使っていません。本来はClassification用のパラメータです。
TYPE DUT_OPCUA_PI4_TPUConfigurations : STRUCT top_k:UINT:=2;//Max number of classification results Count:UINT:=5;//Number of times to run inferences mean:REAL:=128.0; std:REAL:=128.0; threshold:REAL:=0.0; END_STRUCT END_TYPE |
DUT_OPCUA_ReadFromPI4
Raspberry PI4から読み込むNodeです。
TYPE DUT_OPCUA_ReadFromPI4 : STRUCT bClientReady:BOOL; bTPUReady:BOOL; bBusy:BOOL; bFinished:BOOL; arrResult:ARRAY[0..5]OF DUT_OPCUA_PI4_ClassificationResult; END_STRUCT END_TYPE |
DUT_OPCUA_Write2PI4
こちらはTwinCATからRaspberry PI4のコマンドです。
TYPE DUT_OPCUA_Write2PI4 : STRUCT bStart2Classification:BOOL; sImageName :STRING; END_STRUCT END_TYPE |
DUT_OPCUA_ReadFromRoboDK
同じく、RoboDKからのコマンドです。
TYPE DUT_OPCUA_ReadFromRoboDK : STRUCT sImagePath:STRING; bImageisSaved:BOOL; bBusy:BOOL; bEnd:BOOL; END_STRUCT END_TYPE |
DUT_OPCUA_Write2RoboDK
TwinCATからRoboDKへのコマンドです。
TYPE DUT_OPCUA_Write2RoboDK : STRUCT bStartProgram:BOOL; END_STRUCT END_TYPE |
DUT_HMIFileEntry_Display
HMI Dislpay用のDUTです。構造化画面を作ってたので。
あとFileEntry Listの時間タイプはT_FILETYPEが表示上で便利じゃありません。
TYPE DUT_HMIFileEntry_Display : STRUCT LastAccesTime,LastModificationTime,CreationTime:DT; FileEntry:ST_FindFileEntry; ImagePath:STRING; END_STRUCT END_TYPE |
eFBStatus
FBの実行状態示すEnumです。
{attribute ‘qualified_only’} {attribute ‘strict’} TYPE eFBStatus : ( Idle:=0 ,Busy:=1 ,Error:=2 ); END_TYPE |
GVL
GVL
HMI Displayや他のPOUのSignalです。
{attribute ‘qualified_only’} VAR_GLOBAL currentPath:STRING; arrResult:ARRAY[0..49,0..5]OF DUT_OPCUA_PI4_ClassificationResult; FindList:ARRAY[0..49]OF ST_FindFileEntry; nFindFiles: UDINT; bUpload:BOOL; OutputIndex:INT; ImagePath:STRING; AutoRun:BOOL; bTrigger:BOOL; hmiAutoDisplay:ARRAY[0..5]OF DUT_OPCUA_PI4_ClassificationResult; bReset:BOOL; Processing:BOOL; Processing2:BOOL; Hide:BOOL; END_VAR |
GVL_OPCUAServer
OPCServerのNodeです。
{attribute ‘qualified_only’} VAR_GLOBAL {attribute ‘OPC.UA.DA’ := ‘1’} {attribute ‘OPC.UA.DA.StructuredType’ := ‘1’} ReadFromPI4:DUT_OPCUA_ReadFromPI4; {attribute ‘OPC.UA.DA’ := ‘1’} {attribute ‘OPC.UA.DA.StructuredType’ := ‘1’} WriteToPI4:DUT_OPCUA_Write2PI4; {attribute ‘OPC.UA.DA’ := ‘1’} {attribute ‘OPC.UA.DA.StructuredType’ := ‘1’} ReadFromRoboDK:DUT_OPCUA_ReadFromRoboDK; {attribute ‘OPC.UA.DA’ := ‘1’} {attribute ‘OPC.UA.DA.StructuredType’ := ‘1’} Write2RoboDK:DUT_OPCUA_Write2RoboDK; END_VAR |
FB_FTPClient
前の記事とあまり変わりませんので、ここで詳しく説明しません。
以下の記事を参考にしてください。
pFileOpeartions
以下の記事を参考にしてください。TwinCAT側がどうやってDirectoryのFileを検索しリストを作成するかと画面構成も書いています。
VAR
VAR EnumFindFileList:FB_EnumFindFileList; FindList:ARRAY[0..49]OF ST_FindFileEntry; sSearchPath:STRING; bExecute:BOOL; commnd:E_EnumCmdType; bBusy:BOOL; hmiIndex:INT:=0; bError: BOOL; nErrID: UDINT; bEOE: BOOL; nFindFiles: UDINT; hmiDisplayFileEntry:DUT_HMIFileEntry_Display; FileTime:DT; sBasicPath:STRING:=’file:\\\C:\FTPFiles\’; bDisplay:BOOL; hmiIndexDisplayBackup:INT; R_TRIG:R_TRIG; AutoRun: BOOL; F_TRIG:F_TRIG; END_VAR |
Program
AutoRun:=GVL.AutoRun; IF NOT AutoRun THEN sSearchPath:=’C:\FTPFiles\*.jpg’; ELSE sSearchPath:=’C:\FTPFiles\RoboDK*.jpg’; END_IF R_TRIG(CLK:=GVL_OPCUAServer.ReadFromRoboDK.bEnd); IF R_TRIG.Q THEN bExecute:=TRUE; END_IF EnumFindFileList( sNetId:=” ,sPathName:=sSearchPath ,eCmd:=commnd ,pFindList:=ADR(FindList) ,cbFindList:=SIZEOF(FindList) ,bExecute:=bExecute OR R_TRIG.Q ,bBusy=>bBusy ,bError=>bError ,nErrID=>nErrID ,bEOE=>bEOE ,nFindFiles=>nFindFiles ); IF EnumFindFileList.bExecute AND (EnumFindFileList.bEOE OR EnumFindFileList.bError)THEN bExecute:=FALSE; IF AutoRun THEN GVL.bTrigger:=TRUE; END_IF END_IF IF NOT AutoRun THEN IF hmiIndex >=0 AND hmiIndex <=15 THEN hmiDisplayFileEntry.FileEntry:=FindList[hmiIndex]; END_IF IF hmiIndex <0 THEN hmiIndex:=15; ELSIF hmiIndex >15 OR hmiIndex > EnumFindFileList.nFindFiles-1 THEN hmiIndex:=0; END_IF END_IF; IF AutoRun THEN IF hmiIndex >5 THEN hmiIndex:=0; END_IF IF hmiIndex <0 THEN hmiIndex:=5; END_IF hmiDisplayFileEntry.FileEntry:=GVL.FindList[hmiIndex]; GVL.hmiAutoDisplay[0]:=GVL.arrResult[hmiIndex,0]; GVL.hmiAutoDisplay[1]:=GVL.arrResult[hmiIndex,1]; END_IF F_TRIG(CLK:=GVL.Processing); IF F_TRIG.Q THEN hmiIndex:=99; END_IF IF hmiIndex <>hmiIndexDisplayBackup THEN hmiIndexDisplayBackup:=hmiIndex; bDisplay:=TRUE; END_IF hmiDisplayFileEntry.CreationTime:= FILETIME_TO_DT( fileTime:= hmiDisplayFileEntry.FileEntry.creationTime); hmiDisplayFileEntry.LastAccesTime:= FILETIME_TO_DT( fileTime:= hmiDisplayFileEntry.FileEntry.lastAccessTime); hmiDisplayFileEntry.LastModificationTime:= FILETIME_TO_DT( fileTime:= hmiDisplayFileEntry.FileEntry.lastWriteTime); hmiDisplayFileEntry.ImagePath:=CONCAT(STR1:=sBasicPath,STR2:=hmiDisplayFileEntry.FileEntry.sFileName); GVL.currentPath:=hmiDisplayFileEntry.FileEntry.sFileName; GVL.FindList:=FindList; GVL.nFindFiles:=nFindFiles; |
piHandShakeCheck
RoboDKとRaspberry PI4のHandshake制御です。
VAR
VAR Count:INT; R_TRIG:R_TRIG; TON:TON; iStep:INT; i,j:INT; bTrigger:BOOL; OutIndex:INT; testString:STRING; bRun:BOOL; bClear:BOOL; END_VAR |
Program
IF GVL_OPCUAServer.Write2RoboDK.bStartProgram AND GVL_OPCUAServer.ReadFromRoboDK.bBusy AND GVL.AutoRun THEN GVL_OPCUAServer.Write2RoboDK.bStartProgram:=FALSE; END_IF IF NOT GVL.AutoRun THEN GVL_OPCUAServer.Write2RoboDK.bStartProgram :=FALSE; END_IF IF GVL.bReset THEN iStep:=0; GVL.bTrigger:=FALSE; GVL.bUpload:=FALSE; GVL.bReset:=FALSE; bRun:=FALSE; OutIndex:=0; END_IF IF GVL.nFindFiles >0 AND GVL.bTrigger AND NOT bRun THEN iStep:=1; bRun:=TRUE; GVL.bTrigger:=FALSE; FOR i:=0 TO 5 DO FOR j:=0 TO 5 DO GVL.arrResult[i,j].Label:=”; GVL.arrResult[i,j].Score:=0.0; END_FOR END_FOR END_IF; IF bClear THEN FOR i:=0 TO 49 DO FOR j:=0 TO 5 DO GVL.arrResult[i,j].Label:=”; GVL.arrResult[i,j].Score:=0.0; END_FOR END_FOR bClear:=FALSE; END_IF CASE iStep OF 1: GVL.OutputIndex:=OutIndex; GVL.ImagePath:=GVL.FindList[OutIndex].sFileName; GVL.bUpload:=TRUE; IF GVL_OPCUAServer.ReadFromPI4.bBusy THEN iStep:=2; END_IF 2: TON(in:=NOT GVL_OPCUAServer.ReadFromPI4.bBusy ,PT:=T#0.3S); IF TON.Q THEN TON(IN:=FALSE); iStep:=3; END_IF GVL.bUpload:=FALSE; 3: GVL.arrResult[OutIndex,0]:=GVL_OPCUAServer.ReadFromPI4.arrResult[0]; GVL.arrResult[OutIndex,1]:=GVL_OPCUAServer.ReadFromPI4.arrResult[1]; OutIndex:=OutIndex+1; iStep:=99; 99: IF OutIndex > GVL.nFindFiles-1 THEN OutIndex:=0; bRun:=FALSE; iStep:=0; ELSE iStep:=1; END_IF END_CASE GVL.Processing:=iStep=0 ; GVL.Processing2:=NOT GVL_OPCUAServer.ReadFromRoboDK.bBusy; GVL.Hide:=NOT GVL.Processing OR GVL_OPCUAServer.ReadFromRoboDK.bBusy; |
Main
FTP Serverとやりとりや、PIにコマンド転送するMain Programです。
VAR
VAR FTPBasicClientEx:FB_FTPClient; bUpload:BOOL; bDownload:BOOL; iStep:INT; hrUpload:DINT; hrDownload:DINT; TON:TON; iFtpStep:INT; F_TRIG_Upload,F_TRIG_Download:F_TRIG; sSendImage:STRING; sBasicSrcPath:STRING:=’C:\FTPFiles\’; sBasicDestPath:STRING:=’/files/’; Count:INT; R_TRIG:R_TRIG; AutoRun:BOOL; END_VAR |
Program
AutoRun:=GVL.AutoRun; //FTP //Upload Progress4 IF NOT AutoRun THEN FTPBasicClientEx.SrcFilePath:=CONCAT(STR1:=sBasicSrcPath,STR2:=GVL.currentPath); FTPBasicClientEx.DestFilePath:=CONCAT(STR1:=sBasicDestPath,STR2:=GVL.currentPath); ELSE FTPBasicClientEx.SrcFilePath:=CONCAT(STR1:=sBasicSrcPath,STR2:=GVL.ImagePath); FTPBasicClientEx.DestFilePath:=CONCAT(STR1:=sBasicDestPath,STR2:=GVL.ImagePath); END_IF IF AutoRun THEN bUpload:=GVL.bUpload; END_IF IF bUpload AND NOT bDownload AND iFtpStep=0 THEN FTPBasicClientEx.UserName:=’pi’; FTPBasicClientEx.Password:=’raspberry’; FTPBasicClientEx.Host:=’192.168.3.14′; FTPBasicClientEx.Port:=DEFAULT_FTP_PORT; iFtpStep:=1; END_IF //Download Progress IF bDownload AND NOT bUpload AND iFtpStep=0 THEN FTPBasicClientEx.UserName:=’pi’; FTPBasicClientEx.Password:=’raspberry’; FTPBasicClientEx.Host:=’192.168.3.14′; FTPBasicClientEx.Port:=DEFAULT_FTP_PORT; FTPBasicClientEx.SrcFilePath:=’/files/log.txt’; FTPBasicClientEx.DestFilePath:=’ C:\FTPFiles\log.txt’; hrDownload:=FTPBasicClientEx.Download(bDownload); iFtpStep:=2; END_IF //Function Block FTPBasicClientEx(); //Status Control CASE iFtpStep OF 1: hrUpload:=FTPBasicClientEx.Upload(bUpload); F_TRIG_Upload(CLK:= hrUpload = eFBStatus.Busy); IF F_TRIG_Upload.Q THEN iFtpStep:=0; GVL_OPCUAServer.WriteToPI4.bStart2Classification:=TRUE; GVL_OPCUAServer.WriteToPI4.sImageName:=FTPBasicClientEx.DestFilePath; bUpload:=FALSE; hrUpload:=FTPBasicClientEx.Upload(FALSE); END_IF 2: hrUpload:=FTPBasicClientEx.Download(bDownload); F_TRIG_Download(CLK:= hrDownload = eFBStatus.Busy); IF F_TRIG_Download.Q THEN iFtpStep:=0; hrUpload:=FTPBasicClientEx.Download(FALSE); END_IF END_CASE //Flag Reset IF bDownload AND hrDownload >0 THEN bDownload:=FALSE; END_IF TON( IN:=GVL_OPCUAServer.WriteToPI4.bStart2Classification ,PT:=T#0.4S ); IF TON.Q THEN GVL_OPCUAServer.WriteToPI4.bStart2Classification:=FALSE; END_IF // pFileOpeartions(); piHandShakeCheck(); |
Raspberry PI4 Side
いくつかの記事が参考になりますので、よかったら見てきてください。
FTP Sever Setup
以下の記事を参考にしてください。
Coral USB Setup
以下の記事を参考にしてください。
Model
こちらは今回使用するModelです。1000種類のObjectのClassificationのModelです。
https://coral.ai/models/image-classification/
OPCUA Client Setup
今回使用するのはpython-opcuaというLibraryです。もう開発終了になりましたが、使用上では問題ありません。
https://github.com/FreeOpcUa/python-opcua
Install
pip install opcua |
File Configuration
Code
import numpy as np import time from PIL import Image from pycoral.adapters import classify from pycoral.adapters import common from pycoral.utils.dataset import read_label_file from pycoral.utils.edgetpu import make_interpreter from opcua import Client from opcua import ua import os.path import datetime #OPCUA Setup ENDPOINT=”opc.tcp://192.168.3.124:4840″ client=Client(ENDPOINT) NodeID2Server=’ns=4;s=GVL_OPCUAServer.ReadFromPI4′ NodeIDFromServer=’ns=4;s=GVL_OPCUAServer.WriteToPI4′ BASEPATH=’/home/pi/ftp’ #TPU Setup LABELPATH=’imagenet_labels.txt’ MODELPATH=’mobilenet_v1_0.25_128_quant_edgetpu.tflite’ top_k=2 threshold=0.0 count=2 input_mean=128 input_std=128 DEBUG=False def getCurrentTime(): return str(datetime.datetime.now()) def writetext2File(message): with open(‘/home/pi/ftp/files/log.txt’, ‘a’) as log: msg=getCurrentTime()+’:’+message+’\n’ print(msg) log.write(msg) def main(): try: client=Client(ENDPOINT) writetext2File(‘Connecting to OPCUA Server in EndPoint:’+ENDPOINT) client.connect() writetext2File(‘Connecting to OPCUA Server in EndPoint:’+ENDPOINT+’..OK!’) Node2Server=client.get_node(NodeID2Server) NodeFromServer=client.get_node(NodeIDFromServer) client.load_type_definitions() Node2ServerValue=Node2Server.get_value() NodeFromServerValue=NodeFromServer.get_value() Node2ServerValue.bTPUReady=False ReadyCount=0 CommandReceived=False Step=0 #TPU Init #Load the Label labels = read_label_file(LABELPATH) if LABELPATH else {} writetext2File(‘Label is Loaded.’+LABELPATH) #Load the Model interpreter = make_interpreter(*MODELPATH.split(‘@’)) interpreter.allocate_tensors() writetext2File(‘Model is Loaded.’+MODELPATH) # Model must be uint8 quantized if common.input_details(interpreter, ‘dtype’) != np.uint8: raise ValueError(‘Only support uint8 input type.’) ImageFiles=None while True: NodeFromServerValue=NodeFromServer.get_value() ReadyCount=ReadyCount+1 Node2ServerValue.bTPUReady=True Node2ServerValue.bClientReady=True if ReadyCount<=200: Node2ServerValue.bClientReady=False else: Node2ServerValue.bClientReady=True ReadyCount=ReadyCount+1 if ReadyCount >=400: ReadyCount=0 if NodeFromServerValue.bStart2Classification and not CommandReceived: #Command from OPCUA Server writetext2File(‘Command is recevied..’) print(‘Command is received..’) CommandReceived=True writetext2File(‘Path is :’+BASEPATH+NodeFromServerValue.sImageName) print(“path with:”+BASEPATH+NodeFromServerValue.sImageName) #Node2Server.set_value(ua.DataValue(Node2ServerValue)) ImageFiles=os.path.isfile(BASEPATH+NodeFromServerValue.sImageName) print(‘—-‘) if CommandReceived and ImageFiles : Node2ServerValue.bBusy=True #Node2Server.set_value(ua.DataValue(Node2ServerValue)) print(‘Image is found..’) writetext2File(‘Path is found.’) if Step == 2: i=0 # Run inference writetext2File(‘Inferenice with time.,The first inference on Edge TPU is slow because it includes loading the model into Edge TPU memory.’) for _ in range(count): start = time.perf_counter() interpreter.invoke() inference_time = time.perf_counter() – start classes = classify.get_classes(interpreter, top_k, threshold) writetext2File(‘%.1fms’ % (inference_time * 1000)) for c in classes: writetext2File(‘%s: %.5f’ % (labels.get(c.id, c.id), c.score)) Node2ServerValue.arrResult[i].Label=labels.get(c.id, c.id) Node2ServerValue.arrResult[i].Score=c.score i=i+1 Step=99 if Step == 1: params = common.input_details(interpreter, ‘quantization_parameters’) scale = params[‘scales’] zero_point = params[‘zero_points’] mean = input_mean std = input_std writetext2File(‘Step is 1,Initing the Parameters.’) if abs(scale * std – 1) < 1e-5 and abs(mean – zero_point) < 1e-5: # Input data does not require preprocessing. common.set_input(interpreter, image) else: # Input data requires preprocessing normalized_input = (np.asarray(image) – mean) / (std * scale) + zero_point np.clip(normalized_input, 0, 255, out=normalized_input) common.set_input(interpreter, normalized_input.astype(np.uint8)) Step=2 if Step == 0: size = common.input_size(interpreter) s=BASEPATH+NodeFromServerValue.sImageName image = Image.open(s).convert(‘RGB’).resize(size, Image.ANTIALIAS) Step=1 writetext2File(‘Step is 0,Got the image from TwinCAT and Open it.’) for r in Node2ServerValue.arrResult: r.Label=” r.Score=0.0 #not NodeFromServerValue.bStart2Classification and if Step > 0 and Step <99: Node2ServerValue.bBusy=True print(‘–t’) else: CommandReceived=False Node2ServerValue.bBusy=False Step=0 ImageFiles=None Node2Server.set_value(ua.DataValue(Node2ServerValue)) finally: client.disconnect() writetext2File(‘Disconnected from OPCUA Sever.’) if __name__ == ‘__main__’: writetext2File(‘Script is started.’) main() |
RoboDK
RoboDK側の役割は写真を取るときと自動ModeのときのHandShakeです。RoboDKが必要?だと思う人がいるかもしれませんが、RoboDKの簡単操作のおかけで様々のBoxに貼ってる写真をCamera Interfaceに写真保存でき、より現実に近い環境を実現できると私は思います。
StationSetup
Stationは難しいことなく、UR RobotがBoxを持ち上げ、Cameraに向けて様々な角度でBoxに貼ってる写真を取りFile Systemへ保存します。
2D Camera Simulation Interface
以下の記事を参考にしてください。
OPCUA Client Setup
Raspberryと同じLibraryを使用しています。
https://github.com/FreeOpcUa/python-opcua
連携は以下の記事を参考にしてください。
Install
pip install opcua |
Code
from robodk import robolink # RoboDK API from robodk import robomath # Robot toolbox import datetime import time from opcua import Client,ua import random RDK = robolink.Robolink() UR=RDK.Item(“UR5”) CameraPosition1=RDK.Item(‘tCamera1’) CameraPosition2=RDK.Item(‘tCamera2’) CameraPosition3=RDK.Item(‘tCamera3’) CameraPosition4=RDK.Item(‘tCamera4’) CameraPosition5=RDK.Item(‘tCamera5’) CameraPosition6=RDK.Item(‘tCamera6’) Prog=RDK.Item(‘pMain’) ENDPOINT=’opc.tcp://127.0.0.1:4840′ NODEID2Server=’ns=4;s=GVL_OPCUAServer.ReadFromRoboDK’ NODEIDFromServer=’ns=4;s=GVL_OPCUAServer.Write2RoboDK’ client=Client(ENDPOINT) client.connect() cam_item = RDK.Item(“Camera1”) if not cam_item.Valid(): print(‘No camera..’) quit() snapshot_file=’C:\FTPFiles\p1.jpg’ #Make Dir #ret=RDK.Cam2D_Snapshot(snapshot_file,cam_item) def createImagePath(s): return “C:\FTPFiles\RoboDK”+getCurrentTime()+”.jpg” def getCurrentTime(): return time.strftime(‘%Y_%m_%d_%H_%M_%S_’+str(random.randint(0,99))) def Sanp(Path,Cam): return RDK.Cam2D_Snapshot(Path,Cam) flag=[False,False,False,False,False,False,] client.load_type_definitions() Node2Server=client.get_node(NODEID2Server) NodeFromServer=client.get_node(NODEIDFromServer) NodeFromServerValue=NodeFromServer.get_value() Node2ServerValue=Node2Server.get_value() counts=0 start=0 end=0 _busy=False while True: NodeFromServerValue=NodeFromServer.get_value() Node2ServerValue=Node2Server.get_value() Pose=UR.Pose() if not _busy and NodeFromServerValue.bStartProgram : _busy=True value = Prog.RunProgram() if Prog.Busy(): Node2ServerValue.bBusy=True if Pose == CameraPosition1.Pose() and not flag[0]: ret=Sanp(createImagePath(“001”),cam_item) if ret : UR.setDO(‘CamIO_1’,1) flag[0]=True elif Pose == CameraPosition2.Pose()and not flag[1]: ret=Sanp(createImagePath(“002”),cam_item) if ret: UR.setDO(‘CamIO_2’,1) flag[1]=True elif Pose == CameraPosition3.Pose()and not flag[2]: ret=Sanp(createImagePath(“003”),cam_item) if ret: UR.setDO(‘CamIO_3’,1) flag[2]=True elif Pose == CameraPosition4.Pose()and not flag[3]: ret=Sanp(createImagePath(“004”),cam_item) if ret: UR.setDO(‘CamIO_4’,1) flag[3]=True elif Pose == CameraPosition5.Pose()and not flag[4]: ret=Sanp(createImagePath(“005”),cam_item) if ret: UR.setDO(‘CamIO_5’,1) flag[4]=True elif Pose == CameraPosition6.Pose()and not flag[5]: ret=Sanp(createImagePath(“006”),cam_item) if ret: UR.setDO(‘CamIO_6’,1) flag[5]=True if flag[0] and flag[1] and flag[2] and flag[3] and flag[4] and flag[5]: Node2ServerValue.bEnd=True start=time.time() else: Node2ServerValue.bBusy=False for i in range(0,6): flag[i]=False if Node2ServerValue.bEnd: if time.time()-start > 2: Node2ServerValue.bEnd=False _busy=False Node2Server.set_value(ua.DataValue(Node2ServerValue)) |
Result-Manual
Result-Auto
Source Code
https://github.com/soup01Threes/TwinCAT3/blob/main/DemoProject_TwinCATxRoboDKxRaspberryxML.zip
Finally
まず大きな問題がありまして、それはTwinCAT FTP ClientからRaspberry PIに画像を転送するときに想像以上に時間がかかることです。もし他にTwinCATとRaspberry間高速データ共有する方法あれば教えて下さい。
次は、最初にBeckhoffのTF3800のMachine Learning Functionを使うと思いますが、学習済みのモデルからONNX Formatに変換はできますが、そのあとBeckhoffのXML Format再変換するときにエラー出ててき治れなく、最後は諦めてCoral TPUを使用することになります。
あと画像データをどうやってTF3800に渡すかもずっと悩みました。128×128のImageなので3D 配列に変換し渡せるかどうか、まだ考えています。あとTensorflow LiteがSupportしないのかも?そしてTc3_Visionと連携できるかどうか、まだ模索中。
TwinCATを使用する理由は多彩なCommunicationもSupportしますし、いまはOPCUAを採用していますが、場合によってProfinetやEIPやModbusやREST APIでも切り替えられます。
Coral TPUを使用するときもRuntimeをIPCインストールしたいのですが、それもあまりうまく行かずRapbserry なら一発インストール済みなので、TPU+Raspberryの組み合わせにした。そしてこの記事ではCoral USB を使っていますが、他のEdgeデバイスでも流用できます。
RoboDKは今回IPCにインストールされましたが、Raspberryの方にもインストールできます。多分こっちのほうが画像のやりとりは便利だし、FTPも使わなくてもよいのではないかと思っています。1番難しいTwinCATからRaspberryにデータを渡すことさえできれば、他は簡単になるのではないか?と。
もちろんRoboDKはMUSTではないが、RoboDKはPythonのAPIは使いやすいしカメラInterfaceもあり、いまのところわざわざ他のソフトを使う理由がまだ見つかりません。
そして1番感じたのは大きなTaskをいくつの小さいなゴールを切り分けて設定し、一つずつ撃破すればいいのではないかな?と。その考え方は料理と同じだと思います。ハムやレダスの切り方がわかります、パンの美味しい焼き方もわかります。たまごサラダの作り方がわかります。ですが、それらの”わかります”を組み合わせてサンドイッチを作るのを発想できるかどうかで差が付きますね。特に新人にトレーニングするとき、いくら食材の切り方・調理方法を教えても”組み合わせ”し自分の料理になるまではかなり大きなStepだと思います。
最後に、このプロジェクトはモデルをそのまま流用しImageをClassificaitonしましたが、次の目標としてObject DetectionやPose estimationやSemantic segmentation、自分でModelを修正、微調整、Trainingなどにもチャレンジしたいと考えています。みなさんはいかがですか?エッジデバイスを導入するのは想像以上難しくないと思っていただければ嬉しいです。