在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

手把手教你如何自制目標檢測框架

新機器視覺 ? 來源:Huterox ? 2024-11-14 16:39 ? 次閱讀

今天,給大家分享一篇來自知乎的一篇關于目標檢測相關的一些內容,

本文基于Pytorch進行編寫。

在閱讀這篇博文之前,如果讀者真的是還沒有接觸過這個目標檢測的話,作者建議可以先看看這幾篇文章再來:

GitHub 水項目之 快速上手 YOLOV5

(https://blog.csdn.net/FUTEROX/article/details/124079281)

YOLOV5 參數設定與模型訓練的坑點一二三

(https://blog.csdn.net/FUTEROX/article/details/124079281)

YOLOV1論文小整理

(https://blog.csdn.net/FUTEROX/article/details/124111506)

在代碼部分還參考了原先這篇博文的設計:
嘿~全流程帶你基于Pytorch手擼圖片分類“框架“--HuClassify

那么本文兩個目標:

一. 理論

搞清楚什么是目標檢測

目標檢測的重難點

相關目標檢測算法思想

如何設計一個目標檢測算法

二. 編碼

voc數據集的細節

目標檢測網絡

目標分類網絡

相關算法

其中的理論部分像我說不會太深入只是快速入門,編碼部分的話倒是有很多相關算法的實現。那么編碼的話在目標檢測部分的網絡,我們也是直接使用yolo的網絡,當然這里還是會做改動的。這篇博文的更多的一個目的其實還是說搭建一個簡單的目標檢測平臺,這樣感興趣的朋友可以自己DIY,對我本人的話也是有DIY的需求。

那么廢話不多說,馬上發車了!

目標檢測

要說到目標檢測的話,那么我們就不得不先說到圖片分類了。
因為圖片分類在我們的目標檢測當中是非常重要的但是二者的區別也是存在的,不過他們之間卻有很多相似的地方。

圖片分類

圖片分類是一個非常經典的問題,給定一張圖片然后對這個圖片進行分類,它的任務非常簡單,并且設計一個這樣的網絡也非常簡單。
你只需要使用一定量的卷積,最后和一定量的全連接網絡輸出一組大小和類別的最后一個維度一樣的tensor就行了,然后使用交叉熵作為你的損失函數。

比如最簡單的分類網絡:LeNet

e04a92e8-9078-11ef-a511-92fbcf53809c.jpg

class LeNet(nn.Module):
    def __init__(self,classes):
        super().__init__()

        self.feature = Sequential(
            nn.Conv2d(3,6,kernel_size=5),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2,stride=2),
            nn.Conv2d(6,16,5),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2,stride=2)
        )

        self.classifiar = nn.Sequential(
            nn.Linear(16*5*5,120), # B
            nn.ReLU(),
            nn.Linear(120,84),
            nn.ReLU(),
            nn.Linear(84,classes)
        )
    def forward(self,x):
        x = self.feature(x)
        x = x.view(x.size()[0],-1)
        x = self.classifiar(x)
        return x

    def initialize_weights(self):
        #參數初始化,隨便給點權重,這樣的話會加快一點速度(訓練)
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.xavier_normal_(m.weight.data)
                if m.bias is not None:
                    m.bias.data.zero_()
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight.data, 0, 0.1)
                m.bias.data.zero_()

我們只需要輸入一張圖片就闊以得到這張圖片的類別。

但是這還是遠遠不夠的。

目標檢測

目標檢測則是在圖片分類的基礎上,我們還需要知道我們對應的一個物體的位置,比如下面這張圖:

e060c27a-9078-11ef-a511-92fbcf53809c.jpg

我們知道這是一只貓,但是在圖片場景當中并不是只有一只貓,貓只是圖片當中的一個很明顯的特征,如果做圖片分類的話,你說這個是貓可以,但是我說這是個草貌似也可以。所以現在的任務是我不僅僅要知道這個圖片有貓,我還要知道這個貓在圖片的位置。

e079649c-9078-11ef-a511-92fbcf53809c.jpg

單目標檢測

現在我們假設,我們的圖片只有一個物體,就如上面的圖片一樣。那么如果我們需要想辦法讓神經網絡得到這樣一個框的,當然在此基礎上,我們還需要得到對應的概率,也就是,如果圖片只有一個目標的話,我們只需要在原來的基礎上想辦法多生成一組對應的框的坐標就可以了。也就是說,我們以上面的LeNet網絡為例子。我們可以這樣干。

e0900a8a-9078-11ef-a511-92fbcf53809c.jpg

我們只要把原來的那個直接輸出概率的那一個全連接層拆掉,然后再來幾個全連接層之后分別預測就完了。

至于損失函數,這也好辦,一個是交叉熵得到Loss1 還有一個是求方差,求對應的框的點和標注的框的誤差就完了得到Loss2 之后Loss=Loss1+Loss2

多目標檢測

然而理想是很豐滿的,但是現實很殘酷。現在的圖片當中往往都是有多個目標的,而且哪怕是同一個目標,在一張圖片當中也可能有多個,那問題不就尷尬了,比如下面的圖片:

e0aaf2fa-9078-11ef-a511-92fbcf53809c.jpg

所以我們需要解決這個問題。

問題分析

首先我們來想想,我們現在面臨的問題,首先對于一張圖片,對送進神經網絡的圖片來說(假設數據集不是我們 自己搞的)我們是不知道當前這個圖片它是有幾個目標的,所以如果是按照咱們先前那個對LeNet的改動的話,我們是壓根就不知道要生成幾個框,做幾個概率的預測的。假設我們知道了,或者說我們一股腦直接生成一堆框,那么我們需要如何篩選這些有用的框出來?并且我們怎么區別這些框對應的類別是啥?最后我們的損失函數又要怎么設計?

那么如果我們能夠找到一種方式能夠搞定上面的問題,那么多目標檢測應該就能夠實現了,換句話說能夠通用的目標檢測算法就ok了。

滑動窗口

前面分析了我們如果想要實現那個多目標檢測,我們需要解決的問題。那么第一個問題,如何生成框。回到一開始的方式,我們是直接輸入了一張圖片,然后,對這張圖片生成一個框,然后做預測等操作,那么既然如此,那么我就直接這樣,我把一張圖片直接分成一個個區域,相當于截圖一樣,一個一個區域截圖,然后分別送進神經網絡。然后你懂的,我們套用剛剛提出的方法。

e0cd9dfa-9078-11ef-a511-92fbcf53809c.jpg

也就是下面這樣

e0db7e8e-9078-11ef-a511-92fbcf53809c.jpg

我可以生成不同的滑動窗口,然后瘋狂搞。
理論上只要電腦不冒煙,我就可以一直搞。只要效率顯然….

所以還需要優化一下。

RCNN

那么這個時候,你可能會想了,剛剛的問題難度在于我們很難去得到這些框,因為做分類對我們來說還是非常簡單的事情,但是做檢測,偏偏有個預測框很難弄。如果我們可以直接得到一堆候選框,然后對每一個框所屬的類別進行預測之后再采用某一種方法去篩選出合適的框不久變得簡單了嘛。

那么這個時候RCNN出現了,在2014年的時候,那個時候我應該還是個小學生。它的流程是這樣的:

對于一張圖片,找出默認2000個候選區域

2000個侯選區域做大小變換,輸入AlexNet當中,得到特征向量 [2000,4096]

經過20個類別的SVM分類器,對于2000個候選區域做判斷,得 到[2000,20]得分矩陣

2000個候選區域做NMS,取出不好的,重度高的一些候選 區域,得到剩下分數高,結果好的相

修正候選框,bbox的回歸微調

那么現在既然提到了RCNN,那么我們現在就不得不先提到兩個概念了,第一是IOU,第二是NMS算法也就是那個篩選算法。

不過在這里我先說一些IOU,因為NMS在代碼階段會詳細介紹,我們需要手動實現這個算法,當然IOU也需要,但是它非常簡單。
就是這個東西

e0eb53c2-9078-11ef-a511-92fbcf53809c.jpg

我們可以用這個玩意來衡量這兩個生成的框是不是重合了,重合了多少,如果重合太多的話,是不是說他們兩個框都在預測同一個物體,那么我們就可不可以把概率低的給干掉。而這個的話其實也是NMS的思想,具體還是看下文。

那么這里解決了可以自動生成框的問題,但是這里的分類器用的還是SVM,并且這個SVM肯定也是需要先訓練好的,不然很難完成分類呀。而且在訓練SVM的時候,我們是把經過了一個神經網絡的數據給SVM的,那么意味還需要對AlexNet做處理,需要緩存很多中間數據然后訓練。
而且每一個框都要進入神經網絡,2000個要進去似乎也沒有比暴力好到那兒去。

e1104326-9078-11ef-a511-92fbcf53809c.jpg

SPPNet

前面說了RCNN,其實最大的一個改進相對于滑動窗口來說,似乎就是多了一個方式去生成候選框。實際上后面那些SVM我們也未嘗不可以和AlexNet直接合并成一個大網絡然后對2000個候選框做分類,而不是分開來。

但是最大的問題并不是這個,問題在于我們還是需要進入2000次卷積。

那么有沒有辦法可以減少卷積咧,有SPPNet!

e11f876e-9078-11ef-a511-92fbcf53809c.jpg

首先候選框還是咱們RCNN那種方式提取出來的,但是它直接把一張圖片輸入進一個卷積里面。
然后得到一個特征向量,之后這個特征向量里面包含了原來的候選框的信息,他們之間存在這樣的映射關系:

e132d35a-9078-11ef-a511-92fbcf53809c.jpg

這個映射關系的不是咱們的重點,這里就忽略了,感興趣的可以自己去了解,不夠這個拿到feature map 絕對是目標檢測史上最重要的一點之一!不過在這里還沒有太大體現。

那么后面的操作其實就和RCNN類似了,只是中間又加了一些池化等等操作

e1437ca0-9078-11ef-a511-92fbcf53809c.jpg

至于缺點:

1. 訓練依然過慢、效率低,特征需要寫入磁盤(因為SVM的存在)
2. 分階段訓練網絡:選取候選區域、訓練CNN、訓練SVM、訓練bbox回歸器,SPPNet)反向傳播效率 低

