Windows使用SOCKS5-proxy上網

簡介

這裡將介紹一個只需要使用SSH連線到一台可以連到對網網路的主機,就可以讓防火牆內的主機上網的方法
圖片說明

設定putty

https://www.simplified.guide/putty/create-socks-proxy

或是用ssh指令連接到可以上網的電腦

1
ssh -D 4444 -q -C -N user@ma.ttias.be

windows 設定SOCKS5 proxy

https://blog.gtwang.org/linux/ssh-tunnel-socks-proxy-forwarding-tutorial/

新增憑證

https://learn.microsoft.com/zh-tw/biztalk/adapters-and-accelerators/accelerator-swift/adding-certificates-to-the-certificates-store-on-the-client

windows下設定git proxy command

https://serverfault.com/questions/956613/windows-10-ssh-proxycommand-posix-spawn-no-such-file-or-directory

Windows設定自動重啟服務

開啟工作排程器

image info

分別設定停止服務和啟動服務兩個工作

  • 使用最高權限執行工作並且使用者不須登入就執行
    image info

  • 分別設定停止服務和啟動服務兩個工作
    在動作設定中,選擇開啟程式,並且在程式或指令中輸入net,並且在參數中輸入stop "服務名稱"start "服務名稱"

image info
image info

libcurl上傳圖片

快速產生libcurl程式碼

首先先用curl指令發送request,確認可以發送成功後可以用指令自動產生程式碼,在這裡我們將上傳一張圖片到http server,利用curl讀取電腦的圖片並且上傳

1
curl -X POST "http://localhost:8000/upload" -H "accept: application/json" -H "Content-Type: multipart/form-data" -F "uploadedFile=@a.jpg;type=image/jpeg" -F "EventTime=2022-01-01"

利用curl的--libcurl選項就可以快速產生c語言的程式碼,程式碼會被儲存到code.c

1
curl -X POST "http://localhost:8000/upload" -H "accept: application/json" -H "Content-Type: multipart/form-data" -F "uploadedFile=@a.jpg;type=image/jpeg" -F "EventTime=2022-01-01" --libcurl code.c

產生的程式碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/********* Sample code generated by the curl command line tool **********
* All curl_easy_setopt() options are documented at:
* https://curl.haxx.se/libcurl/c/curl_easy_setopt.html
************************************************************************/
#include <curl/curl.h>

int main(int argc, char *argv[])
{
CURLcode ret;
CURL *hnd;
curl_mime *mime1;
curl_mimepart *part1;
struct curl_slist *slist1;

mime1 = NULL;
slist1 = NULL;
slist1 = curl_slist_append(slist1, "accept: application/json");
slist1 = curl_slist_append(slist1, "Content-Type: multipart/form-data");

hnd = curl_easy_init();
curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
curl_easy_setopt(hnd, CURLOPT_URL, "http://localhost:8000/upload");
curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
mime1 = curl_mime_init(hnd);
part1 = curl_mime_addpart(mime1);
curl_mime_filedata(part1, "a.jpg");
curl_mime_name(part1, "uploadedFile");
curl_mime_type(part1, "image/jpeg");
part1 = curl_mime_addpart(mime1);
curl_mime_data(part1, "2022-01-01", CURL_ZERO_TERMINATED);
curl_mime_name(part1, "EventTime");
curl_easy_setopt(hnd, CURLOPT_MIMEPOST, mime1);
curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1);
curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.68.0");
curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
curl_easy_setopt(hnd, CURLOPT_SSH_KNOWNHOSTS, "/home/ai_server/.ssh/known_hosts");
curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "POST");
curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);

/* Here is a list of options the curl code used that cannot get generated
as source easily. You may select to either not use them or implement
them yourself.

CURLOPT_WRITEDATA set to a objectpointer
CURLOPT_INTERLEAVEDATA set to a objectpointer
CURLOPT_WRITEFUNCTION set to a functionpointer
CURLOPT_READDATA set to a objectpointer
CURLOPT_READFUNCTION set to a functionpointer
CURLOPT_SEEKDATA set to a objectpointer
CURLOPT_SEEKFUNCTION set to a functionpointer
CURLOPT_ERRORBUFFER set to a objectpointer
CURLOPT_STDERR set to a objectpointer
CURLOPT_HEADERFUNCTION set to a functionpointer
CURLOPT_HEADERDATA set to a objectpointer

*/

ret = curl_easy_perform(hnd);

curl_easy_cleanup(hnd);
hnd = NULL;
curl_mime_free(mime1);
mime1 = NULL;
curl_slist_free_all(slist1);
slist1 = NULL;

return (int)ret;
}
/**** End of sample code ****/

curl_mime_filedata改成curl_mime_data

由於我的目的是將在記憶體中已經被encode好的圖片直接上傳,所以要用curl_mime_data
直接上傳記憶體內的內容。
注意除了用curl_mime_data放入資料以外,還必須自己手動把Content-Disposition補上filename。
另外記得curl_mime_data不可以用CURL_ZERO_TERMINATED而是要實際算出圖片的資料長度。
可以參考官方的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
curl_mime *mime;
curl_mimepart *part;

/* create a mime handle */
mime = curl_mime_init(easy);

/* add a part */
part = curl_mime_addpart(mime);

/* send image data from memory */
curl_mime_data(part, imagebuf, imagebuf_len);

/* set a file name to make it look like a file upload */
curl_mime_filename(part, "image.png");

/* set name */
curl_mime_name(part, "data");

讀binary file

直接將圖片讀進記憶體內不要做任何處理
範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void ReadFile(char *name)
{
FILE *file;
char *buffer;
unsigned long fileLen;

//Open file
file = fopen(name, "rb");
if (!file)
{
fprintf(stderr, "Unable to open file %s", name);
return;
}

//Get file length
fseek(file, 0, SEEK_END);
fileLen=ftell(file);
fseek(file, 0, SEEK_SET);

//Allocate memory
buffer=(char *)malloc(fileLen+1);
if (!buffer)
{
fprintf(stderr, "Memory error!");
fclose(file);
return;
}

//Read file contents into buffer
fread(buffer, fileLen, 1, file);
fclose(file);

//Do what ever with buffer

free(buffer);
}

termshark

再除錯的過程中,最好可以直接看一看自己的封包長什麼樣子,對於沒有螢幕的Ubuntu Server,可以安裝termshark,它可以做到跟wireshark差不多的事情

完整範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/********* Sample code generated by the curl command line tool **********
* All curl_easy_setopt() options are documented at:
* https://curl.haxx.se/libcurl/c/curl_easy_setopt.html
************************************************************************/
#include <curl/curl.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
CURLcode ret;
CURL *hnd;
curl_mime *mime1;
curl_mimepart *part1;
struct curl_slist *slist1;

mime1 = NULL;
slist1 = NULL;
slist1 = curl_slist_append(slist1, "accept: application/json");
slist1 = curl_slist_append(slist1, "Content-Type: multipart/form-data");

hnd = curl_easy_init();
curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);


curl_easy_setopt(hnd, CURLOPT_URL, "http://localhost:8000/upload");


curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
mime1 = curl_mime_init(hnd);
part1 = curl_mime_addpart(mime1);


// https://www.w3schools.blog/c-read-binary-file
FILE *file;
const char *buffer;
unsigned long fileLen;

//Open file
file = fopen("image.jpg", "rb");
if (!file)
{
fprintf(stderr, "Unable to open file");
return;
}

//Get file length
fseek(file, 0, SEEK_END);
fileLen=ftell(file);
fseek(file, 0, SEEK_SET);

//Allocate memory
buffer=(char *)malloc(fileLen+1);
if (!buffer)
{
fprintf(stderr, "Memory error!");
fclose(file);
return;
}

//Read file contents into buffer
fread(buffer, fileLen, 1, file);
fclose(file);

/* add data to the part */
curl_mime_data(part1, buffer, fileLen);

/* set a file name to make it look like a file upload */
curl_mime_filename(part1, "image.jpg");

free(buffer);


curl_mime_name(part1, "uploadedFile");
curl_mime_type(part1, "image/jpeg");
part1 = curl_mime_addpart(mime1);
curl_mime_data(part1, "2022-02-02", CURL_ZERO_TERMINATED);
curl_mime_name(part1, "EventTime");
curl_easy_setopt(hnd, CURLOPT_MIMEPOST, mime1);
curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1);
curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.68.0");
curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "POST");
curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);


ret = curl_easy_perform(hnd);

curl_easy_cleanup(hnd);
hnd = NULL;
curl_mime_free(mime1);
mime1 = NULL;
curl_slist_free_all(slist1);
slist1 = NULL;

return (int)ret;
}
/**** End of sample code ****/

參考:

https://daniel.haxx.se/blog/2022/09/12/convert-a-curl-cmdline-to-libcurl-source-code/

https://curl.se/libcurl/c/curl_mime_filename.html

https://geekscripts.guru/termshark-terminal-ui-for-tshark/

Computer Vision Models Learning and Inference整理

CH3 常見的probability distributions

要利用第二章操作probabilities的規則,為了使用這些規則,我們需要定義一些機率分布。選擇機率分布的依據是取決於我們正在建模的數據x的領域。

當我們在使用模型去擬合資料的時候,我們需要知道我們對擬合的不確定性有多大,而這個不確定性被表示為”對擬合模型的參數的機率分布”。也就是說每當一個模型在擬合資料的時候,都存在一個與之相關聯的參數的第二個機率分布。

例如 Dirichlet 是用於categorical distribution的參數的模型。在這種狀況下 Dirichlet的參數會被稱為超參數(hyperparameters)。更一般地說,超參數確定了原始分布參數的機率分布的形狀。

Distribution Domain Parameters modeled by
Bernoulli x ∈ {0, 1} beta
categorical x ∈ {1, 2, . . . , K} Dirichlet
univariate normal x ∈ R normal inverse gamma
multivariate normal x ∈ Rk normal inverse Wishart

|Distribution | Domain | Parameters modeled by |
Bernoulli | x ∈ {0, 1} | beta
categorical | x ∈ {1, 2, . . . , K} | Dirichlet
univariate normal | x ∈ R | normal inverse gamma
multivariate normal | x ∈ Rk | normal inverse Wishart

CH3.1 Bernoulli distribution

Bernoulli distribution是一個離散分布用於模擬二元試驗。他用於描述只有兩中輸出結果的狀況,$x \in {0, 1}$分別代表是(success)、否(failure)。在機器視覺中,Bernoulli distribution可以用來模擬資料。例如,它可以描述一個像素具有大於或小於128的強度值的概率。或者是用來描述世界的狀態。例如,它可以描述圖像中臉部存在或不存在的概率。

Bernoulli distribution只有一個parameter $\lambda \in [0, 1]$,用來描述觀察到success的機率。Bernoulli distribution的機率質量函數如下
$$Pr(x = 0) = 1 - \lambda$$
$$Pr(x = 1) = \lambda$$

我們可以用另一種表達方式,將0或1帶入就可以得到上面其中一條式子。
$$Pr(x) = \lambda^x(1-\lambda)^{1-x}$$
或是另一種等價的表達方式
$$Pr(x) = Bern_x[\lambda] $$

CH3.2 Beta distribution

Beta distribution是一個連續分布,他是定義在單變量$\lambda$上的連續分布,$\lambda \in [0, 1]$。它適用於表示伯努利分布參數$\lambda$的不確定性。

beta distribution 有兩個parameter$\alpha, \beta \in [0, \infty]$,兩個parameter均為正數並且影響distribution的形狀。以數學表達如下
$$Pr(\lambda) = \frac{\Gamma[\alpha + \beta]}{\Gamma[\alpha]\Gamma[\beta]}\lambda^{\alpha-1}(1-\lambda)^{\beta-1}$$

式子中的$\Gamma[]$代表gamma function,定義為
$$\Gamma[z] = \int_0^{\infty}t^{z-1}e^{-t}dt$$
,他與階乘密切相關,因此對於正數的積分$\Gamma[z] = (z - 1)!$而且$\Gamma[z+1] = z\Gamma[z]$ 。

beta distribution還有更簡單的表達式
$$Pr(\lambda) = Beta_{\lambda}[\alpha, \beta]$$

CH3.3 Catagorical distribution

Catagorical distribution是一個離散分布,他用來決定K個可能結果之一的概率。因此Bernoulli distribution是Catagorical distribution的一種特例,也就是只有兩種可能結果的Catagorical distribution。在機器視覺中也經常出現多個離散值取一個的情況,例如依照照片可能是{car,motorbike,van,truck}的其中一個。
對於有K種結果的Catagorical distribution,Catagorical distribution會一個$K \times 1$的參數的向量$\lambda = [\lambda_1, \lambda_2 … , \lambda_K]$其中$\lambda_K \in [0, 1]$而且$\sum^K_{k=1}\lambda_k = 1$Catagorical distributiond可以寫成

$$Pr(x = k) = \lambda_k$$

更簡短的可以寫成
$$ Pr(x) = Cat_x[\lambda]$$

或者,我們可以將數據看作取值$x \in {e_1, e_2, …,e_K}$,其中$e_k$是第k個單位向量;除了第k個元素為1之外,$e_k$的所有元素都為零。寫成式子如下

$$Pr(x=e_k) = \prod^K_{j=1}\lambda_k^{x_j} = \lambda_k$$

其中$x_j$是$x$的第j個元素。

CH3.4 Dirichlet distribution

Dirichlet distribution 是一個定義在K個連續值$\lambda_1 … \lambda_K$上的分佈,其中$\lambda_k \in [0, 1]$而且$\sum^K_{k=1}\lambda_k = 1$。因此他很適合用來作為定義Catagorical distribution的參數分布。

在$K$個維度上的Dirichlet distribution有$K$個parameter $\alpha_1 … \alpha_K$,每一個都可以是正數或是零。parameters之間的相對值決定了 expected values $E[\lambda_1] … E[\lambda_K]$。parameters的絕對值決定了Dirichlet distribution的集中度。他的機率密度函數如下
$$Pr(\lambda_{1…L}) = \frac{\Gamma[\sum^K_{k=1}\alpha_k]}{\prod^K_{k=1}\Gamma[\alpha_k]}\prod^K_{k=1}\lambda_k^{\alpha_k-1}$$

更簡短的寫法
$$Pr(\lambda_{1…K}) = Dir_{\lambda_{1…K}}[\alpha_{1…K}]$$

