我用YOLO-V5實現行人社交距離風險提示,程式碼開源!

點選上方“CVer”,選擇加"星標"置頂
重磅乾貨,第一時間送達

本文轉載自:AI人工智慧初學者
由於YOLO V5的作者現在並沒有發表論文,因此只能從程式碼的角度理解它的工作。YOLO V5的網路結構圖如下:

1、與YOLO V4的區別

Yolov4在Yolov3的基礎上進行了很多的創新。比如輸入端採用mosaic資料增強,Backbone上採用了CSPDarknet53Mish啟用函式Dropblock等方式,Neck中採用了SPPFPN+PAN的結構,輸出端則採用CIOU_LossDIOU_nms操作。因此Yolov4對Yolov3的各個部分都進行了很多的整合創新。這裡給出YOLO V4的網路結構圖:
Yolov5的結構其實和Yolov4的結構還是有一定的相似之處的,但也有一些不同,這裡還是按照從整體到細節的方式,對每個板塊進行講解。這裡給出YOLO V4的網路結構圖:

透過Yolov5的網路結構圖可以看到,依舊是把模型分為4個部分,分別是:輸入端、Backbone、Neck、Prediction。

1.1、輸入端的區別

1 Mosaic資料增強

Mosaic是參考CutMix資料增強的方式,但CutMix只使用了兩張圖片進行拼接,而Mosaic資料增強則採用了4張圖片,隨機縮放、隨機裁剪、隨機排布的方式進行拼接。
主要有幾個優點:
  • 1、豐富資料集:隨機使用4張圖片,隨機縮放,再隨機分佈進行拼接,大大豐富了檢測資料集,特別是隨機縮放增加了很多小目標,讓網路的魯棒性更好。
  • 2、減少GPU:可能會有人說,隨機縮放,普通的資料增強也可以做,但作者考慮到很多人可能只有一個GPU,因此Mosaic增強訓練時,可以直接計算4張圖片的資料,使得Mini-batch大小並不需要很大,一個GPU就可以達到比較好的效果。

2 自適應錨框計算

在Yolov3、Yolov4中,訓練不同的資料集時,計算初始錨框的值是透過單獨的程式執行的。但Yolov5中將此功能嵌入到程式碼中,每次訓練時,自適應的計算不同訓練集中的最佳錨框值。
比如Yolov5在Coco資料集上初始設定的錨框:

3 自適應圖片縮放

在常用的目標檢測演算法中,不同的圖片長寬都不相同,因此常用的方式是將原始圖片統一縮放到一個標準尺寸,再送入檢測網路中。比如Yolo演算法中常用416×416,608×608等尺寸,比如對下面800×600的影象進行變換。
但Yolov5程式碼中對此進行了改進,也是Yolov5推理速度能夠很快的一個不錯的trick。作者認為,在專案實際使用時,很多圖片的長寬比不同。因此縮放填充後,兩端的黑邊大小都不同,而如果填充的比較多,則存在資訊冗餘,影響推理速度。
具體操作的步驟:
1 計算縮放比例

原始縮放尺寸是416×416,都除以原始影象的尺寸後,可以得到0.52,和0.69兩個縮放係數,選擇小的縮放係數0.52。

2 計算縮放後的尺寸
原始圖片的長寬都乘以最小的縮放係數0.52,寬變成了416,而高變成了312。

3 計算黑邊填充數值
將416-312=104,得到原本需要填充的高度。再採用numpy中np.mod取餘數的方式,得到40個畫素,再除以2,即得到圖片高度兩端需要填充的數值。

1.2、Backbone的區別

1 Focus結構

Focus結構,在Yolov3&Yolov4中並沒有這個結構,其中比較關鍵是切片操作。比如右圖的切片示意圖,4×4×3的影象切片後變成3×3×12的特徵圖。以Yolov5s的結構為例,原始608×608×3的影象輸入Focus結構,採用切片操作,先變成304×304×12的特徵圖,再經過一次32個卷積核的卷積操作,最終變成304×304×32的特徵圖。

需要注意的是:Yolov5s的Focus結構最後使用了32個卷積核,而其他三種結構,使用的數量有所增加,先注意下,後面會講解到四種結構的不同點。
classFocus(nn.Module):
# Focus wh information into c-space
def__init__(self, c1, c2, k=1):

super(Focus, self).__init__()

