1. 程式人生 > >c++/python opencv實現mask Rcnn

c++/python opencv實現mask Rcnn

OpenCV中使用Mask R-CNN進行基於深度學習的物件檢測和例項分割(Python / C ++)

我覺得可以嘗試一下

 

幾個星期前,我們用YOLOv3寫了一篇關於物體檢測的文章

物件檢測器的輸出是在影象或視訊幀中檢測到的物件周圍的邊界框陣列,但我們沒有得到關於邊界框內物件形狀的任何線索。

如果我們能找到一個包含物件的二進位制掩碼而不僅僅是邊界框,那會不會很酷?

在這篇文章中,我們將學習如何做到這一點。我們將展示如何使用稱為Mask-RCNN(基於區域的卷積神經網路)的卷積神經網路(CNN)模型進行物件檢測和分割。使用Mask-RCNN我們不僅檢測物件,還獲得包含該物件的灰度或二進位制掩碼。

本教程中的結果是使用Mac OS 2.5 GHz Intel Core i7 CPU獲得的。CPU 的推理時間為每幀350 ms到2秒,具體取決於幀中物件的複雜程度和數量。

Mask-RCNN最初於2017年11月由Facebook的AI研究團隊使用Python和Caffe2推出

它後來被移植到Tensorflow,並且在物體檢測模型動物園中共享了幾個預先訓練的模型,這些模型具有不同的骨幹架構,如InceptionV2,ResNet50,ResNet101和Inception-ResnetV2 。它們還為您提供培訓自己模型的工具。

成立之初骨幹是最快的四個。您甚至可以在合理的時間內在CPU

上進行嘗試,因此我們在本教程中選擇了它。該模型在MSCOCO資料集上進行了訓練。

我們將共享OpenCV程式碼以在C ++和Python中載入和使用該模型。

OpenCV的最低要求版本是3.4.3。

在我們深入研究程式碼之前,有必要了解一些術語,特別是如果您是初學者。

什麼是影象分割?

在計算機視覺中,術語“影象分割”或簡稱“分割”意味著基於某些標準將影象分成畫素組。您可以根據顏色,紋理或您已決定的其他一些條件進行此分組。這些組有時也稱為超畫素

什麼是例項細分?

例項分割中,目標是檢測影象中的特定物件並在感興趣的物件周圍建立掩模。例項分割也可以被認為是物件檢測,其中輸出是掩碼而不僅僅是邊界框。與試圖對影象中的每個畫素進行分類的語義分割

不同,例項分割不旨在標記影象中的每個畫素。

下面我們看一個非常相似的彩色背景上的兩隻綿羊的例項分割的例子。


圖1:例項分段示例

Mask-RCNN如何工作?

Mask-RCNN是對原始R-CNN論文(R.Girshick等人,CVPR 2014)的一系列改進的結果,用於物體檢測。R-CNN基於選擇性搜尋生成區域提議,然後使用卷積網路一次一個地處理每個提議的區域以輸出物件標籤及其邊界框。

快速R-CNN(R。Girshik,ICCV 2015)通過使用ROIPool層在其CNN中一起處理所有提出的區域使得R-CNN演算法更快。

更快的R-CNN(S. Run等人,PAMI,2017)通過使用稱為區域提議網路(RPN)的ConvNet執行區域提議步驟進一步推動了它。RPN和分類和邊界框預測網路都在共同的特徵圖上工作,從而使推理更快。在GPU上,更快的R-CNN可以以5 fps執行。

Mask R-CNN(He et al。,ICCV 2017)是對Faster RCNN的改進,它包括一個掩碼預測與類標籤和邊界框預測分支平行的分支,如下圖所示。它只為較快的R-CNN網路增加了一小部分開銷,因此仍然可以在GPU上以5 fps執行。在本教程中,我們通過在Mac OS 2.5 GHz Intel Core i7 CPU上執行來顯示結果,並且在CPU上每幀大約需要2秒,即使對於具有超過30個物件的幀也是如此。