Fast-RCNN

當我把標題單獨放在外面的時候,我想你應該知道了這玩意的重要性。

來我們直接看到整個圖:

e167e1e4-9078-11ef-a511-92fbcf53809c.jpg

前面的部分其實和SPPNet很像,也就是一個卷積,但是后面全部變成 net,這個好像有點像咱們一開始瞎扯提到的方式了,也就是在后半部分。不過有點可惜的是總體上FastRCNN 的改進其實是把SPPNet后面的東西改了,前面的候選框其實還是使用RCNN的那一套機制,也就是SS算法。

不夠盡管如此,fast rcnn 總算是和咱們現在的目標檢測算法的樣子有點像了,因為我們終于廢棄了SVM,終于讓我們的神經網絡去做更多的事情了。

并且提到了咱們的多任務損失,而且不用把網絡拆來了訓練了,而是可以做到端到端了。

并且速度有了很大的提升

e180d08c-9078-11ef-a511-92fbcf53809c.jpg

之后它的網絡圖是這樣的:

e193237c-9078-11ef-a511-92fbcf53809c.jpg

那么雖然已經很快了,那么還有辦法嘛?原來RCNN 可是2000個候選區域啊。能不能縮減!有沒有辦法?

(這里面還有很多細節沒有提到,需要讀者自行搜索,不過不影響本文觀看)

答案是有!

Faster RCNN

前面我們的FastRCNN 已經讓神經網絡做了很多事情了,那么為什么不能把候選框的提取也做了,讓神經網絡做到更多的事情?并且還有哪些東西是可以加強改進的?feature map 能不能利用起來?

嘿!還真能。

我們直接在feature map上面做提取,在上面生成候選區域,然后再執行后續操作,后續操作和咱們fast rcnn是一樣的,我們只需要對這些候選框和分類器處理。于是我們的網絡結構就變成了這樣

e1b05b0e-9078-11ef-a511-92fbcf53809c.jpg

在feature后面提取的網絡叫做RPN

RPN 工作流程

說到這個玩意咱們就必須提一下,因為這個東西的工作流程絕對是非常重要的,這意味著我們可以做出更大的改進在后面!

e1c78d1a-9078-11ef-a511-92fbcf53809c.jpg

我們知道它的工作地方實在feature map上面

那么他如何工作呢。

這里引入一個名稱叫做anchor 其實也就是bbox,那個預測框。

他是這樣的,在那個feature map 的基礎上,每一個網格,都會生成9個框,假設那個特征是20x20 的那么他有9個就是20x20x9 如果要具體表示的話,xmin,ymin,xmax,ymax(左上角,右下角)那就是20x20x36的張量

e1de8114-9078-11ef-a511-92fbcf53809c.jpg

那么這里為什么是9個呢,因為是這樣的,原作者設計了三種比例三種大小的樣式,因為圖片當中物體的大小是不一樣的。

e1f45a5c-9078-11ef-a511-92fbcf53809c.jpg

那么剛好對應的就是9個組合。之后的部分我就不細說了。

Yolo

那么到這里,你可能又有疑問了,那個RPN一樣的網絡能不能放在featur map 前面呢?如果我一開始就指定好圖片的網格,然后不同的網格去生成候選框會怎么樣?
沒錯大名鼎鼎的yolo出來了:(這里是v1)

e212f55c-9078-11ef-a511-92fbcf53809c.jpg

我先直接這樣認為分成7x7的格子然后每個格子產生候選框,這里是2個候選框。

e23f7154-9078-11ef-a511-92fbcf53809c.jpg

之后得到7x7x30的張量.

這里解釋一下30里面包含了啥。

這里面存儲了 兩大類信息。
第一個 是 邊框信息,起點,寬高,可信度。
第二個是 類別的條件概率,這里主要是20個類。

之后我們通過NMS對這些候選框進行篩選。

然后進入損失函數,這部分我們后面說,它的損失函數是這樣的

e253b1c8-9078-11ef-a511-92fbcf53809c.jpg

我們接下來要自制的目標檢測框架其實也是基于yolov1的。

小結

那么對于理論部分我們就先到這里,這里面的話還是有很多細節是沒有說到的,例如Fast rcnn 里面,我們NMS處理以后,我們的那些剩下的框雖然是知道了所屬的分類,但是我們回歸的時候我們是和那些手動標注的框進行回歸?這部分我沒有說,由于篇幅問題,這部分也是需要讀者自行探索,其實讀者也可以大膽猜測一下和IOU有沒有關系咧?此外還有其他的優秀算法沒有介紹到,比如SSD等等。

當然前面的大部分內容只是做了解即可,因為更加完整的將在代碼部分進行。

編碼

接下來我們將針對yolov1 算法進行實現然后將其封裝進去咱們自己搭建的平臺。

那么對我們的編碼實現里面最主要的其實有三個大點:

圖片數據怎么處理,怎么對圖片進行預處理

IOU, NMS 算法的具體實現

損失函數的設計

首先是咱們的第一點,對圖片是否需要,如何進行預處理。

神經網絡實現

我們這邊的話是打算直接集成yolov1的神經網絡結構。

e268b7a8-9078-11ef-a511-92fbcf53809c.jpg

所以的話我們需要先編寫神經網絡。但是呢,為了更好地提高網絡識別的精度和訓練效率,我們這邊還要考慮預訓練一個神經網絡模型。

所以為了實現這個效果我們需要對這個網絡做一點點的改動,提取出一個骨干網絡出來。

e2806560-9078-11ef-a511-92fbcf53809c.jpg

其中BackBone就是我們的核心網絡,也就是其中的10幾個卷積,后面兩個一個是特征提取網絡一個是我們用于目標識別的網絡。我們預訓練是訓練特征提取網絡,這個網絡是依托與骨干網絡的。他們之間的關系是這樣的:

e2a4e9a8-9078-11ef-a511-92fbcf53809c.jpg

特征提取網絡其實就是在骨干網絡的基礎上用于分類,這樣一來就得到了權重,當我們訓練目標檢測網絡的時候,我們可以把先前預訓練的特征網絡當中的骨干網絡的權重提取出來作為初始化權重,這也就是遷移學習。

骨干網絡

import torch.nn as nn
import torch
from collections import OrderedDict


