使用C++指標傳輸
建立與Unity溝通的結構
C++
struct Circle
{
//建構子
Circle(int x, int y, int radius) : X(x), Y(y), Radius(radius) {}
int X, Y, Radius;
};
編譯器在產生dll檔案時會打亂method名稱,為讓方法保持原名稱,則外顯”C”
Normally, the C++ compiler will mangle the method names when packaging them into a .dll. Therefore, we instruct it to use the classic “C” style of signatures, which leaves the method names just as you wrote them.
extern "C" int __declspec(dllexport) __stdcall Init(int& outCameraWidth, int& outCameraHeight)
extern "C" void __declspec(dllexport) __stdcall Detect(Circle* outFaces, int maxOutFacesCount, int& outDetectedFacesCount)
Circle* outFaces 表an array of Circles。
int& outDetectedFacesCount 表該變數是傳址(ref)。
C#
與C++溝通的格式,變數欄位、宣告順序必需與c++相同
// Define the structure to be sequential and with the correct byte size (3 ints = 4 bytes * 3 = 12 bytes)
[StructLayout(LayoutKind.Sequential, Size = 12)]
public struct CvCircle
{
public int X, Y, Radius;
}
unsafe:讓你在C#能使用指標。
fixed:使編譯器讓該變數記憶體位置不被garbage collector處理掉。
在fixed區塊中,openCV會直接將變數寫入CvCircle結構陣列中,而省去copy的成本。
void Update()
{
//接收陣列大小
int detectedFaceCount = 0;
unsafe
{
//pass fixed pointer
fixed (CvCircle* outFaces = _faces)
{
OpenCVInterop.Detect(outFaces, _maxFaceDetectCount, ref detectedFaceCount);
}
}
}
fixed中只接受:
The legal initializers for a fixed statement are:
- The address operator
&applied to a variable reference. - An array
- A string
- A fixed-size buffer.
分段解析
C++
// Declare structure to be used to pass data from C++ to Mono. (用來與Unity溝通的結構)
struct Circle
{
//建構子
Circle(int x, int y, int radius) : X(x), Y(y), Radius(radius) {}
int X, Y, Radius;
};
CascadeClassifier 是Opencv中做人臉檢測的時候的一個級聯分類器。 並且既可以使用Haar,也可以使用LBP特徵。(源)
C#
// Define the structure to be sequential and with the correct byte size
//(3 ints = 4 bytes * 3 = 12 bytes)
[StructLayout(LayoutKind.Sequential, Size = 12)]
public struct CvCircle
{
public int X, Y, Radius;
}
StructLayout :C#中StructLayout的特性 - IT閱讀 (itread01.com)
char型資料,對齊值為1,對於short型為2,對於int,float,double型別,其對齊值為4,單位位元組。
初始化鏡頭大小
C++
extern "C" int __declspec(dllexport) __stdcall Init(int& outCameraWidth,
int& outCameraHeight)
{
// Load LBP face cascade.
if (!_faceCascade.load("lbpcascade_frontalface.xml"))
return -1;
// 打開鏡頭
_capture.open(0);
if (!_capture.isOpened())
return -2;
//取得視訊大小
outCameraWidth = _capture.get(CAP_PROP_FRAME_WIDTH);
outCameraHeight = _capture.get(CAP_PROP_FRAME_HEIGHT);
return 0;
}
C#
在Opencv資料夾下找到lbpcascade_frontalface.xml,並放到Unity專案root資料夾下。
int camWidth = 0, camHeight = 0;
int result = OpenCVInterop.Init(ref camWidth, ref camHeight);
if (result < 0)
{
if (result == -1)
{
Debug.LogWarningFormat("[{0}] Failed to find cascades definition.", GetType());
}
else if (result == -2)
{
Debug.LogWarningFormat("[{0}] Failed to open camera stream.", GetType());
}
return;
}
CameraResolution = new Vector2(camWidth, camHeight);
鏡頭大小的變數使用傳址呼叫,讓c++開啟鏡頭後順便設定好大小。使C#和c++使用相同的變數。
傳遞參數scale
C++
extern "C" void __declspec(dllexport) __stdcall SetScale(int scale)
{
_scale = scale;
}
C#
private const int DetectionDownScale = 1;
void Start()
{
...
OpenCVInterop.SetScale(DetectionDownScale);
_ready = true;
}
辨識人臉
C++
extern "C" void __declspec(dllexport) __stdcall Detect(Circle* outFaces,
int maxOutFacesCount,
int& outDetectedFacesCount)
{
Mat frame;
_capture >> frame;
if (frame.empty())
return;
std::vector<Rect> faces;
// Convert the frame to grayscale for cascade detection.
Mat grayscaleFrame;
cvtColor(frame, grayscaleFrame, COLOR_BGR2GRAY);
Mat resizedGray;
// Scale down for better performance.
resize(grayscaleFrame, resizedGray, Size(frame.cols / _scale, frame.rows / _scale));
equalizeHist(resizedGray, resizedGray);
// Detect faces.
_faceCascade.detectMultiScale(resizedGray, faces);
// Draw faces.
for (size_t i = 0; i < faces.size(); i++)
{
Point center(_scale * (faces[i].x + faces[i].width / 2), _scale * (faces[i].y + faces[i].height / 2));
ellipse(frame, center, Size(_scale * faces[i].width / 2, _scale * faces[i].height / 2), 0, 0, 360, Scalar(0, 0, 255), 4, 8, 0);
// Send to application.
outFaces[i] = Circle(faces[i].x, faces[i].y, faces[i].width / 2);
//返回數量用的
outDetectedFacesCount++;
if (outDetectedFacesCount == maxOutFacesCount)
break;
}
// Display debug output.
imshow(_windowName, frame);
}
步驟:
灰階→縮小解析度→直方圖均衡化→偵測
【補充】
直方圖均衡化(equalizeHist):
將拉伸數值分佈範圍從0-255。假設影像過曝(如藍色曲線),則直方圖均衡化能將其值範圍拉伸0-255區間內,使黑白更分明。 https://youtu.be/jWShMEhMZI4