如同Bernoulli distribution是Catagorical distribution的特例,所以Beta distribution是Dirichlet distribution的特例,也就是$K=2$的Beta distribution。

3.5 Univariate normal distribution

Univariate normal distribution 或是 Gaussian distribution 是一個定義在實數上$x \in [-\infty, \infty]$的連續分佈。
在計算機視覺中,常常忽略像素強度被量化的事實,並使用連續的正態分佈模型來建模。
Normal distribution有兩個parameter,平均數$\mu$和變異數$\sigma^2$,平均數$\mu$決定了分佈的中心的位置,變異數$\sigma^2$決定了分佈的寬度。Normal distribution的機率密度函數如下
$$Pr(x|) = \frac{1}{\sqrt{2\pi\sigma^2}}exp{-\frac{1}{2\sigma^2}(x-\mu)^2}$$

或是更簡單的寫法
$$Pr(x) = Norm_x[\mu, \sigma^2]$$

3.6 Normal-scaled inverse gamma distribution

Normal-scaled inverse gamma distribution是一個定義在一對實數$\mu, \sigma$上的連續分佈,他的機率密度函數如下,其中$\mu$可以為正為負,但是$\sigma$必須為正。
Normal-scaled inverse gamma distribution有四個parameter,$\alpha, \beta, \gamma, \delta$,其中$\alpha, \beta, \gamma$必須為正,而$\delta$可以為正為負。機率密度函數如下:
$$Pr(\mu, \sigma^2) = \frac{\sqrt{\gamma}}{\sigma\sqrt{2\pi}}\frac{\beta^\alpha}{\Gamma[\alpha]}(\frac{1}{\sigma^2})^{\alpha + 1}exp[-\frac{2\beta + \gamma(\delta - \mu)^2}{2\sigma^2}]$$
或者是寫成
$$Pr(\mu, \sigma^2) = NormInvGam_{\mu, \sigma^2}[\alpha, \beta, \gamma, \delta]$$

3.7 Multivariate normal distribution

multivariate normal 也就是 D-dimensional Gaussian distribution,他的維度可以表示成D個元素 $x_1, … x_D$每個維度都是連續並且介於$[-\infty, \infty]$。而univariate normal distribution是multivariate normal distribution的特例,也就是$D=1$。
在機器視覺中,多變量正態分佈可以用來模擬圖像區域內 D 個像素的強度的聯合分佈。

世界的狀態也可以用這個分佈來描述。例如,多變量正態分佈可以描述場景中物體的三維位置(x、y、z)的聯合不確定性。

multivariate normal distribution有兩個parameter,平均數向量$\mu$ 以及covariance $\Sigma$。其中$\mu$是一個$D \times 1$的向量來描述分布的平均值。而covariance $\Sigma$是一個$D \times D$的正定矩陣,因此對於任何實數向量$z$而言,$z^T\Sigma z$都是正數。機率密度函式如下
$$Pr(x) = \frac{1}{\sqrt{(2\pi)^D|\Sigma|}}exp[-\frac{1}{2}(x-\mu)^T\Sigma^{-1}(x-\mu)]$$

或是更簡短一點
$$Pr(x) = Norm_x[\mu, \Sigma]$$

3.8 Normal inverse Wishart distribution

normal inverse Wishart distribution是一個定義在$D \times 1$的向量 $\mu和$D \times D$的矩陣 \Sigma$上的連續分佈。他適合用來描述 multivariate normal distribution 的參數的不確定性。normal inverse Wishart distribution有四個參數,$\alpha, \Psi, \gamma, \delta$,其中$\alpha$和$\gamma$為正純量,$\delta$為$D \times 1$的向量,$\Psi$為$D \times D$的正定矩陣。機率密度函數如下
$$\operatorname{Pr}(\boldsymbol{\mu}, \boldsymbol{\Sigma})=\frac{\gamma^{D / 2}|\boldsymbol{\Psi}|^{\alpha / 2} \exp \left[-0.5\left(\operatorname{Tr}\left[\boldsymbol{\Psi} \boldsymbol{\Sigma}^{-1}\right]+\gamma(\boldsymbol{\mu}-\boldsymbol{\delta})^T \boldsymbol{\Sigma}^{-1}(\boldsymbol{\mu}-\boldsymbol{\delta})\right)\right]}{2^{\alpha D / 2}(2 \pi)^{D / 2}|\boldsymbol{\Sigma}|^{(\alpha+D+2) / 2} \Gamma_D[\alpha / 2]}$$
其中$\Gamma_D[\bullet]$是multivariate gamma function and而$\operatorname{Tr}[\Psi]$回傳矩陣的迹(trace)。更簡單的寫法如下
$$Pr(\mu, \Sigma) = NormInvWish_{\mu, \Sigma}[\alpha, \Psi, \gamma, \delta]$$
很難將normal inverse Wishart distribution分佈可視化,但很容易抽樣並檢查樣本:每個樣本是一個常態分佈的平均值和協方差。

3.9 Conjugacy

在前面幾節有提到有些distribution可以呈現另一個distribution參數的機率分布。因為這些分布之間都有Conjugacy的關係。例如beta distribution conjugate Bernoulli distribution。Dirichlet conjugate categorical。當我們將兩個有Conjugacy的distribution相乘,結果將與共軛具有相同形式的新分佈成正比。例如
$$ Bern_x[\lambda]\times Beta_\lambda[\alpha, \beta] = \kappa(x, \alpha, \beta)\times Beta_\lambda[~{\alpha}, ~{\beta}]$$
在這裡$\kappa$是一個對於所關心的變量$\lambda$來說是常數的縮放因子。
conjugate重要的原因是因為在學習(擬合分佈)和評估模型(評估新數據在擬合分佈下的概率)的過程中,我們需要對分佈進行乘法運算。共軛關係意味著這些乘積都可以以封閉形式整潔地計算得出。

CH4 擬和機率模型

這章節的目標是將機率模型擬和到資料$${x_i}^I_{i=1}$$,這個過程稱為學習learning,目標是找到模型的參數組$$\theta$$。此外我們也學習如何用學習後的模型對新的數據$$x^*$$計算出他的機率,這過程evaluating the predictive distribution。
在這章我們探討三種方法

  1. maximum likelihood
  2. maximum a posteriori
  3. Bayesian approach

maximum likelihood

maximum likelihood目標是要找到一組參數$$\hat{\theta}$$使得資料$${x_i}^I_{i=1}$$出現的機率最大化。而計算likelihood function 在單一個資料點$$x_i$$,也就是$$Pr(x_i|\theta)$$,我們只需在$$x_i$$處評估概率密度函數。
假設每一個資料點都是從分布中獨立被取出的,那麼所有資料點的機率$$Pr(x_{i…I}|{\theta})$$就是所有單獨資料點代入likelihood function的乘積。因此要maximum likelihood可以寫成

$$\hat{\theta} = argmax_{\theta}[Pr(x_{i…I}|{\theta})] =argmax_{\theta} [\prod_{i=1}^I Pr(x_i|\theta)]$$

式子中的$$argmax_{\theta}f[\theta]$$是指找到一組參數組$${\theta}$$,讓$$argmax_{\theta}f[\theta]$$最大化。

要計算新資料$$x^*$$的predictive distribution,只需將新資料和我們找到的參數組帶入likelihood function,計算出機率即可。

Maximum Posteriori

在Maximum Posteriori(MAP)擬合中,我們引入有關參數$$\theta$$的先驗信息。由於我們可能對可能的參數值有所了解。例如,在時間序列中,時間 t 的參數值可以告訴我們在時間 t + 1 可能的值,於是將此信息將被編碼在先驗分佈中。
如同它的名稱,Maximum Posteriori方法將會找到一組參數組$$\hat{\theta}$$,使得Posteriori probability $$Pr(\theta|x_{i…I})$$最大化。

$$\hat{\theta} = argmax_{\theta}[Pr(\theta|x_{i…I})]$$

$$ =argmax_{\theta} [\frac{Pr(x_{i…I}|\theta)Pr(\theta)}{Pr(x_{i…I})}]$$

$$ =argmax_{\theta} [\frac{\prod_{i=1}^I Pr(x_i|\theta)Pr(\theta)}{Pr(x_{i…I})}]$$

在這裡第一行可以由貝葉斯定理推得第二行。另外由於我們找的是參數組$$\theta$$的最大值,所以分母的常數$$Pr(x_{i…I})$$是可以忽略的。因此我們可以將式子簡化成

$$\hat{\theta} = argmax_{\theta} [\prod_{i=1}^I Pr(x_i|\theta)Pr(\theta)]$$

我們可以發現他其實跟maximum likelihood只差了一項先驗分佈$$Pr(\theta)$$,所以maximum likelihood其實是maximum posteriori的一種特例,也就是maximum likelihood是$$Pr(\theta)$$是一個常數的情況。

Bayesian Approach

Bayesian Approach裡,我們不再把參數$$\theta$$當作一個常數,而是承認一件顯而易見的事實,參數組$$\theta$$可能不是唯一的。因此我們嘗試利用貝葉思定裡計算$$Pr(\theta|x_{i…I})$$,也就是在資料$$x_{i…I}$$出現的情況下,參數組$$\theta$$的機率分佈。

$$Pr(\theta|x_{i…I}) = \frac{\prod_{i=1}^I Pr(x_i|\theta)Pr(\theta)}{Pr(x_{i…I})}$$

而要驗證Bayesian Approach會比較複雜一點,因為跟前面不一樣我們沒有一個固定的參數組$$\theta$$可以帶入並且計算出機率,在這裡我們必須可能模型的機率分布。因此我們用以下方法計算。

$$Pr(x^*|x_{i…I}) = \int Pr(x^*|\theta)Pr(\theta|x_{i…I})d\theta$$

這條式子可以用以下方式解讀:
$Pr(x^*|\theta)$ 是對於給定的參數組 $$\theta$$,$$x^*$$出現的機率,因此這個積分可以視為使用不同的參數$$\theta$$所做出預測的加權總和,其中權重是由參數的posterior probability distribution $$Pr(\theta|x_{i…I})$$ 來決定的(代表我們對不同參數正確性的信心程度)。

統一三種方法的 predictive density calculations

如果我們將maximum likelihood, maximum posteriori 的參數的機率分布視為一種特例,也就是maximum likelihood, maximum posteriori參數分布全部集中在$$\hat{\theta}$$。正式的說法就是參數分布為一個以$$\hat{\theta}$$為中心的delta function。一個delta function $$\delta[z]$$是一個函式他的積分為1,而且在除了中心點z以外的任何地方都是0。我們將剛才predictive density帶入delta function,可以得到以下結果。

$$Pr(x^*|x_{i…I}) = \int Pr(x^*|\theta)\delta[\theta - \hat{\theta}]d\theta$$

$$= Pr(x^*|\hat{\theta})$$

ch4.4範例一

下面範例我們考慮擬和一個univariate normal model到一組數據$${x_i}^I_{i=1}$$。
首先univariate normal model的probability density function為

$$Pr(x|\mu,\sigma^2) = Norm_x[\mu,\sigma^2] = \frac{1}{\sqrt{2\pi\sigma^2}}exp[-\frac{(x-\mu)^2}{2\sigma^2}]$$

他有兩個參數平均$$\mu$$和變異數$$\sigma^2$$,首先我們從一個平均為1,變異數為1的normal distribution中取出$$I$$個數據$$x_{1…I}$$,我們的目標是利用前面的三種方法擬和抽取出來的數據集。

方法一Maximum likelihood estimation

對於觀測到的數據$Pr(x_{1…I}|\mu,\sigma^2)$$來說,參數 $${\mu,\sigma^2}$$ 的概率(likelihood) Pr(x_1…I |µ, σ^2) 是通過對每個數據點分別評估概率密度函數,然後取乘積得到的。

$$Pr(x_{1…I}|\mu,\sigma^2) = \prod_{i=1}^I Pr(x_i|\mu,\sigma^2)$$

$$= \prod_{i=1}^I Norm_{x_i}[\mu,\sigma^2]$$

$$= \frac{1}{\sqrt{2\pi\sigma^2}}exp[-\sum_{i=1}^I \frac{(x_i-\mu)^2}{2\sigma^2}]$$

很明顯的某些$${\mu,\sigma^2}$$參數組會使得likelihood比其他參數組還高。而且我們可以在二維平面視覺化各種參數組的likelihood,我們將以平均$$\mu$$和變異數$$\sigma^2$$為軸,而Maximum likelihood的解答就是在圖形的頂點(圖4.2)。也就是以下式子的解答。

$$\hat{\mu}, {\hat\sigma}^2 = argmax_{\mu,\sigma^2} [Pr(x_{1…I}|\mu,\sigma^2)]$$

理論上我們可以藉由微分$$Pr(x_{1…I}|\mu,\sigma^2)$$來求解,但是實際上$$Pr(x_{1…I}|\mu,\sigma^2)$$太複雜,因此我們可以將$$Pr(x_{1…I}|\mu,\sigma^2)$$取log,因為log是一個單調遞增函數,經過轉換後的$$Pr(x_{1…I}|\mu,\sigma^2)$$的最大值會在相同的地方。代數上,對數把各個數據點的可能性的乘積轉化為它們的總和,因此可以將每個數據點的貢獻分離出來。於是Maximum likelihood可以用以下方式計算。

$$\hat{\mu}, {\hat\sigma}^2 = argmax_{\mu,\sigma^2} [\sum_{i=1}^I log [Norm_{x_{i}}[\hat{\mu}, {\hat\sigma}^2]]]$$

$$= argmax_{\mu,\sigma^2} [-0.5Ilog[2\pi] - 0.5Ilog(\sigma^2) - 0.5\sum_{i=1}^I\frac{(x_i-\mu)^2}{\sigma^2}]$$

接著對對 log likelihood function L 進行$$\mu$$的偏微分。
$$\frac{\partial L}{\partial \mu} = \sum_{i=1}^I \frac{(x_i-\mu)}{\sigma^2}$$
$$= \frac{\sum_{i=1}^I x_i}{\sigma^2} - \frac{I\mu}{\sigma^2} = 0$$

整理後可以得到
$$\hat{\mu} = \frac{\sum_{i=1}^I x_i}{I}$$

利用類似的方利用類似的方法可以得到變異數$$\sigma^2$$的解答為
$$\hat{\sigma}^2 = \frac{\sum_{i=1}^I (x_i-\hat{\mu})^2}{I}$$