class Convention(nn.Module):
    def __init__(self,in_channels,out_channels,conv_size,conv_stride,padding,need_bn = True):

        """
        這邊對Conv2d進行一個封裝,參數一致
        但是多加了LeakReLU,和歸一化,原因不多說了
        :param in_channels:
        :param out_channels:
        :param conv_size:
        :param conv_stride:
        :param padding:
        :param need_bn:
        """

        super(Convention,self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, conv_size, conv_stride, padding, bias=False if need_bn else True)
        self.leaky_relu = nn.LeakyReLU()
        self.need_bn = need_bn
        if need_bn:
            self.bn = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        return self.bn(self.leaky_relu(self.conv(x))) if self.need_bn else self.leaky_relu(self.conv(x))

    def weight_init(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                torch.nn.init.kaiming_normal_(m.weight.data)
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()


class BackboneNet(nn.Module):
    """
    骨干網絡,因為那個論文中也提到了預訓練的概念
    那么這個預訓練其實是說訓練這個骨干網絡,而這個
    網絡的話其實是7x7x30的前半部分
    那個yolo是24卷積+2個全連接得到7x7x1024之后flatten4096
    最后變成7x7x30,然后就是NMS,預訓練需要先訓練一個
    分類的網絡,所以這部分是不一樣的
    """
    def __init__(self):
        super(BackboneNet,self).__init__()

        """
        用于特征提取的16個卷積
        """
        self.Conv_Feature = nn.Sequential(
            Convention(3, 64, 7, 2, 3),
            nn.MaxPool2d(2, 2),
            Convention(64, 192, 3, 1, 1),
            nn.MaxPool2d(2, 2),
            Convention(192, 128, 1, 1, 0),
            Convention(128, 256, 3, 1, 1),
            Convention(256, 256, 1, 1, 0),
            Convention(256, 512, 3, 1, 1),
            nn.MaxPool2d(2, 2),
            Convention(512, 256, 1, 1, 0),
            Convention(256, 512, 3, 1, 1),
            Convention(512, 256, 1, 1, 0),
            Convention(256, 512, 3, 1, 1),
            Convention(512, 256, 1, 1, 0),
            Convention(256, 512, 3, 1, 1),
            Convention(512, 256, 1, 1, 0),
            Convention(256, 512, 3, 1, 1),
            Convention(512, 512, 1, 1, 0),
            Convention(512, 1024, 3, 1, 1),
            nn.MaxPool2d(2, 2),
        )
        self.Conv_Semanteme = nn.Sequential(
            Convention(1024, 512, 1, 1, 0),
            Convention(512, 1024, 3, 1, 1),
            Convention(1024, 512, 1, 1, 0),
            Convention(512, 1024, 3, 1, 1),
        )

這里可以看到這個網絡啥也沒有,就是一個最基本的骨架。

特征提取網絡(預訓練)

import torch
import torch.nn as nn
from Models.Backbone import BackboneNet, Convention


class YOLOFeature(BackboneNet):
    def __init__(self,classes_num = 20):
        """
        原文說的就是20個所以咱們也就來個20
        :param classes_num:
        """
        super(YOLOFeature,self).__init__()
        self.classes_num = classes_num

        self.avg_pool = nn.AdaptiveAvgPool2d(1)

        self.linear = nn.Linear(1024, self.classes_num)

    def forward(self, x):
        x = self.Conv_Feature(x)
        x = self.Conv_Semanteme(x)
        x = self.avg_pool(x)
        x = x.permute(0, 2, 3, 1)
        x = torch.flatten(x, start_dim=1, end_dim=3)
        x = self.linear(x)
        return x
    """
    初始化權重
    """
    def initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                torch.nn.init.kaiming_normal_(m.weight.data)
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
            elif isinstance(m, nn.Linear):
                torch.nn.init.kaiming_normal_(m.weight.data)
                m.bias.data.zero_()
            elif isinstance(m, Convention):
                m.weight_init()

目標檢測網絡

最后是咱們的目標檢測網絡。

import torch.nn as nn
import torch

from Models.Backbone import BackboneNet, Convention


class YOLO(BackboneNet):

    def __init__(self, B=2, classes_num=20):
        super(YOLO, self).__init__()
        self.B = B
        self.classes_num = classes_num


        self.Conv_Back = nn.Sequential(
            Convention(1024, 1024, 3, 1, 1, need_bn=False),
            Convention(1024, 1024, 3, 2, 1, need_bn=False),
            Convention(1024, 1024, 3, 1, 1, need_bn=False),
            Convention(1024, 1024, 3, 1, 1, need_bn=False),
        )


        self.Fc = nn.Sequential(
            nn.Linear(7 * 7 * 1024, 4096),
            nn.LeakyReLU(inplace=True, negative_slope=1e-1),
            nn.Linear(4096, 7 * 7 * (B * 5 + classes_num)),
            nn.Sigmoid()
        )
        self.sigmoid = nn.Sigmoid()
        """
        batchx7x7x30讓最后一個維度對應的類別為概率和為1 
        """
        # self.softmax = nn.Softmax(dim=3)
    def forward(self, x):
        x = self.Conv_Feature(x)
        x = self.Conv_Semanteme(x)
        x = self.Conv_Back(x)
        x = x.permute(0, 2, 3, 1)
        x = torch.flatten(x, start_dim=1, end_dim=3)
        x = self.Fc(x)
        x = x.view(-1,7,7,(self.B*5 + self.classes_num))
        # x[:,:,:, 0 : self.B * 5] = self.sigmoid(x[:,:,:, 0 : self.B * 5])
        # x[:,:,:, self.B * 5 : ] = self.softmax(x[:,:,:, self.B * 5 : ])
        """
        在pytorch當中注釋部分的操作屬于inplace操作,而且在官方文檔當中,明確表明
        在多交叉熵當中,pytorch不需要使用softmax,因為在計算的時候是包括了這部分的操作的
        并且在yolov1的損失函數當中,計算的類別損失也不是交叉熵
        """
        x = self.sigmoid(x)
        return x

    def initialize_weights(self, net_param_dict):
        for name, m in self.named_modules():
            if isinstance(m, nn.Conv2d):
                torch.nn.init.kaiming_normal_(m.weight.data)
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
            elif isinstance(m, nn.Linear):
                torch.nn.init.kaiming_normal_(m.weight.data)
                m.bias.data.zero_()
            elif isinstance(m, Convention):
                m.weight_init()

        self_param_dict = self.state_dict()
        for name, layer in self.named_parameters():
            if name in net_param_dict:
                self_param_dict[name] = net_param_dict[name]
        self.load_state_dict(self_param_dict)

這里要特別注意我注釋的這段代碼:

        # x[:,:,:, 0 : self.B * 5] = self.sigmoid(x[:,:,:, 0 : self.B * 5])
        # x[:,:,:, self.B * 5 : ] = self.softmax(x[:,:,:, self.B * 5 : ])

接下來我會更加詳細地說明

數據集編碼

現在我們已經知道了咱們這邊的目的有兩個,一個是要預訓練,一個是要目標檢測

預訓練數據集

其中咱們的預訓練是訓練一個基本的過程。

那么在這里的話,其實很簡單,我們訓練的話我們只需要把那個特征網絡拿過來,重點是咱們的這個預訓練數據集怎么來。

那么這邊的話,如果是老盆友,或者是看來剛剛開頭推薦觀看的文章的朋友應該知道,這邊的話我們可以直接把咱們的HuDataSet拿過來。

首先這個數據集的定義非常簡單:

e2c4dfa6-9078-11ef-a511-92fbcf53809c.jpg

相信你一眼就知道了是怎么一回事。分訓練很驗證集,然后每個分類的標簽放在對應的文件夾下面就可以了。

核心代碼如下:

from  Config.Config import *
import os
from PIL import Image
from torch.utils.data import Dataset, DataLoader
from torchvision.transforms import transforms

from Utils.ReaderProcess.ReadDict import ReadDict


class MyDataSet(Dataset):
    def __init__(self, data_dir,ClassesName, transform=None):
        self.ClassesName = ClassesName
        self.label_name = ReadDict.ReadModelClasses(self.ClassesName)

        self.data_info = self.get_img_info(data_dir)
        self.transform = transform

    def __getitem__(self, index):
        path_img, label = self.data_info[index]
        img = Image.open(path_img).convert('RGB')

        if self.transform is not None:
            img = self.transform(img)
        return img, label

    def __len__(self):
        return len(self.data_info)

    def get_img_info(self,data_dir):
        data_info = list()
        label_dict=ReadDict.ReadModelClasses(self.ClassesName)

        for root, dirs, _ in os.walk(data_dir): #
            # 遍歷類別
            for sub_dir in dirs:
                img_names = os.listdir(os.path.join(root, sub_dir))
                img_names = list(filter(lambda x: x.endswith('.jpg'), img_names))
                # 遍歷圖片
                for i in range(len(img_names)):
                    img_name = img_names[i]
                    path_img = os.path.join(root, sub_dir, img_name)
                    label = label_dict[sub_dir]
                    data_info.append((path_img, int(label)))

        return data_info

目標檢測數據集

這里的話我們采用VOC數據集,數據集的基本樣式其實很簡單。

e2dee504-9078-11ef-a511-92fbcf53809c.jpg

一個是Annotations注解,還有一個是圖片

注解里面是xml文件

e304519a-9078-11ef-a511-92fbcf53809c.jpg

里面包括了類別和手動標注的框的位置。


    images
    001.jpg
    F:projectsPythonProjectyolov5-5.0mydataimages01.jpg
    
        Unknown
    
    
        1200
        701
        3
    
    0
    
        貓羽雫
        Unspecified
        0
        0
        
            647
            23
            932
            282
        
    

由于我們需要進行目標檢測,但是呢,我們除了要提取里面的標簽信息的話,還要把里面的標簽(類別,方框)信息進行轉化,轉化的目的也是為了復合神經網絡的輸出方便損失函數計算。

VOC標簽解析

解析的話很簡單,就這個

        for object_xml in objects_xml:
            bnd_xml = object_xml.find("bndbox")
            class_name = object_xml.find("name").text
            if class_name not in self.class_dict:  # 不屬于我們規定的類
                continue
            xmin = round((float)(bnd_xml.find("xmin").text))
            ymin = round((float)(bnd_xml.find("ymin").text))
            xmax = round((float)(bnd_xml.find("xmax").text))
            ymax = round((float)(bnd_xml.find("ymax").text))
            class_id = self.class_dict[class_name]
            """
            這里解析存儲的是5個值,縮放,歸一化后的坐標和對應的類別的標簽
            """
            coords.append([xmin, ymin, xmax, ymax, class_id])

完整與之配合的代碼是這樣的:
這里還使用了部分數據增強

import torch
from torch.utils.data import Dataset
import os
import cv2
import xml.etree.ElementTree as ET
import torchvision.transforms as transforms
import numpy as np
import random
from Utils import image
from Config.ConfigTrain import *
class VOCDataSet(Dataset):


    def __init__(self, imgs_path="../DataSet/VOC2007+2012/Train/JPEGImages",
                 annotations_path="../DataSet/VOC2007+2012/Train/Annotations",
                 is_train=True, class_num=Classes,
                 label_smooth_value=0.05, input_size=448, grid_size=64):  # input_size:輸入圖像的尺度
        self.label_smooth_value = label_smooth_value
        self.class_num = class_num
        self.imgs_name = os.listdir(imgs_path)
        self.input_size = input_size
        self.grid_size = grid_size
        self.is_train = is_train

        self.transform_common = transforms.Compose([
            transforms.ToTensor(),  # height * width * channel -> channel * height * width
            transforms.Normalize(mean=(0.408, 0.448, 0.471), std=(0.242, 0.239, 0.234))  # 歸一化后.不容易產生梯度爆炸的問題
        ])
        self.imgs_path = imgs_path
        self.annotations_path = annotations_path
        self.class_dict = {}
        class_index = 0
        """
        讀取配置標簽
        """
        for class_name in ClassesName:
            self.class_dict[class_name] = class_index
            class_index+=1

    def __getitem__(self, item):

        img_path = os.path.join(self.imgs_path, self.imgs_name[item])
        annotation_path = os.path.join(self.annotations_path, self.imgs_name[item].replace(".jpg", ".xml"))
        img = cv2.imread(img_path)
        tree = ET.parse(annotation_path)
        annotation_xml = tree.getroot()

        objects_xml = annotation_xml.findall("object")
        coords = []

        for object_xml in objects_xml:
            bnd_xml = object_xml.find("bndbox")
            class_name = object_xml.find("name").text
            if class_name not in self.class_dict:  # 不屬于我們規定的類
                continue
            xmin = round((float)(bnd_xml.find("xmin").text))
            ymin = round((float)(bnd_xml.find("ymin").text))
            xmax = round((float)(bnd_xml.find("xmax").text))
            ymax = round((float)(bnd_xml.find("ymax").text))
            class_id = self.class_dict[class_name]
            """
            這里解析存儲的是5個值,縮放,歸一化后的坐標和對應的類別的標簽
            """
            coords.append([xmin, ymin, xmax, ymax, class_id])

        coords.sort(key=lambda coord: (coord[2] - coord[0]) * (coord[3] - coord[1]))
        if self.is_train:

            transform_seed = random.randint(0, 4)

            if transform_seed == 0:  # 原圖
                img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
                img = self.transform_common(img)

            elif transform_seed == 1:  # 縮放+中心裁剪
                img, coords = image.center_crop_with_coords(img, coords)
                img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
                img = self.transform_common(img)

            elif transform_seed == 2:  # 平移
                img, coords = image.transplant_with_coords(img, coords)
                img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
                img = self.transform_common(img)

            elif transform_seed == 3:  # 明度調整 YOLO在論文中稱曝光度為明度
                img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
                img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
                H, S, V = cv2.split(img)
                cv2.merge([np.uint8(H), np.uint8(S), np.uint8(V * 1.5)], dst=img)
                cv2.cvtColor(src=img, dst=img, code=cv2.COLOR_HSV2BGR)
                img = self.transform_common(img)

            else:  # 飽和度調整
                img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
                H, S, V = cv2.split(img)
                cv2.merge([np.uint8(H), np.uint8(S * 1.5), np.uint8(V)], dst=img)
                cv2.cvtColor(src=img, dst=img, code=cv2.COLOR_HSV2BGR)
                img = self.transform_common(img)

        else:
            img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
            img = self.transform_common(img)

        ground_truth = self.encode(coords)

        """
        這里傳入的coords是經過圖片增強,然后歸一化之后的
        之后的話,我們需要經過encode目的是的為了制作方便后期和pred對比的label
        """

        return img,ground_truth


    def __len__(self):
        return len(self.imgs_name)

    def encode(self, coords):

        feature_size = self.input_size // self.grid_size
        ground_truth = np.zeros([feature_size, feature_size, 10 + self.class_num],dtype=float)

        for coord in coords:
            # positive_num = positive_num + 1
            # bounding box歸一化
            xmin, ymin, xmax, ymax, class_id = coord

            ground_width = (xmax - xmin)
            ground_height = (ymax - ymin)

            center_x = (xmin + xmax) / 2
            center_y = (ymin + ymax) / 2

            index_row = (int)(center_y * feature_size)
            index_col = (int)(center_x * feature_size)


            ground_box = [center_x * feature_size - index_col, center_y * feature_size - index_row,
                          ground_width, ground_height, 1,
                          round(xmin * self.input_size), round(ymin * self.input_size),
                          round(xmax * self.input_size), round(ymax * self.input_size),
                          round(ground_width * self.input_size * ground_height * self.input_size)
                          ]
            # ground_box.extend(class_list)
            class_ = [0 for _ in range(self.class_num)]
            class_[class_id]=1
            ground_box.extend(class_)

            ground_truth[index_row][index_col] = np.array(ground_box,dtype=float)

        return ground_truth

格式轉化

現在請把目光轉移到這里來:

    def encode(self, coords):

        feature_size = self.input_size // self.grid_size
        ground_truth = np.zeros([feature_size, feature_size, 10 + self.class_num],dtype=float)

        for coord in coords:
            # positive_num = positive_num + 1
            # bounding box歸一化
            xmin, ymin, xmax, ymax, class_id = coord

            ground_width = (xmax - xmin)
            ground_height = (ymax - ymin)

            center_x = (xmin + xmax) / 2
            center_y = (ymin + ymax) / 2

            index_row = (int)(center_y * feature_size)
            index_col = (int)(center_x * feature_size)


            ground_box = [center_x * feature_size - index_col, center_y * feature_size - index_row,
                          ground_width, ground_height, 1,
                          round(xmin * self.input_size), round(ymin * self.input_size),
                          round(xmax * self.input_size), round(ymax * self.input_size),
                          1
                          ]
            # ground_box.extend(class_list)
            class_ = [0 for _ in range(self.class_num)]
            class_[class_id]=1
            ground_box.extend(class_)

            ground_truth[index_row][index_col] = np.array(ground_box,dtype=float)

        return ground_truth

我們把VOC的格式解析出來了,也做了數據增強之后做了歸一化得到了幾個標注的框。但是由于在論文當中是這樣的:

作者將一張圖片劃分為了7x7的網格,讓每一格子預測兩個框,所以我們真實標注的框也需要轉化為這種格式,我們需要手動把我們的結果轉化為7x7x(10+類別個數)的樣子,因為網絡最后的輸出就是7x7x(10+類別個數)

當然 實際上,我們標注的框轉化之后一個格子應該是只有一個物體的,所以這里我們轉化的話其實不用那么嚴格只需要7x7x(5+類別個數)就可以了,但是這里為了對得到,同時方便后面轉化,這里還存儲了實際上圖片的框的坐標(以這個格子為中心)

e212f55c-9078-11ef-a511-92fbcf53809c.jpg

那么一來在實際計算損失的時候,我們只需要這樣:

e3318bf6-9078-11ef-a511-92fbcf53809c.jpg

所以因為這個特性,我們需要把標簽這樣進行轉化,方便損失函數計算,而且損失函數的計算是一個一個格子來對比計算的,也就是一個一個的grad cell。

損失函數(目標檢測)

之后咱們的損失函數,前面說了為啥要轉化標簽,那么現在咱們可以來看看損失函數了。

e253b1c8-9078-11ef-a511-92fbcf53809c.jpg

這里提一下正負樣本的概念,這里的話其實也簡單,就是一個一個格子去對比,然后呢有些格子是沒有目標的,但是我們預測的時候每個格子都是預測了兩個框的,那么這兩個框顯然是沒有用的,那么這個玩意就是負樣本,同理如果對應的格子有目標,但是兩個框的IOU不一樣(與實際的框)那么IOU低的也算是負樣本。

import sys
import torch.nn as nn
import math
import torch
import torch.nn.functional as F
from Config.ConfigTrain import ClassesName

class YOLOLoss(nn.Module):

    def __init__(self, S=7, B=2, Classes=20, l_coord=5, l_noobj=0.5, epcoh_threshold=400):
        """
        :param S:
        :param B:
        :param Classes:
        :param l_coord:
        :param l_noobj:
        :param epcoh_threshold:
        有物體的box損失權重設為l_coord,沒有物體的box損失權重設置為l_noobj
        在論文當中應該是正樣本和負樣本之間的一個權重,因為我們不僅僅要預測有物體的,原來沒有物體的也不能有物體
        """

        super(YOLOLoss, self).__init__()
        self.S = S
        self.B = B
        self.Classes = Classes
        self.l_coord = l_coord
        self.l_noobj = l_noobj
        self.epcoh_threshold = epcoh_threshold

    def iou(self, bounding_box, ground_box, gridX, gridY, img_size=448, grid_size=64):
        """
        計算交并比
        :param bounding_box:
        :param ground_box:
        :param gridX:
        :param gridY:
        :param img_size:
        :param grid_size:
        
        由于predict_box 返回的是x y w h 這種格式,所以我們還是需要進行轉換回原來的xmin ymin xmax ymax
        也就是左上右下
        """
        predict_box = [0, 0, 0, 0]
        predict_box[0] = (int)(gridX + bounding_box[0].item() * grid_size)
        predict_box[1] = (int)(gridY + bounding_box[1].item() * grid_size)
        predict_box[2] = (int)(bounding_box[2].item() * img_size)
        predict_box[3] = (int)(bounding_box[3].item() * img_size)

        predict_coord = list([max(0, predict_box[0] - predict_box[2] / 2),
                              max(0, predict_box[1] - predict_box[3] / 2),
                              min(img_size - 1, predict_box[0] + predict_box[2] / 2),
                              min(img_size - 1, predict_box[1] + predict_box[3] / 2)])
        predict_Area = (predict_coord[2] - predict_coord[0]) * (predict_coord[3] - predict_coord[1])

        ground_coord = list([ground_box[5].item() , ground_box[6].item() , ground_box[7].item() , ground_box[8].item() ])
        ground_Area = (ground_coord[2] - ground_coord[0]) * (ground_coord[3] - ground_coord[1])

        """
        轉化為原來左上右下之后進行計算
        """
        CrossLX = max(predict_coord[0], ground_coord[0])
        CrossRX = min(predict_coord[2], ground_coord[2])
        CrossUY = max(predict_coord[1], ground_coord[1])
        CrossDY = min(predict_coord[3], ground_coord[3])

        if CrossRX < CrossLX or CrossDY < CrossUY:  # 沒有交集
            return 0

        interSection = (CrossRX - CrossLX) * (CrossDY - CrossUY)

        return interSection / (predict_Area + ground_Area - interSection)

    def forward(self, bounding_boxes, ground_truth, batch_size=32, grid_size=64,
                img_size=448):  # 輸入是 S * S * ( 2 * B + Classes)
        # 定義三個計算損失的變量 正樣本定位損失 樣本置信度損失 樣本類別損失
        loss = 0
        loss_coord = 0
        loss_confidence = 0
        loss_classes = 0
        iou_sum = 0
        object_num = 0

        mseLoss = nn.MSELoss()
        for batch in range(len(bounding_boxes)):

            for indexRow in range(self.S):  # 先行 - Y
                for indexCol in range(self.S):  # 后列 - X
                    """
                    這里額外統計了三個損失
                    """
                    bounding_box = bounding_boxes[batch][indexRow][indexCol]
                    predict_box_one = bounding_box[0:5]
                    predict_box_two = bounding_box[5:10]
                    ground_box = ground_truth[batch][indexRow][indexCol]
                    # 1.如果此處ground_truth不存在 即只有背景 那么兩個框均為負樣本

                    if (ground_box[4]) == 0:  # 面積為0的grount_truth 表明此處只有背景
                        loss = loss + self.l_noobj * torch.pow(predict_box_one[4], 2) + torch.pow(
                            predict_box_two[4], 2)
                        loss_confidence += self.l_noobj * math.pow(predict_box_one[4].item(), 2) + math.pow(
                            predict_box_two[4].item(), 2)
                    else:
                        # print(ground_box[4].item(), ClassesName[int(ground_box[10].item())])
                        object_num = object_num + 1
                        predict_iou_one = self.iou(predict_box_one, ground_box, indexCol * 64, indexRow * 64)
                        predict_iou_two = self.iou(predict_box_two, ground_box, indexCol * 64, indexRow * 64)
                        # 改進:讓兩個預測的box與ground box擁有更大iou的框進行擬合 讓iou低的作為負樣本
                        if predict_iou_one > predict_iou_two:  # 框1為正樣本  框2為負樣本
                            predict_box = predict_box_one
                            iou = predict_iou_one
                            no_predict_box = predict_box_two
                        else:
                            predict_box = predict_box_two
                            iou = predict_iou_two
                            no_predict_box = predict_box_one
                        # 正樣本:
                        # 定位
                        loss = loss + self.l_coord * (torch.pow((ground_box[0] - predict_box[0]), 2) + torch.pow(
                            (ground_box[1] - predict_box[1]), 2) + torch.pow(
                            torch.sqrt(ground_box[2] + 1e-8) - torch.sqrt(predict_box[2] + 1e-8), 2) + torch.pow(
                            torch.sqrt(ground_box[3] + 1e-8) - torch.sqrt(predict_box[3] + 1e-8), 2))
                        loss_coord += self.l_coord * (
                                    math.pow((ground_box[0] - predict_box[0].item()), 2) + math.pow(
                                (ground_box[1] - predict_box[1].item()), 2) + math.pow(
                                math.sqrt(ground_box[2] + 1e-8) - math.sqrt(predict_box[2].item() + 1e-8),
                                2) + math.pow(
                                math.sqrt(ground_box[3] + 1e-8) - math.sqrt(predict_box[3].item() + 1e-8), 2))
                        # 置信度
                        loss = loss + torch.pow(predict_box[4] - iou, 2)
                        loss_confidence += math.pow(predict_box[4].item() - iou, 2)
                        iou_sum = iou_sum + iou
                        # 分類
                        ground_class = ground_box[10:]
                        predict_class = bounding_box[self.B * 5:]

                        loss = loss + mseLoss(ground_class, predict_class)
                        loss_classes += mseLoss(ground_class, predict_class).item()
                        # 負樣本 置信度:
                        loss = loss + self.l_noobj * torch.pow(no_predict_box[4] - 0, 2)
                        loss_confidence += math.pow(no_predict_box[4].item() - 0, 2)

        return loss/batch_size, loss_coord/batch_size, loss_confidence/batch_size, loss_classes/batch_size, iou_sum, object_num

那么在這里的話我也要說說,剛剛注釋的這個代碼:

        # x[:,:,:, 0 : self.B * 5] = self.sigmoid(x[:,:,:, 0 : self.B * 5])
        # x[:,:,:, self.B * 5 : ] = self.softmax(x[:,:,:, self.B * 5 : ])

它為什么不行了,第一個這個代碼本身存在inplace操作。
第二如果真的需要使用交叉熵作為分類的損失函數的話,pytorch內部的交叉熵損失函數自己是計算了softmax的
第三,就是咱們的sunshine函數里面壓根不是交叉熵來算類別損失的,人家就是MSE。

之后是關于置信度confidence的計算,這個玩意是表示這里面有沒有(這個格子里面)物體的,首先預測的時候,那個值是預測出來的,計算損失的時候,那個c(在有物品的情況下)是等于1的,這個在咱們voc數據集里面可以看到,有物品直接為1

e36c2c48-9078-11ef-a511-92fbcf53809c.jpg

但是呢,實際計算的時候,這個c呢是咱們那個預測框和實際框的IOU。

這個論文當中也有描述。

e396d2e0-9078-11ef-a511-92fbcf53809c.png

訓練部分

接下來是咱們的訓練部分,這個呢,有兩個一個是預訓練一個是實際訓練。

e3b9e5fa-9078-11ef-a511-92fbcf53809c.jpg

預訓練得到的一個模型還可以用于圖片分類。

預訓練

這部分其實很簡單就不多說了。

import argparse
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torch.optim as optim
from Models.FeatureNet import YOLOFeature
from Utils import ModelUtils
from Config.ConfigPre import *
from Utils.DataSet.MyDataSet import MyDataSet
from Utils.DataSet.TransformAtions import TransFormAtions
import os
from Utils import SaveModel
from Utils import Log
from torch.utils.tensorboard import SummaryWriter


def train():

    ModelUtils.set_seed()
    # 初始化驅動
    device = None
    if (torch.cuda.is_available()):
        if (not opt.device == 'cpu'):
            div = "cuda:" + opt.device
            # 這邊后面還得做一個檢測,看看有沒有坑貨,亂輸入
            device = torch.device(div)
            print("33[0;31;0m使用GPU訓練中:{}33[0m".format(torch.cuda.get_device_name()))
        else:
            device = torch.device("cpu")
            print("33[0;31;40m使用CPU訓練33[0m")
    else:
        device = torch.device("cpu")
        print("33[0;31;40m使用CPU訓練33[0m")

    # 創建 runs exp 文件
    EPX_Path = SaveModel.CreatRun(0,"pre")
    # 日志相關的準備工作
    wirter = None
    openTensorboard = opt.tensorboardopen
    path_board = None
    if (openTensorboard):
        path_board = EPX_Path + "\logs"
        wirter = SummaryWriter(path_board)

    fo = Log.PrintLog(EPX_Path)

    # 準備數據集
    transformations = TransFormAtions()
    train_data_dir = opt.train_dir
    if (not train_data_dir):

        train_data_dir = Data_Root + "" + Train
        if (not os.path.exists(train_data_dir)):
            raise Exception("訓練集路徑錯誤")

    train_data = MyDataSet(data_dir=train_data_dir, transform=transformations.train_transform,ClassesName=ClassesName)
    valid_data_dir = opt.valid_dir
    if (not valid_data_dir):
        valid_data_dir = Data_Root + "" + Valid
        if (not os.path.exists(valid_data_dir)):
            raise Exception("測試集路徑錯誤")

    valid_data = MyDataSet(data_dir=valid_data_dir, transform=transformations.valid_transform,ClassesName=ClassesName)

    # 構建DataLoder
    train_loader = DataLoader(dataset=train_data, batch_size=opt.batch_size, num_workers=opt.works, shuffle=True)
    valid_loader = DataLoader(dataset=valid_data, batch_size=opt.batch_size)

    # 開始進入網絡訓練
    # 1 開始初始化網絡,設置參數啥的
    # 1.1 初始化網絡
    net = YOLOFeature(Classes)
    net.initialize_weights()
    net = net.to(device)

    # 1.2選擇交叉熵損失函數,做分類問題一般是選擇這個損失函數的
    criterion = nn.CrossEntropyLoss()

    # 1.3設置優化器
    optimizer = optim.SGD(net.parameters(), lr=opt.lr, momentum=0.09)  # 選擇優化器
    # 設置學習率下降策略,默認的也可以,那就不設置嘛,主要是不斷去自動調整學習的那個速度
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.01)

    # 2 開始進入訓練步驟
    # 2.1 進入網絡訓練

    Best_weight = None
    Best_Acc = 0.0

    for epoch in range(opt.epochs):

        loss_mean = 0.0
        correct = 0.0
        total = 0.0
        current_Acc_ecpho = 0.0
        bacth_index = 0.
        val_time = 0
        net.train()
        print("正在進行第{}輪訓練".format(epoch + 1))
        for i, data in enumerate(train_loader):
            bacth_index+=1
            # forward
            inputs, labels = data
            inputs, labels = inputs.to(device), labels.to(device)
            # print(inputs.shape,labels.shape)
            outputs = net(inputs)
            # print(outputs.shape, labels.shape)
            # backward
            optimizer.zero_grad()
            loss = criterion(outputs, labels)
            loss.backward()

            # update weights
            optimizer.step()

            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).squeeze().sum()

            # 打印訓練信息,進入對比
            loss_mean += loss.item()
            current_Acc = correct / total
            current_Acc_ecpho+=current_Acc
            if (i + 1) % opt.log_interval == 0:
                loss_mean = loss_mean / opt.log_interval

                info = "訓練:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}" 
                    .format 
                        (
                        epoch, opt.epochs, i + 1, len(train_loader), loss_mean, current_Acc
                    )
                print(info, file=fo)

                if (opt.show_log_console):
                    info_print = "33[0;33;0m" + info + "33[0m"
                    print(info_print)

                loss_mean = 0.0

        # tensorboard 繪圖
        if (wirter):
            wirter.add_scalar("訓練準確率", current_Acc_ecpho, (epoch))
            wirter.add_scalar("訓練損失均值", loss_mean, (epoch))
        current_Acc_ecpho/=bacth_index
        # 保存效果最好的玩意
        if (current_Acc_ecpho > Best_Acc):
            Best_weight = net.state_dict()
            Best_Acc = current_Acc_ecpho
        scheduler.step()  # 更新學習率

        # 2.2 進入訓練對比階段

        if (epoch + 1) % opt.val_interval == 0:
            correct_val = 0.0
            total_val = 0.0
            loss_val = 0.0
            current_Acc_val = 0.0
            current_Acc_ecpho_val = 0.
            batch_index_val = 0.0
            net.eval()
            with torch.no_grad():
                for j, data in enumerate(valid_loader):
                    batch_index_val+=1
                    inputs, labels = data
                    inputs, labels = inputs.to(device), labels.to(device)
                    outputs = net(inputs)
                    loss = criterion(outputs, labels)
                    loss_val += loss.item()
                    _, predicted = torch.max(outputs.data, 1)
                    total_val += labels.size(0)
                    correct_val += (predicted == labels).squeeze().sum()
                    current_Acc_val = correct_val / total_val
                    current_Acc_ecpho_val+=current_Acc_val
                info_val = "測試:	Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format 
                        (
                        epoch, opt.epochs, j + 1, len(valid_loader), loss_val, current_Acc_val
                    )
                print(info_val, file=fo)
                if (opt.show_log_console):
                    info_print_val = "33[0;31;0m" + info_val + "33[0m"
                    print(info_print_val)
                current_Acc_ecpho_val/=batch_index_val
                if (wirter):
                    wirter.add_scalar("測試準確率", current_Acc_ecpho_val, (val_time))
                    wirter.add_scalar("測試損失總值", loss_val, (val_time))
                    val_time+=1

    # 最后一次的權重
    Last_weight = net.state_dict()

    # 保存模型
    SaveModel.Save_Model(EPX_Path, Best_weight, Last_weight)

    fo.close()
    if (wirter):
        print("tensorboard dir is:", path_board)
    wirter.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--epochs', type=int, default=10)
    parser.add_argument('--batch-size', type=int, default=8)
    parser.add_argument('--lr', type=float, default=0.01)
    parser.add_argument('--log_interval', type=int, default=10)
    # 訓練幾輪測試一次
    parser.add_argument('--val_interval', type=int, default=1)
    parser.add_argument('--train_dir', type=str, default='')
    parser.add_argument('--valid_dir', type=str, default='')
    # 如果是Mac系注意這個參數可能需要設置為1,本地訓練,不推薦MAC
    parser.add_argument('--works', type=int, default=2)
    parser.add_argument('--show_log_console', type=bool, default=True)
    parser.add_argument('--device', type=str, default="0", help="默認使用顯卡加速訓練參數選擇:0,1,2...or cpu")
    parser.add_argument('--tensorboardopen', type=bool, default=True)
    opt = parser.parse_args()
    train()

    # tensorboard --logdir = runs/train/epx2/logs