self.conv = Conv(c1 *

4

, c2, k,

1

)

defforward(self, x):# x(b,c,w,h) -> y(b,4c,w/2,h/2)
return

self.conv(torch.cat([x[..., ::

2

, ::

2

], x[...,

1

::

2

, ::

2

], x[..., ::

2

,

1

::

2

], x[...,

1

::

2

,

1

::

2

]],

1

))

2 CSP結構

Yolov5與Yolov4不同點在於,Yolov4中只有主幹網路使用了CSP結構,而Yolov5中設計了兩種CSP結構,以Yolov5s網路為例,以CSP1_X結構應用於Backbone主幹網路,另一種CSP2_X結構則應用於Neck中。
classConv(nn.Module):
# Standard convolution
def__init__(self, c1, c2, k=1, s=1, g=1, act=True):# ch_in, ch_out, kernel, stride, groups

super(Conv, self).__init__()

self.conv = nn.Conv2d(c1, c2, k, s, k //

2

, groups=g, bias=

False

)

self.bn = nn.BatchNorm2d(c2)

self.act = nn.LeakyReLU(

0.1

, inplace=

True

)

if

act

else

nn.Identity()

defforward(self, x):
return

self.act(self.bn(self.conv(x)))

deffuseforward(self, x):
return

self.act(self.conv(x))

classBottleneck(nn.Module):
# Standard bottleneck
def__init__(self, c1, c2, shortcut=True, g=1, e=0.5):# ch_in, ch_out, shortcut, groups, expansion

super(Bottleneck, self).__init__()

c_ = int(c2 * e)

# hidden channels

self.cv1 = Conv(c1, c_,

1

,

1

)

self.cv2 = Conv(c_, c2,

3

,

1

, g=g)

self.add = shortcut

and

c1 == c2

defforward(self, x):
return

x + self.cv2(self.cv1(x))

if

self.add

else

self.cv2(self.cv1(x))

classBottleneckCSP(nn.Module):
# CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks
def__init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):# ch_in, ch_out, number, shortcut, groups, expansion

super(BottleneckCSP, self).__init__()

c_ = int(c2 * e)

# hidden channels

self.cv1 = Conv(c1, c_,

1

,

1

)

self.cv2 = nn.Conv2d(c1, c_,

1

,

1

, bias=

False

)

self.cv3 = nn.Conv2d(c_, c_,

1

,

1

, bias=

False

)

self.cv4 = Conv(c2, c2,

1

,

1

)

self.bn = nn.BatchNorm2d(

2

* c_)

# applied to cat(cv2, cv3)

self.act = nn.LeakyReLU(

0.1

, inplace=

True

)

self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=

1.0

)

for

_

in

range(n)])

defforward(self, x):

y1 = self.cv3(self.m(self.cv1(x)))

y2 = self.cv2(x)

return

self.cv4(self.act(self.bn(torch.cat((y1, y2), dim=

1

))))

1.3、Neck的區別

Yolov5現在的Neck和Yolov4中一樣,都採用FPN+PAN的結構,但在Yolov5剛出來時,只使用了FPN接面構,後面才增加了PAN結構,此外網路中其他部分也進行了調整。
Yolov5和Yolov4的不同點在於,Yolov4的Neck中,採用的都是普通的卷積操作。而Yolov5的Neck結構中,採用借鑑CSPNet設計的CSP2結構,加強網路特徵融合的能力。

1.4、輸出端的區別

1 Bounding box損失函式

而Yolov4中採用CIOU_Loss作為目標Bounding box的損失。而Yolov5中採用其中的GIOU_Loss做Bounding box的損失函式。
defcompute_loss(p, targets, model):# predictions, targets, model

ft = torch.cuda.FloatTensor

if

p[

0

].is_cuda

else

torch.Tensor

lcls, lbox, lobj = ft([

0

]), ft([

0

]), ft([

0

])

tcls, tbox, indices, anchors = build_targets(p, targets, model)

# targets

h = model.hyp

# hyperparameters

red =

'mean'# Loss reduction (sum or mean)

# Define criteria

BCEcls = nn.BCEWithLogitsLoss(pos_weight=ft([h[

'cls_pw'

]]), reduction=red)

BCEobj = nn.BCEWithLogitsLoss(pos_weight=ft([h[

'obj_pw'

]]), reduction=red)

