2018年12月29日 星期六

Tello 普通版編程


聖誕假期間,看到 Facebook 內的 DJI 廣告,發現一台入門級航拍機只需要 HK$619,實在很吸引。在航拍機未興起前已經想組裝一台,但一直沒有行動。之前都見過有價錢在 HK$400-$500 間的航拍機,不過質素及外型都不太好。現在 DJI 這台完全能滿足這兩點要求,只花幾百就買得到,於是買了一台回來。


Tello 的操作簡單,用手機 App 便能控制。規格中指飛行距離可達 100 米,而我嘗試過用 iPhone XR 或 iPad Pro 都只能去到 30 米左右,向上飛只能達三層樓高左右。旗艦店職員說是因為手機內的 WiFi 間片細,訊號接收力弱所導致;要是用遙控器則會好多了。我希望用電腦加帶天線的外置 WiFi USB 來控制 Tello,於是研究了一下,發現了 TelloPy 這個 Python 模組。編寫了一個簡單的測試程式,讓 Tello 起飛、拍照、降落。
##----------------------------------------------------------------------------------------
##  Tello DEMO Program
##----------------------------------------------------------------------------------------
##  Platform: Python 3.6 + TelloPy
##  Written by Pacess HO
##  Copyright Pacess Studio, 2018.  All rights reserved
##----------------------------------------------------------------------------------------

from time import sleep
import tellopy

##  Global variable
_counter = 0

##----------------------------------------------------------------------------------------
def handler(event, sender, data, **args):
   global _counter
   drone = sender
   
   if event is drone.EVENT_FLIGHT_DATA:
      print(data)
   
   if event is drone.EVENT_FILE_RECEIVED:
      _counter = _counter+1
      path = "tello_%s.jpg" % str(_counter)
      with open(path, "wb") as file:
         file.write(data)

##----------------------------------------------------------------------------------------
def flyNow():
   drone = tellopy.Tello()
   try:
      drone.subscribe(drone.EVENT_FLIGHT_DATA, handler)
      drone.subscribe(drone.EVENT_FILE_RECEIVED, handler)

      drone.connect()
      drone.wait_for_connection(60.0)
      
      drone.takeoff()
      sleep(3)
      
      drone.take_picture()
      sleep(3)
      
      drone.land()
      sleep(3)
   
   except Exception as ex:
      print(ex)
   
   finally:
      drone.quit()

##----------------------------------------------------------------------------------------
if __name__ == '__main__':
   flyNow()

2018年12月25日 星期二

用 TensorFlow + PoseNet 偵測骨骼位置


最近女兒參加了跳舞比賽,心想怎樣能用影像判斷出骨骼位置,從而收集動作數據呢?在網上找到以 TensorFlow + PoseNet 可以做到。以下是用 Javascript 編寫了一個簡單的偵測程序:
<html>
   <head>
      <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
      <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/posenet"></script>
   </head>

   <body>
      <canvas id="canvas"></canvas>
      <img id="photo" src="./sport_01.jpg" style="display:none;" />

      <script>
         var image = document.getElementById("photo");
         var imageScaleFactor = 0.2;
         var flipHorizontal = false;
         var outputStride = 16;

         //----------------------------------------------------------------------------------------
         function drawConnection(context, keypoints, partA, partB)  {
            var radius = 8;
            var partAPoint = null;
            var partBPoint = null;
            for (var i=0; i<keypoints.length; i++)  {

               var element = keypoints[i];
               var part = element.part;
               if (part != partA && part != partB)  {continue;}

               //  Either matches part A or part B
               if (part == partA)  {partAPoint = element.position;}
               if (part == partB)  {partBPoint = element.position;}

               //  Continue if not both position have been set
               if (partAPoint == null || partBPoint == null)  {continue;}

               //  Both parts are ready, connect them
               context.beginPath();
               context.arc(partAPoint.x, partAPoint.y, radius, 0, 2*Math.PI, false);
               context.fillStyle = 'green';
               context.fill();

               context.beginPath();
               context.moveTo(partAPoint.x, partAPoint.y);
               context.lineTo(partBPoint.x, partBPoint.y);
               context.strokeStyle = 'green';
               context.stroke();

               context.beginPath();
               context.arc(partBPoint.x, partBPoint.y, radius, 0, 2*Math.PI, false);
               context.fillStyle = 'green';
               context.fill();
            }
         }

         //----------------------------------------------------------------------------------------
         function drawConnection12(context, keypoints, partA, partB, partC)  {
            var radius = 8;
            var partAPoint = null;
            var partBPoint = null;
            var partCPoint = null;
            for (var i=0; i<keypoints.length; i++)  {

               var element = keypoints[i];
               var part = element.part;
               if (part != partA && part != partB && part != partC)  {continue;}

               //  Either matches part A or part B or part C
               if (part == partA)  {partAPoint = element.position;}
               if (part == partB)  {partBPoint = element.position;}
               if (part == partC)  {partCPoint = element.position;}

               //  Continue if not both position have been set
               if (partAPoint == null || partBPoint == null || partCPoint == null)  {continue;}

               var pointX = (partBPoint.x+partCPoint.x)/2;
               var pointY = (partBPoint.y+partCPoint.y)/2;

               //  Both parts are ready, connect them
               context.beginPath();
               context.arc(partAPoint.x, partAPoint.y, radius, 0, 2*Math.PI, false);
               context.fillStyle = 'green';
               context.fill();

               context.beginPath();
               context.moveTo(partAPoint.x, partAPoint.y);
               context.lineTo(pointX, pointY);
               context.strokeStyle = 'green';
               context.stroke();
            }
         }

         //----------------------------------------------------------------------------------------
         function drawConnection22(context, keypoints, partA, partB, partC, partD)  {
            var radius = 8;
            var partAPoint = null;
            var partBPoint = null;
            var partCPoint = null;
            var partDPoint = null;
            for (var i=0; i<keypoints.length; i++)  {

               var element = keypoints[i];
               var part = element.part;
               if (part != partA && part != partB && part != partC && part != partD)  {continue;}

               //  Either matches part A or part B or part C or part D
               if (part == partA)  {partAPoint = element.position;}
               if (part == partB)  {partBPoint = element.position;}
               if (part == partC)  {partCPoint = element.position;}
               if (part == partD)  {partDPoint = element.position;}

               //  Continue if not both position have been set
               if (partAPoint == null || partBPoint == null || partCPoint == null || partDPoint == null)  {continue;}

               var pointX1 = (partAPoint.x+partBPoint.x)/2;
               var pointY1 = (partAPoint.y+partBPoint.y)/2;
               var pointX2 = (partCPoint.x+partDPoint.x)/2;
               var pointY2 = (partCPoint.y+partDPoint.y)/2;

               //  Both parts are ready, connect them
               context.beginPath();
               context.arc(pointX1, pointY1, radius, 0, 2*Math.PI, false);
               context.fillStyle = 'green';
               context.fill();

               context.beginPath();
               context.moveTo(pointX1, pointY1);
               context.lineTo(pointX2, pointY2);
               context.strokeStyle = 'green';
               context.stroke();
            }
         }

         //----------------------------------------------------------------------------------------
         posenet.load().then(function(net)  {
            return net.estimateSinglePose(image, imageScaleFactor, flipHorizontal, outputStride);
         }).then(function(pose)  {

            var width = image.width;
            var height = image.height;

            var canvas = document.getElementById("canvas");
            canvas.width = width;
            canvas.height = height;

            var context = canvas.getContext("2d");
            context.drawImage(image, 0, 0);

            var keypoints = pose.keypoints;
            drawConnection(context, keypoints, "leftEye", "rightEye");
            drawConnection(context, keypoints, "leftEye", "nose");
            drawConnection(context, keypoints, "rightEye", "nose");

            drawConnection(context, keypoints, "leftEar", "leftEar");
            drawConnection(context, keypoints, "rightEar", "rightEar");

            drawConnection(context, keypoints, "leftShoulder", "rightShoulder");
            drawConnection(context, keypoints, "leftShoulder", "leftElbow");
            drawConnection(context, keypoints, "rightShoulder", "rightElbow");
            drawConnection(context, keypoints, "leftElbow", "leftWrist");
            drawConnection(context, keypoints, "rightElbow", "rightWrist");

            drawConnection(context, keypoints, "leftHip", "rightHip");
            drawConnection(context, keypoints, "leftHip", "leftKnee");
            drawConnection(context, keypoints, "rightHip", "rightKnee");
            drawConnection(context, keypoints, "leftKnee", "leftAnkle");
            drawConnection(context, keypoints, "rightKnee", "rightAnkle");

            drawConnection12(context, keypoints, "nose", "leftShoulder", "rightShoulder");
            drawConnection22(context, keypoints, "leftShoulder", "rightShoulder", "leftHip", "rightHip");
         });
      </script>
   </body>