這部分還是簡單的。

目標檢測訓練

之后就是咱們目標檢測算法的實現,這個其實核心流程都是一樣的,就是多了一些東西用來做記錄

import argparse
import gc

import torch
from torch.utils.data import DataLoader
import torch.optim as optim
from Models.Yolo import YOLO
from Models.YoloLoss import YOLOLoss
from Utils import ModelUtils
from Config.ConfigTrain import *
from Utils.DataSet.VOC import VOCDataSet
import os
from Utils import SaveModel
from Utils import Log
from torch.utils.tensorboard import SummaryWriter


def train():

    ModelUtils.set_seed()
    # 初始化驅動
    device = None
    if (torch.cuda.is_available()):
        if (not opt.device == 'cpu'):
            div = "cuda:" + opt.device
            device = torch.device(div)
            torch.backends.cudnn.benchmark = True
            print("33[0;31;0m使用GPU訓練中:{}33[0m".format(torch.cuda.get_device_name()))
        else:
            device = torch.device("cpu")
            print("33[0;31;40m使用CPU訓練33[0m")
    else:
        device = torch.device("cpu")
        print("33[0;31;40m使用CPU訓練33[0m")

    # 創建 runs exp 文件
    EPX_Path = SaveModel.CreatRun(0,"detect")
    # 日志相關的準備工作
    wirter = None
    openTensorboard = opt.tensorboardopen
    path_board = None
    if (openTensorboard):
        path_board = EPX_Path + "\logs"
        wirter = SummaryWriter(path_board)

    fo = Log.PrintLog(EPX_Path)


    train_data_dir_image = opt.train_dir_image
    train_data_dir_Ann = opt.train_dir_Ann
    if (not train_data_dir_image):
        train_data_dir_image = TrainImage
        if (not os.path.exists(train_data_dir_image)):
            raise Exception("訓練集路徑錯誤")
    if (not train_data_dir_Ann):
        train_data_dir_Ann = TrainAnn
        if (not os.path.exists(train_data_dir_Ann)):
            raise Exception("訓練集路徑錯誤")



    train_data =VOCDataSet(imgs_path=train_data_dir_image,
                               annotations_path=train_data_dir_Ann,
                               is_train=True)

    valid_data_dir_image = opt.valid_dir_image
    valid_data_dir_Ann = opt.valid_dir_Ann

    if (not valid_data_dir_image):
        valid_data_dir_image = ValImage
        if (not os.path.exists(valid_data_dir_image)):
            raise Exception("訓練集路徑錯誤")
    if (not valid_data_dir_Ann):
        valid_data_dir_Ann = ValAnn
        if (not os.path.exists(valid_data_dir_Ann)):
            raise Exception("訓練集路徑錯誤")

    valid_data = VOCDataSet(imgs_path=valid_data_dir_image,
                             annotations_path=valid_data_dir_Ann,
                             is_train=False)

    # 構建DataLoder
    train_loader = DataLoader(dataset=train_data, batch_size=opt.batch_size, num_workers=opt.works, shuffle=True)
    valid_loader = DataLoader(dataset=valid_data, batch_size=opt.batch_size)
    # 1 開始初始化網絡,設置參數啥的
    net = YOLO(B=2,classes_num=Classes)
    #加載預訓練權重
    if(PreWeight):
        # 1.1 初始化網絡
        preweight = torch.load(PreWeight)
        net.initialize_weights(preweight)

    net = net.to(device)


    loss_func = YOLOLoss(S=7,B=2,Classes=Classes).to(device)

    # 1.3設置優化器
    optimizer = optim.SGD(net.parameters(), lr=opt.lr, momentum=0.09)  # 選擇優化器
    # 設置學習率下降策略,默認的也可以,那就不設置嘛,主要是不斷去自動調整學習的那個速度
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.01)

    # 2 開始進入訓練步驟
    # 2.1 進入網絡訓練

    Best_weight = None

    TotalLoss = 0.
    ValLoss = 0.
    ValTime = 0.
    Best_loss = float("inf")
    for epoch in range(opt.epochs):

        """
        下面是一些用來記錄當前網絡運行狀態的參數
        """
        train_loss = 0
        val_loss = 0
        # train_iou = 0
        # val_iou = 0
        # train_object_num = 0
        # val_object_num = 0
        train_loss_coord = 0
        val_loss_coord = 0
        train_loss_confidence = 0
        val_loss_confidence = 0
        train_loss_classes = 0
        val_loss_classes = 0

        log_loss_mean_train = 0.
        # log_loss_mean_val = 0.
        net.train()
        print("正在進行第{}輪訓練".format(epoch + 1))
        for i, data in enumerate(train_loader):

            # forward
            inputs, labels = data
            inputs, labels = inputs.float().to(device), labels.float().to(device)
            outputs = net(inputs)
            optimizer.zero_grad()

            loss = loss_func(bounding_boxes=outputs, ground_truth=labels,batch_size = opt.batch_size )
            batch_loss = loss[0]
            batch_loss.backward()
            optimizer.step()
            log_loss_mean_train+=batch_loss
            train_loss+=batch_loss
            train_loss_coord+=loss[1]
            train_loss_confidence+=loss[2]
            train_loss_classes+=loss[3]

            # train_iou+=train_iou+loss[4]
            # train_object_num+=loss[5]

            # update weights


            if (i + 1) % opt.log_interval == 0:
                log_loss_mean_train = log_loss_mean_train / opt.log_interval

                info = "訓練:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f}" 
                    .format 
                        (
                        epoch, opt.epochs, i + 1, len(train_loader), log_loss_mean_train
                    )
                print(info, file=fo)

                if (opt.show_log_console):
                    info_print = "33[0;33;0m" + info + "33[0m"
                    print(info_print)

                log_loss_mean_train = 0.0
        #總體損失
        TotalLoss+=train_loss
        # tensorboard 繪圖
        if (wirter):

            wirter.add_scalar("總體損失值",TotalLoss,epoch)
            wirter.add_scalar("每輪損失值",train_loss,epoch)
            wirter.add_scalar("每輪預測預測框損失值",train_loss_coord,epoch)
            wirter.add_scalar("每輪預測框置信度損失",train_loss_confidence,epoch)
            wirter.add_scalar("每輪預測類別損失值",train_loss_classes,epoch)


        # 保存效果最好的玩意
        if (train_loss < Best_loss):
            Best_weight = net.state_dict()
            Best_loss = train_loss

        scheduler.step()  # 更新學習率

        # 2.2 進入訓練對比階段

        if (epoch + 1) % opt.val_interval == 0:

            """
            這部分和訓練的那部分是類似的,可以忽略這部分的代碼
            """

            net.eval()
            with torch.no_grad():
                for j, data in enumerate(valid_loader):
                    inputs, labels = data
                    inputs, labels = inputs.float().to(device), labels.float().to(device)
                    outputs = net(inputs)
                    loss = loss_func(outputs, labels)

                    batch_loss = loss[0]
                    # log_loss_mean_val += batchLoss
                    val_loss += batch_loss
                    val_loss_coord += loss[1]
                    val_loss_confidence += loss[2]
                    val_loss_classes += loss[3]
                    # val_iou += train_iou + loss[4]
                    # val_object_num += loss[5]

                info_val = "測試:	Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} ".format 
                        (
                        epoch, opt.epochs, (j+1), len(valid_loader), val_loss
                    )
                print(info_val, file=fo)
                if (opt.show_log_console):
                    info_print_val = "33[0;31;0m" + info_val + "33[0m"
                    print(info_print_val)
                ValLoss+=val_loss
                if (wirter):
                    wirter.add_scalar("測試總體損失",ValLoss, (ValTime))
                    wirter.add_scalar("每次測試總損失總值", val_loss, (ValTime))
                    wirter.add_scalar("每輪測試預測框損失值", val_loss_coord, ValTime)
                    wirter.add_scalar("每輪測試預測框置信度損失", val_loss_confidence, ValTime)
                    wirter.add_scalar("每輪測試預測類別損失值", val_loss_classes, ValTime)
                ValTime+=1



    # 最后一次的權重
    Last_weight = net.state_dict()

    # 保存模型
    SaveModel.Save_Model(EPX_Path, Best_weight, Last_weight)

    fo.close()
    if (wirter):
        print("tensorboard dir is:", path_board)
    wirter.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--epochs', type=int, default=300)
    parser.add_argument('--batch-size', type=int, default=4)
    parser.add_argument('--lr', type=float, default=0.01)
    #每5個batch輸出一次結果
    parser.add_argument('--log_interval', type=int, default=2)
    # 訓練幾輪測試一次
    parser.add_argument('--val_interval', type=int, default=10)
    parser.add_argument('--train_dir_image', type=str, default='')
    parser.add_argument('--train_dir_Ann', type=str, default='')
    parser.add_argument('--valid_dir_image', type=str, default='')
    parser.add_argument('--valid_dir_Ann', type=str, default='')
    # 如果是Mac系注意這個參數可能需要設置為1,本地訓練,不推薦MAC
    parser.add_argument('--works', type=int, default=0)
    parser.add_argument('--show_log_console', type=bool, default=True)
    parser.add_argument('--device', type=str, default="cpu", help="默認使用顯卡加速訓練參數選擇:0,1,2...or cpu")
    parser.add_argument('--tensorboardopen', type=bool, default=True)
    opt = parser.parse_args()
    train()

    # tensorboard --logdir = runs/train/epx2/logs