掩蓋RCNN框架
圖2:Mask-RCNN的架構(來源

Mask-RCNN網路有兩個主要部分。

第一個是區域提案網路,每個影象生成大約300個區域提案。在訓練期間,這些提議(ROI)中的每一個都通過第二部分,即物件檢測和掩模預測網路,如上所示。注意,由於掩模預測分支與標籤和框預測分支並行執行,因此對於每個給定的ROI,網路預測屬於所有類的掩模。

在推斷期間,區域提議經歷非最大抑制,並且掩模預測分支僅處理最高得分100檢測框。因此,對於100個ROI和90個物件類,網路的掩模預測部分輸出尺寸為100x90x15x15的4D張量,其中每個掩模的大小為15×15。

對於上面顯示的綿羊影象,網路檢測到兩個物件。對於每個物件,它輸出一個數組,該陣列包含預測的類分數(表示物件屬於預測類的概率),幀中檢測到的物件的邊界框的左,上,右和下位置。此陣列中的類id用於從掩碼預測分支的輸出中提取相應的掩碼。檢測到的兩個物件的掩碼如下所示:

羊面具

圖3:Mask-RCNN生成的掩模

然後可以對這些掩模進行閾值處理以獲得完全二元掩模。

與Faster-RCNN一樣,骨幹架構的選擇也很靈活。我們之所以選擇InceptionV2是因為速度更快,但是如ResneXt-101這樣的更好的架構可以獲得更好的結果,正如Mask R-CNN論文的作者指出的那樣。

與其他物體探測器(如YOLOv3)相比,Mask-RCNN的網路在更大的影象上執行。網路調整輸入影象的大小,使得較小的邊是800畫素。下面我們將詳細介紹獲取例項分段結果所需的步驟。為了簡化和清晰視覺化,我們使用相同的顏色來指示上述視訊中同一類的物件,但我們還顯示了一個次要的程式碼更改,以不同的方式為不同的例項著色。

OpenCV中使用Mask-RCNN進行物件檢測和例項分割(C ++ / Python)

現在讓我們看看如何使用OpenCV執行Mask-RCNN。

下載程式碼
要輕鬆學習本教程,請單擊下面的按鈕下載程式碼。免費!

 

下載程式碼

第1步:下載模型

我們將首先將tensorflow模型下載到當前的Mask-RCNN工作目錄。下載完成後,我們提取模型檔案。我們將使用凍結圖形檔案frozen_inference_graph.pb來獲取模型權重。

1

2

wget http://download.tensorflow.org/models/object_detection/mask_rcnn_inception_v2_coco_2018_01_28.tar.gz

tar zxvf mask_rcnn_inception_v2_coco_2018_01_28.tar.gz

第2步:初始化引數

Mask-RCNN演算法將預測的檢測輸出生成為邊界框。每個邊界框與置信度分數相關聯。置信度閾值引數以下的所有框都將被忽略以進行進一步處理。

從網路輸出的物件掩碼是灰度影象。如果需要,可以直接將其用於alpha混合目的。由於我們在本教程中使用二進位制掩碼,因此我們使用maskThreshold引數來閾值灰色掩碼影象。降低其值將導致更大的掩模。有時這有助於包括在邊界附近錯過的部分,但同時,它也可能包括更尖的邊界區域的背景畫素。

蟒蛇

1

2

3

# Initialize the parameters

confThreshold = 0.5  #Confidence threshold

maskThreshold = 0.3  # Mask threshold

C ++

1

2

3

// Initialize the parameters

float confThreshold = 0.5; // Confidence threshold

float maskThreshold = 0.3; // Mask threshold

第3步:載入模型和類

檔案mscoco_labels.names包含訓練模型的所有物件。我們讀了班級名字。然後我們讀取並載入colors.txt檔案,其中包含用於遮蔽各種類物件的所有顏色。

接下來,我們使用這兩個檔案載入網路 -

  1. frozen_inference_graph.pb:預先訓練的權重。
  2. mask_rcnn_inception_v2_coco_2018_01_28.pbtxt:由OpenCV的DNN支援組調整的文字圖形檔案,以便可以使用OpenCV載入網路。

我們在這裡將DNN後端設定為OpenCV,將目標設定為CPU。您可以嘗試將首選目標設定為cv.dnn.DNN_TARGET_OPENCL以在GPU上執行它。但請記住,當前OpenCV版本中的DNN模組僅使用英特爾的GPU進行測試。

蟒蛇

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

# Load names of classes

classesFile = "mscoco_labels.names";

classes = None

with open(classesFile, 'rt') as f:

   classes = f.read().rstrip('\n').split('\n')

 

# Load the colors

colorsFile = "colors.txt";

with open(colorsFile, 'rt') as f:

    colorsStr = f.read().rstrip('\n').split('\n')

colors = []

for i in range(len(colorsStr)):

    rgb = colorsStr[i].split(' ')

    color = np.array([float(rgb[0]), float(rgb[1]), float(rgb[2])])

    colors.append(color)

 

# Give the textGraph and weight files for the model

textGraph = "./mask_rcnn_inception_v2_coco_2018_01_28.pbtxt";

modelWeights = "./mask_rcnn_inception_v2_coco_2018_01_28/frozen_inference_graph.pb";

 

# Load the network

net = cv.dnn.readNetFromTensorflow(modelWeights, textGraph);

net.setPreferableBackend(cv.dnn.DNN_BACKEND_OPENCV)

net.setPreferableTarget(cv.dnn.DNN_TARGET_CPU)

C ++

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

// Load names of classes

string classesFile = "mscoco_labels.names";

ifstream ifs(classesFile.c_str());

string line;

while (getline(ifs, line)) classes.push_back(line);

 

// Load the colors

vector<Scalar> colors;

string colorsFile = "colors.txt";

ifstream colorFptr(colorsFile.c_str());

while (getline(colorFptr, line)) {

    char* pEnd;

    double r, g, b;

    r = strtod (line.c_str(), &pEnd);

    g = strtod (pEnd, NULL);

    b = strtod (pEnd, NULL);

    colors.push_back(Scalar(r, g, b, 255.0));

}

 

// Give the configuration and weight files for the model

String textGraph = "./mask_rcnn_inception_v2_coco_2018_01_28.pbtxt";

String modelWeights = "./mask_rcnn_inception_v2_coco_2018_01_28/frozen_inference_graph.pb";

 

// Load the network

Net net = readNetFromTensorflow(modelWeights, textGraph);

net.setPreferableBackend(DNN_BACKEND_OPENCV);

net.setPreferableTarget(DNN_TARGET_CPU);

第4步:閱讀輸入

在此步驟中,我們將讀取影象,視訊流或網路攝像頭。此外,我們還開啟視訊編寫器以儲存具有檢測到的輸出邊界框的幀。

蟒蛇

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

outputFile = "mask_rcnn_out_py.avi"

if (args.image):

    # Open the image file

    if not os.path.isfile(args.image):

        print("Input image file ", args.image, " doesn't exist")

        sys.exit(1)

    cap = cv.VideoCapture(args.image)

    outputFile = args.image[:-4]+'_mask_rcnn_out_py.jpg'

elif (args.video):

    # Open the video file

    if not os.path.isfile(args.video):

        print("Input video file ", args.video, " doesn't exist")

        sys.exit(1)

    cap = cv.VideoCapture(args.video)

    outputFile = args.video[:-4]+'_mask_rcnn_out_py.avi'

else:

    # Webcam input

    cap = cv.VideoCapture(0)

 

# Get the video writer initialized to save the output video

if (not args.image):

    vid_writer = cv.VideoWriter(outputFile, cv.VideoWriter_fourcc('M','J','P','G'), 28, (round(cap.get(cv.CAP_PROP_FRAME_WIDTH)),round(cap.get(cv.CAP_PROP_FRAME_HEIGHT))))

C ++

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

outputFile = "mask_rcnn_out_cpp.avi";

if (parser.has("image"))

{

    // Open the image file

    str = parser.get<String>("image");

    ifstream ifile(str);

    if (!ifile) throw("error");

    cap.open(str);

    str.replace(str.end()-4, str.end(), "_mask_rcnn_out.jpg");

    outputFile = str;

}

else if (parser.has("video"))

{

    // Open the video file

    str = parser.get<String>("video");

    ifstream ifile(str);

    if (!ifile) throw("error");

    cap.open(str);

    str.replace(str.end()-4, str.end(), "_mask_rcnn_out.avi");

    outputFile = str;

}

// Open the webcam

else cap.open(parser.get<int>("device"));

 

// Get the video writer initialized to save the output video

if (!parser.has("image")) {

   video.open(outputFile, VideoWriter::fourcc('M','J','P','G'), 28, Size(cap.get(CAP_PROP_FRAME_WIDTH),          cap.get(CAP_PROP_FRAME_HEIGHT)));

}

第4步:處理每個幀

神經網路的輸入影象需要採用稱為blob的特定格式。

從輸入影象或視訊流中讀取幀後,將通過blobFromImage函式將其轉換為神經網路的輸入blob。在此過程中,它以原始大小接收輸入影象幀,並將swapRGB引數設定為true。

然後將blob作為其輸入傳遞到網路,並執行前向傳遞以獲得預測的邊界框列表以及來自網路中名為“ detection_out_final ”和“ detection_masks ” 的輸出層的物件掩碼。這些框經過後處理步驟,以濾除具有低置信度分數的那些。我們將在下一節中更詳細地介紹後處理步驟。每個幀的推理時間列印在左上角。然後將具有最終邊界框和相應的重疊掩模的影象儲存到磁碟,作為影象輸入的影象或使用視訊編寫器用於輸入視訊流或網路攝像頭。

蟒蛇

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

三十

31

32

33

34

35

36

while cv.waitKey(1) < 0:

     

    # Get frame from the video

    hasFrame, frame = cap.read()

     

    # Stop the program if reached end of video

    if not hasFrame:

        print("Done processing !!!")

        print("Output file is stored as ", outputFile)

        cv.waitKey(3000)

        break

 

    # Create a 4D blob from a frame.

    blob = cv.dnn.blobFromImage(frame, swapRB=True, crop=False)

 

    # Set the input to the network

    net.setInput(blob)

 

    # Run the forward pass to get output from the output layers

    boxes, masks = net.forward(['detection_out_final', 'detection_masks'])

 

    # Extract the bounding box and mask for each of the detected objects

    postprocess(boxes, masks)

 

    # Put efficiency information.

    t, _ = net.getPerfProfile()

    label = 'Mask-RCNN : Inference time: %.2f ms' % (t * 1000.0 / cv.getTickFrequency())

    cv.putText(frame, label, (0, 15), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0))

 

    # Write the frame with the detection boxes

    if (args.image):

        cv.imwrite(outputFile, frame.astype(np.uint8));

    else:

        vid_writer.write(frame.astype(np.uint8))

 

    cv.imshow(winName, frame)