</html>

2018年11月29日 星期四

「智泉拾叁」創作


2018 年 11 月 22 日,「智泉 13」正式開始。距離上一次的體驗式課程,已經相隔有 17 年左右。如同 19 年前參與「IN117」一樣,我也一起設計團隊標誌及製服;同時也發揮想像力,設計一些美術作品。其中一樣作品,是希望用元祖太極圖案,加上中文字「拾叁」來創作。如果一個一個地畫出太極實在很耗時,作為程式員,這個情景可以幫得上忙。


要製作出這樣的效果,首先要準備一張遮罩圖。黑色代表繪畫太極的空間,白色代表留白的地方。把遮罩圖黑色地方記下,然後在這些地方隨機生成太極的圖案。我希望把整個過程以動畫方式呈現,所以加入 Circle 類別用來處理太極由小變大的過程;並且把每一步驟的幀記錄下來。最後,以 ffmpeg 指令「ffmpeg -framerate 60 -i out_%04d.png -s:v 1024x550 -c:v libx264 -profile:v high -crf 20 -pix_fmt yuv420p w13.mp4」生成影片。
<?php
//----------------------------------------------------------------------------------------
//  Packing Circle with Mask Image
//----------------------------------------------------------------------------------------
//  Platform: macOS Mojave + PHP5
//  Written by Pacess HO
//  Copyrights Pacess Studio, 2018.  All rights reserved.
//----------------------------------------------------------------------------------------

class Circle  {
   private $isGrowing = true;

   public $x = 0;
   public $y = 0;
   public $r = 3.0;

   //----------------------------------------------------------------------------------------
   function setup($x, $y)  {
      $this->x = $x;
      $this->y = $y;
      $this->r = 3.0;
   }

   //----------------------------------------------------------------------------------------
   function draw($image)  {
      $foreground = imagecolorallocate($image, 0, 0, 0);
      imageellipse($image, $this->x, $this->y, $this->r*2, $this->r*2, $foreground);
   }

   //----------------------------------------------------------------------------------------
   function grow()  {
      if ($this->isGrowing == false)  {return;}
      $this->r++;

      if ($this->r > 30)  {$this->isGrowing = false;}
   }

   //----------------------------------------------------------------------------------------
   function stopGrow()  {$this->isGrowing = false;}

   //----------------------------------------------------------------------------------------
   function isEdge($width, $height)  {
      if (($this->x-$this->r) < 0)  {return true;}
      if (($this->y-$this->r) < 0)  {return true;}
      if (($this->x+$this->r) > $width)  {return true;}
      if (($this->y+$this->r) > $height)  {return true;}
      return false;
   }
}

//========================================================================================
//  Main program
$_width = 1024;
$_height = 768;

//  Loading mask image
list($_width, $_height) = getimagesize("w13.png");
$backgroundImage = imagecreatefrompng("w13.png");

//  Convert mask into spot array
$spotArray = array();
for ($y=0; $y<$_height; $y++)  {
   for ($x=0; $x<$_width; $x++)  {
      $color = imagecolorat($backgroundImage, $x, $y);
      $blue = $color&255;
      if ($blue >= 80)  {continue;}

      $spotArray[] = array($x, $y);
   }
}

//----------------------------------------------------------------------------------------
//  Create logo animation
$max = 99999;
$_array = array();
for ($i=0; $i<1000; $i++)  {

   //  New circle
   for ($j=0; $j<5; $j++)  {

      $valid = true;
      $value = rand(0, count($spotArray));
      $spot = $spotArray[$value];
      $x = $spot[0];
      $y = $spot[1];

      $newCircle = new Circle();
      $newCircle->setup($x, $y);
      foreach ($_array as $circle)  {

         $distance = sqrt(pow($circle->x-$newCircle->x, 2)+pow($circle->y-$newCircle->y, 2));
         if ($distance < ($circle->r+$newCircle->r+2))  {$valid = false;}
      }

      if ($valid == true && $max > 0)  {
         $_array[] = $newCircle;
         $max--;
      }
   }

   //  Create image
   $image = imagecreatetruecolor($_width, $_height);
   $foreground = imagecolorallocate($image, 0, 0, 0);
   $background = imagecolorallocate($image, 255, 255, 255);
   imagefilledrectangle($image, 0, 0, $_width, $_height, $background);
   foreach ($_array as $circle)  {

      $boolean = $circle->isEdge($_width, $_height);
      if ($boolean == true)  {$circle->stopGrow();}

      $x = $circle->x;
      $y = $circle->y;
      $r = $circle->r;
      imageellipse($image, $x, $y, $r*2, $r*2, $foreground);

      //  Overlapping
      $overlapping = false;
      foreach ($_array as $circle2)  {

         if ($circle == $circle2)  {continue;}
         $distance = sqrt(pow($circle->x-$circle2->x, 2)+pow($circle->y-$circle2->y, 2));
         if ($distance < ($circle->r+$circle2->r+2))  {$circle->stopGrow();}
      }

      $circle->grow();
   }

   $filename = sprintf("out_%04d.png", $i);
   imagepng($image, $filename);
   imagedestroy($image);
}
?>

2018年11月10日 星期六

準備 WhatsApp 貼紙格式


最近 WhatsApp 推出貼紙功能,特別之處是貼紙不是由內部的商店下載,而是用外部 App 加入。WhatsApp 提供了參考程式,方便大眾自行加入貼紙。不過,貼紙必須為 512x512 像素 PNG 或 WEBP 格式。