Least squares fitting

另外需要注意的是,很多文獻都是以最小二乘法來討論擬合的。我們使用maximum
likelihood來擬和正態分佈的平均參數$$\mu$$。將前面式子將$$\sigma^2$$視為常數可以得到
$$\hat{\mu} = argmax_{\mu,\sigma^2} [-0.5Ilog[2\pi] - 0.5Ilog(\sigma^2) - 0.5\sum_{i=1}^I\frac{(x_i-\mu)^2}{\sigma^2}]$$

$$= argmax_{\mu,\sigma^2} [-\sum_{i=1}^I(x_i-\mu)^2]$$

$$= argmax_{\mu,\sigma^2} [\sum_{i=1}^I(x_i-\mu)^2]$$

也就是說least squares fitting和使用maximum likelihood 估計常態分布的均值參數是等價的。

log likehood好處

likelihood function可以看到是許多乘法的結果,因此微分很不好算。
由於log函數是monotonic的關係,所以likelihood function經過log轉換後的最大值會在相同的地方,而且經過log轉換後乘法變成加法,因此微分變得容易許多。

https://bookdown.org/dereksonderegger/571/13-maximum-likelihood-estimation.html#likelihood-function

https://math.stackexchange.com/questions/3053131/why-are-the-local-extrema-of-a-log-transformed-function-equal-to-local-extrema-o

https://towardsdatascience.com/log-loss-function-math-explained-5b83cd8d9c83

方法二Maximum posteriori estimation

根據前面的定義,Maximum posteriori estimation的cost function為
$$\hat{\mu}, {\hat\sigma}^2 = argmax_{\mu,\sigma^2} [\prod_{i=1}^I Pr(x_i|\mu,\sigma^2)Pr(\mu,\sigma^2)]$$

$$= argmax_{\mu,\sigma^2} [\prod_{i=1}^I Norm_{x_i}[\mu,\sigma^2]NormInvGam_{\mu,\sigma^2}[\alpha,\beta,\gamma,\delta]]$$

在這裡我們選擇我們選擇了normal inverse gamma prior,其參數為α,β,γ,δ(圖4.4),因為它與normal distribution共軛。

在式子中的prior如下
$$Pr(\mu,\sigma^2) = \frac{\sqrt{\gamma}}{\sigma\sqrt{2\pi}}\frac{\beta^{\alpha}}{\Gamma(\alpha)}(\frac{1}{\sigma^2})^{\alpha + 1}exp[-\frac{2\beta + \gamma(\delta-\mu)^2}{2\sigma^2}]$$

而posterior distribution 與likelihood和prior的乘積成正比(見圖4.5),在與數據一致且先驗可信的區域具有最高的機率密度。

而跟maximum likelihood一樣我們利用把式子取log來計算最大值。式子如下
$$\hat{\mu}, {\hat\sigma}^2 = argmax_{\mu,\sigma^2} [\sum_{i=1}^I log [Norm_{x_{i}}[\mu, {\sigma}^2]] + log [NormInvGam_{\mu,\sigma^2}[\alpha,\beta,\gamma,\delta]]]$$

要找到MAP(maximum a posteriori)我們將式子拆成兩段並且分別對$$\mu$$和$$\sigma^2$$做偏微分。式子如下
$$\hat{\mu} = \frac{\sum_{i=1}^I x_i + \gamma\delta}{I + \gamma}$$

$$\hat{\sigma}^2 = \frac{\sum_{i=1}^I (x_i-\hat{\mu})^2 + 2\beta + \gamma(\delta-\hat{\mu})^2}{I + 3 + 2\alpha}$$

而平均數$\hat{\mu}$的解答可以進一步簡化
$$\hat{\mu} = \frac{I\bar{x} + \gamma\delta}{I + \gamma}$$

這式子是兩項的加權平均值,第一項是資料的平均$\bar{x}$並且以訓練樣本的數量$I$為權重,第二項是先驗分佈的參數$\delta$並且以先驗分佈的參數$\gamma$為權重

這裡給我們一些MAP(maximum a posteriori)的洞察。

  1. 當資料數量越多時,MAP的解會越接近資料平均(也就是ML(Maximum likelihood)的解)
  2. 當資料數量少一些的時候,MAP的解會在ML和prior的中間
  3. 當完全沒有資料的時候,MAP的解就是proir

方法三Bayesian estimation

在Bayesian estimation我們利用Bayesian定理計算參數的posterior distribution。
$$Pr(\mu, \sigma^2 | x_{1…I}) = \frac{\prod_{i=1}^I Pr(x_i|\mu,\sigma^2)Pr(\mu,\sigma^2)}{Pr(x_{1…I})}$$

$$= \frac{\prod_{i=1}^I Norm_{x_i}[\mu,\sigma^2]NormInvGam_{\mu,\sigma^2}[\alpha,\beta,\gamma,\delta]}{Pr(x_{1…I})}$$

$$= \frac{\kappa NormInvGam_{\mu,\sigma^2}[~\alpha,~\beta,~\gamma,~\delta]}{Pr(x_{1…I})}$$

在這裡likelihood和prior有共軛關係,而$\kappa$是 associated constant。Normal likelihood和normal inverse gamma prior的乘機產生出一個關於$\mu$$\sigma^2$的posterior distribution。其參數如下

$$~\alpha = \alpha + \frac{I}{2}$$

$$~\gamma = \gamma + I$$

$$~\delta = \frac{\gamma\delta + \sum_{i=1} x_i}{\gamma + I}$$

$$~\beta = \frac{\sum_{i=1} x_i^2}{2} + \beta + \frac{\gamma\delta^2}{2} - \frac{(\gamma\delta + \sum_{i=1} x_i)^2}{2(\gamma + I)}$$

需要注意的是,後驗分布(式4.20左側)必須是一個有效的概率分布,總和為一,因此共軛乘積中的常數 κ 和右側的分母必須完全抵消,才能得到:

$$Pr(\mu, \sigma^2 | x_{1…I}) = NormInvGam_{\mu,\sigma^2}[~\alpha,~\beta,~\gamma,~\delta]$$

現在我們可以看到conjugate prior的好處,我們保證可以得到關於參數的後驗分布的封閉形式表達式。

  • Predictive density
    跟maximum likelihood和MAP(maximum a posteriori)不一樣是,Bayesian estimation計算Predictive density的方式是我們計算每個可能參數集的預測值的加權平均值,其中加權由參數的後驗分布給出。

$$Pr(x_*|x_{1…I}) = \int \int Pr(x^*|\mu,\sigma^2)Pr(\mu,\sigma^2|x_{1…I})d\mu d\sigma^2$$

$$= \int \int Norm_{x^*}[\mu,\sigma^2]NormInvGam_{\mu,\sigma^2}[~\alpha,~\beta,~\gamma,~\delta]d\mu d\sigma^2$$

$$= \int \int \kappa(x^*, ~\alpha, ~\beta, ~\gamma, ~\delta)NormInvGam_{\mu,\sigma^2}[~\alpha,~\beta,~\gamma,~\delta]d\mu d\sigma^2$$

這裡我們又再次用到conjugate relation。積分包含一個與$\mu$和$\sigma^2$無關的常數乘以一個概率分布。將常數移到積分號外,可以得到:

$$Pr(x^*|x_{1…I}) = \kappa(x^*, ~\alpha, ~\beta, ~\gamma, ~\delta)\int \int NormInvGam_{\mu,\sigma^2}[~\alpha,~\beta,~\gamma,~\delta]d\mu d\sigma$$

因為pdf的積分為1,所以

$$= \kappa(x^*, ~\alpha, ~\beta, ~\gamma, ~\delta)$$

常數可以表示為:

$$\kappa(x^*, ~\alpha, ~\beta, ~\gamma, ~\delta) = \frac{1}{\sqrt{2\pi}}\frac{\sqrt{~\gamma}~\beta^{~\alpha}}{\sqrt{\v\gamma}\v\beta^{\v\alpha}}\frac{\Gamma(\v\alpha)}{\Gamma(~\alpha)}$$

其中

$$\v\alpha = ~\alpha + \frac{I}{2}$$

$$\v\gamma = ~\gamma + I$$

$$\v\beta = \frac{\sum_{i=1} x_i^2}{2} + ~\beta + \frac{~\gamma~\delta^2}{2} - \frac{(~\gamma~\delta + \sum_{i=1} x_i)^2}{2(~\gamma + I)}$$

在這裡我可以看到第二個使用conjugate prior的好處:可以計算積分,所以我們得到一個很好的封閉形式表達式來預測密度。

在有大量訓練數據的情況下,貝葉斯預測分布和最大事後概率(MAP)預測分布非常相似,但當數據量減少時,貝葉斯預測分布的尾部明顯更長。這是貝葉斯解決方案的典型特徵:它們在預測時更加中庸(不那麼確定)。在 MAP 的情況下,錯誤地承諾一個 µ 和 σ^2 的估計值導致我們對未來的預測過於自信。

ch4.5範例一

第二個範例我們考慮離散的資料${x_i}^I_{i=1}$其中$x_i \in {1, 2, …, 6}$。這種表達方式可以用來表達一個不均勻的骰子所出現的點數資料。我們將使用 categorical distribution來描述這些資料。
$$ Pr(x=k|\lambda_{1…K}) = \lambda_k $$

利用Maximum posteriori estimation和maximum a posteriori來推測六個參數${\lambda_k}^6_{k=1}$。而Bayesian方法則計算參數的probability distribution

4.5.1 Maximum Likelihood

為了找到Maximum Likelihood,我們要最大化每一個資料點的likelihood相乘的乘積

$$ \hat \lambda_{1…6} = argmax_{\lambda_{1…6}}[\prod^I_{i=1}Pr(x_i|\lambda_{1…6})] \qquad s.t. \sum_k \lambda_k = 1$$

$$ = argmax_{\lambda_{1…6}}[\prod^I_{i=1} Cat_{x_i}[\lambda_{1…6}]] \qquad s.t. \sum_k \lambda_k = 1$$

$$ = argmax_{\lambda_{1…6}}[\prod^6_{k=1}\lambda^{N_k}_k] \qquad s.t. \sum_k \lambda_k = 1$$

其中$N_k$代表在訓練資料中觀察到k的總次數。跟前一個例子一樣,利用log probability來協助尋找最大值。

$$ L = \sum^6_{k=1}N_klog[\lambda_k] + \nu(\sum^6_{k=1}\lambda_k - 1) $$
在這個式子中利用了 Lagrange multiplier $\nu$ 來達成$\sum^6_{k=1} \lambda_k = 1$的限制。接著我們對L對於$\lambda_k$和$\nu$微分並且將導數設為0,可以得到下式。
$$ \hat \lambda_k = \frac{N_k}{\sum^6_{m=1}N_m} $$
換句話說$\lambda_k$和觀察到k的次數成正比。

4.5.2 Maximum a posteriori

要找到 maximum a posteriori 首先要定義一個prior。我們選擇和categorical likelihood共軛的 Dirichlet distribution。式子如下。
$$ \hat \lambda_{1…6} = \underset{\lambda_{1…6}}{\mathrm{argmax}}[\prod^I_{i=1}Pr(x_i|\lambda_{1…6})Pr(\lambda_{1…6})]$$

$$ = \underset{\lambda_{1…6}}{\mathrm{argmax}}[\prod^I_{i=1}Cat_{xi}[\lambda_{1…6}]Dir_{\lambda_{1…6}}[\alpha_{1…6}]]$$

$$ = \underset{\lambda_{1…6}}{\mathrm{argmax}}[\prod^6_{k=1}\lambda_k^{N_k}\prod^6_{k=1}\lambda_k^{\alpha_k-1}]$$

$$ = \underset{\lambda_{1…6}}{\mathrm{argmax}}[\prod^6_{k=1}\lambda^{N_k + \alpha_k -1}_k] $$

接著和 maximum likelihood一樣利用 Lagrange multiplier來達成$\sum^6_{k=1} \lambda_k = 1$的限制。Maximum a posteriori推估參數的式子如下。
$$ \hat \lambda_k = \frac{N_k + \alpha_k -1}{\sum^6_{m=1}(N_m + \alpha_m - 1)}$$
其中$N_k$代表訓練資料中k出現的次數,這裡注意到如果$\alpha_k$全部設為1的時候,式子就變成跟 maximum likelihood的解一樣。

4.5.3 Bayesian Approach

在Bayesian approach中我們計算對於參數的posterior。
$$Pr(\lambda_1 … \lambda_6|x_{1…I}) = \frac{\prod^I_{i=1}Pr(x_i|\lambda_{1…6})Pr(\lambda_{1…6})}{Pr(x_{1…I})}$$
$$=\frac{\prod^I_{i=1}Cat_{x_i}[\lambda_{1…6}]Dir_{\lambda_{1…6}}[\alpha_{1…6}]}{Pr(x_{1…I})}$$
$$=\frac{\kappa(\alpha_{1…6}, x_{1…I})Dir_{\lambda_{1…6}}[~\alpha_{1…6}]}{Pr(x_{1…I})}$$
$$=Dir_{\lambda_{1…6}}[~\alpha_{1…6}]$$

其中$~\alpha_k=N_k+\alpha_k$。我們再次利用共軛關係,以產生具有與先驗分布相同形式的後驗分布。為了確保左邊的概率分布有效,常數κ必須再次與分母相抵消。

Predictive Density

Maximum Likelihood和Maximum a posteriori計算Predictive Density的方式就是把新的資料點代入求出來的參數。注意到如果prior是uniform(也就是$\alpha_{1…6}=1$),則MAP和ML會完全一樣,而預測結果會和觀察資料的頻率一樣。
對於Bayesian Approach,我們計算每個可能的參數集的預測的加權平均值,其中加權由參數的後驗分布給出。
$$Pr(x^*|x_{1…I})=\int Pr(x^*|\lambda_{1…6})Pr(\lambda_{1…6}|x_{1…I})d\lambda_{1…6}$$
$$=\int Cat_{x^*}[\lambda_{1…6}]Dir_{\lambda_{1…6}}[~\alpha_{1…6}]d\lambda_{1…6}$$
$$=\int \kappa(x^*, ~\alpha_{1…6})Dir_{\lambda_{1…6}}[\breve\alpha_{1…6}]d\lambda_{1…6}$$
$$=\kappa(x^*, ~\alpha_{1…6})$$

