2016年11月11日 星期五

如何用Node.js 人臉偵測

        OpenCV的功能非常強大,所以它被其他程式引用,所以node.jsopenCV的函式庫引用進來,所以透過前端網頁做人臉偵測,前端的技術又向前邁進了一大步囉~ 但是前置作業還挺麻煩,就簡單教大家如何從無到有做出人臉偵測的效果吧!

一、安裝OPENCV套件

1. 要先下戴OpenCVwindows 安裝程式
選擇你喜歡的路徑 解壓縮就可以囉~


2 設定環境變數: OPENCV_DIR: OPENCV的路徑\build\x64\vc12
PS注意: 如果是32位元的電腦 要使用 \build\x86\vc12

2 設定path變數: %OPENCV_DIR%\bin
這樣OPENCV就安裝完畢了!!!

二、OpenCV 偵測
編寫pack.json
{
"name": "edi-cam",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "4.10.1",
"morgan": "1.4.1",
"opencv": "git+https://github.com/peterbraden/node-opencv.git",
"socket.io": "1.2.1"
}
}

如果有相關的套件,可以一次編寫下戴的工具包和版本,就會全自動下戴完成囉~

注意事項: 如果有出現紅字就檢查是否有【OPENCV_DEV】沒有設定,或者用錯X86/X64的資料夾,都會出現下戴opencv 失敗

建立serve\config\defaultPath.js 設定HTML網頁路徑
var path = require('path') //路徑模組
module.exports = {
httpPort: 8080,
staticFolder: path.join(__dirname + '/../../client') //預設瀏覽網頁的放置路徑
};

建立serve\config\defautlPage.js 設定HTML網頁名稱
exports.serveIndex = function (app, staticFolder) {
app.get('*', function (req, res) {
res.sendFile('index.html', { root: staticFolder });
});
};

建立 server\config\faceDetect.js 偵測人臉位置
var cv = require('opencv'); //使用openCV模組
var fs=require('fs');
var camWidth = 320;
var camHeight = 240;
var camFps = 10;
var camInterval = 1000 / camFps; //偵測間隔

// 設定外框的顏色
var rectColor = [0, 255, 0];
var rectThickness = 2; // 設定外框的厚度
var filename="./node_modules/opencv/tmp/image.png"; //儲存檔案名稱
// initialize camera
var camera = new cv.VideoCapture(0); //取得視訊串流
camera.setWidth(camWidth);
camera.setHeight(camHeight);

module.exports = function (socket) {
setInterval(function() {
camera.read(function(err, im) {
if (err) throw err;
//偵測臉部用的XML
im.detectObject('./node_modules/opencv/data/haarcascade_frontalface_alt2.xml', {}, function(err, faces) {
if (err) console.log(err);
for (var i = 0; i < faces.length; i++) {
face = faces[i];
//在臉的位置加四方型的框,自訂 RectColor 顏色, 自訂rectThickness 框寬
im.rectangle([face.x, face.y], [face.width, face.height], rectColor, rectThickness);
}
im.save("./node_modules/../../client/image.png"); //儲存圖片
fs.readFile("./node_modules/../../client/image.png", function (err, data) {
//讀取圖片檔案
if (err) throw err;
//觸發frame 事件時,傳回圖片串流 (base64)
socket.emit('frame', { buffer: new Buffer(data).toString('base64') });
});
});
});
}, camInterval);
};

建立 server\server.js 呼叫socket.io 架伺服器

// modules
var express = require('express')
, http = require('http')
, morgan = require('morgan');

//取得網頁檔案路徑的設定
var configServer = require('./config/pathDirectory');

// app parameters
var app = express();
app.set('port', configServer.httpPort); //取得網頁的port
app.use(express.static(configServer.staticFolder)); //取得網頁的路徑
app.use(morgan('dev'));//記錄日誌檔

//呼叫 defautlPage serveIndex 函數
require('./config/defaultPage').serveIndex(app, configServer.staticFolder);

//呼叫 http 建立server
var server = http.createServer(app);
server.listen(app.get('port'), function () {
console.log('HTTP server listening on port ' + app.get('port'));
});

//呼叫soket.io 模組 ,啟動人臉偵測
var io = require('socket.io')(server);
io.on('connection', require('./config/faceDetect'));

module.exports.app = app;

建立 client\app.js
var socket = io.connect('http://localhost');
var canvas = document.getElementById('canvas-video'); //取得HTMLHTMLcanvas ID
var context = canvas.getContext('2d'); //初始化畫布
var img = new Image(); //宣告圖片
context.fillStyle = '#333';
context.fillText('Loading...', canvas.width/2-30, canvas.height/3); //繪製文字