我有一些貼紙不是這個格式,於是編寫了 PHP 程式做準備工作:
<?php
//----------------------------------------------------------------------------------------
//  Create a square base transparent image
//----------------------------------------------------------------------------------------
//  Platform: macOS Mojave + PHP
//  Written by Pacess HO
//  Copyright Pacess Studio, 2018.  All rights reserved.
//----------------------------------------------------------------------------------------

$width = 512;
$height = 512;

//----------------------------------------------------------------------------------------
$fileArray = scandir("./");
foreach ($fileArray as $filename)  {

   //  Skip directories
   if ($filename == ".")  {continue;}
   if ($filename == "..")  {continue;}

   $index = strpos($filename, ".png");
   if ($index == false)  {continue;}

   //  This is a PNG file, create output image
   echo("Processing $filename...\n");
   $outputImage = imagecreatetruecolor($width, $height);
   imagealphablending($outputImage, false);
   $color = imagecolorallocatealpha($outputImage, 255, 255, 255, 127);
   imagefilledrectangle($outputImage, 0, 0, $width, $height, $color);
   imagealphablending($outputImage, true);

   //  Get image size
   $size = getimagesize($filename);
   $imageWidth = $size[0];
   $imageHeight = $size[1];

   //  Calculate zoom scale
   $scaleW = $width/$imageWidth;
   $scaleH = $height/$imageHeight;

   $scale = $scaleW;
   if ($scaleW > $scaleH)  {$scale = $scaleH;}
   $zoomWidth = intval($imageWidth*$scale);
   $zoomHeight = intval($imageHeight*$scale);

   //  Put image to output image
   $x = intval(($width-$zoomWidth)/2);
   $y = intval(($height-$zoomHeight)/2);

   $stickerImage = imagecreatefrompng($filename);
   if ($stickerImage == null)  {continue;}
   imagecopyresampled($outputImage, $stickerImage, $x, $y, 0, 0, $zoomWidth, $zoomHeight, $imageWidth, $imageHeight);

   imagealphablending($outputImage, false);
   imagesavealpha($outputImage, true);
   imagepng($outputImage, "_".$filename);
   imagedestroy($outputImage);
}
?>

2018年11月5日 星期一

用 Python 修正相片日期


上星期參加了 WTIA 舉辦的「2018 北台灣物聯網投資合作商機考察參訪團」。帶了相機及手機拍攝活動相片。回到香港,才發現相機在 8 月到名古屋時調快了一小時,於是編寫以下 Python 程式,讀取相片中的 EXIF 資料,把所有 Canon 拍攝的相片日期都調慢一小時,變回正確時間。
##----------------------------------------------------------------------------------------
##  Fix Photo Creation Date
##----------------------------------------------------------------------------------------
##  Platform: macOS Mojave + Python 2.7
##  Copyrights Pacess Studio, 2018.  All rights reserved.
##----------------------------------------------------------------------------------------

import os
import time
import exifread

##----------------------------------------------------------------------------------------
##  Global variables
_path = "./"

##----------------------------------------------------------------------------------------
##  Get files from directory
for root, dirs, files in os.walk(_path):

   for file in files:
   
      if file.startswith("."):
         continue

      if not file.endswith(".JPG"):
         continue

      print("\nProcessing "+file+"...", end="")

      ##----------------------------------------------------------------------------------------
      ##  Get EXIF
      handle = open(_path+file, "rb")
      tags = exifread.process_file(handle)
   
      machine = str(tags["Image Make"])
      print(machine, end="")

      ##  Process only if "Canon"
      if "Canon" not in machine:
         continue

      ##----------------------------------------------------------------------------------------
      ##  Subtract one hour
      #datetime = os.path.getmtime(file)
      #timeString = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(datetime))
      timeString = str(tags["Image DateTime"])
      datetime = time.mktime(time.strptime(timeString, "%Y:%m:%d %H:%M:%S"))

      newDatetime = datetime-(60*60)
      newTimeString = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(newDatetime))
      
      os.utime(_path+file, (newDatetime, newDatetime))
      print(" ("+timeString+" => GMT:"+newTimeString+")", end="")

print("\nDone\n")

2018年10月8日 星期一

把 Darknet 模型轉換成 CoreML 模型

要把 Darknet 模型轉換成 CoreML 模型,先用 Darkflow 把權重儲存成 TensorFlow PB 檔:
$ ./flow --model yolo-c3.cfg --load yolo-c3.weights --savepb
然後以 tfcoreml 做轉換。由於 tfcoreml 使用到不同的軟件版本組合,所以最好是用 Conda 之類的虛擬環境把軟件獨立出來:
$ git clone https://github.com/tf-coreml/tf-coreml.git
$ cd tf-coreml/
$ conda create --name tf-coreml python=3.6
$ source activate tf-coreml
$ pip install -e .
安裝好所需軟件版本後,下一步是正式轉換。把 darkflow/built_graph/yolo-c3.pb 拷到 tf-coreml 目錄,並進入 Python:
$ python
輸入以下 Python 程序。留意把下面「kerasModelPath」的值改為自己的 PB 檔路徑:
import tfcoreml as tf_converter
import tensorflow as tf

##----------------------------------------------------------------------------------------
##  We load the protobuf file from the disk and parse it to retrieve the unserialized graph_def
def load_graph(frozen_graph_filename):
    with tf.gfile.GFile(frozen_graph_filename, "rb") as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
        
    # Then, we import the graph_def into a new Graph and return it 
    with tf.Graph().as_default() as graph:
        tf.import_graph_def(graph_def, name="")
    return graph

##----------------------------------------------------------------------------------------
##  Load Keras model
kerasModelPath = 'yolo-c3.pb'
graph = load_graph(kerasModelPath)
for op in graph.get_operations(): 
    print (op.name)

##----------------------------------------------------------------------------------------
##  Convert Keras model to Core ML model
##  output_feature_names: the output node name we get from the previouse step
##  image_input_names: CoreML allows image as the input, the only thing we need to do is to set which node is the image input node 
##  input_name_shape_dict: the input node name we get from the previous step, and check the cfg file to know the exact input shape size
##  is_bgr: the channel order is by BGR instead of RGB
##  image_scale: the weights is already normalized in the range from 0 to 1
coreml_model = tf_converter.convert(tf_model_path=kerasModelPath, mlmodel_path='yolo.mlmodel', output_feature_names=['grid'], image_input_names= ['image'], input_name_shape_dict={'image': [1, 416, 416, 3]}, is_bgr=True, image_scale=1/255.0)
完成後便會得到 tf-coreml/yolo-c3.mlmodel 模型檔。

2018年10月7日 星期日

把 Darknet 模型轉換成 Keras 模型


要把 Darknet 模型轉換成 Keras 模型,可以用 YAD2K: Yet Another Darknet 2 Keras。我發現它用的 Python, Keras, TensorFlow 用的又是不同的版本組合,所以最好是用 Conda 之類的虛擬環境把軟件獨立出來:
git clone https://github.com/allanzelener/yad2k.git
cd yad2k
conda env create -f environment.yml
source activate yad2k
訓練好的 YOLO v1 模型,亦即是 yolo-c3.cfg 及 yolo-c3.weights 檔案拷到 YAD2K 目錄,然後執行:
python yad2k.py yolo-c3.cfg yolo-c3.weights yolo-c3.h5
完成後便能看見 Keras 的 yolo-c3.h5 模型檔。