C ++

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

三十

31

32

33

34

35

36

37

38

39

40

41

42

43

44

// Process frames.

while (waitKey(1) < 0)

{

    // get frame from the video

    cap >> frame;

     

    // Stop the program if reached end of video

    if (frame.empty()) {

        cout << "Done processing !!!" << endl;

        cout << "Output file is stored as " << outputFile << endl;

        waitKey(3000);

        break;

    }

    // Create a 4D blob from a frame.

     blobFromImage(frame, blob, 1.0, Size(frame.cols, frame.rows), Scalar(), true, false);

     

    //Sets the input to the network

    net.setInput(blob);

 

    // Runs the forward pass to get output from the output layers

    std::vector<String> outNames(2);

    outNames[0] = "detection_out_final";

    outNames[1] = "detection_masks";

    vector<Mat> outs;

    net.forward(outs, outNames);

     

    // Extract the bounding box and mask for each of the detected objects

    postprocess(frame, outs);

     

    // Put efficiency information. The function getPerfProfile returns the overall time for inference(t) and the timings for each of the layers(in layersTimes)

    vector<double> layersTimes;

    double freq = getTickFrequency() / 1000;

    double t = net.getPerfProfile(layersTimes) / freq;

    string label = format("Mask-RCNN : Inference time for a frame : %.2f ms", t);

    putText(frame, label, Point(0, 15), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 0));

     

    // Write the frame with the detection boxes

    Mat detectedFrame;

    frame.convertTo(detectedFrame, CV_8U);

    if (parser.has("image")) imwrite(outputFile, detectedFrame);

    else video.write(detectedFrame);

     

    imshow(kWinName, frame);

}