//Client連到server端,server端會觸發frame事件
socket.on('frame', function (data) {
img.onload = function () {
context.drawImage(this, 0, 0, canvas.width, canvas.height);
};
img.src = 'data:image/png;base64,' + data.buffer; //取回圖片串流
});

建立 client\index.html
<html>
<head>
<meta name="viewport" content="initial-scale=1"/>
<title>face-detection-node-opencv</title>
<link rel="stylesheet" type="text/css" href="/styles.css">
</head>
<body>
<div class="container center">
<canvas id="canvas-video" width="640" height="480"></canvas>
</div>
<script src="/socket.io/socket.io.js"></script> //引用socket.io-client 目錄下的socket.io.js
<script src="app.js"></script>
</body>
</html>

PS: 原本soket.io.js的路徑放置在 \node_modules\socket.io\node_modules\socket.io-client socket.io.js 拷貝到 \node_modules\socket.io\ 目錄下

建立 client\style.css

.container {
padding-top: 50px;
}

.center {
text-align: center;
}

#canvas-video {
width: 640px;
height: 480px;
border: 1px solid #ccc;
}

範例:

OpenCV除了人臉偵測外,還有其他的模組偵測比如眼晴,此外OPENCV 可以繪製圖框在影像上,不就如外面的AR的虛擬實境了嗎? 當然OPCN最強大的還是偵測物品的功能啦~
感謝大家的拜訪… 也歡迎大家留言多多討論囉!!!


三、參考文獻








2016年11月8日 星期二

specflow 如何自動產生文件規格

剛進公司就有91的訓練課程,剛好有機會接觸BDD的全貌流程,從完全不理解為何步驟如此頻繁透過工具簡化許多手動的操作的動作,進而提昇化工作效率的過程,以免日後忘了這個好用的東西,簡單作個分享囉~

一、外掛SpecFlow:
這是cucumber支援.net的擴充套件,真是佛心來的,可以從使用者的情境角度,直接相對應的產生測試案例,這比之前微軟推出的單元測試更直覺,還能重複使用情境案例


























安裝Nuget 套件
  1. NUnit
  2. SpecFlow
二、建立需求檔



新增Feature 的檔案後,要修改app.config 設定檔 --> 找到specFlow 的xml,修改如下所示
<specFlow>
     <unitTestProvider name="MsTest"></unitTestProvider>
</specFlow>



Feature.cs 預設以下的內文
Feature: example
In order to avoid silly mistakes (說明想做什麼)
As a math idiot (角色)
I want to be told the sum of two numbers (期望要驗證的事)

@mytag (允許將情境做分類)
Scenario: Add two numbers (情境說明要做什麼事)
Given I have entered 50 into the calculator (要加入的參教為 50 )
And I have entered 70 into the calculator (另外一個參教為 70 )
When I press add (執行add事件)
Then the result should be 120 on the screen (期望結果傳回120)

根據說明,修改我們的Feature 檔如下 :
舉例: 如果客戶的資料檔中如果共計有五筆資料,輸入查詢區間為2014-07-03~ 2014-07-07查詢結果為只有只有符合該區間的筆數共3筆,驗證其結果是否符合預期。


