วันอังคารที่ 30 เมษายน พ.ศ. 2562

ผลการทดสอบการจับเวลาการทำงานบน Raspberry Pi

จากบทความที่แล้ว ได้ทำการนำโค้ดทดสอบไปรันบน Raspberry Pi เพื่อเก็บผลเวลาการประมวลผลของโปรแกรมจาก time.clock() ต่อ frame โดยได้ผลคร่าวๆจากข้อมูล 170 ข้อมูลดังนี้


จะพบว่า เวลาที่ใช้ไปในการประมวลผลของ Pipeline ใน Raspberry Pi นั้นน้อยกว่าของรุ่นต้นแบบ อาจเป็นผลมากจากการทำงานแบบ Pipeline ทำให้คอขวดของการทำงานปกติที่ต้องตรวจสอบภาพเป็นเฟรมๆนั้นลดลง ทำให้การประมวลผลเร็วมากขึ้น

ปัญหาและการแก้ไขการใช้ Pipeline Programming ร่วมกับ Multiprocessing

จากการออกแบบตัวโค้ดรุ่นต้นแบบซึ่งมีการทำแบบลำดับ ดังรูป


จึงทดลองปรับปรุงการออกแบบตามลักษณะการใช้ Multiprocessing โดยแบ่งเป็น 4 Process และแต่ละ Process จะทำงานแบบ Pipeline ดังนี้

1. Update ID แยกมาจาก update_old_id ใช้สำหรับปรับค่า old_id ที่ใช้ในการเปรียบเทียบกับข้อมูลตัวปัจจุบัน
2. Image Processing ปรับปรุงมาจาก get_contours โดยจะทำการเก็บภาพ depth image มาประมวลผลทั้ง gaussian blur, erode, dilate, threshold และหา contour ของภาพทำการใส่เข้า queue และส่งไปที่ process ถัดไป
3. Blob Tracking เป็นการรวมกันของ blob_tracking และ blob_buffer เพื่อนำ contour จาก image processing มาทำการใส่ blob_id และเก็บ centroid ของ blob สำหรับใช้คำนวณหาการเคลื่อนที่
4. Check Gesture and Mapping เป็นการรวมกันของ check_gesture และ keymapping เข้าด้วยกัน โดยจะนำ centroid ของ blob ตัวก่อนหน้าและตัวปัจจุบันมาทำการคำนวณหาทิศทางการเคลื่อนที่ และนำผลที่ได้ไปใช้ในการ map เข้ากับคำสั่งที่เราต้องการ

หลังจากทำการทดสอบดูพบว่าส่วนของการตรวจจับภาพนั้นไม่ทำงาน แต่กลับแสดงผลเพียงการทำ update_id และ keymap


จากปัญหาที่เกิดขึ้น จึงได้ทำการทดลองตรวจสอบเวลาการทำงานของทั้ง 4 ฟังค์ชันในรูปแบบการเรียกใช้ฟังค์ชันแบบไม่ต้องใช้ process พบว่าเวลาการทำงานของ image processing และ blob tracking นั้นใช้เวลาในการทำงานเป็นหน่วย millisecond ซึ่งมากกว่า update id และ keymap ซึ่งใช้เวลาในการทำงานเป็นหน่วย microsecond


จากผลเวลาดังกล่าวจึงได้ทำการสันนิษฐานปัญหาที่เกิดขึ้นว่า
  • เนื่องจากเวลาการทำงานที่ต่างกันทำให้ข้อมูลตัวแปรต่างๆนั้นเกิดความคลาดเคลื่อนไปเพราะมีตัวแปรหลายตัวที่เป็น Global variable ทำให้ตัวแปรตัวเดียวกันถูกเรียกใช้พร้อมกันมากกว่า 1 ฟังก์ชัน เมื่อมีการเปลี่ยนแปลงค่าของตัวแปรในฟังค์ชันแรก ตัวแปรตัวเดียวกันในฟังค์ชันที่ 2 จึงถูกเปลี่ยนค่าไปด้วย
จึงทำการตรวจสอบตัวแปรที่ใช้ในทุกๆฟังค์ชันของ Process จะพบว่า มีตัวแปรที่เกี่ยวโยงกัน คือ blobs, blob_buffers, old_id และ blob_movement ซึ่งตัวแปรทั้ง 4 ตัวนี้จะใช้งานร่วมกันระหว่าง 3 Processes คือ Update ID, Image Processing และ Check Gesture ทำให้เมื่อแยกกันทำงานส่งผลตัวแปลแต่ละตัวเกิดความคลาดเคลื่อนไป และในขณะเดียวกัน contours ที่ใช้สำหรับสร้าง blobs นั้นมีการใช้เพียงแค่ 2 Processes คือ Image Processing และ Blob Tracking


จากผลที่ได้ จึงทำการปรับเปลี่ยนโครงสร้างภายใน Process ใหม่ โดยที่แบ่งเป็น 2 Processes คือ Image Processing และกลุ่มของฟังค์ชันเรียงตามลำดับการทำงานคือ Update ID, Blob Tracking และ Check Gesture และใช้ Queue ในการส่งผ่านตัวแปร contours ระหว่าง Process ขณะที่ทำงานเป็น Pipeline


และเมื่อทำการรันโปรแกรมอีกครั้งพบว่าการตรวจจับสามารถทำงานได้


จึงมาทำกาารตรวจสอบประสิทธิภาพการทำงานระหว่างรุ่นต้นแบบและรุ่นปรับปรุงซึ่งค่าที่นำมาเทียบกัน คือ fps ที่คำนวณจากจำนวนของ frame หารด้วยผลต่างของ time.clock() ซึ่งเป็นเวลาที่ใช้ในการประมวลผลของ CPU โดยได้ผลดังนี้


จะพบว่า fps ที่เก็บได้นั้นของรุ่นปรับปรุงจะมีความเร็วการทำงานที่สูงมากเมื่อเทียบกับการทำงานของรุ่นต้นแบบ

นอกจากผลของ fps แล้ว ได้ทำการตรวจสอบผลอีกจุดคือ used_time ที่ใช้ในการประมวลผลโดยเก็บเวลาจาก time.clock() ของทั้ง 2 วิธี โดยมีผลดังนี้


จะสังเกตได้ว่าเวลาการทำงานที่ใช้ไปของรุ่นปรับปรุงนั้นต่างกันกว่า 1000 เท่าเมื่อเทียบกับรุ่นต้นแบบ ซึ่งผลที่ได้นี้เป็นผลจากการทดลองบน Ubuntu ของ Lenovo Z580 และจะนำผลจาก Raspberry Pi มาอัพเดตในหัวข้อถัดไป

วันจันทร์ที่ 22 เมษายน พ.ศ. 2562

ทดสอบการทำงานระหว่าง Pipeline Process และ Basic Process

ทำการทดสอบว่าการทำงานของ Pipeline Process นั้นจะมีประสิทธิภาพที่เร็วกว่าการทำงานปกติหรือไม่ โดยใช้การทดสอบพื้นฐานดังนี้

แบ่งฟังค์ชันออกเป็น 4 ตัว คือ

ฟังค์ชันสำหรับประมวลผลภาพแบบรวม


และฟังค์ชันการประมวลผลแบบแยก โดยจะแบ่งเป็น 3 Group สำหรับ 3 Process ดังนี้


ฟังค์ชันสำหรับทำการปรับสีของภาพและทำ Gaussian blur


ฟังค์ชันสำหรับการทำ Erode ภาพ


ฟังค์ชันสำหรับการทำ Dilate ภาพ

โดยจะทำการ Process รูปภาพ 3 รูป โดยแบ่งเป็น


การเรียกใช้แบบ Pipeline Process 3 Processes ในเวลาเดียวกัน


การเรียกใช้แบบ Basic Process เพื่อประมวลผลภาพ 3 รูป

ผลที่ได้จากการทดลองแสดงให้เห็นดังกราฟข้างต้น


จะพบว่าเวลาการทำงานในช่วงแรกนั้นผลความเร็วนั้นแทบไม่ต่างกันเลย แต่เมื่อจำนวน loop ที่สูงมากๆจะพบว่า เวลาการทำงานของ Pipeline Process นั้นจะเริ่มมีเวลาการทำงานที่เร็วกว่าอย่างเห็นได้ชัด

ข้อควรระวังในการทำงานแบบ Pipeline คือเวลาการทำงานของฟังค์ชันควรจะไล่เลี่ยกันเพื่อป้องกันคอขวด ( Bottleneck ) เช่น กรณีศึกษาที่ให้ Process1 มี loop การทำงานที่สูงกว่าอีก 2 Processes และประมวลผลภาพ 3 ภาพดังเดิม จะทำให้เกิดลักษณะดังนี้


ในช่วงเริ่มต้นยังมีลักษณะการทำงานปกติคือ เมื่อประมวลผลจาก Process1 เสร็จแล้วก็ส่งผลไปให้ Process2 ประมวลผลต่อ และ Process1 ก็นำภาพถัดไปมาประมวลผล สังเกตได้จาก CPU Core ที่ทำงานพร้อมกัน 2 ตัว


จากภาพจะพบว่าถึงแม้ว่า Process2 จะทำงานเสร็จแล้ว แต่เนื่องจาก Process1 ยังประมวลผลไม่เสร็จจึงส่งข้อมูลมาให้ไม่ได้ทำให้เกิดการขาดช่วงกันของ Pipeline Process

วันศุกร์ที่ 12 เมษายน พ.ศ. 2562

ทดสอบการรัน 2 Processes ในเวลาเดียวกัน

ทำการทดสอบว่าการรัน 2 Processes ในเวลาเดียวกันสามารถทำให้ CPU Core ทั้ง 2 Cores ใช้งานได้อย่างเต็มประสิทธิภาพหรือไม่


โดยเริ่มต้นจะใช้ฟังค์ชัน convert_col2 ในการปรับสีของรูปภาพที่ถูกป้อนเข้ามา ซึ่งจะทำซ้ำตามจำนวนของค่า loop ที่กำหนดไว้ เมื่อทำงานเสร็จแล้วจะทำการ print 1 ออกมาและนำค่าของข้อมูลไปเก็บลงใน Queue


ในส่วนของการเรียกใช้นั้น จะเริ่มจากการเก็บข้อมูลภาพ test.jpg มาทำการปรับขนาดและเตรียมรอสำหรับป้อนเข้า Process


ภาพ test.jpg ที่ป้อนเข้าระบบ

จากนั้นจะสร้าง Queue สำหรับเตรียมเก็บข้อมูลระหว่าง Process และจึงเริ่มจับเวลาการทำงาน

โดยจะทำการสร้าง Process ไว้ 2 Processes โดยจะเรียกใช้ฟังค์ชัน convert_col2 และใช้ภาพ test.jpg ทั้งคู่

จากนั้นจะเริ่มทำการสั่งให้ Process ทั้ง 2 ทำงานพร้อมกัน และใช้ join เพื่อรอผลการทำงาน หลังจากเสร็จสิ้นจะทำการแสดงขนาดของ Queue ว่ามีข้อมูลเก็บอยู่ใน Queue กี่ตัว

ซึ่งผลลัพธ์ที่ได้จากการสังเกตการทำงานของ CPU Core ผ่าน htop command ใน cmd พบว่าการทั้ง 2 Processes ทำงานในเวลาเดียวกันและใช้ CPU Core ได้อย่างเต็มประสิทธิภาพ


ภาพการทำงานของ CPU Core


จากภาพการทำงานจะพบว่ามีการ print 1 ออกมา 2 ตัวจริงๆ และจำนวนข้อมูลภายใน Queue ที่แสดงออกมาก็มี 2 ค่าจริงๆ แสดงว่าการทำงานนั้นถูกต้องตามที่คาดการณ์ไว้

วันพฤหัสบดีที่ 11 เมษายน พ.ศ. 2562

แนวคิดการทำ pipeline




       เป็นการทำงานโดยให้ฟังก์ชันแรกส่งต่อ output ไปให้เป็น input ให้อีกฟังก์ชันนึง และในขณะเดียวกันก็รับ input ใหม่เข้ามาด้วย ซึ่งจะต้องแบ่งการทำงานไปในหลายๆ core ของ CPU
เพื่อที่จะเพิ่มประสิทธิภาพการทำงานให้เร็วขึ้น ซึ่งขณะนี้กำลังทดสอบจากการคำนวณเบื้องต้น โดยแบ่งออกเป็น 2 ฟังค์ชัน และข้อมูล Input 2 ตัว ซึ่งขณะนี้ยังไม่ได้ผลตามต้องการ

       ส่วนที่การทำงานของ Process ในครั้งก่อนทำงานเพียงทีละ Core เนื่องจาก Process ที่ 2 ต้องรอข้อมูลจาก Process 1 อยู่ไม่ได้ทำงานไปพร้อมๆกัน ต้องทำการปรับปรุงพัฒนาต่อไป

วันอังคารที่ 2 เมษายน พ.ศ. 2562

การส่งข้อมูลจาก JavaScript ไปหาที่ esp8266

การทำงานโดยรวม จะได้ว่า

เริ่มแรก ตัว javascript และ esp8266 จะทำการเชื่อมต่อตัวเองกับ netpie และเมื่อมีการกดปุ่ม x แล้วตัวไฟล์ javascript จะทำการ chat ไปหา esp8266 โดยผ่าน netpie เมื่อ esp8266 ได้รับข้อมูล จะทำการเปลี่ยนสถานะของ led และส่งสถานะ led กลับมาที่ javascript 

switch.js

<script src="http://netpie.io/microgear.js"></script>
<script>
const APPID = "appid";
const KEY = "key";
const SECRET = "secret key";
const ALIAS = "Board1";
const TARGET = "LivingRoom"
var led_status = "off";
var microgear = Microgear.create({
  key: KEY,
  secret: SECRET,
  alias : ALIAS
});
microgear.on('message',function(topic,msg) {
  if(msg=="0"){
    led_status = "off";
  }else if(msg=="1"){
    led_status = "on";
  }
    document.getElementById("data").innerHTML = led_status;
});

microgear.on('connected', function() {
  document.getElementById("data").innerHTML = "Now I am connected with NETPIE...";
});

microgear.connect(APPID);
document.onkeydown = function (e) {
  var keyCode = e.keyCode;
  keycode = e.keycode;
  if( keyCode == 88){
    if(led_status == "off"){
      microgear.chat(ALIAS,'1');
    }else if (led_status == "on") {
      microgear.chat(ALIAS,'0');
    }
  }
}
</script>
<body>
  <center>
    <div id="data">_____</div>
  </center>
</body>

1.ภายใน JavaScript file จะมีการสร้าง microgear ซึ่งเป็น object ที่เราจะนำมาใช้ทำในการทำงาน ดังนี้
const KEY = "key";
const SECRET = "secret key";
const ALIAS = "Board1";
var microgear = Microgear.create({
  key: KEY,
  secret: SECRET,
  alias : ALIAS
});

2.จากนั้นเราจะเชื่อมต่อตัว microgear กับ app id ของเรา

const APPID = "appid";
microgear.connect(APPID);


3.เราจะมาเริ่มส่งข้อความไปที่ microgear ที่ชื่อ Board1 โดยที่เราจะให้กดปุ่ม x เพื่อที่จะได้ส่ง 1 เมื่อสถานะเป็น off  และ 0 เมื่อสถานะเป็น on ไปที่ตัว esp8266 (led_status จะใช้เพื่อแยกการส่งค่าในแต่ละรอบ เมื่อได้รับสถานะไฟจาก esp8266 ว่าเป็น on ตัว javascript จะทำการส่ง off เมื่อมีการสั่งในครั้งถัดไป)

document.onkeydown = function (e) {
  var keyCode = e.keyCode;
  keycode = e.keycode;
  if( keyCode == 88){
    if(led_status == "off"){
      microgear.chat(ALIAS,'1');
    }else if (led_status == "on") {
      microgear.chat(ALIAS,'0');
    }
  }
}

4.เมื่อส่งข้อมูลได้แล้ว ต่อจากนี้จะเป็นส่วนของการตอบสนองต่อ event ที่เกิดขึ้น ดังนี้
 4.1 'connected' event