分類與目標檢測

之后就是咱們的后處理階段,其實也就是咱們的使用部分。

這里也是兩個部分,一個是圖片分類的實現,還有一個就是咱們目標檢測的實現。

圖片分類

這里面也是兩個部分,一個是預訓練模型,進行前向傳播,還有一個是進行識別后的處理。

import argparse
from PIL import Image
from Utils.DataSet.MyDataSet import MyDataSet
from Utils.DataSet.TransformAtions import TransFormAtions
import argparse
import torch
from torch.utils.data import DataLoader
from Models.FeatureNet import YOLOFeature
from Config.ConfigPre import *
import outProcessClassfiy
def detect():


    ways = opt.valid_imgs
    transformations = TransFormAtions()

    net = YOLOFeature(Classes)
    state_dict_load = torch.load(opt.path_state_dict)
    net.load_state_dict(state_dict_load)

    if(ways):

        test_data = MyDataSet(data_dir=opt.valid_dir, transform=transformations.valid_transform,ClassesName=ClassesName)
        valid_loader = DataLoader(dataset=test_data, batch_size=1)

        net.eval()
        with torch.no_grad():
            for i, data in enumerate(valid_loader):
                # forward
                inputs, labels = data
                outputs = net(inputs)
                _, predicted = torch.max(outputs.data, 1)
                # 輸出處理器

                outProcessClassfiy.Function(predicted.numpy()[0])
    else:
        #指定的是單張圖片,少給我來奇奇怪怪的輸入,這個版本容錯很差滴!!!
        path_img = opt.valid_dir
        if(".jpg" not in path_img):
            raise Exception("小爺打不開這圖片")
        image = Image.open(path_img)
        image = transformations.valid_transform(image)
        image = torch.reshape(image, (1, 3, 32, 32))

        net.eval()
        with torch.no_grad():
            out = net(image)

            outProcessClassfiy.Function(out.argmax(1).item())


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    # False表示識別單張圖片,True表示多張圖片,此時指定路徑即可。
    parser.add_argument('--valid_imgs',type=bool,default=False)
    parser.add_argument('--valid_dir', type=str, default=r'F:projectsPythonProjectHuLookDataPreData	rain貓羽雫1.jpg')
    parser.add_argument('--path_state_dict', type=str, default=r'runs	rainpreepx0weightsest.pth')
    opt = parser.parse_args()
    detect()