2018年10月6日 星期六

訓練 YOLO v1 模型

準備好訓練 YOLO v1 模型的工作後,下一步是製作模型結構及設定。我們將會訓練 YOLO v1 模型,把在 darknet/cfg/yolo.cfg 複製一份,改名為 yolo-c3.cfg。當中「c3」的意思是「Classes 3」,即模型能偵測三款物件。


打開 yolo-c3.cfg,把 # Testing# Training 中的行數,在最頭加入井號變成註釋;把 # Training 下的 batch 設定為 64、subdivisions 設定為 8。如果在訓練期間出現記憶體不足,可以將 batch 值調大,或把 subdivisions 值調低再試試。


在檔案最尾的 [convolutional] 中,把 filters 設定為 40。數值來自公式:
(5 + 偵測物件類別數量) x 5 = (5 + 3) x 5 = 40
並且,把 [region] 下的 classes 設定為 3。之後,生成一個 darknet/cfg/yolo-c3.names 文字檔,裡面記載物件類別的名字。我的情況是:
EDO Pack
烏龍茶
可口可樂
接著,生成一個 darknet/cfg/yolo-c3.data,內容為:
classes = 3
train = train.txt  
valid = test.txt  
names = yolo-c3.names  
backup = backup/
主要是告知 YOLO 有三個偵測類別、訓練的素材在哪、驗證的素材在哪、類別名稱在哪、訓練時的比重放在哪。還有,我們需要 YOLO 預設的權重檔「darknet19_448.conv.23」,可以在這裡下載

最後,確定好所需的檔案都齊備:
darknet/darknet19_448.conv.23
darknet/test.txt
darknet/train.txt
darknet/cfg/yolo-c3.cfg
darknet/cfg/yolo-c3.data
darknet/cfg/yolo-c3.names
在 Terminal 執行:
./darknet detector train cfg/yolo-c3.data cfg/yolo-c3.cfg darknet19_448.conv.23
根據我的經驗,在安裝 darknet, darkflow, YOLO-Annotation-Tool-master,...等工具時都會出現不同的錯誤。有的要求 Python 2.7;有的要求 Python 3.6。有時要 Keras 1.2.2;有時又要低一點的版本;這都可以用 Virtual Environment 來解決。但遇著 CUDA 及 CUDNN 版本問題則比較頭痛。在今次訓練中,需要分別用到 CUDNN 8 及 CUDNN 9,兩者只能選一個,只能用到哪個版本就安裝哪個版本。訓練原本做 100 次迭代便完成,但我的情況卻沒完沒了。只好手動停止。在 darknet/backup/ 中能找到權重檔,加上本身 yolo-c3.cfg 結構檔便能拿來做檢測。

2018年10月5日 星期五

訓練 YOLO v1 模型的準備工作


既然昨天成功用 YOLO v2 來檢測物件,午飯時再接再厲,再用準備好的素材訓練,看看能不能有把自定義物件檢測出來。


按照 How to train multiple objects in YOLOv2 using your own Dataset 的指示,一步一步訓練自己的模型。首先,當然是準備好素材:可樂 10 張、EDO 糖 19 張、烏龍茶 16 張,總共 45 張相片。


然後把它們按類別以數字目錄分成三組,放到 YOLO-Annotation-Tool-master/Images/ 目錄下。由於原本已經有 001 及 002 目錄,所以我用 010 放 EDO 糖, 011 放烏龍茶及 012 來放可口可樂。然後在 Terminal 跳轉到 YOLO-Annotation-Tool-master 目錄,以 Python 2.7 執行:
python main.py

一個工具視窗會彈出來。在 Image Dir 欄位輸入其中一個剛才生成的目錄數字,點擊「Load」載入相片。利用滑鼠點擊相片中物件的左上角,拖拉到右下角再點一下。這樣便完成一年物件的標韱工作。如果相片有多於一件同類物件,則把其他物件也一併標韱。


完成所有數字目錄的標韱後,在 YOLO-Annotation-Tool-master/Labels/ 目錄下會發現記錄了的標韱座標。


不過,這些數字還未 Normalize 到 0 至 1 的小數,所以還未能夠使用。這時,需要打開 convert.py,在 classes 中加入 "010", "011", "012";設定 mypath = "./Labels/010/";再把 cls = "005" 改為 cls = "010",執行一次:
python convert.py
然後 "011" 執行一次;再做 "012"。逐個數字目錄做。同時,在 YOLO-Annotation-Tool-master/ 會自動生成 010_list.txt, 011_list.txt 及 012_list.txt,裡面記載了標韱的相片路徑。


完成後在 YOLO-Annotation-Tool-master/Labels/output 會找到 Normalize 了的座標。接著執行:
python process.py
它會把素材以 80:20 比例分成訓練組及測試組,並生成 train.txt 及 test.txt。

2018年10月4日 星期四

YOLO v2 街頭物件檢測


經過反覆嘗試,很困難才能生成出 YOLO v2 的 mlmodel。可是,當放進 iPad 後卻沒有反應,無法檢測出物件。可能我太過心急,急於一步到位,要在 iPad 上判定物件,連模型本身是否正常運作也未知道;所以,決定先在電腦上成功讓模型運作,之後才放到 iPad 上。


於是,我走到街上拍攝幾段影片,用來實作 YOLO 檢測功能。


然後利用 Darknet 準備好的 YOLO v2 模型進行測試。效果不錯。


這四段影片以 4K 解像度拍攝,然後利用 CentOS 7 + Nvidia GTX680 + Tensorflow + Darkflow 來進行檢測,並把結果儲存成 AVI 格式。所以,以上片段並不是實時執行。檢測的指令如下:
python flow --model yolov2-tiny.cfg --load yolov2-tiny.weights --demo Video.MOV --gpu 1.0 --saveVideo

2018年10月2日 星期二

標韱 YOLO 物件


嘗試過預設的檢測模型後,接著是以自定義的物件來訓練模型。在 https://github.com/ManivannanMurugavel/YOLO-Annotation-Tool 找到了一個簡單好用的標韱工具。

我準備了三款物件共 45 張相片來小試牛刀。利用上面的工具,逐一定義相片中的物件,然後利用下面的指令進行訓練:
./darknet detector train cfg/yolo-obj.data cfg/yolo-obj.cfg darknet19_448.conv.23
可是,在沒有 GPU 的場景下,訓練了 80 小時也未能完成,最終竟然發生錯誤...。

2018年9月28日 星期五

初試 YOLO


放低了「機器學習」一段時間,又是時候進修一下。每次學習之前,我都會訂立一個成果,然後找方法達成。今次,我想用 iPad 鏡頭去辨認物件;在網上搜尋相關的做法,找到了一個較新的 YOLO 算法像乎不錯,於是嘗試一下。這篇文章對 YOLO 算法進行解說,清楚易明。