microgear.on('connected', function() {
  document.getElementById("data").innerHTML = "Now I am connected with NETPIE...";
});

    จากโค้ดข้างต้น เมื่อมีการเชื่อมต่อแล้วเราจะให้ใส่คำว่า "Now I am connected with NETPIE..." เพื่อแสดงให้เห็นว่า มีการเชื่อมต่อแล้ว

 4.2 'message' event

microgear.on('message',function(topic,msg) {
  if(msg=="0"){
    led_status = "off";
  }else if(msg=="1"){
    led_status = "on";
  }
    document.getElementById("data").innerHTML = led_status;
});

    จากโค้ดข้างต้น จะเป็นการนำเปลี่ยน Status ของ led โดย เราจะได้รับข้อความมาจาก netpie ที่เป็นตัวกลางระหว่าง Javascript กับ esp8266 ถ้าหากได้รับ 0 มา เราจะให้ led_status เป็น "off" และ หากได้รับ 1 เราจะให้ led_status เป็น "on" จากนั้นก็แสดงสถานะบนหน้า HTML

html2esp8266.ino

#include <ESP8266WiFi.h>
#include <MicroGear8266.h>
const char* ssid = "ssid";
const char* password = "ssid_password";
#define APPID   "SmartMirror"
#define KEY     "key"
#define SECRET  "secret"
#define ALIAS   "Board1"
#define TARGET   "LivingRoom"
#define LED 16
WiFiClient client;
int timer = 0;

MicroGear microgear(client);
void onMsghandler(char *topic, uint8_t* msg, unsigned int msglen) {
  Serial.print("Incoming message -->");
  msg[msglen] = '\0';
  Serial.println((char *)msg);
  if (*(char *)msg == '1') {
    digitalWrite(LED, LOW); // turn on the LED
    microgear.chat(TARGET, "1");
  } else {
    digitalWrite(LED, HIGH); //turn off the LED
    microgear.chat(TARGET, "0");
  }
}
void onConnected(char *attribute, uint8_t* msg, unsigned int msglen) {
  Serial.println("Connected to NETPIE...");
  microgear.setName(ALIAS);
}
void setup() {
  microgear.on(MESSAGE, onMsghandler);
  microgear.on(CONNECTED, onConnected);
  Serial.begin(115200);
  Serial.println("Starting...");
  pinMode(LED, OUTPUT);
  digitalWrite(LED, LOW);
  if (WiFi.begin(ssid, password)) {
    while (WiFi.status() != WL_CONNECTED) {
      delay(1000);
      Serial.print(".");
    }
  }
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  microgear.init(KEY, SECRET, ALIAS);
  microgear.connect(APPID);
}
void loop() {
  if (microgear.connected()) {
   // Serial.println("...");
    microgear.loop();
    timer = 0;
  }
  else {
    Serial.println("connection lost, reconnect...");
    if (timer >= 5000) {
      microgear.connect(APPID);
      timer = 0;
    }
    else timer += 100;
  }
  delay(100);
}

5.ภายในตัว Arduino file ก็มีการสร้างตัวแปรชนิด microgear เพื่อใช้ในการทำงาน ดังนี้


#define KEY     "key"
#define SECRET  "secret"
#define ALIAS   "Board1"

microgear.init(KEY, SECRET, ALIAS);


6.จากนั้นเราก็จะเชื่อมต่อกับตัว app id ของเรา

#define APPID   "SmartMirror"
microgear.connect(APPID);

7.เมื่อมีการเชื่อมต่อกับ netpie แล้วเราจะให้พิมพ์คำว่า "Connecting to netpie..."

void onConnected(char *attribute, uint8_t* msg, unsigned int msglen) {
  Serial.println("Connected to NETPIE...");
  microgear.setName(ALIAS);
}

และเรียกใช้โดย

microgear.on(CONNECTED, onConnected);

8.เมื่อมีข้อความเข้ามาจะให้สั่ง เปิด/ปิด ตัว led และ ส่งค่าแทนสถานะของ led เป็น 0 และ 1

void onMsghandler(char *topic, uint8_t* msg, unsigned int msglen) {
  Serial.print("Incoming message -->");
  msg[msglen] = '\0';
  Serial.println((char *)msg);
  if (*(char *)msg == '1') {
    digitalWrite(LED, LOW); // turn on the LED
    microgear.chat(TARGET, "1");
  } else {
    digitalWrite(LED, HIGH); //turn off the LED
    microgear.chat(TARGET, "0");
  }
}

และเรียกใช้โดย

microgear.on(MESSAGE, onMsghandler);


อ้างอิง : http://docs.netpie.io/th/latest/PieSketch/netpie/event.html
            : http://docs.netpie.io/th/latest/PieSketch/netpie/microgear.html
            : http://tesrteam.blogspot.com/2015/12/netpie-control-led-with-html5-by.html

การทดสอบเรื่องการใช้ Multiprocessing / Pool และ Queue

เบื้องต้นจะเป็นการทดสอบการเรียกฟังค์ชัน Image Processing จากรูป 1 รูป โดยแบ่งเป็น 2 กรณี

  1. ส่ง Input เป็นภาพเข้าไปในฟังค์ชัน Image Processing 
  2. ทำการ get ภาพจากภายในฟังค์ชันแทน

แบบ get ภาพจากในฟังค์ชัน 11 วินาที


แบบส่ง Input ภาพเข้าไปในฟังค์ชัน 3.7 วินาที

จะสังเกตได้ว่า ถึงแม้จะเป็นการใช้ 4 Process ในการเรียกฟังค์ชัน แต่การทำงานแบบป้อน Input เข้าฟังค์ชันนั้นจะทำให้ทำงานได้เร็วกว่าและใช้ CPU เต็มประสิทธิภาพกว่าแบบ get ภาพจากภายใจฟังค์ชัน


ทดสอบประสิทธิภาพของ Pool ในการ map function

การทดสอบขั้นต่อไปคือการแบ่งรูปภาพออกเป็น 4 ส่วน และทำการส่งเข้าไปประมวลผลโดยแบ่งเป็น 2 กรณีเช่นกัน คือ 
  1. map Input 4 รูปกับฟังค์ชัน Image Processing เดียว


  2. map Input 4 รูปกับฟังค์ชันที่ทำการ Group เอาไว้

ผลลัพธ์ที่ได้


การเปรียบเทียบโดยใช้ Graph



Single Image Processing Function



Group Image Processing Function

ประสิทธิภาพของการใช้ Queue ร่วมกับ Pool

ทำการเทียบประสิทธิภาพระหว่างการใช้ Queue ในการเก็บข้อมูลสำหรับใช้ระหว่าง Pool และแบบไม่ใช้ Queue ซึ่งจากปัญหาที่เคยพบก่อนหน้าคือ หากเราทำการส่ง Queue ผ่านการใช้ map ของ Pool โดยตรง จะทำให้เกิด Error


จึงได้ทำการแก้ไขโดย ปรับการประกาศใช้ Pool ใหม่ โดยเรียกแบบ Initializer จะทำให้สามารถส่ง Queue เข้าไปใช้ใน Pool ได้


การปรับวิธีเรียกใช้ Pool จากแบบปกติเป็น Initializer

ปัญหาที่พบต่อมาคือ ข้อมูลที่ถูกบันทึกลง Queue นั้นไม่ได้เรียงลำดับอย่างถูกต้องเนื่องจากการทำงานของตัว Process จะมองที่ว่าใครทำเสร็จก่อนก็บันทึกลง Queue ก่อน



จากภาพจะสังเกตได้ว่าตำแหน่งของภาพที่ 1 นั้นไม่ใช่ตำแหน่งที่ถูกต้องหาทำการ merge ภาพจะให้ข้อมูลที่ได้ไม่ถูกต้อง ทำการแก้ไขโดยใน array เก็บรูปให้เพิ่มค่า key ที่เป็นลำดับของภาพไว้ และใช้คำสั่ง Sort() หลังจากได้ผลสำเร็จจะทำให้ภาพกลับมาเรียงในตำแหน่งที่ถูกต้องอีกครั้ง


ผลลัพธ์ที่ได้


จากภาพจะพบว่าตำแหน่งของรูปนั้นอยู่ในลำดับที่ควรจะเป็นแล้ว


ผลลัพธ์จากการเทียบข้อมูลในกราฟพบว่าเวลาในการทำงานนั้นอยู่ในระดับที่ใกล้เคียงกัน


Pool without Queue


Pool with Queue