OpenCV dnn text_detection
本文记录opencv自带的DNN模块中其中一个范例的测试记录和学习笔记,该范例用于检测自然场景文本,功能及其强大,按要求加载训练模型以后,不需要更改任何参数,可检测多种语言自然场景中的的文本。该范例源代码路径如下https://github.com/opencv/opencv/blob/master/samples/dnn/text_detection.cpp.
测试环境
window10 + VS2017+opencv3.4.2及以上版本(V3.4.2及以上版本有该范例)
dnn text_detection.cpp运行说明
在运行之前,我们先浏览一下text_detection.cpp的源代码。如下所示,源代码起始有定义终端所需要输入的参数及其意义。这里我们需要特别注意的是参数width和height的说明,这两个参数都必须是32的倍数。
1 2 3 4 5 6 7 8 |
const char* keys = "{ help h | | Print help message. }" "{ input i | | Path to input image or video file. Skip this argument to capture frames from a camera.}" "{ model m | | Path to a binary .pb file contains trained network.}" "{ width | 320 | Preprocess input image by resizing to a specific width. It should be multiple by 32. }" "{ height | 320 | Preprocess input image by resizing to a specific height. It should be multiple by 32. }" "{ thr | 0.5 | Confidence threshold. }" "{ nms | 0.4 | Non-maximum suppression threshold. }"; |
接下来在主函数main()中有如下代码,有5个变量用于接收从终端输入的参数。结合const char* keys中的定义,期中参数confThreshold 、nmsThreshold 、inpWidth 、inpHeight 有默认值,我们可以暂时使用系统默认值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
float confThreshold = parser.get<float>("thr"); float nmsThreshold = parser.get<float>("nms"); int inpWidth = parser.get<int>("width"); int inpHeight = parser.get<int>("height"); String model = parser.get<String>("model"); if (!parser.check()) { parser.printErrors(); return 1; } CV_Assert(!model.empty()); // Load network. Net net = readNet(model); |
其中参数model是dnn需要加载的对应的已训练好的Tensorflow模型在本地的路径,因此在正式运行该代码之前, 我们需要自行下载该模型并放在范例所运行的工程路径下,该模型的下载地址https://www.dropbox.com/s/r2ingd0l3zt8hxs/frozen_east_text_detection.tar.gz?dl=1
opencv dnn模块中其他范例所需模型均可以从这个文件中找到相应模型的下载地址:
https://github.com/opencv/opencv_extra/blob/master/testdata/dnn/download_models.py
模型下载完成后,源代码可以开始试运行,按照运行提示输入图像或视频文件路径,以及待加载的.pb文件路径即可。
dnn text_detection.cpp测试记录
为方便测试,我个人喜欢将待加载模型和待测图像放在指定文件夹中,在程序中设定相应路径,程序开始运行后自动读取。因此我将范例的源代码进行了简单的修改,修改后的代码如下,其中有一些简单的注释。需要参考的同学拷贝以下代码到新建工程中,根据个人电脑中的模型与待测图像途径修改相应程序即可。
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 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
#include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp> #include <opencv2/dnn.hpp> #include <iostream> using namespace cv; using namespace cv::dnn; using namespace std; const char* keys = "{ help h | | Print help message. }" "{ input i | | Path to input image or video file. Skip this argument to capture frames from a camera.}" "{ model m | | Path to a binary .pb file contains trained network.}" "{ width | 320 | Preprocess input image by resizing to a specific width. It should be multiple by 32. }" "{ height | 320 | Preprocess input image by resizing to a specific height. It should be multiple by 32. }" "{ thr | 0.5 | Confidence threshold. }" "{ nms | 0.4 | Non-maximum suppression threshold. }"; void decode(const Mat& scores, const Mat& geometry, float scoreThresh, std::vector<RotatedRect>& detections, std::vector<float>& confidences); int main(int argc, char** argv) { float confThreshold = 0.5;//parser.get<float>("thr"); float nmsThreshold = 0.4;//parser.get<float>("nms"); //make sure the value of model is same as the path of frozen_east_text_detection.pb in local computer String model = "frozen_east_text_detection.pb"; CV_Assert(!model.empty()); TickMeter tm; tm.start(); // Load network. Net net = readNet(model); tm.stop(); double getReadNetTime = tm.getTimeMilli(); // Open a video file or an image file or a camera stream. tm.reset(); tm.start(); std::vector<Mat> outs; std::vector<String> outNames(2); outNames[0] = "feature_fusion/Conv_7/Sigmoid"; outNames[1] = "feature_fusion/concat_3"; //1. make sure that the inpWidth/inpHeight could be multiple by 32 <span style="color:#008080;"> //</span>2. scale the source image, make sure all the text could be detected. the scale size could be resize Mat frame = imread("test.png"); float w32 = (float)frame.cols / 32; float h32 = (float)frame.rows / 32; //1. make sure that the inpWidth/inpHeight could be multiple by 32 //2. scale the source image, make sure all the text could be detected. the scale size could be resize int inpWidth = ceil(w32) * 96; int inpHeight = ceil(h32) * 96; Mat blob; blobFromImage(frame, blob, 1.0, Size(inpWidth, inpHeight), Scalar(123.68, 116.78, 103.94), true, false); net.setInput(blob); net.forward(outs, outNames); Mat scores = outs[0]; Mat geometry = outs[1]; // Decode predicted bounding boxes. std::vector<RotatedRect> boxes; std::vector<float> confidences; decode(scores, geometry, confThreshold, boxes, confidences); // Apply non-maximum suppression procedure. std::vector<int> indices; NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices); tm.stop(); double getProcessTime = tm.getTimeMilli(); // Render detections. Point2f ratio((float)frame.cols / inpWidth, (float)frame.rows / inpHeight); for (size_t i = 0; i < indices.size(); ++i) { RotatedRect& box = boxes[indices[i]]; Point2f vertices[4]; box.points(vertices); for (int j = 0; j < 4; ++j) { vertices[j].x *= ratio.x; vertices[j].y *= ratio.y; } for (int j = 0; j < 4; ++j) line(frame, vertices[j], vertices[(j + 1) % 4], Scalar(0, 0, 255), 1); } cout << "ReadNet time: " << getReadNetTime << " ms; " << endl; cout << ">" << endl; cout << "image process time: " << getProcessTime << " ms; " << endl; cout << ">" << endl; // Put efficiency information. std::vector<double> layersTimes; double freq = getTickFrequency() / 1000; double t = net.getPerfProfile(layersTimes) / freq; std::string label = format("Inference time: %.2f ms", t); // putText(frame, label, Point(0, 15), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0)); imshow("frame", frame); //imwrite("7_3.png", frame); waitKey(0); return 0; } void decode(const Mat& scores, const Mat& geometry, float scoreThresh, std::vector<RotatedRect>& detections, std::vector<float>& confidences) { detections.clear(); CV_Assert(scores.dims == 4); CV_Assert(geometry.dims == 4); CV_Assert(scores.size[0] == 1); CV_Assert(geometry.size[0] == 1); CV_Assert(scores.size[1] == 1); CV_Assert(geometry.size[1] == 5); CV_Assert(scores.size[2] == geometry.size[2]); CV_Assert(scores.size[3] == geometry.size[3]); const int height = scores.size[2]; const int width = scores.size[3]; for (int y = 0; y < height; ++y) { const float* scoresData = scores.ptr<float>(0, 0, y); const float* x0_data = geometry.ptr<float>(0, 0, y); const float* x1_data = geometry.ptr<float>(0, 1, y); const float* x2_data = geometry.ptr<float>(0, 2, y); const float* x3_data = geometry.ptr<float>(0, 3, y); const float* anglesData = geometry.ptr<float>(0, 4, y); for (int x = 0; x < width; ++x) { float score = scoresData[x]; if (score < scoreThresh) continue; // Decode a prediction. // Multiple by 4 because feature maps are 4 time less than input image. float offsetX = x * 4.0f, offsetY = y * 4.0f; float angle = anglesData[x]; float cosA = std::cos(angle); float sinA = std::sin(angle); float h = x0_data[x] + x2_data[x]; float w = x1_data[x] + x3_data[x]; Point2f offset(offsetX + cosA * x1_data[x] + sinA * x2_data[x], offsetY - sinA * x1_data[x] + cosA * x2_data[x]); Point2f p1 = Point2f(-sinA * h, -cosA * h) + offset; Point2f p3 = Point2f(-cosA * w, sinA * w) + offset; RotatedRect r(0.5f * (p1 + p3), Size2f(w, h), -angle * 180.0f / (float)CV_PI); detections.push_back(r); confidences.push_back(score); } } } |
两张图片的测试结果如下图所示,检测到的文字以红色矩形框标示出来。
dnn text_detection学习笔记
1. dnn text_detection的重点函数与基本工作流程
- readNet:为DNN网络加载训练模型
- blobFromImage:对输入图像的size,depth,swapRB,mean,scalefactor做处理,保证图像满足NCHW数据格式
- setInput:为DNN网络设置输入图像
- forward:输出整个网络的运行结果
- decode:将DNN网络的运行结果解析为bounding boxes.
- NMSBoxes:对得到的检测区域和相应的评分应用非最大抑制程序,得到更合理的检测结果
1 |
blobFromImage(frame, blob, 1.0, Size(inpWidth, inpHeight), Scalar(123.68, 116.78, 103.94), true, false); |
不知道是否有读者跟我一样对blobFromImage函数中的参数Scalar(123.68, 116.78, 103.94)感到好奇?我查了一些资料,在Deep learning: How OpenCV’s blobFromImage works有原理讲解。
在这一篇专栏https://zhuanlan.zhihu.com/p/51928656有简单的说明。我并没有看懂全部内容,但在知乎专栏中说到,参数Scalar(123.68, 116.78, 103.94)是在googleNet训练的时候设定的。我想对于初级使用者而言,我们知道这个参数有固定值,一般不需要修改,与confThreshold 、nmsThreshold类似。同时这篇文章里有函数blobFromImage的简单注释,我转载过来,感谢原作者。
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 |
//申请blob Mat blobFromImage(InputArray image, double scalefactor, const Size& size, const Scalar& mean, bool swapRB, bool crop, int ddepth) { CV_TRACE_FUNCTION(); Mat blob; blobFromImage(image, blob, scalefactor, size, mean, swapRB, crop, ddepth); return blob; } //把输入图像写入vector<Mat> void blobFromImage(InputArray image, OutputArray blob, double scalefactor, const Size& size, const Scalar& mean, bool swapRB, bool crop, int ddepth) { CV_TRACE_FUNCTION(); std::vector<Mat> images(1, image.getMat()); blobFromImages(images, blob, scalefactor, size, mean, swapRB, crop, ddepth); } //核心计算函数,接收的输入是vector<Mat>,输出是Mat void blobFromImages(InputArrayOfArrays images_, OutputArray blob_, double scalefactor, Size size, const Scalar& mean_, bool swapRB, bool crop, int ddepth) { //检测输入参数的合法性 CV_TRACE_FUNCTION(); CV_CheckType(ddepth, ddepth == CV_32F || ddepth == CV_8U, "Blob depth should be CV_32F or CV_8U"); if (ddepth == CV_8U) { CV_CheckEQ(scalefactor, 1.0, "Scaling is not supported for CV_8U blob depth"); CV_Assert(mean_ == Scalar() && "Mean subtraction is not supported for CV_8U blob depth"); } std::vector<Mat> images; images_.getMatVector(images); CV_Assert(!images.empty()); //对输入图像的size,depth,swapRB,mean,scalefactor做处理 for (int i = 0; i < images.size(); i++) { //处理输入Image的尺寸 Size imgSize = images[i].size(); if (size == Size()) size = imgSize; if (size != imgSize) { if(crop) { //计算缩放系数图像宽和高的缩放系数,按照较大的缩放 float resizeFactor = std::max(size.width / (float)imgSize.width, size.height / (float)imgSize.height); //按照缩放系数做双线性插值 resize(images[i], images[i], Size(), resizeFactor, resizeFactor, INTER_LINEAR); //裁剪图像保证中心点对齐 Rect crop(Point(0.5 * (images[i].cols - size.width), 0.5 * (images[i].rows - size.height)), size); images[i] = images[i](crop); } else //如果没有设置crop,则直接进行双线性插值,缩放输入图像到目标尺寸 resize(images[i], images[i], size, 0, 0, INTER_LINEAR); } //如果输入图像数据类型与目标类型不一致,进行数据类型转换 if(images[i].depth() == CV_8U && ddepth == CV_32F) images[i].convertTo(images[i], CV_32F); Scalar mean = mean_; //交换RB if (swapRB) std::swap(mean[0], mean[2]); //处理mean和scalefactor(注意直接操作的是图像,运算符应该在Mat中重载了) images[i] -= mean; images[i] *= scalefactor; } //保证图像满足NCHW数据格式 size_t i, nimages = images.size(); Mat image0 = images[0]; int nch = image0.channels(); CV_Assert(image0.dims == 2); Mat image; if (nch == 3 || nch == 4) { ... |
另外,关于函数NMSBoxes,它有三个重载函数,主要差异是输入的文字区域可以是Rect/Rect2d/RotatedRect的格式,我们在使用过程中可以按照实际需求选择。
1 2 3 4 5 6 |
void cv::dnn::NMSBoxes (const std::vector< Rect > &bboxes, const std::vector< float > &scores, const float score_threshold, const float nms_threshold, std::vector< int > &indices, const float eta=1.f, const int top_k=0) Performs non maximum suppression given boxes and corresponding scores. More... void cv::dnn::NMSBoxes (const std::vector< Rect2d > &bboxes, const std::vector< float > &scores, const float score_threshold, const float nms_threshold, std::vector< int > &indices, const float eta=1.f, const int top_k=0) void cv::dnn::NMSBoxes (const std::vector< RotatedRect > &bboxes, const std::vector< float > &scores, const float score_threshold, const float nms_threshold, std::vector< int > &indices, const float eta=1.f, const int top_k=0) |
2. 关于参数inpWidth/inpHeight
从以上注释中,我们知道输入的图像会被缩放为inpWidth/inpHeight这两个参数设置的尺寸,然后再进行进一步的处理。关于这两个参数,我有两个疑问:1. 若inpWidth/inpHeight设定值相对于原始尺寸而言,原始图像中的文字有扭曲或者缩放,是否会影响检测结果?2. 假如第一个问题的答案是有影响,这两个参数是否可以根据图像大小动态设定呢?
我们根据第一个问题来做一个实验。如下图所示,我们为该图片命名为PVANet,它的原始大小为161*517,图中分别为按我所修改的代码放大1倍,3倍和使用320*320的测试效果。从测试结果来看,按原始尺寸放大1倍和放大3倍的检测效果基本相当,但放大3倍的效果跟好一些,相对而言,使用320*320的检测效果是最差的。,但
第二个测试图像,如下图所示,其原始尺寸为292*41,分别按原始尺寸放大1倍,3倍和使用固定尺寸320*320。从输出结果来看,使用原始尺寸按比例放大,检测效果会好一些。
由以上的实验结果可以知道,扭曲比较严重的图像会令检测结果不准确。因此我认为缩放尺寸最好是按照源图像的比例进行缩放,保证缩放后的长与宽均为32的倍数即可。这个结论并没有理论依据,只是多张图片的实验结果,欢迎探讨。
接下来我们看第二个问题,这两个参数是否可以动态修改?
这里的动态修改,指的是cv::dnn::Net net作为全局变量初始化一次,而blobFromImage每一次调用时这两个参数可以设定的值。这个问题的出发点其实与第一个疑问一致,Dnn读取模型执行完成一次初始化之后,我们可以用它来检测不同图像中的文字,那么如果我每一次输入图像的尺寸不一致,为了尽量保持源图像无变形,inpWidth/inpHeight的尺寸需要根据每张图像的尺寸的变化而变化。实验结论是这两个参数不能动态修改,我们在dnn.cpp中可以看到该函数对这两个参数的定义是const.
在学习过程中,我看到readNet函数有如下形式的重载函数,请参考https://docs.opencv.org/master/d6/d0f/group__dnn.html#ga9d118d70a1659af729d01b10233213ee
1 2 |
Net cv::dnn::readNet ( const String & framework, const std::vector< uchar > & bufferModel, const std::vector< uchar > & bufferConfig = std::vector< uchar >() ) |
在这个重载函数中,训练模型以const std::vector< uchar > 的类型存在。这是否表示我可以在初始化函数中,将.pb文件读取到const std::vector< uchar > & bufferModel变量中;在需要的时候用readNet来初始化cv::dnn::Net net。这样操作是否能确保我们在连续处理不同图像时,可以动态设置inpWidth/inpHeight的值,并且又比较节省时间呢?
针对这个问题,我查阅了很多资料,但可惜的是我暂时未找到将.pb文件读取并存储为const std::vector< uchar > 变量的方式,若后续找到的方式,会验证我的疑虑在此分享更新。
3. dnn text_detection的工作效率
该方法功能如此强大,它的检测效率如何呢?
我修改的代码中,加入了测试读取模型的时间以及图像处理的时间。针对图像PVAnet的测试时间,总结如下表所示。
由测试结果可知,读取模型的时间还是比较久的,一般而言读取模型只需要在程序初始化时做一次即可。但是假如我们需要将此功能封装为CLR被调用,为了提高程序的执行效率,此时cv::dnn::Net net就需要定义为一个全局变量,将读取模型的动作放在初始化函数中进行且仅调用一次。
但是目前opencv4.0版本中并不支持直接将cv::dnn::Net net按以下代码定义,程序会报错Linker Error LNK2022,也就是说它并不支持将cv::dnn::Net net定义为全局变量。
1 2 |
cv::dnn::Net my_net; my_net = cv::dnn::readNetFromTensorflow("frozen_east_text_detection.pb"); |
具体说明大家可以参考这里 Linker Error LNK2022 of dnn module in CLR project,从这个链接中我们也可以找到这种需求正确的解决方案:
1 2 3 4 5 6 7 8 9 10 11 |
cv::dnn::Net *net = NULL; //global variable void init(){ cv::dnn::Net tmp_net = cv::dnn::readNetFromTensorflow("frozen_east_text_detection.pb"); net = new Net(tmp_net); //memory allocation and copy constructor } void leave(){ delete net; net =NULL; } |
本文到此结束,感谢阅读,欢迎关注。