現在讓我們詳細介紹上面使用的一些後處理函式呼叫。

步驟4a:後處理網路的輸出

網路的輸出掩碼物件是一個四維物件,其中第一維表示幀中檢測到的框的數量,第二維表示模型中的類數,第三維和第四維表示掩模形狀(15× 15)在我們的例子中。

如果框的置信度小於給定閾值,則刪除邊界框並且不考慮進行進一步處理。

蟒蛇

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

三十

31

32

33

34

35

# For each frame, extract the bounding box and mask for each detected object

def postprocess(boxes, masks):

    # Output size of masks is NxCxHxW where

    # N - number of detected boxes

    # C - number of classes (excluding background)

    # HxW - segmentation shape

    numClasses = masks.shape[1]

    numDetections = boxes.shape[2]

     

    frameH = frame.shape[0]

    frameW = frame.shape[1]

     

    for i in range(numDetections):

        box = boxes[0, 0, i]

        mask = masks[i]

        score = box[2]

        if score > confThreshold:

            classId = int(box[1])

             

            # Extract the bounding box

            left = int(frameW * box[3])

            top = int(frameH * box[4])

            right = int(frameW * box[5])

            bottom = int(frameH * box[6])

             

            left = max(0, min(left, frameW - 1))

            top = max(0, min(top, frameH - 1))

            right = max(0, min(right, frameW - 1))

            bottom = max(0, min(bottom, frameH - 1))

             

            # Extract the mask for the object

            classMask = mask[classId]

             

            # Draw bounding box, colorize and show the mask on the image

            drawBox(frame, classId, score, left, top, right, bottom, classMask)