之后是咱們的后處理

from Config.ConfigPre import *


def Function(out):
    print("類別為:", ClassesName[out])

e3d90322-9078-11ef-a511-92fbcf53809c.jpg

目標檢測

這個也是類似的,但是的話,這里就不去拆什么后置處理器了哈
那么這里要注意的就是編碼的時候opencv是不支持中文的,解決方案的話也不難,需要自己準備一個字體文件就完了,當然咱們的項目工程里面是帶了一個的。

e406ea4e-9078-11ef-a511-92fbcf53809c.jpg

import cv2
import torchvision.transforms as transforms
from Models.Yolo import YOLO
import argparse
import torch
from Config.ConfigTrain import *
import numpy as np
from PIL import Image,ImageDraw,ImageFont

def iou(box_one, box_two):
    LX = max(box_one[0], box_two[0])
    LY = max(box_one[1], box_two[1])
    RX = min(box_one[2], box_two[2])
    RY = min(box_one[3], box_two[3])
    if LX >= RX or LY >= RY:
        return 0
    return (RX - LX) * (RY - LY) / ((box_one[2]-box_one[0]) * (box_one[3] - box_one[1]) + (box_two[2]-box_two[0]) * (box_two[3] - box_two[1]))


def NMS(bounding_boxes,S=7,B=2,img_size=448,confidence_threshold=0.5,iou_threshold=0.0,possible_pred=0.4):
    bounding_boxes = bounding_boxes.cpu().detach().numpy().tolist()
    predict_boxes = []
    nms_boxes = []
    grid_size = img_size / S
    for batch in range(len(bounding_boxes)):
        for i in range(S):
            for j in range(S):
                gridX = grid_size * j
                gridY = grid_size * i
                if bounding_boxes[batch][i][j][4] < bounding_boxes[batch][i][j][9]:
                    bounding_box = bounding_boxes[batch][i][j][5:10]
                else:
                    bounding_box = bounding_boxes[batch][i][j][0:5]
                class_possible = (bounding_boxes[batch][i][j][10:])
                bounding_box.extend(class_possible)
                possible = max(class_possible)
                if (bounding_box[4] < confidence_threshold

                    ):
                    continue

                if(bounding_box[4]*possible < possible_pred):
                    continue
                # print(bounding_box[4]*possible)

                centerX = (int)(gridX + bounding_box[0] * grid_size)
                centerY = (int)(gridY + bounding_box[1] * grid_size)
                width = (int)(bounding_box[2] * img_size)
                height = (int)(bounding_box[3] * img_size)
                bounding_box[0] = max(0, (int)(centerX - width / 2))
                bounding_box[1] = max(0, (int)(centerY - height / 2))
                bounding_box[2] = min(img_size - 1, (int)(centerX + width / 2))
                bounding_box[3] = min(img_size - 1, (int)(centerY + height / 2))
                predict_boxes.append(bounding_box)

        while len(predict_boxes) != 0:
            predict_boxes.sort(key=lambda box:box[4])
            assured_box = predict_boxes[0]
            temp = []
            classIndex = np.argmax(assured_box[5:])
            #print("類別:{}".format(ClassesName[classIndex))
            assured_box[4] = assured_box[4] * assured_box[5 + classIndex]
            #修正置信度為 物體分類準確度 × 含有物體的置信度
            assured_box[5] = classIndex
            nms_boxes.append(assured_box)
            i = 1
            while i < len(predict_boxes):
                if iou(assured_box,predict_boxes[i]) <= iou_threshold:
                    temp.append(predict_boxes[i])
                i = i + 1
            predict_boxes = temp

        return nms_boxes