# class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3

cp, cn = smooth_BCE(eps=

0.0

)

# focal loss

g = h[

'fl_gamma'

]

# focal loss gamma
if

g >

0

:

BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)

# per output

nt =

0# targets
for

i, pi

in

enumerate(p):

# layer index, layer predictions

b, a, gj, gi = indices[i]

# image, anchor, gridy, gridx

tobj = torch.zeros_like(pi[...,

0

])

# target obj

nb = b.shape[

0

]

# number of targets
if

nb:

nt += nb

# cumulative targets

ps = pi[b, a, gj, gi]

# prediction subset corresponding to targets

# GIoU

pxy = ps[:, :

2

].sigmoid() *

2.

-

0.5

pwh = (ps[:,

2

:

4

].sigmoid() *

2

) **

2

* anchors[i]

pbox = torch.cat((pxy, pwh),

1

)

# predicted box

giou = bbox_iou(pbox.t(), tbox[i], x1y1x2y2=

False

, GIoU=

True

)

# giou(prediction, target)

lbox += (

1.0

- giou).sum()

if

red ==

'sum'else

(

1.0

- giou).mean()

# giou loss

# Obj

tobj[b, a, gj, gi] = (

1.0

- model.gr) + model.gr * giou.detach().clamp(

0

).type(tobj.dtype)

# giou ratio

# Class
if

model.nc >

1

:

# cls loss (only if multiple classes)

t = torch.full_like(ps[:,

5

:], cn)

# targets

t[range(nb), tcls[i]] = cp

lcls += BCEcls(ps[:,

5

:], t)

# BCE

# Append targets to text file
# with open('targets.txt', 'a') as file:
#     [file.write('.5g ' * 4 % tuple(x) + 'n') for x in torch.cat((txy[i], twh[i]), 1)]

lobj += BCEobj(pi[...,

4

], tobj)

# obj loss

lbox *= h[

'giou'

]

lobj *= h[

'obj'

]

lcls *= h[

'cls'

]

bs = tobj.shape[

0

]

# batch size
if

red ==

'sum'

:

g =

3.0# loss gain

lobj *= g / bs

if

nt:

lcls *= g / nt / model.nc

lbox *= g / nt
loss = lbox + lobj + lcls

return

loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach()

2 NMS非極大值抑制

Yolov4在DIOU_Loss的基礎上採用DIOU_NMS的方式,而Yolov5中採用加權NMS的方式。可以看出,採用DIOU_NMS,下方中間箭頭的黃色部分,原本被遮擋的摩托車也可以檢出。
在同樣的引數情況下,將NMS中IOU修改成DIOU_NMS。對於一些

遮擋重疊

的目標,確實會有一些改進。

2、YOLOv5社交距離專案

yolov5檢測要檢測的影片流中的所有人,然後再計算所有檢測到的人之間的相互“距離”,和現實生活中用“m”這樣的單位衡量距離不一樣的是,在計算機中,簡單的方法是用檢測到的兩個人的質心,也就是檢測到的目標框的中心之間相隔的畫素值作為計算機中的“距離”來衡量影片中的人之間的距離是否超過安全距離。
構建步驟
  • 使用目標檢測演算法檢測影片流中的所有人,得到位置資訊和質心位置;
  • 計算所有檢測到的人質心之間的相互距離;
  • 設定安全距離,計算每個人之間的距離對,檢測兩個人之間的距離是否小於N個畫素,小於則處於安全距離,反之則不處於。
專案架構:

detect.py程式碼註釋如下:
import

argparse

from

utils.datasets

import

*

from

utils.utils

import

*

defdetect(save_img=False):

out, source, weights, view_img, save_txt, imgsz = 

opt.output, opt.source, opt.weights, opt.view_img, opt.save_txt, opt.img_size

webcam = source ==

'0'or

source.startswith(

'rtsp'

)

or

source.startswith(

'http'

)

or

source.endswith(

'.txt'

)

# Initialize

device = torch_utils.select_device(opt.device)

if

os.path.exists(out):

shutil.rmtree(out)

# delete output folder

os.makedirs(out)

# make new output folder

half = device.type !=

'cpu'# half precision only supported on CUDA

# 下載模型

google_utils.attempt_download(weights)

# 載入權重

model = torch.load(weights, map_location=device)[

'model'

].float()