YOLO 的官方網頁是 https://pjreddie.com/darknet/yolo/。按照指示安裝好 Darknet,下載已訓練好的模型便能使用。不過,要執行 Darknet 需要花一點功夫去設定好運行環境;而我的過程不太順利。經過一輪版本問題後,終於成功輸入圖片並檢測當中的物件。我用的是 YOLO v2,預設能檢測 80 樣不同的物品。需要下載或自行製作 .cfg 及 .weights 檔。前者是定義智能網絡的結構,後者是該結構每一個節點的比重值。有了這兩組數據,便能進行偵測。

2018年9月25日 星期二

micro:bit 示範程式


早前想買一塊 BBC 的 micro:bit 來玩玩;到大阪日本橋時找到它,但最後沒有購買。昨天,女兒從中學帶回來一塊 micro:bit 及 robot:bit,於是我研究了一下,並編寫了第一個程式:

//----------------------------------------------------------------------------------------
//  micro:bit DEMO Program 01
//----------------------------------------------------------------------------------------
//  Platform: micro:bit Javascript or Blocks
//  Written by Pacess HO
//  Copyright Pacess Studio, 2018.  All rights reserved.
//----------------------------------------------------------------------------------------

let x = 0
let y = 0
let led1 = 0
let led2 = 0
let led3 = 0
let led4 = 0
let step = 0
let offset = 0
let direction = 0

led1 = 0
led2 = 50
led3 = 100
led4 = 150
step = 20
offset = 0
direction = 1

music.beginMelody(music.builtInMelody(Melodies.Birthday), MelodyOptions.OnceInBackground)
basic.forever(() => {

   offset = offset + direction
   if (offset <= 0) {
      direction = 1
   }
   if (offset >= 5 * 5 - 1) {
      direction = -1
   }

   x = offset % 5
   y = offset / 5

   led.plot(x, y)
   basic.pause(80)
   led.unplot(x, y)

   robotbit.rgb().setPixelColor(0, neopixel.hsl(led1, 50, 30))
   robotbit.rgb().setPixelColor(1, neopixel.hsl(led2, 50, 50))
   robotbit.rgb().setPixelColor(2, neopixel.hsl(led3, 50, 50))
   robotbit.rgb().setPixelColor(3, neopixel.hsl(led4, 50, 30))
   robotbit.rgb().show()

   led1 = led1 + step
   led2 = led2 + step
   led3 = led3 + step
   led4 = led4 + step
})

2018年9月24日 星期一

外幣兌換率數據


這個星期收集了不少數據,有些還沒有想到用途;然而,有些數據則會拿來作為投資趨勢分析,再加上 LINE Bot 作為訊息媒體,希望能及時通知自己關於投資的機會。其中一樣,就是外幣兌換率數據。利用定時執行的程式,去找出哪隻外幣值得買入,又或是留意有哪隻外幣應該拋售。

2018年9月23日 星期日

香港恆生指數數據


朋友介紹我玩期指。做了咁多年人,知道自己沒有橫財命,所有成就都要靠一雙手,腳踏實地去掙回來;炒股賭博不是我杯茶。不過,我絕對不介意從數學角度出發,去探求提升勝算之方法。研究是需要數據,於是花了一個小時,收集起恆生指數數據。

2018年9月22日 星期六

地震數據


又花了一個小時編寫數據抓取程式。今次爬的是世界各地的地震數據。原來一天內有不多地震...。目前這些數據雖然沒有特定用途,但日後把它們導入「機器學習」中,可能會發掘出人類發現不了的模式。雖然香港政府沒有做統一數據開放,但想要數據的人,目前還是有方法能抓到所需。

2018年9月21日 星期五

香港潮汐漲退數據


除了日落日出數據外,還有甚麼天文數據可以抓下來?我發現了潮汐漲退數據。只要把原先用的數據擷取程式稍微改動一下,便能抓取潮汐漲退數據。暫時還沒有想到確切的用途,不知道是否能從數據中推斷出天災的降臨呢?

2018年9月20日 星期四

香港日落日出數據


我正在一點一滴地收集不同的數據;雖然還未想到怎麼用,但有些事情還是越早開始越好;這樣才能收集到數據的變化。自從學習術數後,知道日月星辰的軌跡能影響人類的表現,所以一提起收集數據,我便特別希望收集天文相關的數據。香港的溫度數據收集了,下一組便是日出日落數據。花了一小時完成了程式,設定好定時器,每天自動收集數據,又多了一點點資源去應付未來的需要。

2018年9月7日 星期五

香港樓宇成交數據


同事希望取得香港樓宇成交數據作為分析之用,給我一個參考網址。認為數據除了能幫到他外,也能滿足到我的好奇心;於是花了兩個半小時編寫出一個數據擷取程式。它會每天自動執行,收集租賃及售樓的成交數據,儲存在 MySQL 數據庫中。

既然數據到手,我也嘗試看看當中的啟示。首先是顯示 2018 年 6-8 月份售樓成交量最多的地區:
SELECT min(contractDate) AS min, max(contractDate) AS max, district, COUNT(*) AS count, avg(price/saleableArea) AS average_price FROM `estate_price` WHERE contractDate>="2018-06-01" AND contractDate<"2018-07-01" GROUP BY district ORDER BY count DESC, min ASC, max ASC LIMIT 10;




得出最近三個月,元朗區的樓盤成放宗數一直上升;不過每尺價錢回落了,每尺售價 HK$12817。大埔區也很利害,每個月的成交宗數都破 400。

之後是顯示 2018 年 6-8 月份租賃成交量最多的地區:
SELECT min(contractDate) AS min, max(contractDate) AS max, district, COUNT(*) AS count, avg(price/saleableArea) AS average_rent FROM `estate_rent` WHERE contractDate>="2018-06-01" AND contractDate<"2018-07-01" GROUP BY district ORDER BY count DESC, min ASC, max ASC LIMIT 10;




原來沙田區是最受租賃歡迎;跟售樓一下,七月份每尺租金上升後,到八月份回落少許,每尺租 HK$43.17。

再看看八月份最多成交的元朗區,最受歡迎的十大樓盤:
SELECT min(contractDate) AS min, max(contractDate) AS max, district, estate, COUNT(*) AS count, avg(price/saleableArea) AS average_price FROM `estate_price` WHERE contractDate>="2018-08-01" AND contractDate<"2018-09-01" AND district="元朗" AND saleableArea>0 GROUP BY estate ORDER BY count DESC, min ASC, max ASC LIMIT 10;


原來是「Yoho Midtown」!尺價達 HK$16706,年輕人怎樣才能供得起?!

有了這些數據,只要編寫一個報告板,便能顯示走勢;若配合 LINE Bot 的話,更可以在運算出投資建議時發出即時訊息呢。

2018年8月8日 星期三

Sita 的即時翻譯功能


外遊在即,雖然我的日語尚算靈光,但老人家少不免會一時忘記句子說法;為了解決這個問題,於是花了一小時教懂了私人助理 Sita 多國語言。在有需要時可以隨時請 Sita 幫忙。

