Beckhoff#TwinCAT x RoboDK x Pi4 x Coral USB ML – Image Classification

最初に言っておきたいのは自分も機械学習やロボットや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

以下の記事を参考にしてください。

Beckhoff#TwinCAT TF6300 でFTPServerにFileをUploadしよう

OPCUA Server Setup

以下の記事を参考にしてください。

Beckhoff#TwinCAT3 TF6100 OPCUA _Part1_Server立ち上げよう

Allow to access With Structure

変数の上に以下のAttributeを追加してください。

{attribute ‘OPC.UA.DA.StructuredType’ := ‘1’}

Visualization

以下の記事を参考にしてください。TwinCAT側がどうやってDirectoryのFileを検索しリストを作成するかと画面構成も書いています。

Beckhoff# FB_EnumFindFileListで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

前の記事とあまり変わりませんので、ここで詳しく説明しません。

以下の記事を参考にしてください。

Beckhoff#TwinCAT TF6300 でFTPServerにFileをUploadしよう

pFileOpeartions

以下の記事を参考にしてください。TwinCAT側がどうやってDirectoryのFileを検索しリストを作成するかと画面構成も書いています。

Beckhoff# FB_EnumFindFileListで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

いくつかの記事が参考になりますので、よかったら見てきてください。

RaspberryPI#初期でcmdline.txt編集しStatic IP設定
RaspberryPI#OSを入れずにSSHをEnableする方法


FTP Sever Setup

以下の記事を参考にしてください。

Beckhoff#TwinCAT TF6300 でFTPServerにFileをUploadしよう


Coral USB Setup

以下の記事を参考にしてください。

Raspberry#Coral USB Acceleratorで行うML

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

以下の記事を参考にしてください。

RoboDK# 2D Camera Simulation Interfaceを使ってみよう

OPCUA Client Setup

Raspberryと同じLibraryを使用しています。

https://github.com/FreeOpcUa/python-opcua

連携は以下の記事を参考にしてください。

RoboDK#TwinCATと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などにもチャレンジしたいと考えています。みなさんはいかがですか?エッジデバイスを導入するのは想像以上難しくないと思っていただければ嬉しいです。

Footer_Basic

Please Support some devices for my blog

Amazon Gift List

Find ME

Twitter:@3threes2
Email:soup01threes*gmail.com (* to @)
YoutubeChannel:https://www.youtube.com/channel/UCQ3CHGAIXZAbeOC_9mjQiWQ

シェアする

  • このエントリーをはてなブックマークに追加

フォローする