在這裡,我們再次利用共軛關係,得到一個常數乘以一個概率分布,而積分則簡單地等於該常數,因為概率分布的積分為一。
$$Pr(x^*=k|x_{1…I})=\kappa(x^*, ~\alpha_{1…6})=\frac{N_k+\alpha_k}{\sum^6_{j=1}(N_j+\alpha_j)}$$

再次強調貝葉斯預測密度比 ML/MAP解更不自信。特別是,儘管在訓練數據中從未觀察到$x^*=4$這個值,但它並未將觀察到該值的概率分配為零。這是合理的;僅僅因為在15次觀察中我們並未抽到4這個數字,並不意味著我們將永遠不會看到它。我們可能只是運氣不好。貝葉斯方法將這一點納入考慮,並給予這個類別一個小的概率。

CH5 The normal distribution

回顧第三章 multivariate normal distribution有兩個參數:平均$\mu$和變異數$\Sigma$。Proability density function為:
$$Pr(x) = \frac{1}{(2\pi)^{D/2}|\Sigma|^{1/2}}exp[-0.5(x - \mu)^T\Sigma^{-1}(x-\mu)]$$

或是簡短的
$$Pr(x)=Norm_x[\mu, \Sigma]$$

5.1 covariance矩陣的種類

covariance矩陣有三種類型

  1. spherical:對角元素全部都是同樣的正數,而且除了對角元素以外都是0
  2. diagonal:對角元素數字不一樣且均為正數,而且除了對角元素以外都是0
  3. full covariances:所有元素都不為0的正數,此外矩陣式對稱的也就是$\sigma_{12}^2=\sigma_{21}^2$

對於bivariate的情況spherical covariances產生出圓的圖形,Diagonal covariances產生出橢圓的圖形,而且橢圓的主軸和座標軸重疊,Full covariances產生出橢圓但是他的主軸方向可以是任意方向。
如果為 covariance 為 spherical 或 diagonal,則個別的變數均為獨立的,也就是
$$Pr(x_1, x_2) = Pr(x_1)Pr(x_2)$$

5.2 Decomposition of covariance

利用幾何的觀點可以Decomposition full covariances matric。想像一下有一個新的座標軸對齊full covariances所產生的橢圓圖形的兩個主軸,在這個新的座標軸上觀察這這個圖形,他的covariance matric就變成diagonal covariance matric。所以用座標轉換的觀點,我們可以得到新坐標軸上的diagonal covariance matric和原作標軸上full covariances matric的關係。其中R為座標旋轉矩陣。
$$\Sigma_{full}=R^T\Sigma’{diag}R$$
拆解過後,$\Sigma’
{diag}$裡面隱含了varience,也就是在新座標軸上圖形的寬度資訊,因此可以再利用eigen-decomposition得在空間中哪一個方向對圖形比較重要。

5.3 Linear transformations of variables

multivariate normal pdf 經過線性轉換後,依然是一個multivariate normal,而轉換後的multivariate normal他的mean和covarience會和轉換前的multivariate normal有關,也和線性轉換方程式的$y=Ax+b$的係數和常數有關。
這個關係讓我麼可以簡化抽樣normal distribitaion的過程。假設想要從mean為$\mu$和covarianc為$\Sigma$的normal distribitaion抽樣,首先我們可以先從一個standart normal distribution抽樣一個點(mean $\mu=0$, covariance $\Sigma=I$),接著套用Linear transformations $y=\Sigma^{1/2}x+\mu$

影音串流如何運作的

Live Streaming工作流程

首先Live Streaming起頭於將大量的影音檔壓縮以便傳送,我們利用encoder將原始影音檔用指定的codec(例如H.264)進行壓縮。經過壓縮後gigabytes大小的資料被縮小成megabytes大小。

經過壓縮的資料encoder會把資料放入media container,這動作稱為打包。media container的目的是為了讓其他人知道這些被壓縮的資料是用什麼codec壓縮的。media container另一個重要功能是紀錄用來同步聲音和影像的資訊。常見的media container格式有mp4。

被打包過後的資料透過特定的傳輸協議protocol在網路上傳送,常見的協議有RTMP、HLS。

參考:
https://pjchender.blogspot.com/2019/07/protocol-of-media-video-and-audio.html
https://yarslv.com/codecs-and-containers-explained/#what-is-a-container
https://www.wowza.com/blog/complete-guide-to-live-streaming

Data exploration for Object Detection

資料整體的品質

首先可以對整個資料集做一個初步的檢查,包含

  1. 瀏覽整份資料集
  2. 確認沒有嚴重有誤的照片(例如全黑的照片)
  3. 確認所有照片都能夠被電腦讀取(以免訓練到一半程式被中斷)

照片尺寸和深寬比

對於整份資料集,統計所有照片的深寬比和尺寸十分重要,這些將會影響anchor size 和 ratios。通常有三種情況

  1. 照片大小和深寬比都一樣:
    只需要決定縮放比例
  2. 照片大小和深寬比不同但差異不大,深寬比都介於0.7~1.5:
    可以non-destructive resize(也就是不改變深寬比的縮放) -> padding
  3. 照片大小和深寬比差異很大

參考:
https://neptune.ai/blog/data-exploration-for-image-segmentation-and-object-detection

makefile教學

Makefiles用途

用來決定大型程式需要被重新編譯的部分。

第一個範例

首先安裝make,並且將下面程式放到名稱為Makefile的檔案裡面。注意Makefile必須要用TAB來縮排而不是用空白鍵。

1
2
hello:
echo "Hello, World"

接下來在Makefile所在的資料夾位置下make指令

1
2
3
$ make
echo "Hello, World"
Hello, World

Makefile語法

Makefile是由許多的規則組合而成,每一條則看起來如下

1
2
3
4
targets: prerequisites
command
command
command
  • targets是檔案名稱,用空白建作為分隔。通常每個rule只有一個target
  • command是產生targets的一系列的步驟。command以Tab作為開頭,而不是空白鍵。
  • prerequisites也是檔案名稱,以空白鍵作為分隔,這些是指令開始製作target前必須存在的檔案,因此這些檔案也可以稱為dependencies

Make基礎元素

1
2
3
hello:
echo "Hello, World"
echo "This line will always print, because the file hello does not exist."

以這個範例來說,

  • 有一個名為hello的target
  • 這個target有兩個command
  • 這個target沒有prerequisites
    接下來我們執行make hello,由於hello檔不存在,所以下面的指令會被執行。如果hello檔存在,make就不會做任何事。
    特別注意在這裡hello同時代表target以及檔案,因為通常來說下面的command執行的目的就是要生成target。
    下面舉一個編譯c語言的例子,首先我們製作一個blah.c檔。
    1
    2
    // blah.c
    int main() { return 0; }
    接下來製作另一個Makefile
    1
    2
    blah:
    cc blah.c -o blah
    接下來執行make,因為我們每有在第一個參數指定目標target,所以第一個target會被執行。第一次執行的時blah檔會被生成,如果再執行一次就會出現make: 'blah' is up to date的訊息,因為blah檔已經存在了。但是這有一個問題,如果我們更動blah.c檔案,make並不會重新編譯!!
    要解決這個問題就必須要增加prerequisite。
    1
    2
    blah: blah.c
    cc blah.c -o blah
    我們增加了blah的prerequisite,這時候make就除了檢查blah有沒有存在以外,還會另外去檢查blah.c是不是比blah還新。這裡我們可以看到make是利用系統的時間戳來判定blah.c有沒有被修改過,因此如果修改blah.c之後又把blah.c時間戳改回修改前的時間,那make就會以為blah.c沒有被修改過。

Make clean

clean常用來清除make產生的檔案,但是他並不是make的關鍵字。可以用make clean清除make產生的檔案。我們可以在makefile中編寫clean要清除的檔案。注意clean這個target除非你用make clean指令,不然他不會被執行。

1
2
3
4
5
some_file: 
touch some_file

clean:
rm -f some_file

Makefile使用相對路徑

1
rootdir = $(realpath .)

https://stackoverflow.com/a/3342259

Variables

變數Variables只能夠是字串,並且用:=賦值。對make來說單引號跟雙引號並意義,make會把他當成字元來處理。因此賦值的時候不需要加引號。
下面範例使用Variables

1
2
3
4
5
6
7
8
9
10
11
12
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file

file1:
touch file1
file2:
touch file2

clean:
rm -f file1 file2 some_file

要引用變數可以用${}或是$()

1
2
3
4
5
6
7
8
x := dude

all:
echo $(x)
echo ${x}

# Bad practice, but works
echo $x

Targets

all target

可以用來依次產生所有需要的target,通常會放在第一個target的位置,如此一來只要下make指令就可以生成所有target。

1
2
3
4
5
6
7
8
9
10
11
all: one two three

one:
touch one
two:
touch two
three:
touch three

clean:
rm -f one two three

多個target

當一個rule有多個target的時候,底下的command就會針對每一個target都跑一次
$@就是一個有target名稱的automatic variable,下面範例就可以用$@看看現在command正在執行的是哪一個target

1
2
3
4
5
6
7
8
9
all: f1.o f2.o

f1.o f2.o:
echo $@
# Equivalent to:
# f1.o:
# echo f1.o
# f2.o:
# echo f2.o

Automatic Variables and Wildcards

* 萬用字元

在cmake中*%在cmake中都是萬用字元,但是它們代表的意義不一樣。*最好要包裝在wildcard 萬用字符函式中。否則可能會常陷入下面常見的陷阱。

  1. 陷阱: *不能直接在變量定義中使用
  2. 陷阱: 當*未匹配任何文件時,它會保持不變(除非在萬用字符函數(wildcard)中運行)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    thing_wrong := *.o # Don't do this! '*' will not get expanded
    thing_right := $(wildcard *.o)

    all: one two three four

    # Fails, because $(thing_wrong) is the string "*.o"
    one: $(thing_wrong)

    # Stays as *.o if there are no files that match this pattern :(
    two: *.o

    # Works as you would expect! In this case, it does nothing.
    three: $(thing_right)

    # Same as rule three
    four: $(wildcard *.o)

% 萬用字元

%非常有用,但由於它可以在各種情況下使用,因此有些令人困惑。

  1. 在“匹配”模式下使用時,它會在字符串中匹配一個或多個字符。此匹配稱為stem。
  2. 在“替換”模式下使用時,它會取出匹配的stem,並將其替換為一個字符串。
  3. 在規則定義和某些特定函數中最常用。

Automatic Variables

這裡有完整的Automatic Variables表,下面只介紹常用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
hey: one two
# Outputs "hey", since this is the target name
# 印出目前的target
echo $@

# Outputs all prerequisites newer than the target
# 印出所有比target新的prerequisite
echo $?

# Outputs all prerequisites
# 印出所有的prerequisite
echo $^

touch hey

one:
touch one

two:
touch two

clean:
rm -f hey one two

Rules

Make的隱藏規則

CC CXX CFLAGS CXXFLAGS LDFLAGS LDLIBS這些變數是make的隱藏規則。

  • CC : 編譯C語言的編譯器; 預設是 cc
  • CXX : 編譯C++語言的編譯器; 預設是 g++
  • CFLAGS : 給C語言編譯器的額外參數
  • CXXFLAGS : 給C++語言編譯器的額外參數
  • CPPFLAGS : 給C/C++編譯器的額外參數
  • LDFLAGS : 給連結器的額外參數

下面範例使用隱藏規則

1
2
3
4
5
6
7
8
9
10
11
12
CC = gcc # 使用gcc來編譯C語言
CFLAGS = -g # 給gcc的額外參數,開啟debug模式

# 隱藏規則 #1: blah會由C連接器產生(即使我們的command沒有呼叫C連接器)
# 隱藏規則 #2: blah.o 會由c編譯器產生,因為blah.c存在(即使我們的command沒有呼叫c編譯器)
blah: blah.o

blah.c:
echo "int main() { return 0; }" > blah.c

clean:
rm -f blah*

Static Pattern Rules

他的語法如下

1
2
targets...: target-pattern: prereq-patterns ...
commands

這個語法的意思是,如果有一個target符合target-pattern(利用% wildcard),且它的所有prerequisite都符合prereq-patterns,那麼就會執行commands
例如我們可以改寫下面makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
foo.o: foo.c
bar.o: bar.c
all.o: all.c

all.c:
echo "int main() { return 0; }" > all.c

%.c:
touch $@

clean:
rm -f *.c *.o all

改寫後如下,可以看到,我們把foo.o bar.o all.o的規則都合併成一個規則$(objects): %.o: %.c。首先foo.o符合%.o,且它的所有prerequisite都符合%.c,因此會執行%.o: %.c的規則。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
# Syntax - targets ...: target-pattern: prereq-patterns ...
# In the case of the first target, foo.o, the target-pattern matches foo.o and sets the "stem" to be "foo".
# It then replaces the '%' in prereq-patterns with that stem
$(objects): %.o: %.c

all.c:
echo "int main() { return 0; }" > all.c

%.c:
touch $@

clean:
rm -f *.c *.o all

Static Pattern Rules and Filter

此外,我們可以使用filter函式來過濾掉不需要的檔案,後面會再講到函式這裡只是先展示如何跟函式搭配使用,在下面的範例我們使用了 .raw 和 .result 這兩個擴展名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

all: $(obj_files)
# Note: PHONY is important here. Without it, implicit rules will try to build the executable "all", since the prereqs are ".o" files.
.PHONY: all

# Ex 1: .o files depend on .c files. Though we don't actually make the .o file.
$(filter %.o,$(obj_files)): %.o: %.c
@echo "target: $@ prereq: $<"

# Ex 2: .result files depend on .raw files. Though we don't actually make the .result file.
$(filter %.result,$(obj_files)): %.result: %.raw
@echo "target: $@ prereq: $<"

%.c %.raw:
@echo "touch $@ prereq: $<"
touch $@

clean:
rm -f $(src_files)

執行的結果如下,首先執行第一條規則all: $(obj_files)產生第一個target foo.result,並由$(filter %.result,$(obj_files)): %.result: %.raw產生foo.resultfoo.result的prerequisitfoo.raw%.c %.raw:產生。
可以看到,我們把foo.result bar.o lose.o的規則都合併成一個規則$(filter %.result,$(obj_files)): %.result: %.raw。首先foo.result符合%.result,且它的所有prerequisite都符合%.raw,因此會執行%.result: %.raw的規則。