相信大家能估到背後使用了 Google API;不過,不是收費或需要信用卡資料的 Google Cloud API。重點是免費。原來 Google 有一條免費的 API 可供隨時使用。不過,在開發時遇到小問題;當要由中文轉成英文,或日文轉換成英文時,結果卻是爛字。花了好一會時間,才發現需要加入 User Agent 去解決。以下是相關 PHP 程式碼:
//----------------------------------------------------------------------------------------
//  Translation
//  zh-tw: = Colon is at position 5
$count = substr_count($incomingMessage, ":");
if ($count >= 2)  {

 $array = explode(":", $incomingMessage);
 $count = count($array);
 if ($count >= 3)  {

  $sourceLanguage = $array[0];
  $targetLanguage = $array[1];

  $position = strlen($sourceLanguage)+1+strlen($targetLanguage)+1;
  $text = mb_substr($incomingMessage, $position);

  $url = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=".$sourceLanguage."&tl=".$targetLanguage."&dt=t&q=".urlencode($text);

  $curl = curl_init();
  curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "GET");
  curl_setopt($curl, CURLOPT_URL, $url);
  curl_setopt($curl, CURLOPT_TIMEOUT, 10);
  curl_setopt($curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36");
  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  $response = curl_exec($curl);
  curl_close($curl);

  if ($response != null)  {

   $json = json_decode($response);
   $translatedText = $json[0][0][0];
   $_lineBot->sendText($replyToken, $translatedText." ($text)");
   break;

  }  else  {
   $_lineBot->sendText($replyToken, "Translation API return null...");
   break;
  }
 }  else  {
  $_lineBot->sendText($replyToken, "We need 3 translation parameters, but only $count now...");
  break;
 }
}

2018年6月23日 星期六

機器學習基礎.第三課


機器學習基礎」來到第三課,亦是最後一課。承接首兩堂的系統設定、Python 編程、數據收集,今天教會學生如何訓練模型去分辨樣貌。我們即場拍了數十張相片,有學生也有老師;把相片輸入到系統並進行訓練。成功令機器分辨出各人的樣貌。希望學生回家能導入不同的相片,訓練模型去處理更多的分類工作,繼續探索及研究。

在此,多謝東華三院張明添中學的梁老師及東華三院甲寅年總理中學的溫副校的信任與支持!我才有機會向同學分享機器學習的知識,課程才得以順利完成。

2018年6月11日 星期一

收集股票詳細交易資料


今日放工時,跟同事談起收集到的世界盃數據,他希望用相同方法收集股票數據。對我來說,自從上年五月 Yahoo 停止了 API 運作後,已經好一段時間沒有收集到數據;雖然兩三個月前重新開始,而且也能順利收集;不過,今次卻是拿 Yahoo 沒提供的詳細交易資料,於是我接下這個案件。急不及待地在晚上研究取得數據的方法,編寫自動化程式,正式開始收集數據。下一步是利用這些數據及股價升跌去尋找買入賣出機會。

2018年6月10日 星期日

自動下載抖音影片


女兒愛玩抖音,拍了一些短片。我希望能把它保存起來,於是用 Charles Proxy 研究了一下當中的通訊內容;然後寫了下面的 PHP 程式,配合 crontab 定時執行。只要女兒有公開的影片,程式便會自動下載。
<?php
//----------------------------------------------------------------------------------------
//  Douyin Video Downloader
//----------------------------------------------------------------------------------------
//  Platform: CentOS 7 + PHP 5
//  Written by Pacess HO
//  Copyright Pacess Studio, 2018.  All rights reserved.
//----------------------------------------------------------------------------------------

//  Global variables
$_downloadDirectory = "./__files__/";

$_readLiveList = true;
$_responseFile = "dorothy.json";

$_userID = "6519251381372489395";
$_url = "https://api.tiktokv.com/aweme/v1/aweme/post/?version_code=2.2.1&language=ja"+
        "&app_name=trill&vid=18DD92EA-42E1-848F-4BA4-CDFAB132F79D&app_version=2.2.1"+
        "&carrier_region=HK&is_my_cn=1&channel=App%20Store&mcc_mnc=45406"+
        "&device_id=6539510560658635543&tz_offset=28800&account_region=HK&sys_region=HK"+
        "&aid=1180&screen_width=640&openudid=92046a2f432be94bec4d1b7369d754ab57259918"+
        "&os_api=18&ac=WIFI&os_version=11.4&app_language=ja&tz_name=Asia/Hong_Kong"+
        "&device_platform=iphone&build_number=22102&device_type=iPhone8,4"+
        "&iid=6564579886684473090&idfa=CE02FD8A-E2BA-44B5-884B-C03EBAFC413B"+
        "&count=21&max_cursor=0&min_cursor=0&user_id=$_userID"+
        "&mas=035160f738fae1dd802eef2d7ae9b80526a7d06a0300db4d1378d8&as=".time();

//========================================================================================
//  Main program
$response = null;
if ($_readLiveList == true)  {

   //  Get response from server
   $curl = curl_init($_url);
   curl_setopt($curl, CURLOPT_HEADER, 0);
   curl_setopt($curl, CURLOPT_TIMEOUT, 30);
   curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
   curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
   $response = curl_exec($curl);
   curl_close($curl);

   file_put_contents($_responseFile, $response);

}  else  {

   //  Get response from file
   $response = file_get_contents($_responseFile);
}

//----------------------------------------------------------------------------------------
$json = json_decode($response, true);

if (isset($json["aweme_list"]) == false)  {
   echo("### Aweme list not found...\n");
   exit(-1);
}

$awemeArray = $json["aweme_list"];
$videoCount = count($awemeArray);
echo("$videoCount videos found...\n");
foreach ($awemeArray as $aweme)  {

   //  Get video create time, use this value for filename
   if (isset($aweme["create_time"]) == false)  {
      echo("### Create time not found...\n");
      continue;
   }
   $createTime = $aweme["create_time"];

   //  Check if video already exists
   $filename = $_userID."_".$createTime.".mov";
   $filePath = $_downloadDirectory.$filename;

   $exists = file_exists($filePath);
   if ($exists == true)  {
      echo("Skip video [$filename]...");
      continue;
   }

   //----------------------------------------------------------------------------------------
   //  Video not exists, check for video URL
   if (isset($aweme["video"]) == false)  {
      echo("### Video not found...\n");
      continue;
   }

   $videoDictionary = $aweme["video"];
   if (isset($videoDictionary["play_addr"]) == false)  {
      echo("### Video address not found...\n");
      continue;
   }

   $videoAddressArray = $videoDictionary["play_addr"]["url_list"];
   $count = count($videoAddressArray);
   $index = 0;
   if ($index >= $count || $index < 0)  {
      echo("### Invalid address index #$index...\n");
      continue;
   }

   $videoAddress = $videoAddressArray[$index];
   echo("Downloading [$videoAddress]...");

   $curl = curl_init($videoAddress);
   curl_setopt($curl, CURLOPT_HEADER, false);
   curl_setopt($curl, CURLOPT_TIMEOUT, 30);
   curl_setopt($curl, CURLOPT_BINARYTRANSFER, true);
   curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
   curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
   $response = curl_exec($curl);
   curl_close($curl);

   //  The content is share information only, need to download video with URL inside
   $count = file_put_contents($filePath, $response);
   echo("$count saved.\n");
}

?>

2018年6月9日 星期六

Donkeycar 的 Tensorflow 錯誤解決方法


既然 Raspberry Pi 3 Model B+ 無法使用 Donkeycar 的官方影像,就只好自行安裝。要在 NOOBS 成功安裝 Donkeycar 所須配件,網友 Dennis 指示以 https://github.com/wroscoe/donkey/blob/bd311a19acdc36b198882d3afbe9a023673ca007/install/make_pi_disk_img.sh 內的做法。花了數小時安裝,在啟動 Donkeycar 時,調用 Tensorflow 出現錯誤。嘗試了不同方法,最後發現轉用最新 Tensorflow 1.8.0 能解決問題。以下是我的安裝步驟:
##----------------------------------------------------------------------------------------
##  Donkey installation script for NOOBS.
##  Original version by wroscoe
##  Modified version by Pacess HO, 2018.
##----------------------------------------------------------------------------------------

##  Manually make sure the camera and I2C are enabled
sudo raspi-config

##  Standard updates (5 min)
sudo apt update -y
sudo apt upgrade -y
sudo rpi-update -y

##  Helpful libraries (2 min)
sudo apt install build-essential python3-dev python3-distlib python3-setuptools python3-pip python3-wheel -y
sudo apt install libzmq-dev -y
sudo apt install xsel xclip -y

##  Install numpy which is needed for OpenCV
pip3 install pandas
pip3 install h5py==2.8.0rc1

##  Install numpy and pandas (3 min)
sudo apt install libxml2-dev python3-lxml -y
sudo apt install libxslt-dev -y

##  Create a python virtualenv (2 min)
sudo apt install virtualenv -y
virtualenv donkey --system-site-packages --python python3
echo '##  Start donkeycar envoronment' >> ~/.bashrc
echo 'source ~/donkey/bin/activate' >> ~/.bashrc
source ~/.bashrc

##  Install redis-server (1 min)
sudo apt install redis-server

##  Install OpenCV (1 hour)
sudo apt-get install build-essential git cmake pkg-config -y
sudo apt-get install libjpeg-dev libtiff5-dev libjasper-dev libpng12-dev -y
sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev -y
sudo apt-get install libxvidcore-dev libx264-dev -y
sudo apt-get install libatlas-base-dev gfortran -y

##  NOTE: this gets the dev version. Use tags to get specific version
git clone https://github.com/opencv/opencv.git --depth 1
git clone https://github.com/opencv/opencv_contrib.git --depth 1

cd ~/opencv
mkdir build
cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
  -D CMAKE_INSTALL_PREFIX=/usr/local \
  -D INSTALL_C_EXAMPLES=OFF \
  -D INSTALL_PYTHON_EXAMPLES=OFF \
  -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib/modules \
  -D BUILD_EXAMPLES=OFF ..
make -j4
sudo make install
sudo ldconfig

##  Install tensorflow (5 min)
tf_file=tensorflow-1.8.0-cp35-none-linux_armv7l.whl
wget https://github.com/lhelontra/tensorflow-on-arm/releases/download/v1.8.0/${tf_file}
pip install ${tf_file}

##  Install donkey (1 min)
git clone https://github.com/wroscoe/donkey.git donkeycar
pip install -e donkeycar/[pi]

2018年6月8日 星期五

Raspberry Pi 多連線設定


3 月 23 日購買了一塊 Raspberry Pi 3 Model B+。經過兩個月的手續,在 5 月 21 日取得香港的入口簽証。前晚收到過數通知,到昨天收到由新加坡寄來的板子。

我拿了在 Raspberry Pi 2 開發的 Donkeycar SD 卡放到 Pi 3,竟然啟動不了。紅燈閃爍,畫面停在彩虹魔方。


我重灌 Donkeycar 影像,同樣不行。於是,只好安裝 NOOBS 到 SD 卡,今次卻成功了。證明了 Donkeycar 官方網站的 donkey_v22.img 影像檔是不對應 Pi 3 Model B+(Pi 3 Model B 是沒問題)。由於我希望 Raspberry Pi 能自動進行 WiFi 連線,而應用的場景有三個:
1. 公司
2. 家中
3. 比賽

三個場景所提供的 WiFi 都不一樣,要讓 Raspberry Pi 能有多個連線設定,只要修改 /etc/wpa_supplicant/wpa_supplicant.conf,把它加入數組連線設定就可以:

2018年6月7日 星期四

為 WDA 加入新指令

機緣巧合之下,找到了一個有趣的程式。它利用 Facebook 的 WebDriverAgent 及 Python 程序來控制手機內的《抖音》應用程式,利用臉部識別去找出顏值高於 80 的美女,並進行關注。吸引到我的地方不是美女,而是控制 iOS 的方法,亦即是 WDA。

過往也曾試過分析 iPhone 的遊戲畫面,開發計算出最好步數的指導器;但當時卻沒能把結果傳回 iPhone。一直希望能得到解決。現在 WDA 有望能達到這樣的效果。我嘗試用 WDA 去玩一個遊戲,可是當中需要快速連擊,而 WDA 本身沒有支援,於是動手修改一下 ~/anaconda3/lib/python3.6/site-packages/wda/__init__.py:
    ##  2018.06.07 Pacess
    def sequenceTap(self, list):
        return self.http.post('/wda/sequenceTap', dict(taps=list))
    ##  2018.06.07 End

在 FBElementCommands.m 的 + (NSArray *)routes 加入:
[[FBRoute POST:@"/wda/sequenceTap/"] respondWithTarget:self action:@selector(handleSequenceTap:)],

及加入新的函數:
//  Sequence-tap by Pacess
+ (id<FBResponsePayload>)handleSequenceTap:(FBRouteRequest *)request  {
   CGPoint tapPoint = CGPointZero;
   NSArray *tapArray = (NSArray *)request.arguments[@"taps"];
   NSInteger count = [tapArray count];
   for (int i=0; i<count; i+=2)  {

      CGFloat x = [[tapArray objectAtIndex:i] doubleValue];
      CGFloat y = [[tapArray objectAtIndex:i+1] doubleValue];
      tapPoint = CGPointMake(x, y);
      XCUICoordinate *tapCoordinate = [self.class gestureCoordinateWithCoordinate:tapPoint application:request.session.application shouldApplyOrientationWorkaround:isSDKVersionLessThan(@"11.0")];
      [tapCoordinate tap];
   }
   return FBResponseWithOK();
}

2018年6月6日 星期三

世界盃賠率走勢


為了提高學習 Machine Learning 的興趣,我提議同事收集世界盃球隊資料,尋找用得著的機器學習算法,去估算最有機會勝出的隊伍,然後根據結果進行投注。如果能選中勝出隊伍,大家又有點小收穫,相信會有動力;若然不想輸,就得想盡方法去增加勝算,技術也因此得到提升。不過,說說當然是容易,收集甚麼數據、如何收集、怎樣處理,到目前還是沒有頭緒...。然而,我提出了一個與機器學習無關,反而用了逆向思維的方法卻得到大家的認同。現在,賠率起了變化,也差不多是時候入市了。希望能帶來一點收穫!