# torch.save(torch.load(weights, map_location=device), weights)  # update model if SourceChangeWarning
# model.fuse()
# 設定模型為推理模式

model.to(device).eval()

if

half:

model.half()

# to FP16

# Second-stage classifier

classify =

False
if

classify:

modelc = torch_utils.load_classifier(name=

'resnet101'

, n=

2

)

# initialize

modelc.load_state_dict(torch.load(

'weights/resnet101.pt'

, map_location=device)[

'model'

])

# load weights

modelc.to(device).eval()

# 設定 Dataloader

vid_path, vid_writer =

None

,

None
if

webcam:

view_img =

True

torch.backends.cudnn.benchmark =

True# set True to speed up constant image size inference

dataset = LoadStreams(source, img_size=imgsz)

else

:

save_img =

True

dataset = LoadImages(source, img_size=imgsz)

# 獲取檢測類別的標籤名稱

names = model.names

if

hasattr(model,

'names'

)

else

model.modules.names

# 定義顏色

colors = [[random.randint(

0

,

255

)

for

_

in

range(

3

)]

for

_

in

range(len(names))]

# 開始推理

t0 = time.time()

# 初始化一張全為0的圖片

img = torch.zeros((

1

,

3

, imgsz, imgsz), device=device)

_ = model(img.half()

if

half

else

img)

if

device.type !=

'cpu'elseNone
for

path, img, im0s, vid_cap

in

dataset:

img = torch.from_numpy(img).to(device)

img = img.half()

if

half

else

img.float()

# uint8 to fp16/32

img /=

255.0# 0 - 255 to 0.0 - 1.0
if

img.ndimension() ==

3

:

img = img.unsqueeze(

0

)

# 預測結果

t1 = torch_utils.time_synchronized()

pred = model(img, augment=opt.augment)[

0

]

# 使用NMS

pred = non_max_suppression(pred, opt.conf_thres, opt.iou_thres, fast=

True

, classes=opt.classes, agnostic=opt.agnostic_nms)

t2 = torch_utils.time_synchronized()

# 進行分類
if

classify:

pred = apply_classifier(pred, modelc, img, im0s)
people_coords = []

# 處理預測得到的檢測目標
for

i, det

in

enumerate(pred):

if

webcam:

p, s, im0 = path[i],

'%g: '

% i, im0s[i].copy()

else

:

p, s, im0 = path,

''

, im0s
save_path = str(Path(out) / Path(p).name)

s +=

'%gx%g '

% img.shape[

2

:]

# print string

gn = torch.tensor(im0.shape)[[

1

,

0

,

1

,

0

]]

#  normalization gain whwh
if

det

isnotNoneand

len(det):

# 把boxes resize到im0的size

det[:, :

4

] = scale_coords(img.shape[

2

:], det[:, :

4

], im0.shape).round()

# 列印結果
for

c

in

det[:,

-1

].unique():

n = (det[:,

-1

] == c).sum()

# detections per class

s +=

'%g %ss, '

% (n, names[int(c)])

# add to string

# 書寫結果
for

*xyxy, conf, cls

in

det:

if

save_txt:

# xyxy2xywh ==> 把預測得到的座標結果[x1, y1, x2, y2]轉換為[x, y, w, h]其中 xy1=top-left, xy2=bottom-right

xywh = (xyxy2xywh(torch.tensor(xyxy).view(

1

,

4

)) / gn).view(

-1

).tolist()

# normalized xywh
with

open(save_path[:save_path.rfind(

'.'

)] +

'.txt'

,

'a'

)

as

file:

file.write((

'%g '

*

5

+

'n'

) % (cls, *xywh))

# label format

if

save_img

or

view_img:

# Add bbox to image

label =

'%s %.2f'

% (names[int(cls)], conf)

if

label

isnotNone

:

if

(label.split())[

0

] ==

'person'

:

# print(xyxy)

people_coords.append(xyxy)

# plot_one_box(xyxy, im0, line_thickness=3)

plot_dots_on_people(xyxy, im0)

# 透過people_coords繪製people之間的連線線
# 這裡主要分為"Low Risk "和"High Risk"

distancing(people_coords, im0, dist_thres_lim=(

200

,

250

))

# Print time (inference + NMS)

print(

'%sDone. (%.3fs)'

% (s, t2 - t1))

# Stream results
if

view_img:

cv2.imshow(p, im0)

if

cv2.waitKey(

1

) == ord(

'q'

):

# q to quit
raise

StopIteration

# Save results (image with detections)
if

save_img:

if

dataset.mode ==

'images'

:

cv2.imwrite(save_path, im0)

else

:

if

vid_path != save_path:

# new video

vid_path = save_path

if

isinstance(vid_writer, cv2.VideoWriter):

vid_writer.release()

# release previous video writer

fps = vid_cap.get(cv2.CAP_PROP_FPS)

w = int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH))

h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

vid_writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*opt.fourcc), fps, (w, h))

vid_writer.write(im0)

if

save_txt

or

save_img:

print(

'Results saved to %s'

% os.getcwd() + os.sep + out)

if

platform ==

'darwin'

:

# MacOS

os.system(

'open '

+ save_path)
print(

'Done. (%.3fs)'

% (time.time() - t0))

if

__name__ ==

'__main__'

:

parser = argparse.ArgumentParser()

parser.add_argument(

'--weights'

, type=str, default=

'./weights/yolov5s.pt'

, help=

'model.pt path'

)

parser.add_argument(

'--source'

, type=str, default=

'./inference/videos/'

, help=

'source'

)

# file/folder, 0 for webcam

parser.add_argument(

'--output'

, type=str, default=

'./inference/output'

, help=

'output folder'

)

# output folder

parser.add_argument(

'--img-size'

, type=int, default=

640

, help=

'inference size (pixels)'

)

parser.add_argument(

'--conf-thres'

, type=float, default=

0.4

, help=

'object confidence threshold'

)

parser.add_argument(

'--iou-thres'

, type=float, default=

0.5

, help=

'IOU threshold for NMS'

)

parser.add_argument(

'--fourcc'

, type=str, default=

'mp4v'

, help=

'output video codec (verify ffmpeg support)'

)

parser.add_argument(

'--device'

, default=

'0'

, help=

'cuda device, i.e. 0 or 0,1,2,3 or cpu'

)

parser.add_argument(

'--view-img'

, action=

'store_true'

, help=

'display results'

)

parser.add_argument(

'--save-txt'

, action=

'store_true'

, help=

'save results to *.txt'

)

parser.add_argument(

'--classes'

, nargs=

'+'

, type=int, help=

'filter by class'

)

parser.add_argument(

'--agnostic-nms'

, action=

'store_true'

, help=

'class-agnostic NMS'

)

parser.add_argument(

'--augment'

, action=

'store_true'

, help=

'augmented inference'

)

opt = parser.parse_args()

opt.img_size = check_img_size(opt.img_size)

print(opt)

with

torch.no_grad():

detect()

參考

[1].https://zhuanlan.zhihu.com/p/172121380

[2].https://blog.csdn.net/weixin_45192980/article/details/108354169

[3].https://github.com/ultralytics/yoloV5

[4].https://github.com/Akbonline/Social-Distancing-using-YOLOv5

原文程式碼獲取方式,掃描下方二維碼
回覆【YOLOV5-SD】即可獲取原始碼

CV資源下載
後臺回覆:CVPR2020,即可下載程式碼開源的論文合集
後臺回覆:ECCV2020,即可下載程式碼開源的論文合集
後臺回覆:YOLO,即可下載YOLOv4論文和程式碼
後臺回覆:Transformer綜述,即可下載兩個最新的視覺Transformer綜述PDF,肝起來!
重磅!CVer-目標檢測交流群成立
掃碼新增CVer助手,可申請加入CVer-目標檢測方向微信交流群,可申請加入CVer大群,細分方向已涵蓋:目標檢測、影象分割、目標跟蹤、人臉檢測&識別、OCR、姿態估計、超解析度、SLAM、醫療影像、Re-ID、GAN、NAS、深度估計、自動駕駛、強化學習、車道線檢測、模型剪枝&壓縮、去噪、去霧、去雨、風格遷移、遙感影象、行為識別、影片理解、影象融合、影象檢索、論文投稿&交流、Transformer、PyTorch和TensorFlow等群。
一定要備註:研究方向+地點+學校/公司+暱稱(如目標檢測+上海+上交+卡卡),根據格式備註,才能透過且邀請進群
▲長按加微信群
▲長按關注CVer公眾號
整理不易,請給CVer點贊和在看


相關文章