def detect():
    transform = transforms.Compose([
        transforms.ToTensor(),  # height * width * channel -> channel * height * width
        transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))
    ])

    image_dir = opt.valid_dir
    img_data = cv2.imread(image_dir)
    img_data = cv2.resize(img_data, (448, 448), interpolation=cv2.INTER_AREA)
    train_data = transform(img_data)
    train_data = train_data.unsqueeze(0)

    net = YOLO(B=2,classes_num=Classes)
    state_dict_load = torch.load(opt.path_state_dict)
    net.load_state_dict(state_dict_load)

    net.eval()
    with torch.no_grad():
        bounding_boxes = net(train_data)

    NMS_boxes = NMS(bounding_boxes,confidence_threshold=opt.confidence,iou_threshold=opt.iou,possible_pred=opt.possible_pre)
    font = ImageFont.truetype(r'font/simsun.ttc', 20, encoding='utf-8')

    for box in NMS_boxes:

        img_data = cv2.rectangle(img_data, (box[0], box[1]), (box[2], box[3]), (0, 255, 0), 1)
        """
         處理中文
        """
        pil_img = Image.fromarray(cv2.cvtColor(img_data, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_img)

        draw.text((box[0], box[1]),"{}:{}".format(ClassesName[box[5]], round(box[4], 2)),(148,175,100),font)

        print("class_name:{} confidence:{}".format(ClassesName[int(box[5])],round(box[4],2)))

        img_data = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)

    if(opt.show_img):

        cv2.imshow("img_detection", img_data)
        cv2.waitKey()
        cv2.destroyAllWindows()
    if(opt.save_dir):
        cv2.imwrite(opt.save_dir, img_data)




