python神經(jīng)網(wǎng)絡Keras搭建RFBnet目標檢測平臺
什么是RFBnet目標檢測算法
RFBnet是SSD的一種加強版,主要是利用了膨脹卷積這一方法增大了感受野,相比于普通的ssd,RFBnet也是一種加強吧
RFBnet是改進版的SSD,其整體的結(jié)構與SSD相差不大,其主要特點是在SSD的特征提取網(wǎng)絡上用了RFB模塊。
RFB的全稱Receptive Field Block,是一種輕量級的、而且集成了各類檢測算法優(yōu)點的模塊,結(jié)合了Inception、蟲洞卷積的思想,以提高感受野的方式提高網(wǎng)絡的特征提取能力。

RFBnet實現(xiàn)思路
一、預測部分
1、主干網(wǎng)絡介紹

RFBnet采用的主干網(wǎng)絡是VGG網(wǎng)絡,關于VGG的介紹大家可以看我的另外一篇博客http://www.dbjr.com.cn/article/246917.htm,這里的VGG網(wǎng)絡相比普通的VGG網(wǎng)絡有一定的修改,主要修改的地方就是:
1、將VGG16的FC6和FC7層轉(zhuǎn)化為卷積層。
2、增加了RFB模塊。
主要使用到的RFB模塊有兩種,一種是BasicRFB,另一種是BasicRFB_a。
二者使用的思想相同,構造有些許不同。
BasicRFB的結(jié)構如下:

BasicRFB_a和BasicRFB類似,并聯(lián)結(jié)構增加,有8個并聯(lián)。
實現(xiàn)代碼:
from keras.layers import (Activation, BatchNormalization, Conv2D, Lambda,
MaxPooling2D, UpSampling2D, concatenate)
def conv2d_bn(x,filters,num_row,num_col,padding='same',stride=1,dilation_rate=1,relu=True):
x = Conv2D(
filters, (num_row, num_col),
strides=(stride,stride),
padding=padding,
dilation_rate=(dilation_rate, dilation_rate),
use_bias=False)(x)
x = BatchNormalization()(x)
if relu:
x = Activation("relu")(x)
return x
def BasicRFB(x,input_filters,output_filters,stride=1,map_reduce=8):
#-------------------------------------------------------#
# BasicRFB模塊是一個殘差結(jié)構
# 主干部分使用不同膨脹率的卷積進行特征提取
# 殘差邊只包含一個調(diào)整寬高和通道的1x1卷積
#-------------------------------------------------------#
input_filters_div = input_filters//map_reduce
branch_0 = conv2d_bn(x, input_filters_div*2, 1, 1, stride=stride)
branch_0 = conv2d_bn(branch_0, input_filters_div*2, 3, 3, relu=False)
branch_1 = conv2d_bn(x, input_filters_div, 1, 1)
branch_1 = conv2d_bn(branch_1, input_filters_div*2, 3, 3, stride=stride)
branch_1 = conv2d_bn(branch_1, input_filters_div*2, 3, 3, dilation_rate=3, relu=False)
branch_2 = conv2d_bn(x, input_filters_div, 1, 1)
branch_2 = conv2d_bn(branch_2, (input_filters_div//2)*3, 3, 3)
branch_2 = conv2d_bn(branch_2, input_filters_div*2, 3, 3, stride=stride)
branch_2 = conv2d_bn(branch_2, input_filters_div*2, 3, 3, dilation_rate=5, relu=False)
branch_3 = conv2d_bn(x, input_filters_div, 1, 1)
branch_3 = conv2d_bn(branch_3, (input_filters_div//2)*3, 1, 7)
branch_3 = conv2d_bn(branch_3, input_filters_div*2, 7, 1, stride=stride)
branch_3 = conv2d_bn(branch_3, input_filters_div*2, 3, 3, dilation_rate=7, relu=False)
#-------------------------------------------------------#
# 將不同膨脹率的卷積結(jié)果進行堆疊
# 利用1x1卷積調(diào)整通道數(shù)
#-------------------------------------------------------#
out = concatenate([branch_0,branch_1,branch_2,branch_3],axis=-1)
out = conv2d_bn(out, output_filters, 1, 1, relu=False)
#-------------------------------------------------------#
# 殘差邊也需要卷積,才可以相加
#-------------------------------------------------------#
short = conv2d_bn(x, output_filters, 1, 1, stride=stride, relu=False)
out = Lambda(lambda x: x[0] + x[1])([out,short])
out = Activation("relu")(out)
return out
def BasicRFB_a(x, input_filters, output_filters, stride=1, map_reduce=8):
#-------------------------------------------------------#
# BasicRFB_a模塊也是一個殘差結(jié)構
# 主干部分使用不同膨脹率的卷積進行特征提取
# 殘差邊只包含一個調(diào)整寬高和通道的1x1卷積
#-------------------------------------------------------#
input_filters_div = input_filters//map_reduce
branch_0 = conv2d_bn(x,input_filters_div,1,1,stride=stride)
branch_0 = conv2d_bn(branch_0,input_filters_div,3,3,relu=False)
branch_1 = conv2d_bn(x,input_filters_div,1,1)
branch_1 = conv2d_bn(branch_1,input_filters_div,3,1,stride=stride)
branch_1 = conv2d_bn(branch_1,input_filters_div,3,3,dilation_rate=3,relu=False)
branch_2 = conv2d_bn(x,input_filters_div,1,1)
branch_2 = conv2d_bn(branch_2,input_filters_div,1,3,stride=stride)
branch_2 = conv2d_bn(branch_2,input_filters_div,3,3,dilation_rate=3,relu=False)
branch_3 = conv2d_bn(x,input_filters_div,1,1)
branch_3 = conv2d_bn(branch_3,input_filters_div,3,1,stride=stride)
branch_3 = conv2d_bn(branch_3,input_filters_div,3,3,dilation_rate=5,relu=False)
branch_4 = conv2d_bn(x,input_filters_div,1,1)
branch_4 = conv2d_bn(branch_4,input_filters_div,1,3,stride=stride)
branch_4 = conv2d_bn(branch_4,input_filters_div,3,3,dilation_rate=5,relu=False)
branch_5 = conv2d_bn(x,input_filters_div//2,1,1)
branch_5 = conv2d_bn(branch_5,(input_filters_div//4)*3,1,3)
branch_5 = conv2d_bn(branch_5,input_filters_div,3,1,stride=stride)
branch_5 = conv2d_bn(branch_5,input_filters_div,3,3,dilation_rate=7,relu=False)
branch_6 = conv2d_bn(x,input_filters_div//2,1,1)
branch_6 = conv2d_bn(branch_6,(input_filters_div//4)*3,3,1)
branch_6 = conv2d_bn(branch_6,input_filters_div,1,3,stride=stride)
branch_6 = conv2d_bn(branch_6,input_filters_div,3,3,dilation_rate=7,relu=False)
#-------------------------------------------------------#
# 將不同膨脹率的卷積結(jié)果進行堆疊
# 利用1x1卷積調(diào)整通道數(shù)
#-------------------------------------------------------#
out = concatenate([branch_0,branch_1,branch_2,branch_3,branch_4,branch_5,branch_6],axis=-1)
out = conv2d_bn(out, output_filters, 1, 1, relu=False)
#-------------------------------------------------------#
# 殘差邊也需要卷積,才可以相加
#-------------------------------------------------------#
short = conv2d_bn(x, output_filters, 1, 1, stride=stride, relu=False)
out = Lambda(lambda x: x[0] + x[1])([out, short])
out = Activation("relu")(out)
return out
#--------------------------------#
# 取Conv4_3和fc7進行特征融合
#--------------------------------#
def Normalize(net):
# 38,38,512 -> 38,38,256
branch_0 = conv2d_bn(net["conv4_3"], 256, 1, 1)
# 19,19,512 -> 38,38,256
branch_1 = conv2d_bn(net['fc7'], 256, 1, 1)
branch_1 = UpSampling2D()(branch_1)
# 38,38,256 + 38,38,256 -> 38,38,512
out = concatenate([branch_0,branch_1],axis=-1)
# 38,38,512 -> 38,38,512
out = BasicRFB_a(out,512,512)
return out
def backbone(input_tensor):
#----------------------------主干特征提取網(wǎng)絡開始---------------------------#
# RFB結(jié)構,net字典
net = {}
# Block 1
net['input'] = input_tensor
# 300,300,3 -> 150,150,64
net['conv1_1'] = Conv2D(64, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv1_1')(net['input'])
net['conv1_2'] = Conv2D(64, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv1_2')(net['conv1_1'])
net['pool1'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
name='pool1')(net['conv1_2'])
# Block 2
# 150,150,64 -> 75,75,128
net['conv2_1'] = Conv2D(128, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv2_1')(net['pool1'])
net['conv2_2'] = Conv2D(128, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv2_2')(net['conv2_1'])
net['pool2'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
name='pool2')(net['conv2_2'])
# Block 3
# 75,75,128 -> 38,38,256
net['conv3_1'] = Conv2D(256, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv3_1')(net['pool2'])
net['conv3_2'] = Conv2D(256, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv3_2')(net['conv3_1'])
net['conv3_3'] = Conv2D(256, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv3_3')(net['conv3_2'])
net['pool3'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
name='pool3')(net['conv3_3'])
# Block 4
# 38,38,256 -> 19,19,512
net['conv4_1'] = Conv2D(512, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv4_1')(net['pool3'])
net['conv4_2'] = Conv2D(512, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv4_2')(net['conv4_1'])
net['conv4_3'] = Conv2D(512, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv4_3')(net['conv4_2'])
net['pool4'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
name='pool4')(net['conv4_3'])
# Block 5
# 19,19,512 -> 19,19,512
net['conv5_1'] = Conv2D(512, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv5_1')(net['pool4'])
net['conv5_2'] = Conv2D(512, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv5_2')(net['conv5_1'])
net['conv5_3'] = Conv2D(512, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv5_3')(net['conv5_2'])
net['pool5'] = MaxPooling2D((3, 3), strides=(1, 1), padding='same',
name='pool5')(net['conv5_3'])
# FC6
# 19,19,512 -> 19,19,1024
net['fc6'] = Conv2D(1024, kernel_size=(3,3), dilation_rate=(6, 6),
activation='relu', padding='same',
name='fc6')(net['pool5'])
# x = Dropout(0.5, name='drop6')(x)
# FC7
# 19,19,1024 -> 19,19,1024
net['fc7'] = Conv2D(1024, kernel_size=(1,1), activation='relu',
padding='same', name='fc7')(net['fc6'])
#----------------------------------------------------------#
# conv4_3 38,38,512 -> 38,38,512 net['norm']
# fc7 19,19,1024 ->
#----------------------------------------------------------#
net['norm'] = Normalize(net)
# 19,19,1024 -> 19,19,1024
net['rfb_1'] = BasicRFB(net['fc7'],1024,1024)
# 19,19,1024 -> 10,10,512
net['rfb_2'] = BasicRFB(net['rfb_1'],1024,512,stride=2)
# 10,10,512 -> 5,5,256
net['rfb_3'] = BasicRFB(net['rfb_2'],512,256,stride=2)
# 5,5,256 -> 5,5,128
net['conv6_1'] = conv2d_bn(net['rfb_3'],128,1,1)
# 5,5,128 -> 3,3,256
net['conv6_2'] = conv2d_bn(net['conv6_1'],256,3,3,padding="valid")
# 3,3,256 -> 3,3,128
net['conv7_1'] = conv2d_bn(net['conv6_2'],128,1,1)
# 3,3,128 -> 1,1,256
net['conv7_2'] = conv2d_bn(net['conv7_1'],256,3,3,padding="valid")
return net
2、從特征獲取預測結(jié)果

由上圖我們可以知道,我們?nèi)onv4的第三次卷積的特征、fc7的特征進行組合后經(jīng)過一個BasicRFB_a獲得P3作為有效特征層、還有上圖的P4、P5、P6、P7、P8作為有效特征層,為了和普通特征層區(qū)分,我們稱之為有效特征層,來獲取預測結(jié)果。
對獲取到的每一個有效特征層,我們分別對其進行一次num_anchors x 4的卷積、一次num_anchors x num_classes的卷積。而num_anchors指的是該特征層所擁有的先驗框數(shù)量。
其中:num_anchors x 4的卷積 用于預測 該特征層上 每一個網(wǎng)格點上 每一個先驗框的變化情況。(為什么說是變化情況呢,這是因為ssd的預測結(jié)果需要結(jié)合先驗框獲得預測框,預測結(jié)果就是先驗框的變化情況。)
num_anchors x num_classes的卷積 用于預測 該特征層上 每一個網(wǎng)格點上 每一個預測框?qū)姆N類。
每一個有效特征層對應的先驗框?qū)撎卣鲗由?每一個網(wǎng)格點上 預先設定好的三個框。
所有的特征層對應的預測結(jié)果的shape如下:

實現(xiàn)代碼為:
from keras.layers import (Activation, Concatenate, Conv2D, Flatten, Input,
Reshape)
from keras.models import Model
from nets.backbone import backbone
def RFB300(input_shape, num_classes=21):
#---------------------------------#
# 典型的輸入大小為[300,300,3]
#---------------------------------#
input_tensor = Input(shape=input_shape)
# net變量里面包含了整個RFB的結(jié)構,通過層名可以找到對應的特征層
net = backbone(input_tensor)
#-----------------------將提取到的主干特征進行處理---------------------------#
# 對conv4_3的通道進行l(wèi)2標準化處理
# 38,38,512
num_anchors = 6
# 預測框的處理
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,4是x,y,h,w的調(diào)整
net['norm_mbox_loc'] = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same', name='norm_mbox_loc')(net['norm'])
net['norm_mbox_loc_flat'] = Flatten(name='norm_mbox_loc_flat')(net['norm_mbox_loc'])
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,num_classes是所分的類
net['norm_mbox_conf'] = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='norm_mbox_conf')(net['norm'])
net['norm_mbox_conf_flat'] = Flatten(name='norm_mbox_conf_flat')(net['norm_mbox_conf'])
# 對rfb_1層進行處理
# 19,19,1024
num_anchors = 6
# 預測框的處理
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,4是x,y,h,w的調(diào)整
net['rfb_1_mbox_loc'] = Conv2D(num_anchors * 4, kernel_size=(3,3),padding='same',name='rfb_1_mbox_loc')(net['rfb_1'])
net['rfb_1_mbox_loc_flat'] = Flatten(name='rfb_1_mbox_loc_flat')(net['rfb_1_mbox_loc'])
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,num_classes是所分的類
net['rfb_1_mbox_conf'] = Conv2D(num_anchors * num_classes, kernel_size=(3,3),padding='same',name='rfb_1_mbox_conf')(net['rfb_1'])
net['rfb_1_mbox_conf_flat'] = Flatten(name='rfb_1_mbox_conf_flat')(net['rfb_1_mbox_conf'])
# 對rfb_2進行處理
# 10,10,512
num_anchors = 6
# 預測框的處理
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,4是x,y,h,w的調(diào)整
net['rfb_2_mbox_loc'] = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='rfb_2_mbox_loc')(net['rfb_2'])
net['rfb_2_mbox_loc_flat'] = Flatten(name='rfb_2_mbox_loc_flat')(net['rfb_2_mbox_loc'])
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,num_classes是所分的類
net['rfb_2_mbox_conf'] = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='rfb_2_mbox_conf')(net['rfb_2'])
net['rfb_2_mbox_conf_flat'] = Flatten(name='rfb_2_mbox_conf_flat')(net['rfb_2_mbox_conf'])
# 對rfb_3進行處理
# 5,5,256
num_anchors = 6
# 預測框的處理
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,4是x,y,h,w的調(diào)整
net['rfb_3_mbox_loc'] = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='rfb_3_mbox_loc')(net['rfb_3'])
net['rfb_3_mbox_loc_flat'] = Flatten(name='rfb_3_mbox_loc_flat')(net['rfb_3_mbox_loc'])
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,num_classes是所分的類
net['rfb_3_mbox_conf'] = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='rfb_3_mbox_conf')(net['rfb_3'])
net['rfb_3_mbox_conf_flat'] = Flatten(name='rfb_3_mbox_conf_flat')(net['rfb_3_mbox_conf'])
# 對conv6_2進行處理
# 3,3,256
num_anchors = 4
# 預測框的處理
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,4是x,y,h,w的調(diào)整
net['conv6_2_mbox_loc'] = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='conv6_2_mbox_loc')(net['conv6_2'])
net['conv6_2_mbox_loc_flat'] = Flatten(name='conv6_2_mbox_loc_flat')(net['conv6_2_mbox_loc'])
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,num_classes是所分的類
net['conv6_2_mbox_conf'] = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv6_2_mbox_conf')(net['conv6_2'])
net['conv6_2_mbox_conf_flat'] = Flatten(name='conv6_2_mbox_conf_flat')(net['conv6_2_mbox_conf'])
# 對conv7_2進行處理
# 1,1,256
num_anchors = 4
# 預測框的處理
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,4是x,y,h,w的調(diào)整
net['conv7_2_mbox_loc'] = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='conv7_2_mbox_loc')(net['conv7_2'])
net['conv7_2_mbox_loc_flat'] = Flatten(name='conv7_2_mbox_loc_flat')(net['conv7_2_mbox_loc'])
# num_anchors表示每個網(wǎng)格點先驗框的數(shù)量,num_classes是所分的類
net['conv7_2_mbox_conf'] = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv7_2_mbox_conf')(net['conv7_2'])
net['conv7_2_mbox_conf_flat'] = Flatten(name='conv7_2_mbox_conf_flat')(net['conv7_2_mbox_conf'])
# 將所有結(jié)果進行堆疊
net['mbox_loc'] = Concatenate(axis=1, name='mbox_loc')([net['norm_mbox_loc_flat'],
net['rfb_1_mbox_loc_flat'],
net['rfb_2_mbox_loc_flat'],
net['rfb_3_mbox_loc_flat'],
net['conv6_2_mbox_loc_flat'],
net['conv7_2_mbox_loc_flat']])
net['mbox_conf'] = Concatenate(axis=1, name='mbox_conf')([net['norm_mbox_conf_flat'],
net['rfb_1_mbox_conf_flat'],
net['rfb_2_mbox_conf_flat'],
net['rfb_3_mbox_conf_flat'],
net['conv6_2_mbox_conf_flat'],
net['conv7_2_mbox_conf_flat']])
# 11620,4
net['mbox_loc'] = Reshape((-1, 4), name='mbox_loc_final')(net['mbox_loc'])
# 11620,21
net['mbox_conf'] = Reshape((-1, num_classes), name='mbox_conf_logits')(net['mbox_conf'])
net['mbox_conf'] = Activation('softmax', name='mbox_conf_final')(net['mbox_conf'])
# 11620,25
net['predictions'] = Concatenate(axis =-1, name='predictions')([net['mbox_loc'], net['mbox_conf']])
model = Model(net['input'], net['predictions'])
return model
3、預測結(jié)果的解碼
我們通過對每一個特征層的處理,可以獲得三個內(nèi)容,分別是:
num_anchors x 4的卷積 用于預測 該特征層上 每一個網(wǎng)格點上 每一個先驗框的變化情況。**
num_anchors x num_classes的卷積 用于預測 該特征層上 每一個網(wǎng)格點上 每一個預測框?qū)姆N類。
每一個有效特征層對應的先驗框?qū)撎卣鲗由?每一個網(wǎng)格點上 預先設定好的多個框。
我們利用 num_anchors x 4的卷積 與 每一個有效特征層對應的先驗框 獲得框的真實位置。
每一個有效特征層對應的先驗框就是,如圖所示的作用:
每一個有效特征層將整個圖片分成與其長寬對應的網(wǎng)格,如conv4-3和fl7組合成的特征層就是將整個圖像分成38x38個網(wǎng)格;然后從每個網(wǎng)格中心建立多個先驗框,如conv4-3和fl7組合成的有效特征層就是建立了6個先驗框;
對于conv4-3和fl7組合成的特征層來講,整個圖片被分成38x38個網(wǎng)格,每個網(wǎng)格中心對應6個先驗框,一共包含了,38x38x6個,8664個先驗框。

先驗框雖然可以代表一定的框的位置信息與框的大小信息,但是其是有限的,無法表示任意情況,因此還需要調(diào)整,RFBnet利用num_anchors x 4的卷積的結(jié)果對先驗框進行調(diào)整。
num_anchors x 4中的num_anchors表示了這個網(wǎng)格點所包含的先驗框數(shù)量,其中的4表示了x_offset、y_offset、h和w的調(diào)整情況。
x_offset與y_offset代表了真實框距離先驗框中心的xy軸偏移情況。h和w代表了真實框的寬與高相對于先驗框的變化情況。
RFBnet解碼過程就是將每個網(wǎng)格的中心點加上它對應的x_offset和y_offset,加完后的結(jié)果就是預測框的中心,然后再利用 先驗框和h、w結(jié)合 計算出預測框的長和寬。這樣就能得到整個預測框的位置了。
當然得到最終的預測結(jié)構后還要進行得分排序與非極大抑制篩選這一部分基本上是所有目標檢測通用的部分。
1、取出每一類得分大于self.obj_threshold的框和得分。
2、利用框的位置和得分進行非極大抑制。
實現(xiàn)代碼如下:
def decode_boxes(self, mbox_loc, anchors, variances):
# 獲得先驗框的寬與高
anchor_width = anchors[:, 2] - anchors[:, 0]
anchor_height = anchors[:, 3] - anchors[:, 1]
# 獲得先驗框的中心點
anchor_center_x = 0.5 * (anchors[:, 2] + anchors[:, 0])
anchor_center_y = 0.5 * (anchors[:, 3] + anchors[:, 1])
# 真實框距離先驗框中心的xy軸偏移情況
decode_bbox_center_x = mbox_loc[:, 0] * anchor_width * variances[0]
decode_bbox_center_x += anchor_center_x
decode_bbox_center_y = mbox_loc[:, 1] * anchor_height * variances[1]
decode_bbox_center_y += anchor_center_y
# 真實框的寬與高的求取
decode_bbox_width = np.exp(mbox_loc[:, 2] * variances[2])
decode_bbox_width *= anchor_width
decode_bbox_height = np.exp(mbox_loc[:, 3] * variances[3])
decode_bbox_height *= anchor_height
# 獲取真實框的左上角與右下角
decode_bbox_xmin = decode_bbox_center_x - 0.5 * decode_bbox_width
decode_bbox_ymin = decode_bbox_center_y - 0.5 * decode_bbox_height
decode_bbox_xmax = decode_bbox_center_x + 0.5 * decode_bbox_width
decode_bbox_ymax = decode_bbox_center_y + 0.5 * decode_bbox_height
# 真實框的左上角與右下角進行堆疊
decode_bbox = np.concatenate((decode_bbox_xmin[:, None],
decode_bbox_ymin[:, None],
decode_bbox_xmax[:, None],
decode_bbox_ymax[:, None]), axis=-1)
# 防止超出0與1
decode_bbox = np.minimum(np.maximum(decode_bbox, 0.0), 1.0)
return decode_bbox
def decode_box(self, predictions, anchors, image_shape, input_shape, letterbox_image, variances = [0.1, 0.1, 0.2, 0.2], confidence=0.5):
#---------------------------------------------------#
# :4是回歸預測結(jié)果
#---------------------------------------------------#
mbox_loc = predictions[:, :, :4]
#---------------------------------------------------#
# 獲得種類的置信度
#---------------------------------------------------#
mbox_conf = predictions[:, :, 4:]
results = []
#----------------------------------------------------------------------------------------------------------------#
# 對每一張圖片進行處理,由于在predict.py的時候,我們只輸入一張圖片,所以for i in range(len(mbox_loc))只進行一次
#----------------------------------------------------------------------------------------------------------------#
for i in range(len(mbox_loc)):
results.append([])
#--------------------------------#
# 利用回歸結(jié)果對先驗框進行解碼
#--------------------------------#
decode_bbox = self.decode_boxes(mbox_loc[i], anchors, variances)
for c in range(1, self.num_classes):
#--------------------------------#
# 取出屬于該類的所有框的置信度
# 判斷是否大于門限
#--------------------------------#
c_confs = mbox_conf[i, :, c]
c_confs_m = c_confs > confidence
if len(c_confs[c_confs_m]) > 0:
#-----------------------------------------#
# 取出得分高于confidence的框
#-----------------------------------------#
boxes_to_process = decode_bbox[c_confs_m]
confs_to_process = c_confs[c_confs_m]
#-----------------------------------------#
# 進行iou的非極大抑制
#-----------------------------------------#
idx = self.sess.run(self.nms, feed_dict={self.boxes: boxes_to_process, self.scores: confs_to_process})
#-----------------------------------------#
# 取出在非極大抑制中效果較好的內(nèi)容
#-----------------------------------------#
good_boxes = boxes_to_process[idx]
confs = confs_to_process[idx][:, None]
labels = (c - 1) * np.ones((len(idx), 1))
#-----------------------------------------#
# 將label、置信度、框的位置進行堆疊。
#-----------------------------------------#
c_pred = np.concatenate((good_boxes, labels, confs), axis=1)
# 添加進result里
results[-1].extend(c_pred)
if len(results[-1]) > 0:
results[-1] = np.array(results[-1])
box_xy, box_wh = (results[-1][:, 0:2] + results[-1][:, 2:4])/2, results[-1][:, 2:4] - results[-1][:, 0:2]
results[-1][:, :4] = self.ssd_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image)
return results
4、在原圖上進行繪制
通過第三步,我們可以獲得預測框在原圖上的位置,而且這些預測框都是經(jīng)過篩選的。這些篩選后的框可以直接繪制在圖片上,就可以獲得結(jié)果了。
二、訓練部分
1、真實框的處理
從預測部分我們知道,每個特征層的預測結(jié)果,num_anchors x 4的卷積 用于預測 該特征層上 每一個網(wǎng)格點上 每一個先驗框的變化情況。
也就是說,我們直接利用ssd網(wǎng)絡預測到的結(jié)果,并不是預測框在圖片上的真實位置,需要解碼才能得到真實位置。
而在訓練的時候,我們需要計算loss函數(shù),這個loss函數(shù)是相對于RFB網(wǎng)絡的預測結(jié)果的。我們需要把圖片輸入到當前的RFB網(wǎng)絡中,得到預測結(jié)果;同時還需要把真實框的信息,進行編碼,這個編碼是把真實框的位置信息格式轉(zhuǎn)化為RFB預測結(jié)果的格式信息。
也就是,我們需要找到 每一張用于訓練的圖片的每一個真實框?qū)南闰灴?,并求出如果想要得到這樣一個真實框,我們的預測結(jié)果應該是怎么樣的。
從預測結(jié)果獲得真實框的過程被稱作解碼,而從真實框獲得預測結(jié)果的過程就是編碼的過程。
因此我們只需要將解碼過程逆過來就是編碼過程了。
實現(xiàn)代碼如下:
def encode_box(self, box, return_iou=True, variances = [0.1, 0.1, 0.2, 0.2]):
#---------------------------------------------#
# 計算當前真實框和先驗框的重合情況
# iou [self.num_anchors]
# encoded_box [self.num_anchors, 5]
#---------------------------------------------#
iou = self.iou(box)
encoded_box = np.zeros((self.num_anchors, 4 + return_iou))
#---------------------------------------------#
# 找到每一個真實框,重合程度較高的先驗框
# 真實框可以由這個先驗框來負責預測
#---------------------------------------------#
assign_mask = iou > self.overlap_threshold
#---------------------------------------------#
# 如果沒有一個先驗框重合度大于self.overlap_threshold
# 則選擇重合度最大的為正樣本
#---------------------------------------------#
if not assign_mask.any():
assign_mask[iou.argmax()] = True
#---------------------------------------------#
# 利用iou進行賦值
#---------------------------------------------#
if return_iou:
encoded_box[:, -1][assign_mask] = iou[assign_mask]
#---------------------------------------------#
# 找到對應的先驗框
#---------------------------------------------#
assigned_anchors = self.anchors[assign_mask]
#---------------------------------------------#
# 逆向編碼,將真實框轉(zhuǎn)化為rfb預測結(jié)果的格式
# 先計算真實框的中心與長寬
#---------------------------------------------#
box_center = 0.5 * (box[:2] + box[2:])
box_wh = box[2:] - box[:2]
#---------------------------------------------#
# 再計算重合度較高的先驗框的中心與長寬
#---------------------------------------------#
assigned_anchors_center = (assigned_anchors[:, 0:2] + assigned_anchors[:, 2:4]) * 0.5
assigned_anchors_wh = (assigned_anchors[:, 2:4] - assigned_anchors[:, 0:2])
#------------------------------------------------#
# 逆向求取rfb應該有的預測結(jié)果
# 先求取中心的預測結(jié)果,再求取寬高的預測結(jié)果
# 存在改變數(shù)量級的參數(shù),默認為[0.1,0.1,0.2,0.2]
#------------------------------------------------#
encoded_box[:, :2][assign_mask] = box_center - assigned_anchors_center
encoded_box[:, :2][assign_mask] /= assigned_anchors_wh
encoded_box[:, :2][assign_mask] /= np.array(variances)[:2]
encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_anchors_wh)
encoded_box[:, 2:4][assign_mask] /= np.array(variances)[2:4]
return encoded_box.ravel()
利用上述代碼我們可以獲得,真實框?qū)乃械膇ou較大先驗框,并計算了真實框?qū)乃衖ou較大的先驗框應該有的預測結(jié)果。
在訓練的時候我們只需要選擇iou最大的先驗框就行了,這個iou最大的先驗框就是我們用來預測這個真實框所用的先驗框。
因此我們還要經(jīng)過一次篩選,將上述代碼獲得的真實框?qū)乃械膇ou較大先驗框的預測結(jié)果中,iou最大的那個篩選出來。
通過assign_boxes我們就獲得了,輸入進來的這張圖片,應該有的預測結(jié)果是什么樣子的。
實現(xiàn)代碼如下:
def assign_boxes(self, boxes):
#---------------------------------------------------#
# assignment分為3個部分
# :4 的內(nèi)容為網(wǎng)絡應該有的回歸預測結(jié)果
# 4:-1 的內(nèi)容為先驗框所對應的種類,默認為背景
# -1 的內(nèi)容為當前先驗框是否包含目標
#---------------------------------------------------#
assignment = np.zeros((self.num_anchors, 4 + self.num_classes + 1))
assignment[:, 4] = 1.0
if len(boxes) == 0:
return assignment
# 對每一個真實框都進行iou計算
encoded_boxes = np.apply_along_axis(self.encode_box, 1, boxes[:, :4])
#---------------------------------------------------#
# 在reshape后,獲得的encoded_boxes的shape為:
# [num_true_box, num_anchors, 4 + 1]
# 4是編碼后的結(jié)果,1為iou
#---------------------------------------------------#
encoded_boxes = encoded_boxes.reshape(-1, self.num_anchors, 5)
#---------------------------------------------------#
# [num_anchors]求取每一個先驗框重合度最大的真實框
#---------------------------------------------------#
best_iou = encoded_boxes[:, :, -1].max(axis=0)
best_iou_idx = encoded_boxes[:, :, -1].argmax(axis=0)
best_iou_mask = best_iou > 0
best_iou_idx = best_iou_idx[best_iou_mask]
#---------------------------------------------------#
# 計算一共有多少先驗框滿足需求
#---------------------------------------------------#
assign_num = len(best_iou_idx)
# 將編碼后的真實框取出
encoded_boxes = encoded_boxes[:, best_iou_mask, :]
#---------------------------------------------------#
# 編碼后的真實框的賦值
#---------------------------------------------------#
assignment[:, :4][best_iou_mask] = encoded_boxes[best_iou_idx,np.arange(assign_num),:4]
#----------------------------------------------------------#
# 4代表為背景的概率,設定為0,因為這些先驗框有對應的物體
#----------------------------------------------------------#
assignment[:, 4][best_iou_mask] = 0
assignment[:, 5:-1][best_iou_mask] = boxes[best_iou_idx, 4:]
#----------------------------------------------------------#
# -1表示先驗框是否有對應的物體
#----------------------------------------------------------#
assignment[:, -1][best_iou_mask] = 1
# 通過assign_boxes我們就獲得了,輸入進來的這張圖片,應該有的預測結(jié)果是什么樣子的
return assignment
2、利用處理完的真實框與對應圖片的預測結(jié)果計算loss
loss的計算分為三個部分:
1、獲取所有正標簽的框的預測結(jié)果的回歸loss。
2、獲取所有正標簽的種類的預測結(jié)果的交叉熵loss。
3、獲取一定負標簽的種類的預測結(jié)果的交叉熵loss。
由于在RFBnet的訓練過程中,正負樣本極其不平衡,即 存在對應真實框的先驗框可能只有十來個,但是不存在對應真實框的負樣本卻有幾千個,這就會導致負樣本的loss值極大,因此我們可以考慮減少負樣本的選取,對于ssd的訓練來講,常見的情況是取三倍正樣本數(shù)量的負樣本用于訓練。這個三倍呢,也可以修改,調(diào)整成自己喜歡的數(shù)字。
實現(xiàn)代碼如下:
import tensorflow as tf
class MultiboxLoss(object):
def __init__(self, num_classes, alpha=1.0, neg_pos_ratio=3.0,
background_label_id=0, negatives_for_hard=100.0):
self.num_classes = num_classes
self.alpha = alpha
self.neg_pos_ratio = neg_pos_ratio
if background_label_id != 0:
raise Exception('Only 0 as background label id is supported')
self.background_label_id = background_label_id
self.negatives_for_hard = negatives_for_hard
def _l1_smooth_loss(self, y_true, y_pred):
abs_loss = tf.abs(y_true - y_pred)
sq_loss = 0.5 * (y_true - y_pred)**2
l1_loss = tf.where(tf.less(abs_loss, 1.0), sq_loss, abs_loss - 0.5)
return tf.reduce_sum(l1_loss, -1)
def _softmax_loss(self, y_true, y_pred):
y_pred = tf.maximum(y_pred, 1e-7)
softmax_loss = -tf.reduce_sum(y_true * tf.log(y_pred),
axis=-1)
return softmax_loss
def compute_loss(self, y_true, y_pred):
# --------------------------------------------- #
# y_true batch_size, 11620, 4 + self.num_classes + 1
# y_pred batch_size, 11620, 4 + self.num_classes
# --------------------------------------------- #
num_boxes = tf.to_float(tf.shape(y_true)[1])
# --------------------------------------------- #
# 分類的loss
# batch_size,11620,21 -> batch_size,11620
# --------------------------------------------- #
conf_loss = self._softmax_loss(y_true[:, :, 4:-1],
y_pred[:, :, 4:])
# --------------------------------------------- #
# 框的位置的loss
# batch_size,11620,4 -> batch_size,11620
# --------------------------------------------- #
loc_loss = self._l1_smooth_loss(y_true[:, :, :4],
y_pred[:, :, :4])
# --------------------------------------------- #
# 獲取所有的正標簽的loss
# --------------------------------------------- #
pos_loc_loss = tf.reduce_sum(loc_loss * y_true[:, :, -1],
axis=1)
pos_conf_loss = tf.reduce_sum(conf_loss * y_true[:, :, -1],
axis=1)
# --------------------------------------------- #
# 每一張圖的正樣本的個數(shù)
# num_pos [batch_size,]
# --------------------------------------------- #
num_pos = tf.reduce_sum(y_true[:, :, -1], axis=-1)
# --------------------------------------------- #
# 每一張圖的負樣本的個數(shù)
# num_neg [batch_size,]
# --------------------------------------------- #
num_neg = tf.minimum(self.neg_pos_ratio * num_pos, num_boxes - num_pos)
# 找到了哪些值是大于0的
pos_num_neg_mask = tf.greater(num_neg, 0)
# --------------------------------------------- #
# 如果所有的圖,正樣本的數(shù)量均為0
# 那么則默認選取100個先驗框作為負樣本
# --------------------------------------------- #
has_min = tf.to_float(tf.reduce_any(pos_num_neg_mask))
num_neg = tf.concat(axis=0, values=[num_neg, [(1 - has_min) * self.negatives_for_hard]])
# --------------------------------------------- #
# 從這里往后,與視頻中看到的代碼有些許不同。
# 由于以前的負樣本選取方式存在一些問題,
# 我對該部分代碼進行重構。
# 求整個batch應該的負樣本數(shù)量總和
# --------------------------------------------- #
num_neg_batch = tf.reduce_sum(tf.boolean_mask(num_neg, tf.greater(num_neg, 0)))
num_neg_batch = tf.to_int32(num_neg_batch)
# --------------------------------------------- #
# 對預測結(jié)果進行判斷,如果該先驗框沒有包含物體
# 那么它的不屬于背景的預測概率過大的話
# 就是難分類樣本
# --------------------------------------------- #
confs_start = 4 + self.background_label_id + 1
confs_end = confs_start + self.num_classes - 1
# --------------------------------------------- #
# batch_size,11620
# 把不是背景的概率求和,求和后的概率越大
# 代表越難分類。
# --------------------------------------------- #
max_confs = tf.reduce_sum(y_pred[:, :, confs_start:confs_end], axis=2)
# --------------------------------------------------- #
# 只有沒有包含物體的先驗框才得到保留
# 我們在整個batch里面選取最難分類的num_neg_batch個
# 先驗框作為負樣本。
# --------------------------------------------------- #
max_confs = tf.reshape(max_confs * (1 - y_true[:, :, -1]), [-1])
_, indices = tf.nn.top_k(max_confs, k=num_neg_batch)
neg_conf_loss = tf.gather(tf.reshape(conf_loss, [-1]), indices)
# 進行歸一化
num_pos = tf.where(tf.not_equal(num_pos, 0), num_pos, tf.ones_like(num_pos))
total_loss = tf.reduce_sum(pos_conf_loss) + tf.reduce_sum(neg_conf_loss) + tf.reduce_sum(self.alpha * pos_loc_loss)
total_loss /= tf.reduce_sum(num_pos)
return total_loss
訓練自己的RFB模型
首先前往Github下載對應的倉庫,下載完后利用解壓軟件解壓,之后用編程軟件打開文件夾。注意打開的根目錄必須正確,否則相對目錄不正確的情況下,代碼將無法運行。一定要注意打開后的根目錄是文件存放的目錄。

一、數(shù)據(jù)集的準備
本文使用VOC格式進行訓練,訓練前需要自己制作好數(shù)據(jù)集,如果沒有自己的數(shù)據(jù)集,可以通過Github連接下載VOC12+07的數(shù)據(jù)集嘗試下。訓練前將標簽文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。

訓練前將圖片文件放在VOCdevkit文件夾下的VOC2007文件夾下的JPEGImages中。

此時數(shù)據(jù)集的擺放已經(jīng)結(jié)束。
二、數(shù)據(jù)集的處理
在完成數(shù)據(jù)集的擺放之后,我們需要對數(shù)據(jù)集進行下一步的處理,目的是獲得訓練用的2007_train.txt以及2007_val.txt,需要用到根目錄下的voc_annotation.py。
voc_annotation.py里面有一些參數(shù)需要設置。
分別是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次訓練可以僅修改classes_path
''' annotation_mode用于指定該文件運行時計算的內(nèi)容 annotation_mode為0代表整個標簽處理過程,包括獲得VOCdevkit/VOC2007/ImageSets里面的txt以及訓練用的2007_train.txt、2007_val.txt annotation_mode為1代表獲得VOCdevkit/VOC2007/ImageSets里面的txt annotation_mode為2代表獲得訓練用的2007_train.txt、2007_val.txt ''' annotation_mode = 0 ''' 必須要修改,用于生成2007_train.txt、2007_val.txt的目標信息 與訓練和預測所用的classes_path一致即可 如果生成的2007_train.txt里面沒有目標信息 那么就是因為classes沒有設定正確 僅在annotation_mode為0和2的時候有效 ''' classes_path = 'model_data/voc_classes.txt' ''' trainval_percent用于指定(訓練集+驗證集)與測試集的比例,默認情況下 (訓練集+驗證集):測試集 = 9:1 train_percent用于指定(訓練集+驗證集)中訓練集與驗證集的比例,默認情況下 訓練集:驗證集 = 9:1 僅在annotation_mode為0和1的時候有效 ''' trainval_percent = 0.9 train_percent = 0.9 ''' 指向VOC數(shù)據(jù)集所在的文件夾 默認指向根目錄下的VOC數(shù)據(jù)集 ''' VOCdevkit_path = 'VOCdevkit'
classes_path用于指向檢測類別所對應的txt,以voc數(shù)據(jù)集為例,我們用的txt為:

訓練自己的數(shù)據(jù)集時,可以自己建立一個cls_classes.txt,里面寫自己所需要區(qū)分的類別。
三、開始網(wǎng)絡訓練
通過voc_annotation.py我們已經(jīng)生成了2007_train.txt以及2007_val.txt,此時我們可以開始訓練了。
訓練的參數(shù)較多,大家可以在下載庫后仔細看注釋,其中最重要的部分依然是train.py里的classes_path。
classes_path用于指向檢測類別所對應的txt,這個txt和voc_annotation.py里面的txt一樣!訓練自己的數(shù)據(jù)集必須要修改!

修改完classes_path后就可以運行train.py開始訓練了,在訓練多個epoch后,權值會生成在logs文件夾中。
其它參數(shù)的作用如下:
#--------------------------------------------------------# # 訓練前一定要修改classes_path,使其對應自己的數(shù)據(jù)集 #--------------------------------------------------------# classes_path = 'model_data/voc_classes.txt' #----------------------------------------------------------------------------------------------------------------------------# # 權值文件請看README,百度網(wǎng)盤下載。數(shù)據(jù)的預訓練權重對不同數(shù)據(jù)集是通用的,因為特征是通用的。 # 預訓練權重對于99%的情況都必須要用,不用的話權值太過隨機,特征提取效果不明顯,網(wǎng)絡訓練的結(jié)果也不會好。 # 訓練自己的數(shù)據(jù)集時提示維度不匹配正常,預測的東西都不一樣了自然維度不匹配 # # 如果想要斷點續(xù)練就將model_path設置成logs文件夾下已經(jīng)訓練的權值文件。 # 當model_path = ''的時候不加載整個模型的權值。 # # 此處使用的是整個模型的權重,因此是在train.py進行加載的。 # 如果想要讓模型從主干的預訓練權值開始訓練,則設置model_path為主干網(wǎng)絡的權值,此時僅加載主干。 # 如果想要讓模型從0開始訓練,則設置model_path = '',F(xiàn)reeze_Train = Fasle,此時從0開始訓練,且沒有凍結(jié)主干的過程。 # 一般來講,從0開始訓練效果會很差,因為權值太過隨機,特征提取效果不明顯。 #----------------------------------------------------------------------------------------------------------------------------# model_path = 'model_data/rfb_weights.h5' #------------------------------------------------------# # 輸入的shape大小 #------------------------------------------------------# input_shape = [300, 300] #----------------------------------------------------# # 可用于設定先驗框的大小,默認的anchors_size # 是根據(jù)voc數(shù)據(jù)集設定的,大多數(shù)情況下都是通用的! # 如果想要檢測小物體,可以修改anchors_size # 一般調(diào)小淺層先驗框的大小就行了!因為淺層負責小物體檢測! # 比如anchors_size = [21, 45, 99, 153, 207, 261, 315] #----------------------------------------------------# anchors_size = [30, 60, 111, 162, 213, 264, 315] #----------------------------------------------------# # 訓練分為兩個階段,分別是凍結(jié)階段和解凍階段。 # 顯存不足與數(shù)據(jù)集大小無關,提示顯存不足請調(diào)小batch_size。 # 受到BatchNorm層影響,batch_size最小為2,不能為1。 #----------------------------------------------------# #----------------------------------------------------# # 凍結(jié)階段訓練參數(shù) # 此時模型的主干被凍結(jié)了,特征提取網(wǎng)絡不發(fā)生改變 # 占用的顯存較小,僅對網(wǎng)絡進行微調(diào) #----------------------------------------------------# Init_Epoch = 0 Freeze_Epoch = 50 Freeze_batch_size = 16 Freeze_lr = 5e-4 #----------------------------------------------------# # 解凍階段訓練參數(shù) # 此時模型的主干不被凍結(jié)了,特征提取網(wǎng)絡會發(fā)生改變 # 占用的顯存較大,網(wǎng)絡所有的參數(shù)都會發(fā)生改變 #----------------------------------------------------# UnFreeze_Epoch = 100 Unfreeze_batch_size = 8 Unfreeze_lr = 1e-4 #------------------------------------------------------# # 是否進行凍結(jié)訓練,默認先凍結(jié)主干訓練后解凍訓練。 #------------------------------------------------------# Freeze_Train = True #------------------------------------------------------# # 用于設置是否使用多線程讀取數(shù)據(jù),0代表關閉多線程 # 開啟后會加快數(shù)據(jù)讀取速度,但是會占用更多內(nèi)存 # keras里開啟多線程有些時候速度反而慢了許多 # 在IO為瓶頸的時候再開啟多線程,即GPU運算速度遠大于讀取圖片的速度。 #------------------------------------------------------# num_workers = 0 #----------------------------------------------------# # 獲得圖片路徑和標簽 #----------------------------------------------------# train_annotation_path = '2007_train.txt' val_annotation_path = '2007_val.txt'
四、訓練結(jié)果預測
訓練結(jié)果預測需要用到兩個文件,分別是yolo.py和predict.py。我們首先需要去yolo.py里面修改model_path以及classes_path,這兩個參數(shù)必須要修改。
model_path指向訓練好的權值文件,在logs文件夾里。
classes_path指向檢測類別所對應的txt。

完成修改后就可以運行predict.py進行檢測了。運行后輸入圖片路徑即可檢測。
以上就是python神經(jīng)網(wǎng)絡Keras搭建RFBnet目標檢測平臺的詳細內(nèi)容,更多關于Keras搭建RFBnet目標檢測的資料請關注腳本之家其它相關文章!
相關文章
pytorch中F.avg_pool1d()和F.avg_pool2d()的使用操作
這篇文章主要介紹了pytorch中F.avg_pool1d()和F.avg_pool2d()的使用操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-05-05
Python實現(xiàn)數(shù)據(jù)地址實體抽取
大家好,本篇文章主要講的是Python實現(xiàn)數(shù)據(jù)地址實體抽取,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下2022-02-02
Python控制windows系統(tǒng)音量實現(xiàn)實例
這篇文章主要介紹了Python控制windows系統(tǒng)音量實現(xiàn)實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2023-01-01
Python?OpenCV超詳細講解調(diào)整大小與圖像操作的實現(xiàn)
OpenCV用C++語言編寫,它具有C?++,Python,Java和MATLAB接口,并支持Windows,Linux,Android和Mac?OS,OpenCV主要傾向于實時視覺應用,并在可用時利用MMX和SSE指令,本篇文章帶你通過OpenCV實現(xiàn)重調(diào)大小與圖像裁剪2022-04-04