人臉偵測 (detectMultiScale):
https://blog.csdn.net/leaf_zizi/article/details/107637433
CascadeClassifier.detectMultiScale(輸入圖片, 輸出向量, scaleFactor=1.1 , minNeighbor=3);
輸入圖片: 只接受灰階
scaleFactor:每次圖像縮小的比例,
minNeighbor:每個候選矩形有多少個”鄰居”,我的理解是:一個滑動窗口中的圖元需要符合幾個條件才能判斷為真。
大概意思是Haar cascade的工作原理是一種”滑動視窗”的方法,通過在圖像中不斷的”滑動檢測視窗”來匹配人臉。
因為圖像的圖元有大有小,圖像中的人臉因為遠近不同也會有大有小,所以需要通過scaleFactor參數設置一個縮小的比例,對圖像進行逐步縮小來檢測,這個參數設置的越大,計算速度越快,但可能會錯過了某個大小的人臉。
其實可以根據圖像的圖元值來設置此參數,圖元大縮小的速度就可以快一點,通常在1~1.5之間。
那麼,經過多次的反覆運算,實際會檢測出很多很多個人臉,這一點可以通過把minNeighbors 設為0來驗證。
所以呢,minNeighbors參數的作用就來了,只有其”鄰居”大於等於這個值的結果才認為是正確結果。
返回Rect ,其包含<x,y,w,h>
使用UDP
Python 傳送資料
import numpy as np
import cv2
import socket
UDP_IP = "127.0.0.1"
UDP_PORT = 5065
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while true:
sock.sendto( ("data!").encode(), (UDP_IP, UDP_PORT) )
print("data sent")
capture.release()
cv2.destroyAllWindows()
C# UPD 建立連線
// 1. Declare Variables
Thread receiveThread; //在背景持續接受UDP訊息
UdpClient client; // parse the pre-defined address for data
int port; //port number
// 2. Initialize variables
void Start()
{
port = 5065;
InitUDP();
}
// 3. InitUDP
private void InitUDP()
{
print("UDP Initialized");
receiveThread = new Thread(new ThreadStart(ReceiveData)); //開個新的帶有參數的thread,傳入方法當參數
receiveThread.IsBackground = true;
receiveThread.Start();
}
C# 定義接受方法
// 4. Receive Data
private void ReceiveData()
{
client = new UdpClient(port); //指定port
while (true)
{
try
{
IPEndPoint anyIP = new IPEndPoint(IPAddress.Parse("0.0.0.0"), port); //任何ip
byte[] data = client.Receive(ref anyIP); //資料
string text = Encoding.UTF8.GetString(data); //binary => utf8 text
print(">> " + text);
//....
}
catch (Exception e)
{
print(e.ToString());
}
}
}
使用TCP
需經過三項交握確認連線。
若server(這裡是unity)的port沒有開,會收到傳送失敗的exception。
Python 建立連線
import numpy as np
import cv2
import socket
TCP_IP = "127.0.0.1"
TCP_PORT = 5066
#sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # SOCK_DGRAM 長度限制 548bytes,但不需要預先connect
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP
#TCP連線
address=(TCP_IP ,TCP_PORT )
sock.connect(address)
print('sock init')
sock.send('Hi'.encode('utf-8'));
sock.close() #才會把累積的資料傳送
C# 建立連線
public class ImageReceiver : MonoBehaviour
{
//TCP Port 開啟
Thread receiveThread;
TcpClient client;
TcpListener listener;
int port;
private void Start()
{
InitTcp();
}
void InitTcp()
{
port = 5066;
print("TCP Initialized");
IPEndPoint anyIP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), port);
listener = new TcpListener(anyIP);
listener.Start();
//開個新的帶有參數的thread,傳入方法當參數
receiveThread = new Thread(new ThreadStart(ReceiveData));
receiveThread.IsBackground = true;
receiveThread.Start();
}
private void OnDestroy()
{
receiveThread.Abort();
}
}
定義接收方法
private void ReceiveData()
{
print("received somthing...");
try
{
while (true)
{
client = listener.AcceptTcpClient();
NetworkStream stream = new NetworkStream(client.Client);
StreamReader sr = new StreamReader(stream);
print(sr.ReadToEnd());
}
}
catch (Exception e)
{
print(e);
}
}
注意1,由於一開始需要經過三項交握,所以”TCP Initialized”之後會log一次”received something…”,該訊息為用來回應tcp連線的。
注意2,在python端sock.close()之前收到的訊息會一直存在sr,直到close之後才一次print出,所以傳輸每frame都會經過:建立連線=>打包資料=>傳送=>close() 的循環。
傳輸畫面
Python 端
注意資料只能傳byte,string等型態,所以這邊使用json格式。Python有另一個類次的pickel插件,但該格式只能python使用,不方便給unity。
while cap.isOpened():
ret, img = cap.read()
img_data={'image':cv2.imencode('.jpg',img)[1].ravel().tolist()}
data=json.dumps(img_data);
#準備連線
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP
sock.connect(address)
#傳送資料
sock.sendall(bytes(data,encoding='utf-8'))
#print('sock sent')
cv2.imshow("Image",img)
cv2.waitKey(10)
sock.close()
C# 端
宣告texture:
public class ImageReceiver : MonoBehaviour
{
//TCP Port 開啟
Thread receiveThread;
TcpClient client;
TcpListener listener;
int port;
private void Start()
{
InitTcp();
}
void InitTcp()
{
port = 5066;
print("TCP Initialized");
IPEndPoint anyIP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), port);
listener = new TcpListener(anyIP);
listener.Start();
//開個新的帶有參數的thread,傳入方法當參數
receiveThread = new Thread(new ThreadStart(ReceiveData));
receiveThread.IsBackground = true;
receiveThread.Start();
}
private void OnDestroy()
{
receiveThread.Abort();
}
}
由於Unity不支援多線程,無法在接收方法中直接設定texture,所以在fixedUpdate中設定。
private void FixedUpdate()
{
tex.LoadImage(imageDatas);
img.texture = tex;
}