if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--valid_dir', type=str, default=r'F:projectsPythonProjectHuLookDataDetData	rainimages02.jpg')
    parser.add_argument('--path_state_dict', type=str, default=r'F:projectsPythonProjectHuLook
uns	raindetectepx0weightsest.pth')
    parser.add_argument("--iou",type=float,default=0.2)
    parser.add_argument("--confidence",type=float,default=0.5)
    parser.add_argument("--possible_pre",type=float,default=0.35)
    parser.add_argument("--show_img",type=bool,default=True)
    parser.add_argument("--save_dir",type=str,default="")
    opt = parser.parse_args()
    detect()

項目獲取

那么整個玩意咱們就搞定了,考慮到特殊原因,項目上傳至碼云:https://gitee.com/Huterox/hu-look

此外由于咱們訓練出來的權重文件太大了,所以這理的話就不上傳入權重文件了。

e422a806-9078-11ef-a511-92fbcf53809c.jpg

當然其實還有一個原因是,咱們的這個權重文件只是用來做測試的,所以實際的意義不大。

不過你以為這就完了嘛,不,接下來是咱們的這個玩意如何使用!

項目使用

預訓練數據集

這個的話其實可以考慮省去,我們可以選擇直接訓練,問題不大。

這個預訓練數據集就和前面說的一樣,按照類別放在不同的文件夾下面。

e43b3150-9078-11ef-a511-92fbcf53809c.jpg

例如我這里準備這幾種圖片:

e464f332-9078-11ef-a511-92fbcf53809c.jpg

e489feb6-9078-11ef-a511-92fbcf53809c.jpg

(我這個是用于測試的數據集所以很小,就幾十張圖片)

預訓練

這部分的話需要打開配置

e4a73472-9078-11ef-a511-92fbcf53809c.jpg

e4bf9cd8-9078-11ef-a511-92fbcf53809c.jpg

配置一下就好了

當然在訓練文件當中也是可以配置的

e4eccd2a-9078-11ef-a511-92fbcf53809c.jpg

訓練完畢后,你可以打開tensorboad

我們的訓練過程當中的數據都在這兒

e50f406c-9078-11ef-a511-92fbcf53809c.jpg

e52bdb6e-9078-11ef-a511-92fbcf53809c.jpg

之后的話,預訓練完之后,這個網絡是具備圖片分類功能的,可以使用

e548461e-9078-11ef-a511-92fbcf53809c.jpg

進行圖片分類。

不過這里注意的是,預訓練的只是一個用于分類的網絡,目的為了讓骨干網絡具備權重。所以準備的數據集最好是一張圖片里面只有一個目標,因為那玩意只是用來分類的。

目標檢測數據集

這部分的話就是咱們的voc數據集,和正常的一樣就可以了,咱們可以直接使用labelimg進行標注。
那個怎么使用前面的博客有,那么在咱們這里的話還是需要手動劃分一下訓練集和驗證集的。

e561c7d8-9078-11ef-a511-92fbcf53809c.jpg

然后里面的內容就和voc一樣了

e5815152-9078-11ef-a511-92fbcf53809c.jpg

e5a3f5cc-9078-11ef-a511-92fbcf53809c.jpg

訓練目標檢測

之后就是咱們的訓練
還是先到配置處

e5ccc39e-9078-11ef-a511-92fbcf53809c.jpg

e5e1451c-9078-11ef-a511-92fbcf53809c.jpg

之后打開tensorboard

tensorboard --logdir=runs/traindetect/epx0/logs

e5fe027e-9078-11ef-a511-92fbcf53809c.jpg

識別

這個就不用我說了,打開detect

e625b256-9078-11ef-a511-92fbcf53809c.jpg

我們可以看到這識別的情況

e639e942-9078-11ef-a511-92fbcf53809c.jpg

這里的話由于咱們的數據集太那啥了,而且數據集本身設置的就不好,所以導致這里的效果也不好,同時這其實我不上傳權重的原因之一,只是用來做測試的。

總結

以上就是全部內容了,全網應該找不到比這個還全的了吧?

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 目標檢測
    +關注

    關注

    0

    文章

    209

    瀏覽量

    15611
  • pytorch
    +關注

    關注

    2

    文章

    808

    瀏覽量

    13225

原文標題:近兩萬字長文,從理論到實現!手把手教你如何自制目標檢測框架

文章出處:【微信號:vision263com,微信公眾號:新機器視覺】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    源碼開放 智能監測電源管理教程寶典!

    源碼開放,今天我們學習的是電源管理系統的核心功能模塊,手把手教你如何通過不同的技術手段實現有效的電源管理。
    的頭像 發表于 12-11 09:26 ?242次閱讀
    源碼開放  智能監測電源管理教程寶典!

    Air780E模組LuatOS開發實戰 —— 手把手教你搞定數據打包解包

    本文要說的是低功耗4G模組Air780E的LuatOS開發實戰,我將手把手教你搞定數據打包解包。
    的頭像 發表于 12-03 11:17 ?172次閱讀
    Air780E模組LuatOS開發實戰 —— <b class='flag-5'>手把手</b><b class='flag-5'>教你</b>搞定數據打包解包

    第14章-藍牙遙控小車 藍牙串口通訊講解藍牙APP遙控小車 藍牙串口通訊講解

    第14章-藍牙遙控小車 手把手做藍牙APP遙控小車 藍牙串口通訊講解
    的頭像 發表于 08-21 16:24 ?733次閱讀
    第14章-藍牙遙控小車 藍牙串口通訊講解藍牙APP遙控小車 藍牙串口通訊講解

    手把手教你通過宏集物聯網工控屏&amp;網關進行協議轉換,將底層PLC/傳感器的數據轉換為TCP協議并傳輸到用戶

    手把手教你通過宏集物聯網工控屏&網關進行協議轉換,將底層PLC/傳感器的數據轉換為TCP協議并傳輸到用戶終端
    的頭像 發表于 08-15 13:29 ?518次閱讀
    <b class='flag-5'>手把手</b><b class='flag-5'>教你</b>通過宏集物聯網工控屏&amp;網關進行協議轉換,將底層PLC/傳感器的數據轉換為TCP協議并傳輸到用戶

    手把手教你在orcad中設置CIS元器件數據庫,提高工作效率

    元器件數據庫,就是實現上述查找元件、放置元件時所需要調用的數據庫。本文將手把手教你如何在orcad中配置CIS元器件數據庫。
    的頭像 發表于 06-15 17:27 ?6237次閱讀
    <b class='flag-5'>手把手</b><b class='flag-5'>教你</b>在orcad中設置CIS元器件數據庫,提高工作效率

    手把手教你使用物模型連接DDSU電表

    物模型其實就是云平臺對產品功能的數字化描述。以“燈”為例,最簡單的“燈”具有“開”和“關”屬性,只需要在平臺定義一個布爾量的數據點位,有些高級的“燈”還具有“亮度”、“色溫”、“顏色”等屬性,可以和簡單“燈”一樣定義多個屬性描述,也可以定義一個結構體,下圖就是基于阿里云“物聯網平臺”定義的兩種“燈具”舉例。利用物模型規范數據傳輸的格式更好的整合和管理多樣化的
    的頭像 發表于 06-14 08:21 ?429次閱讀
    <b class='flag-5'>手把手</b><b class='flag-5'>教你</b>使用物模型連接DDSU電表

    手把手教你排序算法怎么寫

    今天以直接插入排序算法,給大家分享一下排序算法的實現思路,主要包含以下部分內容:插入排序介紹插入排序算法實現手把手教你排序算法怎么寫在添加新的記錄時,使用順序查找的方式找到其要插入的位置,然后將
    的頭像 發表于 06-04 08:03 ?690次閱讀
    <b class='flag-5'>手把手</b><b class='flag-5'>教你</b>排序算法怎么寫

    手把手帶你移植HAL庫函數

    開發者更高效地進行嵌入式開發。手把手帶你移植HAL庫函數HAL庫提供了一套抽象接口,使開發者無需直接操作底層硬件寄存器,就能實現對硬件的控制。這種抽象使得代碼能夠更
    的頭像 發表于 05-18 08:04 ?1908次閱讀
    <b class='flag-5'>手把手</b>帶你移植HAL庫函數

    手把手教你制作高速吹風機

    前言: 高速吹風 機 量價齊升 市場競爭格局初顯 吹風機是居家生活必備物品,然而傳統型吹風機所帶來的體驗并不佳,高頻使用的女性群體對此更是深有感觸。究其原因主要有:轉速低,通常在每分鐘2萬轉左右,導致干發速度慢;高溫干發,容易損傷頭發;噪聲大且體積笨重等等。因此,能改善這些問題的高速吹風機一經推出便迅速風靡市場、發展迅猛,品牌數量與產品型號均呈加速增長態勢。據統計,2020年僅有6個品牌生產高速吹風機,共16款機型;
    發表于 03-28 09:22 ?819次閱讀
    <b class='flag-5'>手把手</b><b class='flag-5'>教你</b>制作高速吹風機

    無刷電機無感FOC控制培訓系列課程

    | 本工作室推出電機控制無感foc電機控制系列培訓課程本課程主要讓想進階的算法工程師,和剛參加工作的工程師或者在校學生能夠進一步提高自己的技能,1.從企業用人角度手把手教你做電機控制,提高你的個人
    發表于 03-10 13:52

    【先楫HPM5361EVK開發板試用體驗】(原創)6.手把手實戰紅外線傳感器源代碼

    試用體驗】2手把手實戰密鑰管理器 KEYM 【先楫HPM5361EVK開發板試用體驗】3手把手實戰安全數據處理器 SDP 【先楫HPM5361EVK開發板試用體驗】4手把手實戰EXIP在線解密引擎 【先
    發表于 02-09 15:08

    【先楫HPM5361EVK開發板試用體驗】(原創)5.手把手實戰AI機械臂

    HPMicro 【先楫HPM5361EVK開發板試用體驗】2手把手實戰密鑰管理器 KEYM 【先楫HPM5361EVK開發板試用體驗】3手把手實戰安全數據處理器 SDP 【先楫HPM5361EVK開發板
    發表于 02-06 10:28

    【飛騰派4G版免費試用】4.手把手玩轉QT界面設計

    完成了使用Qt Designer進行界面設計的全部流程!是不是覺得像魔法一樣神奇呢?趕緊試試吧! 接上三篇: 【飛騰派4G版免費試用】1.實戰交叉編譯環境搭建和手把手uboot編譯 【飛騰派4G版免費
    發表于 01-27 12:49

    工程送樣!手把手教你用好廣和通RedCap模組FG131&amp;amp;FG132系列

    工程送樣!手把手教你用好廣和通RedCap模組FG131&FG132系列
    的頭像 發表于 01-11 18:22 ?704次閱讀
    工程送樣!<b class='flag-5'>手把手</b><b class='flag-5'>教你</b>用好廣和通RedCap模組FG131&amp;amp;FG132系列

    【飛騰派4G版免費試用】3.手把手玩轉制作rootfs根文件系統

    接上兩篇:【飛騰派4G版免費試用】1.實戰交叉編譯環境搭建和手把手uboot編譯 【飛騰派4G版免費試用】2.手把手實戰編譯Linux內核 嗨,親愛的工程師、學生和愛好者們,我來啦!今天我要帶
    發表于 01-09 10:49
    主站蜘蛛池模板: 免费一级特黄视频| 黄网页在线观看| 爱爱小视频免费| 久久免费国产| 卡一卡二卡三国色天香永不失联| 三级在线观看视频网站| 成年女人毛片免费视频| 91极品女神嫩模在线播放| 一级做a爰片久久毛片免费 | 婷婷九月丁香| 国产免费成人在线视频| 国产高清一级视频在线观看| 丁香六月激情网| 色噜噜噜噜色| 久久人成| 97玖玖| 高清性欧美xxx| 色鬼久久| 神马国产| 亚洲激情| china3p单男精品自拍| 久久天天躁狠狠躁夜夜| 一级毛片q片| bt天堂资源| 亚洲网站免费| 久久九九国产精品怡红院| 国产一级在线观看| 四虎永久在线精品| 天天曰天天爽| 69xxx欧美| 免费番茄社区性色大片| 亚洲天天做日日做天天欢毛片| 亚洲国产人久久久成人精品网站| 男女爱爱爽爽福利免费视频| 国产精品久久福利网站| 四虎精品影院4hutv四虎| 天天草夜夜骑| 视频一区亚洲| 国产在线精品美女观看| 久操视频网站| 手机看片a永久免费看大片|