【转载】浅谈OpenCV人脸检测以及两个函数cvHaarDetectObjects和cvRunHaarClassifierCascade
来源:互联网 发布:python cartopy 编辑:程序博客网 时间:2024/05/17 07:00
本文转自:http://2000liuzhenxing.blog.163.com/blog/static/51677475200981952828662/和http://2000liuzhenxing.blog.163.com/blog/static/5167747520098195217494/。
第一部分 cvHaarDetectObjects
OpenCV的人脸检测主要是调用训练好的cascade(Haar分类器)来进行模式匹配。
cvHaarDetectObjects,先将图像灰度化,根据传入参数判断是否进行canny边缘处理(默认不使用),再进行匹配。匹配后收集找出的匹配块,过滤噪声,计算相邻个数如果超过了规定值(传入的min_neighbors)就当成输出结果,否则删去。
匹配循环:将匹配分类器放大scale(传入值)倍,同时原图缩小scale倍,进行匹配,直到匹配分类器的大小大于原图,则返回匹配结果。匹配的时候调用cvRunHaarClassifierCascade来进行匹配,将所有结果存入CvSeq* Seq (可动态增长元素序列),将结果传给cvHaarDetectObjects。
cvRunHaarClassifierCascade函数整体是根据传入的图像和cascade来进行匹配。并且可以根据传入的cascade类型不同(树型、stump(不完整的树)或其他的),进行不同的匹配方式。
函数 cvRunHaarClassifierCascade 用于对单幅图片的检测。在函数调用前首先利用 cvSetImagesForHaarClassifierCascade设定积分图和合适的比例系数 (=> 窗口尺寸)。当分析的矩形框全部通过级联分类器每一层的时返回正值(这是一个候选目标),否则返回0或负值。
为了了解OpenCV人脸检测中寻找匹配图像的详细过程,就把cvHaarDetectObjects和cvRunHaarClassifierCascade的源文件详细看了一遍,并打上了注释。方便大家阅读。
附cvHaarDetectObjects代码:
CV_IMPL CvSeq*cvHaarDetectObjects( const CvArr* _img, CvHaarClassifierCascade* cascade, CvMemStorage* storage, double scale_factor, int min_neighbors, int flags, CvSize min_size ){ int split_stage = 2; CvMat stub, *img = (CvMat*)_img; //CvMat多通道矩阵 *img=_img指针代换传入图 CvMat *temp = 0, *sum = 0, *tilted = 0, *sqsum = 0, *norm_img = 0, *sumcanny = 0, *img_small = 0; CvSeq* seq = 0; CvSeq* seq2 = 0; //CvSeq可动态增长元素序列 CvSeq* idx_seq = 0; CvSeq* result_seq = 0; CvMemStorage* temp_storage = 0; CvAvgComp* comps = 0; int i; #ifdef _OPENMP CvSeq* seq_thread[CV_MAX_THREADS] = {0}; int max_threads = 0;#endif CV_FUNCNAME( “cvHaarDetectObjects” ); __BEGIN__; double factor; int npass = 2, coi; //npass=2 int do_canny_pruning = flags & CV_HAAR_DO_CANNY_PRUNING; //true做canny边缘处理 if( !CV_IS_HAAR_CLASSIFIER(cascade) ) CV_ERROR( !cascade ? CV_StsNullPtr : CV_StsBadArg, “Invalid classifier cascade” ); if( !storage ) CV_ERROR( CV_StsNullPtr, “Null storage pointer” ); CV_CALL( img = cvGetMat( img, &stub, &coi )); if( coi ) CV_ERROR( CV_BadCOI, “COI is not supported” ); //一些出错代码 if( CV_MAT_DEPTH(img->type) != CV_8U ) CV_ERROR( CV_StsUnsupportedFormat, “Only 8-bit images are supported” ); CV_CALL( temp = cvCreateMat( img->rows, img->cols, CV_8UC1 )); CV_CALL( sum = cvCreateMat( img->rows + 1, img->cols + 1, CV_32SC1 )); CV_CALL( sqsum = cvCreateMat( img->rows + 1, img->cols + 1, CV_64FC1 )); CV_CALL( temp_storage = cvCreateChildMemStorage( storage )); #ifdef _OPENMP max_threads = cvGetNumThreads(); for( i = 0; i < max_threads; i++ ) { CvMemStorage* temp_storage_thread; CV_CALL( temp_storage_thread = cvCreateMemStorage(0)); //CV_CALL就是运行,假如出错就报错。 CV_CALL( seq_thread[i] = cvCreateSeq( 0, sizeof(CvSeq), //CvSeq可动态增长元素序列 sizeof(CvRect), temp_storage_thread )); }#endif if( !cascade->hid_cascade ) CV_CALL( icvCreateHidHaarClassifierCascade(cascade) ); if( cascade->hid_cascade->has_tilted_features ) tilted = cvCreateMat( img->rows + 1, img->cols + 1, CV_32SC1 ); //多通道矩阵 图像长宽+1 4通道 seq = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvRect), temp_storage ); //创建序列seq 矩形 seq2 = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvAvgComp), temp_storage ); //创建序列seq2 矩形和邻近 result_seq = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvAvgComp), storage ); //创建序列result_seq 矩形和邻近 if( min_neighbors == 0 ) seq = result_seq; if( CV_MAT_CN(img->type) > 1 ) { cvCvtColor( img, temp, CV_BGR2GRAY ); //img转为灰度 img = temp; } if( flags & CV_HAAR_SCALE_IMAGE ) //flag && 匹配图 { CvSize win_size0 = cascade->orig_window_size; //CvSize win_size0为分类器的原始大小 int use_ipp = cascade->hid_cascade->ipp_stages != 0 && icvApplyHaarClassifier_32s32f_C1R_p != 0; //IPP相关函数 if( use_ipp ) CV_CALL( norm_img = cvCreateMat( img->rows, img->cols, CV_32FC1 )); //图像的矩阵化 4通道. CV_CALL( img_small = cvCreateMat( img->rows + 1, img->cols + 1, CV_8UC1 )); //小图矩阵化 单通道 长宽+1 for( factor = 1; ; factor *= scale_factor ) //成scale_factor倍数匹配 { int positive = 0; int x, y; CvSize win_size = { cvRound(win_size0.width*factor), cvRound(win_size0.height*factor) }; //winsize 分类器行列(扩大factor倍) CvSize sz = { cvRound( img->cols/factor ), cvRound( img->rows/factor ) }; //sz 图像行列(缩小factor倍) 三个Cvsize CvSize sz1 = { sz.width – win_size0.width, sz.height – win_size0.height }; //sz1 图像 减 分类器行列 CvRect rect1 = { icv_object_win_border, icv_object_win_border, win_size0.width – icv_object_win_border*2, //icv_object_win_border (int) 初始值=1 win_size0.height – icv_object_win_border*2 }; //矩形框rect1 CvMat img1, sum1, sqsum1, norm1, tilted1, mask1; //多通道矩阵 CvMat* _tilted = 0; if( sz1.width <= 0 || sz1.height <= 0 ) //图片宽或高小于分类器–>跳出 break; if( win_size.width < min_size.width || win_size.height < min_size.height ) //分类器高或宽小于给定的mini_size的高或宽–>继续 continue;//CV_8UC1见定义.//#define CV_MAKETYPE(depth,cn) ((depth) + (((cn)-1) << CV_CN_SHIFT)) //深度+(cn-1)左移3位 depth,depth+8,depth+16,depth+24. img1 = cvMat( sz.height, sz.width, CV_8UC1, img_small->data.ptr ); //小图的矩阵化 img1 单通道 sum1 = cvMat( sz.height+1, sz.width+1, CV_32SC1, sum->data.ptr ); //长宽+1 4通道8位 多通道矩阵 sqsum1 = cvMat( sz.height+1, sz.width+1, CV_64FC1, sqsum->data.ptr ); //长宽+1 4通道16位 if( tilted ) { tilted1 = cvMat( sz.height+1, sz.width+1, CV_32SC1, tilted->data.ptr ); //长宽+1 4通道8位 _tilted = &tilted1; //长宽+1 4通道8位 } norm1 = cvMat( sz1.height, sz1.width, CV_32FC1, norm_img ? norm_img->data.ptr : 0 ); //norm1 图像 减 分类器行列 4通道 mask1 = cvMat( sz1.height, sz1.width, CV_8UC1, temp->data.ptr ); //mask1 灰度图 cvResize( img, &img1, CV_INTER_LINEAR ); //img双线性插值 输出到img1 cvIntegral( &img1, &sum1, &sqsum1, _tilted ); //计算积分图像 if( use_ipp && icvRectStdDev_32s32f_C1R_p( sum1.data.i, sum1.step, sqsum1.data.db, sqsum1.step, norm1.data.fl, norm1.step, sz1, rect1 ) < 0 ) use_ipp = 0; if( use_ipp ) //如果ipp=true (intel视频处理加速等的函数库) { positive = mask1.cols*mask1.rows; //mask1长乘宽–>positive cvSet( &mask1, cvScalarAll(255) ); //mask1赋值为255 for( i = 0; i < cascade->count; i++ ) { if( icvApplyHaarClassifier_32s32f_C1R_p(sum1.data.i, sum1.step, norm1.data.fl, norm1.step, mask1.data.ptr, mask1.step, sz1, &positive, cascade->hid_cascade->stage_classifier[i].threshold, cascade->hid_cascade->ipp_stages[i]) < 0 ) { use_ipp = 0; //ipp=false; break; } if( positive <= 0 ) break; } } if( !use_ipp ) //如果ipp=false { cvSetImagesForHaarClassifierCascade( cascade, &sum1, &sqsum1, 0, 1. ); for( y = 0, positive = 0; y < sz1.height; y++ ) for( x = 0; x < sz1.width; x++ ) { mask1.data.ptr[mask1.step*y + x] = cvRunHaarClassifierCascade( cascade, cvPoint(x,y), 0 ) > 0; //匹配图像. positive += mask1.data.ptr[mask1.step*y + x]; } } if( positive > 0 ) { for( y = 0; y < sz1.height; y++ ) for( x = 0; x < sz1.width; x++ ) if( mask1.data.ptr[mask1.step*y + x] != 0 ) { CvRect obj_rect = { cvRound(y*factor), cvRound(x*factor), win_size.width, win_size.height }; cvSeqPush( seq, &obj_rect ); //将匹配块放到seq中 } } } } else //!(flag && 匹配图) { cvIntegral( img, sum, sqsum, tilted ); if( do_canny_pruning ) { sumcanny = cvCreateMat( img->rows + 1, img->cols + 1, CV_32SC1 ); //如果 做canny边缘检测 cvCanny( img, temp, 0, 50, 3 ); cvIntegral( temp, sumcanny ); } if( (unsigned)split_stage >= (unsigned)cascade->count || cascade->hid_cascade->is_tree ) { split_stage = cascade->count; npass = 1; } for( factor = 1; factor*cascade->orig_window_size.width < img->cols – 10 && //匹配 factor*cascade->orig_window_size.height < img->rows – 10; factor *= scale_factor ) { const double ystep = MAX( 2, factor ); CvSize win_size = { cvRound( cascade->orig_window_size.width * factor ), cvRound( cascade->orig_window_size.height * factor )}; CvRect equ_rect = { 0, 0, 0, 0 }; int *p0 = 0, *p1 = 0, *p2 = 0, *p3 = 0; int *pq0 = 0, *pq1 = 0, *pq2 = 0, *pq3 = 0; int pass, stage_offset = 0; int stop_height = cvRound((img->rows – win_size.height) / ystep); if( win_size.width < min_size.width || win_size.height < min_size.height ) //超边跳出 continue; cvSetImagesForHaarClassifierCascade( cascade, sum, sqsum, tilted, factor ); //匹配 cvZero( temp ); //清空temp数组 if( do_canny_pruning ) //canny边缘检测 { equ_rect.x = cvRound(win_size.width*0.15); equ_rect.y = cvRound(win_size.height*0.15); equ_rect.width = cvRound(win_size.width*0.7); equ_rect.height = cvRound(win_size.height*0.7); p0 = (int*)(sumcanny->data.ptr + equ_rect.y*sumcanny->step) + equ_rect.x; p1 = (int*)(sumcanny->data.ptr + equ_rect.y*sumcanny->step) + equ_rect.x + equ_rect.width; p2 = (int*)(sumcanny->data.ptr + (equ_rect.y + equ_rect.height)*sumcanny->step) + equ_rect.x; p3 = (int*)(sumcanny->data.ptr + (equ_rect.y + equ_rect.height)*sumcanny->step) + equ_rect.x + equ_rect.width; pq0 = (int*)(sum->data.ptr + equ_rect.y*sum->step) + equ_rect.x; pq1 = (int*)(sum->data.ptr + equ_rect.y*sum->step) + equ_rect.x + equ_rect.width; pq2 = (int*)(sum->data.ptr + (equ_rect.y + equ_rect.height)*sum->step) + equ_rect.x; pq3 = (int*)(sum->data.ptr + (equ_rect.y + equ_rect.height)*sum->step) + equ_rect.x + equ_rect.width; } cascade->hid_cascade->count = split_stage; //分裂级 for( pass = 0; pass < npass; pass++ ) {#ifdef _OPENMP #pragma omp parallel for num_threads(max_threads), schedule(dynamic)#endif for( int _iy = 0; _iy < stop_height; _iy++ ) { int iy = cvRound(_iy*ystep); int _ix, _xstep = 1; int stop_width = cvRound((img->cols – win_size.width) / ystep); uchar* mask_row = temp->data.ptr + temp->step * iy; for( _ix = 0; _ix < stop_width; _ix += _xstep ) { int ix = cvRound(_ix*ystep); // it really should be ystep if( pass == 0 ) //第一次循环 做 { int result; _xstep = 2; if( do_canny_pruning ) //canny边缘检测 { int offset; int s, sq; offset = iy*(sum->step/sizeof(p0[0])) + ix; s = p0[offset] – p1[offset] – p2[offset] + p3[offset]; sq = pq0[offset] – pq1[offset] – pq2[offset] + pq3[offset]; if( s < 100 || sq < 20 ) continue; } result = cvRunHaarClassifierCascade( cascade, cvPoint(ix,iy), 0 ); //匹配结果存到result里 if( result > 0 ) { if( pass < npass – 1 ) mask_row[ix] = 1; else { CvRect rect = cvRect(ix,iy,win_size.width,win_size.height);#ifndef _OPENMP //如果用OpenMP cvSeqPush( seq, &rect ); //result 放到seq中#else //如果不用OpenMP cvSeqPush( seq_thread[omp_get_thread_num()], &rect ); //result放到seq_thread里#endif } } if( result < 0 ) _xstep = 1; } else if( mask_row[ix] ) //不是第一次 { int result = cvRunHaarClassifierCascade( cascade, cvPoint(ix,iy), stage_offset ); if( result > 0 ) { if( pass == npass – 1 ) //如果是最后一次 { CvRect rect = cvRect(ix,iy,win_size.width,win_size.height);#ifndef _OPENMP cvSeqPush( seq, &rect );#else cvSeqPush( seq_thread[omp_get_thread_num()], &rect );#endif } } else mask_row[ix] = 0; } } } stage_offset = cascade->hid_cascade->count; cascade->hid_cascade->count = cascade->count; } } } #ifdef _OPENMP// gather the results //收集结果for( i = 0; i < max_threads; i++ ){CvSeq* s = seq_thread[i]; int j, total = s->total; CvSeqBlock* b = s->first; for( j = 0; j < total; j += b->count, b = b->next ) cvSeqPushMulti( seq, b->data, b->count ); //结果输出到seq}#endif if( min_neighbors != 0 ) { // group retrieved rectangles in order to filter out noise 收集找出的匹配块,过滤噪声 int ncomp = cvSeqPartition( seq, 0, &idx_seq, is_equal, 0 ); CV_CALL( comps = (CvAvgComp*)cvAlloc( (ncomp+1)*sizeof(comps[0]))); memset( comps, 0, (ncomp+1)*sizeof(comps[0])); // count number of neighbors 计算相邻个数 for( i = 0; i < seq->total; i++ ) { CvRect r1 = *(CvRect*)cvGetSeqElem( seq, i ); int idx = *(int*)cvGetSeqElem( idx_seq, i ); assert( (unsigned)idx < (unsigned)ncomp ); comps[idx].neighbors++; comps[idx].rect.x += r1.x; comps[idx].rect.y += r1.y; comps[idx].rect.width += r1.width; comps[idx].rect.height += r1.height; } // calculate average bounding box 计算重心 for( i = 0; i < ncomp; i++ ) { int n = comps[i].neighbors; if( n >= min_neighbors ) { CvAvgComp comp; comp.rect.x = (comps[i].rect.x*2 + n)/(2*n); comp.rect.y = (comps[i].rect.y*2 + n)/(2*n); comp.rect.width = (comps[i].rect.width*2 + n)/(2*n); comp.rect.height = (comps[i].rect.height*2 + n)/(2*n); comp.neighbors = comps[i].neighbors; cvSeqPush( seq2, &comp ); //结果输入到seq2 } } // filter out small face rectangles inside large face rectangles 在大的面块中找出小的面块 for( i = 0; i < seq2->total; i++ ) //在seq2中寻找 { CvAvgComp r1 = *(CvAvgComp*)cvGetSeqElem( seq2, i ); //r1指向结果 int j, flag = 1; for( j = 0; j < seq2->total; j++ ) { CvAvgComp r2 = *(CvAvgComp*)cvGetSeqElem( seq2, j ); int distance = cvRound( r2.rect.width * 0.2 ); if( i != j && r1.rect.x >= r2.rect.x – distance && r1.rect.y >= r2.rect.y – distance && r1.rect.x + r1.rect.width <= r2.rect.x + r2.rect.width + distance && r1.rect.y + r1.rect.height <= r2.rect.y + r2.rect.height + distance && (r2.neighbors > MAX( 3, r1.neighbors ) || r1.neighbors < 3) ) { flag = 0; break; } } if( flag ) { cvSeqPush( result_seq, &r1 ); //添加r1到返回结果. /* cvSeqPush( result_seq, &r1.rect ); */ } } } __END__;
第二部分 cvRunHaarClassifierCascade
#ifdef _OPENMPfor( i = 0; i < max_threads; i++ ){if( seq_thread[i] ) cvReleaseMemStorage( &seq_thread[i]->storage ); //如果使用了OpenMP就释放使用的seq_thread}#endif cvReleaseMemStorage( &temp_storage ); cvReleaseMat( &sum ); cvReleaseMat( &sqsum ); cvReleaseMat( &tilted ); //释放使用的空间 cvReleaseMat( &temp ); cvReleaseMat( &sumcanny ); cvReleaseMat( &norm_img ); cvReleaseMat( &img_small ); cvFree( &comps ); return result_seq; //返回结果} 下面是cvRunHaarClassifierCascade的:CV_IMPL intcvRunHaarClassifierCascade( CvHaarClassifierCascade* _cascade, CvPoint pt, int start_stage ){ int result = -1; CV_FUNCNAME(”cvRunHaarClassifierCascade”); __BEGIN__; int p_offset, pq_offset; int i, j; double mean, variance_norm_factor; CvHidHaarClassifierCascade* cascade; if( !CV_IS_HAAR_CLASSIFIER(_cascade) ) CV_ERROR( !_cascade ? CV_StsNullPtr : CV_StsBadArg, “Invalid cascade pointer” ); cascade = _cascade->hid_cascade; if( !cascade ) CV_ERROR( CV_StsNullPtr, “Hidden cascade has not been created.\n” “Use cvSetImagesForHaarClassifierCascade” ); if( pt.x < 0 || pt.y < 0 || pt.x + _cascade->real_window_size.width >= cascade->sum.width-2 || pt.y + _cascade->real_window_size.height >= cascade->sum.height-2 ) //超边退出 EXIT; p_offset = pt.y * (cascade->sum.step/sizeof(sumtype)) + pt.x; pq_offset = pt.y * (cascade->sqsum.step/sizeof(sqsumtype)) + pt.x; mean = calc_sum(*cascade,p_offset)*cascade->inv_window_area; variance_norm_factor = cascade->pq0[pq_offset] – cascade->pq1[pq_offset] - //左上+右下-右上-左下 cascade->pq2[pq_offset] + cascade->pq3[pq_offset]; variance_norm_factor = variance_norm_factor*cascade->inv_window_area – mean*mean; if( variance_norm_factor >= 0. ) variance_norm_factor = sqrt(variance_norm_factor); else variance_norm_factor = 1.; if( cascade->is_tree ) //是树形的分类器,就按照层来匹配. { CvHidHaarStageClassifier* ptr; assert( start_stage == 0 ); //start_stage==0继续 result = 1; ptr = cascade->stage_classifier; while( ptr ) { double stage_sum = 0; for( j = 0; j < ptr->count; j++ ) { stage_sum += icvEvalHidHaarClassifier( ptr->classifier + j, //层判断 variance_norm_factor, p_offset ); } if( stage_sum >= ptr->threshold ) { ptr = ptr->child; //层判断通过,到下一层. } else { while( ptr && ptr->next == NULL ) ptr = ptr->parent; //未通过,且当前子分类器没有同层分类器,没有返回上层 if( ptr == NULL ) //如果刚才已经是最顶层了. { result = 0; //返回0,退出. EXIT; } ptr = ptr->next; //指向下一个分类器. } } } else if( cascade->is_stump_based ) //如果是stump类的分类器 { for( i = start_stage; i < cascade->count; i++ ) { double stage_sum = 0; if( cascade->stage_classifier[i].two_rects ) { for( j = 0; j < cascade->stage_classifier[i].count; j++ ) { CvHidHaarClassifier* classifier = cascade->stage_classifier[i].classifier + j; CvHidHaarTreeNode* node = classifier->node; double sum, t = node->threshold*variance_norm_factor, a, b; sum = calc_sum(node->feature.rect[0],p_offset) * node->feature.rect[0].weight; sum += calc_sum(node->feature.rect[1],p_offset) * node->feature.rect[1].weight; a = classifier->alpha[0]; b = classifier->alpha[1]; stage_sum += sum < t ? a : b; } } else { for( j = 0; j < cascade->stage_classifier[i].count; j++ ) { CvHidHaarClassifier* classifier = cascade->stage_classifier[i].classifier + j; CvHidHaarTreeNode* node = classifier->node; double sum, t = node->threshold*variance_norm_factor, a, b; sum = calc_sum(node->feature.rect[0],p_offset) * node->feature.rect[0].weight; sum += calc_sum(node->feature.rect[1],p_offset) * node->feature.rect[1].weight; if( node->feature.rect[2].p0 ) sum += calc_sum(node->feature.rect[2],p_offset) * node->feature.rect[2].weight; a = classifier->alpha[0]; b = classifier->alpha[1]; stage_sum += sum < t ? a : b; } } if( stage_sum < cascade->stage_classifier[i].threshold ) { //没通过.则返回负的没通过的分类器数. result = -i; EXIT; } } } else //如果不是那两种强分类器 { for( i = start_stage; i < cascade->count; i++ ) { double stage_sum = 0; for( j = 0; j < cascade->stage_classifier[i].count; j++ ) { stage_sum += icvEvalHidHaarClassifier( cascade->stage_classifier[i].classifier + j, variance_norm_factor, p_offset ); } if( stage_sum < cascade->stage_classifier[i].threshold ) { result = -i; EXIT; } } } result = 1; __END__; return result; //返回结果}
- 【转载】浅谈OpenCV人脸检测以及两个函数cvHaarDetectObjects和cvRunHaarClassifierCascade
- OpenCV的人脸检测:cvRunHaarClassifierCascade函数解析
- opencv人脸识别--cvHaarDetectObjects函数
- 浅谈OPENCV人脸检测
- 浅谈OPENCV人脸检测
- undefined reference to `cvHaarDetectObjects'()(人脸检测)
- 转:浅谈OpenCV人脸检测
- cvHaarDetectObjects函数与人脸识别等应用
- cvHaarDetectObjects函数
- cvRunHaarClassifierCascade函数说明
- cvSmooth函数 和 OpenCV自带的人脸检测
- OpenCV:人脸检测和行人检测
- cvRunHaarClassifierCascade
- 转载【OpenCV入门指南】第十三篇 人脸检测
- 学习分析cvhaardetectobjects函数
- cvHaarDetectObjects函数参数解释
- opencv 人脸识别和行人检测
- opencv人脸检测和跟踪
- python自省函数的总结(源码剖析)
- hdfs创建文件出错
- JS判断客户端是手机还是PC
- 自定义函数-文本拆分多行
- 读DL论文心得之SPP代码运行实验
- 【转载】浅谈OpenCV人脸检测以及两个函数cvHaarDetectObjects和cvRunHaarClassifierCascade
- 多标识AR例程分析(二)
- C语言宏定义技巧(常用宏定义)
- 编译httpd 和 php
- MongoDB小结11 - update【save】
- linux下修改环境变量
- Anorm, simple SQL data access (play 2.4.x)
- android studio中常用设置及其方法详解
- sicily 1099. Packing Passengers