1
2
3
4
5
6
touch foo.raw
target: foo.result prereq: foo.raw
touch bar.c
target: bar.o prereq: bar.c
touch lose.c
target: lose.o prereq: lose.c

Pattern Rules

我們可以將Pattern Rules視為兩種用法。

  • 自定義的implicit rules

    1
    2
    3
    # 自訂一個 pattern rule 將每一個.c檔編譯成.o檔
    %.o : %.c
    $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
  • 簡化版的static pattern rules

    1
    2
    3
    4
    # 定義一個沒有 prerequisites 的 pattern rule
    # 他將會在需要的時候產出一個空白的.c檔
    %.c:
    touch $@

    在這裡%代表任意非空白的字串。

Double-Colon Rules

Double-Colon Rules 很少被用到,他允許對同一個target定義多個規則,且這些規則可以是不同的commands。如以下範例

1
2
3
4
5
6
7
all: blah

blah::
@echo "hello"

blah::
@echo "hello again"

他的輸出是

1
2
hello
hello again

Commands and execution

指令回顯/禁用

在預設情況下,make會回顯每一個command,如果你不想要回顯,可以在command前面加上@,如下

1
2
3
all: 
@echo "This make line will not be printed"
echo "But this will"

命令執行

每一行命令都會在一個新的shell中執行,因此如果你想要在同一個shell中執行,可以使用分號;來連接命令,如下

1
2
3
4
5
6
7
8
9
10
11
all: 
cd ..
# The cd above does not affect this line, because each command is effectively run in a new shell
echo `pwd`

# This cd command affects the next because they are on the same line
cd ..;echo `pwd`

# Same as above
cd ..; \
echo `pwd`

預設shell

預設情況下,make會使用/bin/sh來執行命令,如果你想要使用其他的shell,可以使用.SHELL來指定,如下

1
2
3
4
SHELL=/bin/bash

cool:
echo "Hello from bash"

$$符號

在Makefile中,$$代表一個$符號,如此一來,我們就可以在Makefile中使用bash或是sh的shell variable。在下面這個例子中特別注一一下 Makefile variables 和 Shell variables

1
2
3
4
5
6
7
make_var = I am a make variable
all:
# Same as running "sh_var='I am a shell variable'; echo $sh_var" in the shell
sh_var='I am a shell variable'; echo $$sh_var

# Same as running "echo I am a make variable" in the shell
echo $(make_var)

-k-i-進行錯誤處理

執行make的時候使用-k參數可以讓make繼續執行,即使其中一個target失敗了。執行make的時候使用-i參數可以讓make忽略所有的錯誤。

在command前面加上-可以讓make忽略該command的錯誤,如下

1
2
3
4
one:
# This error will be printed but ignored, and make will continue to run
-false
touch one

打斷或是結束make

ctrl + c可以打斷或是結束make,他將會刪掉剛生成的target

遞迴使用 make

為了遞迴調用 Makefile,使用特殊的 $(MAKE) 代替 make,因為它將為您傳遞 make 標誌,並且不會受到這些標誌的影響。

當使用 $(MAKE) 來遞迴調用 Makefile 時,它將傳遞先前用於調用 make 的所有標誌和選項,以及在 Makefile 中定義的任何其他變量。這有助於確保在整個項目中使用相同的編譯選項和變量。同時,$(MAKE) 不會受到當前 make 的影響,這可以避免不必要的錯誤和不一致性。

1
2
3
4
5
6
7
8
new_contents = "hello:\n\ttouch inside_file"
all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
cd subdir && $(MAKE)

clean:
rm -rf subdir

Export, environments, and recursive make

當make執行的時候,他會先把所有環境變數轉換成make的變數,例如下面範例假如我們先在shell設定環境變數shell_env_var

  1. 設定環境並且執行make
    1
    export shell_env_var='I am an environment variable'; make
  2. 執行下面的makefile
    1
    2
    3
    4
    5
    6
    all:
    # Print out the Shell variable
    echo $$shell_env_var

    # Print out the Make variable
    echo $(shell_env_var)
  • make的export指令可以把make的變數直接轉換成環境變數
    1
    2
    3
    4
    5
    shell_env_var=Shell env var, created inside of Make
    export shell_env_var
    all:
    echo $(shell_env_var)
    echo $$shell_env_var

如此一來當我們在make command呼叫make的時候,就可以利用export指令將變數傳遞給子make程式。下面範例中cooly變數會被傳遞到子資料夾內所執行的make的makefile裡面。這裡可以注意到,cooly變數在all target之前被定義,但是他還是可以被all target使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new_contents = "hello:\n\techo \$$(cooly)"

all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
@echo "---MAKEFILE CONTENTS---"
@cd subdir && cat makefile
@echo "---END MAKEFILE CONTENTS---"
cd subdir && $(MAKE)

# Note that variables and exports. They are set/affected globally.
cooly = "The subdirectory can see me!"
export cooly
# This would nullify the line above: unexport cooly

clean:
rm -rf subdir
  • .EXPORT_ALL_VARIABLES
    .EXPORT_ALL_VARIABLES可以直接把所有的make變數都轉換成環境變數
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    .EXPORT_ALL_VARIABLES:
    new_contents = "hello:\n\techo \$$(cooly)"

    cooly = "The subdirectory can see me!"
    # This would nullify the line above: unexport cooly

    all:
    mkdir -p subdir
    printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
    @echo "---MAKEFILE CONTENTS---"
    @cd subdir && cat makefile
    @echo "---END MAKEFILE CONTENTS---"
    cd subdir && $(MAKE)

    clean:
    rm -rf subdir

make的命令列選項

這裡有make的命令列選項,可以注意--dry-run, --touch, --old-file這幾個。
另外make可以一次接受多個target,例如make clean run test就會先執行clean,接著run和test。

變數part2

兩種賦值方法

  • 延遲賦值(lazy evaluation)=:

    1
    2
    3
    4
    5
    6
    7
    VAR = foo
    VAR2 = $(VAR)
    VAR = bar

    all:
    # 在此處 VAR2 的值將是 "bar",因為VAR2直到被真正使用展開
    echo $(VAR2)

    ?=這個符號可以為沒被設定過的店數設定值

    1
    2
    3
    4
    5
    6
    7
    one = hello
    one ?= will not be set #one被設定過了,所以沒作用
    two ?= will be set #two還沒被設定做,將設定值

    all:
    echo $(one)
    echo $(two)

    輸出如下

    1
    2
    3
    4
    echo hello
    hello
    echo will be set
    will be set
  • 立即賦值(immediate assignment):=:

    1
    2
    3
    4
    5
    6
    7
    VAR := foo
    VAR2 := $(VAR)
    VAR := bar

    all:
    # 在此處 VAR2 的值將是 "foo",因為VAR2在:=的時候就展開了
    echo $(VAR2)

    因此:=可以append variable,如果是=就會齣戲無窮迴圈錯誤

    1
    2
    3
    4
    5
    6
    7
    one = hello
    # 這段程式可以運行
    # one gets defined as a simply expanded variable (:=) and thus can handle appending
    one := ${one} there

    all:
    echo $(one)

    下面這段程式會出現無窮迴圈錯誤。

    1
    2
    3
    4
    5
    6
    one = hello
    # 注意這裡用的是 = ,會造成無窮迴圈錯誤
    one = ${one} there

    all:
    echo $(one)

空白

一行字起頭的空白會被make忽略掉,但是尾巴空白的不會,要在起頭加空白可以用$(nullstring),更精確地說其實未定義的變數都是empty string

1
2
3
4
5
6
7
8
9
with_spaces =   hello   # with_spaces has many spaces after "hello"
after = $(with_spaces)there

start_space = $(nullstring) hello

all:
echo "$(after)"
echo "$(start_space)"
echo $(nowhere) #這也會輸出empty string

append

+=可以用來append variable

1
2
3
4
5
foo := start
foo += more

all:
echo $(foo)

覆寫make命列列參數

override可以用來覆寫make命令列參數,例如下面這個例子,分別用make option_one=himake option_two=hi去執行,可以發現只有option_one會被覆寫

1
2
3
4
5
6
7
# Overrides command line arguments
override option_one = did_override
# Does not override command line arguments
option_two = not_override
all:
echo $(option_one)
echo $(option_two)

針對target設定變數

變數可以只設定給指定的target,例如下面例子,one只定義給all target

1
2
3
4
5
6
7
all: one = cool

all:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

Pattern-specific variables

變數也可以指定義給特定的target patterns,例如下面例子,只有符合%.cpattern的target會被定義one

1
2
3
4
5
6
7
%.c: one = cool

blah.c:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

Makefile判斷式

if/else

1
2
3
4
5
6
7
8
foo = ok

all:
ifeq ($(foo), ok)
echo "foo equals ok"
else
echo "nope"
endif

檢查變數是否為空

1
2
3
4
5
6
7
8
9
10
nullstring =
foo = $(nullstring) # end of line; there is a space here

all:
ifeq ($(strip $(foo)),)
echo "foo is empty after being stripped"
endif
ifeq ($(nullstring),)
echo "nullstring doesn't even have spaces"
endif

檢查變數是否被定義

1
2
3
4
5
6
7
8
9
10
bar =
foo = $(bar)

all:
ifdef foo
echo "foo is defined"
endif
ifndef bar
echo "but bar is not"
endif

$(MAKEFLAGS)

下面範例展示如何使用 findstringMAKEFLAGS 測試 make flag。分別用make指令和make -i指令執行下面makefile

1
2
3
4
5
all:
# Search for the "-i" flag. MAKEFLAGS is just a list of single characters, one per flag. So look for "i" in this case.
ifneq (,$(findstring i, $(MAKEFLAGS)))
echo "i was passed to MAKEFLAGS"
endif

Functions

First Functions

函式主要用來處理文字。呼叫函式的方法有$(fn, arguments)${fn, arguments},而make也內建許多函式。例如subst替換掉文字。

1
2
3
bar := ${subst not, totally, "I am not superman"}
all:
@echo $(bar)

而如果你相替換掉空白或是逗號,可以利用變數。

1
2
3
4
5
6
7
8
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))

all:
@echo $(bar)

特別注意到不要在逗號和下一個參數之間留空白,因為它會被視為文字。

1
2
3
4
5
6
7
8
9
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space), $(comma) , $(foo))

all:
# Output is ", a , b , c". Notice the spaces introduced
@echo $(bar)

字串替換

函式$(patsubst pattern,replacement,text)的功能如下。

1
2
3
4
5
6
7
8
9
10
11
foo := a.o b.o l.a c.o
one := $(patsubst %.o,%.c,$(foo))
# This is a shorthand for the above
two := $(foo:%.o=%.c)
# This is the suffix-only shorthand, and is also equivalent to the above.
three := $(foo:.o=.c)

all:
echo $(one)
echo $(two)
echo $(three)

The foreach function

foreach函式的用法為$(foreach var,list,text),foreach會把以空白間區隔文字的list一個一個賦值給var,而text會累加前面的結果,範例如下

1
2
3
4
5
6
7
foo := who are you
# For each "word" in foo, output that same word with an exclamation after
bar := $(foreach wrd,$(foo),$(wrd)!)

all:
# Output is "who! are! you!"
@echo $(bar)

if function

用法如下

1
2
3
4
5
6
7
foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)

all:
@echo $(foo)
@echo $(bar)

The call function

make可以用call來呼叫自定義函式

1
2
3
4
5
sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)

all:
# Outputs "Variable Name: sweet_new_fn First: go Second: tigers Empty Variable:"
@echo $(call sweet_new_fn, go, tigers)

The shell function

make也可以呼叫shell函式,但是會把輸出的換行符號改成空白鍵

其他功能

Include Makefiles

使用Include可以讓makefile裡面呼叫其他makefile

vpath 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vpath %.h ../headers ../other-directory

# Note: vpath allows blah.h to be found even though blah.h is never in the current directory
some_binary: ../headers blah.h
touch some_binary

../headers:
mkdir ../headers

# We call the target blah.h instead of ../headers/blah.h, because that's the prereq that some_binary is looking for
# Typically, blah.h would already exist and you wouldn't need this.
blah.h:
touch ../headers/blah.h

clean:
rm -rf ../headers
rm -f some_binary

換行

指令太長可以利用\換行

1
2
3
some_file: 
echo This line is too long, so \
it is broken up into multiple lines

.phony

在目標中添加”.PHONY”將防止Make將虛擬目標與文件名混淆。在這個例子中,如果創建了名為”clean”的文件,”make clean”仍然會運行。從技術上講,我應該在每個帶有”all”或”clean”的例子中都使用它,但為了保持例子的清晰,我沒有這樣做。此外,”phony”目標通常具有很少用作文件名的名稱,在實踐中許多人都會跳過這一步。

1
2
3
4
5
6
7
8
some_file:
touch some_file
touch clean

.PHONY: clean
clean:
rm -f some_file
rm -f clean

.delete_on_error

如果命令返回非零的退出狀態,make工具將停止運行規則(並將向前傳播到前置要求)。
DELETE_ON_ERROR將在規則以這種方式失敗時刪除該規則的目標。這將對所有目標發生,不僅僅是像PHONY這樣的目標。儘管由於歷史原因,make工具沒有使用這個選項,但始終使用它是一個好主意。

1
2
3
4
5
6
7
8
9
10
.DELETE_ON_ERROR:
all: one two

one:
touch one
false

two:
touch two
false

deepstream教學

客製化模型實作nvinfer介面

nvinfer呼叫介面

任何客製化介面最終必須被編譯成一個獨立的shared library。nvinfer在執行期間利用dlopen()呼叫函式庫,並且利用dlsym()呼叫函式庫中的函式。進一步的資訊紀錄在nvdsinfer_custom_impl.h裡面https://docs.nvidia.com/metropolis/deepstream/sdk-api/nvdsinfer__custom__impl_8h.html