C ++

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

三十

31

32

33

34

35

36

37

38

39

40

41

// For each frame, extract the bounding box and mask for each detected object

void postprocess(Mat& frame, const vector<Mat>& outs)

{

    Mat outDetections = outs[0];

    Mat outMasks = outs[1];

     

    // Output size of masks is NxCxHxW where

    // N - number of detected boxes

    // C - number of classes (excluding background)

    // HxW - segmentation shape

    const int numDetections = outDetections.size[2];

    const int numClasses = outMasks.size[1];

     

    outDetections = outDetections.reshape(1, outDetections.total() / 7);

    for (int i = 0; i < numDetections; ++i)

    {

        float score = outDetections.at<float>(i, 2);

        if (score > confThreshold)

        {

            // Extract the bounding box

            int classId = static_cast<int>(outDetections.at<float>(i, 1));

            int left = static_cast<int>(frame.cols * outDetections.at<float>(i, 3));

            int top = static_cast<int>(frame.rows * outDetections.at<float>(i, 4));

            int right = static_cast<int>(frame.cols * outDetections.at<float>(i, 5));

            int bottom = static_cast<int>(frame.rows * outDetections.at<float>(i, 6));

             

            left = max(0, min(left, frame.cols - 1));

            top = max(0, min(top, frame.rows - 1));

            right = max(0, min(right, frame.cols - 1));

            bottom = max(0, min(bottom, frame.rows - 1));

            Rect box = Rect(left, top, right - left + 1, bottom - top + 1);

             

            // Extract the mask for the object

            Mat objectMask(outMasks.size[2], outMasks.size[3],CV_32F, outMasks.ptr<float>(i,classId));

             

            // Draw bounding box, colorize and show the mask on the image

            drawBox(frame, classId, score, box, objectMask);

             

        }

    }

}

步驟4c:繪製預測的框

最後,我們在輸入框架上繪製通過後處理步驟過濾的框,其中包含指定的類標籤和置信度分數。我們還將彩色蒙版與其輪廓疊加在框內。在此程式碼中,我們對屬於同一類的所有物件使用相同的顏色,但您也可以對不同的例項進行不同的著色。

蟒蛇

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

三十

31

32

33

# Draw the predicted bounding box, colorize and show the mask on the image

def drawBox(frame, classId, conf, left, top, right, bottom, classMask):

    # Draw a bounding box.

    cv.rectangle(frame, (left, top), (right, bottom), (255, 178, 50), 3)

     

    # Print a label of class.

    label = '%.2f' % conf

    if classes:

        assert(classId < len(classes))

        label = '%s:%s' % (classes[classId], label)

     

    # Display the label at the top of the bounding box

    labelSize, baseLine = cv.getTextSize(label, cv.FONT_HERSHEY_SIMPLEX, 0.5, 1)

    top = max(top, labelSize[1])

    cv.rectangle(frame, (left, top - round(1.5*labelSize[1])), (left + round(1.5*labelSize[0]), top + baseLine), (255, 255, 255), cv.FILLED)

    cv.putText(frame, label, (left, top), cv.FONT_HERSHEY_SIMPLEX, 0.75, (0,0,0), 1)

 

    # Resize the mask, threshold, color and apply it on the image

    classMask = cv.resize(classMask, (right - left + 1, bottom - top + 1))

    mask = (classMask > maskThreshold)

    roi = frame[top:bottom+1, left:right+1][mask]

 

    color = colors[classId%len(colors)]

    # Comment the above line and uncomment the two lines below to generate different instance colors

    #colorIndex = random.randint(0, len(colors)-1)

    #color = colors[colorIndex]

 

    frame[top:bottom+1, left:right+1][mask] = ([0.3*color[0], 0.3*color[1], 0.3*color[2]] + 0.7 * roi).astype(np.uint8)

 

    # Draw the contours on the image

    mask = mask.astype(np.uint8)

    im2, contours, hierarchy = cv.findContours(mask,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE)

    cv.drawContours(frame[top:bottom+1, left:right+1], contours, -1, color, 3, cv.LINE_8, hierarchy, 100)

C ++

1

2

3

4

6

7

8