Scenario: 依據姓名、訂單日期起、訂單日期迄條件,查出顧客的訂單 (情境說明要做什麼事)
Given 查詢條件為 (要加入的參教為 資料表(CustomerId | OrderDateStart | OrderDateEnd)
| CustomerId | OrderDateStart | OrderDateEnd |
| Joey | 2014-07-03 | 2014-07-07 |
And 預計Customers資料應有 (要加入的參教為 資料表(CustomerID | CompanyName )
| CustomerID | CompanyName |
| Joey | SkillTree |
And 預計Orders資料應有 (要加入的參教為 資料表(CustomerID | OrderDate | ShipCity )
| CustomerID | OrderDate | ShipCity |
| Joey | 2014-07-02 | Taipei |
| Joey | 2014-07-03 | Taipei |
| Joey | 2014-07-04 | Changhua |
| Joey | 2014-07-05 | Changhua |
| Joey | 2014-07-08 | Changhua |
When 呼叫Query方法 (要執行 Query 的方法 )
Then 查詢結果應為 (預期傳回的結果 )
| CustomerID | OrderDate | ShipCity |
| Joey | 2014-07-03 | Taipei |
| Joey | 2014-07-04 | Changhua |
| Joey | 2014-07-05 | Changhua |

當我們的條件模擬狀況寫完了之後… 然到還要回到測試案例寫測試程式嗎當然不用啦Featur 的內文,按下【右鍵】產生Generate Stdep Definition 就產生對應的測試案例囉先給個讚吧!


按下右鍵 go to stdep Definition (到測試程式的函式名稱)
PS:測試內容當然要自已寫囉~~ 所以以下示範如何寫測試程式
[Binding]
[Scope(Feature = "OrderQuery")]
public class OrderQuerySteps
{
private OrderService target;
private NorthwindEntitiesInTest dbContext;
//在所有測試案例起動之前,執行此區塊,目的用來先清掉之前的遺留假資料
[BeforeScenario]
public void BeforeScenario()
{
this.target = new OrderService();
//為了直接測試資料庫的真實行為 呼叫entity 實體資料庫
using (dbContext = new NorthwindEntitiesInTest())
{
dbContext.Database.ExecuteSqlCommand("Delete [Orders] Where CustomerID IN ('Joey','JoeyTest')");
dbContext.Database.ExecuteSqlCommand("Delete [Customers] Where CustomerID IN('Joey','JoeyTest')");
}
}

//在所有測試案例起動之後,執行此區塊,目的用來先清掉之前的遺留假資料
[AfterScenario]
public void AfterScenario()
{
using (dbContext = new NorthwindEntitiesInTest())
{
dbContext.Database.ExecuteSqlCommand("Delete [Orders] Where CustomerID IN ('Joey','JoeyTest')");
dbContext.Database.ExecuteSqlCommand("Delete [Customers] Where CustomerID IN('Joey','JoeyTest')");
}
}

[Given(@"查詢條件為")]
public void Given查詢條件為(Table table)
{
//傳入的table 型態,直接轉換成物件
var condition = table.CreateInstance<OrderQueryCondition>();
//要將查詢條件暫存記憶體
ScenarioContext.Current.Set<OrderQueryCondition>(condition);

}

[Given(@"預計Customers資料應有")]
public void Given預計Customers資料應有(Table table)
{
var customers = table.CreateSet<SpecFlowWithEf.Tests.ModelInTest.Customers>();

using (dbContext = new NorthwindEntitiesInTest())
{
foreach (var customer in customers)
{
dbContext.Customers.Add(customer);
}
dbContext.SaveChanges(); //要將客戶的假資料存到實體資料庫中
}
}


[Given(@"預計Orders資料應有")]
public void Given預計Orders資料應有(Table table)
{
var orders = table.CreateSet<SpecFlowWithEf.Tests.ModelInTest.Orders>();

using (dbContext = new NorthwindEntitiesInTest())
{
foreach (var order in orders)
{
dbContext.Orders.Add(order);
}
dbContext.SaveChanges(); //要將訂單的假資料存到實體資料庫中
}
}

[When(@"呼叫Query方法")]
public void When呼叫Query方法()
{
//取得查詢條件的資料
var condition = ScenarioContext.Current.Get<OrderQueryCondition>();
//service回傳的,不一定需要是 dbContext EF 產生的 model 型別
IEnumerable<MyOrder> actual = this.target.Query(condition);
//將回傳的結果儲存
ScenarioContext.Current.Set<IEnumerable<MyOrder>>(actual);
}

[Then(@"查詢結果應為")]
public void Then查詢結果應為(Table table)
{
//將預期的結果儲存
var actual = ScenarioContext.Current.Get<IEnumerable<MyOrder>>();
//比較結果
table.CompareToSet<MyOrder>(actual);
}
}
PS:[BeforeScenario] [AfterScenario] 是自已加上的區塊,跑所以測試案例之前 或 之後都執行的區塊,如果不需要用到,也就不用特別加了。 以上截取91的上課程式範例

四、用工具產生文件
如果寫完所有的測試案例之後,主管要求寫文件,這也令工程師頭痛的吧~ 所以也有人佛心的寫的套件,自動產生線上文件喔,前提是要有Feature檔和自動產生的測試程式,才能自動產生文件喔… 讓我們再來看看神奇的魔術吧~

下戴工具包

或者 nuget pickles

nuget picklesui 圖形化的線上文件產生器


feature Directory : 選擇Feature 檔的資料夾
output Directory : 選擇 匯出文件的目錄
project name : 專案名稱
project version : 版次

產生的OrderQuery的需求案例


結論: 各位看倌~ 你不用寫任何一行文件需求,自動生成的文件,隨著案例不同,文件也隨之自動產生,不需要SA 或 工程師辛苦的修改文件了… 多麼神奇啊,每一次的修改程式都有不如預期的地方,也許是邏輯不夠完善,透過每次的演化BDD的情境模擬,來讓程式更加完善,需求異動頻繁更需要測試,透過VS擴充工具讓原本無法預期的程式變得更可預測,縮短BUG的週期… 如果有不完善的地方,還期望各位多多的補充喔…