客製化Output Parsing

  • 對於detectors使用者必須自行解析模型的輸出並且將之轉化成bounding box 座標和物件類別。對於classifiers則是必須自行解析出物件屬性。範例在/opt/nvidia/deepstream/deepstream/sources/libs/nvdsinfer_customparser,裡面的README有關於使用custom parser的說明。

  • 客製化parsing function必須為NvDsInferParseCustomFunc型態。在nvdsinfer_custom_impl.h的221行可以看到下面的型態定義,代表每一個客製化的解析函式都必須符合這個格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * Type definition for the custom bounding box parsing function.
    *
    * @param[in] outputLayersInfo A vector containing information on the output
    * layers of the model.
    * @param[in] networkInfo Network information.
    * @param[in] detectionParams Detection parameters required for parsing
    * objects.
    * @param[out] objectList A reference to a vector in which the function
    * is to add parsed objects.
    */
    typedef bool (* NvDsInferParseCustomFunc) (
    std::vector<NvDsInferLayerInfo> const &outputLayersInfo,
    NvDsInferNetworkInfo const &networkInfo,
    NvDsInferParseDetectionParams const &detectionParams,
    std::vector<NvDsInferObjectDetectionInfo> &objectList);
  • 客製化parsing function可以在Gst-nvinfer的參數檔parse-bbox-func-namecustom-lib-name屬性指定。例如我們定義了Yolov2-tiny的客製化bounding box解析函式NvDsInferParseCustomYoloV2Tiny,編譯出來的shared library位於nvdsinfer_custom_impl_Yolo/libnvdsinfer_custom_impl_Yolo.so,我們在設定檔就就必須要有以下設定

    1
    2
    parse-bbox-func-name=NvDsInferParseCustomYoloV2Tiny
    custom-lib-path=nvdsinfer_custom_impl_Yolo/libnvdsinfer_custom_impl_Yolo.so

可以藉由在定義函式後呼叫CHECK_CUSTOM_PARSE_FUNC_PROTOTYPE()marco來驗證函式的定義。
使用範例如下

1
2
3
4
5
6
7
8
9
extern "C" bool NvDsInferParseCustomYoloV2Tiny(
std::vector<NvDsInferLayerInfo> const& outputLayersInfo,
NvDsInferNetworkInfo const& networkInfo,
NvDsInferParseDetectionParams const& detectionParams,
std::vector<NvDsInferParseObjectInfo>& objectList)
{
...
}
CHECK_CUSTOM_PARSE_FUNC_PROTOTYPE(NvDsInferParseCustomYoloV2Tiny);

https://forums.developer.nvidia.com/t/deepstreamsdk-4-0-1-custom-yolov3-tiny-error/108391?u=jenhao

IPlugin Implementation

對於TensorRT不支援的network layer,Deepstream提供IPlugin interface來客製化處理。在/opt/nvidia/deepstream/deepstream/sources底下的objectDetector_SSD, objectDetector_FasterRCNN, 和 objectDetector_YoloV3資料夾展示了如何使用custom layers。

objectDetector_YoloV3範例中我們可以看到如何製作Tensorrt不支援的Yolov3的yolo layer。可以在yolo.cpp中看到自定義的layer是如何被呼叫使用的,程式節錄如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
....
else if (m_ConfigBlocks.at(i).at("type") == "yolo") {
nvinfer1::Dims prevTensorDims = previous->getDimensions();
assert(prevTensorDims.d[1] == prevTensorDims.d[2]);
TensorInfo& curYoloTensor = m_OutputTensors.at(outputTensorCount);
curYoloTensor.gridSize = prevTensorDims.d[1];
curYoloTensor.stride = m_InputW / curYoloTensor.gridSize;
m_OutputTensors.at(outputTensorCount).volume = curYoloTensor.gridSize
* curYoloTensor.gridSize
* (curYoloTensor.numBBoxes * (5 + curYoloTensor.numClasses));
std::string layerName = "yolo_" + std::to_string(i);
curYoloTensor.blobName = layerName;
nvinfer1::IPluginV2* yoloPlugin
= new YoloLayerV3(m_OutputTensors.at(outputTensorCount).numBBoxes,
m_OutputTensors.at(outputTensorCount).numClasses,
m_OutputTensors.at(outputTensorCount).gridSize);
assert(yoloPlugin != nullptr);
nvinfer1::IPluginV2Layer* yolo =
network.addPluginV2(&previous, 1, *yoloPlugin);
assert(yolo != nullptr);
yolo->setName(layerName.c_str());
std::string inputVol = dimsToString(previous->getDimensions());
previous = yolo->getOutput(0);
assert(previous != nullptr);
previous->setName(layerName.c_str());
std::string outputVol = dimsToString(previous->getDimensions());
network.markOutput(*previous);
channels = getNumChannels(previous);
tensorOutputs.push_back(yolo->getOutput(0));
printLayerInfo(layerIndex, "yolo", inputVol, outputVol, std::to_string(weightPtr));
++outputTensorCount;
}
...

而其他版本的YOLO,Nvidia也已經幫我們建立好許多Plugin,例如yolov2的region layer,Nvidia已經幫我們建立,其他已經建立好的layer可以在這裡找到。
https://github.com/NVIDIA/TensorRT/tree/1c0e3fdd039c92e584430a2ed91b4e2612e375b8/plugin

畫出範例的結構圖

首先在~/.bashrc加入下面這行設定pipeline圖儲存的位置,注意GStreamer不會幫你建立資料夾,你必須確認資料夾存在

1
export GST_DEBUG_DUMP_DOT_DIR=/tmp

接下來在pipeline 狀態設為PLAYING之前加入下面這行程式

1
GST_DEBUG_BIN_TO_DOT_FILE(pipeline, GST_DEBUG_GRAPH_SHOW_ALL, "dstest1-pipeline");

最後執行程式後就會產生.dot在前面設定的資料夾,你可以下載Graphviz,或是用VScode的插件來看圖

Deepstream 說明書

https://docs.nvidia.com/metropolis/deepstream/dev-guide/text/DS_plugin_gst-nvdsxfer.html

gst-launch-1.0建立rtsp輸入源的pipeline

首先先用gst-launch-1.0建立一個簡單的rtsp輸入、螢幕輸出的pipeline

1
gst-launch-1.0 rtspsrc location='rtsp://192.168.1.10:554/user=admin_password=xxxxxx_channel=1_stream=0.sdp' ! rtph264depay ! h264parse ! nvv4l2decoder ! nvvideoconvert ! video/x-raw,format=BGRx ! videoconvert ! video/x-raw,format=BGR ! autovideosink

將python的範例程式轉成c++

https://github.com/NVIDIA-AI-IOT/deepstream_python_apps/tree/master/apps/deepstream-rtsp-in-rtsp-out

建立mjpeg串流

  1. 指令方式
    1
    2
    3
    4
    gst-launch-1.0 -v rtspsrc location="rtsp://<rtsp url>/live1.sdp" \
    ! rtph264depay ! avdec_h264 \
    ! timeoverlay halignment=right valignment=bottom \
    ! videorate ! video/x-raw,framerate=37000/1001 ! jpegenc ! multifilesink location="snapshot.jpeg"
    https://stackoverflow.com/questions/59885450/jpeg-live-stream-in-html-slow

查詢deepstream bin的說明

1
gst-inspect-1.0 nvurisrcbin

gst-launch-1.0輸出除錯訊息到檔案

參考:
https://www.cnblogs.com/xleng/p/12228720.html

1
GST_DEBUG_NO_COLOR=1 GST_DEBUG_FILE=pipeline.log GST_DEBUG=5 gst-launch-1.0 -v rtspsrc location="rtsp://192.168.8.19/live.sdp" user-id="root" user-pw="3edc\$RFV" ! rtph264depay ! avdec_h264 ! timeoverlay halignment=right valignment=bottom ! videorate ! video/x-raw,framerate=37000/1001 ! jpegenc ! multifilesink location="snapshot.jpeg"

參考:
https://gstreamer.freedesktop.org/documentation/tutorials/basic/debugging-tools.html?gi-language=c
https://embeddedartistry.com/blog/2018/02/22/generating-gstreamer-pipeline-graphs/

本地端觀看udp傳送影像

host設為本機ip或127.0.0.1

send:

1
gst-launch-1.0 -v videotestsrc ! x264enc tune=zerolatency bitrate=500 speed-preset=superfast ! rtph264pay ! udpsink port=5000 host=$HOST

receive:

1
gst-launch-1.0 -v udpsrc port=5000 ! "application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264, payload=(int)96" ! rtph264depay ! h264parse ! avdec_h264 ! videoconvert ! autovideosink

Glibs說明書

http://irtfweb.ifa.hawaii.edu/SoftwareDocs/gtk20/glib/glib-hash-tables.html#g-int-hash

GDB文字圖形介面

https://blog.louie.lu/2016/09/12/gdb-%E9%8C%A6%E5%9B%8A%E5%A6%99%E8%A8%88/

範例

https://gist.github.com/liviaerxin/bb34725037fd04afa76ef9252c2ee875#tips-for-debug

rtsp 元件nvrtspoutsinkbin

nvrtspoutsinkbin沒有說明書,只能用gst-inspect-1.0看
https://forums.developer.nvidia.com/t/where-can-fine-nvrtspoutsinkbin-info/199124

範例
/opt/nvidia/deepstream/deepstream/sources/apps/sample_apps/deepstream_reference_apps/deepstream-bodypose-3d/sources/deepstream_pose_estimation_app.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Create RTSP output bin */
rtsp_out_bin = gst_element_factory_make ("nvrtspoutsinkbin", "nvrtsp-renderer");

if (!rtsp_out_bin) {
g_printerr ("Failed to create RTSP output elements. Exiting.\n");
return -1;
}

g_object_set (G_OBJECT (rtsp_out_bin), "sync", TRUE, NULL);
g_object_set (G_OBJECT (rtsp_out_bin), "bitrate", 768000, NULL);
g_object_set (G_OBJECT (rtsp_out_bin), "rtsp-port", rtsp_port_num, NULL);
g_object_set (G_OBJECT (rtsp_out_bin), "enc-type", enc_type, NULL);

gst_bin_add_many (GST_BIN (pipeline), rtsp_out_bin, NULL);

取得source id

https://forums.developer.nvidia.com/t/how-to-get-sources-index-in-deepstream/244461

可以用prob取得meta data
deepstream_test3_app.c 有範例

probe使用範例

metadata

切換輸入源

https://forums.developer.nvidia.com/t/how-switch-camera-output-gst-nvmultistreamtiler/233062

1
2
3
tiler_sink_pad.add_probe(Gst.PadProbeType.BUFFER, tiler_sink_pad_buffer_probe, 0)

tiler.set_property("show-source", <stream_id>) `

/opt/nvidia/deepstream/deepstream/sources/apps/apps-common/src/deepstream-yaml/deepstream_source_yaml.cpp有範例

斷線重連

rust的插件(可能可以編譯成c函式庫)
https://coaxion.net/blog/2020/07/automatic-retry-on-error-and-fallback-stream-handling-for-gstreamer-sources/

https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/tree/master/utils/fallbackswitch

編譯rust插件
https://www.collabora.com/news-and-blog/blog/2020/06/23/cross-building-rust-gstreamer-plugins-for-the-raspberry-pi/

RUST說明書
https://rust-lang.tw/book-tw/ch01-03-hello-cargo.html

截出有物件的圖

https://forums.developer.nvidia.com/t/saving-frame-with-detected-object-jetson-nano-ds4-0-2/121797/3

關閉Ubuntu圖形介面

https://linuxconfig.org/how-to-disable-enable-gui-on-boot-in-ubuntu-20-04-focal-fossa-linux-desktop

關閉使用gpu的資源

https://heary.cn/posts/Linux环境下重装NVIDIA驱动报错kernel-module-nvidia-modeset-in-use问题分析/

發現nvidia smi persistence mode會占用GPU資源,必須釋放掉才能安裝新的driver
可以用nvidia-smi的指令關掉https://docs.nvidia.com/deploy/driver-persistence/index.html#usage

1
nvidia-smi -pm 0

移除舊的driver

1
2
apt-get remove --purge nvidia-driver-520
apt-get autoremove

queue的用途

https://docs.xilinx.com/r/en-US/ug1449-multimedia/Performance-Improvement-from-the-GStreamer-Perspective

probe

https://coaxion.net/blog/2014/01/gstreamer-dynamic-pipelines/

https://erit-lvx.medium.com/probes-handling-in-gstreamer-pipelines-3f96ea367f31

deepstream-test4 用prob取得metadata的範例

NvDsBatchMeta資料圖:
https://docs.nvidia.com/metropolis/deepstream/dev-guide/text/DS_plugin_metadata.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
static GstPadProbeReturn
osd_sink_pad_buffer_probe (GstPad * pad, GstPadProbeInfo * info,
gpointer u_data)
{
GstBuffer *buf = (GstBuffer *) info->data;
NvDsFrameMeta *frame_meta = NULL;
NvOSD_TextParams *txt_params = NULL;
guint vehicle_count = 0;
guint person_count = 0;
gboolean is_first_object = TRUE;
NvDsMetaList *l_frame, *l_obj;

NvDsBatchMeta *batch_meta = gst_buffer_get_nvds_batch_meta (buf);
if (!batch_meta) {
// No batch meta attached.
return GST_PAD_PROBE_OK;
}

//batch_meta : NvDsBatchMeta https://docs.nvidia.com/metropolis/deepstream/sdk-api/struct__NvDsBatchMeta.html
//
//l_frame : NvDsFrameMetaList, 本質是GList http://irtfweb.ifa.hawaii.edu/SoftwareDocs/gtk20/glib/glib-doubly-linked-lists.html#GList
// struct GList
// {
// gpointer data;
// GList *next;
// GList *prev;
// };

for (l_frame = batch_meta->frame_meta_list; l_frame; l_frame = l_frame->next) {
frame_meta = (NvDsFrameMeta *) l_frame->data;

if (frame_meta == NULL) {
// Ignore Null frame meta.
continue;
}

is_first_object = TRUE;

// frame_meta : NvDsFrameMeta https://docs.nvidia.com/metropolis/deepstream/sdk-api/struct__NvDsFrameMeta.html
// l_obj : NvDsObjectMetaList * 本質是GList
// obj_meta : NvDsObjectMeta
for (l_obj = frame_meta->obj_meta_list; l_obj; l_obj = l_obj->next) {
NvDsObjectMeta *obj_meta = (NvDsObjectMeta *) l_obj->data;

if (obj_meta == NULL) {
// Ignore Null object.
continue;
}

// obj_meta : NvDsObjectMeta
// text_params : NvOSD_TextParams 描述物件的文字
// line233 - 241應該是清掉原本的文字然後放入字定義的class名稱
txt_params = &(obj_meta->text_params);
if (txt_params->display_text)
g_free (txt_params->display_text);

txt_params->display_text = g_malloc0 (MAX_DISPLAY_LEN);

g_snprintf (txt_params->display_text, MAX_DISPLAY_LEN, "%s ",
pgie_classes_str[obj_meta->class_id]);

if (obj_meta->class_id == PGIE_CLASS_ID_VEHICLE)
vehicle_count++;
if (obj_meta->class_id == PGIE_CLASS_ID_PERSON)
person_count++;

/* Now set the offsets where the string should appear */
txt_params->x_offset = obj_meta->rect_params.left;
txt_params->y_offset = obj_meta->rect_params.top - 25;

/* Font , font-color and font-size */
txt_params->font_params.font_name = "Serif";
txt_params->font_params.font_size = 10;
txt_params->font_params.font_color.red = 1.0;
txt_params->font_params.font_color.green = 1.0;
txt_params->font_params.font_color.blue = 1.0;
txt_params->font_params.font_color.alpha = 1.0;

/* Text background color */
txt_params->set_bg_clr = 1;
txt_params->text_bg_clr.red = 0.0;
txt_params->text_bg_clr.green = 0.0;
txt_params->text_bg_clr.blue = 0.0;
txt_params->text_bg_clr.alpha = 1.0;

/*
* Ideally NVDS_EVENT_MSG_META should be attached to buffer by the
* component implementing detection / recognition logic.
* Here it demonstrates how to use / attach that meta data.
*/
if (is_first_object && !(frame_number % frame_interval)) {
/* Frequency of messages to be send will be based on use case.
* Here message is being sent for first object every frame_interval(default=30).
*/

NvDsEventMsgMeta *msg_meta =
(NvDsEventMsgMeta *) g_malloc0 (sizeof (NvDsEventMsgMeta));
msg_meta->bbox.top = obj_meta->rect_params.top;
msg_meta->bbox.left = obj_meta->rect_params.left;
msg_meta->bbox.width = obj_meta->rect_params.width;
msg_meta->bbox.height = obj_meta->rect_params.height;
msg_meta->frameId = frame_number;
msg_meta->trackingId = obj_meta->object_id;
msg_meta->confidence = obj_meta->confidence;
generate_event_msg_meta (msg_meta, obj_meta->class_id, obj_meta);

// 要增加自訂的meta data必須要先用 nvds_acquire_user_meta_from_pool (batch_meta);取得
// https://docs.nvidia.com/metropolis/deepstream/dev-guide/text/DS_plugin_metadata.html#user-custom-metadata-addition-inside-nvdsbatchmeta

NvDsUserMeta *user_event_meta =
nvds_acquire_user_meta_from_pool (batch_meta);
if (user_event_meta) {
user_event_meta->user_meta_data = (void *) msg_meta;
user_event_meta->base_meta.meta_type = NVDS_EVENT_MSG_META;
user_event_meta->base_meta.copy_func =
(NvDsMetaCopyFunc) meta_copy_func;
user_event_meta->base_meta.release_func =
(NvDsMetaReleaseFunc) meta_free_func;
nvds_add_user_meta_to_frame (frame_meta, user_event_meta);
} else {
g_print ("Error in attaching event meta to buffer\n");
}
is_first_object = FALSE;
}
}
}
g_print ("Frame Number = %d "
"Vehicle Count = %d Person Count = %d\n",
frame_number, vehicle_count, person_count);
frame_number++;

return GST_PAD_PROBE_OK;
}

NvDsObjEncUsrArgs參數的功用

  • bool isFrame : 告訴encoder要編碼整張照片還是編碼每一個偵測物件的截圖。
    • 1: Encodes the entire frame.
    • 0: Encodes object of specified resolution.
  • bool saveImg : 會直接儲存一張照片到當前資料夾
  • bool attachUsrMeta :
    • 決定是否加上NVDS_CROP_IMAGE_META metadata

Deepstream截圖,以deepstream_image_meta_test為例

注意:

根據文件nvds_obj_enc_process是一個非阻塞的函式,使用者必須呼叫nvds_obj_enc_finish()以確保所有的圖片都已經確實被處理完成。

第一步,設定要儲存照片的條件並且encode成jpg檔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/* pgie_src_pad_buffer_probe will extract metadata received on pgie src pad
* and update params for drawing rectangle, object information etc. We also
* iterate through the object list and encode the cropped objects as jpeg
* images and attach it as user meta to the respective objects.*/
static GstPadProbeReturn
pgie_src_pad_buffer_probe (GstPad * pad, GstPadProbeInfo * info, gpointer ctx)
{
GstBuffer *buf = (GstBuffer *) info->data;
GstMapInfo inmap = GST_MAP_INFO_INIT;
if (!gst_buffer_map (buf, &inmap, GST_MAP_READ)) {
GST_ERROR ("input buffer mapinfo failed");
return GST_PAD_PROBE_DROP;
}
NvBufSurface *ip_surf = (NvBufSurface *) inmap.data;
gst_buffer_unmap (buf, &inmap);

NvDsObjectMeta *obj_meta = NULL;
guint vehicle_count = 0;
guint person_count = 0;
NvDsMetaList *l_frame = NULL;
NvDsMetaList *l_obj = NULL;
NvDsBatchMeta *batch_meta = gst_buffer_get_nvds_batch_meta (buf);
for (l_frame = batch_meta->frame_meta_list; l_frame != NULL;
l_frame = l_frame->next) {
NvDsFrameMeta *frame_meta = (NvDsFrameMeta *) (l_frame->data);
/* For demonstration purposes, we will encode the first 10 frames. */
if(frame_count <= 10) {
NvDsObjEncUsrArgs frameData = { 0 };
/* Preset */
frameData.isFrame = 1;
/* To be set by user */
frameData.saveImg = save_img;
frameData.attachUsrMeta = attach_user_meta;
/* Set if Image scaling Required */
frameData.scaleImg = FALSE;
frameData.scaledWidth = 0;
frameData.scaledHeight = 0;
/* Quality */
frameData.quality = 80;
/* Main Function Call */
nvds_obj_enc_process (ctx, &frameData, ip_surf, NULL, frame_meta);
}
guint num_rects = 0;
for (l_obj = frame_meta->obj_meta_list; l_obj != NULL; l_obj = l_obj->next) {
obj_meta = (NvDsObjectMeta *) (l_obj->data);
if (obj_meta->class_id == PGIE_CLASS_ID_VEHICLE) {
vehicle_count++;
num_rects++;
}
if (obj_meta->class_id == PGIE_CLASS_ID_PERSON) {
person_count++;
num_rects++;
}
/* Conditions that user needs to set to encode the detected objects of
* interest. Here, by default all the detected objects are encoded.
* For demonstration, we will encode the first object in the frame. */
if ((obj_meta->class_id == PGIE_CLASS_ID_PERSON
|| obj_meta->class_id == PGIE_CLASS_ID_VEHICLE)
&& num_rects == 1) {
NvDsObjEncUsrArgs objData = { 0 };
/* To be set by user */
objData.saveImg = save_img;
objData.attachUsrMeta = attach_user_meta;
/* Set if Image scaling Required */
objData.scaleImg = FALSE;
objData.scaledWidth = 0;
objData.scaledHeight = 0;
/* Preset */
objData.objNum = num_rects;
/* Quality */
objData.quality = 80;
/*Main Function Call */
nvds_obj_enc_process (ctx, &objData, ip_surf, obj_meta, frame_meta);
}
}
}
nvds_obj_enc_finish (ctx);
frame_count++;
return GST_PAD_PROBE_OK;
}

第二步,檢查usrMetaData是否的meta_type是不是NVDS_CROP_IMAGE_META

如果發現是NVDS_CROP_IMAGE_META,就儲存照片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/* osd_sink_pad_buffer_probe will extract metadata received on OSD sink pad
* and update params for drawing rectangle, object information. We also iterate
* through the user meta of type "NVDS_CROP_IMAGE_META" to find image crop meta
* and demonstrate how to access it.*/
static GstPadProbeReturn
osd_sink_pad_buffer_probe (GstPad * pad, GstPadProbeInfo * info,
gpointer u_data)
{
GstBuffer *buf = (GstBuffer *) info->data;

guint num_rects = 0;
NvDsObjectMeta *obj_meta = NULL;
guint vehicle_count = 0;
guint person_count = 0;
NvDsMetaList *l_frame = NULL;
NvDsMetaList *l_obj = NULL;
NvDsDisplayMeta *display_meta = NULL;
NvDsBatchMeta *batch_meta = gst_buffer_get_nvds_batch_meta (buf);
g_print ("Running osd_sink_pad_buffer_probe...\n");
for (l_frame = batch_meta->frame_meta_list; l_frame != NULL;
l_frame = l_frame->next) {
NvDsFrameMeta *frame_meta = (NvDsFrameMeta *) (l_frame->data);
int offset = 0;
/* To verify encoded metadata of cropped frames, we iterate through the
* user metadata of each frame and if a metadata of the type
* 'NVDS_CROP_IMAGE_META' is found then we write that to a file as
* implemented below.
*/
char fileFrameNameString[FILE_NAME_SIZE];
const char *osd_string = "OSD";

/* For Demonstration Purposes we are writing metadata to jpeg images of
* the first 10 frames only.
* The files generated have an 'OSD' prefix. */
if (frame_number < 11) {
NvDsUserMetaList *usrMetaList = frame_meta->frame_user_meta_list;
FILE *file;
int stream_num = 0;
while (usrMetaList != NULL) {
NvDsUserMeta *usrMetaData = (NvDsUserMeta *) usrMetaList->data;
if (usrMetaData->base_meta.meta_type == NVDS_CROP_IMAGE_META) {
snprintf (fileFrameNameString, FILE_NAME_SIZE, "%s_frame_%d_%d.jpg",
osd_string, frame_number, stream_num++);
NvDsObjEncOutParams *enc_jpeg_image =
(NvDsObjEncOutParams *) usrMetaData->user_meta_data;
/* Write to File */
file = fopen (fileFrameNameString, "wb");
fwrite (enc_jpeg_image->outBuffer, sizeof (uint8_t),
enc_jpeg_image->outLen, file);
fclose (file);
}
usrMetaList = usrMetaList->next;
}
}
for (l_obj = frame_meta->obj_meta_list; l_obj != NULL; l_obj = l_obj->next) {
obj_meta = (NvDsObjectMeta *) (l_obj->data);
if (obj_meta->class_id == PGIE_CLASS_ID_VEHICLE) {
vehicle_count++;
num_rects++;
}
if (obj_meta->class_id == PGIE_CLASS_ID_PERSON) {
person_count++;
num_rects++;
}
/* To verify encoded metadata of cropped objects, we iterate through the
* user metadata of each object and if a metadata of the type
* 'NVDS_CROP_IMAGE_META' is found then we write that to a file as
* implemented below.
*/
char fileObjNameString[FILE_NAME_SIZE];

/* For Demonstration Purposes we are writing metadata to jpeg images of
* vehicles or persons for the first 100 frames only.
* The files generated have a 'OSD' prefix. */
if (frame_number < 100 && (obj_meta->class_id == PGIE_CLASS_ID_PERSON
|| obj_meta->class_id == PGIE_CLASS_ID_VEHICLE)) {
NvDsUserMetaList *usrMetaList = obj_meta->obj_user_meta_list;
FILE *file;
while (usrMetaList != NULL) {
NvDsUserMeta *usrMetaData = (NvDsUserMeta *) usrMetaList->data;
if (usrMetaData->base_meta.meta_type == NVDS_CROP_IMAGE_META) {
NvDsObjEncOutParams *enc_jpeg_image =
(NvDsObjEncOutParams *) usrMetaData->user_meta_data;

snprintf (fileObjNameString, FILE_NAME_SIZE, "%s_%d_%d_%d_%s.jpg",
osd_string, frame_number, frame_meta->batch_id, num_rects,
obj_meta->obj_label);
/* Write to File */
file = fopen (fileObjNameString, "wb");
fwrite (enc_jpeg_image->outBuffer, sizeof (uint8_t),
enc_jpeg_image->outLen, file);
fclose (file);
usrMetaList = NULL;
} else {
usrMetaList = usrMetaList->next;
}
}
}
}
display_meta = nvds_acquire_display_meta_from_pool (batch_meta);
NvOSD_TextParams *txt_params = &display_meta->text_params[0];
txt_params->display_text = g_malloc0 (MAX_DISPLAY_LEN);
offset =
snprintf (txt_params->display_text, MAX_DISPLAY_LEN, "Person = %d ",
person_count);
offset =
snprintf (txt_params->display_text + offset, MAX_DISPLAY_LEN,
"Vehicle = %d ", vehicle_count);

/* Now set the offsets where the string should appear */
txt_params->x_offset = 10;
txt_params->y_offset = 12;

/* Font , font-color and font-size */
txt_params->font_params.font_name = "Serif";
txt_params->font_params.font_size = 10;
txt_params->font_params.font_color.red = 1.0;
txt_params->font_params.font_color.green = 1.0;
txt_params->font_params.font_color.blue = 1.0;
txt_params->font_params.font_color.alpha = 1.0;

/* Text background color */
txt_params->set_bg_clr = 1;
txt_params->text_bg_clr.red = 0.0;
txt_params->text_bg_clr.green = 0.0;
txt_params->text_bg_clr.blue = 0.0;
txt_params->text_bg_clr.alpha = 1.0;

nvds_add_display_meta_to_frame (frame_meta, display_meta);
}
g_print ("Frame Number = %d Number of objects = %d "
"Vehicle Count = %d Person Count = %d\n",
frame_number, num_rects, vehicle_count, person_count);
frame_number++;
return GST_PAD_PROBE_OK;
}

加入自己客製的的metadata

參考deepstream-user-metadata-test範例的nvinfer_src_pad_buffer_probe

  1. 有四個東西需要使用者自行提供

    1. user_meta_data : pointer to User specific meta data
    2. meta_type : Metadata type that user sets to identify its metadata
    3. copy_func : Metadata copy or transform function to be provided when there is buffer transformation
    4. release_func : Metadata release function to be provided when it is no longer required.
  2. 這個範例添加一個亂數到metadata上面,以下是要達成這個目標要準備的函式

    1. user_meta_data

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      void *set_metadata_ptr()
      {
      int i = 0;
      gchar *user_metadata = (gchar*)g_malloc0(USER_ARRAY_SIZE);

      g_print("\n**************** Setting user metadata array of 16 on nvinfer src pad\n");
      for(i = 0; i < USER_ARRAY_SIZE; i++) {
      user_metadata[i] = rand() % 255;
      g_print("user_meta_data [%d] = %d\n", i, user_metadata[i]);
      }
      return (void *)user_metadata;
      }
    2. meta_type

記得要在在probe function裡面定義變數

1
2
3
/** set the user metadata type */
#define NVDS_USER_FRAME_META_EXAMPLE (nvds_get_user_meta_type("NVIDIA.NVINFER.USER_META"))
NvDsMetaType user_meta_type = NVDS_USER_FRAME_META_EXAMPLE;
  1. copy_func
1
2
3
4
5
6
7
8
9
/* copy function set by user. "data" holds a pointer to NvDsUserMeta*/
static gpointer copy_user_meta(gpointer data, gpointer user_data)
{
NvDsUserMeta *user_meta = (NvDsUserMeta *)data;
gchar *src_user_metadata = (gchar*)user_meta->user_meta_data;
gchar *dst_user_metadata = (gchar*)g_malloc0(USER_ARRAY_SIZE);
memcpy(dst_user_metadata, src_user_metadata, USER_ARRAY_SIZE);
return (gpointer)dst_user_metadata;
}
  1. release_func
1
2
3
4
5
6
7
8
9
10
/* release function set by user. "data" holds a pointer to NvDsUserMeta*/
static void release_user_meta(gpointer data, gpointer user_data)
{
NvDsUserMeta *user_meta = (NvDsUserMeta *) data;
if(user_meta->user_meta_data) {
g_free(user_meta->user_meta_data);
user_meta->user_meta_data = NULL;
}
}

  1. 新增一個probe把資料放入metadata
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    /* Set nvds user metadata at frame level. User need to set 4 parameters after
    * acquring user meta from pool using nvds_acquire_user_meta_from_pool().
    *
    * Below parameters are required to be set.
    * 1. user_meta_data : pointer to User specific meta data
    * 2. meta_type: Metadata type that user sets to identify its metadata
    * 3. copy_func: Metadata copy or transform function to be provided when there
    * is buffer transformation
    * 4. release_func: Metadata release function to be provided when it is no
    * longer required.
    *
    * osd_sink_pad_buffer_probe will extract metadata received on OSD sink pad
    * and update params for drawing rectangle, object information etc. */

    static GstPadProbeReturn
    nvinfer_src_pad_buffer_probe (GstPad * pad, GstPadProbeInfo * info,
    gpointer u_data)
    {
    GstBuffer *buf = (GstBuffer *) info->data;
    NvDsMetaList * l_frame = NULL;
    NvDsUserMeta *user_meta = NULL;
    NvDsMetaType user_meta_type = NVDS_USER_FRAME_META_EXAMPLE;

    NvDsBatchMeta *batch_meta = gst_buffer_get_nvds_batch_meta (buf);

    for (l_frame = batch_meta->frame_meta_list; l_frame != NULL;
    l_frame = l_frame->next) {
    NvDsFrameMeta *frame_meta = (NvDsFrameMeta *) (l_frame->data);

    /* Acquire NvDsUserMeta user meta from pool */
    user_meta = nvds_acquire_user_meta_from_pool(batch_meta);

    /* Set NvDsUserMeta below */
    user_meta->user_meta_data = (void *)set_metadata_ptr();
    user_meta->base_meta.meta_type = user_meta_type;
    user_meta->base_meta.copy_func = (NvDsMetaCopyFunc)copy_user_meta;
    user_meta->base_meta.release_func = (NvDsMetaReleaseFunc)release_user_meta;

    /* We want to add NvDsUserMeta to frame level */
    nvds_add_user_meta_to_frame(frame_meta, user_meta);
    }
    return GST_PAD_PROBE_OK;
    }

將客製化訊息傳換json以之後發送訊息

nvmsgconv的功能:
利用NVDS_EVENT_MSG_METAmetadata來產生JSON格式的”DeepStream Schema” payload。
所產生的payload會以NVDS_META_PAYLOAD的型態儲存到buffer。
除了NvDsEventMsgMeta定義的常用訊號結構,使用者還可以自訂客製化物件並加到NVDS_EVENT_MSG_METAmetadata。要加入自定義訊息NvDsEventMsgMeta提供”extMsg”和”extMsgSize”欄位。使用者可以把自定義的structure指針assign給”extMsg”,並且在”extMsgSize”指令資料大小。

以deepstream-test4為例,在這裡message放入了客製化訊息NvDsVehicleObject和NvDsPersonObject,如果想要客製化自己的訊息就必須要自己定義。

自製自己的客製化訊息
可以參考
/opt/nvidia/deepstream/deepstream-6.2/sources/libs/nvmsgconv/deepstream_schema/eventmsg_payload.cpp參考客製化訊息如何定義轉換成json

nvmsgconv的原始碼
/opt/nvidia/deepstream/deepstream-6.2/sources/gst-plugins/gst-nvmsgconv
/opt/nvidia/deepstream/deepstream-6.2/sources/libs/nvmsgconv

  • nvmsgconv開啟除錯功能
    debug-payload-dir : Absolute path of the directory to dump payloads for debugging
  1. 以deepstream-test4為例,首先將模型的偵測結果NvDsObjectMeta轉換成NvDsEventMsgMeta,在這步將訊息struct加到extMsg
  2. 將製作好的NvDsEventMsgMeta加進buffer裡面,metadata為NvDsUserMeta,在這一步也要指定meta_copy_func、meta_free_func

mvmsgbroker使用方法

以下將以rabbitmq為範例

  1. 安裝rabbitmq client
    說明文件在/opt/nvidia/deepstream/deepstream/sources/libs/amqp_protocol_adaptor的readme.md

    1
    2
    3
    4
    5
    6
    7
    git clone -b v0.8.0  --recursive https://github.com/alanxz/rabbitmq-c.git
    cd rabbitmq-c
    mkdir build && cd build
    cmake ..
    cmake --build .
    sudo cp librabbitmq/librabbitmq.so.4 /opt/nvidia/deepstream/deepstream/lib/
    sudo ldconfig
  2. 安裝rabbitmq server

1
2
3
4
5
6
7
8
9
10
#Install rabbitmq on your ubuntu system: https://www.rabbitmq.com/install-debian.html
#The “Using rabbitmq.com APT Repository” procedure is known to work well

sudo apt-get install rabbitmq-server

#Ensure rabbitmq service has started by running (should be the case):
sudo service rabbitmq-server status

#Otherwise
sudo service rabbitmq-server start
  1. 設定連線詳細資訊
  2. 建立cfg_amqp.txt連線資訊檔(/opt/nvidia/deepstream/deepstream/sources/libs/amqp_protocol_adaptor 有範例),並且傳給nvmsgbroker。內容範例如下
1
2
3
4
5
6
7
8
9
[message-broker]
hostname = localhost
port = 5672
username = guest
password = guest
exchange = amq.topic
topic = topicname
amqp-framesize = 131072
#share-connection = 1
  • exchange: 預設的exchange是amq.topic,可以更改成其他的
  • Topic : 設定要發送的topic名稱
  • share-connection : Uncommenting this field signifies that the connection created can be shared with other components within the same process.
  1. 直接將連線資訊傳給msgapi_connect_ptr
1
conn_handle = msgapi_connect_ptr((char *)"url;port;username;password",(nvds_msgapi_connect_cb_t) sample_msgapi_connect_cb, (char *)CFG_FILE);
  1. 測試用程式
    /opt/nvidia/deepstream/deepstream/sources/libs/amqp_protocol_adaptor有測試用的程式test_amqp_proto_async.ctest_amqp_proto_sync.c,可以用來測試連線是否成功,編譯方式如下
    1
    2
    3
    make -f Makefile.test
    ./test_amqp_proto_async
    ./test_amqp_proto_sync
    注意:
  • 你可能須要root權限才能在這個資料夾編譯程式
  • libnvds_amqp_proto.so 位於 /opt/nvidia/deepstream/deepstream-/lib/
  1. 測試和驗證發送出去的訊息
    • 建立exchange , queue,並且將他們綁定在一起
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Rabbitmq management:
It comes with a command line tool which you can use to create/configure all of your queues/exchanges/etc
https://www.rabbitmq.com/management.html

# Install rabbitmq management plugin:
sudo rabbitmq-plugins enable rabbitmq_management

# Use the default exchange amq.topic
OR create an exchange as below, the same name as the one you specify within the cfg_amqp.txt
#sudo rabbitmqadmin -u guest -p guest -V / declare exchange name=myexchange type=topic

# Create a Queue
sudo rabbitmqadmin -u guest -p guest -V / declare queue name=myqueue durable=false auto_delete=true

#Bind Queue to exchange with routhing_key specification
rabbitmqadmin -u guest -p guest -V / declare binding source=amq.topic destination=myqueue routing_key=topicname

#To check if the queues were actually created, execute:
$ sudo rabbitmqctl list_queues
Listing queues
myqueue 0
* 接收訊息
1
2
3
4
5
6
7
8
9
10
#Install the amqp-tools
sudo apt-get install amqp-tools

cat <<EOF > test_amqp_recv.sh
while read -r line; do
echo "\$line"
done
EOF

chmod +x test_amqp_recv.sh
* 執行consumer
1
amqp-consume  -q "myqueue" -r "topicname" -e "amq.topic" ./test_amqp_recv.sh

混用c 和 c++ 程式

https://hackmd.io/@rhythm/HyOxzDkmD
https://embeddedartistry.com/blog/2017/05/01/mixing-c-and-c-extern-c/

async property

某些狀況下async property 設為true會讓pipeline卡住,還需要進一步了解原因

nvmsgconv 詳細payload設定

/opt/nvidia/deepstream/deepstream/sources/libs/nvmsgconv/nvmsgconv.cpp裡面可以看到nvds_msg2p_ctx_create這個函式,是用來產出payload的函式。在nvmsgconv讀取的yaml檔裡面可以設定的group和屬性如下

sensor

  • enable : 是否啟用這個sensor
  • id : 對應NvDsEventMsgMeta的sensorId
  • type :
  • description
  • location
    • lat;lon;alt的格式
  • coordinate
    • x;y;z的格式

place

analytics

NvDsEventMsgMeta 轉換成json的詳細實作

/opt/nvidia/deepstream/deepstream-6.2/sources/libs/nvmsgconv/deepstream_schema/eventmsg_payload.cpp這個程式裡,分別有sensor, place, analytics的轉換實作

客製化nvmsgconv payload

如果要客製化payload的話,可以參考/opt/nvidia/deepstream/deepstream-6.2/sources/libs/nvmsgconv/deepstream_schema/eventmsg_payload.cpp裡面的實作,並且加入自己需要的客製化payload。首先將整個/opt/nvidia/deepstream/deepstream-6.2/sources/libs/nvmsgconv複製到其他資料夾並且準備編譯環境

編譯環境

這裡介紹在Ubuntu20.04的桌上型主機上環境的配置方法,Jetson的環境配置方法可能略有不同。

  • 下載並且編譯protobuf
    在Ubuntu20.04下使用apt-get install protobuf 只會安裝到protobuf 3.6的版本,而許多標頭檔要到3.7以上才有,而且不能超過3.19,以免某些標頭檔又遺失。如果中間有步驟做錯,只要還沒make install,建議直接刪除protobuf的資料夾,重新下載並且編譯。

首先直接從github下載protobuf原始碼

1
git clone https://github.com/protocolbuffers/protobuf.git

切換版本到v3.19.6,並且更新submodule。

1
2
3
cd protobuf
git submodule update --init --recursive
./autogen.sh

編譯並且安裝,make check過程中如果有錯誤,編譯出來的程式可能會有部分功能遺失。

1
2
3
4
5
./configure
make
make check
sudo make install
sudo ldconfig # refresh shared library cache.

編譯客製化的nvmsgconv

接下來進到nvmsgconv的資料夾,修改一下最後產出的lib檔案名稱和install的位置,然後用make指令編譯

預訓練模型

/opt/nvidia/deepstream/deepstream-6.2/samples/models/tao_pretrained_mod
els/trafficcamnet

usb相機

https://docs.nvidia.com/jetson/archives/r35.4.1/DeveloperGuide/text/SD/CameraDevelopment/CameraSoftwareDevelopmentSolution.html#applications-using-gstreamer-with-the-nvarguscamerasrc-plugin

儲存影像

https://forums.developer.nvidia.com/t/drawn-rectangle-is-not-available-on-encoded-frame/178022/7?u=jenhao

元件速度測量

https://forums.developer.nvidia.com/t/deepstream-sdk-faq/80236/12?u=jenhao

參考:
https://www.gclue.jp/2022/06/gstreamer.html

Ubuntu設定home目錄到定另一顆硬碟

在現在常見的SSD作業系統碟加上HDD資料碟的配置,下面接介紹如何手動將/home移動到HDD資料碟

格式化硬碟(完全新的硬碟才需要),這裡假設整顆硬碟不再分割磁碟

https://www.digitalocean.com/community/tutorials/how-to-partition-and-format-storage-devices-in-linux

1
2
3
4
lsblk #找出硬碟的名稱
sudo parted -a opt /dev/sda mkpart primary ext4 0% 100% #分割磁碟
sudo mkfs.ext4 -L datapartition /dev/sda1 #格式化磁碟並加加上label datapartition
#You can add a partition label with the -L flag. Select a name that will help you identify this particular drive

將資料碟mount在一個暫時的資料夾下面

1
2
sudo mkdir /mnt/tmp
sudo mount /dev/sdb1 /mnt/tmp

複製原本/home裡面的資料

1
sudo rsync -avx /home/ /mnt/tmp

建立/home的永久mount點

  • 先用以下指令查詢資料碟的UUID
    1
    sudo blkid
  • sudo nano /etc/fstab # or any other editor將下面一行寫入/etc/fstab文件最後面來設定mount點
1
UUID=<noted number from above>    /home    ext4    defaults   0  2

重開機檢查是否生效

(危險區)刪除舊的/home

以下指令會刪掉舊的/home。務必先unmount新的home以免刪錯

1
2
sudo umount /home    # unmount the new home first!
sudo rm -rf /home/* # deletes the old home

掛載另一顆硬碟

將/home掛載到另一顆硬碟

參考:
https://askubuntu.com/a/50539

https://www.tecmint.com/convert-home-directory-partition-linux/

https://help.ubuntu.com/community/DiskSpace