基於HEP(histograms of equivalent patterns【1】)框架下的特征具有良好的紋理分類效果,LBP(local binary patterns【2】)屬於HEP框架下最常用的特征,具有對亮度、旋轉等良好的不變特性。在基於分塊的視頻煙霧檢測中,常使用其作為紋理分類的特征。然而,分塊的圖像具有局部性。這篇文章主要提出使用圖像金字塔的方法讓提取的煙霧塊特征具有一定的全局屬性。它將待檢測煙霧塊構成3級的金字塔,再對金字塔每一級提取不同模式的LBP特征,構成一個直方圖序列作為特征向量,最后采用神經網絡分類。
1.LBP 與 LBPV
本文用到的LBP共三種模式:統一模式,旋轉不變模式,統一的旋轉不變模式。每一級金字塔采用一種模式。LBP詳見博客【3】
LBPV與LBP不同的是,LBP統計直方圖的時候是每一個LBP(i,j)使第k個分量加1,而LBPV是加一個VAR(方差吧),VAR計算公式:
上面gp為領域內像素值,然后再歸一化。
2.金字塔
共三級金字塔,I0,I1,I2,I0為輸入圖像,I0通過高斯低通濾波(Gaussian low pass filter, LPF),然后下采樣得到I1(采樣大小2),同樣由I1得到I2,如圖:
最后從下到上對每一級分別提取統一模式LBP,旋轉不變LBP,統一旋轉不變LBP,按照如下的順序組合成向量:
對於24x24的圖像塊,I2的大小僅為6x6,這樣會獲得稀疏的特征向量,不利於分類。因此,I1和I2通過相鄰像素增大搜索窗口。如圖:
3.實現
本人采用文章中的數據集作為訓練與測試,剪切成96x96的灰度圖,這樣大小的圖像下采樣兩次剛好24x24。提取每一張圖像特征代碼:
feature_t get_pyramid_feature(cv::Mat & img) { assert((img.dims == 2) && (img.rows == 96) && (img.cols == 96)); feature_t result, lbp0, lbp1, lbp2, lbpv0, lbpv1, lbpv2; result.resize(210); cv::Mat I0, I1, I2, L0, S0, L1; I0 = img(cv::Rect(36, 36, 24, 24)).clone(); cv::GaussianBlur(img, L0, cv::Size(3,3), 0); cv::resize(L0, S0, cv::Size(48, 48)); I1 = S0(cv::Rect(12, 12, 24, 24)).clone(); cv::GaussianBlur(S0, L1, cv::Size(3,3), 0); cv::resize(L1, I2, cv::Size(24, 24)); lbp0 = get_u_lbp_gray(I0); lbp1 = get_ri_lbp_gray(I1); lbp2 = get_riu_lbp_gray(I2); lbpv0 = get_u_lbpv_gray(I0); lbpv1 = get_ri_lbpv_gray(I1); lbpv2 = get_riu_lbpv_gray(I2); std::copy(lbp0.begin(), lbp0.end(), result.begin()); std::copy(lbpv0.begin(), lbpv0.end(), result.begin()+59); std::copy(lbp1.begin(), lbp1.end(), result.begin()+118); std::copy(lbpv1.begin(), lbpv1.end(), result.begin()+154); std::copy(lbp2.begin(), lbp2.end(), result.begin()+190); std::copy(lbpv2.begin(), lbpv2.end(), result.begin()+200); return result; }
提取特征過后,采用神經網絡訓練(采樣tiny_cnn這個輕量級的神經網絡庫):
void mlp_train(std::vector<feature_t> & train_x, std::vector<int> & train_y, std::vector<feature_t> & test_x, std::vector<int> & test_y, const char * weights_file, int iter_num = 20) { const int num_input = train_x[0].size(); const int num_hidden_units = 30; int num_units[] = { num_input, num_hidden_units, 2 }; auto nn = make_mlp<mse, gradient_descent_levenberg_marquardt, tan_h>(num_units, num_units + 3); //train mlp nn.optimizer().alpha = 0.005; boost::progress_display disp(train_x.size()); boost::timer t; // create callback auto on_enumerate_epoch = [&](){ std::cout << t.elapsed() << "s elapsed." << std::endl; tiny_cnn::result res = nn.test(test_x, test_y); std::cout << nn.optimizer().alpha << "," << res.num_success << "/" << res.num_total << std::endl; nn.optimizer().alpha *= 0.85; // decay learning rate nn.optimizer().alpha = std::max(0.00001, nn.optimizer().alpha); disp.restart(train_x.size()); t.restart(); }; auto on_enumerate_data = [&](){ ++disp; }; nn.train(train_x, train_y, 1, iter_num, on_enumerate_data, on_enumerate_epoch); nn.test(test_x, test_y).print_detail(std::cout); nn.save_weights(weights_file); }
最后得到測試的結果96%以上。
然后用於視頻的處理中,只用處理單幅圖像的情況。對於一副圖像,首先得分塊,將每一個分塊的區域保存到向量中。分塊的時候就考慮到金字塔每一級,因此3級的每一個向量對應的位置是對應的一個塊。對於邊緣塊的處理忽略圖像外的像素。分塊代碼如下:
std::vector<std::vector<cv::Rect>> make_image_blocks(const cv::Mat &in, const int iWD) { std::vector<std::vector<cv::Rect>> result; std::vector<cv::Rect> level0, level1, level2; int rows = in.rows; int cols = in.cols; int rows_level1 = rows / 2; int cols_level1 = cols / 2; int rows_level2 = rows / 4; int cols_level2 = cols / 4; int left, top, right, bottom; for(int i = 0; i <= rows - iWD; i += iWD) { for(int j = 0; j <= cols - iWD; j += iWD) { level0.push_back(cv::Rect(j, i, iWD, iWD)); //level1 left = std::max(j/2-6,0); top = std::max(i/2-6,0); right = std::min(j/2+18, cols_level1); bottom = std::min(i/2+18, rows_level1); level1.push_back(cv::Rect(left, top, right-left, bottom-top)); //level2 left = std::max(j/4-9,0); top = std::max(i/4-9,0); right = std::min(j/4+15, cols_level2); bottom = std::min(i/4+15, rows_level2); level2.push_back(cv::Rect(left, top, right-left, bottom-top)); } } result.push_back(level0); result.push_back(level1); result.push_back(level2); return result; }
然后就是對每一張圖像的處理了,首先創建3級金字塔,然后按照上面得到的分塊索引輕松得到每一塊的特征,然后predict就完事了。
template<typename NN> std::vector<int> single_image_smoke_detect(const cv::Mat & img, const std::vector<std::vector<cv::Rect>> & locate_list, const std::vector<int> smoke_block, NN & nn) { std::vector<int> result; cv::Mat I1, I2, L0, L1; cv::GaussianBlur(img, L0, cv::Size(3,3), 0); cv::resize(L0, I1, cv::Size(img.cols/2, img.rows/2)); cv::GaussianBlur(I1, L1, cv::Size(3,3), 0); cv::resize(L1, I2, cv::Size(I1.cols/2, I1.rows/2)); for(auto i : smoke_block) { cv::Mat block = img(locate_list[0][i]); auto lbp0 = get_u_lbp_gray(block); auto lbpv0 = get_u_lbpv_gray(block); block = I1(locate_list[1][i]); auto lbp1 = get_ri_lbp_gray(block); auto lbpv1 = get_ri_lbpv_gray(block); block = I2(locate_list[2][i]); auto lbp2 = get_riu_lbp_gray(block); auto lbpv2 = get_riu_lbpv_gray(block); feature_t feat; feat.resize(210,0); std::copy(lbp0.begin(), lbp0.end(), feat.begin()); std::copy(lbpv0.begin(), lbpv0.end(), feat.begin()+59); std::copy(lbp1.begin(), lbp1.end(), feat.begin()+118); std::copy(lbpv1.begin(), lbpv1.end(), feat.begin()+154); std::copy(lbp2.begin(), lbp2.end(), feat.begin()+190); std::copy(lbpv2.begin(), lbpv2.end(), feat.begin()+200); vec_t y; nn.predict(feat, &y); const int predicted = max_index(y); if(predicted == 1) result.push_back(i); } return result; }
在這之前加入運動檢測:
smoke_blocks = single_image_smoke_detect(gray_img, locate_list, smoke_blocks, nn);
然后將檢測得到的煙霧塊顯示出來:
auto merged_blocks = merge_blocks(smoke_blocks, gray_img.size(), 24); for(auto rect : merged_blocks) { cv::rectangle(img, rect, cv::Scalar(0,0,255)); } cv::imshow("smoke", img);
效果圖:
1.實驗結果分析
試了一下僅使用LBP或LBPV的情況下,單獨使用LBPV的分類效果最差,多次測試平均94%多一點,而LBP和LBP+LBPV情況差不多,平均都在96%以上,因此,不加LBPV更好一點。
2.總結
加入金字塔對每一級提取特征的方法的確比單獨采用分塊提取特征的方法稍好一點,但是對於煙霧的檢測,單幀的檢測並不能滿足於實際。這個可以作為一個novel方法用在一個框架中。
6.引用
【1】 Texture description through histograms of equivalent patterns
【2】 Multiresolution gray-scale and rotation invariant texture classification with local binary patterns
【3】 http://blog.csdn.net/zouxy09/article/details/7929531