2018年6月2日 星期六

Donkey car 一號


今天出席了香港自動駕駛模型協會的聚會,看看大家的 Donkey car 之餘,也了解一下實際運行時遇到的問題,及不同的改裝。

我的車子還沒完成,所以沒有帶來比試。目前大家的 Donkey car 都是按照官方版本製作,只有 3D 打印的部份用了不同顏色,他們都想進入下一階段,製作有個人風格的車子。我也是不想一式一樣而自行改裝,難度是有的,但結果將會是好玩的。其中一樣討論過的事情,是希望成功用積木砌成 Donkey car,跟 mbot 比試。原本貪拖頭車的空間較大,想優先把它開發成 Donkey car 二號,但需要花很多功夫去改裝;以及車長的關係,轉彎會不夠靈活。於是繼續裝嵌 Donkey car 一號。同時加入了超聲波模組及鏡頭模組的固定支架。目前,只欠 Raspberry Pi 3 Model B+ 來到,及加入自訂的 GPIO PWM 控作,希望能在一個月內完成!

2018年6月1日 星期五

Donkey car 二號


原本買了一套樂拼遙控跑車作為 Donkey car 的車架。在拼砌到一半後,發現車子有點小,無法容得下所有電子零件;於是購買另一台卡車。


卡車早幾到已運到,今晚趕工把它完成。我自行改裝一下,加入了伺服馬達,把原本手動的轉向改為馬達控制;同時也放入鏡頭及 Raspberry Pi 運殻。一切看似順利,零件也容得下,外觀也很趣緻。不過...


卻發現模型的馬達只負責升降台,而沒有前進動力... (T_T);可能零件多了,車頭也較為貼地,容易卡住。我是用來當遙控車,得自行大改才能達成...。反正今晚夜了,明日再想。

2018年5月31日 星期四

樂拼馬達速度比較


為了拼砌出 Donkey car,我買了三套樂拼;其中兩套配備了中型馬達。從外觀看是一樣的,只是軸心的顏色有些微的分別。原本認為速度是一樣的,但緊慎起見,還是做了一個測試。由於兩顆馬達的速度很快,我用了細比大齒輪外,也利用了 iPhone 的慢鏡拍攝。片中左面的是橙色軸心馬達,右面是黑色軸心馬達,跟上圖對掉了。從片中可見,橙色的是快一些。

2018年5月30日 星期三


Raspberry Pi Zero 推出時,因它的體積細小,的確吸引。不過,機能卻差了,用來開發似乎力不到位。而且在香港不見它的影蹤...。

很久前買了一塊紅外線鏡頭模組,安裝到 Pi 2 後加裝了 3D 打印的外殻;由於不方便,基本上沒有拿出外拍照。上星期,決定在淘寶購買。希望利用 Zero W 的細小,加上官方的外殻,能隨時用它來拍照。拍照的話,Zero W 能夠勝任。

2018年5月27日 星期日

用 Arduino 控制樂拼伺服馬達


既然樂拼伺服馬達的接線造好了,接著是找出控制訊號的格式。我選了方便使用的 Arduino Nano 及用來方便切換馬達正負極的 L293D 來進行測試。


測試的概念是利用 Arduino Nano 的 D3 腳接 L293D 的 3,4EN 腳;D3 可以輸出 PWM 訊號,用來測試樂拼伺服馬達是否跟坊間的伺服馬達一樣,同樣使用 1.5ms 至 2.5ms 的方波訊號來決定角度。利用 Arduino Nano 的 D7 及 D8 接到 L293D 的 3A 及 4A。這兩隻腳的 HIGH, LOW 決定馬達順轉或 LOW, HIGH 逆轉。


把樂拼伺服馬達的 9V 接到 L293D 的 VCC2,同時也接到鋰電池的 +9V;把 GND 接到 GND,同時也接到鋰電池的 GND;把 C1 接到 3Y;把 C2 接到 4Y。到目前為止,接線已經差不多,只欠 L293D 自己的電源。把 VCC1 接到 Arduino 的 VIN 腳。我的 Arduino Nano 是由 USB 取電,所以 VIN 可當成 5V 輸出。

接線完成!之後就要編寫一個簡單程序進行測試:
//------------------------------------------------------------
//  LEGO Servo Controller
//------------------------------------------------------------
//  Platfrom: Arduino Nano + L293D + LEGO Power Function Servo
//  Copyrights Pacess Studio, 2018.  All rights reserved.
//------------------------------------------------------------

//  Constant values
const int servoEnablePin = 3;
const int servoControlPinA = 7;
const int servoControlPinB = 8;

//------------------------------------------------------------
//  60 = Center
//  200 = Side
int steering = 0;
int delta = 1;

//  0 = Clockwise
//  1 = Counter-clockwise
int type = 0;

//------------------------------------------------------------
void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);

  pinMode(servoControlPinA, OUTPUT);
  pinMode(servoControlPinB, OUTPUT);
  pinMode(servoEnablePin, OUTPUT);

  //  Steering centered
  digitalWrite(servoEnablePin, LOW);
  setDirection();

  Serial.println("Please input a value:");
}

//------------------------------------------------------------
void setDirection()  {
  if (type == 0)  {

    //  Clockwise
    digitalWrite(servoControlPinA, LOW);
    digitalWrite(servoControlPinB, HIGH);
    return;
  }

  //  Counter clockwise
  digitalWrite(servoControlPinA, HIGH);
  digitalWrite(servoControlPinB, LOW);
}

//------------------------------------------------------------
void loop() {
  // put your main code here, to run repeatedly:
  char input[16];
  byte size = Serial.readBytesUntil('\n', input, 16-1);
  input[size] = 0;

  steering = atoi(input);
  if (steering > 0)  {
    Serial.println(steering);
    analogWrite(servoEnablePin, steering);
  }
  
  if (steering < 0)  {
    type = (type+1)&1;
    setDirection();
  }
}

結果發現,樂拼伺服馬達不像普請的伺服馬達,暫時只能做出 90 度的旋轉。數值為 60 置中,170 為 90 度。利用 C1 及 C2 的正反向決定順時針,還是逆時針。

2018年5月26日 星期六

改裝樂拼伺服馬達


要利用樂拼作為 Donkey car 的車架,需要把樂拼的馬達及伺服馬達連接到 Raspberry Pi。這樣得先了解馬達的接線及訊號規則。我把伺服馬達的接線剪斷,兩頭都分別接上母頭的彩虹線。把它們焊接,並用熱縮通包裹著,以免金屬外露而發生問題。


另一端的伺服馬達作相同的焊接。我特意選擇顏色鮮艷的排線,還把紅色線接上 +9V 端以作識別。+9V 端接正電流,GND 端是接地的,相信很容易理解。而 C1 及 C2 是用來控制馬達的方向


焊接完成後,把接線安裝到接收器,並把 C1 及 C2 母頭連到儀錶看看是如何控制角度。是用電壓?還是 PWM?


發現其中一個方向是正電壓,而另一個是負電壓。原來是靠正值、零及負值去決定伺服馬達的角度。似乎它不像遙控車般用 PWM 來決定,也不能調校任意角度。