OpenCV探索之路(二十五):制作簡易的圖像標註小工具

分類:IT技術 時間:2017-09-30

搞圖像深度學習的童鞋一定碰過圖像數據標註的東西,當我們訓練網絡時需要訓練集數據,但在網上又沒有找到自己想要的數據集,這時候就考慮自己制作自己的數據集了,這時就需要對圖像進行標註。圖像標註是件很枯燥又很費人力物力的一件事情,但是又不能回避,畢竟搞深度學習如果沒有數據集那一切都是瞎搞。最近我在參加一個有關圖像深度學習的比賽,因為命題方沒有給出訓練集,所以需要隊伍自己去標註訓練集,所以我花點時間開發了一些圖像標註小工具給我的團隊使用,以減輕標註的難度,加快標註的速度。

這篇文章我將分享三個標註小工具,分別用於圖像分類、目標檢測以及語義分割的圖像標註任務。

圖像分類標註小工具

實現圖像分類的小工具太好開發了,因為它功能很簡單,無非是對一個文件夾內的所有圖片進行分類,生成每張圖片所對應的類別標簽,用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 = http://www.cnblogs.com/skyfsm/p/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 = http://www.cnblogs.com/skyfsm/p/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 = http://www.cnblogs.com/skyfsm/p/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 = http://www.cnblogs.com/skyfsm/p/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 = http://www.cnblogs.com/skyfsm/p/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圖

當然我們也可以根據需求把每個標註的每個圖形的邊緣點記錄下來

希望這三款小工具能給你帶來一點小幫助和小啟發~


Tags: 標註 圖像 工具 圖片 分類 文件夾

文章來源:


ads
ads

相關文章
ads

相關文章

ad