OpenCV探索之路(二十五):制作简易的图像标注小工具
搞图像深度学习的童鞋一定碰过图像数据标注的东西,当我们训练网络时需要训练集数据,但在网上又没有找到自己想要的数据集,这时候就考虑自己制作自己的数据集了,这时就需要对图像进行标注。图像标注是件很枯燥又很费人力物力的一件事情,但是又不能回避,毕竟搞深度学习如果没有数据集那一切都是瞎搞。最近我在参加一个有关图像深度学习的比赛,因为命题方没有给出训练集,所以需要队伍自己去标注训练集,所以我花点时间开发了一些图像标注小工具给我的团队使用,以减轻标注的难度,加快标注的速度。
这篇文章我将分享三个标注小工具,分别用于图像分类、目标检测以及语义分割的图像标注任务。
图像分类标注小工具
实现图像分类的小工具太好开发了,因为它功能很简单,无非是对一个文件夹内的所有图片进行分类,生成每张图片所对应的类别标签,用txt文件存储起来,当然也可以把每一类图片放在对应的该类的文件夹下。
我实现的这个图像分类小工具的功能就是,循环弹出一个文件夹内所有的图片,标注人员对这张图片进行分类,属于1类就按1,属于2类就按2,如此类推,按完相应号码后图片自动跳到下一张,直至文件夹内的图片都被标注完毕。
我们以下面的图库为例,将其分为3类。
首先我们需要创建相应的文件夹来存储每个类的图片
图像分类标注小工具代码:
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
#define DATA_DIR ".\\dataset\\"
#define IMG_MAX_NUM 20
using namespace cv;
using namespace std;
int main()
{
FILE* fp;
FILE* fp_result;
fp = fopen("start.txt", "r"); //读取开始的图片名字,方便从某一图片开始标注
int start_i = 0;
fscanf(fp, "%d", &start_i);
fclose(fp);
fp_result = fopen("classify_record.txt", "a+"); //用于记录每张图每个框的标注信息
printf("start_i: %d\n", start_i);
/*循环读取图片来标注*/
for (int i = start_i; i < IMG_MAX_NUM; i++)
{
stringstream ss1,ss2,ss3;
ss1 << DATA_DIR <<"data\\"<< i << ".jpg";
ss3 << i << ".jpg";
Mat src = imread(ss1.str());
if (src.empty())
{
continue;
}
printf("正在操作的图像: %s\n", string(ss1.str()).c_str());
imshow("标注", src);
char c = 0;
c = waitKey(0);
while ( c != '1' && c != '2' && c != '3')
{
c = waitKey(0);
printf("invaid input!\n");
}
ss2 << DATA_DIR << c << "\\" << i << ".jpg";
char type = c - '0';
printf("分类为: %d\n", c - '0');
imwrite(ss2.str(), src); //copy一份到对应类别的文件夹
fprintf(fp_result, "%s %d\n", string(ss3.str()).c_str(), type);
}
fclose(fp_result);
return 0;
}
利用工具进行标注
每一类图片被分到相应的文件夹内
同时也生成标签文件,每行以图片路径+对应的类别的方式呈现。
目标检测图像标注小工具
在目标检测相关的网络训练中,我们需要有带有以下标签的数据集:
我们做标注时不仅仅要把我们想要识别的物体用矩形框将其框出来,还需要记录这个框的相关信息,比如这个框的左顶点坐标、宽度高度等(x,y,w,h)。为了能实现这个标注任务,这个标注小工具必须具备框图和自动记录(x,y,w,h)信息的功能。
利用opencv我们可以快速实现用矩形框框出对应物体的功能,再加上将每个矩形框的信息有序记录在txt文件的功能,一个用于检测图像标注小工具就算开发好了。
目标检测图像标注小工具代码:
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
#define DATA_DIR ".\\cut256\\"
#define IM_ROWS 5106
#define IM_COLS 15106
#define ROI_SIZE 256
using namespace cv;
using namespace std;
Point ptL, ptR; //鼠标画出矩形框的起点和终点,矩形的左下角和右下角
Mat imageSource, imageSourceCopy;
FILE* fp_result;
struct UserData
{
Mat src;
vector<Rect> rect;
};
void OnMouse(int event, int x, int y, int flag, void *dp)
{
UserData *d = (UserData *)dp;
imageSourceCopy = imageSource.clone();
if (event == CV_EVENT_LBUTTONDOWN) //按下鼠标右键,即拖动开始
{
ptL = Point(x, y);
ptR = Point(x, y);
}
if (flag == CV_EVENT_FLAG_LBUTTON) //拖拽鼠标右键,即拖动进行
{
ptR = Point(x, y);
imageSourceCopy = imageSource.clone();
rectangle(imageSourceCopy, ptL, ptR, Scalar(0, 255, 0));
imshow("标注", imageSourceCopy);
}
if (event == CV_EVENT_LBUTTONUP) //拖动结束
{
if (ptL != ptR)
{
rectangle(imageSourceCopy, ptL, ptR, Scalar(0, 255, 0));
imshow("标注", imageSourceCopy);
int h = ptR.y - ptL.y;
int w = ptR.x - ptL.x;
printf("选择的信息区域是:x:%d y:%d w:%d h:%d\n", ptL.x, ptL.y, w, h);
d->rect.push_back(Rect(ptL.x, ptL.y, w, h));
//d->src(imageSourceCopy);
}
}
//点击右键删除一个矩形
if (event == CV_EVENT_RBUTTONDOWN)
{
if (d->rect.size() > 0)
{
Rect temp = d->rect.back();
printf("删除的信息区域是:x:%d y:%d w:%d h:%d\n", temp.x, temp.y, temp.width, temp.height);
d->rect.pop_back();
for (int i = 0; i < d->rect.size(); i++)
{
rectangle(imageSourceCopy, d->rect[i], Scalar(0, 255, 0), 1);
}
}
}
}
void DrawArea(Mat& src, string img_name, string path_name)
{
Mat img = src.clone();
char c = 'x';
UserData d;
d.src = img.clone();
while (c != 'n')
{
Mat backup = src.clone();
imageSource = img.clone();
namedWindow("标注", 1);
imshow("标注", imageSource);
setMouseCallback("标注", OnMouse, &d);
c = waitKey(0);
if (c == 'a')
{
printf("rect size: %d\n", d.rect.size());
for (int i = 0; i < d.rect.size(); i++)
{
rectangle(backup, d.rect[i], Scalar(0, 255, 0), 1);
}
img = backup.clone();
}
}
fprintf(fp_result, "%s\n", img_name.c_str());
fprintf(fp_result, "%d\n", d.rect.size());
for (int i = 0; i < d.rect.size(); i++)
{
Rect t = d.rect[i];
fprintf(fp_result, "%d %d %d %d\n", t.x, t.y, t.width, t.height);
}
imwrite(path_name, img);
}
int main()
{
FILE* fp;
fp = fopen("start.txt", "r");
int start_i = 0;
int start_j = 0;
fscanf(fp, "%d %d", &start_i, &start_j);
fclose(fp);
fp_result = fopen("record.txt", "a+");
printf("start_i: %d, start_j: %d\n", start_i, start_j);
/*循环读取图片来标注*/
for (int i = start_i; i< IM_ROWS / ROI_SIZE + 1; i++)
{
for (int j = start_j; j<IM_COLS / ROI_SIZE; j++)
{
stringstream ss1, ss2;
ss1 << DATA_DIR << "2017\\" << i << "_" << j << "_" << ROI_SIZE << "_.jpg";
ss2 << DATA_DIR << "label_img\\" << i << "_" << j << "_" << ROI_SIZE << "_.jpg";
cout << ss1.str() << endl;
string str(ss1.str());
string str2(ss2.str());
cv::Mat src = cv::imread(ss1.str());
DrawArea(src, str,str2);
}
}
fclose(fp_result);
return 0;
}
以标注建筑物为例子吧!
然后在txt文件中可以看到我们标记的矩形信息记录,第一行是图片路径+框的个数,第二行开始是每个矩形的x,y,w,h。
语义分割图像标注小工具
语义分割的标注相比上面的标注要复杂得多,所以标注工具开发起来也略难一点。
比如有这么一个任务,我们需要把图像中的建筑物给标注出来,生成一个mask图。
比如这样子
然后我们以后就可以根据这些mask图作为label来进行语义分割网络的训练了。
实现这么一个工具还是不算太复杂,主要功能的实现就在于使用了opencv的多边形的生成与填充函数。标注人员只需要在要标注的物体边缘打点,然后工具就会自动填充该区域,进而生成黑白mask图。
#include <iostream>
#include <sstream>
#include <vector>
#include <opencv2/opencv.hpp>
using namespace std;
#define DATA_DIR ".\\cut256\\"
#define IM_ROWS 5106
#define IM_COLS 15106
#define ROI_SIZE 256
struct UserData
{
cv::Mat src;
vector<cv::Point> pts;
};
FILE* fpts_set;
void on_mouse(int event, int x, int y, int flags, void *dp)
{
UserData *d = (UserData *)dp;
if (event == CV_EVENT_LBUTTONDOWN)
{
d->pts.push_back(cv::Point(x, y));
}
if (event == CV_EVENT_RBUTTONDOWN)
{
if (d->pts.size()>0)
d->pts.pop_back();
}
cv::Mat temp = d->src.clone();
if (d->pts.size()>2)
{
const cv::Point* ppt[1] = { &d->pts[0] };
int npt[] = { static_cast<int>(d->pts.size()) };
cv::fillPoly(temp, ppt, npt, 1, cv::Scalar(0, 0, 255), 16);
}
for (int i = 0; i<d->pts.size(); i++)
{
cv::circle(temp, d->pts[i], 1, cv::Scalar(0, 0, 255), 1, 16);
}
cv::circle(temp, cv::Point(x, y), 1, cv::Scalar(0, 255, 0), 1, 16);
cv::imshow("2017", temp);
}
void WriteTxT(vector<cv::Point>& pst)
{
for (int i = 0; i < pst.size(); i++)
{
fprintf(fpts_set, "%d %d", pst[i].x, pst[i].y);
if (i == pst.size() - 1)
{
fprintf(fpts_set, "\n");
}
else
{
fprintf(fpts_set, " ");
}
}
}
int label_img(cv::Mat &src, cv::Mat &mask, string& name)
{
char c = 'x';
vector<vector<cv::Point> > poly_point_set;
while (c != 'n')
{
UserData d;
d.src = src.clone();
cv::namedWindow("2017", 1);
cv::setMouseCallback("2017", on_mouse, &d);
cv::imshow("2017", src);
c = cv::waitKey(0);
if (c == 'a')
{
if (d.pts.size()>0)
{
const cv::Point* ppt[1] = { &d.pts[0] };
int npt[] = { static_cast<int>(d.pts.size()) };
cv::fillPoly(src, ppt, npt, 1, cv::Scalar(0, 0, 255), 16);
cv::fillPoly(mask, ppt, npt, 1, cv::Scalar(255), 16);
poly_point_set.push_back(d.pts);
}
}
}
fprintf(stdout, "%s %d\n", name.c_str(), poly_point_set.size());
fprintf(fpts_set, "%s %d\n", name.c_str(), poly_point_set.size());
//将点集写入文件
for (int i = 0; i < poly_point_set.size(); i++)
{
WriteTxT(poly_point_set[i]);
}
return 0;
}
int main()
{
FILE* fp;
fp = fopen("start.txt", "r");
int start_i = 0;
int start_j = 0;
fscanf(fp, "%d %d", &start_i, &start_j);
fclose(fp);
fpts_set = fopen("semantic_label.txt", "a+");
printf("start_i: %d, start_j: %d\n", start_i, start_j);
for (int i = start_i; i<IM_ROWS / ROI_SIZE + 1; i++)
{
for (int j = start_j; j<IM_COLS / ROI_SIZE; j++)
{
stringstream ss1,ss2,ss3;
cv::Mat mask(256, 256, CV_8UC1);
mask.setTo(0);
ss1 << DATA_DIR << "2017\\" << i << "_" << j << "_" << ROI_SIZE << "_.jpg";
ss2 << DATA_DIR << "label\\" << i << "_" << j << "_" << ROI_SIZE << "_.jpg";
ss3 << i << "_" << j << "_" << ROI_SIZE << "_.jpg";
cout << ss1.str() << endl;
cv::Mat src = cv::imread(ss1.str());
label_img(src, mask, string(ss3.str()));// label based on tiny
cv::imwrite(ss2.str(), mask);
}
}
fclose(fpts_set);
return 0;
}
所以我们可以利用这个标注工具对任意形状的物体进行标注,原理就是利用多边形的逼近。看看效果吧
生成的mask图
当然我们也可以根据需求把每个标注的每个图形的边缘点记录下来
希望这三款小工具能给你带来一点小帮助和小启发~
OpenCV探索之路(二十五):制作简易的图像标注小工具的更多相关文章
- OpenCV探索之路(十五):角点检测
角点检测是计算机视觉系统中用来获取图像特征的一种方法.我们都常说,这幅图像很有特点,但是一问他到底有哪些特点,或者这幅图有哪些特征可以让你一下子就识别出该物体,你可能就说不出来了.其实说图像的特征,你 ...
- Bootstrap <基础二十五>警告(Alerts)
警告(Alerts)以及 Bootstrap 所提供的用于警告的 class.警告(Alerts)向用户提供了一种定义消息样式的方式.它们为典型的用户操作提供了上下文信息反馈. 您可以为警告框添加一个 ...
- VMware vSphere 服务器虚拟化之二十五 桌面虚拟化之终端服务池
VMware vSphere 服务器虚拟化之二十五 桌面虚拟化之终端服务池 终端服务池是指由一台或多台微软终端服务器提供服务的桌面源组成的池.终端服务器桌面源可交付多个桌面.它具有以下特征: 1.终端 ...
- WCF技术剖析之二十五: 元数据(Metadata)架构体系全景展现[元数据描述篇]
原文:WCF技术剖析之二十五: 元数据(Metadata)架构体系全景展现[元数据描述篇] 在[WS标准篇]中我花了很大的篇幅介绍了WS-MEX以及与它相关的WS规范:WS-Policy.WS-Tra ...
- Bootstrap入门(二十五)JS插件2:过渡效果
Bootstrap入门(二十五)JS插件2:过渡效果 对于简单的过渡效果,只需将 transition.js 和其它 JS 文件一起引入即可.如果你使用的是编译(或压缩)版的bootstrap.js ...
- JAVA基础再回首(二十五)——Lock锁的使用、死锁问题、多线程生产者和消费者、线程池、匿名内部类使用多线程、定时器、面试题
JAVA基础再回首(二十五)--Lock锁的使用.死锁问题.多线程生产者和消费者.线程池.匿名内部类使用多线程.定时器.面试题 版权声明:转载必须注明本文转自程序猿杜鹏程的博客:http://blog ...
- JAVA之旅(二十五)——文件复制,字符流的缓冲区,BufferedWriter,BufferedReader,通过缓冲区复制文件,readLine工作原理,自定义readLine
JAVA之旅(二十五)--文件复制,字符流的缓冲区,BufferedWriter,BufferedReader,通过缓冲区复制文件,readLine工作原理,自定义readLine 我们继续IO上个篇 ...
- Java进阶(二十五)Java连接mysql数据库(底层实现)
Java进阶(二十五)Java连接mysql数据库(底层实现) 前言 很长时间没有系统的使用java做项目了.现在需要使用java完成一个实验,其中涉及到java连接数据库.让自己来写,记忆中已无从搜 ...
- 策略模式 Strategy 政策Policy 行为型 设计模式(二十五)
策略模式 Strategy 与策略相关的常见词汇有:营销策略.折扣策略.教学策略.记忆策略.学习策略.... “策略”意味着分情况讨论,而不是一概而论 面对不同年龄段的人,面对不同的商品,必然将会 ...
随机推荐
- Java温故而知新-冒泡法排序
冒泡法排序是各种初学者在学习数组与循环结构时都会练习的一种简单排序算法. 冒泡法的精髓在于比较相邻的两个元素,较大的元素会不断的排到队伍后面去,就像水里的泡泡一样不断向上跑. 想像一下倒在一个透明玻璃 ...
- Java入门(1) —— 变量、运算符、分支结构和程序员思维的理解
1.计算机语言的发展史: 机器语言:机器语言是指一台计算机全部的指令集合. 汇编语言:为了减轻使用机器语言编程的痛苦,人们进行了一种有益的改进:用一些简洁的英文字母.符号串来替代一个特定的指令的二进制 ...
- WIN7+IE8环境QTP11不能录制和识别web对象的解决方法
在项目稍微空闲的时间,在办公电脑上面装上QTP11来学习.但是发现在录制脚本时无法录制web对象,在网上找解决方法说以管理员的身份运行QTP就可以解决无法录制的问题,用这方法证明是ok的.后来用Obj ...
- jmeter性能测试 套路二
1.一般我们不会通过下面这种去跑性能测试 2.我们会通过这种方式去跑性能测试 3.录制自动化 就用新的 4.录制性能测试 就用
- Spark Submit 脚本
当我们需要命令行传递参数时候,将--class 写在前面,然后是jar 最后是参数 spark-submit --master yarn --num-executors 3 --executor-me ...
- win10下移动硬盘位置不可用无法访问
win10下移动硬盘位置不可用无法访问 网上搜索得到的答案是: 请参考以下步骤解决: 1.按Windows+R输入"CHKDSK H: /F /R"(H:是硬盘所在盘符./R 找到 ...
- 【C#多线程编程实战笔记】二、 线程同步
使用Mutex类-互斥锁 owned为true,互斥锁的初始状态就是被主线程所获取,否则处于未获取状态 name为定义的互斥锁名称,在整个操作系统只有一个命名未CSharpThreadingCookb ...
- ButterKnife的安装与使用以及ButterKnife右键不显示的大坑
作为从安卓的的入门选手,第一次看到还以为是个第三方呢,从github下来之后感觉不对啊,这么多东西,后来一搜原来是个插件,而且不用从github上下载. 安装的方法很简单. 第一步:打开安卓studi ...
- 初入PHP,(for循环~水仙花数)
找出100-999之间的所有"水仙花数".所谓水仙花数是指一个三位 数,各位数字的立方和等于该数本身.(如153次方=1的3次方+5的3次方+3的3次方)并输出这些数字 想想153 ...
- 联合线程(案例顾客买蛋糕 :使用join()方法)
运行效果图: