adicionado reconhecimento de ervas v7
This commit is contained in:
parent
2591a0209f
commit
96c375ee16
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -388,6 +388,69 @@
|
|||
<Content Include="Python\Models\deeplabv3plus\__init__.py">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\models\common.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\models\experimental.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\models\yolo.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\models\__init__.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\activations.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\add_nms.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\autoanchor.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\aws\resume.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\aws\__init__.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\datasets.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\general.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\google_app_engine\additional_requirements.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\google_utils.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\loss.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\metrics.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\plots.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\torch_utils.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\wandb_logging\log_dataset.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\wandb_logging\wandb_utils.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\wandb_logging\__init__.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\__init__.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Output\folium\mqtt.min.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
|
@ -433,6 +496,9 @@
|
|||
<Content Include="Python\Scripts\street-detector.py">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Scripts\weed-detector-v7.py">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Scripts\weed-detector.py">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
|
@ -585,6 +651,63 @@
|
|||
<None Include="Python\Models\deeplabv3plus\__pycache__\__init__.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="Python\Models\yolov7\models\__pycache__\common.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="Python\Models\yolov7\models\__pycache__\experimental.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="Python\Models\yolov7\models\__pycache__\yolo.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="Python\Models\yolov7\models\__pycache__\__init__.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Content Include="Python\Models\yolov7\utils\aws\mime.sh">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\aws\userdata.sh">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\google_app_engine\app.yaml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\google_app_engine\Dockerfile">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\wandb_logging\__pycache__\wandb_utils.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\wandb_logging\__pycache__\__init__.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\__pycache__\autoanchor.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\__pycache__\datasets.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\__pycache__\general.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\__pycache__\google_utils.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\__pycache__\loss.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\__pycache__\metrics.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\__pycache__\plots.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\__pycache__\torch_utils.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Python\Models\yolov7\utils\__pycache__\__init__.cpython-311.pyc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Include="Python\Output\fonts\glyphicons-halflings-regular.ttf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@
|
|||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea1 = new System.Windows.Forms.DataVisualization.Charting.ChartArea();
|
||||
System.Windows.Forms.DataVisualization.Charting.Legend legend1 = new System.Windows.Forms.DataVisualization.Charting.Legend();
|
||||
System.Windows.Forms.DataVisualization.Charting.Series series1 = new System.Windows.Forms.DataVisualization.Charting.Series();
|
||||
System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea2 = new System.Windows.Forms.DataVisualization.Charting.ChartArea();
|
||||
System.Windows.Forms.DataVisualization.Charting.Legend legend2 = new System.Windows.Forms.DataVisualization.Charting.Legend();
|
||||
System.Windows.Forms.DataVisualization.Charting.Series series2 = new System.Windows.Forms.DataVisualization.Charting.Series();
|
||||
this.pnlCamerasSolo = new System.Windows.Forms.Panel();
|
||||
this.pnlCameraSoloE = new System.Windows.Forms.Panel();
|
||||
this.picCameraSoloE = new System.Windows.Forms.PictureBox();
|
||||
|
|
@ -189,17 +189,17 @@
|
|||
//
|
||||
this.chartGraficos.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
chartArea1.Name = "ChartArea1";
|
||||
this.chartGraficos.ChartAreas.Add(chartArea1);
|
||||
legend1.Name = "Legend1";
|
||||
this.chartGraficos.Legends.Add(legend1);
|
||||
chartArea2.Name = "ChartArea1";
|
||||
this.chartGraficos.ChartAreas.Add(chartArea2);
|
||||
legend2.Name = "Legend1";
|
||||
this.chartGraficos.Legends.Add(legend2);
|
||||
this.chartGraficos.Location = new System.Drawing.Point(640, 7);
|
||||
this.chartGraficos.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.chartGraficos.Name = "chartGraficos";
|
||||
series1.ChartArea = "ChartArea1";
|
||||
series1.Legend = "Legend1";
|
||||
series1.Name = "Series1";
|
||||
this.chartGraficos.Series.Add(series1);
|
||||
series2.ChartArea = "ChartArea1";
|
||||
series2.Legend = "Legend1";
|
||||
series2.Name = "Series1";
|
||||
this.chartGraficos.Series.Add(series2);
|
||||
this.chartGraficos.Size = new System.Drawing.Size(591, 318);
|
||||
this.chartGraficos.TabIndex = 2;
|
||||
this.chartGraficos.Text = "chart1";
|
||||
|
|
@ -246,6 +246,9 @@
|
|||
//
|
||||
// tabControl1
|
||||
//
|
||||
this.tabControl1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.tabControl1.Controls.Add(this.tabMapa);
|
||||
this.tabControl1.Controls.Add(this.tabInformacoes);
|
||||
this.tabControl1.Controls.Add(this.tabDeteccoes);
|
||||
|
|
@ -259,6 +262,7 @@
|
|||
//
|
||||
// tabMapa
|
||||
//
|
||||
this.tabMapa.BackColor = System.Drawing.Color.White;
|
||||
this.tabMapa.Controls.Add(this.lblGPSprecisao);
|
||||
this.tabMapa.Controls.Add(this.lblGPSaltitude);
|
||||
this.tabMapa.Controls.Add(this.lblGPSvelocidade);
|
||||
|
|
@ -275,7 +279,6 @@
|
|||
this.tabMapa.Size = new System.Drawing.Size(463, 288);
|
||||
this.tabMapa.TabIndex = 0;
|
||||
this.tabMapa.Text = "Mapa";
|
||||
this.tabMapa.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// lblGPSprecisao
|
||||
//
|
||||
|
|
@ -319,10 +322,10 @@
|
|||
//
|
||||
// pnlMapa
|
||||
//
|
||||
this.pnlMapa.Location = new System.Drawing.Point(89, 2);
|
||||
this.pnlMapa.Location = new System.Drawing.Point(104, 2);
|
||||
this.pnlMapa.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.pnlMapa.Name = "pnlMapa";
|
||||
this.pnlMapa.Size = new System.Drawing.Size(374, 286);
|
||||
this.pnlMapa.Size = new System.Drawing.Size(359, 286);
|
||||
this.pnlMapa.TabIndex = 0;
|
||||
//
|
||||
// lblGPS
|
||||
|
|
|
|||
|
|
@ -586,6 +586,14 @@ namespace AgroBase.Forms.Operacoes
|
|||
gridDados.Rows.Add("Nível de Bateria", "", "%");
|
||||
gridDados.Rows.Add("Tensão da Bateria", "", "v");
|
||||
gridDados.Rows.Add("Corrente da Bateria", "", "A");
|
||||
gridDados.Rows.Add("Barramento 3V", "", "V");
|
||||
gridDados.Rows.Add("Barramento 3V", "", "mA");
|
||||
gridDados.Rows.Add("Barramento 5V", "", "V");
|
||||
gridDados.Rows.Add("Barramento 5V", "", "mA");
|
||||
gridDados.Rows.Add("Barramento 7,2V", "", "V");
|
||||
gridDados.Rows.Add("Barramento 7,2V", "", "mA");
|
||||
gridDados.Rows.Add("Barramento 12V", "", "V");
|
||||
gridDados.Rows.Add("Barramento 12V", "", "mA");
|
||||
gridDados.Rows.Add("Nível do Reservatório", "", "%");
|
||||
gridDados.Rows.Add("Volume do Reservatório", "", "L");
|
||||
gridDados.Rows.Add("Pressão da Linha", "", "psi");
|
||||
|
|
@ -603,10 +611,18 @@ namespace AgroBase.Forms.Operacoes
|
|||
gridDados.Rows[4].Cells[1].Value = (PZEMService.Iniciado && PZEMService.UltimaLeitura != null ? Variaveis.OperacaoEmAndamento.Sensoriamento.PorcentagemBateria.ToString("00.00") : "");
|
||||
gridDados.Rows[5].Cells[1].Value = (PZEMService.Iniciado && PZEMService.UltimaLeitura != null ? PZEMService.UltimaLeitura.Tensao.ToString("0.00") : "");
|
||||
gridDados.Rows[6].Cells[1].Value = (PZEMService.Iniciado && PZEMService.UltimaLeitura != null ? PZEMService.UltimaLeitura.Corrente.ToString("0.00") : "");
|
||||
gridDados.Rows[7].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispAtu.Dados.PercentualReservatorio.ToString("00.00");
|
||||
gridDados.Rows[8].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispAtu.Dados.VolumeReservatorio.ToString("0.000");
|
||||
gridDados.Rows[9].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispAtu.Dados.PressaoLinha.ToString("0.00");
|
||||
gridDados.Rows[10].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispAtu.Dados.BombaPressurizadora.Potencia.ToString("0.00");
|
||||
gridDados.Rows[7].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispSen.Dados.Sensores.FirstOrDefault(x => x.ID == "AB3V").Leituras?[1].ToString("0.00");
|
||||
gridDados.Rows[8].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispSen.Dados.Sensores.FirstOrDefault(x => x.ID == "AB3V").Leituras?[0].ToString("0.00");
|
||||
gridDados.Rows[9].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispSen.Dados.Sensores.FirstOrDefault(x => x.ID == "AB5V").Leituras?[1].ToString("0.00");
|
||||
gridDados.Rows[10].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispSen.Dados.Sensores.FirstOrDefault(x => x.ID == "AB5V").Leituras?[0].ToString("0.00");
|
||||
gridDados.Rows[11].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispSen.Dados.Sensores.FirstOrDefault(x => x.ID == "AB7V2").Leituras?[1].ToString("0.00");
|
||||
gridDados.Rows[12].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispSen.Dados.Sensores.FirstOrDefault(x => x.ID == "AB7V2").Leituras?[0].ToString("0.00");
|
||||
gridDados.Rows[13].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispSen.Dados.Sensores.FirstOrDefault(x => x.ID == "AB12V").Leituras?[1].ToString("0.00");
|
||||
gridDados.Rows[14].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispSen.Dados.Sensores.FirstOrDefault(x => x.ID == "AB12V").Leituras?[0].ToString("0.00");
|
||||
gridDados.Rows[15].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispAtu.Dados.PercentualReservatorio.ToString("00.00");
|
||||
gridDados.Rows[16].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispAtu.Dados.VolumeReservatorio.ToString("0.000");
|
||||
gridDados.Rows[17].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispAtu.Dados.PressaoLinha.ToString("0.00");
|
||||
gridDados.Rows[18].Cells[1].Value = Variaveis.OperacaoEmAndamento.DispAtu.Dados.BombaPressurizadora.Potencia.ToString("0.00");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ namespace AgroBase.Forms.Operacoes
|
|||
private void frmParametrizacaoOperacao_Load(object sender, EventArgs e)
|
||||
{
|
||||
cmbScriptErvas.Items.Clear();
|
||||
cmbScriptErvas.Items.AddRange(new string[] { PythonService.ScriptWeedDetector.Replace(".py", ""), PythonService.ScriptGreenDetector.Replace(".py", "") });
|
||||
cmbScriptErvas.Items.AddRange(new string[] { PythonService.ScriptWeedDetector.Replace(".py", ""), PythonService.ScriptWeedDetectorV7.Replace(".py", ""), PythonService.ScriptGreenDetector.Replace(".py", "") });
|
||||
CarregarParametros();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# init
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,272 @@
|
|||
import numpy as np
|
||||
import random
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from models.common import Conv, DWConv
|
||||
from utils.google_utils import attempt_download
|
||||
|
||||
|
||||
class CrossConv(nn.Module):
|
||||
# Cross Convolution Downsample
|
||||
def __init__(self, c1, c2, k=3, s=1, g=1, e=1.0, shortcut=False):
|
||||
# ch_in, ch_out, kernel, stride, groups, expansion, shortcut
|
||||
super(CrossConv, self).__init__()
|
||||
c_ = int(c2 * e) # hidden channels
|
||||
self.cv1 = Conv(c1, c_, (1, k), (1, s))
|
||||
self.cv2 = Conv(c_, c2, (k, 1), (s, 1), g=g)
|
||||
self.add = shortcut and c1 == c2
|
||||
|
||||
def forward(self, x):
|
||||
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
|
||||
|
||||
|
||||
class Sum(nn.Module):
|
||||
# Weighted sum of 2 or more layers https://arxiv.org/abs/1911.09070
|
||||
def __init__(self, n, weight=False): # n: number of inputs
|
||||
super(Sum, self).__init__()
|
||||
self.weight = weight # apply weights boolean
|
||||
self.iter = range(n - 1) # iter object
|
||||
if weight:
|
||||
self.w = nn.Parameter(-torch.arange(1., n) / 2, requires_grad=True) # layer weights
|
||||
|
||||
def forward(self, x):
|
||||
y = x[0] # no weight
|
||||
if self.weight:
|
||||
w = torch.sigmoid(self.w) * 2
|
||||
for i in self.iter:
|
||||
y = y + x[i + 1] * w[i]
|
||||
else:
|
||||
for i in self.iter:
|
||||
y = y + x[i + 1]
|
||||
return y
|
||||
|
||||
|
||||
class MixConv2d(nn.Module):
|
||||
# Mixed Depthwise Conv https://arxiv.org/abs/1907.09595
|
||||
def __init__(self, c1, c2, k=(1, 3), s=1, equal_ch=True):
|
||||
super(MixConv2d, self).__init__()
|
||||
groups = len(k)
|
||||
if equal_ch: # equal c_ per group
|
||||
i = torch.linspace(0, groups - 1E-6, c2).floor() # c2 indices
|
||||
c_ = [(i == g).sum() for g in range(groups)] # intermediate channels
|
||||
else: # equal weight.numel() per group
|
||||
b = [c2] + [0] * groups
|
||||
a = np.eye(groups + 1, groups, k=-1)
|
||||
a -= np.roll(a, 1, axis=1)
|
||||
a *= np.array(k) ** 2
|
||||
a[0] = 1
|
||||
c_ = np.linalg.lstsq(a, b, rcond=None)[0].round() # solve for equal weight indices, ax = b
|
||||
|
||||
self.m = nn.ModuleList([nn.Conv2d(c1, int(c_[g]), k[g], s, k[g] // 2, bias=False) for g in range(groups)])
|
||||
self.bn = nn.BatchNorm2d(c2)
|
||||
self.act = nn.LeakyReLU(0.1, inplace=True)
|
||||
|
||||
def forward(self, x):
|
||||
return x + self.act(self.bn(torch.cat([m(x) for m in self.m], 1)))
|
||||
|
||||
|
||||
class Ensemble(nn.ModuleList):
|
||||
# Ensemble of models
|
||||
def __init__(self):
|
||||
super(Ensemble, self).__init__()
|
||||
|
||||
def forward(self, x, augment=False):
|
||||
y = []
|
||||
for module in self:
|
||||
y.append(module(x, augment)[0])
|
||||
# y = torch.stack(y).max(0)[0] # max ensemble
|
||||
# y = torch.stack(y).mean(0) # mean ensemble
|
||||
y = torch.cat(y, 1) # nms ensemble
|
||||
return y, None # inference, train output
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ORT_NMS(torch.autograd.Function):
|
||||
'''ONNX-Runtime NMS operation'''
|
||||
@staticmethod
|
||||
def forward(ctx,
|
||||
boxes,
|
||||
scores,
|
||||
max_output_boxes_per_class=torch.tensor([100]),
|
||||
iou_threshold=torch.tensor([0.45]),
|
||||
score_threshold=torch.tensor([0.25])):
|
||||
device = boxes.device
|
||||
batch = scores.shape[0]
|
||||
num_det = random.randint(0, 100)
|
||||
batches = torch.randint(0, batch, (num_det,)).sort()[0].to(device)
|
||||
idxs = torch.arange(100, 100 + num_det).to(device)
|
||||
zeros = torch.zeros((num_det,), dtype=torch.int64).to(device)
|
||||
selected_indices = torch.cat([batches[None], zeros[None], idxs[None]], 0).T.contiguous()
|
||||
selected_indices = selected_indices.to(torch.int64)
|
||||
return selected_indices
|
||||
|
||||
@staticmethod
|
||||
def symbolic(g, boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold):
|
||||
return g.op("NonMaxSuppression", boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold)
|
||||
|
||||
|
||||
class TRT_NMS(torch.autograd.Function):
|
||||
'''TensorRT NMS operation'''
|
||||
@staticmethod
|
||||
def forward(
|
||||
ctx,
|
||||
boxes,
|
||||
scores,
|
||||
background_class=-1,
|
||||
box_coding=1,
|
||||
iou_threshold=0.45,
|
||||
max_output_boxes=100,
|
||||
plugin_version="1",
|
||||
score_activation=0,
|
||||
score_threshold=0.25,
|
||||
):
|
||||
batch_size, num_boxes, num_classes = scores.shape
|
||||
num_det = torch.randint(0, max_output_boxes, (batch_size, 1), dtype=torch.int32)
|
||||
det_boxes = torch.randn(batch_size, max_output_boxes, 4)
|
||||
det_scores = torch.randn(batch_size, max_output_boxes)
|
||||
det_classes = torch.randint(0, num_classes, (batch_size, max_output_boxes), dtype=torch.int32)
|
||||
return num_det, det_boxes, det_scores, det_classes
|
||||
|
||||
@staticmethod
|
||||
def symbolic(g,
|
||||
boxes,
|
||||
scores,
|
||||
background_class=-1,
|
||||
box_coding=1,
|
||||
iou_threshold=0.45,
|
||||
max_output_boxes=100,
|
||||
plugin_version="1",
|
||||
score_activation=0,
|
||||
score_threshold=0.25):
|
||||
out = g.op("TRT::EfficientNMS_TRT",
|
||||
boxes,
|
||||
scores,
|
||||
background_class_i=background_class,
|
||||
box_coding_i=box_coding,
|
||||
iou_threshold_f=iou_threshold,
|
||||
max_output_boxes_i=max_output_boxes,
|
||||
plugin_version_s=plugin_version,
|
||||
score_activation_i=score_activation,
|
||||
score_threshold_f=score_threshold,
|
||||
outputs=4)
|
||||
nums, boxes, scores, classes = out
|
||||
return nums, boxes, scores, classes
|
||||
|
||||
|
||||
class ONNX_ORT(nn.Module):
|
||||
'''onnx module with ONNX-Runtime NMS operation.'''
|
||||
def __init__(self, max_obj=100, iou_thres=0.45, score_thres=0.25, max_wh=640, device=None, n_classes=80):
|
||||
super().__init__()
|
||||
self.device = device if device else torch.device("cpu")
|
||||
self.max_obj = torch.tensor([max_obj]).to(device)
|
||||
self.iou_threshold = torch.tensor([iou_thres]).to(device)
|
||||
self.score_threshold = torch.tensor([score_thres]).to(device)
|
||||
self.max_wh = max_wh # if max_wh != 0 : non-agnostic else : agnostic
|
||||
self.convert_matrix = torch.tensor([[1, 0, 1, 0], [0, 1, 0, 1], [-0.5, 0, 0.5, 0], [0, -0.5, 0, 0.5]],
|
||||
dtype=torch.float32,
|
||||
device=self.device)
|
||||
self.n_classes=n_classes
|
||||
|
||||
def forward(self, x):
|
||||
boxes = x[:, :, :4]
|
||||
conf = x[:, :, 4:5]
|
||||
scores = x[:, :, 5:]
|
||||
if self.n_classes == 1:
|
||||
scores = conf # for models with one class, cls_loss is 0 and cls_conf is always 0.5,
|
||||
# so there is no need to multiplicate.
|
||||
else:
|
||||
scores *= conf # conf = obj_conf * cls_conf
|
||||
boxes @= self.convert_matrix
|
||||
max_score, category_id = scores.max(2, keepdim=True)
|
||||
dis = category_id.float() * self.max_wh
|
||||
nmsbox = boxes + dis
|
||||
max_score_tp = max_score.transpose(1, 2).contiguous()
|
||||
selected_indices = ORT_NMS.apply(nmsbox, max_score_tp, self.max_obj, self.iou_threshold, self.score_threshold)
|
||||
X, Y = selected_indices[:, 0], selected_indices[:, 2]
|
||||
selected_boxes = boxes[X, Y, :]
|
||||
selected_categories = category_id[X, Y, :].float()
|
||||
selected_scores = max_score[X, Y, :]
|
||||
X = X.unsqueeze(1).float()
|
||||
return torch.cat([X, selected_boxes, selected_categories, selected_scores], 1)
|
||||
|
||||
class ONNX_TRT(nn.Module):
|
||||
'''onnx module with TensorRT NMS operation.'''
|
||||
def __init__(self, max_obj=100, iou_thres=0.45, score_thres=0.25, max_wh=None ,device=None, n_classes=80):
|
||||
super().__init__()
|
||||
assert max_wh is None
|
||||
self.device = device if device else torch.device('cpu')
|
||||
self.background_class = -1,
|
||||
self.box_coding = 1,
|
||||
self.iou_threshold = iou_thres
|
||||
self.max_obj = max_obj
|
||||
self.plugin_version = '1'
|
||||
self.score_activation = 0
|
||||
self.score_threshold = score_thres
|
||||
self.n_classes=n_classes
|
||||
|
||||
def forward(self, x):
|
||||
boxes = x[:, :, :4]
|
||||
conf = x[:, :, 4:5]
|
||||
scores = x[:, :, 5:]
|
||||
if self.n_classes == 1:
|
||||
scores = conf # for models with one class, cls_loss is 0 and cls_conf is always 0.5,
|
||||
# so there is no need to multiplicate.
|
||||
else:
|
||||
scores *= conf # conf = obj_conf * cls_conf
|
||||
num_det, det_boxes, det_scores, det_classes = TRT_NMS.apply(boxes, scores, self.background_class, self.box_coding,
|
||||
self.iou_threshold, self.max_obj,
|
||||
self.plugin_version, self.score_activation,
|
||||
self.score_threshold)
|
||||
return num_det, det_boxes, det_scores, det_classes
|
||||
|
||||
|
||||
class End2End(nn.Module):
|
||||
'''export onnx or tensorrt model with NMS operation.'''
|
||||
def __init__(self, model, max_obj=100, iou_thres=0.45, score_thres=0.25, max_wh=None, device=None, n_classes=80):
|
||||
super().__init__()
|
||||
device = device if device else torch.device('cpu')
|
||||
assert isinstance(max_wh,(int)) or max_wh is None
|
||||
self.model = model.to(device)
|
||||
self.model.model[-1].end2end = True
|
||||
self.patch_model = ONNX_TRT if max_wh is None else ONNX_ORT
|
||||
self.end2end = self.patch_model(max_obj, iou_thres, score_thres, max_wh, device, n_classes)
|
||||
self.end2end.eval()
|
||||
|
||||
def forward(self, x):
|
||||
x = self.model(x)
|
||||
x = self.end2end(x)
|
||||
return x
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def attempt_load(weights, map_location=None):
|
||||
# Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a
|
||||
model = Ensemble()
|
||||
for w in weights if isinstance(weights, list) else [weights]:
|
||||
attempt_download(w)
|
||||
ckpt = torch.load(w, map_location=map_location) # load
|
||||
model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().fuse().eval()) # FP32 model
|
||||
|
||||
# Compatibility updates
|
||||
for m in model.modules():
|
||||
if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU]:
|
||||
m.inplace = True # pytorch 1.7.0 compatibility
|
||||
elif type(m) is nn.Upsample:
|
||||
m.recompute_scale_factor = None # torch 1.11.0 compatibility
|
||||
elif type(m) is Conv:
|
||||
m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility
|
||||
|
||||
if len(model) == 1:
|
||||
return model[-1] # return model
|
||||
else:
|
||||
print('Ensemble created with %s\n' % weights)
|
||||
for k in ['names', 'stride']:
|
||||
setattr(model, k, getattr(model[-1], k))
|
||||
return model # return ensemble
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,843 @@
|
|||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
|
||||
sys.path.append('./') # to run '$ python *.py' files in subdirectories
|
||||
logger = logging.getLogger(__name__)
|
||||
import torch
|
||||
from models.common import *
|
||||
from models.experimental import *
|
||||
from utils.autoanchor import check_anchor_order
|
||||
from utils.general import make_divisible, check_file, set_logging
|
||||
from utils.torch_utils import time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, \
|
||||
select_device, copy_attr
|
||||
from utils.loss import SigmoidBin
|
||||
|
||||
try:
|
||||
import thop # for FLOPS computation
|
||||
except ImportError:
|
||||
thop = None
|
||||
|
||||
|
||||
class Detect(nn.Module):
|
||||
stride = None # strides computed during build
|
||||
export = False # onnx export
|
||||
end2end = False
|
||||
include_nms = False
|
||||
concat = False
|
||||
|
||||
def __init__(self, nc=80, anchors=(), ch=()): # detection layer
|
||||
super(Detect, self).__init__()
|
||||
self.nc = nc # number of classes
|
||||
self.no = nc + 5 # number of outputs per anchor
|
||||
self.nl = len(anchors) # number of detection layers
|
||||
self.na = len(anchors[0]) // 2 # number of anchors
|
||||
self.grid = [torch.zeros(1)] * self.nl # init grid
|
||||
a = torch.tensor(anchors).float().view(self.nl, -1, 2)
|
||||
self.register_buffer('anchors', a) # shape(nl,na,2)
|
||||
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2)
|
||||
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv
|
||||
|
||||
def forward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](x[i]) # conv
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
y = x[i].sigmoid()
|
||||
if not torch.onnx.is_in_onnx_export():
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
else:
|
||||
xy, wh, conf = y.split((2, 2, self.nc + 1), 4) # y.tensor_split((2, 4, 5), 4) # torch 1.8.0
|
||||
xy = xy * (2. * self.stride[i]) + (self.stride[i] * (self.grid[i] - 0.5)) # new xy
|
||||
wh = wh ** 2 * (4 * self.anchor_grid[i].data) # new wh
|
||||
y = torch.cat((xy, wh, conf), 4)
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
if self.training:
|
||||
out = x
|
||||
elif self.end2end:
|
||||
out = torch.cat(z, 1)
|
||||
elif self.include_nms:
|
||||
z = self.convert(z)
|
||||
out = (z, )
|
||||
elif self.concat:
|
||||
out = torch.cat(z, 1)
|
||||
else:
|
||||
out = (torch.cat(z, 1), x)
|
||||
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _make_grid(nx=20, ny=20):
|
||||
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
|
||||
return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
|
||||
|
||||
def convert(self, z):
|
||||
z = torch.cat(z, 1)
|
||||
box = z[:, :, :4]
|
||||
conf = z[:, :, 4:5]
|
||||
score = z[:, :, 5:]
|
||||
score *= conf
|
||||
convert_matrix = torch.tensor([[1, 0, 1, 0], [0, 1, 0, 1], [-0.5, 0, 0.5, 0], [0, -0.5, 0, 0.5]],
|
||||
dtype=torch.float32,
|
||||
device=z.device)
|
||||
box @= convert_matrix
|
||||
return (box, score)
|
||||
|
||||
|
||||
class IDetect(nn.Module):
|
||||
stride = None # strides computed during build
|
||||
export = False # onnx export
|
||||
end2end = False
|
||||
include_nms = False
|
||||
concat = False
|
||||
|
||||
def __init__(self, nc=80, anchors=(), ch=()): # detection layer
|
||||
super(IDetect, self).__init__()
|
||||
self.nc = nc # number of classes
|
||||
self.no = nc + 5 # number of outputs per anchor
|
||||
self.nl = len(anchors) # number of detection layers
|
||||
self.na = len(anchors[0]) // 2 # number of anchors
|
||||
self.grid = [torch.zeros(1)] * self.nl # init grid
|
||||
a = torch.tensor(anchors).float().view(self.nl, -1, 2)
|
||||
self.register_buffer('anchors', a) # shape(nl,na,2)
|
||||
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2)
|
||||
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv
|
||||
|
||||
self.ia = nn.ModuleList(ImplicitA(x) for x in ch)
|
||||
self.im = nn.ModuleList(ImplicitM(self.no * self.na) for _ in ch)
|
||||
|
||||
def forward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](self.ia[i](x[i])) # conv
|
||||
x[i] = self.im[i](x[i])
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
|
||||
y = x[i].sigmoid()
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
return x if self.training else (torch.cat(z, 1), x)
|
||||
|
||||
def fuseforward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](x[i]) # conv
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
|
||||
y = x[i].sigmoid()
|
||||
if not torch.onnx.is_in_onnx_export():
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
else:
|
||||
xy, wh, conf = y.split((2, 2, self.nc + 1), 4) # y.tensor_split((2, 4, 5), 4) # torch 1.8.0
|
||||
xy = xy * (2. * self.stride[i]) + (self.stride[i] * (self.grid[i] - 0.5)) # new xy
|
||||
wh = wh ** 2 * (4 * self.anchor_grid[i].data) # new wh
|
||||
y = torch.cat((xy, wh, conf), 4)
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
if self.training:
|
||||
out = x
|
||||
elif self.end2end:
|
||||
out = torch.cat(z, 1)
|
||||
elif self.include_nms:
|
||||
z = self.convert(z)
|
||||
out = (z, )
|
||||
elif self.concat:
|
||||
out = torch.cat(z, 1)
|
||||
else:
|
||||
out = (torch.cat(z, 1), x)
|
||||
|
||||
return out
|
||||
|
||||
def fuse(self):
|
||||
print("IDetect.fuse")
|
||||
# fuse ImplicitA and Convolution
|
||||
for i in range(len(self.m)):
|
||||
c1,c2,_,_ = self.m[i].weight.shape
|
||||
c1_,c2_, _,_ = self.ia[i].implicit.shape
|
||||
self.m[i].bias += torch.matmul(self.m[i].weight.reshape(c1,c2),self.ia[i].implicit.reshape(c2_,c1_)).squeeze(1)
|
||||
|
||||
# fuse ImplicitM and Convolution
|
||||
for i in range(len(self.m)):
|
||||
c1,c2, _,_ = self.im[i].implicit.shape
|
||||
self.m[i].bias *= self.im[i].implicit.reshape(c2)
|
||||
self.m[i].weight *= self.im[i].implicit.transpose(0,1)
|
||||
|
||||
@staticmethod
|
||||
def _make_grid(nx=20, ny=20):
|
||||
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
|
||||
return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
|
||||
|
||||
def convert(self, z):
|
||||
z = torch.cat(z, 1)
|
||||
box = z[:, :, :4]
|
||||
conf = z[:, :, 4:5]
|
||||
score = z[:, :, 5:]
|
||||
score *= conf
|
||||
convert_matrix = torch.tensor([[1, 0, 1, 0], [0, 1, 0, 1], [-0.5, 0, 0.5, 0], [0, -0.5, 0, 0.5]],
|
||||
dtype=torch.float32,
|
||||
device=z.device)
|
||||
box @= convert_matrix
|
||||
return (box, score)
|
||||
|
||||
|
||||
class IKeypoint(nn.Module):
|
||||
stride = None # strides computed during build
|
||||
export = False # onnx export
|
||||
|
||||
def __init__(self, nc=80, anchors=(), nkpt=17, ch=(), inplace=True, dw_conv_kpt=False): # detection layer
|
||||
super(IKeypoint, self).__init__()
|
||||
self.nc = nc # number of classes
|
||||
self.nkpt = nkpt
|
||||
self.dw_conv_kpt = dw_conv_kpt
|
||||
self.no_det=(nc + 5) # number of outputs per anchor for box and class
|
||||
self.no_kpt = 3*self.nkpt ## number of outputs per anchor for keypoints
|
||||
self.no = self.no_det+self.no_kpt
|
||||
self.nl = len(anchors) # number of detection layers
|
||||
self.na = len(anchors[0]) // 2 # number of anchors
|
||||
self.grid = [torch.zeros(1)] * self.nl # init grid
|
||||
self.flip_test = False
|
||||
a = torch.tensor(anchors).float().view(self.nl, -1, 2)
|
||||
self.register_buffer('anchors', a) # shape(nl,na,2)
|
||||
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2)
|
||||
self.m = nn.ModuleList(nn.Conv2d(x, self.no_det * self.na, 1) for x in ch) # output conv
|
||||
|
||||
self.ia = nn.ModuleList(ImplicitA(x) for x in ch)
|
||||
self.im = nn.ModuleList(ImplicitM(self.no_det * self.na) for _ in ch)
|
||||
|
||||
if self.nkpt is not None:
|
||||
if self.dw_conv_kpt: #keypoint head is slightly more complex
|
||||
self.m_kpt = nn.ModuleList(
|
||||
nn.Sequential(DWConv(x, x, k=3), Conv(x,x),
|
||||
DWConv(x, x, k=3), Conv(x, x),
|
||||
DWConv(x, x, k=3), Conv(x,x),
|
||||
DWConv(x, x, k=3), Conv(x, x),
|
||||
DWConv(x, x, k=3), Conv(x, x),
|
||||
DWConv(x, x, k=3), nn.Conv2d(x, self.no_kpt * self.na, 1)) for x in ch)
|
||||
else: #keypoint head is a single convolution
|
||||
self.m_kpt = nn.ModuleList(nn.Conv2d(x, self.no_kpt * self.na, 1) for x in ch)
|
||||
|
||||
self.inplace = inplace # use in-place ops (e.g. slice assignment)
|
||||
|
||||
def forward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
if self.nkpt is None or self.nkpt==0:
|
||||
x[i] = self.im[i](self.m[i](self.ia[i](x[i]))) # conv
|
||||
else :
|
||||
x[i] = torch.cat((self.im[i](self.m[i](self.ia[i](x[i]))), self.m_kpt[i](x[i])), axis=1)
|
||||
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
x_det = x[i][..., :6]
|
||||
x_kpt = x[i][..., 6:]
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
kpt_grid_x = self.grid[i][..., 0:1]
|
||||
kpt_grid_y = self.grid[i][..., 1:2]
|
||||
|
||||
if self.nkpt == 0:
|
||||
y = x[i].sigmoid()
|
||||
else:
|
||||
y = x_det.sigmoid()
|
||||
|
||||
if self.inplace:
|
||||
xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i].view(1, self.na, 1, 1, 2) # wh
|
||||
if self.nkpt != 0:
|
||||
x_kpt[..., 0::3] = (x_kpt[..., ::3] * 2. - 0.5 + kpt_grid_x.repeat(1,1,1,1,17)) * self.stride[i] # xy
|
||||
x_kpt[..., 1::3] = (x_kpt[..., 1::3] * 2. - 0.5 + kpt_grid_y.repeat(1,1,1,1,17)) * self.stride[i] # xy
|
||||
#x_kpt[..., 0::3] = (x_kpt[..., ::3] + kpt_grid_x.repeat(1,1,1,1,17)) * self.stride[i] # xy
|
||||
#x_kpt[..., 1::3] = (x_kpt[..., 1::3] + kpt_grid_y.repeat(1,1,1,1,17)) * self.stride[i] # xy
|
||||
#print('=============')
|
||||
#print(self.anchor_grid[i].shape)
|
||||
#print(self.anchor_grid[i][...,0].unsqueeze(4).shape)
|
||||
#print(x_kpt[..., 0::3].shape)
|
||||
#x_kpt[..., 0::3] = ((x_kpt[..., 0::3].tanh() * 2.) ** 3 * self.anchor_grid[i][...,0].unsqueeze(4).repeat(1,1,1,1,self.nkpt)) + kpt_grid_x.repeat(1,1,1,1,17) * self.stride[i] # xy
|
||||
#x_kpt[..., 1::3] = ((x_kpt[..., 1::3].tanh() * 2.) ** 3 * self.anchor_grid[i][...,1].unsqueeze(4).repeat(1,1,1,1,self.nkpt)) + kpt_grid_y.repeat(1,1,1,1,17) * self.stride[i] # xy
|
||||
#x_kpt[..., 0::3] = (((x_kpt[..., 0::3].sigmoid() * 4.) ** 2 - 8.) * self.anchor_grid[i][...,0].unsqueeze(4).repeat(1,1,1,1,self.nkpt)) + kpt_grid_x.repeat(1,1,1,1,17) * self.stride[i] # xy
|
||||
#x_kpt[..., 1::3] = (((x_kpt[..., 1::3].sigmoid() * 4.) ** 2 - 8.) * self.anchor_grid[i][...,1].unsqueeze(4).repeat(1,1,1,1,self.nkpt)) + kpt_grid_y.repeat(1,1,1,1,17) * self.stride[i] # xy
|
||||
x_kpt[..., 2::3] = x_kpt[..., 2::3].sigmoid()
|
||||
|
||||
y = torch.cat((xy, wh, y[..., 4:], x_kpt), dim = -1)
|
||||
|
||||
else: # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953
|
||||
xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
if self.nkpt != 0:
|
||||
y[..., 6:] = (y[..., 6:] * 2. - 0.5 + self.grid[i].repeat((1,1,1,1,self.nkpt))) * self.stride[i] # xy
|
||||
y = torch.cat((xy, wh, y[..., 4:]), -1)
|
||||
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
return x if self.training else (torch.cat(z, 1), x)
|
||||
|
||||
@staticmethod
|
||||
def _make_grid(nx=20, ny=20):
|
||||
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
|
||||
return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
|
||||
|
||||
|
||||
class IAuxDetect(nn.Module):
|
||||
stride = None # strides computed during build
|
||||
export = False # onnx export
|
||||
end2end = False
|
||||
include_nms = False
|
||||
concat = False
|
||||
|
||||
def __init__(self, nc=80, anchors=(), ch=()): # detection layer
|
||||
super(IAuxDetect, self).__init__()
|
||||
self.nc = nc # number of classes
|
||||
self.no = nc + 5 # number of outputs per anchor
|
||||
self.nl = len(anchors) # number of detection layers
|
||||
self.na = len(anchors[0]) // 2 # number of anchors
|
||||
self.grid = [torch.zeros(1)] * self.nl # init grid
|
||||
a = torch.tensor(anchors).float().view(self.nl, -1, 2)
|
||||
self.register_buffer('anchors', a) # shape(nl,na,2)
|
||||
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2)
|
||||
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch[:self.nl]) # output conv
|
||||
self.m2 = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch[self.nl:]) # output conv
|
||||
|
||||
self.ia = nn.ModuleList(ImplicitA(x) for x in ch[:self.nl])
|
||||
self.im = nn.ModuleList(ImplicitM(self.no * self.na) for _ in ch[:self.nl])
|
||||
|
||||
def forward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](self.ia[i](x[i])) # conv
|
||||
x[i] = self.im[i](x[i])
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
x[i+self.nl] = self.m2[i](x[i+self.nl])
|
||||
x[i+self.nl] = x[i+self.nl].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
|
||||
y = x[i].sigmoid()
|
||||
if not torch.onnx.is_in_onnx_export():
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
else:
|
||||
xy, wh, conf = y.split((2, 2, self.nc + 1), 4) # y.tensor_split((2, 4, 5), 4) # torch 1.8.0
|
||||
xy = xy * (2. * self.stride[i]) + (self.stride[i] * (self.grid[i] - 0.5)) # new xy
|
||||
wh = wh ** 2 * (4 * self.anchor_grid[i].data) # new wh
|
||||
y = torch.cat((xy, wh, conf), 4)
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
return x if self.training else (torch.cat(z, 1), x[:self.nl])
|
||||
|
||||
def fuseforward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](x[i]) # conv
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
|
||||
y = x[i].sigmoid()
|
||||
if not torch.onnx.is_in_onnx_export():
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
else:
|
||||
xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i].data # wh
|
||||
y = torch.cat((xy, wh, y[..., 4:]), -1)
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
if self.training:
|
||||
out = x
|
||||
elif self.end2end:
|
||||
out = torch.cat(z, 1)
|
||||
elif self.include_nms:
|
||||
z = self.convert(z)
|
||||
out = (z, )
|
||||
elif self.concat:
|
||||
out = torch.cat(z, 1)
|
||||
else:
|
||||
out = (torch.cat(z, 1), x)
|
||||
|
||||
return out
|
||||
|
||||
def fuse(self):
|
||||
print("IAuxDetect.fuse")
|
||||
# fuse ImplicitA and Convolution
|
||||
for i in range(len(self.m)):
|
||||
c1,c2,_,_ = self.m[i].weight.shape
|
||||
c1_,c2_, _,_ = self.ia[i].implicit.shape
|
||||
self.m[i].bias += torch.matmul(self.m[i].weight.reshape(c1,c2),self.ia[i].implicit.reshape(c2_,c1_)).squeeze(1)
|
||||
|
||||
# fuse ImplicitM and Convolution
|
||||
for i in range(len(self.m)):
|
||||
c1,c2, _,_ = self.im[i].implicit.shape
|
||||
self.m[i].bias *= self.im[i].implicit.reshape(c2)
|
||||
self.m[i].weight *= self.im[i].implicit.transpose(0,1)
|
||||
|
||||
@staticmethod
|
||||
def _make_grid(nx=20, ny=20):
|
||||
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
|
||||
return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
|
||||
|
||||
def convert(self, z):
|
||||
z = torch.cat(z, 1)
|
||||
box = z[:, :, :4]
|
||||
conf = z[:, :, 4:5]
|
||||
score = z[:, :, 5:]
|
||||
score *= conf
|
||||
convert_matrix = torch.tensor([[1, 0, 1, 0], [0, 1, 0, 1], [-0.5, 0, 0.5, 0], [0, -0.5, 0, 0.5]],
|
||||
dtype=torch.float32,
|
||||
device=z.device)
|
||||
box @= convert_matrix
|
||||
return (box, score)
|
||||
|
||||
|
||||
class IBin(nn.Module):
|
||||
stride = None # strides computed during build
|
||||
export = False # onnx export
|
||||
|
||||
def __init__(self, nc=80, anchors=(), ch=(), bin_count=21): # detection layer
|
||||
super(IBin, self).__init__()
|
||||
self.nc = nc # number of classes
|
||||
self.bin_count = bin_count
|
||||
|
||||
self.w_bin_sigmoid = SigmoidBin(bin_count=self.bin_count, min=0.0, max=4.0)
|
||||
self.h_bin_sigmoid = SigmoidBin(bin_count=self.bin_count, min=0.0, max=4.0)
|
||||
# classes, x,y,obj
|
||||
self.no = nc + 3 + \
|
||||
self.w_bin_sigmoid.get_length() + self.h_bin_sigmoid.get_length() # w-bce, h-bce
|
||||
# + self.x_bin_sigmoid.get_length() + self.y_bin_sigmoid.get_length()
|
||||
|
||||
self.nl = len(anchors) # number of detection layers
|
||||
self.na = len(anchors[0]) // 2 # number of anchors
|
||||
self.grid = [torch.zeros(1)] * self.nl # init grid
|
||||
a = torch.tensor(anchors).float().view(self.nl, -1, 2)
|
||||
self.register_buffer('anchors', a) # shape(nl,na,2)
|
||||
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2)
|
||||
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv
|
||||
|
||||
self.ia = nn.ModuleList(ImplicitA(x) for x in ch)
|
||||
self.im = nn.ModuleList(ImplicitM(self.no * self.na) for _ in ch)
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
#self.x_bin_sigmoid.use_fw_regression = True
|
||||
#self.y_bin_sigmoid.use_fw_regression = True
|
||||
self.w_bin_sigmoid.use_fw_regression = True
|
||||
self.h_bin_sigmoid.use_fw_regression = True
|
||||
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](self.ia[i](x[i])) # conv
|
||||
x[i] = self.im[i](x[i])
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
|
||||
y = x[i].sigmoid()
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
#y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
|
||||
|
||||
#px = (self.x_bin_sigmoid.forward(y[..., 0:12]) + self.grid[i][..., 0]) * self.stride[i]
|
||||
#py = (self.y_bin_sigmoid.forward(y[..., 12:24]) + self.grid[i][..., 1]) * self.stride[i]
|
||||
|
||||
pw = self.w_bin_sigmoid.forward(y[..., 2:24]) * self.anchor_grid[i][..., 0]
|
||||
ph = self.h_bin_sigmoid.forward(y[..., 24:46]) * self.anchor_grid[i][..., 1]
|
||||
|
||||
#y[..., 0] = px
|
||||
#y[..., 1] = py
|
||||
y[..., 2] = pw
|
||||
y[..., 3] = ph
|
||||
|
||||
y = torch.cat((y[..., 0:4], y[..., 46:]), dim=-1)
|
||||
|
||||
z.append(y.view(bs, -1, y.shape[-1]))
|
||||
|
||||
return x if self.training else (torch.cat(z, 1), x)
|
||||
|
||||
@staticmethod
|
||||
def _make_grid(nx=20, ny=20):
|
||||
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
|
||||
return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, cfg='yolor-csp-c.yaml', ch=3, nc=None, anchors=None): # model, input channels, number of classes
|
||||
super(Model, self).__init__()
|
||||
self.traced = False
|
||||
if isinstance(cfg, dict):
|
||||
self.yaml = cfg # model dict
|
||||
else: # is *.yaml
|
||||
import yaml # for torch hub
|
||||
self.yaml_file = Path(cfg).name
|
||||
with open(cfg) as f:
|
||||
self.yaml = yaml.load(f, Loader=yaml.SafeLoader) # model dict
|
||||
|
||||
# Define model
|
||||
ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels
|
||||
if nc and nc != self.yaml['nc']:
|
||||
logger.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
|
||||
self.yaml['nc'] = nc # override yaml value
|
||||
if anchors:
|
||||
logger.info(f'Overriding model.yaml anchors with anchors={anchors}')
|
||||
self.yaml['anchors'] = round(anchors) # override yaml value
|
||||
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist
|
||||
self.names = [str(i) for i in range(self.yaml['nc'])] # default names
|
||||
# print([x.shape for x in self.forward(torch.zeros(1, ch, 64, 64))])
|
||||
|
||||
# Build strides, anchors
|
||||
m = self.model[-1] # Detect()
|
||||
if isinstance(m, Detect):
|
||||
s = 256 # 2x min stride
|
||||
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
|
||||
check_anchor_order(m)
|
||||
m.anchors /= m.stride.view(-1, 1, 1)
|
||||
self.stride = m.stride
|
||||
self._initialize_biases() # only run once
|
||||
# print('Strides: %s' % m.stride.tolist())
|
||||
if isinstance(m, IDetect):
|
||||
s = 256 # 2x min stride
|
||||
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
|
||||
check_anchor_order(m)
|
||||
m.anchors /= m.stride.view(-1, 1, 1)
|
||||
self.stride = m.stride
|
||||
self._initialize_biases() # only run once
|
||||
# print('Strides: %s' % m.stride.tolist())
|
||||
if isinstance(m, IAuxDetect):
|
||||
s = 256 # 2x min stride
|
||||
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))[:4]]) # forward
|
||||
#print(m.stride)
|
||||
check_anchor_order(m)
|
||||
m.anchors /= m.stride.view(-1, 1, 1)
|
||||
self.stride = m.stride
|
||||
self._initialize_aux_biases() # only run once
|
||||
# print('Strides: %s' % m.stride.tolist())
|
||||
if isinstance(m, IBin):
|
||||
s = 256 # 2x min stride
|
||||
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
|
||||
check_anchor_order(m)
|
||||
m.anchors /= m.stride.view(-1, 1, 1)
|
||||
self.stride = m.stride
|
||||
self._initialize_biases_bin() # only run once
|
||||
# print('Strides: %s' % m.stride.tolist())
|
||||
if isinstance(m, IKeypoint):
|
||||
s = 256 # 2x min stride
|
||||
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
|
||||
check_anchor_order(m)
|
||||
m.anchors /= m.stride.view(-1, 1, 1)
|
||||
self.stride = m.stride
|
||||
self._initialize_biases_kpt() # only run once
|
||||
# print('Strides: %s' % m.stride.tolist())
|
||||
|
||||
# Init weights, biases
|
||||
initialize_weights(self)
|
||||
self.info()
|
||||
logger.info('')
|
||||
|
||||
def forward(self, x, augment=False, profile=False):
|
||||
if augment:
|
||||
img_size = x.shape[-2:] # height, width
|
||||
s = [1, 0.83, 0.67] # scales
|
||||
f = [None, 3, None] # flips (2-ud, 3-lr)
|
||||
y = [] # outputs
|
||||
for si, fi in zip(s, f):
|
||||
xi = scale_img(x.flip(fi) if fi else x, si, gs=int(self.stride.max()))
|
||||
yi = self.forward_once(xi)[0] # forward
|
||||
# cv2.imwrite(f'img_{si}.jpg', 255 * xi[0].cpu().numpy().transpose((1, 2, 0))[:, :, ::-1]) # save
|
||||
yi[..., :4] /= si # de-scale
|
||||
if fi == 2:
|
||||
yi[..., 1] = img_size[0] - yi[..., 1] # de-flip ud
|
||||
elif fi == 3:
|
||||
yi[..., 0] = img_size[1] - yi[..., 0] # de-flip lr
|
||||
y.append(yi)
|
||||
return torch.cat(y, 1), None # augmented inference, train
|
||||
else:
|
||||
return self.forward_once(x, profile) # single-scale inference, train
|
||||
|
||||
def forward_once(self, x, profile=False):
|
||||
y, dt = [], [] # outputs
|
||||
for m in self.model:
|
||||
if m.f != -1: # if not from previous layer
|
||||
x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers
|
||||
|
||||
if not hasattr(self, 'traced'):
|
||||
self.traced=False
|
||||
|
||||
if self.traced:
|
||||
if isinstance(m, Detect) or isinstance(m, IDetect) or isinstance(m, IAuxDetect) or isinstance(m, IKeypoint):
|
||||
break
|
||||
|
||||
if profile:
|
||||
c = isinstance(m, (Detect, IDetect, IAuxDetect, IBin))
|
||||
o = thop.profile(m, inputs=(x.copy() if c else x,), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPS
|
||||
for _ in range(10):
|
||||
m(x.copy() if c else x)
|
||||
t = time_synchronized()
|
||||
for _ in range(10):
|
||||
m(x.copy() if c else x)
|
||||
dt.append((time_synchronized() - t) * 100)
|
||||
print('%10.1f%10.0f%10.1fms %-40s' % (o, m.np, dt[-1], m.type))
|
||||
|
||||
x = m(x) # run
|
||||
|
||||
y.append(x if m.i in self.save else None) # save output
|
||||
|
||||
if profile:
|
||||
print('%.1fms total' % sum(dt))
|
||||
return x
|
||||
|
||||
def _initialize_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency
|
||||
# https://arxiv.org/abs/1708.02002 section 3.3
|
||||
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
|
||||
m = self.model[-1] # Detect() module
|
||||
for mi, s in zip(m.m, m.stride): # from
|
||||
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
|
||||
b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
|
||||
b.data[:, 5:] += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls
|
||||
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
|
||||
|
||||
def _initialize_aux_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency
|
||||
# https://arxiv.org/abs/1708.02002 section 3.3
|
||||
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
|
||||
m = self.model[-1] # Detect() module
|
||||
for mi, mi2, s in zip(m.m, m.m2, m.stride): # from
|
||||
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
|
||||
b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
|
||||
b.data[:, 5:] += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls
|
||||
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
|
||||
b2 = mi2.bias.view(m.na, -1) # conv.bias(255) to (3,85)
|
||||
b2.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
|
||||
b2.data[:, 5:] += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls
|
||||
mi2.bias = torch.nn.Parameter(b2.view(-1), requires_grad=True)
|
||||
|
||||
def _initialize_biases_bin(self, cf=None): # initialize biases into Detect(), cf is class frequency
|
||||
# https://arxiv.org/abs/1708.02002 section 3.3
|
||||
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
|
||||
m = self.model[-1] # Bin() module
|
||||
bc = m.bin_count
|
||||
for mi, s in zip(m.m, m.stride): # from
|
||||
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
|
||||
old = b[:, (0,1,2,bc+3)].data
|
||||
obj_idx = 2*bc+4
|
||||
b[:, :obj_idx].data += math.log(0.6 / (bc + 1 - 0.99))
|
||||
b[:, obj_idx].data += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
|
||||
b[:, (obj_idx+1):].data += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls
|
||||
b[:, (0,1,2,bc+3)].data = old
|
||||
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
|
||||
|
||||
def _initialize_biases_kpt(self, cf=None): # initialize biases into Detect(), cf is class frequency
|
||||
# https://arxiv.org/abs/1708.02002 section 3.3
|
||||
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
|
||||
m = self.model[-1] # Detect() module
|
||||
for mi, s in zip(m.m, m.stride): # from
|
||||
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
|
||||
b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
|
||||
b.data[:, 5:] += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls
|
||||
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
|
||||
|
||||
def _print_biases(self):
|
||||
m = self.model[-1] # Detect() module
|
||||
for mi in m.m: # from
|
||||
b = mi.bias.detach().view(m.na, -1).T # conv.bias(255) to (3,85)
|
||||
print(('%6g Conv2d.bias:' + '%10.3g' * 6) % (mi.weight.shape[1], *b[:5].mean(1).tolist(), b[5:].mean()))
|
||||
|
||||
# def _print_weights(self):
|
||||
# for m in self.model.modules():
|
||||
# if type(m) is Bottleneck:
|
||||
# print('%10.3g' % (m.w.detach().sigmoid() * 2)) # shortcut weights
|
||||
|
||||
def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers
|
||||
print('Fusing layers... ')
|
||||
for m in self.model.modules():
|
||||
if isinstance(m, RepConv):
|
||||
#print(f" fuse_repvgg_block")
|
||||
m.fuse_repvgg_block()
|
||||
elif isinstance(m, RepConv_OREPA):
|
||||
#print(f" switch_to_deploy")
|
||||
m.switch_to_deploy()
|
||||
elif type(m) is Conv and hasattr(m, 'bn'):
|
||||
m.conv = fuse_conv_and_bn(m.conv, m.bn) # update conv
|
||||
delattr(m, 'bn') # remove batchnorm
|
||||
m.forward = m.fuseforward # update forward
|
||||
elif isinstance(m, (IDetect, IAuxDetect)):
|
||||
m.fuse()
|
||||
m.forward = m.fuseforward
|
||||
self.info()
|
||||
return self
|
||||
|
||||
def nms(self, mode=True): # add or remove NMS module
|
||||
present = type(self.model[-1]) is NMS # last layer is NMS
|
||||
if mode and not present:
|
||||
print('Adding NMS... ')
|
||||
m = NMS() # module
|
||||
m.f = -1 # from
|
||||
m.i = self.model[-1].i + 1 # index
|
||||
self.model.add_module(name='%s' % m.i, module=m) # add
|
||||
self.eval()
|
||||
elif not mode and present:
|
||||
print('Removing NMS... ')
|
||||
self.model = self.model[:-1] # remove
|
||||
return self
|
||||
|
||||
def autoshape(self): # add autoShape module
|
||||
print('Adding autoShape... ')
|
||||
m = autoShape(self) # wrap model
|
||||
copy_attr(m, self, include=('yaml', 'nc', 'hyp', 'names', 'stride'), exclude=()) # copy attributes
|
||||
return m
|
||||
|
||||
def info(self, verbose=False, img_size=640): # print model information
|
||||
model_info(self, verbose, img_size)
|
||||
|
||||
|
||||
def parse_model(d, ch): # model_dict, input_channels(3)
|
||||
logger.info('\n%3s%18s%3s%10s %-40s%-30s' % ('', 'from', 'n', 'params', 'module', 'arguments'))
|
||||
anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']
|
||||
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors
|
||||
no = na * (nc + 5) # number of outputs = anchors * (classes + 5)
|
||||
|
||||
layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out
|
||||
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']): # from, number, module, args
|
||||
m = eval(m) if isinstance(m, str) else m # eval strings
|
||||
for j, a in enumerate(args):
|
||||
try:
|
||||
args[j] = eval(a) if isinstance(a, str) else a # eval strings
|
||||
except:
|
||||
pass
|
||||
|
||||
n = max(round(n * gd), 1) if n > 1 else n # depth gain
|
||||
if m in [nn.Conv2d, Conv, RobustConv, RobustConv2, DWConv, GhostConv, RepConv, RepConv_OREPA, DownC,
|
||||
SPP, SPPF, SPPCSPC, GhostSPPCSPC, MixConv2d, Focus, Stem, GhostStem, CrossConv,
|
||||
Bottleneck, BottleneckCSPA, BottleneckCSPB, BottleneckCSPC,
|
||||
RepBottleneck, RepBottleneckCSPA, RepBottleneckCSPB, RepBottleneckCSPC,
|
||||
Res, ResCSPA, ResCSPB, ResCSPC,
|
||||
RepRes, RepResCSPA, RepResCSPB, RepResCSPC,
|
||||
ResX, ResXCSPA, ResXCSPB, ResXCSPC,
|
||||
RepResX, RepResXCSPA, RepResXCSPB, RepResXCSPC,
|
||||
Ghost, GhostCSPA, GhostCSPB, GhostCSPC,
|
||||
SwinTransformerBlock, STCSPA, STCSPB, STCSPC,
|
||||
SwinTransformer2Block, ST2CSPA, ST2CSPB, ST2CSPC]:
|
||||
c1, c2 = ch[f], args[0]
|
||||
if c2 != no: # if not output
|
||||
c2 = make_divisible(c2 * gw, 8)
|
||||
|
||||
args = [c1, c2, *args[1:]]
|
||||
if m in [DownC, SPPCSPC, GhostSPPCSPC,
|
||||
BottleneckCSPA, BottleneckCSPB, BottleneckCSPC,
|
||||
RepBottleneckCSPA, RepBottleneckCSPB, RepBottleneckCSPC,
|
||||
ResCSPA, ResCSPB, ResCSPC,
|
||||
RepResCSPA, RepResCSPB, RepResCSPC,
|
||||
ResXCSPA, ResXCSPB, ResXCSPC,
|
||||
RepResXCSPA, RepResXCSPB, RepResXCSPC,
|
||||
GhostCSPA, GhostCSPB, GhostCSPC,
|
||||
STCSPA, STCSPB, STCSPC,
|
||||
ST2CSPA, ST2CSPB, ST2CSPC]:
|
||||
args.insert(2, n) # number of repeats
|
||||
n = 1
|
||||
elif m is nn.BatchNorm2d:
|
||||
args = [ch[f]]
|
||||
elif m is Concat:
|
||||
c2 = sum([ch[x] for x in f])
|
||||
elif m is Chuncat:
|
||||
c2 = sum([ch[x] for x in f])
|
||||
elif m is Shortcut:
|
||||
c2 = ch[f[0]]
|
||||
elif m is Foldcut:
|
||||
c2 = ch[f] // 2
|
||||
elif m in [Detect, IDetect, IAuxDetect, IBin, IKeypoint]:
|
||||
args.append([ch[x] for x in f])
|
||||
if isinstance(args[1], int): # number of anchors
|
||||
args[1] = [list(range(args[1] * 2))] * len(f)
|
||||
elif m is ReOrg:
|
||||
c2 = ch[f] * 4
|
||||
elif m is Contract:
|
||||
c2 = ch[f] * args[0] ** 2
|
||||
elif m is Expand:
|
||||
c2 = ch[f] // args[0] ** 2
|
||||
else:
|
||||
c2 = ch[f]
|
||||
|
||||
m_ = nn.Sequential(*[m(*args) for _ in range(n)]) if n > 1 else m(*args) # module
|
||||
t = str(m)[8:-2].replace('__main__.', '') # module type
|
||||
np = sum([x.numel() for x in m_.parameters()]) # number params
|
||||
m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
|
||||
logger.info('%3s%18s%3s%10.0f %-40s%-30s' % (i, f, n, np, t, args)) # print
|
||||
save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
|
||||
layers.append(m_)
|
||||
if i == 0:
|
||||
ch = []
|
||||
ch.append(c2)
|
||||
return nn.Sequential(*layers), sorted(save)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--cfg', type=str, default='yolor-csp-c.yaml', help='model.yaml')
|
||||
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
|
||||
parser.add_argument('--profile', action='store_true', help='profile model speed')
|
||||
opt = parser.parse_args()
|
||||
opt.cfg = check_file(opt.cfg) # check file
|
||||
set_logging()
|
||||
device = select_device(opt.device)
|
||||
|
||||
# Create model
|
||||
model = Model(opt.cfg).to(device)
|
||||
model.train()
|
||||
|
||||
if opt.profile:
|
||||
img = torch.rand(1, 3, 640, 640).to(device)
|
||||
y = model(img, profile=True)
|
||||
|
||||
# Profile
|
||||
# img = torch.rand(8 if torch.cuda.is_available() else 1, 3, 640, 640).to(device)
|
||||
# y = model(img, profile=True)
|
||||
|
||||
# Tensorboard
|
||||
# from torch.utils.tensorboard import SummaryWriter
|
||||
# tb_writer = SummaryWriter()
|
||||
# print("Run 'tensorboard --logdir=models/runs' to view tensorboard at http://localhost:6006/")
|
||||
# tb_writer.add_graph(model.model, img) # add model to tensorboard
|
||||
# tb_writer.add_image('test', img[0], dataformats='CWH') # add model to tensorboard
|
||||
|
|
@ -0,0 +1 @@
|
|||
# init
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# Activation functions
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
# SiLU https://arxiv.org/pdf/1606.08415.pdf ----------------------------------------------------------------------------
|
||||
class SiLU(nn.Module): # export-friendly version of nn.SiLU()
|
||||
@staticmethod
|
||||
def forward(x):
|
||||
return x * torch.sigmoid(x)
|
||||
|
||||
|
||||
class Hardswish(nn.Module): # export-friendly version of nn.Hardswish()
|
||||
@staticmethod
|
||||
def forward(x):
|
||||
# return x * F.hardsigmoid(x) # for torchscript and CoreML
|
||||
return x * F.hardtanh(x + 3, 0., 6.) / 6. # for torchscript, CoreML and ONNX
|
||||
|
||||
|
||||
class MemoryEfficientSwish(nn.Module):
|
||||
class F(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(ctx, x):
|
||||
ctx.save_for_backward(x)
|
||||
return x * torch.sigmoid(x)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
x = ctx.saved_tensors[0]
|
||||
sx = torch.sigmoid(x)
|
||||
return grad_output * (sx * (1 + x * (1 - sx)))
|
||||
|
||||
def forward(self, x):
|
||||
return self.F.apply(x)
|
||||
|
||||
|
||||
# Mish https://github.com/digantamisra98/Mish --------------------------------------------------------------------------
|
||||
class Mish(nn.Module):
|
||||
@staticmethod
|
||||
def forward(x):
|
||||
return x * F.softplus(x).tanh()
|
||||
|
||||
|
||||
class MemoryEfficientMish(nn.Module):
|
||||
class F(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(ctx, x):
|
||||
ctx.save_for_backward(x)
|
||||
return x.mul(torch.tanh(F.softplus(x))) # x * tanh(ln(1 + exp(x)))
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
x = ctx.saved_tensors[0]
|
||||
sx = torch.sigmoid(x)
|
||||
fx = F.softplus(x).tanh()
|
||||
return grad_output * (fx + x * sx * (1 - fx * fx))
|
||||
|
||||
def forward(self, x):
|
||||
return self.F.apply(x)
|
||||
|
||||
|
||||
# FReLU https://arxiv.org/abs/2007.11824 -------------------------------------------------------------------------------
|
||||
class FReLU(nn.Module):
|
||||
def __init__(self, c1, k=3): # ch_in, kernel
|
||||
super().__init__()
|
||||
self.conv = nn.Conv2d(c1, c1, k, 1, 1, groups=c1, bias=False)
|
||||
self.bn = nn.BatchNorm2d(c1)
|
||||
|
||||
def forward(self, x):
|
||||
return torch.max(x, self.bn(self.conv(x)))
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import numpy as np
|
||||
import onnx
|
||||
from onnx import shape_inference
|
||||
try:
|
||||
import onnx_graphsurgeon as gs
|
||||
except Exception as e:
|
||||
print('Import onnx_graphsurgeon failure: %s' % e)
|
||||
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class RegisterNMS(object):
|
||||
def __init__(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
precision: str = "fp32",
|
||||
):
|
||||
|
||||
self.graph = gs.import_onnx(onnx.load(onnx_model_path))
|
||||
assert self.graph
|
||||
LOGGER.info("ONNX graph created successfully")
|
||||
# Fold constants via ONNX-GS that PyTorch2ONNX may have missed
|
||||
self.graph.fold_constants()
|
||||
self.precision = precision
|
||||
self.batch_size = 1
|
||||
def infer(self):
|
||||
"""
|
||||
Sanitize the graph by cleaning any unconnected nodes, do a topological resort,
|
||||
and fold constant inputs values. When possible, run shape inference on the
|
||||
ONNX graph to determine tensor shapes.
|
||||
"""
|
||||
for _ in range(3):
|
||||
count_before = len(self.graph.nodes)
|
||||
|
||||
self.graph.cleanup().toposort()
|
||||
try:
|
||||
for node in self.graph.nodes:
|
||||
for o in node.outputs:
|
||||
o.shape = None
|
||||
model = gs.export_onnx(self.graph)
|
||||
model = shape_inference.infer_shapes(model)
|
||||
self.graph = gs.import_onnx(model)
|
||||
except Exception as e:
|
||||
LOGGER.info(f"Shape inference could not be performed at this time:\n{e}")
|
||||
try:
|
||||
self.graph.fold_constants(fold_shapes=True)
|
||||
except TypeError as e:
|
||||
LOGGER.error(
|
||||
"This version of ONNX GraphSurgeon does not support folding shapes, "
|
||||
f"please upgrade your onnx_graphsurgeon module. Error:\n{e}"
|
||||
)
|
||||
raise
|
||||
|
||||
count_after = len(self.graph.nodes)
|
||||
if count_before == count_after:
|
||||
# No new folding occurred in this iteration, so we can stop for now.
|
||||
break
|
||||
|
||||
def save(self, output_path):
|
||||
"""
|
||||
Save the ONNX model to the given location.
|
||||
Args:
|
||||
output_path: Path pointing to the location where to write
|
||||
out the updated ONNX model.
|
||||
"""
|
||||
self.graph.cleanup().toposort()
|
||||
model = gs.export_onnx(self.graph)
|
||||
onnx.save(model, output_path)
|
||||
LOGGER.info(f"Saved ONNX model to {output_path}")
|
||||
|
||||
def register_nms(
|
||||
self,
|
||||
*,
|
||||
score_thresh: float = 0.25,
|
||||
nms_thresh: float = 0.45,
|
||||
detections_per_img: int = 100,
|
||||
):
|
||||
"""
|
||||
Register the ``EfficientNMS_TRT`` plugin node.
|
||||
NMS expects these shapes for its input tensors:
|
||||
- box_net: [batch_size, number_boxes, 4]
|
||||
- class_net: [batch_size, number_boxes, number_labels]
|
||||
Args:
|
||||
score_thresh (float): The scalar threshold for score (low scoring boxes are removed).
|
||||
nms_thresh (float): The scalar threshold for IOU (new boxes that have high IOU
|
||||
overlap with previously selected boxes are removed).
|
||||
detections_per_img (int): Number of best detections to keep after NMS.
|
||||
"""
|
||||
|
||||
self.infer()
|
||||
# Find the concat node at the end of the network
|
||||
op_inputs = self.graph.outputs
|
||||
op = "EfficientNMS_TRT"
|
||||
attrs = {
|
||||
"plugin_version": "1",
|
||||
"background_class": -1, # no background class
|
||||
"max_output_boxes": detections_per_img,
|
||||
"score_threshold": score_thresh,
|
||||
"iou_threshold": nms_thresh,
|
||||
"score_activation": False,
|
||||
"box_coding": 0,
|
||||
}
|
||||
|
||||
if self.precision == "fp32":
|
||||
dtype_output = np.float32
|
||||
elif self.precision == "fp16":
|
||||
dtype_output = np.float16
|
||||
else:
|
||||
raise NotImplementedError(f"Currently not supports precision: {self.precision}")
|
||||
|
||||
# NMS Outputs
|
||||
output_num_detections = gs.Variable(
|
||||
name="num_dets",
|
||||
dtype=np.int32,
|
||||
shape=[self.batch_size, 1],
|
||||
) # A scalar indicating the number of valid detections per batch image.
|
||||
output_boxes = gs.Variable(
|
||||
name="det_boxes",
|
||||
dtype=dtype_output,
|
||||
shape=[self.batch_size, detections_per_img, 4],
|
||||
)
|
||||
output_scores = gs.Variable(
|
||||
name="det_scores",
|
||||
dtype=dtype_output,
|
||||
shape=[self.batch_size, detections_per_img],
|
||||
)
|
||||
output_labels = gs.Variable(
|
||||
name="det_classes",
|
||||
dtype=np.int32,
|
||||
shape=[self.batch_size, detections_per_img],
|
||||
)
|
||||
|
||||
op_outputs = [output_num_detections, output_boxes, output_scores, output_labels]
|
||||
|
||||
# Create the NMS Plugin node with the selected inputs. The outputs of the node will also
|
||||
# become the final outputs of the graph.
|
||||
self.graph.layer(op=op, name="batched_nms", inputs=op_inputs, outputs=op_outputs, attrs=attrs)
|
||||
LOGGER.info(f"Created NMS plugin '{op}' with attributes: {attrs}")
|
||||
|
||||
self.graph.outputs = op_outputs
|
||||
|
||||
self.infer()
|
||||
|
||||
def save(self, output_path):
|
||||
"""
|
||||
Save the ONNX model to the given location.
|
||||
Args:
|
||||
output_path: Path pointing to the location where to write
|
||||
out the updated ONNX model.
|
||||
"""
|
||||
self.graph.cleanup().toposort()
|
||||
model = gs.export_onnx(self.graph)
|
||||
onnx.save(model, output_path)
|
||||
LOGGER.info(f"Saved ONNX model to {output_path}")
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
# Auto-anchor utils
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import yaml
|
||||
from scipy.cluster.vq import kmeans
|
||||
from tqdm import tqdm
|
||||
|
||||
from utils.general import colorstr
|
||||
|
||||
|
||||
def check_anchor_order(m):
|
||||
# Check anchor order against stride order for YOLO Detect() module m, and correct if necessary
|
||||
a = m.anchor_grid.prod(-1).view(-1) # anchor area
|
||||
da = a[-1] - a[0] # delta a
|
||||
ds = m.stride[-1] - m.stride[0] # delta s
|
||||
if da.sign() != ds.sign(): # same order
|
||||
print('Reversing anchor order')
|
||||
m.anchors[:] = m.anchors.flip(0)
|
||||
m.anchor_grid[:] = m.anchor_grid.flip(0)
|
||||
|
||||
|
||||
def check_anchors(dataset, model, thr=4.0, imgsz=640):
|
||||
# Check anchor fit to data, recompute if necessary
|
||||
prefix = colorstr('autoanchor: ')
|
||||
print(f'\n{prefix}Analyzing anchors... ', end='')
|
||||
m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect()
|
||||
shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
||||
scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale
|
||||
wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float() # wh
|
||||
|
||||
def metric(k): # compute metric
|
||||
r = wh[:, None] / k[None]
|
||||
x = torch.min(r, 1. / r).min(2)[0] # ratio metric
|
||||
best = x.max(1)[0] # best_x
|
||||
aat = (x > 1. / thr).float().sum(1).mean() # anchors above threshold
|
||||
bpr = (best > 1. / thr).float().mean() # best possible recall
|
||||
return bpr, aat
|
||||
|
||||
anchors = m.anchor_grid.clone().cpu().view(-1, 2) # current anchors
|
||||
bpr, aat = metric(anchors)
|
||||
print(f'anchors/target = {aat:.2f}, Best Possible Recall (BPR) = {bpr:.4f}', end='')
|
||||
if bpr < 0.98: # threshold to recompute
|
||||
print('. Attempting to improve anchors, please wait...')
|
||||
na = m.anchor_grid.numel() // 2 # number of anchors
|
||||
try:
|
||||
anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
|
||||
except Exception as e:
|
||||
print(f'{prefix}ERROR: {e}')
|
||||
new_bpr = metric(anchors)[0]
|
||||
if new_bpr > bpr: # replace anchors
|
||||
anchors = torch.tensor(anchors, device=m.anchors.device).type_as(m.anchors)
|
||||
m.anchor_grid[:] = anchors.clone().view_as(m.anchor_grid) # for inference
|
||||
check_anchor_order(m)
|
||||
m.anchors[:] = anchors.clone().view_as(m.anchors) / m.stride.to(m.anchors.device).view(-1, 1, 1) # loss
|
||||
print(f'{prefix}New anchors saved to model. Update model *.yaml to use these anchors in the future.')
|
||||
else:
|
||||
print(f'{prefix}Original anchors better than new anchors. Proceeding with original anchors.')
|
||||
print('') # newline
|
||||
|
||||
|
||||
def kmean_anchors(path='./data/coco.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
|
||||
""" Creates kmeans-evolved anchors from training dataset
|
||||
|
||||
Arguments:
|
||||
path: path to dataset *.yaml, or a loaded dataset
|
||||
n: number of anchors
|
||||
img_size: image size used for training
|
||||
thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0
|
||||
gen: generations to evolve anchors using genetic algorithm
|
||||
verbose: print all results
|
||||
|
||||
Return:
|
||||
k: kmeans evolved anchors
|
||||
|
||||
Usage:
|
||||
from utils.autoanchor import *; _ = kmean_anchors()
|
||||
"""
|
||||
thr = 1. / thr
|
||||
prefix = colorstr('autoanchor: ')
|
||||
|
||||
def metric(k, wh): # compute metrics
|
||||
r = wh[:, None] / k[None]
|
||||
x = torch.min(r, 1. / r).min(2)[0] # ratio metric
|
||||
# x = wh_iou(wh, torch.tensor(k)) # iou metric
|
||||
return x, x.max(1)[0] # x, best_x
|
||||
|
||||
def anchor_fitness(k): # mutation fitness
|
||||
_, best = metric(torch.tensor(k, dtype=torch.float32), wh)
|
||||
return (best * (best > thr).float()).mean() # fitness
|
||||
|
||||
def print_results(k):
|
||||
k = k[np.argsort(k.prod(1))] # sort small to large
|
||||
x, best = metric(k, wh0)
|
||||
bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr
|
||||
print(f'{prefix}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr')
|
||||
print(f'{prefix}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, '
|
||||
f'past_thr={x[x > thr].mean():.3f}-mean: ', end='')
|
||||
for i, x in enumerate(k):
|
||||
print('%i,%i' % (round(x[0]), round(x[1])), end=', ' if i < len(k) - 1 else '\n') # use in *.cfg
|
||||
return k
|
||||
|
||||
if isinstance(path, str): # *.yaml file
|
||||
with open(path) as f:
|
||||
data_dict = yaml.load(f, Loader=yaml.SafeLoader) # model dict
|
||||
from utils.datasets import LoadImagesAndLabels
|
||||
dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
|
||||
else:
|
||||
dataset = path # dataset
|
||||
|
||||
# Get label wh
|
||||
shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
||||
wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)]) # wh
|
||||
|
||||
# Filter
|
||||
i = (wh0 < 3.0).any(1).sum()
|
||||
if i:
|
||||
print(f'{prefix}WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.')
|
||||
wh = wh0[(wh0 >= 2.0).any(1)] # filter > 2 pixels
|
||||
# wh = wh * (np.random.rand(wh.shape[0], 1) * 0.9 + 0.1) # multiply by random scale 0-1
|
||||
|
||||
# Kmeans calculation
|
||||
print(f'{prefix}Running kmeans for {n} anchors on {len(wh)} points...')
|
||||
s = wh.std(0) # sigmas for whitening
|
||||
k, dist = kmeans(wh / s, n, iter=30) # points, mean distance
|
||||
assert len(k) == n, print(f'{prefix}ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}')
|
||||
k *= s
|
||||
wh = torch.tensor(wh, dtype=torch.float32) # filtered
|
||||
wh0 = torch.tensor(wh0, dtype=torch.float32) # unfiltered
|
||||
k = print_results(k)
|
||||
|
||||
# Plot
|
||||
# k, d = [None] * 20, [None] * 20
|
||||
# for i in tqdm(range(1, 21)):
|
||||
# k[i-1], d[i-1] = kmeans(wh / s, i) # points, mean distance
|
||||
# fig, ax = plt.subplots(1, 2, figsize=(14, 7), tight_layout=True)
|
||||
# ax = ax.ravel()
|
||||
# ax[0].plot(np.arange(1, 21), np.array(d) ** 2, marker='.')
|
||||
# fig, ax = plt.subplots(1, 2, figsize=(14, 7)) # plot wh
|
||||
# ax[0].hist(wh[wh[:, 0]<100, 0],400)
|
||||
# ax[1].hist(wh[wh[:, 1]<100, 1],400)
|
||||
# fig.savefig('wh.png', dpi=200)
|
||||
|
||||
# Evolve
|
||||
npr = np.random
|
||||
f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
|
||||
pbar = tqdm(range(gen), desc=f'{prefix}Evolving anchors with Genetic Algorithm:') # progress bar
|
||||
for _ in pbar:
|
||||
v = np.ones(sh)
|
||||
while (v == 1).all(): # mutate until a change occurs (prevent duplicates)
|
||||
v = ((npr.random(sh) < mp) * npr.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
|
||||
kg = (k.copy() * v).clip(min=2.0)
|
||||
fg = anchor_fitness(kg)
|
||||
if fg > f:
|
||||
f, k = fg, kg.copy()
|
||||
pbar.desc = f'{prefix}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'
|
||||
if verbose:
|
||||
print_results(k)
|
||||
|
||||
return print_results(k)
|
||||
|
|
@ -0,0 +1 @@
|
|||
#init
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# AWS EC2 instance startup 'MIME' script https://aws.amazon.com/premiumsupport/knowledge-center/execute-user-data-ec2/
|
||||
# This script will run on every instance restart, not only on first start
|
||||
# --- DO NOT COPY ABOVE COMMENTS WHEN PASTING INTO USERDATA ---
|
||||
|
||||
Content-Type: multipart/mixed; boundary="//"
|
||||
MIME-Version: 1.0
|
||||
|
||||
--//
|
||||
Content-Type: text/cloud-config; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: attachment; filename="cloud-config.txt"
|
||||
|
||||
#cloud-config
|
||||
cloud_final_modules:
|
||||
- [scripts-user, always]
|
||||
|
||||
--//
|
||||
Content-Type: text/x-shellscript; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: attachment; filename="userdata.txt"
|
||||
|
||||
#!/bin/bash
|
||||
# --- paste contents of userdata.sh here ---
|
||||
--//
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Resume all interrupted trainings in yolor/ dir including DDP trainings
|
||||
# Usage: $ python utils/aws/resume.py
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
sys.path.append('./') # to run '$ python *.py' files in subdirectories
|
||||
|
||||
port = 0 # --master_port
|
||||
path = Path('').resolve()
|
||||
for last in path.rglob('*/**/last.pt'):
|
||||
ckpt = torch.load(last)
|
||||
if ckpt['optimizer'] is None:
|
||||
continue
|
||||
|
||||
# Load opt.yaml
|
||||
with open(last.parent.parent / 'opt.yaml') as f:
|
||||
opt = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
|
||||
# Get device count
|
||||
d = opt['device'].split(',') # devices
|
||||
nd = len(d) # number of devices
|
||||
ddp = nd > 1 or (nd == 0 and torch.cuda.device_count() > 1) # distributed data parallel
|
||||
|
||||
if ddp: # multi-GPU
|
||||
port += 1
|
||||
cmd = f'python -m torch.distributed.launch --nproc_per_node {nd} --master_port {port} train.py --resume {last}'
|
||||
else: # single-GPU
|
||||
cmd = f'python train.py --resume {last}'
|
||||
|
||||
cmd += ' > /dev/null 2>&1 &' # redirect output to dev/null and run in daemon thread
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
# AWS EC2 instance startup script https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
|
||||
# This script will run only once on first instance start (for a re-start script see mime.sh)
|
||||
# /home/ubuntu (ubuntu) or /home/ec2-user (amazon-linux) is working dir
|
||||
# Use >300 GB SSD
|
||||
|
||||
cd home/ubuntu
|
||||
if [ ! -d yolor ]; then
|
||||
echo "Running first-time script." # install dependencies, download COCO, pull Docker
|
||||
git clone -b main https://github.com/WongKinYiu/yolov7 && sudo chmod -R 777 yolov7
|
||||
cd yolov7
|
||||
bash data/scripts/get_coco.sh && echo "Data done." &
|
||||
sudo docker pull nvcr.io/nvidia/pytorch:21.08-py3 && echo "Docker done." &
|
||||
python -m pip install --upgrade pip && pip install -r requirements.txt && python detect.py && echo "Requirements done." &
|
||||
wait && echo "All tasks done." # finish background tasks
|
||||
else
|
||||
echo "Running re-start script." # resume interrupted runs
|
||||
i=0
|
||||
list=$(sudo docker ps -qa) # container list i.e. $'one\ntwo\nthree\nfour'
|
||||
while IFS= read -r id; do
|
||||
((i++))
|
||||
echo "restarting container $i: $id"
|
||||
sudo docker start $id
|
||||
# sudo docker exec -it $id python train.py --resume # single-GPU
|
||||
sudo docker exec -d $id python utils/aws/resume.py # multi-scenario
|
||||
done <<<"$list"
|
||||
fi
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,892 @@
|
|||
# YOLOR general utils
|
||||
|
||||
import glob
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import torchvision
|
||||
import yaml
|
||||
|
||||
from utils.google_utils import gsutil_getsize
|
||||
from utils.metrics import fitness
|
||||
from utils.torch_utils import init_torch_seeds
|
||||
|
||||
# Settings
|
||||
torch.set_printoptions(linewidth=320, precision=5, profile='long')
|
||||
np.set_printoptions(linewidth=320, formatter={'float_kind': '{:11.5g}'.format}) # format short g, %precision=5
|
||||
pd.options.display.max_columns = 10
|
||||
cv2.setNumThreads(0) # prevent OpenCV from multithreading (incompatible with PyTorch DataLoader)
|
||||
os.environ['NUMEXPR_MAX_THREADS'] = str(min(os.cpu_count(), 8)) # NumExpr max threads
|
||||
|
||||
|
||||
def set_logging(rank=-1):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
level=logging.INFO if rank in [-1, 0] else logging.WARN)
|
||||
|
||||
|
||||
def init_seeds(seed=0):
|
||||
# Initialize random number generator (RNG) seeds
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
init_torch_seeds(seed)
|
||||
|
||||
|
||||
def get_latest_run(search_dir='.'):
|
||||
# Return path to most recent 'last.pt' in /runs (i.e. to --resume from)
|
||||
last_list = glob.glob(f'{search_dir}/**/last*.pt', recursive=True)
|
||||
return max(last_list, key=os.path.getctime) if last_list else ''
|
||||
|
||||
|
||||
def isdocker():
|
||||
# Is environment a Docker container
|
||||
return Path('/workspace').exists() # or Path('/.dockerenv').exists()
|
||||
|
||||
|
||||
def emojis(str=''):
|
||||
# Return platform-dependent emoji-safe version of string
|
||||
return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str
|
||||
|
||||
|
||||
def check_online():
|
||||
# Check internet connectivity
|
||||
import socket
|
||||
try:
|
||||
socket.create_connection(("1.1.1.1", 443), 5) # check host accesability
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def check_git_status():
|
||||
# Recommend 'git pull' if code is out of date
|
||||
print(colorstr('github: '), end='')
|
||||
try:
|
||||
assert Path('.git').exists(), 'skipping check (not a git repository)'
|
||||
assert not isdocker(), 'skipping check (Docker image)'
|
||||
assert check_online(), 'skipping check (offline)'
|
||||
|
||||
cmd = 'git fetch && git config --get remote.origin.url'
|
||||
url = subprocess.check_output(cmd, shell=True).decode().strip().rstrip('.git') # github repo url
|
||||
branch = subprocess.check_output('git rev-parse --abbrev-ref HEAD', shell=True).decode().strip() # checked out
|
||||
n = int(subprocess.check_output(f'git rev-list {branch}..origin/master --count', shell=True)) # commits behind
|
||||
if n > 0:
|
||||
s = f"⚠️ WARNING: code is out of date by {n} commit{'s' * (n > 1)}. " \
|
||||
f"Use 'git pull' to update or 'git clone {url}' to download latest."
|
||||
else:
|
||||
s = f'up to date with {url} ✅'
|
||||
print(emojis(s)) # emoji-safe
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
def check_requirements(requirements='requirements.txt', exclude=()):
|
||||
# Check installed dependencies meet requirements (pass *.txt file or list of packages)
|
||||
import pkg_resources as pkg
|
||||
prefix = colorstr('red', 'bold', 'requirements:')
|
||||
if isinstance(requirements, (str, Path)): # requirements.txt file
|
||||
file = Path(requirements)
|
||||
if not file.exists():
|
||||
print(f"{prefix} {file.resolve()} not found, check failed.")
|
||||
return
|
||||
requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(file.open()) if x.name not in exclude]
|
||||
else: # list or tuple of packages
|
||||
requirements = [x for x in requirements if x not in exclude]
|
||||
|
||||
n = 0 # number of packages updates
|
||||
for r in requirements:
|
||||
try:
|
||||
pkg.require(r)
|
||||
except Exception as e: # DistributionNotFound or VersionConflict if requirements not met
|
||||
n += 1
|
||||
print(f"{prefix} {e.req} not found and is required by YOLOR, attempting auto-update...")
|
||||
print(subprocess.check_output(f"pip install '{e.req}'", shell=True).decode())
|
||||
|
||||
if n: # if packages updated
|
||||
source = file.resolve() if 'file' in locals() else requirements
|
||||
s = f"{prefix} {n} package{'s' * (n > 1)} updated per {source}\n" \
|
||||
f"{prefix} ⚠️ {colorstr('bold', 'Restart runtime or rerun command for updates to take effect')}\n"
|
||||
print(emojis(s)) # emoji-safe
|
||||
|
||||
|
||||
def check_img_size(img_size, s=32):
|
||||
# Verify img_size is a multiple of stride s
|
||||
new_size = make_divisible(img_size, int(s)) # ceil gs-multiple
|
||||
if new_size != img_size:
|
||||
print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size))
|
||||
return new_size
|
||||
|
||||
|
||||
def check_imshow():
|
||||
# Check if environment supports image displays
|
||||
try:
|
||||
assert not isdocker(), 'cv2.imshow() is disabled in Docker environments'
|
||||
cv2.imshow('test', np.zeros((1, 1, 3)))
|
||||
cv2.waitKey(1)
|
||||
cv2.destroyAllWindows()
|
||||
cv2.waitKey(1)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f'WARNING: Environment does not support cv2.imshow() or PIL Image.show() image displays\n{e}')
|
||||
return False
|
||||
|
||||
|
||||
def check_file(file):
|
||||
# Search for file if not found
|
||||
if Path(file).is_file() or file == '':
|
||||
return file
|
||||
else:
|
||||
files = glob.glob('./**/' + file, recursive=True) # find file
|
||||
assert len(files), f'File Not Found: {file}' # assert file was found
|
||||
assert len(files) == 1, f"Multiple files match '{file}', specify exact path: {files}" # assert unique
|
||||
return files[0] # return file
|
||||
|
||||
|
||||
def check_dataset(dict):
|
||||
# Download dataset if not found locally
|
||||
val, s = dict.get('val'), dict.get('download')
|
||||
if val and len(val):
|
||||
val = [Path(x).resolve() for x in (val if isinstance(val, list) else [val])] # val path
|
||||
if not all(x.exists() for x in val):
|
||||
print('\nWARNING: Dataset not found, nonexistent paths: %s' % [str(x) for x in val if not x.exists()])
|
||||
if s and len(s): # download script
|
||||
print('Downloading %s ...' % s)
|
||||
if s.startswith('http') and s.endswith('.zip'): # URL
|
||||
f = Path(s).name # filename
|
||||
torch.hub.download_url_to_file(s, f)
|
||||
r = os.system('unzip -q %s -d ../ && rm %s' % (f, f)) # unzip
|
||||
else: # bash script
|
||||
r = os.system(s)
|
||||
print('Dataset autodownload %s\n' % ('success' if r == 0 else 'failure')) # analyze return value
|
||||
else:
|
||||
raise Exception('Dataset not found.')
|
||||
|
||||
|
||||
def make_divisible(x, divisor):
|
||||
# Returns x evenly divisible by divisor
|
||||
return math.ceil(x / divisor) * divisor
|
||||
|
||||
|
||||
def clean_str(s):
|
||||
# Cleans a string by replacing special characters with underscore _
|
||||
return re.sub(pattern="[|@#!¡·$€%&()=?¿^*;:,¨´><+]", repl="_", string=s)
|
||||
|
||||
|
||||
def one_cycle(y1=0.0, y2=1.0, steps=100):
|
||||
# lambda function for sinusoidal ramp from y1 to y2
|
||||
return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1
|
||||
|
||||
|
||||
def colorstr(*input):
|
||||
# Colors a string https://en.wikipedia.org/wiki/ANSI_escape_code, i.e. colorstr('blue', 'hello world')
|
||||
*args, string = input if len(input) > 1 else ('blue', 'bold', input[0]) # color arguments, string
|
||||
colors = {'black': '\033[30m', # basic colors
|
||||
'red': '\033[31m',
|
||||
'green': '\033[32m',
|
||||
'yellow': '\033[33m',
|
||||
'blue': '\033[34m',
|
||||
'magenta': '\033[35m',
|
||||
'cyan': '\033[36m',
|
||||
'white': '\033[37m',
|
||||
'bright_black': '\033[90m', # bright colors
|
||||
'bright_red': '\033[91m',
|
||||
'bright_green': '\033[92m',
|
||||
'bright_yellow': '\033[93m',
|
||||
'bright_blue': '\033[94m',
|
||||
'bright_magenta': '\033[95m',
|
||||
'bright_cyan': '\033[96m',
|
||||
'bright_white': '\033[97m',
|
||||
'end': '\033[0m', # misc
|
||||
'bold': '\033[1m',
|
||||
'underline': '\033[4m'}
|
||||
return ''.join(colors[x] for x in args) + f'{string}' + colors['end']
|
||||
|
||||
|
||||
def labels_to_class_weights(labels, nc=80):
|
||||
# Get class weights (inverse frequency) from training labels
|
||||
if labels[0] is None: # no labels loaded
|
||||
return torch.Tensor()
|
||||
|
||||
labels = np.concatenate(labels, 0) # labels.shape = (866643, 5) for COCO
|
||||
classes = labels[:, 0].astype(np.int32) # labels = [class xywh]
|
||||
weights = np.bincount(classes, minlength=nc) # occurrences per class
|
||||
|
||||
# Prepend gridpoint count (for uCE training)
|
||||
# gpi = ((320 / 32 * np.array([1, 2, 4])) ** 2 * 3).sum() # gridpoints per image
|
||||
# weights = np.hstack([gpi * len(labels) - weights.sum() * 9, weights * 9]) ** 0.5 # prepend gridpoints to start
|
||||
|
||||
weights[weights == 0] = 1 # replace empty bins with 1
|
||||
weights = 1 / weights # number of targets per class
|
||||
weights /= weights.sum() # normalize
|
||||
return torch.from_numpy(weights)
|
||||
|
||||
|
||||
def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)):
|
||||
# Produces image weights based on class_weights and image contents
|
||||
class_counts = np.array([np.bincount(x[:, 0].astype(np.int32), minlength=nc) for x in labels])
|
||||
image_weights = (class_weights.reshape(1, nc) * class_counts).sum(1)
|
||||
# index = random.choices(range(n), weights=image_weights, k=1) # weight image sample
|
||||
return image_weights
|
||||
|
||||
|
||||
def coco80_to_coco91_class(): # converts 80-index (val2014) to 91-index (paper)
|
||||
# https://tech.amikelive.com/node-718/what-object-categories-labels-are-in-coco-dataset/
|
||||
# a = np.loadtxt('data/coco.names', dtype='str', delimiter='\n')
|
||||
# b = np.loadtxt('data/coco_paper.names', dtype='str', delimiter='\n')
|
||||
# x1 = [list(a[i] == b).index(True) + 1 for i in range(80)] # darknet to coco
|
||||
# x2 = [list(b[i] == a).index(True) if any(b[i] == a) else None for i in range(91)] # coco to darknet
|
||||
x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34,
|
||||
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
|
||||
64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90]
|
||||
return x
|
||||
|
||||
|
||||
def xyxy2xywh(x):
|
||||
# Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = (x[:, 0] + x[:, 2]) / 2 # x center
|
||||
y[:, 1] = (x[:, 1] + x[:, 3]) / 2 # y center
|
||||
y[:, 2] = x[:, 2] - x[:, 0] # width
|
||||
y[:, 3] = x[:, 3] - x[:, 1] # height
|
||||
return y
|
||||
|
||||
|
||||
def xywh2xyxy(x):
|
||||
# Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x
|
||||
y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y
|
||||
y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x
|
||||
y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y
|
||||
return y
|
||||
|
||||
|
||||
def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0):
|
||||
# Convert nx4 boxes from [x, y, w, h] normalized to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = w * (x[:, 0] - x[:, 2] / 2) + padw # top left x
|
||||
y[:, 1] = h * (x[:, 1] - x[:, 3] / 2) + padh # top left y
|
||||
y[:, 2] = w * (x[:, 0] + x[:, 2] / 2) + padw # bottom right x
|
||||
y[:, 3] = h * (x[:, 1] + x[:, 3] / 2) + padh # bottom right y
|
||||
return y
|
||||
|
||||
|
||||
def xyn2xy(x, w=640, h=640, padw=0, padh=0):
|
||||
# Convert normalized segments into pixel segments, shape (n,2)
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = w * x[:, 0] + padw # top left x
|
||||
y[:, 1] = h * x[:, 1] + padh # top left y
|
||||
return y
|
||||
|
||||
|
||||
def segment2box(segment, width=640, height=640):
|
||||
# Convert 1 segment label to 1 box label, applying inside-image constraint, i.e. (xy1, xy2, ...) to (xyxy)
|
||||
x, y = segment.T # segment xy
|
||||
inside = (x >= 0) & (y >= 0) & (x <= width) & (y <= height)
|
||||
x, y, = x[inside], y[inside]
|
||||
return np.array([x.min(), y.min(), x.max(), y.max()]) if any(x) else np.zeros((1, 4)) # xyxy
|
||||
|
||||
|
||||
def segments2boxes(segments):
|
||||
# Convert segment labels to box labels, i.e. (cls, xy1, xy2, ...) to (cls, xywh)
|
||||
boxes = []
|
||||
for s in segments:
|
||||
x, y = s.T # segment xy
|
||||
boxes.append([x.min(), y.min(), x.max(), y.max()]) # cls, xyxy
|
||||
return xyxy2xywh(np.array(boxes)) # cls, xywh
|
||||
|
||||
|
||||
def resample_segments(segments, n=1000):
|
||||
# Up-sample an (n,2) segment
|
||||
for i, s in enumerate(segments):
|
||||
s = np.concatenate((s, s[0:1, :]), axis=0)
|
||||
x = np.linspace(0, len(s) - 1, n)
|
||||
xp = np.arange(len(s))
|
||||
segments[i] = np.concatenate([np.interp(x, xp, s[:, i]) for i in range(2)]).reshape(2, -1).T # segment xy
|
||||
return segments
|
||||
|
||||
|
||||
def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
|
||||
# Rescale coords (xyxy) from img1_shape to img0_shape
|
||||
if ratio_pad is None: # calculate from img0_shape
|
||||
gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new
|
||||
pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # wh padding
|
||||
else:
|
||||
gain = ratio_pad[0][0]
|
||||
pad = ratio_pad[1]
|
||||
|
||||
coords[:, [0, 2]] -= pad[0] # x padding
|
||||
coords[:, [1, 3]] -= pad[1] # y padding
|
||||
coords[:, :4] /= gain
|
||||
clip_coords(coords, img0_shape)
|
||||
return coords
|
||||
|
||||
|
||||
def clip_coords(boxes, img_shape):
|
||||
# Clip bounding xyxy bounding boxes to image shape (height, width)
|
||||
boxes[:, 0].clamp_(0, img_shape[1]) # x1
|
||||
boxes[:, 1].clamp_(0, img_shape[0]) # y1
|
||||
boxes[:, 2].clamp_(0, img_shape[1]) # x2
|
||||
boxes[:, 3].clamp_(0, img_shape[0]) # y2
|
||||
|
||||
|
||||
def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7):
|
||||
# Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
|
||||
box2 = box2.T
|
||||
|
||||
# Get the coordinates of bounding boxes
|
||||
if x1y1x2y2: # x1, y1, x2, y2 = box1
|
||||
b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
|
||||
b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
|
||||
else: # transform from xywh to xyxy
|
||||
b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
|
||||
b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
|
||||
b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
|
||||
b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2
|
||||
|
||||
# Intersection area
|
||||
inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
|
||||
(torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
|
||||
|
||||
# Union Area
|
||||
w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps
|
||||
w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps
|
||||
union = w1 * h1 + w2 * h2 - inter + eps
|
||||
|
||||
iou = inter / union
|
||||
|
||||
if GIoU or DIoU or CIoU:
|
||||
cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width
|
||||
ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height
|
||||
if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
|
||||
c2 = cw ** 2 + ch ** 2 + eps # convex diagonal squared
|
||||
rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 +
|
||||
(b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center distance squared
|
||||
if DIoU:
|
||||
return iou - rho2 / c2 # DIoU
|
||||
elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
|
||||
v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / (h2 + eps)) - torch.atan(w1 / (h1 + eps)), 2)
|
||||
with torch.no_grad():
|
||||
alpha = v / (v - iou + (1 + eps))
|
||||
return iou - (rho2 / c2 + v * alpha) # CIoU
|
||||
else: # GIoU https://arxiv.org/pdf/1902.09630.pdf
|
||||
c_area = cw * ch + eps # convex area
|
||||
return iou - (c_area - union) / c_area # GIoU
|
||||
else:
|
||||
return iou # IoU
|
||||
|
||||
|
||||
|
||||
|
||||
def bbox_alpha_iou(box1, box2, x1y1x2y2=False, GIoU=False, DIoU=False, CIoU=False, alpha=2, eps=1e-9):
|
||||
# Returns tsqrt_he IoU of box1 to box2. box1 is 4, box2 is nx4
|
||||
box2 = box2.T
|
||||
|
||||
# Get the coordinates of bounding boxes
|
||||
if x1y1x2y2: # x1, y1, x2, y2 = box1
|
||||
b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
|
||||
b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
|
||||
else: # transform from xywh to xyxy
|
||||
b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
|
||||
b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
|
||||
b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
|
||||
b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2
|
||||
|
||||
# Intersection area
|
||||
inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
|
||||
(torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
|
||||
|
||||
# Union Area
|
||||
w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps
|
||||
w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps
|
||||
union = w1 * h1 + w2 * h2 - inter + eps
|
||||
|
||||
# change iou into pow(iou+eps)
|
||||
# iou = inter / union
|
||||
iou = torch.pow(inter/union + eps, alpha)
|
||||
# beta = 2 * alpha
|
||||
if GIoU or DIoU or CIoU:
|
||||
cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width
|
||||
ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height
|
||||
if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
|
||||
c2 = (cw ** 2 + ch ** 2) ** alpha + eps # convex diagonal
|
||||
rho_x = torch.abs(b2_x1 + b2_x2 - b1_x1 - b1_x2)
|
||||
rho_y = torch.abs(b2_y1 + b2_y2 - b1_y1 - b1_y2)
|
||||
rho2 = ((rho_x ** 2 + rho_y ** 2) / 4) ** alpha # center distance
|
||||
if DIoU:
|
||||
return iou - rho2 / c2 # DIoU
|
||||
elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
|
||||
v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2)
|
||||
with torch.no_grad():
|
||||
alpha_ciou = v / ((1 + eps) - inter / union + v)
|
||||
# return iou - (rho2 / c2 + v * alpha_ciou) # CIoU
|
||||
return iou - (rho2 / c2 + torch.pow(v * alpha_ciou + eps, alpha)) # CIoU
|
||||
else: # GIoU https://arxiv.org/pdf/1902.09630.pdf
|
||||
# c_area = cw * ch + eps # convex area
|
||||
# return iou - (c_area - union) / c_area # GIoU
|
||||
c_area = torch.max(cw * ch + eps, union) # convex area
|
||||
return iou - torch.pow((c_area - union) / c_area + eps, alpha) # GIoU
|
||||
else:
|
||||
return iou # torch.log(iou+eps) or iou
|
||||
|
||||
|
||||
def box_iou(box1, box2):
|
||||
# https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
|
||||
"""
|
||||
Return intersection-over-union (Jaccard index) of boxes.
|
||||
Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
|
||||
Arguments:
|
||||
box1 (Tensor[N, 4])
|
||||
box2 (Tensor[M, 4])
|
||||
Returns:
|
||||
iou (Tensor[N, M]): the NxM matrix containing the pairwise
|
||||
IoU values for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
# inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
return inter / (area1[:, None] + area2 - inter) # iou = inter / (area1 + area2 - inter)
|
||||
|
||||
|
||||
def wh_iou(wh1, wh2):
|
||||
# Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2
|
||||
wh1 = wh1[:, None] # [N,1,2]
|
||||
wh2 = wh2[None] # [1,M,2]
|
||||
inter = torch.min(wh1, wh2).prod(2) # [N,M]
|
||||
return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter)
|
||||
|
||||
|
||||
def box_giou(box1, box2):
|
||||
"""
|
||||
Return generalized intersection-over-union (Jaccard index) between two sets of boxes.
|
||||
Both sets of boxes are expected to be in ``(x1, y1, x2, y2)`` format with
|
||||
``0 <= x1 < x2`` and ``0 <= y1 < y2``.
|
||||
Args:
|
||||
boxes1 (Tensor[N, 4]): first set of boxes
|
||||
boxes2 (Tensor[M, 4]): second set of boxes
|
||||
Returns:
|
||||
Tensor[N, M]: the NxM matrix containing the pairwise generalized IoU values
|
||||
for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
union = (area1[:, None] + area2 - inter)
|
||||
|
||||
iou = inter / union
|
||||
|
||||
lti = torch.min(box1[:, None, :2], box2[:, :2])
|
||||
rbi = torch.max(box1[:, None, 2:], box2[:, 2:])
|
||||
|
||||
whi = (rbi - lti).clamp(min=0) # [N,M,2]
|
||||
areai = whi[:, :, 0] * whi[:, :, 1]
|
||||
|
||||
return iou - (areai - union) / areai
|
||||
|
||||
|
||||
def box_ciou(box1, box2, eps: float = 1e-7):
|
||||
"""
|
||||
Return complete intersection-over-union (Jaccard index) between two sets of boxes.
|
||||
Both sets of boxes are expected to be in ``(x1, y1, x2, y2)`` format with
|
||||
``0 <= x1 < x2`` and ``0 <= y1 < y2``.
|
||||
Args:
|
||||
boxes1 (Tensor[N, 4]): first set of boxes
|
||||
boxes2 (Tensor[M, 4]): second set of boxes
|
||||
eps (float, optional): small number to prevent division by zero. Default: 1e-7
|
||||
Returns:
|
||||
Tensor[N, M]: the NxM matrix containing the pairwise complete IoU values
|
||||
for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
union = (area1[:, None] + area2 - inter)
|
||||
|
||||
iou = inter / union
|
||||
|
||||
lti = torch.min(box1[:, None, :2], box2[:, :2])
|
||||
rbi = torch.max(box1[:, None, 2:], box2[:, 2:])
|
||||
|
||||
whi = (rbi - lti).clamp(min=0) # [N,M,2]
|
||||
diagonal_distance_squared = (whi[:, :, 0] ** 2) + (whi[:, :, 1] ** 2) + eps
|
||||
|
||||
# centers of boxes
|
||||
x_p = (box1[:, None, 0] + box1[:, None, 2]) / 2
|
||||
y_p = (box1[:, None, 1] + box1[:, None, 3]) / 2
|
||||
x_g = (box2[:, 0] + box2[:, 2]) / 2
|
||||
y_g = (box2[:, 1] + box2[:, 3]) / 2
|
||||
# The distance between boxes' centers squared.
|
||||
centers_distance_squared = (x_p - x_g) ** 2 + (y_p - y_g) ** 2
|
||||
|
||||
w_pred = box1[:, None, 2] - box1[:, None, 0]
|
||||
h_pred = box1[:, None, 3] - box1[:, None, 1]
|
||||
|
||||
w_gt = box2[:, 2] - box2[:, 0]
|
||||
h_gt = box2[:, 3] - box2[:, 1]
|
||||
|
||||
v = (4 / (torch.pi ** 2)) * torch.pow((torch.atan(w_gt / h_gt) - torch.atan(w_pred / h_pred)), 2)
|
||||
with torch.no_grad():
|
||||
alpha = v / (1 - iou + v + eps)
|
||||
return iou - (centers_distance_squared / diagonal_distance_squared) - alpha * v
|
||||
|
||||
|
||||
def box_diou(box1, box2, eps: float = 1e-7):
|
||||
"""
|
||||
Return distance intersection-over-union (Jaccard index) between two sets of boxes.
|
||||
Both sets of boxes are expected to be in ``(x1, y1, x2, y2)`` format with
|
||||
``0 <= x1 < x2`` and ``0 <= y1 < y2``.
|
||||
Args:
|
||||
boxes1 (Tensor[N, 4]): first set of boxes
|
||||
boxes2 (Tensor[M, 4]): second set of boxes
|
||||
eps (float, optional): small number to prevent division by zero. Default: 1e-7
|
||||
Returns:
|
||||
Tensor[N, M]: the NxM matrix containing the pairwise distance IoU values
|
||||
for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
union = (area1[:, None] + area2 - inter)
|
||||
|
||||
iou = inter / union
|
||||
|
||||
lti = torch.min(box1[:, None, :2], box2[:, :2])
|
||||
rbi = torch.max(box1[:, None, 2:], box2[:, 2:])
|
||||
|
||||
whi = (rbi - lti).clamp(min=0) # [N,M,2]
|
||||
diagonal_distance_squared = (whi[:, :, 0] ** 2) + (whi[:, :, 1] ** 2) + eps
|
||||
|
||||
# centers of boxes
|
||||
x_p = (box1[:, None, 0] + box1[:, None, 2]) / 2
|
||||
y_p = (box1[:, None, 1] + box1[:, None, 3]) / 2
|
||||
x_g = (box2[:, 0] + box2[:, 2]) / 2
|
||||
y_g = (box2[:, 1] + box2[:, 3]) / 2
|
||||
# The distance between boxes' centers squared.
|
||||
centers_distance_squared = (x_p - x_g) ** 2 + (y_p - y_g) ** 2
|
||||
|
||||
# The distance IoU is the IoU penalized by a normalized
|
||||
# distance between boxes' centers squared.
|
||||
return iou - (centers_distance_squared / diagonal_distance_squared)
|
||||
|
||||
|
||||
def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False,
|
||||
labels=()):
|
||||
"""Runs Non-Maximum Suppression (NMS) on inference results
|
||||
|
||||
Returns:
|
||||
list of detections, on (n,6) tensor per image [xyxy, conf, cls]
|
||||
"""
|
||||
|
||||
nc = prediction.shape[2] - 5 # number of classes
|
||||
xc = prediction[..., 4] > conf_thres # candidates
|
||||
|
||||
# Settings
|
||||
min_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and height
|
||||
max_det = 300 # maximum number of detections per image
|
||||
max_nms = 30000 # maximum number of boxes into torchvision.ops.nms()
|
||||
time_limit = 10.0 # seconds to quit after
|
||||
redundant = True # require redundant detections
|
||||
multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)
|
||||
merge = False # use merge-NMS
|
||||
|
||||
t = time.time()
|
||||
output = [torch.zeros((0, 6), device=prediction.device)] * prediction.shape[0]
|
||||
for xi, x in enumerate(prediction): # image index, image inference
|
||||
# Apply constraints
|
||||
# x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height
|
||||
x = x[xc[xi]] # confidence
|
||||
|
||||
# Cat apriori labels if autolabelling
|
||||
if labels and len(labels[xi]):
|
||||
l = labels[xi]
|
||||
v = torch.zeros((len(l), nc + 5), device=x.device)
|
||||
v[:, :4] = l[:, 1:5] # box
|
||||
v[:, 4] = 1.0 # conf
|
||||
v[range(len(l)), l[:, 0].long() + 5] = 1.0 # cls
|
||||
x = torch.cat((x, v), 0)
|
||||
|
||||
# If none remain process next image
|
||||
if not x.shape[0]:
|
||||
continue
|
||||
|
||||
# Compute conf
|
||||
if nc == 1:
|
||||
x[:, 5:] = x[:, 4:5] # for models with one class, cls_loss is 0 and cls_conf is always 0.5,
|
||||
# so there is no need to multiplicate.
|
||||
else:
|
||||
x[:, 5:] *= x[:, 4:5] # conf = obj_conf * cls_conf
|
||||
|
||||
# Box (center x, center y, width, height) to (x1, y1, x2, y2)
|
||||
box = xywh2xyxy(x[:, :4])
|
||||
|
||||
# Detections matrix nx6 (xyxy, conf, cls)
|
||||
if multi_label:
|
||||
i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
|
||||
x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
|
||||
else: # best class only
|
||||
conf, j = x[:, 5:].max(1, keepdim=True)
|
||||
x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]
|
||||
|
||||
# Filter by class
|
||||
if classes is not None:
|
||||
x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]
|
||||
|
||||
# Apply finite constraint
|
||||
# if not torch.isfinite(x).all():
|
||||
# x = x[torch.isfinite(x).all(1)]
|
||||
|
||||
# Check shape
|
||||
n = x.shape[0] # number of boxes
|
||||
if not n: # no boxes
|
||||
continue
|
||||
elif n > max_nms: # excess boxes
|
||||
x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence
|
||||
|
||||
# Batched NMS
|
||||
c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
|
||||
boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores
|
||||
i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS
|
||||
if i.shape[0] > max_det: # limit detections
|
||||
i = i[:max_det]
|
||||
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
|
||||
# update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
|
||||
iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
|
||||
weights = iou * scores[None] # box weights
|
||||
x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes
|
||||
if redundant:
|
||||
i = i[iou.sum(1) > 1] # require redundancy
|
||||
|
||||
output[xi] = x[i]
|
||||
if (time.time() - t) > time_limit:
|
||||
print(f'WARNING: NMS time limit {time_limit}s exceeded')
|
||||
break # time limit exceeded
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def non_max_suppression_kpt(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False,
|
||||
labels=(), kpt_label=False, nc=None, nkpt=None):
|
||||
"""Runs Non-Maximum Suppression (NMS) on inference results
|
||||
|
||||
Returns:
|
||||
list of detections, on (n,6) tensor per image [xyxy, conf, cls]
|
||||
"""
|
||||
if nc is None:
|
||||
nc = prediction.shape[2] - 5 if not kpt_label else prediction.shape[2] - 56 # number of classes
|
||||
xc = prediction[..., 4] > conf_thres # candidates
|
||||
|
||||
# Settings
|
||||
min_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and height
|
||||
max_det = 300 # maximum number of detections per image
|
||||
max_nms = 30000 # maximum number of boxes into torchvision.ops.nms()
|
||||
time_limit = 10.0 # seconds to quit after
|
||||
redundant = True # require redundant detections
|
||||
multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)
|
||||
merge = False # use merge-NMS
|
||||
|
||||
t = time.time()
|
||||
output = [torch.zeros((0,6), device=prediction.device)] * prediction.shape[0]
|
||||
for xi, x in enumerate(prediction): # image index, image inference
|
||||
# Apply constraints
|
||||
# x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height
|
||||
x = x[xc[xi]] # confidence
|
||||
|
||||
# Cat apriori labels if autolabelling
|
||||
if labels and len(labels[xi]):
|
||||
l = labels[xi]
|
||||
v = torch.zeros((len(l), nc + 5), device=x.device)
|
||||
v[:, :4] = l[:, 1:5] # box
|
||||
v[:, 4] = 1.0 # conf
|
||||
v[range(len(l)), l[:, 0].long() + 5] = 1.0 # cls
|
||||
x = torch.cat((x, v), 0)
|
||||
|
||||
# If none remain process next image
|
||||
if not x.shape[0]:
|
||||
continue
|
||||
|
||||
# Compute conf
|
||||
x[:, 5:5+nc] *= x[:, 4:5] # conf = obj_conf * cls_conf
|
||||
|
||||
# Box (center x, center y, width, height) to (x1, y1, x2, y2)
|
||||
box = xywh2xyxy(x[:, :4])
|
||||
|
||||
# Detections matrix nx6 (xyxy, conf, cls)
|
||||
if multi_label:
|
||||
i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
|
||||
x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
|
||||
else: # best class only
|
||||
if not kpt_label:
|
||||
conf, j = x[:, 5:].max(1, keepdim=True)
|
||||
x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]
|
||||
else:
|
||||
kpts = x[:, 6:]
|
||||
conf, j = x[:, 5:6].max(1, keepdim=True)
|
||||
x = torch.cat((box, conf, j.float(), kpts), 1)[conf.view(-1) > conf_thres]
|
||||
|
||||
|
||||
# Filter by class
|
||||
if classes is not None:
|
||||
x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]
|
||||
|
||||
# Apply finite constraint
|
||||
# if not torch.isfinite(x).all():
|
||||
# x = x[torch.isfinite(x).all(1)]
|
||||
|
||||
# Check shape
|
||||
n = x.shape[0] # number of boxes
|
||||
if not n: # no boxes
|
||||
continue
|
||||
elif n > max_nms: # excess boxes
|
||||
x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence
|
||||
|
||||
# Batched NMS
|
||||
c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
|
||||
boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores
|
||||
i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS
|
||||
if i.shape[0] > max_det: # limit detections
|
||||
i = i[:max_det]
|
||||
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
|
||||
# update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
|
||||
iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
|
||||
weights = iou * scores[None] # box weights
|
||||
x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes
|
||||
if redundant:
|
||||
i = i[iou.sum(1) > 1] # require redundancy
|
||||
|
||||
output[xi] = x[i]
|
||||
if (time.time() - t) > time_limit:
|
||||
print(f'WARNING: NMS time limit {time_limit}s exceeded')
|
||||
break # time limit exceeded
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def strip_optimizer(f='best.pt', s=''): # from utils.general import *; strip_optimizer()
|
||||
# Strip optimizer from 'f' to finalize training, optionally save as 's'
|
||||
x = torch.load(f, map_location=torch.device('cpu'))
|
||||
if x.get('ema'):
|
||||
x['model'] = x['ema'] # replace model with ema
|
||||
for k in 'optimizer', 'training_results', 'wandb_id', 'ema', 'updates': # keys
|
||||
x[k] = None
|
||||
x['epoch'] = -1
|
||||
x['model'].half() # to FP16
|
||||
for p in x['model'].parameters():
|
||||
p.requires_grad = False
|
||||
torch.save(x, s or f)
|
||||
mb = os.path.getsize(s or f) / 1E6 # filesize
|
||||
print(f"Optimizer stripped from {f},{(' saved as %s,' % s) if s else ''} {mb:.1f}MB")
|
||||
|
||||
|
||||
def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''):
|
||||
# Print mutation results to evolve.txt (for use with train.py --evolve)
|
||||
a = '%10s' * len(hyp) % tuple(hyp.keys()) # hyperparam keys
|
||||
b = '%10.3g' * len(hyp) % tuple(hyp.values()) # hyperparam values
|
||||
c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
|
||||
print('\n%s\n%s\nEvolved fitness: %s\n' % (a, b, c))
|
||||
|
||||
if bucket:
|
||||
url = 'gs://%s/evolve.txt' % bucket
|
||||
if gsutil_getsize(url) > (os.path.getsize('evolve.txt') if os.path.exists('evolve.txt') else 0):
|
||||
os.system('gsutil cp %s .' % url) # download evolve.txt if larger than local
|
||||
|
||||
with open('evolve.txt', 'a') as f: # append result
|
||||
f.write(c + b + '\n')
|
||||
x = np.unique(np.loadtxt('evolve.txt', ndmin=2), axis=0) # load unique rows
|
||||
x = x[np.argsort(-fitness(x))] # sort
|
||||
np.savetxt('evolve.txt', x, '%10.3g') # save sort by fitness
|
||||
|
||||
# Save yaml
|
||||
for i, k in enumerate(hyp.keys()):
|
||||
hyp[k] = float(x[0, i + 7])
|
||||
with open(yaml_file, 'w') as f:
|
||||
results = tuple(x[0, :7])
|
||||
c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
|
||||
f.write('# Hyperparameter Evolution Results\n# Generations: %g\n# Metrics: ' % len(x) + c + '\n\n')
|
||||
yaml.dump(hyp, f, sort_keys=False)
|
||||
|
||||
if bucket:
|
||||
os.system('gsutil cp evolve.txt %s gs://%s' % (yaml_file, bucket)) # upload
|
||||
|
||||
|
||||
def apply_classifier(x, model, img, im0):
|
||||
# applies a second stage classifier to yolo outputs
|
||||
im0 = [im0] if isinstance(im0, np.ndarray) else im0
|
||||
for i, d in enumerate(x): # per image
|
||||
if d is not None and len(d):
|
||||
d = d.clone()
|
||||
|
||||
# Reshape and pad cutouts
|
||||
b = xyxy2xywh(d[:, :4]) # boxes
|
||||
b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # rectangle to square
|
||||
b[:, 2:] = b[:, 2:] * 1.3 + 30 # pad
|
||||
d[:, :4] = xywh2xyxy(b).long()
|
||||
|
||||
# Rescale boxes from img_size to im0 size
|
||||
scale_coords(img.shape[2:], d[:, :4], im0[i].shape)
|
||||
|
||||
# Classes
|
||||
pred_cls1 = d[:, 5].long()
|
||||
ims = []
|
||||
for j, a in enumerate(d): # per item
|
||||
cutout = im0[i][int(a[1]):int(a[3]), int(a[0]):int(a[2])]
|
||||
im = cv2.resize(cutout, (224, 224)) # BGR
|
||||
# cv2.imwrite('test%i.jpg' % j, cutout)
|
||||
|
||||
im = im[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
|
||||
im = np.ascontiguousarray(im, dtype=np.float32) # uint8 to float32
|
||||
im /= 255.0 # 0 - 255 to 0.0 - 1.0
|
||||
ims.append(im)
|
||||
|
||||
pred_cls2 = model(torch.Tensor(ims).to(d.device)).argmax(1) # classifier prediction
|
||||
x[i] = x[i][pred_cls1 == pred_cls2] # retain matching class detections
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def increment_path(path, exist_ok=True, sep=''):
|
||||
# Increment path, i.e. runs/exp --> runs/exp{sep}0, runs/exp{sep}1 etc.
|
||||
path = Path(path) # os-agnostic
|
||||
if (path.exists() and exist_ok) or (not path.exists()):
|
||||
return str(path)
|
||||
else:
|
||||
dirs = glob.glob(f"{path}{sep}*") # similar paths
|
||||
matches = [re.search(rf"%s{sep}(\d+)" % path.stem, d) for d in dirs]
|
||||
i = [int(m.groups()[0]) for m in matches if m] # indices
|
||||
n = max(i) + 1 if i else 2 # increment number
|
||||
return f"{path}{sep}{n}" # update path
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
FROM gcr.io/google-appengine/python
|
||||
|
||||
# Create a virtualenv for dependencies. This isolates these packages from
|
||||
# system-level packages.
|
||||
# Use -p python3 or -p python3.7 to select python version. Default is version 2.
|
||||
RUN virtualenv /env -p python3
|
||||
|
||||
# Setting these environment variables are the same as running
|
||||
# source /env/bin/activate.
|
||||
ENV VIRTUAL_ENV /env
|
||||
ENV PATH /env/bin:$PATH
|
||||
|
||||
RUN apt-get update && apt-get install -y python-opencv
|
||||
|
||||
# Copy the application's requirements.txt and run pip to install all
|
||||
# dependencies into the virtualenv.
|
||||
ADD requirements.txt /app/requirements.txt
|
||||
RUN pip install -r /app/requirements.txt
|
||||
|
||||
# Add the application source code.
|
||||
ADD . /app
|
||||
|
||||
# Run a WSGI server to serve the application. gunicorn must be declared as
|
||||
# a dependency in requirements.txt.
|
||||
CMD gunicorn -b :$PORT main:app
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# add these requirements in your app on top of the existing ones
|
||||
pip==18.1
|
||||
Flask==1.0.2
|
||||
gunicorn==19.9.0
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
runtime: custom
|
||||
env: flex
|
||||
|
||||
service: yolorapp
|
||||
|
||||
liveness_check:
|
||||
initial_delay_sec: 600
|
||||
|
||||
manual_scaling:
|
||||
instances: 1
|
||||
resources:
|
||||
cpu: 1
|
||||
memory_gb: 4
|
||||
disk_size_gb: 20
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
# Google utils: https://cloud.google.com/storage/docs/reference/libraries
|
||||
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
import torch
|
||||
|
||||
|
||||
def gsutil_getsize(url=''):
|
||||
# gs://bucket/file size https://cloud.google.com/storage/docs/gsutil/commands/du
|
||||
s = subprocess.check_output(f'gsutil du {url}', shell=True).decode('utf-8')
|
||||
return eval(s.split(' ')[0]) if len(s) else 0 # bytes
|
||||
|
||||
|
||||
def attempt_download(file, repo='WongKinYiu/yolov7'):
|
||||
# Attempt file download if does not exist
|
||||
file = Path(str(file).strip().replace("'", '').lower())
|
||||
|
||||
if not file.exists():
|
||||
try:
|
||||
response = requests.get(f'https://api.github.com/repos/{repo}/releases/latest').json() # github api
|
||||
assets = [x['name'] for x in response['assets']] # release assets
|
||||
tag = response['tag_name'] # i.e. 'v1.0'
|
||||
except: # fallback plan
|
||||
assets = ['yolov7.pt', 'yolov7-tiny.pt', 'yolov7x.pt', 'yolov7-d6.pt', 'yolov7-e6.pt',
|
||||
'yolov7-e6e.pt', 'yolov7-w6.pt']
|
||||
tag = subprocess.check_output('git tag', shell=True).decode().split()[-1]
|
||||
|
||||
name = file.name
|
||||
if name in assets:
|
||||
msg = f'{file} missing, try downloading from https://github.com/{repo}/releases/'
|
||||
redundant = False # second download option
|
||||
try: # GitHub
|
||||
url = f'https://github.com/{repo}/releases/download/{tag}/{name}'
|
||||
print(f'Downloading {url} to {file}...')
|
||||
torch.hub.download_url_to_file(url, file)
|
||||
assert file.exists() and file.stat().st_size > 1E6 # check
|
||||
except Exception as e: # GCP
|
||||
print(f'Download error: {e}')
|
||||
assert redundant, 'No secondary mirror'
|
||||
url = f'https://storage.googleapis.com/{repo}/ckpt/{name}'
|
||||
print(f'Downloading {url} to {file}...')
|
||||
os.system(f'curl -L {url} -o {file}') # torch.hub.download_url_to_file(url, weights)
|
||||
finally:
|
||||
if not file.exists() or file.stat().st_size < 1E6: # check
|
||||
file.unlink(missing_ok=True) # remove partial downloads
|
||||
print(f'ERROR: Download failure: {msg}')
|
||||
print('')
|
||||
return
|
||||
|
||||
|
||||
def gdrive_download(id='', file='tmp.zip'):
|
||||
# Downloads a file from Google Drive. from yolov7.utils.google_utils import *; gdrive_download()
|
||||
t = time.time()
|
||||
file = Path(file)
|
||||
cookie = Path('cookie') # gdrive cookie
|
||||
print(f'Downloading https://drive.google.com/uc?export=download&id={id} as {file}... ', end='')
|
||||
file.unlink(missing_ok=True) # remove existing file
|
||||
cookie.unlink(missing_ok=True) # remove existing cookie
|
||||
|
||||
# Attempt file download
|
||||
out = "NUL" if platform.system() == "Windows" else "/dev/null"
|
||||
os.system(f'curl -c ./cookie -s -L "drive.google.com/uc?export=download&id={id}" > {out}')
|
||||
if os.path.exists('cookie'): # large file
|
||||
s = f'curl -Lb ./cookie "drive.google.com/uc?export=download&confirm={get_token()}&id={id}" -o {file}'
|
||||
else: # small file
|
||||
s = f'curl -s -L -o {file} "drive.google.com/uc?export=download&id={id}"'
|
||||
r = os.system(s) # execute, capture return
|
||||
cookie.unlink(missing_ok=True) # remove existing cookie
|
||||
|
||||
# Error check
|
||||
if r != 0:
|
||||
file.unlink(missing_ok=True) # remove partial
|
||||
print('Download error ') # raise Exception('Download error')
|
||||
return r
|
||||
|
||||
# Unzip if archive
|
||||
if file.suffix == '.zip':
|
||||
print('unzipping... ', end='')
|
||||
os.system(f'unzip -q {file}') # unzip
|
||||
file.unlink() # remove zip to free space
|
||||
|
||||
print(f'Done ({time.time() - t:.1f}s)')
|
||||
return r
|
||||
|
||||
|
||||
def get_token(cookie="./cookie"):
|
||||
with open(cookie) as f:
|
||||
for line in f:
|
||||
if "download" in line:
|
||||
return line.split()[-1]
|
||||
return ""
|
||||
|
||||
# def upload_blob(bucket_name, source_file_name, destination_blob_name):
|
||||
# # Uploads a file to a bucket
|
||||
# # https://cloud.google.com/storage/docs/uploading-objects#storage-upload-object-python
|
||||
#
|
||||
# storage_client = storage.Client()
|
||||
# bucket = storage_client.get_bucket(bucket_name)
|
||||
# blob = bucket.blob(destination_blob_name)
|
||||
#
|
||||
# blob.upload_from_filename(source_file_name)
|
||||
#
|
||||
# print('File {} uploaded to {}.'.format(
|
||||
# source_file_name,
|
||||
# destination_blob_name))
|
||||
#
|
||||
#
|
||||
# def download_blob(bucket_name, source_blob_name, destination_file_name):
|
||||
# # Uploads a blob from a bucket
|
||||
# storage_client = storage.Client()
|
||||
# bucket = storage_client.get_bucket(bucket_name)
|
||||
# blob = bucket.blob(source_blob_name)
|
||||
#
|
||||
# blob.download_to_filename(destination_file_name)
|
||||
#
|
||||
# print('Blob {} downloaded to {}.'.format(
|
||||
# source_blob_name,
|
||||
# destination_file_name))
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,227 @@
|
|||
# Model validation metrics
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from . import general
|
||||
|
||||
|
||||
def fitness(x):
|
||||
# Model fitness as a weighted combination of metrics
|
||||
w = [0.0, 0.0, 0.1, 0.9] # weights for [P, R, mAP@0.5, mAP@0.5:0.95]
|
||||
return (x[:, :4] * w).sum(1)
|
||||
|
||||
|
||||
def ap_per_class(tp, conf, pred_cls, target_cls, v5_metric=False, plot=False, save_dir='.', names=()):
|
||||
""" Compute the average precision, given the recall and precision curves.
|
||||
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
|
||||
# Arguments
|
||||
tp: True positives (nparray, nx1 or nx10).
|
||||
conf: Objectness value from 0-1 (nparray).
|
||||
pred_cls: Predicted object classes (nparray).
|
||||
target_cls: True object classes (nparray).
|
||||
plot: Plot precision-recall curve at mAP@0.5
|
||||
save_dir: Plot save directory
|
||||
# Returns
|
||||
The average precision as computed in py-faster-rcnn.
|
||||
"""
|
||||
|
||||
# Sort by objectness
|
||||
i = np.argsort(-conf)
|
||||
tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]
|
||||
|
||||
# Find unique classes
|
||||
unique_classes = np.unique(target_cls)
|
||||
nc = unique_classes.shape[0] # number of classes, number of detections
|
||||
|
||||
# Create Precision-Recall curve and compute AP for each class
|
||||
px, py = np.linspace(0, 1, 1000), [] # for plotting
|
||||
ap, p, r = np.zeros((nc, tp.shape[1])), np.zeros((nc, 1000)), np.zeros((nc, 1000))
|
||||
for ci, c in enumerate(unique_classes):
|
||||
i = pred_cls == c
|
||||
n_l = (target_cls == c).sum() # number of labels
|
||||
n_p = i.sum() # number of predictions
|
||||
|
||||
if n_p == 0 or n_l == 0:
|
||||
continue
|
||||
else:
|
||||
# Accumulate FPs and TPs
|
||||
fpc = (1 - tp[i]).cumsum(0)
|
||||
tpc = tp[i].cumsum(0)
|
||||
|
||||
# Recall
|
||||
recall = tpc / (n_l + 1e-16) # recall curve
|
||||
r[ci] = np.interp(-px, -conf[i], recall[:, 0], left=0) # negative x, xp because xp decreases
|
||||
|
||||
# Precision
|
||||
precision = tpc / (tpc + fpc) # precision curve
|
||||
p[ci] = np.interp(-px, -conf[i], precision[:, 0], left=1) # p at pr_score
|
||||
|
||||
# AP from recall-precision curve
|
||||
for j in range(tp.shape[1]):
|
||||
ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j], v5_metric=v5_metric)
|
||||
if plot and j == 0:
|
||||
py.append(np.interp(px, mrec, mpre)) # precision at mAP@0.5
|
||||
|
||||
# Compute F1 (harmonic mean of precision and recall)
|
||||
f1 = 2 * p * r / (p + r + 1e-16)
|
||||
if plot:
|
||||
plot_pr_curve(px, py, ap, Path(save_dir) / 'PR_curve.png', names)
|
||||
plot_mc_curve(px, f1, Path(save_dir) / 'F1_curve.png', names, ylabel='F1')
|
||||
plot_mc_curve(px, p, Path(save_dir) / 'P_curve.png', names, ylabel='Precision')
|
||||
plot_mc_curve(px, r, Path(save_dir) / 'R_curve.png', names, ylabel='Recall')
|
||||
|
||||
i = f1.mean(0).argmax() # max F1 index
|
||||
return p[:, i], r[:, i], ap, f1[:, i], unique_classes.astype('int32')
|
||||
|
||||
|
||||
def compute_ap(recall, precision, v5_metric=False):
|
||||
""" Compute the average precision, given the recall and precision curves
|
||||
# Arguments
|
||||
recall: The recall curve (list)
|
||||
precision: The precision curve (list)
|
||||
v5_metric: Assume maximum recall to be 1.0, as in YOLOv5, MMDetetion etc.
|
||||
# Returns
|
||||
Average precision, precision curve, recall curve
|
||||
"""
|
||||
|
||||
# Append sentinel values to beginning and end
|
||||
if v5_metric: # New YOLOv5 metric, same as MMDetection and Detectron2 repositories
|
||||
mrec = np.concatenate(([0.], recall, [1.0]))
|
||||
else: # Old YOLOv5 metric, i.e. default YOLOv7 metric
|
||||
mrec = np.concatenate(([0.], recall, [recall[-1] + 0.01]))
|
||||
mpre = np.concatenate(([1.], precision, [0.]))
|
||||
|
||||
# Compute the precision envelope
|
||||
mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
|
||||
|
||||
# Integrate area under curve
|
||||
method = 'interp' # methods: 'continuous', 'interp'
|
||||
if method == 'interp':
|
||||
x = np.linspace(0, 1, 101) # 101-point interp (COCO)
|
||||
ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate
|
||||
else: # 'continuous'
|
||||
i = np.where(mrec[1:] != mrec[:-1])[0] # points where x axis (recall) changes
|
||||
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) # area under curve
|
||||
|
||||
return ap, mpre, mrec
|
||||
|
||||
|
||||
class ConfusionMatrix:
|
||||
# Updated version of https://github.com/kaanakan/object_detection_confusion_matrix
|
||||
def __init__(self, nc, conf=0.25, iou_thres=0.45):
|
||||
self.matrix = np.zeros((nc + 1, nc + 1))
|
||||
self.nc = nc # number of classes
|
||||
self.conf = conf
|
||||
self.iou_thres = iou_thres
|
||||
|
||||
def process_batch(self, detections, labels):
|
||||
"""
|
||||
Return intersection-over-union (Jaccard index) of boxes.
|
||||
Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
|
||||
Arguments:
|
||||
detections (Array[N, 6]), x1, y1, x2, y2, conf, class
|
||||
labels (Array[M, 5]), class, x1, y1, x2, y2
|
||||
Returns:
|
||||
None, updates confusion matrix accordingly
|
||||
"""
|
||||
detections = detections[detections[:, 4] > self.conf]
|
||||
gt_classes = labels[:, 0].int()
|
||||
detection_classes = detections[:, 5].int()
|
||||
iou = general.box_iou(labels[:, 1:], detections[:, :4])
|
||||
|
||||
x = torch.where(iou > self.iou_thres)
|
||||
if x[0].shape[0]:
|
||||
matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy()
|
||||
if x[0].shape[0] > 1:
|
||||
matches = matches[matches[:, 2].argsort()[::-1]]
|
||||
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
|
||||
matches = matches[matches[:, 2].argsort()[::-1]]
|
||||
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
|
||||
else:
|
||||
matches = np.zeros((0, 3))
|
||||
|
||||
n = matches.shape[0] > 0
|
||||
m0, m1, _ = matches.transpose().astype(np.int16)
|
||||
for i, gc in enumerate(gt_classes):
|
||||
j = m0 == i
|
||||
if n and sum(j) == 1:
|
||||
self.matrix[gc, detection_classes[m1[j]]] += 1 # correct
|
||||
else:
|
||||
self.matrix[self.nc, gc] += 1 # background FP
|
||||
|
||||
if n:
|
||||
for i, dc in enumerate(detection_classes):
|
||||
if not any(m1 == i):
|
||||
self.matrix[dc, self.nc] += 1 # background FN
|
||||
|
||||
def matrix(self):
|
||||
return self.matrix
|
||||
|
||||
def plot(self, save_dir='', names=()):
|
||||
try:
|
||||
import seaborn as sn
|
||||
|
||||
array = self.matrix / (self.matrix.sum(0).reshape(1, self.nc + 1) + 1E-6) # normalize
|
||||
array[array < 0.005] = np.nan # don't annotate (would appear as 0.00)
|
||||
|
||||
fig = plt.figure(figsize=(12, 9), tight_layout=True)
|
||||
sn.set(font_scale=1.0 if self.nc < 50 else 0.8) # for label size
|
||||
labels = (0 < len(names) < 99) and len(names) == self.nc # apply names to ticklabels
|
||||
sn.heatmap(array, annot=self.nc < 30, annot_kws={"size": 8}, cmap='Blues', fmt='.2f', square=True,
|
||||
xticklabels=names + ['background FP'] if labels else "auto",
|
||||
yticklabels=names + ['background FN'] if labels else "auto").set_facecolor((1, 1, 1))
|
||||
fig.axes[0].set_xlabel('True')
|
||||
fig.axes[0].set_ylabel('Predicted')
|
||||
fig.savefig(Path(save_dir) / 'confusion_matrix.png', dpi=250)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def print(self):
|
||||
for i in range(self.nc + 1):
|
||||
print(' '.join(map(str, self.matrix[i])))
|
||||
|
||||
|
||||
# Plots ----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def plot_pr_curve(px, py, ap, save_dir='pr_curve.png', names=()):
|
||||
# Precision-recall curve
|
||||
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
|
||||
py = np.stack(py, axis=1)
|
||||
|
||||
if 0 < len(names) < 21: # display per-class legend if < 21 classes
|
||||
for i, y in enumerate(py.T):
|
||||
ax.plot(px, y, linewidth=1, label=f'{names[i]} {ap[i, 0]:.3f}') # plot(recall, precision)
|
||||
else:
|
||||
ax.plot(px, py, linewidth=1, color='grey') # plot(recall, precision)
|
||||
|
||||
ax.plot(px, py.mean(1), linewidth=3, color='blue', label='all classes %.3f mAP@0.5' % ap[:, 0].mean())
|
||||
ax.set_xlabel('Recall')
|
||||
ax.set_ylabel('Precision')
|
||||
ax.set_xlim(0, 1)
|
||||
ax.set_ylim(0, 1)
|
||||
plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
|
||||
fig.savefig(Path(save_dir), dpi=250)
|
||||
|
||||
|
||||
def plot_mc_curve(px, py, save_dir='mc_curve.png', names=(), xlabel='Confidence', ylabel='Metric'):
|
||||
# Metric-confidence curve
|
||||
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
|
||||
|
||||
if 0 < len(names) < 21: # display per-class legend if < 21 classes
|
||||
for i, y in enumerate(py):
|
||||
ax.plot(px, y, linewidth=1, label=f'{names[i]}') # plot(confidence, metric)
|
||||
else:
|
||||
ax.plot(px, py.T, linewidth=1, color='grey') # plot(confidence, metric)
|
||||
|
||||
y = py.mean(0)
|
||||
ax.plot(px, y, linewidth=3, color='blue', label=f'all classes {y.max():.2f} at {px[y.argmax()]:.3f}')
|
||||
ax.set_xlabel(xlabel)
|
||||
ax.set_ylabel(ylabel)
|
||||
ax.set_xlim(0, 1)
|
||||
ax.set_ylim(0, 1)
|
||||
plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
|
||||
fig.savefig(Path(save_dir), dpi=250)
|
||||
|
|
@ -0,0 +1,489 @@
|
|||
# Plotting utils
|
||||
|
||||
import glob
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import seaborn as sns
|
||||
import torch
|
||||
import yaml
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from scipy.signal import butter, filtfilt
|
||||
|
||||
from utils.general import xywh2xyxy, xyxy2xywh
|
||||
from utils.metrics import fitness
|
||||
|
||||
# Settings
|
||||
matplotlib.rc('font', **{'size': 11})
|
||||
matplotlib.use('Agg') # for writing to files only
|
||||
|
||||
|
||||
def color_list():
|
||||
# Return first 10 plt colors as (r,g,b) https://stackoverflow.com/questions/51350872/python-from-color-name-to-rgb
|
||||
def hex2rgb(h):
|
||||
return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))
|
||||
|
||||
return [hex2rgb(h) for h in matplotlib.colors.TABLEAU_COLORS.values()] # or BASE_ (8), CSS4_ (148), XKCD_ (949)
|
||||
|
||||
|
||||
def hist2d(x, y, n=100):
|
||||
# 2d histogram used in labels.png and evolve.png
|
||||
xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n)
|
||||
hist, xedges, yedges = np.histogram2d(x, y, (xedges, yedges))
|
||||
xidx = np.clip(np.digitize(x, xedges) - 1, 0, hist.shape[0] - 1)
|
||||
yidx = np.clip(np.digitize(y, yedges) - 1, 0, hist.shape[1] - 1)
|
||||
return np.log(hist[xidx, yidx])
|
||||
|
||||
|
||||
def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5):
|
||||
# https://stackoverflow.com/questions/28536191/how-to-filter-smooth-with-scipy-numpy
|
||||
def butter_lowpass(cutoff, fs, order):
|
||||
nyq = 0.5 * fs
|
||||
normal_cutoff = cutoff / nyq
|
||||
return butter(order, normal_cutoff, btype='low', analog=False)
|
||||
|
||||
b, a = butter_lowpass(cutoff, fs, order=order)
|
||||
return filtfilt(b, a, data) # forward-backward filter
|
||||
|
||||
|
||||
def plot_one_box(x, img, color=None, label=None, line_thickness=3):
|
||||
# Plots one bounding box on image img
|
||||
tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 # line/font thickness
|
||||
color = color or [random.randint(0, 255) for _ in range(3)]
|
||||
c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
|
||||
cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
|
||||
if label:
|
||||
tf = max(tl - 1, 1) # font thickness
|
||||
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
||||
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
|
||||
cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
|
||||
cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)
|
||||
|
||||
|
||||
def plot_one_box_PIL(box, img, color=None, label=None, line_thickness=None):
|
||||
img = Image.fromarray(img)
|
||||
draw = ImageDraw.Draw(img)
|
||||
line_thickness = line_thickness or max(int(min(img.size) / 200), 2)
|
||||
draw.rectangle(box, width=line_thickness, outline=tuple(color)) # plot
|
||||
if label:
|
||||
fontsize = max(round(max(img.size) / 40), 12)
|
||||
font = ImageFont.truetype("Arial.ttf", fontsize)
|
||||
txt_width, txt_height = font.getsize(label)
|
||||
draw.rectangle([box[0], box[1] - txt_height + 4, box[0] + txt_width, box[1]], fill=tuple(color))
|
||||
draw.text((box[0], box[1] - txt_height + 1), label, fill=(255, 255, 255), font=font)
|
||||
return np.asarray(img)
|
||||
|
||||
|
||||
def plot_wh_methods(): # from utils.plots import *; plot_wh_methods()
|
||||
# Compares the two methods for width-height anchor multiplication
|
||||
# https://github.com/ultralytics/yolov3/issues/168
|
||||
x = np.arange(-4.0, 4.0, .1)
|
||||
ya = np.exp(x)
|
||||
yb = torch.sigmoid(torch.from_numpy(x)).numpy() * 2
|
||||
|
||||
fig = plt.figure(figsize=(6, 3), tight_layout=True)
|
||||
plt.plot(x, ya, '.-', label='YOLOv3')
|
||||
plt.plot(x, yb ** 2, '.-', label='YOLOR ^2')
|
||||
plt.plot(x, yb ** 1.6, '.-', label='YOLOR ^1.6')
|
||||
plt.xlim(left=-4, right=4)
|
||||
plt.ylim(bottom=0, top=6)
|
||||
plt.xlabel('input')
|
||||
plt.ylabel('output')
|
||||
plt.grid()
|
||||
plt.legend()
|
||||
fig.savefig('comparison.png', dpi=200)
|
||||
|
||||
|
||||
def output_to_target(output):
|
||||
# Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
|
||||
targets = []
|
||||
for i, o in enumerate(output):
|
||||
for *box, conf, cls in o.cpu().numpy():
|
||||
targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf])
|
||||
return np.array(targets)
|
||||
|
||||
|
||||
def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=640, max_subplots=16):
|
||||
# Plot image grid with labels
|
||||
|
||||
if isinstance(images, torch.Tensor):
|
||||
images = images.cpu().float().numpy()
|
||||
if isinstance(targets, torch.Tensor):
|
||||
targets = targets.cpu().numpy()
|
||||
|
||||
# un-normalise
|
||||
if np.max(images[0]) <= 1:
|
||||
images *= 255
|
||||
|
||||
tl = 3 # line thickness
|
||||
tf = max(tl - 1, 1) # font thickness
|
||||
bs, _, h, w = images.shape # batch size, _, height, width
|
||||
bs = min(bs, max_subplots) # limit plot images
|
||||
ns = np.ceil(bs ** 0.5) # number of subplots (square)
|
||||
|
||||
# Check if we should resize
|
||||
scale_factor = max_size / max(h, w)
|
||||
if scale_factor < 1:
|
||||
h = math.ceil(scale_factor * h)
|
||||
w = math.ceil(scale_factor * w)
|
||||
|
||||
colors = color_list() # list of colors
|
||||
mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) # init
|
||||
for i, img in enumerate(images):
|
||||
if i == max_subplots: # if last batch has fewer images than we expect
|
||||
break
|
||||
|
||||
block_x = int(w * (i // ns))
|
||||
block_y = int(h * (i % ns))
|
||||
|
||||
img = img.transpose(1, 2, 0)
|
||||
if scale_factor < 1:
|
||||
img = cv2.resize(img, (w, h))
|
||||
|
||||
mosaic[block_y:block_y + h, block_x:block_x + w, :] = img
|
||||
if len(targets) > 0:
|
||||
image_targets = targets[targets[:, 0] == i]
|
||||
boxes = xywh2xyxy(image_targets[:, 2:6]).T
|
||||
classes = image_targets[:, 1].astype('int')
|
||||
labels = image_targets.shape[1] == 6 # labels if no conf column
|
||||
conf = None if labels else image_targets[:, 6] # check for confidence presence (label vs pred)
|
||||
|
||||
if boxes.shape[1]:
|
||||
if boxes.max() <= 1.01: # if normalized with tolerance 0.01
|
||||
boxes[[0, 2]] *= w # scale to pixels
|
||||
boxes[[1, 3]] *= h
|
||||
elif scale_factor < 1: # absolute coords need scale if image scales
|
||||
boxes *= scale_factor
|
||||
boxes[[0, 2]] += block_x
|
||||
boxes[[1, 3]] += block_y
|
||||
for j, box in enumerate(boxes.T):
|
||||
cls = int(classes[j])
|
||||
color = colors[cls % len(colors)]
|
||||
cls = names[cls] if names else cls
|
||||
if labels or conf[j] > 0.25: # 0.25 conf thresh
|
||||
label = '%s' % cls if labels else '%s %.1f' % (cls, conf[j])
|
||||
plot_one_box(box, mosaic, label=label, color=color, line_thickness=tl)
|
||||
|
||||
# Draw image filename labels
|
||||
if paths:
|
||||
label = Path(paths[i]).name[:40] # trim to 40 char
|
||||
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
||||
cv2.putText(mosaic, label, (block_x + 5, block_y + t_size[1] + 5), 0, tl / 3, [220, 220, 220], thickness=tf,
|
||||
lineType=cv2.LINE_AA)
|
||||
|
||||
# Image border
|
||||
cv2.rectangle(mosaic, (block_x, block_y), (block_x + w, block_y + h), (255, 255, 255), thickness=3)
|
||||
|
||||
if fname:
|
||||
r = min(1280. / max(h, w) / ns, 1.0) # ratio to limit image size
|
||||
mosaic = cv2.resize(mosaic, (int(ns * w * r), int(ns * h * r)), interpolation=cv2.INTER_AREA)
|
||||
# cv2.imwrite(fname, cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB)) # cv2 save
|
||||
Image.fromarray(mosaic).save(fname) # PIL save
|
||||
return mosaic
|
||||
|
||||
|
||||
def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''):
|
||||
# Plot LR simulating training for full epochs
|
||||
optimizer, scheduler = copy(optimizer), copy(scheduler) # do not modify originals
|
||||
y = []
|
||||
for _ in range(epochs):
|
||||
scheduler.step()
|
||||
y.append(optimizer.param_groups[0]['lr'])
|
||||
plt.plot(y, '.-', label='LR')
|
||||
plt.xlabel('epoch')
|
||||
plt.ylabel('LR')
|
||||
plt.grid()
|
||||
plt.xlim(0, epochs)
|
||||
plt.ylim(0)
|
||||
plt.savefig(Path(save_dir) / 'LR.png', dpi=200)
|
||||
plt.close()
|
||||
|
||||
|
||||
def plot_test_txt(): # from utils.plots import *; plot_test()
|
||||
# Plot test.txt histograms
|
||||
x = np.loadtxt('test.txt', dtype=np.float32)
|
||||
box = xyxy2xywh(x[:, :4])
|
||||
cx, cy = box[:, 0], box[:, 1]
|
||||
|
||||
fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True)
|
||||
ax.hist2d(cx, cy, bins=600, cmax=10, cmin=0)
|
||||
ax.set_aspect('equal')
|
||||
plt.savefig('hist2d.png', dpi=300)
|
||||
|
||||
fig, ax = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True)
|
||||
ax[0].hist(cx, bins=600)
|
||||
ax[1].hist(cy, bins=600)
|
||||
plt.savefig('hist1d.png', dpi=200)
|
||||
|
||||
|
||||
def plot_targets_txt(): # from utils.plots import *; plot_targets_txt()
|
||||
# Plot targets.txt histograms
|
||||
x = np.loadtxt('targets.txt', dtype=np.float32).T
|
||||
s = ['x targets', 'y targets', 'width targets', 'height targets']
|
||||
fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
|
||||
ax = ax.ravel()
|
||||
for i in range(4):
|
||||
ax[i].hist(x[i], bins=100, label='%.3g +/- %.3g' % (x[i].mean(), x[i].std()))
|
||||
ax[i].legend()
|
||||
ax[i].set_title(s[i])
|
||||
plt.savefig('targets.jpg', dpi=200)
|
||||
|
||||
|
||||
def plot_study_txt(path='', x=None): # from utils.plots import *; plot_study_txt()
|
||||
# Plot study.txt generated by test.py
|
||||
fig, ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True)
|
||||
# ax = ax.ravel()
|
||||
|
||||
fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True)
|
||||
# for f in [Path(path) / f'study_coco_{x}.txt' for x in ['yolor-p6', 'yolor-w6', 'yolor-e6', 'yolor-d6']]:
|
||||
for f in sorted(Path(path).glob('study*.txt')):
|
||||
y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T
|
||||
x = np.arange(y.shape[1]) if x is None else np.array(x)
|
||||
s = ['P', 'R', 'mAP@.5', 'mAP@.5:.95', 't_inference (ms/img)', 't_NMS (ms/img)', 't_total (ms/img)']
|
||||
# for i in range(7):
|
||||
# ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8)
|
||||
# ax[i].set_title(s[i])
|
||||
|
||||
j = y[3].argmax() + 1
|
||||
ax2.plot(y[6, 1:j], y[3, 1:j] * 1E2, '.-', linewidth=2, markersize=8,
|
||||
label=f.stem.replace('study_coco_', '').replace('yolo', 'YOLO'))
|
||||
|
||||
ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5],
|
||||
'k.-', linewidth=2, markersize=8, alpha=.25, label='EfficientDet')
|
||||
|
||||
ax2.grid(alpha=0.2)
|
||||
ax2.set_yticks(np.arange(20, 60, 5))
|
||||
ax2.set_xlim(0, 57)
|
||||
ax2.set_ylim(30, 55)
|
||||
ax2.set_xlabel('GPU Speed (ms/img)')
|
||||
ax2.set_ylabel('COCO AP val')
|
||||
ax2.legend(loc='lower right')
|
||||
plt.savefig(str(Path(path).name) + '.png', dpi=300)
|
||||
|
||||
|
||||
def plot_labels(labels, names=(), save_dir=Path(''), loggers=None):
|
||||
# plot dataset labels
|
||||
print('Plotting labels... ')
|
||||
c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes
|
||||
nc = int(c.max() + 1) # number of classes
|
||||
colors = color_list()
|
||||
x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height'])
|
||||
|
||||
# seaborn correlogram
|
||||
sns.pairplot(x, corner=True, diag_kind='auto', kind='hist', diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9))
|
||||
plt.savefig(save_dir / 'labels_correlogram.jpg', dpi=200)
|
||||
plt.close()
|
||||
|
||||
# matplotlib labels
|
||||
matplotlib.use('svg') # faster
|
||||
ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel()
|
||||
ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8)
|
||||
ax[0].set_ylabel('instances')
|
||||
if 0 < len(names) < 30:
|
||||
ax[0].set_xticks(range(len(names)))
|
||||
ax[0].set_xticklabels(names, rotation=90, fontsize=10)
|
||||
else:
|
||||
ax[0].set_xlabel('classes')
|
||||
sns.histplot(x, x='x', y='y', ax=ax[2], bins=50, pmax=0.9)
|
||||
sns.histplot(x, x='width', y='height', ax=ax[3], bins=50, pmax=0.9)
|
||||
|
||||
# rectangles
|
||||
labels[:, 1:3] = 0.5 # center
|
||||
labels[:, 1:] = xywh2xyxy(labels[:, 1:]) * 2000
|
||||
img = Image.fromarray(np.ones((2000, 2000, 3), dtype=np.uint8) * 255)
|
||||
for cls, *box in labels[:1000]:
|
||||
ImageDraw.Draw(img).rectangle(box, width=1, outline=colors[int(cls) % 10]) # plot
|
||||
ax[1].imshow(img)
|
||||
ax[1].axis('off')
|
||||
|
||||
for a in [0, 1, 2, 3]:
|
||||
for s in ['top', 'right', 'left', 'bottom']:
|
||||
ax[a].spines[s].set_visible(False)
|
||||
|
||||
plt.savefig(save_dir / 'labels.jpg', dpi=200)
|
||||
matplotlib.use('Agg')
|
||||
plt.close()
|
||||
|
||||
# loggers
|
||||
for k, v in loggers.items() or {}:
|
||||
if k == 'wandb' and v:
|
||||
v.log({"Labels": [v.Image(str(x), caption=x.name) for x in save_dir.glob('*labels*.jpg')]}, commit=False)
|
||||
|
||||
|
||||
def plot_evolution(yaml_file='data/hyp.finetune.yaml'): # from utils.plots import *; plot_evolution()
|
||||
# Plot hyperparameter evolution results in evolve.txt
|
||||
with open(yaml_file) as f:
|
||||
hyp = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
x = np.loadtxt('evolve.txt', ndmin=2)
|
||||
f = fitness(x)
|
||||
# weights = (f - f.min()) ** 2 # for weighted results
|
||||
plt.figure(figsize=(10, 12), tight_layout=True)
|
||||
matplotlib.rc('font', **{'size': 8})
|
||||
for i, (k, v) in enumerate(hyp.items()):
|
||||
y = x[:, i + 7]
|
||||
# mu = (y * weights).sum() / weights.sum() # best weighted result
|
||||
mu = y[f.argmax()] # best single result
|
||||
plt.subplot(6, 5, i + 1)
|
||||
plt.scatter(y, f, c=hist2d(y, f, 20), cmap='viridis', alpha=.8, edgecolors='none')
|
||||
plt.plot(mu, f.max(), 'k+', markersize=15)
|
||||
plt.title('%s = %.3g' % (k, mu), fontdict={'size': 9}) # limit to 40 characters
|
||||
if i % 5 != 0:
|
||||
plt.yticks([])
|
||||
print('%15s: %.3g' % (k, mu))
|
||||
plt.savefig('evolve.png', dpi=200)
|
||||
print('\nPlot saved as evolve.png')
|
||||
|
||||
|
||||
def profile_idetection(start=0, stop=0, labels=(), save_dir=''):
|
||||
# Plot iDetection '*.txt' per-image logs. from utils.plots import *; profile_idetection()
|
||||
ax = plt.subplots(2, 4, figsize=(12, 6), tight_layout=True)[1].ravel()
|
||||
s = ['Images', 'Free Storage (GB)', 'RAM Usage (GB)', 'Battery', 'dt_raw (ms)', 'dt_smooth (ms)', 'real-world FPS']
|
||||
files = list(Path(save_dir).glob('frames*.txt'))
|
||||
for fi, f in enumerate(files):
|
||||
try:
|
||||
results = np.loadtxt(f, ndmin=2).T[:, 90:-30] # clip first and last rows
|
||||
n = results.shape[1] # number of rows
|
||||
x = np.arange(start, min(stop, n) if stop else n)
|
||||
results = results[:, x]
|
||||
t = (results[0] - results[0].min()) # set t0=0s
|
||||
results[0] = x
|
||||
for i, a in enumerate(ax):
|
||||
if i < len(results):
|
||||
label = labels[fi] if len(labels) else f.stem.replace('frames_', '')
|
||||
a.plot(t, results[i], marker='.', label=label, linewidth=1, markersize=5)
|
||||
a.set_title(s[i])
|
||||
a.set_xlabel('time (s)')
|
||||
# if fi == len(files) - 1:
|
||||
# a.set_ylim(bottom=0)
|
||||
for side in ['top', 'right']:
|
||||
a.spines[side].set_visible(False)
|
||||
else:
|
||||
a.remove()
|
||||
except Exception as e:
|
||||
print('Warning: Plotting error for %s; %s' % (f, e))
|
||||
|
||||
ax[1].legend()
|
||||
plt.savefig(Path(save_dir) / 'idetection_profile.png', dpi=200)
|
||||
|
||||
|
||||
def plot_results_overlay(start=0, stop=0): # from utils.plots import *; plot_results_overlay()
|
||||
# Plot training 'results*.txt', overlaying train and val losses
|
||||
s = ['train', 'train', 'train', 'Precision', 'mAP@0.5', 'val', 'val', 'val', 'Recall', 'mAP@0.5:0.95'] # legends
|
||||
t = ['Box', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles
|
||||
for f in sorted(glob.glob('results*.txt') + glob.glob('../../Downloads/results*.txt')):
|
||||
results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
|
||||
n = results.shape[1] # number of rows
|
||||
x = range(start, min(stop, n) if stop else n)
|
||||
fig, ax = plt.subplots(1, 5, figsize=(14, 3.5), tight_layout=True)
|
||||
ax = ax.ravel()
|
||||
for i in range(5):
|
||||
for j in [i, i + 5]:
|
||||
y = results[j, x]
|
||||
ax[i].plot(x, y, marker='.', label=s[j])
|
||||
# y_smooth = butter_lowpass_filtfilt(y)
|
||||
# ax[i].plot(x, np.gradient(y_smooth), marker='.', label=s[j])
|
||||
|
||||
ax[i].set_title(t[i])
|
||||
ax[i].legend()
|
||||
ax[i].set_ylabel(f) if i == 0 else None # add filename
|
||||
fig.savefig(f.replace('.txt', '.png'), dpi=200)
|
||||
|
||||
|
||||
def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''):
|
||||
# Plot training 'results*.txt'. from utils.plots import *; plot_results(save_dir='runs/train/exp')
|
||||
fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True)
|
||||
ax = ax.ravel()
|
||||
s = ['Box', 'Objectness', 'Classification', 'Precision', 'Recall',
|
||||
'val Box', 'val Objectness', 'val Classification', 'mAP@0.5', 'mAP@0.5:0.95']
|
||||
if bucket:
|
||||
# files = ['https://storage.googleapis.com/%s/results%g.txt' % (bucket, x) for x in id]
|
||||
files = ['results%g.txt' % x for x in id]
|
||||
c = ('gsutil cp ' + '%s ' * len(files) + '.') % tuple('gs://%s/results%g.txt' % (bucket, x) for x in id)
|
||||
os.system(c)
|
||||
else:
|
||||
files = list(Path(save_dir).glob('results*.txt'))
|
||||
assert len(files), 'No results.txt files found in %s, nothing to plot.' % os.path.abspath(save_dir)
|
||||
for fi, f in enumerate(files):
|
||||
try:
|
||||
results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
|
||||
n = results.shape[1] # number of rows
|
||||
x = range(start, min(stop, n) if stop else n)
|
||||
for i in range(10):
|
||||
y = results[i, x]
|
||||
if i in [0, 1, 2, 5, 6, 7]:
|
||||
y[y == 0] = np.nan # don't show zero loss values
|
||||
# y /= y[0] # normalize
|
||||
label = labels[fi] if len(labels) else f.stem
|
||||
ax[i].plot(x, y, marker='.', label=label, linewidth=2, markersize=8)
|
||||
ax[i].set_title(s[i])
|
||||
# if i in [5, 6, 7]: # share train and val loss y axes
|
||||
# ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
|
||||
except Exception as e:
|
||||
print('Warning: Plotting error for %s; %s' % (f, e))
|
||||
|
||||
ax[1].legend()
|
||||
fig.savefig(Path(save_dir) / 'results.png', dpi=200)
|
||||
|
||||
|
||||
def output_to_keypoint(output):
|
||||
# Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
|
||||
targets = []
|
||||
for i, o in enumerate(output):
|
||||
kpts = o[:,6:]
|
||||
o = o[:,:6]
|
||||
for index, (*box, conf, cls) in enumerate(o.detach().cpu().numpy()):
|
||||
targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf, *list(kpts.detach().cpu().numpy()[index])])
|
||||
return np.array(targets)
|
||||
|
||||
|
||||
def plot_skeleton_kpts(im, kpts, steps, orig_shape=None):
|
||||
#Plot the skeleton and keypointsfor coco datatset
|
||||
palette = np.array([[255, 128, 0], [255, 153, 51], [255, 178, 102],
|
||||
[230, 230, 0], [255, 153, 255], [153, 204, 255],
|
||||
[255, 102, 255], [255, 51, 255], [102, 178, 255],
|
||||
[51, 153, 255], [255, 153, 153], [255, 102, 102],
|
||||
[255, 51, 51], [153, 255, 153], [102, 255, 102],
|
||||
[51, 255, 51], [0, 255, 0], [0, 0, 255], [255, 0, 0],
|
||||
[255, 255, 255]])
|
||||
|
||||
skeleton = [[16, 14], [14, 12], [17, 15], [15, 13], [12, 13], [6, 12],
|
||||
[7, 13], [6, 7], [6, 8], [7, 9], [8, 10], [9, 11], [2, 3],
|
||||
[1, 2], [1, 3], [2, 4], [3, 5], [4, 6], [5, 7]]
|
||||
|
||||
pose_limb_color = palette[[9, 9, 9, 9, 7, 7, 7, 0, 0, 0, 0, 0, 16, 16, 16, 16, 16, 16, 16]]
|
||||
pose_kpt_color = palette[[16, 16, 16, 16, 16, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9]]
|
||||
radius = 5
|
||||
num_kpts = len(kpts) // steps
|
||||
|
||||
for kid in range(num_kpts):
|
||||
r, g, b = pose_kpt_color[kid]
|
||||
x_coord, y_coord = kpts[steps * kid], kpts[steps * kid + 1]
|
||||
if not (x_coord % 640 == 0 or y_coord % 640 == 0):
|
||||
if steps == 3:
|
||||
conf = kpts[steps * kid + 2]
|
||||
if conf < 0.5:
|
||||
continue
|
||||
cv2.circle(im, (int(x_coord), int(y_coord)), radius, (int(r), int(g), int(b)), -1)
|
||||
|
||||
for sk_id, sk in enumerate(skeleton):
|
||||
r, g, b = pose_limb_color[sk_id]
|
||||
pos1 = (int(kpts[(sk[0]-1)*steps]), int(kpts[(sk[0]-1)*steps+1]))
|
||||
pos2 = (int(kpts[(sk[1]-1)*steps]), int(kpts[(sk[1]-1)*steps+1]))
|
||||
if steps == 3:
|
||||
conf1 = kpts[(sk[0]-1)*steps+2]
|
||||
conf2 = kpts[(sk[1]-1)*steps+2]
|
||||
if conf1<0.5 or conf2<0.5:
|
||||
continue
|
||||
if pos1[0]%640 == 0 or pos1[1]%640==0 or pos1[0]<0 or pos1[1]<0:
|
||||
continue
|
||||
if pos2[0] % 640 == 0 or pos2[1] % 640 == 0 or pos2[0]<0 or pos2[1]<0:
|
||||
continue
|
||||
cv2.line(im, pos1, pos2, (int(r), int(g), int(b)), thickness=2)
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
# YOLOR PyTorch utils
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import torch.backends.cudnn as cudnn
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torchvision
|
||||
|
||||
try:
|
||||
import thop # for FLOPS computation
|
||||
except ImportError:
|
||||
thop = None
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def torch_distributed_zero_first(local_rank: int):
|
||||
"""
|
||||
Decorator to make all processes in distributed training wait for each local_master to do something.
|
||||
"""
|
||||
if local_rank not in [-1, 0]:
|
||||
torch.distributed.barrier()
|
||||
yield
|
||||
if local_rank == 0:
|
||||
torch.distributed.barrier()
|
||||
|
||||
|
||||
def init_torch_seeds(seed=0):
|
||||
# Speed-reproducibility tradeoff https://pytorch.org/docs/stable/notes/randomness.html
|
||||
torch.manual_seed(seed)
|
||||
if seed == 0: # slower, more reproducible
|
||||
cudnn.benchmark, cudnn.deterministic = False, True
|
||||
else: # faster, less reproducible
|
||||
cudnn.benchmark, cudnn.deterministic = True, False
|
||||
|
||||
|
||||
def date_modified(path=__file__):
|
||||
# return human-readable file modification date, i.e. '2021-3-26'
|
||||
t = datetime.datetime.fromtimestamp(Path(path).stat().st_mtime)
|
||||
return f'{t.year}-{t.month}-{t.day}'
|
||||
|
||||
|
||||
def git_describe(path=Path(__file__).parent): # path must be a directory
|
||||
# return human-readable git description, i.e. v5.0-5-g3e25f1e https://git-scm.com/docs/git-describe
|
||||
s = f'git -C {path} describe --tags --long --always'
|
||||
try:
|
||||
return subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).decode()[:-1]
|
||||
except subprocess.CalledProcessError as e:
|
||||
return '' # not a git repository
|
||||
|
||||
|
||||
def select_device(device='', batch_size=None):
|
||||
# device = 'cpu' or '0' or '0,1,2,3'
|
||||
s = f'YOLOR 🚀 {git_describe() or date_modified()} torch {torch.__version__} ' # string
|
||||
cpu = device.lower() == 'cpu'
|
||||
if cpu:
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # force torch.cuda.is_available() = False
|
||||
elif device: # non-cpu device requested
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = device # set environment variable
|
||||
assert torch.cuda.is_available(), f'CUDA unavailable, invalid device {device} requested' # check availability
|
||||
|
||||
cuda = not cpu and torch.cuda.is_available()
|
||||
if cuda:
|
||||
n = torch.cuda.device_count()
|
||||
if n > 1 and batch_size: # check that batch_size is compatible with device_count
|
||||
assert batch_size % n == 0, f'batch-size {batch_size} not multiple of GPU count {n}'
|
||||
space = ' ' * len(s)
|
||||
for i, d in enumerate(device.split(',') if device else range(n)):
|
||||
p = torch.cuda.get_device_properties(i)
|
||||
s += f"{'' if i == 0 else space}CUDA:{d} ({p.name}, {p.total_memory / 1024 ** 2}MB)\n" # bytes to MB
|
||||
else:
|
||||
s += 'CPU\n'
|
||||
|
||||
logger.info(s.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else s) # emoji-safe
|
||||
return torch.device('cuda:0' if cuda else 'cpu')
|
||||
|
||||
|
||||
def time_synchronized():
|
||||
# pytorch-accurate time
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.synchronize()
|
||||
return time.time()
|
||||
|
||||
|
||||
def profile(x, ops, n=100, device=None):
|
||||
# profile a pytorch module or list of modules. Example usage:
|
||||
# x = torch.randn(16, 3, 640, 640) # input
|
||||
# m1 = lambda x: x * torch.sigmoid(x)
|
||||
# m2 = nn.SiLU()
|
||||
# profile(x, [m1, m2], n=100) # profile speed over 100 iterations
|
||||
|
||||
device = device or torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
|
||||
x = x.to(device)
|
||||
x.requires_grad = True
|
||||
print(torch.__version__, device.type, torch.cuda.get_device_properties(0) if device.type == 'cuda' else '')
|
||||
print(f"\n{'Params':>12s}{'GFLOPS':>12s}{'forward (ms)':>16s}{'backward (ms)':>16s}{'input':>24s}{'output':>24s}")
|
||||
for m in ops if isinstance(ops, list) else [ops]:
|
||||
m = m.to(device) if hasattr(m, 'to') else m # device
|
||||
m = m.half() if hasattr(m, 'half') and isinstance(x, torch.Tensor) and x.dtype is torch.float16 else m # type
|
||||
dtf, dtb, t = 0., 0., [0., 0., 0.] # dt forward, backward
|
||||
try:
|
||||
flops = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 # GFLOPS
|
||||
except:
|
||||
flops = 0
|
||||
|
||||
for _ in range(n):
|
||||
t[0] = time_synchronized()
|
||||
y = m(x)
|
||||
t[1] = time_synchronized()
|
||||
try:
|
||||
_ = y.sum().backward()
|
||||
t[2] = time_synchronized()
|
||||
except: # no backward method
|
||||
t[2] = float('nan')
|
||||
dtf += (t[1] - t[0]) * 1000 / n # ms per op forward
|
||||
dtb += (t[2] - t[1]) * 1000 / n # ms per op backward
|
||||
|
||||
s_in = tuple(x.shape) if isinstance(x, torch.Tensor) else 'list'
|
||||
s_out = tuple(y.shape) if isinstance(y, torch.Tensor) else 'list'
|
||||
p = sum(list(x.numel() for x in m.parameters())) if isinstance(m, nn.Module) else 0 # parameters
|
||||
print(f'{p:12}{flops:12.4g}{dtf:16.4g}{dtb:16.4g}{str(s_in):>24s}{str(s_out):>24s}')
|
||||
|
||||
|
||||
def is_parallel(model):
|
||||
return type(model) in (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel)
|
||||
|
||||
|
||||
def intersect_dicts(da, db, exclude=()):
|
||||
# Dictionary intersection of matching keys and shapes, omitting 'exclude' keys, using da values
|
||||
return {k: v for k, v in da.items() if k in db and not any(x in k for x in exclude) and v.shape == db[k].shape}
|
||||
|
||||
|
||||
def initialize_weights(model):
|
||||
for m in model.modules():
|
||||
t = type(m)
|
||||
if t is nn.Conv2d:
|
||||
pass # nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
||||
elif t is nn.BatchNorm2d:
|
||||
m.eps = 1e-3
|
||||
m.momentum = 0.03
|
||||
elif t in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6]:
|
||||
m.inplace = True
|
||||
|
||||
|
||||
def find_modules(model, mclass=nn.Conv2d):
|
||||
# Finds layer indices matching module class 'mclass'
|
||||
return [i for i, m in enumerate(model.module_list) if isinstance(m, mclass)]
|
||||
|
||||
|
||||
def sparsity(model):
|
||||
# Return global model sparsity
|
||||
a, b = 0., 0.
|
||||
for p in model.parameters():
|
||||
a += p.numel()
|
||||
b += (p == 0).sum()
|
||||
return b / a
|
||||
|
||||
|
||||
def prune(model, amount=0.3):
|
||||
# Prune model to requested global sparsity
|
||||
import torch.nn.utils.prune as prune
|
||||
print('Pruning model... ', end='')
|
||||
for name, m in model.named_modules():
|
||||
if isinstance(m, nn.Conv2d):
|
||||
prune.l1_unstructured(m, name='weight', amount=amount) # prune
|
||||
prune.remove(m, 'weight') # make permanent
|
||||
print(' %.3g global sparsity' % sparsity(model))
|
||||
|
||||
|
||||
def fuse_conv_and_bn(conv, bn):
|
||||
# Fuse convolution and batchnorm layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/
|
||||
fusedconv = nn.Conv2d(conv.in_channels,
|
||||
conv.out_channels,
|
||||
kernel_size=conv.kernel_size,
|
||||
stride=conv.stride,
|
||||
padding=conv.padding,
|
||||
groups=conv.groups,
|
||||
bias=True).requires_grad_(False).to(conv.weight.device)
|
||||
|
||||
# prepare filters
|
||||
w_conv = conv.weight.clone().view(conv.out_channels, -1)
|
||||
w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps + bn.running_var)))
|
||||
fusedconv.weight.copy_(torch.mm(w_bn, w_conv).view(fusedconv.weight.shape))
|
||||
|
||||
# prepare spatial bias
|
||||
b_conv = torch.zeros(conv.weight.size(0), device=conv.weight.device) if conv.bias is None else conv.bias
|
||||
b_bn = bn.bias - bn.weight.mul(bn.running_mean).div(torch.sqrt(bn.running_var + bn.eps))
|
||||
fusedconv.bias.copy_(torch.mm(w_bn, b_conv.reshape(-1, 1)).reshape(-1) + b_bn)
|
||||
|
||||
return fusedconv
|
||||
|
||||
|
||||
def model_info(model, verbose=False, img_size=640):
|
||||
# Model information. img_size may be int or list, i.e. img_size=640 or img_size=[640, 320]
|
||||
n_p = sum(x.numel() for x in model.parameters()) # number parameters
|
||||
n_g = sum(x.numel() for x in model.parameters() if x.requires_grad) # number gradients
|
||||
if verbose:
|
||||
print('%5s %40s %9s %12s %20s %10s %10s' % ('layer', 'name', 'gradient', 'parameters', 'shape', 'mu', 'sigma'))
|
||||
for i, (name, p) in enumerate(model.named_parameters()):
|
||||
name = name.replace('module_list.', '')
|
||||
print('%5g %40s %9s %12g %20s %10.3g %10.3g' %
|
||||
(i, name, p.requires_grad, p.numel(), list(p.shape), p.mean(), p.std()))
|
||||
|
||||
try: # FLOPS
|
||||
from thop import profile
|
||||
stride = max(int(model.stride.max()), 32) if hasattr(model, 'stride') else 32
|
||||
img = torch.zeros((1, model.yaml.get('ch', 3), stride, stride), device=next(model.parameters()).device) # input
|
||||
flops = profile(deepcopy(model), inputs=(img,), verbose=False)[0] / 1E9 * 2 # stride GFLOPS
|
||||
img_size = img_size if isinstance(img_size, list) else [img_size, img_size] # expand if int/float
|
||||
fs = ', %.1f GFLOPS' % (flops * img_size[0] / stride * img_size[1] / stride) # 640x640 GFLOPS
|
||||
except (ImportError, Exception):
|
||||
fs = ''
|
||||
|
||||
logger.info(f"Model Summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}")
|
||||
|
||||
|
||||
def load_classifier(name='resnet101', n=2):
|
||||
# Loads a pretrained model reshaped to n-class output
|
||||
model = torchvision.models.__dict__[name](pretrained=True)
|
||||
|
||||
# ResNet model properties
|
||||
# input_size = [3, 224, 224]
|
||||
# input_space = 'RGB'
|
||||
# input_range = [0, 1]
|
||||
# mean = [0.485, 0.456, 0.406]
|
||||
# std = [0.229, 0.224, 0.225]
|
||||
|
||||
# Reshape output to n classes
|
||||
filters = model.fc.weight.shape[1]
|
||||
model.fc.bias = nn.Parameter(torch.zeros(n), requires_grad=True)
|
||||
model.fc.weight = nn.Parameter(torch.zeros(n, filters), requires_grad=True)
|
||||
model.fc.out_features = n
|
||||
return model
|
||||
|
||||
|
||||
def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416)
|
||||
# scales img(bs,3,y,x) by ratio constrained to gs-multiple
|
||||
if ratio == 1.0:
|
||||
return img
|
||||
else:
|
||||
h, w = img.shape[2:]
|
||||
s = (int(h * ratio), int(w * ratio)) # new size
|
||||
img = F.interpolate(img, size=s, mode='bilinear', align_corners=False) # resize
|
||||
if not same_shape: # pad/crop img
|
||||
h, w = [math.ceil(x * ratio / gs) * gs for x in (h, w)]
|
||||
return F.pad(img, [0, w - s[1], 0, h - s[0]], value=0.447) # value = imagenet mean
|
||||
|
||||
|
||||
def copy_attr(a, b, include=(), exclude=()):
|
||||
# Copy attributes from b to a, options to only include [...] and to exclude [...]
|
||||
for k, v in b.__dict__.items():
|
||||
if (len(include) and k not in include) or k.startswith('_') or k in exclude:
|
||||
continue
|
||||
else:
|
||||
setattr(a, k, v)
|
||||
|
||||
|
||||
class ModelEMA:
|
||||
""" Model Exponential Moving Average from https://github.com/rwightman/pytorch-image-models
|
||||
Keep a moving average of everything in the model state_dict (parameters and buffers).
|
||||
This is intended to allow functionality like
|
||||
https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage
|
||||
A smoothed version of the weights is necessary for some training schemes to perform well.
|
||||
This class is sensitive where it is initialized in the sequence of model init,
|
||||
GPU assignment and distributed training wrappers.
|
||||
"""
|
||||
|
||||
def __init__(self, model, decay=0.9999, updates=0):
|
||||
# Create EMA
|
||||
self.ema = deepcopy(model.module if is_parallel(model) else model).eval() # FP32 EMA
|
||||
# if next(model.parameters()).device.type != 'cpu':
|
||||
# self.ema.half() # FP16 EMA
|
||||
self.updates = updates # number of EMA updates
|
||||
self.decay = lambda x: decay * (1 - math.exp(-x / 2000)) # decay exponential ramp (to help early epochs)
|
||||
for p in self.ema.parameters():
|
||||
p.requires_grad_(False)
|
||||
|
||||
def update(self, model):
|
||||
# Update EMA parameters
|
||||
with torch.no_grad():
|
||||
self.updates += 1
|
||||
d = self.decay(self.updates)
|
||||
|
||||
msd = model.module.state_dict() if is_parallel(model) else model.state_dict() # model state_dict
|
||||
for k, v in self.ema.state_dict().items():
|
||||
if v.dtype.is_floating_point:
|
||||
v *= d
|
||||
v += (1. - d) * msd[k].detach()
|
||||
|
||||
def update_attr(self, model, include=(), exclude=('process_group', 'reducer')):
|
||||
# Update EMA attributes
|
||||
copy_attr(self.ema, model, include, exclude)
|
||||
|
||||
|
||||
class BatchNormXd(torch.nn.modules.batchnorm._BatchNorm):
|
||||
def _check_input_dim(self, input):
|
||||
# The only difference between BatchNorm1d, BatchNorm2d, BatchNorm3d, etc
|
||||
# is this method that is overwritten by the sub-class
|
||||
# This original goal of this method was for tensor sanity checks
|
||||
# If you're ok bypassing those sanity checks (eg. if you trust your inference
|
||||
# to provide the right dimensional inputs), then you can just use this method
|
||||
# for easy conversion from SyncBatchNorm
|
||||
# (unfortunately, SyncBatchNorm does not store the original class - if it did
|
||||
# we could return the one that was originally created)
|
||||
return
|
||||
|
||||
def revert_sync_batchnorm(module):
|
||||
# this is very similar to the function that it is trying to revert:
|
||||
# https://github.com/pytorch/pytorch/blob/c8b3686a3e4ba63dc59e5dcfe5db3430df256833/torch/nn/modules/batchnorm.py#L679
|
||||
module_output = module
|
||||
if isinstance(module, torch.nn.modules.batchnorm.SyncBatchNorm):
|
||||
new_cls = BatchNormXd
|
||||
module_output = BatchNormXd(module.num_features,
|
||||
module.eps, module.momentum,
|
||||
module.affine,
|
||||
module.track_running_stats)
|
||||
if module.affine:
|
||||
with torch.no_grad():
|
||||
module_output.weight = module.weight
|
||||
module_output.bias = module.bias
|
||||
module_output.running_mean = module.running_mean
|
||||
module_output.running_var = module.running_var
|
||||
module_output.num_batches_tracked = module.num_batches_tracked
|
||||
if hasattr(module, "qconfig"):
|
||||
module_output.qconfig = module.qconfig
|
||||
for name, child in module.named_children():
|
||||
module_output.add_module(name, revert_sync_batchnorm(child))
|
||||
del module
|
||||
return module_output
|
||||
|
||||
|
||||
class TracedModel(nn.Module):
|
||||
|
||||
def __init__(self, model=None, device=None, img_size=(640,640)):
|
||||
super(TracedModel, self).__init__()
|
||||
|
||||
print(" Convert model to Traced-model... ")
|
||||
self.stride = model.stride
|
||||
self.names = model.names
|
||||
self.model = model
|
||||
|
||||
self.model = revert_sync_batchnorm(self.model)
|
||||
self.model.to('cpu')
|
||||
self.model.eval()
|
||||
|
||||
self.detect_layer = self.model.model[-1]
|
||||
self.model.traced = True
|
||||
|
||||
rand_example = torch.rand(1, 3, img_size, img_size)
|
||||
|
||||
traced_script_module = torch.jit.trace(self.model, rand_example, strict=False)
|
||||
#traced_script_module = torch.jit.script(self.model)
|
||||
traced_script_module.save("traced_model.pt")
|
||||
print(" traced_script_module saved! ")
|
||||
self.model = traced_script_module
|
||||
self.model.to(device)
|
||||
self.detect_layer.to(device)
|
||||
print(" model is traced! \n")
|
||||
|
||||
def forward(self, x, augment=False, profile=False):
|
||||
out = self.model(x)
|
||||
out = self.detect_layer(out)
|
||||
return out
|
||||
|
|
@ -0,0 +1 @@
|
|||
# init
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import argparse
|
||||
|
||||
import yaml
|
||||
|
||||
from wandb_utils import WandbLogger
|
||||
|
||||
WANDB_ARTIFACT_PREFIX = 'wandb-artifact://'
|
||||
|
||||
|
||||
def create_dataset_artifact(opt):
|
||||
with open(opt.data) as f:
|
||||
data = yaml.load(f, Loader=yaml.SafeLoader) # data dict
|
||||
logger = WandbLogger(opt, '', None, data, job_type='Dataset Creation')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--data', type=str, default='data/coco.yaml', help='data.yaml path')
|
||||
parser.add_argument('--single-cls', action='store_true', help='train as single-class dataset')
|
||||
parser.add_argument('--project', type=str, default='YOLOR', help='name of W&B Project')
|
||||
opt = parser.parse_args()
|
||||
opt.resume = False # Explicitly disallow resume check for dataset upload job
|
||||
|
||||
create_dataset_artifact(opt)
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import yaml
|
||||
from tqdm import tqdm
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent)) # add utils/ to path
|
||||
from utils.datasets import LoadImagesAndLabels
|
||||
from utils.datasets import img2label_paths
|
||||
from utils.general import colorstr, xywh2xyxy, check_dataset
|
||||
|
||||
try:
|
||||
import wandb
|
||||
from wandb import init, finish
|
||||
except ImportError:
|
||||
wandb = None
|
||||
|
||||
WANDB_ARTIFACT_PREFIX = 'wandb-artifact://'
|
||||
|
||||
|
||||
def remove_prefix(from_string, prefix=WANDB_ARTIFACT_PREFIX):
|
||||
return from_string[len(prefix):]
|
||||
|
||||
|
||||
def check_wandb_config_file(data_config_file):
|
||||
wandb_config = '_wandb.'.join(data_config_file.rsplit('.', 1)) # updated data.yaml path
|
||||
if Path(wandb_config).is_file():
|
||||
return wandb_config
|
||||
return data_config_file
|
||||
|
||||
|
||||
def get_run_info(run_path):
|
||||
run_path = Path(remove_prefix(run_path, WANDB_ARTIFACT_PREFIX))
|
||||
run_id = run_path.stem
|
||||
project = run_path.parent.stem
|
||||
model_artifact_name = 'run_' + run_id + '_model'
|
||||
return run_id, project, model_artifact_name
|
||||
|
||||
|
||||
def check_wandb_resume(opt):
|
||||
process_wandb_config_ddp_mode(opt) if opt.global_rank not in [-1, 0] else None
|
||||
if isinstance(opt.resume, str):
|
||||
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
|
||||
if opt.global_rank not in [-1, 0]: # For resuming DDP runs
|
||||
run_id, project, model_artifact_name = get_run_info(opt.resume)
|
||||
api = wandb.Api()
|
||||
artifact = api.artifact(project + '/' + model_artifact_name + ':latest')
|
||||
modeldir = artifact.download()
|
||||
opt.weights = str(Path(modeldir) / "last.pt")
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
def process_wandb_config_ddp_mode(opt):
|
||||
with open(opt.data) as f:
|
||||
data_dict = yaml.load(f, Loader=yaml.SafeLoader) # data dict
|
||||
train_dir, val_dir = None, None
|
||||
if isinstance(data_dict['train'], str) and data_dict['train'].startswith(WANDB_ARTIFACT_PREFIX):
|
||||
api = wandb.Api()
|
||||
train_artifact = api.artifact(remove_prefix(data_dict['train']) + ':' + opt.artifact_alias)
|
||||
train_dir = train_artifact.download()
|
||||
train_path = Path(train_dir) / 'data/images/'
|
||||
data_dict['train'] = str(train_path)
|
||||
|
||||
if isinstance(data_dict['val'], str) and data_dict['val'].startswith(WANDB_ARTIFACT_PREFIX):
|
||||
api = wandb.Api()
|
||||
val_artifact = api.artifact(remove_prefix(data_dict['val']) + ':' + opt.artifact_alias)
|
||||
val_dir = val_artifact.download()
|
||||
val_path = Path(val_dir) / 'data/images/'
|
||||
data_dict['val'] = str(val_path)
|
||||
if train_dir or val_dir:
|
||||
ddp_data_path = str(Path(val_dir) / 'wandb_local_data.yaml')
|
||||
with open(ddp_data_path, 'w') as f:
|
||||
yaml.dump(data_dict, f)
|
||||
opt.data = ddp_data_path
|
||||
|
||||
|
||||
class WandbLogger():
|
||||
def __init__(self, opt, name, run_id, data_dict, job_type='Training'):
|
||||
# Pre-training routine --
|
||||
self.job_type = job_type
|
||||
self.wandb, self.wandb_run, self.data_dict = wandb, None if not wandb else wandb.run, data_dict
|
||||
# It's more elegant to stick to 1 wandb.init call, but useful config data is overwritten in the WandbLogger's wandb.init call
|
||||
if isinstance(opt.resume, str): # checks resume from artifact
|
||||
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
|
||||
run_id, project, model_artifact_name = get_run_info(opt.resume)
|
||||
model_artifact_name = WANDB_ARTIFACT_PREFIX + model_artifact_name
|
||||
assert wandb, 'install wandb to resume wandb runs'
|
||||
# Resume wandb-artifact:// runs here| workaround for not overwriting wandb.config
|
||||
self.wandb_run = wandb.init(id=run_id, project=project, resume='allow')
|
||||
opt.resume = model_artifact_name
|
||||
elif self.wandb:
|
||||
self.wandb_run = wandb.init(config=opt,
|
||||
resume="allow",
|
||||
project='YOLOR' if opt.project == 'runs/train' else Path(opt.project).stem,
|
||||
name=name,
|
||||
job_type=job_type,
|
||||
id=run_id) if not wandb.run else wandb.run
|
||||
if self.wandb_run:
|
||||
if self.job_type == 'Training':
|
||||
if not opt.resume:
|
||||
wandb_data_dict = self.check_and_upload_dataset(opt) if opt.upload_dataset else data_dict
|
||||
# Info useful for resuming from artifacts
|
||||
self.wandb_run.config.opt = vars(opt)
|
||||
self.wandb_run.config.data_dict = wandb_data_dict
|
||||
self.data_dict = self.setup_training(opt, data_dict)
|
||||
if self.job_type == 'Dataset Creation':
|
||||
self.data_dict = self.check_and_upload_dataset(opt)
|
||||
else:
|
||||
prefix = colorstr('wandb: ')
|
||||
print(f"{prefix}Install Weights & Biases for YOLOR logging with 'pip install wandb' (recommended)")
|
||||
|
||||
def check_and_upload_dataset(self, opt):
|
||||
assert wandb, 'Install wandb to upload dataset'
|
||||
check_dataset(self.data_dict)
|
||||
config_path = self.log_dataset_artifact(opt.data,
|
||||
opt.single_cls,
|
||||
'YOLOR' if opt.project == 'runs/train' else Path(opt.project).stem)
|
||||
print("Created dataset config file ", config_path)
|
||||
with open(config_path) as f:
|
||||
wandb_data_dict = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
return wandb_data_dict
|
||||
|
||||
def setup_training(self, opt, data_dict):
|
||||
self.log_dict, self.current_epoch, self.log_imgs = {}, 0, 16 # Logging Constants
|
||||
self.bbox_interval = opt.bbox_interval
|
||||
if isinstance(opt.resume, str):
|
||||
modeldir, _ = self.download_model_artifact(opt)
|
||||
if modeldir:
|
||||
self.weights = Path(modeldir) / "last.pt"
|
||||
config = self.wandb_run.config
|
||||
opt.weights, opt.save_period, opt.batch_size, opt.bbox_interval, opt.epochs, opt.hyp = str(
|
||||
self.weights), config.save_period, config.total_batch_size, config.bbox_interval, config.epochs, \
|
||||
config.opt['hyp']
|
||||
data_dict = dict(self.wandb_run.config.data_dict) # eliminates the need for config file to resume
|
||||
if 'val_artifact' not in self.__dict__: # If --upload_dataset is set, use the existing artifact, don't download
|
||||
self.train_artifact_path, self.train_artifact = self.download_dataset_artifact(data_dict.get('train'),
|
||||
opt.artifact_alias)
|
||||
self.val_artifact_path, self.val_artifact = self.download_dataset_artifact(data_dict.get('val'),
|
||||
opt.artifact_alias)
|
||||
self.result_artifact, self.result_table, self.val_table, self.weights = None, None, None, None
|
||||
if self.train_artifact_path is not None:
|
||||
train_path = Path(self.train_artifact_path) / 'data/images/'
|
||||
data_dict['train'] = str(train_path)
|
||||
if self.val_artifact_path is not None:
|
||||
val_path = Path(self.val_artifact_path) / 'data/images/'
|
||||
data_dict['val'] = str(val_path)
|
||||
self.val_table = self.val_artifact.get("val")
|
||||
self.map_val_table_path()
|
||||
if self.val_artifact is not None:
|
||||
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
|
||||
self.result_table = wandb.Table(["epoch", "id", "prediction", "avg_confidence"])
|
||||
if opt.bbox_interval == -1:
|
||||
self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1
|
||||
return data_dict
|
||||
|
||||
def download_dataset_artifact(self, path, alias):
|
||||
if isinstance(path, str) and path.startswith(WANDB_ARTIFACT_PREFIX):
|
||||
dataset_artifact = wandb.use_artifact(remove_prefix(path, WANDB_ARTIFACT_PREFIX) + ":" + alias)
|
||||
assert dataset_artifact is not None, "'Error: W&B dataset artifact doesn\'t exist'"
|
||||
datadir = dataset_artifact.download()
|
||||
return datadir, dataset_artifact
|
||||
return None, None
|
||||
|
||||
def download_model_artifact(self, opt):
|
||||
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
|
||||
model_artifact = wandb.use_artifact(remove_prefix(opt.resume, WANDB_ARTIFACT_PREFIX) + ":latest")
|
||||
assert model_artifact is not None, 'Error: W&B model artifact doesn\'t exist'
|
||||
modeldir = model_artifact.download()
|
||||
epochs_trained = model_artifact.metadata.get('epochs_trained')
|
||||
total_epochs = model_artifact.metadata.get('total_epochs')
|
||||
assert epochs_trained < total_epochs, 'training to %g epochs is finished, nothing to resume.' % (
|
||||
total_epochs)
|
||||
return modeldir, model_artifact
|
||||
return None, None
|
||||
|
||||
def log_model(self, path, opt, epoch, fitness_score, best_model=False):
|
||||
model_artifact = wandb.Artifact('run_' + wandb.run.id + '_model', type='model', metadata={
|
||||
'original_url': str(path),
|
||||
'epochs_trained': epoch + 1,
|
||||
'save period': opt.save_period,
|
||||
'project': opt.project,
|
||||
'total_epochs': opt.epochs,
|
||||
'fitness_score': fitness_score
|
||||
})
|
||||
model_artifact.add_file(str(path / 'last.pt'), name='last.pt')
|
||||
wandb.log_artifact(model_artifact,
|
||||
aliases=['latest', 'epoch ' + str(self.current_epoch), 'best' if best_model else ''])
|
||||
print("Saving model artifact on epoch ", epoch + 1)
|
||||
|
||||
def log_dataset_artifact(self, data_file, single_cls, project, overwrite_config=False):
|
||||
with open(data_file) as f:
|
||||
data = yaml.load(f, Loader=yaml.SafeLoader) # data dict
|
||||
nc, names = (1, ['item']) if single_cls else (int(data['nc']), data['names'])
|
||||
names = {k: v for k, v in enumerate(names)} # to index dictionary
|
||||
self.train_artifact = self.create_dataset_table(LoadImagesAndLabels(
|
||||
data['train']), names, name='train') if data.get('train') else None
|
||||
self.val_artifact = self.create_dataset_table(LoadImagesAndLabels(
|
||||
data['val']), names, name='val') if data.get('val') else None
|
||||
if data.get('train'):
|
||||
data['train'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'train')
|
||||
if data.get('val'):
|
||||
data['val'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'val')
|
||||
path = data_file if overwrite_config else '_wandb.'.join(data_file.rsplit('.', 1)) # updated data.yaml path
|
||||
data.pop('download', None)
|
||||
with open(path, 'w') as f:
|
||||
yaml.dump(data, f)
|
||||
|
||||
if self.job_type == 'Training': # builds correct artifact pipeline graph
|
||||
self.wandb_run.use_artifact(self.val_artifact)
|
||||
self.wandb_run.use_artifact(self.train_artifact)
|
||||
self.val_artifact.wait()
|
||||
self.val_table = self.val_artifact.get('val')
|
||||
self.map_val_table_path()
|
||||
else:
|
||||
self.wandb_run.log_artifact(self.train_artifact)
|
||||
self.wandb_run.log_artifact(self.val_artifact)
|
||||
return path
|
||||
|
||||
def map_val_table_path(self):
|
||||
self.val_table_map = {}
|
||||
print("Mapping dataset")
|
||||
for i, data in enumerate(tqdm(self.val_table.data)):
|
||||
self.val_table_map[data[3]] = data[0]
|
||||
|
||||
def create_dataset_table(self, dataset, class_to_id, name='dataset'):
|
||||
# TODO: Explore multiprocessing to slpit this loop parallely| This is essential for speeding up the the logging
|
||||
artifact = wandb.Artifact(name=name, type="dataset")
|
||||
img_files = tqdm([dataset.path]) if isinstance(dataset.path, str) and Path(dataset.path).is_dir() else None
|
||||
img_files = tqdm(dataset.img_files) if not img_files else img_files
|
||||
for img_file in img_files:
|
||||
if Path(img_file).is_dir():
|
||||
artifact.add_dir(img_file, name='data/images')
|
||||
labels_path = 'labels'.join(dataset.path.rsplit('images', 1))
|
||||
artifact.add_dir(labels_path, name='data/labels')
|
||||
else:
|
||||
artifact.add_file(img_file, name='data/images/' + Path(img_file).name)
|
||||
label_file = Path(img2label_paths([img_file])[0])
|
||||
artifact.add_file(str(label_file),
|
||||
name='data/labels/' + label_file.name) if label_file.exists() else None
|
||||
table = wandb.Table(columns=["id", "train_image", "Classes", "name"])
|
||||
class_set = wandb.Classes([{'id': id, 'name': name} for id, name in class_to_id.items()])
|
||||
for si, (img, labels, paths, shapes) in enumerate(tqdm(dataset)):
|
||||
height, width = shapes[0]
|
||||
labels[:, 2:] = (xywh2xyxy(labels[:, 2:].view(-1, 4))) * torch.Tensor([width, height, width, height])
|
||||
box_data, img_classes = [], {}
|
||||
for cls, *xyxy in labels[:, 1:].tolist():
|
||||
cls = int(cls)
|
||||
box_data.append({"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]},
|
||||
"class_id": cls,
|
||||
"box_caption": "%s" % (class_to_id[cls]),
|
||||
"scores": {"acc": 1},
|
||||
"domain": "pixel"})
|
||||
img_classes[cls] = class_to_id[cls]
|
||||
boxes = {"ground_truth": {"box_data": box_data, "class_labels": class_to_id}} # inference-space
|
||||
table.add_data(si, wandb.Image(paths, classes=class_set, boxes=boxes), json.dumps(img_classes),
|
||||
Path(paths).name)
|
||||
artifact.add(table, name)
|
||||
return artifact
|
||||
|
||||
def log_training_progress(self, predn, path, names):
|
||||
if self.val_table and self.result_table:
|
||||
class_set = wandb.Classes([{'id': id, 'name': name} for id, name in names.items()])
|
||||
box_data = []
|
||||
total_conf = 0
|
||||
for *xyxy, conf, cls in predn.tolist():
|
||||
if conf >= 0.25:
|
||||
box_data.append(
|
||||
{"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]},
|
||||
"class_id": int(cls),
|
||||
"box_caption": "%s %.3f" % (names[cls], conf),
|
||||
"scores": {"class_score": conf},
|
||||
"domain": "pixel"})
|
||||
total_conf = total_conf + conf
|
||||
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
|
||||
id = self.val_table_map[Path(path).name]
|
||||
self.result_table.add_data(self.current_epoch,
|
||||
id,
|
||||
wandb.Image(self.val_table.data[id][1], boxes=boxes, classes=class_set),
|
||||
total_conf / max(1, len(box_data))
|
||||
)
|
||||
|
||||
def log(self, log_dict):
|
||||
if self.wandb_run:
|
||||
for key, value in log_dict.items():
|
||||
self.log_dict[key] = value
|
||||
|
||||
def end_epoch(self, best_result=False):
|
||||
if self.wandb_run:
|
||||
wandb.log(self.log_dict)
|
||||
self.log_dict = {}
|
||||
if self.result_artifact:
|
||||
train_results = wandb.JoinedTable(self.val_table, self.result_table, "id")
|
||||
self.result_artifact.add(train_results, 'result')
|
||||
wandb.log_artifact(self.result_artifact, aliases=['latest', 'epoch ' + str(self.current_epoch),
|
||||
('best' if best_result else '')])
|
||||
self.result_table = wandb.Table(["epoch", "id", "prediction", "avg_confidence"])
|
||||
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
|
||||
|
||||
def finish_run(self):
|
||||
if self.wandb_run:
|
||||
if self.log_dict:
|
||||
wandb.log(self.log_dict)
|
||||
wandb.run.finish()
|
||||
|
|
@ -0,0 +1 @@
|
|||
# init
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# Activation functions
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
# SiLU https://arxiv.org/pdf/1606.08415.pdf ----------------------------------------------------------------------------
|
||||
class SiLU(nn.Module): # export-friendly version of nn.SiLU()
|
||||
@staticmethod
|
||||
def forward(x):
|
||||
return x * torch.sigmoid(x)
|
||||
|
||||
|
||||
class Hardswish(nn.Module): # export-friendly version of nn.Hardswish()
|
||||
@staticmethod
|
||||
def forward(x):
|
||||
# return x * F.hardsigmoid(x) # for torchscript and CoreML
|
||||
return x * F.hardtanh(x + 3, 0., 6.) / 6. # for torchscript, CoreML and ONNX
|
||||
|
||||
|
||||
class MemoryEfficientSwish(nn.Module):
|
||||
class F(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(ctx, x):
|
||||
ctx.save_for_backward(x)
|
||||
return x * torch.sigmoid(x)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
x = ctx.saved_tensors[0]
|
||||
sx = torch.sigmoid(x)
|
||||
return grad_output * (sx * (1 + x * (1 - sx)))
|
||||
|
||||
def forward(self, x):
|
||||
return self.F.apply(x)
|
||||
|
||||
|
||||
# Mish https://github.com/digantamisra98/Mish --------------------------------------------------------------------------
|
||||
class Mish(nn.Module):
|
||||
@staticmethod
|
||||
def forward(x):
|
||||
return x * F.softplus(x).tanh()
|
||||
|
||||
|
||||
class MemoryEfficientMish(nn.Module):
|
||||
class F(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(ctx, x):
|
||||
ctx.save_for_backward(x)
|
||||
return x.mul(torch.tanh(F.softplus(x))) # x * tanh(ln(1 + exp(x)))
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
x = ctx.saved_tensors[0]
|
||||
sx = torch.sigmoid(x)
|
||||
fx = F.softplus(x).tanh()
|
||||
return grad_output * (fx + x * sx * (1 - fx * fx))
|
||||
|
||||
def forward(self, x):
|
||||
return self.F.apply(x)
|
||||
|
||||
|
||||
# FReLU https://arxiv.org/abs/2007.11824 -------------------------------------------------------------------------------
|
||||
class FReLU(nn.Module):
|
||||
def __init__(self, c1, k=3): # ch_in, kernel
|
||||
super().__init__()
|
||||
self.conv = nn.Conv2d(c1, c1, k, 1, 1, groups=c1, bias=False)
|
||||
self.bn = nn.BatchNorm2d(c1)
|
||||
|
||||
def forward(self, x):
|
||||
return torch.max(x, self.bn(self.conv(x)))
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import numpy as np
|
||||
import onnx
|
||||
from onnx import shape_inference
|
||||
try:
|
||||
import onnx_graphsurgeon as gs
|
||||
except Exception as e:
|
||||
print('Import onnx_graphsurgeon failure: %s' % e)
|
||||
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class RegisterNMS(object):
|
||||
def __init__(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
precision: str = "fp32",
|
||||
):
|
||||
|
||||
self.graph = gs.import_onnx(onnx.load(onnx_model_path))
|
||||
assert self.graph
|
||||
LOGGER.info("ONNX graph created successfully")
|
||||
# Fold constants via ONNX-GS that PyTorch2ONNX may have missed
|
||||
self.graph.fold_constants()
|
||||
self.precision = precision
|
||||
self.batch_size = 1
|
||||
def infer(self):
|
||||
"""
|
||||
Sanitize the graph by cleaning any unconnected nodes, do a topological resort,
|
||||
and fold constant inputs values. When possible, run shape inference on the
|
||||
ONNX graph to determine tensor shapes.
|
||||
"""
|
||||
for _ in range(3):
|
||||
count_before = len(self.graph.nodes)
|
||||
|
||||
self.graph.cleanup().toposort()
|
||||
try:
|
||||
for node in self.graph.nodes:
|
||||
for o in node.outputs:
|
||||
o.shape = None
|
||||
model = gs.export_onnx(self.graph)
|
||||
model = shape_inference.infer_shapes(model)
|
||||
self.graph = gs.import_onnx(model)
|
||||
except Exception as e:
|
||||
LOGGER.info(f"Shape inference could not be performed at this time:\n{e}")
|
||||
try:
|
||||
self.graph.fold_constants(fold_shapes=True)
|
||||
except TypeError as e:
|
||||
LOGGER.error(
|
||||
"This version of ONNX GraphSurgeon does not support folding shapes, "
|
||||
f"please upgrade your onnx_graphsurgeon module. Error:\n{e}"
|
||||
)
|
||||
raise
|
||||
|
||||
count_after = len(self.graph.nodes)
|
||||
if count_before == count_after:
|
||||
# No new folding occurred in this iteration, so we can stop for now.
|
||||
break
|
||||
|
||||
def save(self, output_path):
|
||||
"""
|
||||
Save the ONNX model to the given location.
|
||||
Args:
|
||||
output_path: Path pointing to the location where to write
|
||||
out the updated ONNX model.
|
||||
"""
|
||||
self.graph.cleanup().toposort()
|
||||
model = gs.export_onnx(self.graph)
|
||||
onnx.save(model, output_path)
|
||||
LOGGER.info(f"Saved ONNX model to {output_path}")
|
||||
|
||||
def register_nms(
|
||||
self,
|
||||
*,
|
||||
score_thresh: float = 0.25,
|
||||
nms_thresh: float = 0.45,
|
||||
detections_per_img: int = 100,
|
||||
):
|
||||
"""
|
||||
Register the ``EfficientNMS_TRT`` plugin node.
|
||||
NMS expects these shapes for its input tensors:
|
||||
- box_net: [batch_size, number_boxes, 4]
|
||||
- class_net: [batch_size, number_boxes, number_labels]
|
||||
Args:
|
||||
score_thresh (float): The scalar threshold for score (low scoring boxes are removed).
|
||||
nms_thresh (float): The scalar threshold for IOU (new boxes that have high IOU
|
||||
overlap with previously selected boxes are removed).
|
||||
detections_per_img (int): Number of best detections to keep after NMS.
|
||||
"""
|
||||
|
||||
self.infer()
|
||||
# Find the concat node at the end of the network
|
||||
op_inputs = self.graph.outputs
|
||||
op = "EfficientNMS_TRT"
|
||||
attrs = {
|
||||
"plugin_version": "1",
|
||||
"background_class": -1, # no background class
|
||||
"max_output_boxes": detections_per_img,
|
||||
"score_threshold": score_thresh,
|
||||
"iou_threshold": nms_thresh,
|
||||
"score_activation": False,
|
||||
"box_coding": 0,
|
||||
}
|
||||
|
||||
if self.precision == "fp32":
|
||||
dtype_output = np.float32
|
||||
elif self.precision == "fp16":
|
||||
dtype_output = np.float16
|
||||
else:
|
||||
raise NotImplementedError(f"Currently not supports precision: {self.precision}")
|
||||
|
||||
# NMS Outputs
|
||||
output_num_detections = gs.Variable(
|
||||
name="num_dets",
|
||||
dtype=np.int32,
|
||||
shape=[self.batch_size, 1],
|
||||
) # A scalar indicating the number of valid detections per batch image.
|
||||
output_boxes = gs.Variable(
|
||||
name="det_boxes",
|
||||
dtype=dtype_output,
|
||||
shape=[self.batch_size, detections_per_img, 4],
|
||||
)
|
||||
output_scores = gs.Variable(
|
||||
name="det_scores",
|
||||
dtype=dtype_output,
|
||||
shape=[self.batch_size, detections_per_img],
|
||||
)
|
||||
output_labels = gs.Variable(
|
||||
name="det_classes",
|
||||
dtype=np.int32,
|
||||
shape=[self.batch_size, detections_per_img],
|
||||
)
|
||||
|
||||
op_outputs = [output_num_detections, output_boxes, output_scores, output_labels]
|
||||
|
||||
# Create the NMS Plugin node with the selected inputs. The outputs of the node will also
|
||||
# become the final outputs of the graph.
|
||||
self.graph.layer(op=op, name="batched_nms", inputs=op_inputs, outputs=op_outputs, attrs=attrs)
|
||||
LOGGER.info(f"Created NMS plugin '{op}' with attributes: {attrs}")
|
||||
|
||||
self.graph.outputs = op_outputs
|
||||
|
||||
self.infer()
|
||||
|
||||
def save(self, output_path):
|
||||
"""
|
||||
Save the ONNX model to the given location.
|
||||
Args:
|
||||
output_path: Path pointing to the location where to write
|
||||
out the updated ONNX model.
|
||||
"""
|
||||
self.graph.cleanup().toposort()
|
||||
model = gs.export_onnx(self.graph)
|
||||
onnx.save(model, output_path)
|
||||
LOGGER.info(f"Saved ONNX model to {output_path}")
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
# Auto-anchor utils
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import yaml
|
||||
from scipy.cluster.vq import kmeans
|
||||
from tqdm import tqdm
|
||||
|
||||
from utils.general import colorstr
|
||||
|
||||
|
||||
def check_anchor_order(m):
|
||||
# Check anchor order against stride order for YOLO Detect() module m, and correct if necessary
|
||||
a = m.anchor_grid.prod(-1).view(-1) # anchor area
|
||||
da = a[-1] - a[0] # delta a
|
||||
ds = m.stride[-1] - m.stride[0] # delta s
|
||||
if da.sign() != ds.sign(): # same order
|
||||
print('Reversing anchor order')
|
||||
m.anchors[:] = m.anchors.flip(0)
|
||||
m.anchor_grid[:] = m.anchor_grid.flip(0)
|
||||
|
||||
|
||||
def check_anchors(dataset, model, thr=4.0, imgsz=640):
|
||||
# Check anchor fit to data, recompute if necessary
|
||||
prefix = colorstr('autoanchor: ')
|
||||
print(f'\n{prefix}Analyzing anchors... ', end='')
|
||||
m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect()
|
||||
shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
||||
scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale
|
||||
wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float() # wh
|
||||
|
||||
def metric(k): # compute metric
|
||||
r = wh[:, None] / k[None]
|
||||
x = torch.min(r, 1. / r).min(2)[0] # ratio metric
|
||||
best = x.max(1)[0] # best_x
|
||||
aat = (x > 1. / thr).float().sum(1).mean() # anchors above threshold
|
||||
bpr = (best > 1. / thr).float().mean() # best possible recall
|
||||
return bpr, aat
|
||||
|
||||
anchors = m.anchor_grid.clone().cpu().view(-1, 2) # current anchors
|
||||
bpr, aat = metric(anchors)
|
||||
print(f'anchors/target = {aat:.2f}, Best Possible Recall (BPR) = {bpr:.4f}', end='')
|
||||
if bpr < 0.98: # threshold to recompute
|
||||
print('. Attempting to improve anchors, please wait...')
|
||||
na = m.anchor_grid.numel() // 2 # number of anchors
|
||||
try:
|
||||
anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
|
||||
except Exception as e:
|
||||
print(f'{prefix}ERROR: {e}')
|
||||
new_bpr = metric(anchors)[0]
|
||||
if new_bpr > bpr: # replace anchors
|
||||
anchors = torch.tensor(anchors, device=m.anchors.device).type_as(m.anchors)
|
||||
m.anchor_grid[:] = anchors.clone().view_as(m.anchor_grid) # for inference
|
||||
check_anchor_order(m)
|
||||
m.anchors[:] = anchors.clone().view_as(m.anchors) / m.stride.to(m.anchors.device).view(-1, 1, 1) # loss
|
||||
print(f'{prefix}New anchors saved to model. Update model *.yaml to use these anchors in the future.')
|
||||
else:
|
||||
print(f'{prefix}Original anchors better than new anchors. Proceeding with original anchors.')
|
||||
print('') # newline
|
||||
|
||||
|
||||
def kmean_anchors(path='./data/coco.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
|
||||
""" Creates kmeans-evolved anchors from training dataset
|
||||
|
||||
Arguments:
|
||||
path: path to dataset *.yaml, or a loaded dataset
|
||||
n: number of anchors
|
||||
img_size: image size used for training
|
||||
thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0
|
||||
gen: generations to evolve anchors using genetic algorithm
|
||||
verbose: print all results
|
||||
|
||||
Return:
|
||||
k: kmeans evolved anchors
|
||||
|
||||
Usage:
|
||||
from utils.autoanchor import *; _ = kmean_anchors()
|
||||
"""
|
||||
thr = 1. / thr
|
||||
prefix = colorstr('autoanchor: ')
|
||||
|
||||
def metric(k, wh): # compute metrics
|
||||
r = wh[:, None] / k[None]
|
||||
x = torch.min(r, 1. / r).min(2)[0] # ratio metric
|
||||
# x = wh_iou(wh, torch.tensor(k)) # iou metric
|
||||
return x, x.max(1)[0] # x, best_x
|
||||
|
||||
def anchor_fitness(k): # mutation fitness
|
||||
_, best = metric(torch.tensor(k, dtype=torch.float32), wh)
|
||||
return (best * (best > thr).float()).mean() # fitness
|
||||
|
||||
def print_results(k):
|
||||
k = k[np.argsort(k.prod(1))] # sort small to large
|
||||
x, best = metric(k, wh0)
|
||||
bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr
|
||||
print(f'{prefix}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr')
|
||||
print(f'{prefix}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, '
|
||||
f'past_thr={x[x > thr].mean():.3f}-mean: ', end='')
|
||||
for i, x in enumerate(k):
|
||||
print('%i,%i' % (round(x[0]), round(x[1])), end=', ' if i < len(k) - 1 else '\n') # use in *.cfg
|
||||
return k
|
||||
|
||||
if isinstance(path, str): # *.yaml file
|
||||
with open(path) as f:
|
||||
data_dict = yaml.load(f, Loader=yaml.SafeLoader) # model dict
|
||||
from utils.datasets import LoadImagesAndLabels
|
||||
dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
|
||||
else:
|
||||
dataset = path # dataset
|
||||
|
||||
# Get label wh
|
||||
shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
||||
wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)]) # wh
|
||||
|
||||
# Filter
|
||||
i = (wh0 < 3.0).any(1).sum()
|
||||
if i:
|
||||
print(f'{prefix}WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.')
|
||||
wh = wh0[(wh0 >= 2.0).any(1)] # filter > 2 pixels
|
||||
# wh = wh * (np.random.rand(wh.shape[0], 1) * 0.9 + 0.1) # multiply by random scale 0-1
|
||||
|
||||
# Kmeans calculation
|
||||
print(f'{prefix}Running kmeans for {n} anchors on {len(wh)} points...')
|
||||
s = wh.std(0) # sigmas for whitening
|
||||
k, dist = kmeans(wh / s, n, iter=30) # points, mean distance
|
||||
assert len(k) == n, print(f'{prefix}ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}')
|
||||
k *= s
|
||||
wh = torch.tensor(wh, dtype=torch.float32) # filtered
|
||||
wh0 = torch.tensor(wh0, dtype=torch.float32) # unfiltered
|
||||
k = print_results(k)
|
||||
|
||||
# Plot
|
||||
# k, d = [None] * 20, [None] * 20
|
||||
# for i in tqdm(range(1, 21)):
|
||||
# k[i-1], d[i-1] = kmeans(wh / s, i) # points, mean distance
|
||||
# fig, ax = plt.subplots(1, 2, figsize=(14, 7), tight_layout=True)
|
||||
# ax = ax.ravel()
|
||||
# ax[0].plot(np.arange(1, 21), np.array(d) ** 2, marker='.')
|
||||
# fig, ax = plt.subplots(1, 2, figsize=(14, 7)) # plot wh
|
||||
# ax[0].hist(wh[wh[:, 0]<100, 0],400)
|
||||
# ax[1].hist(wh[wh[:, 1]<100, 1],400)
|
||||
# fig.savefig('wh.png', dpi=200)
|
||||
|
||||
# Evolve
|
||||
npr = np.random
|
||||
f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
|
||||
pbar = tqdm(range(gen), desc=f'{prefix}Evolving anchors with Genetic Algorithm:') # progress bar
|
||||
for _ in pbar:
|
||||
v = np.ones(sh)
|
||||
while (v == 1).all(): # mutate until a change occurs (prevent duplicates)
|
||||
v = ((npr.random(sh) < mp) * npr.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
|
||||
kg = (k.copy() * v).clip(min=2.0)
|
||||
fg = anchor_fitness(kg)
|
||||
if fg > f:
|
||||
f, k = fg, kg.copy()
|
||||
pbar.desc = f'{prefix}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'
|
||||
if verbose:
|
||||
print_results(k)
|
||||
|
||||
return print_results(k)
|
||||
|
|
@ -0,0 +1 @@
|
|||
#init
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# AWS EC2 instance startup 'MIME' script https://aws.amazon.com/premiumsupport/knowledge-center/execute-user-data-ec2/
|
||||
# This script will run on every instance restart, not only on first start
|
||||
# --- DO NOT COPY ABOVE COMMENTS WHEN PASTING INTO USERDATA ---
|
||||
|
||||
Content-Type: multipart/mixed; boundary="//"
|
||||
MIME-Version: 1.0
|
||||
|
||||
--//
|
||||
Content-Type: text/cloud-config; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: attachment; filename="cloud-config.txt"
|
||||
|
||||
#cloud-config
|
||||
cloud_final_modules:
|
||||
- [scripts-user, always]
|
||||
|
||||
--//
|
||||
Content-Type: text/x-shellscript; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: attachment; filename="userdata.txt"
|
||||
|
||||
#!/bin/bash
|
||||
# --- paste contents of userdata.sh here ---
|
||||
--//
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Resume all interrupted trainings in yolor/ dir including DDP trainings
|
||||
# Usage: $ python utils/aws/resume.py
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
sys.path.append('./') # to run '$ python *.py' files in subdirectories
|
||||
|
||||
port = 0 # --master_port
|
||||
path = Path('').resolve()
|
||||
for last in path.rglob('*/**/last.pt'):
|
||||
ckpt = torch.load(last)
|
||||
if ckpt['optimizer'] is None:
|
||||
continue
|
||||
|
||||
# Load opt.yaml
|
||||
with open(last.parent.parent / 'opt.yaml') as f:
|
||||
opt = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
|
||||
# Get device count
|
||||
d = opt['device'].split(',') # devices
|
||||
nd = len(d) # number of devices
|
||||
ddp = nd > 1 or (nd == 0 and torch.cuda.device_count() > 1) # distributed data parallel
|
||||
|
||||
if ddp: # multi-GPU
|
||||
port += 1
|
||||
cmd = f'python -m torch.distributed.launch --nproc_per_node {nd} --master_port {port} train.py --resume {last}'
|
||||
else: # single-GPU
|
||||
cmd = f'python train.py --resume {last}'
|
||||
|
||||
cmd += ' > /dev/null 2>&1 &' # redirect output to dev/null and run in daemon thread
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
# AWS EC2 instance startup script https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
|
||||
# This script will run only once on first instance start (for a re-start script see mime.sh)
|
||||
# /home/ubuntu (ubuntu) or /home/ec2-user (amazon-linux) is working dir
|
||||
# Use >300 GB SSD
|
||||
|
||||
cd home/ubuntu
|
||||
if [ ! -d yolor ]; then
|
||||
echo "Running first-time script." # install dependencies, download COCO, pull Docker
|
||||
git clone -b main https://github.com/WongKinYiu/yolov7 && sudo chmod -R 777 yolov7
|
||||
cd yolov7
|
||||
bash data/scripts/get_coco.sh && echo "Data done." &
|
||||
sudo docker pull nvcr.io/nvidia/pytorch:21.08-py3 && echo "Docker done." &
|
||||
python -m pip install --upgrade pip && pip install -r requirements.txt && python detect.py && echo "Requirements done." &
|
||||
wait && echo "All tasks done." # finish background tasks
|
||||
else
|
||||
echo "Running re-start script." # resume interrupted runs
|
||||
i=0
|
||||
list=$(sudo docker ps -qa) # container list i.e. $'one\ntwo\nthree\nfour'
|
||||
while IFS= read -r id; do
|
||||
((i++))
|
||||
echo "restarting container $i: $id"
|
||||
sudo docker start $id
|
||||
# sudo docker exec -it $id python train.py --resume # single-GPU
|
||||
sudo docker exec -d $id python utils/aws/resume.py # multi-scenario
|
||||
done <<<"$list"
|
||||
fi
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,892 @@
|
|||
# YOLOR general utils
|
||||
|
||||
import glob
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import torchvision
|
||||
import yaml
|
||||
|
||||
from utils.google_utils import gsutil_getsize
|
||||
from utils.metrics import fitness
|
||||
from utils.torch_utils import init_torch_seeds
|
||||
|
||||
# Settings
|
||||
torch.set_printoptions(linewidth=320, precision=5, profile='long')
|
||||
np.set_printoptions(linewidth=320, formatter={'float_kind': '{:11.5g}'.format}) # format short g, %precision=5
|
||||
pd.options.display.max_columns = 10
|
||||
cv2.setNumThreads(0) # prevent OpenCV from multithreading (incompatible with PyTorch DataLoader)
|
||||
os.environ['NUMEXPR_MAX_THREADS'] = str(min(os.cpu_count(), 8)) # NumExpr max threads
|
||||
|
||||
|
||||
def set_logging(rank=-1):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
level=logging.INFO if rank in [-1, 0] else logging.WARN)
|
||||
|
||||
|
||||
def init_seeds(seed=0):
|
||||
# Initialize random number generator (RNG) seeds
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
init_torch_seeds(seed)
|
||||
|
||||
|
||||
def get_latest_run(search_dir='.'):
|
||||
# Return path to most recent 'last.pt' in /runs (i.e. to --resume from)
|
||||
last_list = glob.glob(f'{search_dir}/**/last*.pt', recursive=True)
|
||||
return max(last_list, key=os.path.getctime) if last_list else ''
|
||||
|
||||
|
||||
def isdocker():
|
||||
# Is environment a Docker container
|
||||
return Path('/workspace').exists() # or Path('/.dockerenv').exists()
|
||||
|
||||
|
||||
def emojis(str=''):
|
||||
# Return platform-dependent emoji-safe version of string
|
||||
return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str
|
||||
|
||||
|
||||
def check_online():
|
||||
# Check internet connectivity
|
||||
import socket
|
||||
try:
|
||||
socket.create_connection(("1.1.1.1", 443), 5) # check host accesability
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def check_git_status():
|
||||
# Recommend 'git pull' if code is out of date
|
||||
print(colorstr('github: '), end='')
|
||||
try:
|
||||
assert Path('.git').exists(), 'skipping check (not a git repository)'
|
||||
assert not isdocker(), 'skipping check (Docker image)'
|
||||
assert check_online(), 'skipping check (offline)'
|
||||
|
||||
cmd = 'git fetch && git config --get remote.origin.url'
|
||||
url = subprocess.check_output(cmd, shell=True).decode().strip().rstrip('.git') # github repo url
|
||||
branch = subprocess.check_output('git rev-parse --abbrev-ref HEAD', shell=True).decode().strip() # checked out
|
||||
n = int(subprocess.check_output(f'git rev-list {branch}..origin/master --count', shell=True)) # commits behind
|
||||
if n > 0:
|
||||
s = f"⚠️ WARNING: code is out of date by {n} commit{'s' * (n > 1)}. " \
|
||||
f"Use 'git pull' to update or 'git clone {url}' to download latest."
|
||||
else:
|
||||
s = f'up to date with {url} ✅'
|
||||
print(emojis(s)) # emoji-safe
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
def check_requirements(requirements='requirements.txt', exclude=()):
|
||||
# Check installed dependencies meet requirements (pass *.txt file or list of packages)
|
||||
import pkg_resources as pkg
|
||||
prefix = colorstr('red', 'bold', 'requirements:')
|
||||
if isinstance(requirements, (str, Path)): # requirements.txt file
|
||||
file = Path(requirements)
|
||||
if not file.exists():
|
||||
print(f"{prefix} {file.resolve()} not found, check failed.")
|
||||
return
|
||||
requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(file.open()) if x.name not in exclude]
|
||||
else: # list or tuple of packages
|
||||
requirements = [x for x in requirements if x not in exclude]
|
||||
|
||||
n = 0 # number of packages updates
|
||||
for r in requirements:
|
||||
try:
|
||||
pkg.require(r)
|
||||
except Exception as e: # DistributionNotFound or VersionConflict if requirements not met
|
||||
n += 1
|
||||
print(f"{prefix} {e.req} not found and is required by YOLOR, attempting auto-update...")
|
||||
print(subprocess.check_output(f"pip install '{e.req}'", shell=True).decode())
|
||||
|
||||
if n: # if packages updated
|
||||
source = file.resolve() if 'file' in locals() else requirements
|
||||
s = f"{prefix} {n} package{'s' * (n > 1)} updated per {source}\n" \
|
||||
f"{prefix} ⚠️ {colorstr('bold', 'Restart runtime or rerun command for updates to take effect')}\n"
|
||||
print(emojis(s)) # emoji-safe
|
||||
|
||||
|
||||
def check_img_size(img_size, s=32):
|
||||
# Verify img_size is a multiple of stride s
|
||||
new_size = make_divisible(img_size, int(s)) # ceil gs-multiple
|
||||
if new_size != img_size:
|
||||
print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size))
|
||||
return new_size
|
||||
|
||||
|
||||
def check_imshow():
|
||||
# Check if environment supports image displays
|
||||
try:
|
||||
assert not isdocker(), 'cv2.imshow() is disabled in Docker environments'
|
||||
cv2.imshow('test', np.zeros((1, 1, 3)))
|
||||
cv2.waitKey(1)
|
||||
cv2.destroyAllWindows()
|
||||
cv2.waitKey(1)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f'WARNING: Environment does not support cv2.imshow() or PIL Image.show() image displays\n{e}')
|
||||
return False
|
||||
|
||||
|
||||
def check_file(file):
|
||||
# Search for file if not found
|
||||
if Path(file).is_file() or file == '':
|
||||
return file
|
||||
else:
|
||||
files = glob.glob('./**/' + file, recursive=True) # find file
|
||||
assert len(files), f'File Not Found: {file}' # assert file was found
|
||||
assert len(files) == 1, f"Multiple files match '{file}', specify exact path: {files}" # assert unique
|
||||
return files[0] # return file
|
||||
|
||||
|
||||
def check_dataset(dict):
|
||||
# Download dataset if not found locally
|
||||
val, s = dict.get('val'), dict.get('download')
|
||||
if val and len(val):
|
||||
val = [Path(x).resolve() for x in (val if isinstance(val, list) else [val])] # val path
|
||||
if not all(x.exists() for x in val):
|
||||
print('\nWARNING: Dataset not found, nonexistent paths: %s' % [str(x) for x in val if not x.exists()])
|
||||
if s and len(s): # download script
|
||||
print('Downloading %s ...' % s)
|
||||
if s.startswith('http') and s.endswith('.zip'): # URL
|
||||
f = Path(s).name # filename
|
||||
torch.hub.download_url_to_file(s, f)
|
||||
r = os.system('unzip -q %s -d ../ && rm %s' % (f, f)) # unzip
|
||||
else: # bash script
|
||||
r = os.system(s)
|
||||
print('Dataset autodownload %s\n' % ('success' if r == 0 else 'failure')) # analyze return value
|
||||
else:
|
||||
raise Exception('Dataset not found.')
|
||||
|
||||
|
||||
def make_divisible(x, divisor):
|
||||
# Returns x evenly divisible by divisor
|
||||
return math.ceil(x / divisor) * divisor
|
||||
|
||||
|
||||
def clean_str(s):
|
||||
# Cleans a string by replacing special characters with underscore _
|
||||
return re.sub(pattern="[|@#!¡·$€%&()=?¿^*;:,¨´><+]", repl="_", string=s)
|
||||
|
||||
|
||||
def one_cycle(y1=0.0, y2=1.0, steps=100):
|
||||
# lambda function for sinusoidal ramp from y1 to y2
|
||||
return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1
|
||||
|
||||
|
||||
def colorstr(*input):
|
||||
# Colors a string https://en.wikipedia.org/wiki/ANSI_escape_code, i.e. colorstr('blue', 'hello world')
|
||||
*args, string = input if len(input) > 1 else ('blue', 'bold', input[0]) # color arguments, string
|
||||
colors = {'black': '\033[30m', # basic colors
|
||||
'red': '\033[31m',
|
||||
'green': '\033[32m',
|
||||
'yellow': '\033[33m',
|
||||
'blue': '\033[34m',
|
||||
'magenta': '\033[35m',
|
||||
'cyan': '\033[36m',
|
||||
'white': '\033[37m',
|
||||
'bright_black': '\033[90m', # bright colors
|
||||
'bright_red': '\033[91m',
|
||||
'bright_green': '\033[92m',
|
||||
'bright_yellow': '\033[93m',
|
||||
'bright_blue': '\033[94m',
|
||||
'bright_magenta': '\033[95m',
|
||||
'bright_cyan': '\033[96m',
|
||||
'bright_white': '\033[97m',
|
||||
'end': '\033[0m', # misc
|
||||
'bold': '\033[1m',
|
||||
'underline': '\033[4m'}
|
||||
return ''.join(colors[x] for x in args) + f'{string}' + colors['end']
|
||||
|
||||
|
||||
def labels_to_class_weights(labels, nc=80):
|
||||
# Get class weights (inverse frequency) from training labels
|
||||
if labels[0] is None: # no labels loaded
|
||||
return torch.Tensor()
|
||||
|
||||
labels = np.concatenate(labels, 0) # labels.shape = (866643, 5) for COCO
|
||||
classes = labels[:, 0].astype(np.int32) # labels = [class xywh]
|
||||
weights = np.bincount(classes, minlength=nc) # occurrences per class
|
||||
|
||||
# Prepend gridpoint count (for uCE training)
|
||||
# gpi = ((320 / 32 * np.array([1, 2, 4])) ** 2 * 3).sum() # gridpoints per image
|
||||
# weights = np.hstack([gpi * len(labels) - weights.sum() * 9, weights * 9]) ** 0.5 # prepend gridpoints to start
|
||||
|
||||
weights[weights == 0] = 1 # replace empty bins with 1
|
||||
weights = 1 / weights # number of targets per class
|
||||
weights /= weights.sum() # normalize
|
||||
return torch.from_numpy(weights)
|
||||
|
||||
|
||||
def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)):
|
||||
# Produces image weights based on class_weights and image contents
|
||||
class_counts = np.array([np.bincount(x[:, 0].astype(np.int32), minlength=nc) for x in labels])
|
||||
image_weights = (class_weights.reshape(1, nc) * class_counts).sum(1)
|
||||
# index = random.choices(range(n), weights=image_weights, k=1) # weight image sample
|
||||
return image_weights
|
||||
|
||||
|
||||
def coco80_to_coco91_class(): # converts 80-index (val2014) to 91-index (paper)
|
||||
# https://tech.amikelive.com/node-718/what-object-categories-labels-are-in-coco-dataset/
|
||||
# a = np.loadtxt('data/coco.names', dtype='str', delimiter='\n')
|
||||
# b = np.loadtxt('data/coco_paper.names', dtype='str', delimiter='\n')
|
||||
# x1 = [list(a[i] == b).index(True) + 1 for i in range(80)] # darknet to coco
|
||||
# x2 = [list(b[i] == a).index(True) if any(b[i] == a) else None for i in range(91)] # coco to darknet
|
||||
x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34,
|
||||
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
|
||||
64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90]
|
||||
return x
|
||||
|
||||
|
||||
def xyxy2xywh(x):
|
||||
# Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = (x[:, 0] + x[:, 2]) / 2 # x center
|
||||
y[:, 1] = (x[:, 1] + x[:, 3]) / 2 # y center
|
||||
y[:, 2] = x[:, 2] - x[:, 0] # width
|
||||
y[:, 3] = x[:, 3] - x[:, 1] # height
|
||||
return y
|
||||
|
||||
|
||||
def xywh2xyxy(x):
|
||||
# Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x
|
||||
y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y
|
||||
y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x
|
||||
y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y
|
||||
return y
|
||||
|
||||
|
||||
def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0):
|
||||
# Convert nx4 boxes from [x, y, w, h] normalized to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = w * (x[:, 0] - x[:, 2] / 2) + padw # top left x
|
||||
y[:, 1] = h * (x[:, 1] - x[:, 3] / 2) + padh # top left y
|
||||
y[:, 2] = w * (x[:, 0] + x[:, 2] / 2) + padw # bottom right x
|
||||
y[:, 3] = h * (x[:, 1] + x[:, 3] / 2) + padh # bottom right y
|
||||
return y
|
||||
|
||||
|
||||
def xyn2xy(x, w=640, h=640, padw=0, padh=0):
|
||||
# Convert normalized segments into pixel segments, shape (n,2)
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = w * x[:, 0] + padw # top left x
|
||||
y[:, 1] = h * x[:, 1] + padh # top left y
|
||||
return y
|
||||
|
||||
|
||||
def segment2box(segment, width=640, height=640):
|
||||
# Convert 1 segment label to 1 box label, applying inside-image constraint, i.e. (xy1, xy2, ...) to (xyxy)
|
||||
x, y = segment.T # segment xy
|
||||
inside = (x >= 0) & (y >= 0) & (x <= width) & (y <= height)
|
||||
x, y, = x[inside], y[inside]
|
||||
return np.array([x.min(), y.min(), x.max(), y.max()]) if any(x) else np.zeros((1, 4)) # xyxy
|
||||
|
||||
|
||||
def segments2boxes(segments):
|
||||
# Convert segment labels to box labels, i.e. (cls, xy1, xy2, ...) to (cls, xywh)
|
||||
boxes = []
|
||||
for s in segments:
|
||||
x, y = s.T # segment xy
|
||||
boxes.append([x.min(), y.min(), x.max(), y.max()]) # cls, xyxy
|
||||
return xyxy2xywh(np.array(boxes)) # cls, xywh
|
||||
|
||||
|
||||
def resample_segments(segments, n=1000):
|
||||
# Up-sample an (n,2) segment
|
||||
for i, s in enumerate(segments):
|
||||
s = np.concatenate((s, s[0:1, :]), axis=0)
|
||||
x = np.linspace(0, len(s) - 1, n)
|
||||
xp = np.arange(len(s))
|
||||
segments[i] = np.concatenate([np.interp(x, xp, s[:, i]) for i in range(2)]).reshape(2, -1).T # segment xy
|
||||
return segments
|
||||
|
||||
|
||||
def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
|
||||
# Rescale coords (xyxy) from img1_shape to img0_shape
|
||||
if ratio_pad is None: # calculate from img0_shape
|
||||
gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new
|
||||
pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # wh padding
|
||||
else:
|
||||
gain = ratio_pad[0][0]
|
||||
pad = ratio_pad[1]
|
||||
|
||||
coords[:, [0, 2]] -= pad[0] # x padding
|
||||
coords[:, [1, 3]] -= pad[1] # y padding
|
||||
coords[:, :4] /= gain
|
||||
clip_coords(coords, img0_shape)
|
||||
return coords
|
||||
|
||||
|
||||
def clip_coords(boxes, img_shape):
|
||||
# Clip bounding xyxy bounding boxes to image shape (height, width)
|
||||
boxes[:, 0].clamp_(0, img_shape[1]) # x1
|
||||
boxes[:, 1].clamp_(0, img_shape[0]) # y1
|
||||
boxes[:, 2].clamp_(0, img_shape[1]) # x2
|
||||
boxes[:, 3].clamp_(0, img_shape[0]) # y2
|
||||
|
||||
|
||||
def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7):
|
||||
# Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
|
||||
box2 = box2.T
|
||||
|
||||
# Get the coordinates of bounding boxes
|
||||
if x1y1x2y2: # x1, y1, x2, y2 = box1
|
||||
b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
|
||||
b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
|
||||
else: # transform from xywh to xyxy
|
||||
b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
|
||||
b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
|
||||
b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
|
||||
b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2
|
||||
|
||||
# Intersection area
|
||||
inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
|
||||
(torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
|
||||
|
||||
# Union Area
|
||||
w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps
|
||||
w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps
|
||||
union = w1 * h1 + w2 * h2 - inter + eps
|
||||
|
||||
iou = inter / union
|
||||
|
||||
if GIoU or DIoU or CIoU:
|
||||
cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width
|
||||
ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height
|
||||
if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
|
||||
c2 = cw ** 2 + ch ** 2 + eps # convex diagonal squared
|
||||
rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 +
|
||||
(b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center distance squared
|
||||
if DIoU:
|
||||
return iou - rho2 / c2 # DIoU
|
||||
elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
|
||||
v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / (h2 + eps)) - torch.atan(w1 / (h1 + eps)), 2)
|
||||
with torch.no_grad():
|
||||
alpha = v / (v - iou + (1 + eps))
|
||||
return iou - (rho2 / c2 + v * alpha) # CIoU
|
||||
else: # GIoU https://arxiv.org/pdf/1902.09630.pdf
|
||||
c_area = cw * ch + eps # convex area
|
||||
return iou - (c_area - union) / c_area # GIoU
|
||||
else:
|
||||
return iou # IoU
|
||||
|
||||
|
||||
|
||||
|
||||
def bbox_alpha_iou(box1, box2, x1y1x2y2=False, GIoU=False, DIoU=False, CIoU=False, alpha=2, eps=1e-9):
|
||||
# Returns tsqrt_he IoU of box1 to box2. box1 is 4, box2 is nx4
|
||||
box2 = box2.T
|
||||
|
||||
# Get the coordinates of bounding boxes
|
||||
if x1y1x2y2: # x1, y1, x2, y2 = box1
|
||||
b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
|
||||
b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
|
||||
else: # transform from xywh to xyxy
|
||||
b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
|
||||
b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
|
||||
b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
|
||||
b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2
|
||||
|
||||
# Intersection area
|
||||
inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
|
||||
(torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
|
||||
|
||||
# Union Area
|
||||
w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps
|
||||
w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps
|
||||
union = w1 * h1 + w2 * h2 - inter + eps
|
||||
|
||||
# change iou into pow(iou+eps)
|
||||
# iou = inter / union
|
||||
iou = torch.pow(inter/union + eps, alpha)
|
||||
# beta = 2 * alpha
|
||||
if GIoU or DIoU or CIoU:
|
||||
cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width
|
||||
ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height
|
||||
if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
|
||||
c2 = (cw ** 2 + ch ** 2) ** alpha + eps # convex diagonal
|
||||
rho_x = torch.abs(b2_x1 + b2_x2 - b1_x1 - b1_x2)
|
||||
rho_y = torch.abs(b2_y1 + b2_y2 - b1_y1 - b1_y2)
|
||||
rho2 = ((rho_x ** 2 + rho_y ** 2) / 4) ** alpha # center distance
|
||||
if DIoU:
|
||||
return iou - rho2 / c2 # DIoU
|
||||
elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
|
||||
v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2)
|
||||
with torch.no_grad():
|
||||
alpha_ciou = v / ((1 + eps) - inter / union + v)
|
||||
# return iou - (rho2 / c2 + v * alpha_ciou) # CIoU
|
||||
return iou - (rho2 / c2 + torch.pow(v * alpha_ciou + eps, alpha)) # CIoU
|
||||
else: # GIoU https://arxiv.org/pdf/1902.09630.pdf
|
||||
# c_area = cw * ch + eps # convex area
|
||||
# return iou - (c_area - union) / c_area # GIoU
|
||||
c_area = torch.max(cw * ch + eps, union) # convex area
|
||||
return iou - torch.pow((c_area - union) / c_area + eps, alpha) # GIoU
|
||||
else:
|
||||
return iou # torch.log(iou+eps) or iou
|
||||
|
||||
|
||||
def box_iou(box1, box2):
|
||||
# https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
|
||||
"""
|
||||
Return intersection-over-union (Jaccard index) of boxes.
|
||||
Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
|
||||
Arguments:
|
||||
box1 (Tensor[N, 4])
|
||||
box2 (Tensor[M, 4])
|
||||
Returns:
|
||||
iou (Tensor[N, M]): the NxM matrix containing the pairwise
|
||||
IoU values for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
# inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
return inter / (area1[:, None] + area2 - inter) # iou = inter / (area1 + area2 - inter)
|
||||
|
||||
|
||||
def wh_iou(wh1, wh2):
|
||||
# Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2
|
||||
wh1 = wh1[:, None] # [N,1,2]
|
||||
wh2 = wh2[None] # [1,M,2]
|
||||
inter = torch.min(wh1, wh2).prod(2) # [N,M]
|
||||
return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter)
|
||||
|
||||
|
||||
def box_giou(box1, box2):
|
||||
"""
|
||||
Return generalized intersection-over-union (Jaccard index) between two sets of boxes.
|
||||
Both sets of boxes are expected to be in ``(x1, y1, x2, y2)`` format with
|
||||
``0 <= x1 < x2`` and ``0 <= y1 < y2``.
|
||||
Args:
|
||||
boxes1 (Tensor[N, 4]): first set of boxes
|
||||
boxes2 (Tensor[M, 4]): second set of boxes
|
||||
Returns:
|
||||
Tensor[N, M]: the NxM matrix containing the pairwise generalized IoU values
|
||||
for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
union = (area1[:, None] + area2 - inter)
|
||||
|
||||
iou = inter / union
|
||||
|
||||
lti = torch.min(box1[:, None, :2], box2[:, :2])
|
||||
rbi = torch.max(box1[:, None, 2:], box2[:, 2:])
|
||||
|
||||
whi = (rbi - lti).clamp(min=0) # [N,M,2]
|
||||
areai = whi[:, :, 0] * whi[:, :, 1]
|
||||
|
||||
return iou - (areai - union) / areai
|
||||
|
||||
|
||||
def box_ciou(box1, box2, eps: float = 1e-7):
|
||||
"""
|
||||
Return complete intersection-over-union (Jaccard index) between two sets of boxes.
|
||||
Both sets of boxes are expected to be in ``(x1, y1, x2, y2)`` format with
|
||||
``0 <= x1 < x2`` and ``0 <= y1 < y2``.
|
||||
Args:
|
||||
boxes1 (Tensor[N, 4]): first set of boxes
|
||||
boxes2 (Tensor[M, 4]): second set of boxes
|
||||
eps (float, optional): small number to prevent division by zero. Default: 1e-7
|
||||
Returns:
|
||||
Tensor[N, M]: the NxM matrix containing the pairwise complete IoU values
|
||||
for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
union = (area1[:, None] + area2 - inter)
|
||||
|
||||
iou = inter / union
|
||||
|
||||
lti = torch.min(box1[:, None, :2], box2[:, :2])
|
||||
rbi = torch.max(box1[:, None, 2:], box2[:, 2:])
|
||||
|
||||
whi = (rbi - lti).clamp(min=0) # [N,M,2]
|
||||
diagonal_distance_squared = (whi[:, :, 0] ** 2) + (whi[:, :, 1] ** 2) + eps
|
||||
|
||||
# centers of boxes
|
||||
x_p = (box1[:, None, 0] + box1[:, None, 2]) / 2
|
||||
y_p = (box1[:, None, 1] + box1[:, None, 3]) / 2
|
||||
x_g = (box2[:, 0] + box2[:, 2]) / 2
|
||||
y_g = (box2[:, 1] + box2[:, 3]) / 2
|
||||
# The distance between boxes' centers squared.
|
||||
centers_distance_squared = (x_p - x_g) ** 2 + (y_p - y_g) ** 2
|
||||
|
||||
w_pred = box1[:, None, 2] - box1[:, None, 0]
|
||||
h_pred = box1[:, None, 3] - box1[:, None, 1]
|
||||
|
||||
w_gt = box2[:, 2] - box2[:, 0]
|
||||
h_gt = box2[:, 3] - box2[:, 1]
|
||||
|
||||
v = (4 / (torch.pi ** 2)) * torch.pow((torch.atan(w_gt / h_gt) - torch.atan(w_pred / h_pred)), 2)
|
||||
with torch.no_grad():
|
||||
alpha = v / (1 - iou + v + eps)
|
||||
return iou - (centers_distance_squared / diagonal_distance_squared) - alpha * v
|
||||
|
||||
|
||||
def box_diou(box1, box2, eps: float = 1e-7):
|
||||
"""
|
||||
Return distance intersection-over-union (Jaccard index) between two sets of boxes.
|
||||
Both sets of boxes are expected to be in ``(x1, y1, x2, y2)`` format with
|
||||
``0 <= x1 < x2`` and ``0 <= y1 < y2``.
|
||||
Args:
|
||||
boxes1 (Tensor[N, 4]): first set of boxes
|
||||
boxes2 (Tensor[M, 4]): second set of boxes
|
||||
eps (float, optional): small number to prevent division by zero. Default: 1e-7
|
||||
Returns:
|
||||
Tensor[N, M]: the NxM matrix containing the pairwise distance IoU values
|
||||
for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
union = (area1[:, None] + area2 - inter)
|
||||
|
||||
iou = inter / union
|
||||
|
||||
lti = torch.min(box1[:, None, :2], box2[:, :2])
|
||||
rbi = torch.max(box1[:, None, 2:], box2[:, 2:])
|
||||
|
||||
whi = (rbi - lti).clamp(min=0) # [N,M,2]
|
||||
diagonal_distance_squared = (whi[:, :, 0] ** 2) + (whi[:, :, 1] ** 2) + eps
|
||||
|
||||
# centers of boxes
|
||||
x_p = (box1[:, None, 0] + box1[:, None, 2]) / 2
|
||||
y_p = (box1[:, None, 1] + box1[:, None, 3]) / 2
|
||||
x_g = (box2[:, 0] + box2[:, 2]) / 2
|
||||
y_g = (box2[:, 1] + box2[:, 3]) / 2
|
||||
# The distance between boxes' centers squared.
|
||||
centers_distance_squared = (x_p - x_g) ** 2 + (y_p - y_g) ** 2
|
||||
|
||||
# The distance IoU is the IoU penalized by a normalized
|
||||
# distance between boxes' centers squared.
|
||||
return iou - (centers_distance_squared / diagonal_distance_squared)
|
||||
|
||||
|
||||
def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False,
|
||||
labels=()):
|
||||
"""Runs Non-Maximum Suppression (NMS) on inference results
|
||||
|
||||
Returns:
|
||||
list of detections, on (n,6) tensor per image [xyxy, conf, cls]
|
||||
"""
|
||||
|
||||
nc = prediction.shape[2] - 5 # number of classes
|
||||
xc = prediction[..., 4] > conf_thres # candidates
|
||||
|
||||
# Settings
|
||||
min_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and height
|
||||
max_det = 300 # maximum number of detections per image
|
||||
max_nms = 30000 # maximum number of boxes into torchvision.ops.nms()
|
||||
time_limit = 10.0 # seconds to quit after
|
||||
redundant = True # require redundant detections
|
||||
multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)
|
||||
merge = False # use merge-NMS
|
||||
|
||||
t = time.time()
|
||||
output = [torch.zeros((0, 6), device=prediction.device)] * prediction.shape[0]
|
||||
for xi, x in enumerate(prediction): # image index, image inference
|
||||
# Apply constraints
|
||||
# x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height
|
||||
x = x[xc[xi]] # confidence
|
||||
|
||||
# Cat apriori labels if autolabelling
|
||||
if labels and len(labels[xi]):
|
||||
l = labels[xi]
|
||||
v = torch.zeros((len(l), nc + 5), device=x.device)
|
||||
v[:, :4] = l[:, 1:5] # box
|
||||
v[:, 4] = 1.0 # conf
|
||||
v[range(len(l)), l[:, 0].long() + 5] = 1.0 # cls
|
||||
x = torch.cat((x, v), 0)
|
||||
|
||||
# If none remain process next image
|
||||
if not x.shape[0]:
|
||||
continue
|
||||
|
||||
# Compute conf
|
||||
if nc == 1:
|
||||
x[:, 5:] = x[:, 4:5] # for models with one class, cls_loss is 0 and cls_conf is always 0.5,
|
||||
# so there is no need to multiplicate.
|
||||
else:
|
||||
x[:, 5:] *= x[:, 4:5] # conf = obj_conf * cls_conf
|
||||
|
||||
# Box (center x, center y, width, height) to (x1, y1, x2, y2)
|
||||
box = xywh2xyxy(x[:, :4])
|
||||
|
||||
# Detections matrix nx6 (xyxy, conf, cls)
|
||||
if multi_label:
|
||||
i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
|
||||
x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
|
||||
else: # best class only
|
||||
conf, j = x[:, 5:].max(1, keepdim=True)
|
||||
x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]
|
||||
|
||||
# Filter by class
|
||||
if classes is not None:
|
||||
x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]
|
||||
|
||||
# Apply finite constraint
|
||||
# if not torch.isfinite(x).all():
|
||||
# x = x[torch.isfinite(x).all(1)]
|
||||
|
||||
# Check shape
|
||||
n = x.shape[0] # number of boxes
|
||||
if not n: # no boxes
|
||||
continue
|
||||
elif n > max_nms: # excess boxes
|
||||
x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence
|
||||
|
||||
# Batched NMS
|
||||
c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
|
||||
boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores
|
||||
i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS
|
||||
if i.shape[0] > max_det: # limit detections
|
||||
i = i[:max_det]
|
||||
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
|
||||
# update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
|
||||
iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
|
||||
weights = iou * scores[None] # box weights
|
||||
x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes
|
||||
if redundant:
|
||||
i = i[iou.sum(1) > 1] # require redundancy
|
||||
|
||||
output[xi] = x[i]
|
||||
if (time.time() - t) > time_limit:
|
||||
print(f'WARNING: NMS time limit {time_limit}s exceeded')
|
||||
break # time limit exceeded
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def non_max_suppression_kpt(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False,
|
||||
labels=(), kpt_label=False, nc=None, nkpt=None):
|
||||
"""Runs Non-Maximum Suppression (NMS) on inference results
|
||||
|
||||
Returns:
|
||||
list of detections, on (n,6) tensor per image [xyxy, conf, cls]
|
||||
"""
|
||||
if nc is None:
|
||||
nc = prediction.shape[2] - 5 if not kpt_label else prediction.shape[2] - 56 # number of classes
|
||||
xc = prediction[..., 4] > conf_thres # candidates
|
||||
|
||||
# Settings
|
||||
min_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and height
|
||||
max_det = 300 # maximum number of detections per image
|
||||
max_nms = 30000 # maximum number of boxes into torchvision.ops.nms()
|
||||
time_limit = 10.0 # seconds to quit after
|
||||
redundant = True # require redundant detections
|
||||
multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)
|
||||
merge = False # use merge-NMS
|
||||
|
||||
t = time.time()
|
||||
output = [torch.zeros((0,6), device=prediction.device)] * prediction.shape[0]
|
||||
for xi, x in enumerate(prediction): # image index, image inference
|
||||
# Apply constraints
|
||||
# x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height
|
||||
x = x[xc[xi]] # confidence
|
||||
|
||||
# Cat apriori labels if autolabelling
|
||||
if labels and len(labels[xi]):
|
||||
l = labels[xi]
|
||||
v = torch.zeros((len(l), nc + 5), device=x.device)
|
||||
v[:, :4] = l[:, 1:5] # box
|
||||
v[:, 4] = 1.0 # conf
|
||||
v[range(len(l)), l[:, 0].long() + 5] = 1.0 # cls
|
||||
x = torch.cat((x, v), 0)
|
||||
|
||||
# If none remain process next image
|
||||
if not x.shape[0]:
|
||||
continue
|
||||
|
||||
# Compute conf
|
||||
x[:, 5:5+nc] *= x[:, 4:5] # conf = obj_conf * cls_conf
|
||||
|
||||
# Box (center x, center y, width, height) to (x1, y1, x2, y2)
|
||||
box = xywh2xyxy(x[:, :4])
|
||||
|
||||
# Detections matrix nx6 (xyxy, conf, cls)
|
||||
if multi_label:
|
||||
i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
|
||||
x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
|
||||
else: # best class only
|
||||
if not kpt_label:
|
||||
conf, j = x[:, 5:].max(1, keepdim=True)
|
||||
x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]
|
||||
else:
|
||||
kpts = x[:, 6:]
|
||||
conf, j = x[:, 5:6].max(1, keepdim=True)
|
||||
x = torch.cat((box, conf, j.float(), kpts), 1)[conf.view(-1) > conf_thres]
|
||||
|
||||
|
||||
# Filter by class
|
||||
if classes is not None:
|
||||
x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]
|
||||
|
||||
# Apply finite constraint
|
||||
# if not torch.isfinite(x).all():
|
||||
# x = x[torch.isfinite(x).all(1)]
|
||||
|
||||
# Check shape
|
||||
n = x.shape[0] # number of boxes
|
||||
if not n: # no boxes
|
||||
continue
|
||||
elif n > max_nms: # excess boxes
|
||||
x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence
|
||||
|
||||
# Batched NMS
|
||||
c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
|
||||
boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores
|
||||
i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS
|
||||
if i.shape[0] > max_det: # limit detections
|
||||
i = i[:max_det]
|
||||
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
|
||||
# update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
|
||||
iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
|
||||
weights = iou * scores[None] # box weights
|
||||
x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes
|
||||
if redundant:
|
||||
i = i[iou.sum(1) > 1] # require redundancy
|
||||
|
||||
output[xi] = x[i]
|
||||
if (time.time() - t) > time_limit:
|
||||
print(f'WARNING: NMS time limit {time_limit}s exceeded')
|
||||
break # time limit exceeded
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def strip_optimizer(f='best.pt', s=''): # from utils.general import *; strip_optimizer()
|
||||
# Strip optimizer from 'f' to finalize training, optionally save as 's'
|
||||
x = torch.load(f, map_location=torch.device('cpu'))
|
||||
if x.get('ema'):
|
||||
x['model'] = x['ema'] # replace model with ema
|
||||
for k in 'optimizer', 'training_results', 'wandb_id', 'ema', 'updates': # keys
|
||||
x[k] = None
|
||||
x['epoch'] = -1
|
||||
x['model'].half() # to FP16
|
||||
for p in x['model'].parameters():
|
||||
p.requires_grad = False
|
||||
torch.save(x, s or f)
|
||||
mb = os.path.getsize(s or f) / 1E6 # filesize
|
||||
print(f"Optimizer stripped from {f},{(' saved as %s,' % s) if s else ''} {mb:.1f}MB")
|
||||
|
||||
|
||||
def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''):
|
||||
# Print mutation results to evolve.txt (for use with train.py --evolve)
|
||||
a = '%10s' * len(hyp) % tuple(hyp.keys()) # hyperparam keys
|
||||
b = '%10.3g' * len(hyp) % tuple(hyp.values()) # hyperparam values
|
||||
c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
|
||||
print('\n%s\n%s\nEvolved fitness: %s\n' % (a, b, c))
|
||||
|
||||
if bucket:
|
||||
url = 'gs://%s/evolve.txt' % bucket
|
||||
if gsutil_getsize(url) > (os.path.getsize('evolve.txt') if os.path.exists('evolve.txt') else 0):
|
||||
os.system('gsutil cp %s .' % url) # download evolve.txt if larger than local
|
||||
|
||||
with open('evolve.txt', 'a') as f: # append result
|
||||
f.write(c + b + '\n')
|
||||
x = np.unique(np.loadtxt('evolve.txt', ndmin=2), axis=0) # load unique rows
|
||||
x = x[np.argsort(-fitness(x))] # sort
|
||||
np.savetxt('evolve.txt', x, '%10.3g') # save sort by fitness
|
||||
|
||||
# Save yaml
|
||||
for i, k in enumerate(hyp.keys()):
|
||||
hyp[k] = float(x[0, i + 7])
|
||||
with open(yaml_file, 'w') as f:
|
||||
results = tuple(x[0, :7])
|
||||
c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
|
||||
f.write('# Hyperparameter Evolution Results\n# Generations: %g\n# Metrics: ' % len(x) + c + '\n\n')
|
||||
yaml.dump(hyp, f, sort_keys=False)
|
||||
|
||||
if bucket:
|
||||
os.system('gsutil cp evolve.txt %s gs://%s' % (yaml_file, bucket)) # upload
|
||||
|
||||
|
||||
def apply_classifier(x, model, img, im0):
|
||||
# applies a second stage classifier to yolo outputs
|
||||
im0 = [im0] if isinstance(im0, np.ndarray) else im0
|
||||
for i, d in enumerate(x): # per image
|
||||
if d is not None and len(d):
|
||||
d = d.clone()
|
||||
|
||||
# Reshape and pad cutouts
|
||||
b = xyxy2xywh(d[:, :4]) # boxes
|
||||
b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # rectangle to square
|
||||
b[:, 2:] = b[:, 2:] * 1.3 + 30 # pad
|
||||
d[:, :4] = xywh2xyxy(b).long()
|
||||
|
||||
# Rescale boxes from img_size to im0 size
|
||||
scale_coords(img.shape[2:], d[:, :4], im0[i].shape)
|
||||
|
||||
# Classes
|
||||
pred_cls1 = d[:, 5].long()
|
||||
ims = []
|
||||
for j, a in enumerate(d): # per item
|
||||
cutout = im0[i][int(a[1]):int(a[3]), int(a[0]):int(a[2])]
|
||||
im = cv2.resize(cutout, (224, 224)) # BGR
|
||||
# cv2.imwrite('test%i.jpg' % j, cutout)
|
||||
|
||||
im = im[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
|
||||
im = np.ascontiguousarray(im, dtype=np.float32) # uint8 to float32
|
||||
im /= 255.0 # 0 - 255 to 0.0 - 1.0
|
||||
ims.append(im)
|
||||
|
||||
pred_cls2 = model(torch.Tensor(ims).to(d.device)).argmax(1) # classifier prediction
|
||||
x[i] = x[i][pred_cls1 == pred_cls2] # retain matching class detections
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def increment_path(path, exist_ok=True, sep=''):
|
||||
# Increment path, i.e. runs/exp --> runs/exp{sep}0, runs/exp{sep}1 etc.
|
||||
path = Path(path) # os-agnostic
|
||||
if (path.exists() and exist_ok) or (not path.exists()):
|
||||
return str(path)
|
||||
else:
|
||||
dirs = glob.glob(f"{path}{sep}*") # similar paths
|
||||
matches = [re.search(rf"%s{sep}(\d+)" % path.stem, d) for d in dirs]
|
||||
i = [int(m.groups()[0]) for m in matches if m] # indices
|
||||
n = max(i) + 1 if i else 2 # increment number
|
||||
return f"{path}{sep}{n}" # update path
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
FROM gcr.io/google-appengine/python
|
||||
|
||||
# Create a virtualenv for dependencies. This isolates these packages from
|
||||
# system-level packages.
|
||||
# Use -p python3 or -p python3.7 to select python version. Default is version 2.
|
||||
RUN virtualenv /env -p python3
|
||||
|
||||
# Setting these environment variables are the same as running
|
||||
# source /env/bin/activate.
|
||||
ENV VIRTUAL_ENV /env
|
||||
ENV PATH /env/bin:$PATH
|
||||
|
||||
RUN apt-get update && apt-get install -y python-opencv
|
||||
|
||||
# Copy the application's requirements.txt and run pip to install all
|
||||
# dependencies into the virtualenv.
|
||||
ADD requirements.txt /app/requirements.txt
|
||||
RUN pip install -r /app/requirements.txt
|
||||
|
||||
# Add the application source code.
|
||||
ADD . /app
|
||||
|
||||
# Run a WSGI server to serve the application. gunicorn must be declared as
|
||||
# a dependency in requirements.txt.
|
||||
CMD gunicorn -b :$PORT main:app
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# add these requirements in your app on top of the existing ones
|
||||
pip==18.1
|
||||
Flask==1.0.2
|
||||
gunicorn==19.9.0
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
runtime: custom
|
||||
env: flex
|
||||
|
||||
service: yolorapp
|
||||
|
||||
liveness_check:
|
||||
initial_delay_sec: 600
|
||||
|
||||
manual_scaling:
|
||||
instances: 1
|
||||
resources:
|
||||
cpu: 1
|
||||
memory_gb: 4
|
||||
disk_size_gb: 20
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
# Google utils: https://cloud.google.com/storage/docs/reference/libraries
|
||||
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
import torch
|
||||
|
||||
|
||||
def gsutil_getsize(url=''):
|
||||
# gs://bucket/file size https://cloud.google.com/storage/docs/gsutil/commands/du
|
||||
s = subprocess.check_output(f'gsutil du {url}', shell=True).decode('utf-8')
|
||||
return eval(s.split(' ')[0]) if len(s) else 0 # bytes
|
||||
|
||||
|
||||
def attempt_download(file, repo='WongKinYiu/yolov7'):
|
||||
# Attempt file download if does not exist
|
||||
file = Path(str(file).strip().replace("'", '').lower())
|
||||
|
||||
if not file.exists():
|
||||
try:
|
||||
response = requests.get(f'https://api.github.com/repos/{repo}/releases/latest').json() # github api
|
||||
assets = [x['name'] for x in response['assets']] # release assets
|
||||
tag = response['tag_name'] # i.e. 'v1.0'
|
||||
except: # fallback plan
|
||||
assets = ['yolov7.pt', 'yolov7-tiny.pt', 'yolov7x.pt', 'yolov7-d6.pt', 'yolov7-e6.pt',
|
||||
'yolov7-e6e.pt', 'yolov7-w6.pt']
|
||||
tag = subprocess.check_output('git tag', shell=True).decode().split()[-1]
|
||||
|
||||
name = file.name
|
||||
if name in assets:
|
||||
msg = f'{file} missing, try downloading from https://github.com/{repo}/releases/'
|
||||
redundant = False # second download option
|
||||
try: # GitHub
|
||||
url = f'https://github.com/{repo}/releases/download/{tag}/{name}'
|
||||
print(f'Downloading {url} to {file}...')
|
||||
torch.hub.download_url_to_file(url, file)
|
||||
assert file.exists() and file.stat().st_size > 1E6 # check
|
||||
except Exception as e: # GCP
|
||||
print(f'Download error: {e}')
|
||||
assert redundant, 'No secondary mirror'
|
||||
url = f'https://storage.googleapis.com/{repo}/ckpt/{name}'
|
||||
print(f'Downloading {url} to {file}...')
|
||||
os.system(f'curl -L {url} -o {file}') # torch.hub.download_url_to_file(url, weights)
|
||||
finally:
|
||||
if not file.exists() or file.stat().st_size < 1E6: # check
|
||||
file.unlink(missing_ok=True) # remove partial downloads
|
||||
print(f'ERROR: Download failure: {msg}')
|
||||
print('')
|
||||
return
|
||||
|
||||
|
||||
def gdrive_download(id='', file='tmp.zip'):
|
||||
# Downloads a file from Google Drive. from yolov7.utils.google_utils import *; gdrive_download()
|
||||
t = time.time()
|
||||
file = Path(file)
|
||||
cookie = Path('cookie') # gdrive cookie
|
||||
print(f'Downloading https://drive.google.com/uc?export=download&id={id} as {file}... ', end='')
|
||||
file.unlink(missing_ok=True) # remove existing file
|
||||
cookie.unlink(missing_ok=True) # remove existing cookie
|
||||
|
||||
# Attempt file download
|
||||
out = "NUL" if platform.system() == "Windows" else "/dev/null"
|
||||
os.system(f'curl -c ./cookie -s -L "drive.google.com/uc?export=download&id={id}" > {out}')
|
||||
if os.path.exists('cookie'): # large file
|
||||
s = f'curl -Lb ./cookie "drive.google.com/uc?export=download&confirm={get_token()}&id={id}" -o {file}'
|
||||
else: # small file
|
||||
s = f'curl -s -L -o {file} "drive.google.com/uc?export=download&id={id}"'
|
||||
r = os.system(s) # execute, capture return
|
||||
cookie.unlink(missing_ok=True) # remove existing cookie
|
||||
|
||||
# Error check
|
||||
if r != 0:
|
||||
file.unlink(missing_ok=True) # remove partial
|
||||
print('Download error ') # raise Exception('Download error')
|
||||
return r
|
||||
|
||||
# Unzip if archive
|
||||
if file.suffix == '.zip':
|
||||
print('unzipping... ', end='')
|
||||
os.system(f'unzip -q {file}') # unzip
|
||||
file.unlink() # remove zip to free space
|
||||
|
||||
print(f'Done ({time.time() - t:.1f}s)')
|
||||
return r
|
||||
|
||||
|
||||
def get_token(cookie="./cookie"):
|
||||
with open(cookie) as f:
|
||||
for line in f:
|
||||
if "download" in line:
|
||||
return line.split()[-1]
|
||||
return ""
|
||||
|
||||
# def upload_blob(bucket_name, source_file_name, destination_blob_name):
|
||||
# # Uploads a file to a bucket
|
||||
# # https://cloud.google.com/storage/docs/uploading-objects#storage-upload-object-python
|
||||
#
|
||||
# storage_client = storage.Client()
|
||||
# bucket = storage_client.get_bucket(bucket_name)
|
||||
# blob = bucket.blob(destination_blob_name)
|
||||
#
|
||||
# blob.upload_from_filename(source_file_name)
|
||||
#
|
||||
# print('File {} uploaded to {}.'.format(
|
||||
# source_file_name,
|
||||
# destination_blob_name))
|
||||
#
|
||||
#
|
||||
# def download_blob(bucket_name, source_blob_name, destination_file_name):
|
||||
# # Uploads a blob from a bucket
|
||||
# storage_client = storage.Client()
|
||||
# bucket = storage_client.get_bucket(bucket_name)
|
||||
# blob = bucket.blob(source_blob_name)
|
||||
#
|
||||
# blob.download_to_filename(destination_file_name)
|
||||
#
|
||||
# print('Blob {} downloaded to {}.'.format(
|
||||
# source_blob_name,
|
||||
# destination_file_name))
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,227 @@
|
|||
# Model validation metrics
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from . import general
|
||||
|
||||
|
||||
def fitness(x):
|
||||
# Model fitness as a weighted combination of metrics
|
||||
w = [0.0, 0.0, 0.1, 0.9] # weights for [P, R, mAP@0.5, mAP@0.5:0.95]
|
||||
return (x[:, :4] * w).sum(1)
|
||||
|
||||
|
||||
def ap_per_class(tp, conf, pred_cls, target_cls, v5_metric=False, plot=False, save_dir='.', names=()):
|
||||
""" Compute the average precision, given the recall and precision curves.
|
||||
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
|
||||
# Arguments
|
||||
tp: True positives (nparray, nx1 or nx10).
|
||||
conf: Objectness value from 0-1 (nparray).
|
||||
pred_cls: Predicted object classes (nparray).
|
||||
target_cls: True object classes (nparray).
|
||||
plot: Plot precision-recall curve at mAP@0.5
|
||||
save_dir: Plot save directory
|
||||
# Returns
|
||||
The average precision as computed in py-faster-rcnn.
|
||||
"""
|
||||
|
||||
# Sort by objectness
|
||||
i = np.argsort(-conf)
|
||||
tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]
|
||||
|
||||
# Find unique classes
|
||||
unique_classes = np.unique(target_cls)
|
||||
nc = unique_classes.shape[0] # number of classes, number of detections
|
||||
|
||||
# Create Precision-Recall curve and compute AP for each class
|
||||
px, py = np.linspace(0, 1, 1000), [] # for plotting
|
||||
ap, p, r = np.zeros((nc, tp.shape[1])), np.zeros((nc, 1000)), np.zeros((nc, 1000))
|
||||
for ci, c in enumerate(unique_classes):
|
||||
i = pred_cls == c
|
||||
n_l = (target_cls == c).sum() # number of labels
|
||||
n_p = i.sum() # number of predictions
|
||||
|
||||
if n_p == 0 or n_l == 0:
|
||||
continue
|
||||
else:
|
||||
# Accumulate FPs and TPs
|
||||
fpc = (1 - tp[i]).cumsum(0)
|
||||
tpc = tp[i].cumsum(0)
|
||||
|
||||
# Recall
|
||||
recall = tpc / (n_l + 1e-16) # recall curve
|
||||
r[ci] = np.interp(-px, -conf[i], recall[:, 0], left=0) # negative x, xp because xp decreases
|
||||
|
||||
# Precision
|
||||
precision = tpc / (tpc + fpc) # precision curve
|
||||
p[ci] = np.interp(-px, -conf[i], precision[:, 0], left=1) # p at pr_score
|
||||
|
||||
# AP from recall-precision curve
|
||||
for j in range(tp.shape[1]):
|
||||
ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j], v5_metric=v5_metric)
|
||||
if plot and j == 0:
|
||||
py.append(np.interp(px, mrec, mpre)) # precision at mAP@0.5
|
||||
|
||||
# Compute F1 (harmonic mean of precision and recall)
|
||||
f1 = 2 * p * r / (p + r + 1e-16)
|
||||
if plot:
|
||||
plot_pr_curve(px, py, ap, Path(save_dir) / 'PR_curve.png', names)
|
||||
plot_mc_curve(px, f1, Path(save_dir) / 'F1_curve.png', names, ylabel='F1')
|
||||
plot_mc_curve(px, p, Path(save_dir) / 'P_curve.png', names, ylabel='Precision')
|
||||
plot_mc_curve(px, r, Path(save_dir) / 'R_curve.png', names, ylabel='Recall')
|
||||
|
||||
i = f1.mean(0).argmax() # max F1 index
|
||||
return p[:, i], r[:, i], ap, f1[:, i], unique_classes.astype('int32')
|
||||
|
||||
|
||||
def compute_ap(recall, precision, v5_metric=False):
|
||||
""" Compute the average precision, given the recall and precision curves
|
||||
# Arguments
|
||||
recall: The recall curve (list)
|
||||
precision: The precision curve (list)
|
||||
v5_metric: Assume maximum recall to be 1.0, as in YOLOv5, MMDetetion etc.
|
||||
# Returns
|
||||
Average precision, precision curve, recall curve
|
||||
"""
|
||||
|
||||
# Append sentinel values to beginning and end
|
||||
if v5_metric: # New YOLOv5 metric, same as MMDetection and Detectron2 repositories
|
||||
mrec = np.concatenate(([0.], recall, [1.0]))
|
||||
else: # Old YOLOv5 metric, i.e. default YOLOv7 metric
|
||||
mrec = np.concatenate(([0.], recall, [recall[-1] + 0.01]))
|
||||
mpre = np.concatenate(([1.], precision, [0.]))
|
||||
|
||||
# Compute the precision envelope
|
||||
mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
|
||||
|
||||
# Integrate area under curve
|
||||
method = 'interp' # methods: 'continuous', 'interp'
|
||||
if method == 'interp':
|
||||
x = np.linspace(0, 1, 101) # 101-point interp (COCO)
|
||||
ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate
|
||||
else: # 'continuous'
|
||||
i = np.where(mrec[1:] != mrec[:-1])[0] # points where x axis (recall) changes
|
||||
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) # area under curve
|
||||
|
||||
return ap, mpre, mrec
|
||||
|
||||
|
||||
class ConfusionMatrix:
|
||||
# Updated version of https://github.com/kaanakan/object_detection_confusion_matrix
|
||||
def __init__(self, nc, conf=0.25, iou_thres=0.45):
|
||||
self.matrix = np.zeros((nc + 1, nc + 1))
|
||||
self.nc = nc # number of classes
|
||||
self.conf = conf
|
||||
self.iou_thres = iou_thres
|
||||
|
||||
def process_batch(self, detections, labels):
|
||||
"""
|
||||
Return intersection-over-union (Jaccard index) of boxes.
|
||||
Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
|
||||
Arguments:
|
||||
detections (Array[N, 6]), x1, y1, x2, y2, conf, class
|
||||
labels (Array[M, 5]), class, x1, y1, x2, y2
|
||||
Returns:
|
||||
None, updates confusion matrix accordingly
|
||||
"""
|
||||
detections = detections[detections[:, 4] > self.conf]
|
||||
gt_classes = labels[:, 0].int()
|
||||
detection_classes = detections[:, 5].int()
|
||||
iou = general.box_iou(labels[:, 1:], detections[:, :4])
|
||||
|
||||
x = torch.where(iou > self.iou_thres)
|
||||
if x[0].shape[0]:
|
||||
matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy()
|
||||
if x[0].shape[0] > 1:
|
||||
matches = matches[matches[:, 2].argsort()[::-1]]
|
||||
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
|
||||
matches = matches[matches[:, 2].argsort()[::-1]]
|
||||
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
|
||||
else:
|
||||
matches = np.zeros((0, 3))
|
||||
|
||||
n = matches.shape[0] > 0
|
||||
m0, m1, _ = matches.transpose().astype(np.int16)
|
||||
for i, gc in enumerate(gt_classes):
|
||||
j = m0 == i
|
||||
if n and sum(j) == 1:
|
||||
self.matrix[gc, detection_classes[m1[j]]] += 1 # correct
|
||||
else:
|
||||
self.matrix[self.nc, gc] += 1 # background FP
|
||||
|
||||
if n:
|
||||
for i, dc in enumerate(detection_classes):
|
||||
if not any(m1 == i):
|
||||
self.matrix[dc, self.nc] += 1 # background FN
|
||||
|
||||
def matrix(self):
|
||||
return self.matrix
|
||||
|
||||
def plot(self, save_dir='', names=()):
|
||||
try:
|
||||
import seaborn as sn
|
||||
|
||||
array = self.matrix / (self.matrix.sum(0).reshape(1, self.nc + 1) + 1E-6) # normalize
|
||||
array[array < 0.005] = np.nan # don't annotate (would appear as 0.00)
|
||||
|
||||
fig = plt.figure(figsize=(12, 9), tight_layout=True)
|
||||
sn.set(font_scale=1.0 if self.nc < 50 else 0.8) # for label size
|
||||
labels = (0 < len(names) < 99) and len(names) == self.nc # apply names to ticklabels
|
||||
sn.heatmap(array, annot=self.nc < 30, annot_kws={"size": 8}, cmap='Blues', fmt='.2f', square=True,
|
||||
xticklabels=names + ['background FP'] if labels else "auto",
|
||||
yticklabels=names + ['background FN'] if labels else "auto").set_facecolor((1, 1, 1))
|
||||
fig.axes[0].set_xlabel('True')
|
||||
fig.axes[0].set_ylabel('Predicted')
|
||||
fig.savefig(Path(save_dir) / 'confusion_matrix.png', dpi=250)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def print(self):
|
||||
for i in range(self.nc + 1):
|
||||
print(' '.join(map(str, self.matrix[i])))
|
||||
|
||||
|
||||
# Plots ----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def plot_pr_curve(px, py, ap, save_dir='pr_curve.png', names=()):
|
||||
# Precision-recall curve
|
||||
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
|
||||
py = np.stack(py, axis=1)
|
||||
|
||||
if 0 < len(names) < 21: # display per-class legend if < 21 classes
|
||||
for i, y in enumerate(py.T):
|
||||
ax.plot(px, y, linewidth=1, label=f'{names[i]} {ap[i, 0]:.3f}') # plot(recall, precision)
|
||||
else:
|
||||
ax.plot(px, py, linewidth=1, color='grey') # plot(recall, precision)
|
||||
|
||||
ax.plot(px, py.mean(1), linewidth=3, color='blue', label='all classes %.3f mAP@0.5' % ap[:, 0].mean())
|
||||
ax.set_xlabel('Recall')
|
||||
ax.set_ylabel('Precision')
|
||||
ax.set_xlim(0, 1)
|
||||
ax.set_ylim(0, 1)
|
||||
plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
|
||||
fig.savefig(Path(save_dir), dpi=250)
|
||||
|
||||
|
||||
def plot_mc_curve(px, py, save_dir='mc_curve.png', names=(), xlabel='Confidence', ylabel='Metric'):
|
||||
# Metric-confidence curve
|
||||
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
|
||||
|
||||
if 0 < len(names) < 21: # display per-class legend if < 21 classes
|
||||
for i, y in enumerate(py):
|
||||
ax.plot(px, y, linewidth=1, label=f'{names[i]}') # plot(confidence, metric)
|
||||
else:
|
||||
ax.plot(px, py.T, linewidth=1, color='grey') # plot(confidence, metric)
|
||||
|
||||
y = py.mean(0)
|
||||
ax.plot(px, y, linewidth=3, color='blue', label=f'all classes {y.max():.2f} at {px[y.argmax()]:.3f}')
|
||||
ax.set_xlabel(xlabel)
|
||||
ax.set_ylabel(ylabel)
|
||||
ax.set_xlim(0, 1)
|
||||
ax.set_ylim(0, 1)
|
||||
plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
|
||||
fig.savefig(Path(save_dir), dpi=250)
|
||||
|
|
@ -0,0 +1,489 @@
|
|||
# Plotting utils
|
||||
|
||||
import glob
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import seaborn as sns
|
||||
import torch
|
||||
import yaml
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from scipy.signal import butter, filtfilt
|
||||
|
||||
from utils.general import xywh2xyxy, xyxy2xywh
|
||||
from utils.metrics import fitness
|
||||
|
||||
# Settings
|
||||
matplotlib.rc('font', **{'size': 11})
|
||||
matplotlib.use('Agg') # for writing to files only
|
||||
|
||||
|
||||
def color_list():
|
||||
# Return first 10 plt colors as (r,g,b) https://stackoverflow.com/questions/51350872/python-from-color-name-to-rgb
|
||||
def hex2rgb(h):
|
||||
return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))
|
||||
|
||||
return [hex2rgb(h) for h in matplotlib.colors.TABLEAU_COLORS.values()] # or BASE_ (8), CSS4_ (148), XKCD_ (949)
|
||||
|
||||
|
||||
def hist2d(x, y, n=100):
|
||||
# 2d histogram used in labels.png and evolve.png
|
||||
xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n)
|
||||
hist, xedges, yedges = np.histogram2d(x, y, (xedges, yedges))
|
||||
xidx = np.clip(np.digitize(x, xedges) - 1, 0, hist.shape[0] - 1)
|
||||
yidx = np.clip(np.digitize(y, yedges) - 1, 0, hist.shape[1] - 1)
|
||||
return np.log(hist[xidx, yidx])
|
||||
|
||||
|
||||
def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5):
|
||||
# https://stackoverflow.com/questions/28536191/how-to-filter-smooth-with-scipy-numpy
|
||||
def butter_lowpass(cutoff, fs, order):
|
||||
nyq = 0.5 * fs
|
||||
normal_cutoff = cutoff / nyq
|
||||
return butter(order, normal_cutoff, btype='low', analog=False)
|
||||
|
||||
b, a = butter_lowpass(cutoff, fs, order=order)
|
||||
return filtfilt(b, a, data) # forward-backward filter
|
||||
|
||||
|
||||
def plot_one_box(x, img, color=None, label=None, line_thickness=3):
|
||||
# Plots one bounding box on image img
|
||||
tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 # line/font thickness
|
||||
color = color or [random.randint(0, 255) for _ in range(3)]
|
||||
c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
|
||||
cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
|
||||
if label:
|
||||
tf = max(tl - 1, 1) # font thickness
|
||||
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
||||
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
|
||||
cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
|
||||
cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)
|
||||
|
||||
|
||||
def plot_one_box_PIL(box, img, color=None, label=None, line_thickness=None):
|
||||
img = Image.fromarray(img)
|
||||
draw = ImageDraw.Draw(img)
|
||||
line_thickness = line_thickness or max(int(min(img.size) / 200), 2)
|
||||
draw.rectangle(box, width=line_thickness, outline=tuple(color)) # plot
|
||||
if label:
|
||||
fontsize = max(round(max(img.size) / 40), 12)
|
||||
font = ImageFont.truetype("Arial.ttf", fontsize)
|
||||
txt_width, txt_height = font.getsize(label)
|
||||
draw.rectangle([box[0], box[1] - txt_height + 4, box[0] + txt_width, box[1]], fill=tuple(color))
|
||||
draw.text((box[0], box[1] - txt_height + 1), label, fill=(255, 255, 255), font=font)
|
||||
return np.asarray(img)
|
||||
|
||||
|
||||
def plot_wh_methods(): # from utils.plots import *; plot_wh_methods()
|
||||
# Compares the two methods for width-height anchor multiplication
|
||||
# https://github.com/ultralytics/yolov3/issues/168
|
||||
x = np.arange(-4.0, 4.0, .1)
|
||||
ya = np.exp(x)
|
||||
yb = torch.sigmoid(torch.from_numpy(x)).numpy() * 2
|
||||
|
||||
fig = plt.figure(figsize=(6, 3), tight_layout=True)
|
||||
plt.plot(x, ya, '.-', label='YOLOv3')
|
||||
plt.plot(x, yb ** 2, '.-', label='YOLOR ^2')
|
||||
plt.plot(x, yb ** 1.6, '.-', label='YOLOR ^1.6')
|
||||
plt.xlim(left=-4, right=4)
|
||||
plt.ylim(bottom=0, top=6)
|
||||
plt.xlabel('input')
|
||||
plt.ylabel('output')
|
||||
plt.grid()
|
||||
plt.legend()
|
||||
fig.savefig('comparison.png', dpi=200)
|
||||
|
||||
|
||||
def output_to_target(output):
|
||||
# Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
|
||||
targets = []
|
||||
for i, o in enumerate(output):
|
||||
for *box, conf, cls in o.cpu().numpy():
|
||||
targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf])
|
||||
return np.array(targets)
|
||||
|
||||
|
||||
def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=640, max_subplots=16):
|
||||
# Plot image grid with labels
|
||||
|
||||
if isinstance(images, torch.Tensor):
|
||||
images = images.cpu().float().numpy()
|
||||
if isinstance(targets, torch.Tensor):
|
||||
targets = targets.cpu().numpy()
|
||||
|
||||
# un-normalise
|
||||
if np.max(images[0]) <= 1:
|
||||
images *= 255
|
||||
|
||||
tl = 3 # line thickness
|
||||
tf = max(tl - 1, 1) # font thickness
|
||||
bs, _, h, w = images.shape # batch size, _, height, width
|
||||
bs = min(bs, max_subplots) # limit plot images
|
||||
ns = np.ceil(bs ** 0.5) # number of subplots (square)
|
||||
|
||||
# Check if we should resize
|
||||
scale_factor = max_size / max(h, w)
|
||||
if scale_factor < 1:
|
||||
h = math.ceil(scale_factor * h)
|
||||
w = math.ceil(scale_factor * w)
|
||||
|
||||
colors = color_list() # list of colors
|
||||
mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) # init
|
||||
for i, img in enumerate(images):
|
||||
if i == max_subplots: # if last batch has fewer images than we expect
|
||||
break
|
||||
|
||||
block_x = int(w * (i // ns))
|
||||
block_y = int(h * (i % ns))
|
||||
|
||||
img = img.transpose(1, 2, 0)
|
||||
if scale_factor < 1:
|
||||
img = cv2.resize(img, (w, h))
|
||||
|
||||
mosaic[block_y:block_y + h, block_x:block_x + w, :] = img
|
||||
if len(targets) > 0:
|
||||
image_targets = targets[targets[:, 0] == i]
|
||||
boxes = xywh2xyxy(image_targets[:, 2:6]).T
|
||||
classes = image_targets[:, 1].astype('int')
|
||||
labels = image_targets.shape[1] == 6 # labels if no conf column
|
||||
conf = None if labels else image_targets[:, 6] # check for confidence presence (label vs pred)
|
||||
|
||||
if boxes.shape[1]:
|
||||
if boxes.max() <= 1.01: # if normalized with tolerance 0.01
|
||||
boxes[[0, 2]] *= w # scale to pixels
|
||||
boxes[[1, 3]] *= h
|
||||
elif scale_factor < 1: # absolute coords need scale if image scales
|
||||
boxes *= scale_factor
|
||||
boxes[[0, 2]] += block_x
|
||||
boxes[[1, 3]] += block_y
|
||||
for j, box in enumerate(boxes.T):
|
||||
cls = int(classes[j])
|
||||
color = colors[cls % len(colors)]
|
||||
cls = names[cls] if names else cls
|
||||
if labels or conf[j] > 0.25: # 0.25 conf thresh
|
||||
label = '%s' % cls if labels else '%s %.1f' % (cls, conf[j])
|
||||
plot_one_box(box, mosaic, label=label, color=color, line_thickness=tl)
|
||||
|
||||
# Draw image filename labels
|
||||
if paths:
|
||||
label = Path(paths[i]).name[:40] # trim to 40 char
|
||||
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
||||
cv2.putText(mosaic, label, (block_x + 5, block_y + t_size[1] + 5), 0, tl / 3, [220, 220, 220], thickness=tf,
|
||||
lineType=cv2.LINE_AA)
|
||||
|
||||
# Image border
|
||||
cv2.rectangle(mosaic, (block_x, block_y), (block_x + w, block_y + h), (255, 255, 255), thickness=3)
|
||||
|
||||
if fname:
|
||||
r = min(1280. / max(h, w) / ns, 1.0) # ratio to limit image size
|
||||
mosaic = cv2.resize(mosaic, (int(ns * w * r), int(ns * h * r)), interpolation=cv2.INTER_AREA)
|
||||
# cv2.imwrite(fname, cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB)) # cv2 save
|
||||
Image.fromarray(mosaic).save(fname) # PIL save
|
||||
return mosaic
|
||||
|
||||
|
||||
def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''):
|
||||
# Plot LR simulating training for full epochs
|
||||
optimizer, scheduler = copy(optimizer), copy(scheduler) # do not modify originals
|
||||
y = []
|
||||
for _ in range(epochs):
|
||||
scheduler.step()
|
||||
y.append(optimizer.param_groups[0]['lr'])
|
||||
plt.plot(y, '.-', label='LR')
|
||||
plt.xlabel('epoch')
|
||||
plt.ylabel('LR')
|
||||
plt.grid()
|
||||
plt.xlim(0, epochs)
|
||||
plt.ylim(0)
|
||||
plt.savefig(Path(save_dir) / 'LR.png', dpi=200)
|
||||
plt.close()
|
||||
|
||||
|
||||
def plot_test_txt(): # from utils.plots import *; plot_test()
|
||||
# Plot test.txt histograms
|
||||
x = np.loadtxt('test.txt', dtype=np.float32)
|
||||
box = xyxy2xywh(x[:, :4])
|
||||
cx, cy = box[:, 0], box[:, 1]
|
||||
|
||||
fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True)
|
||||
ax.hist2d(cx, cy, bins=600, cmax=10, cmin=0)
|
||||
ax.set_aspect('equal')
|
||||
plt.savefig('hist2d.png', dpi=300)
|
||||
|
||||
fig, ax = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True)
|
||||
ax[0].hist(cx, bins=600)
|
||||
ax[1].hist(cy, bins=600)
|
||||
plt.savefig('hist1d.png', dpi=200)
|
||||
|
||||
|
||||
def plot_targets_txt(): # from utils.plots import *; plot_targets_txt()
|
||||
# Plot targets.txt histograms
|
||||
x = np.loadtxt('targets.txt', dtype=np.float32).T
|
||||
s = ['x targets', 'y targets', 'width targets', 'height targets']
|
||||
fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
|
||||
ax = ax.ravel()
|
||||
for i in range(4):
|
||||
ax[i].hist(x[i], bins=100, label='%.3g +/- %.3g' % (x[i].mean(), x[i].std()))
|
||||
ax[i].legend()
|
||||
ax[i].set_title(s[i])
|
||||
plt.savefig('targets.jpg', dpi=200)
|
||||
|
||||
|
||||
def plot_study_txt(path='', x=None): # from utils.plots import *; plot_study_txt()
|
||||
# Plot study.txt generated by test.py
|
||||
fig, ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True)
|
||||
# ax = ax.ravel()
|
||||
|
||||
fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True)
|
||||
# for f in [Path(path) / f'study_coco_{x}.txt' for x in ['yolor-p6', 'yolor-w6', 'yolor-e6', 'yolor-d6']]:
|
||||
for f in sorted(Path(path).glob('study*.txt')):
|
||||
y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T
|
||||
x = np.arange(y.shape[1]) if x is None else np.array(x)
|
||||
s = ['P', 'R', 'mAP@.5', 'mAP@.5:.95', 't_inference (ms/img)', 't_NMS (ms/img)', 't_total (ms/img)']
|
||||
# for i in range(7):
|
||||
# ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8)
|
||||
# ax[i].set_title(s[i])
|
||||
|
||||
j = y[3].argmax() + 1
|
||||
ax2.plot(y[6, 1:j], y[3, 1:j] * 1E2, '.-', linewidth=2, markersize=8,
|
||||
label=f.stem.replace('study_coco_', '').replace('yolo', 'YOLO'))
|
||||
|
||||
ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5],
|
||||
'k.-', linewidth=2, markersize=8, alpha=.25, label='EfficientDet')
|
||||
|
||||
ax2.grid(alpha=0.2)
|
||||
ax2.set_yticks(np.arange(20, 60, 5))
|
||||
ax2.set_xlim(0, 57)
|
||||
ax2.set_ylim(30, 55)
|
||||
ax2.set_xlabel('GPU Speed (ms/img)')
|
||||
ax2.set_ylabel('COCO AP val')
|
||||
ax2.legend(loc='lower right')
|
||||
plt.savefig(str(Path(path).name) + '.png', dpi=300)
|
||||
|
||||
|
||||
def plot_labels(labels, names=(), save_dir=Path(''), loggers=None):
|
||||
# plot dataset labels
|
||||
print('Plotting labels... ')
|
||||
c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes
|
||||
nc = int(c.max() + 1) # number of classes
|
||||
colors = color_list()
|
||||
x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height'])
|
||||
|
||||
# seaborn correlogram
|
||||
sns.pairplot(x, corner=True, diag_kind='auto', kind='hist', diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9))
|
||||
plt.savefig(save_dir / 'labels_correlogram.jpg', dpi=200)
|
||||
plt.close()
|
||||
|
||||
# matplotlib labels
|
||||
matplotlib.use('svg') # faster
|
||||
ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel()
|
||||
ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8)
|
||||
ax[0].set_ylabel('instances')
|
||||
if 0 < len(names) < 30:
|
||||
ax[0].set_xticks(range(len(names)))
|
||||
ax[0].set_xticklabels(names, rotation=90, fontsize=10)
|
||||
else:
|
||||
ax[0].set_xlabel('classes')
|
||||
sns.histplot(x, x='x', y='y', ax=ax[2], bins=50, pmax=0.9)
|
||||
sns.histplot(x, x='width', y='height', ax=ax[3], bins=50, pmax=0.9)
|
||||
|
||||
# rectangles
|
||||
labels[:, 1:3] = 0.5 # center
|
||||
labels[:, 1:] = xywh2xyxy(labels[:, 1:]) * 2000
|
||||
img = Image.fromarray(np.ones((2000, 2000, 3), dtype=np.uint8) * 255)
|
||||
for cls, *box in labels[:1000]:
|
||||
ImageDraw.Draw(img).rectangle(box, width=1, outline=colors[int(cls) % 10]) # plot
|
||||
ax[1].imshow(img)
|
||||
ax[1].axis('off')
|
||||
|
||||
for a in [0, 1, 2, 3]:
|
||||
for s in ['top', 'right', 'left', 'bottom']:
|
||||
ax[a].spines[s].set_visible(False)
|
||||
|
||||
plt.savefig(save_dir / 'labels.jpg', dpi=200)
|
||||
matplotlib.use('Agg')
|
||||
plt.close()
|
||||
|
||||
# loggers
|
||||
for k, v in loggers.items() or {}:
|
||||
if k == 'wandb' and v:
|
||||
v.log({"Labels": [v.Image(str(x), caption=x.name) for x in save_dir.glob('*labels*.jpg')]}, commit=False)
|
||||
|
||||
|
||||
def plot_evolution(yaml_file='data/hyp.finetune.yaml'): # from utils.plots import *; plot_evolution()
|
||||
# Plot hyperparameter evolution results in evolve.txt
|
||||
with open(yaml_file) as f:
|
||||
hyp = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
x = np.loadtxt('evolve.txt', ndmin=2)
|
||||
f = fitness(x)
|
||||
# weights = (f - f.min()) ** 2 # for weighted results
|
||||
plt.figure(figsize=(10, 12), tight_layout=True)
|
||||
matplotlib.rc('font', **{'size': 8})
|
||||
for i, (k, v) in enumerate(hyp.items()):
|
||||
y = x[:, i + 7]
|
||||
# mu = (y * weights).sum() / weights.sum() # best weighted result
|
||||
mu = y[f.argmax()] # best single result
|
||||
plt.subplot(6, 5, i + 1)
|
||||
plt.scatter(y, f, c=hist2d(y, f, 20), cmap='viridis', alpha=.8, edgecolors='none')
|
||||
plt.plot(mu, f.max(), 'k+', markersize=15)
|
||||
plt.title('%s = %.3g' % (k, mu), fontdict={'size': 9}) # limit to 40 characters
|
||||
if i % 5 != 0:
|
||||
plt.yticks([])
|
||||
print('%15s: %.3g' % (k, mu))
|
||||
plt.savefig('evolve.png', dpi=200)
|
||||
print('\nPlot saved as evolve.png')
|
||||
|
||||
|
||||
def profile_idetection(start=0, stop=0, labels=(), save_dir=''):
|
||||
# Plot iDetection '*.txt' per-image logs. from utils.plots import *; profile_idetection()
|
||||
ax = plt.subplots(2, 4, figsize=(12, 6), tight_layout=True)[1].ravel()
|
||||
s = ['Images', 'Free Storage (GB)', 'RAM Usage (GB)', 'Battery', 'dt_raw (ms)', 'dt_smooth (ms)', 'real-world FPS']
|
||||
files = list(Path(save_dir).glob('frames*.txt'))
|
||||
for fi, f in enumerate(files):
|
||||
try:
|
||||
results = np.loadtxt(f, ndmin=2).T[:, 90:-30] # clip first and last rows
|
||||
n = results.shape[1] # number of rows
|
||||
x = np.arange(start, min(stop, n) if stop else n)
|
||||
results = results[:, x]
|
||||
t = (results[0] - results[0].min()) # set t0=0s
|
||||
results[0] = x
|
||||
for i, a in enumerate(ax):
|
||||
if i < len(results):
|
||||
label = labels[fi] if len(labels) else f.stem.replace('frames_', '')
|
||||
a.plot(t, results[i], marker='.', label=label, linewidth=1, markersize=5)
|
||||
a.set_title(s[i])
|
||||
a.set_xlabel('time (s)')
|
||||
# if fi == len(files) - 1:
|
||||
# a.set_ylim(bottom=0)
|
||||
for side in ['top', 'right']:
|
||||
a.spines[side].set_visible(False)
|
||||
else:
|
||||
a.remove()
|
||||
except Exception as e:
|
||||
print('Warning: Plotting error for %s; %s' % (f, e))
|
||||
|
||||
ax[1].legend()
|
||||
plt.savefig(Path(save_dir) / 'idetection_profile.png', dpi=200)
|
||||
|
||||
|
||||
def plot_results_overlay(start=0, stop=0): # from utils.plots import *; plot_results_overlay()
|
||||
# Plot training 'results*.txt', overlaying train and val losses
|
||||
s = ['train', 'train', 'train', 'Precision', 'mAP@0.5', 'val', 'val', 'val', 'Recall', 'mAP@0.5:0.95'] # legends
|
||||
t = ['Box', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles
|
||||
for f in sorted(glob.glob('results*.txt') + glob.glob('../../Downloads/results*.txt')):
|
||||
results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
|
||||
n = results.shape[1] # number of rows
|
||||
x = range(start, min(stop, n) if stop else n)
|
||||
fig, ax = plt.subplots(1, 5, figsize=(14, 3.5), tight_layout=True)
|
||||
ax = ax.ravel()
|
||||
for i in range(5):
|
||||
for j in [i, i + 5]:
|
||||
y = results[j, x]
|
||||
ax[i].plot(x, y, marker='.', label=s[j])
|
||||
# y_smooth = butter_lowpass_filtfilt(y)
|
||||
# ax[i].plot(x, np.gradient(y_smooth), marker='.', label=s[j])
|
||||
|
||||
ax[i].set_title(t[i])
|
||||
ax[i].legend()
|
||||
ax[i].set_ylabel(f) if i == 0 else None # add filename
|
||||
fig.savefig(f.replace('.txt', '.png'), dpi=200)
|
||||
|
||||
|
||||
def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''):
|
||||
# Plot training 'results*.txt'. from utils.plots import *; plot_results(save_dir='runs/train/exp')
|
||||
fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True)
|
||||
ax = ax.ravel()
|
||||
s = ['Box', 'Objectness', 'Classification', 'Precision', 'Recall',
|
||||
'val Box', 'val Objectness', 'val Classification', 'mAP@0.5', 'mAP@0.5:0.95']
|
||||
if bucket:
|
||||
# files = ['https://storage.googleapis.com/%s/results%g.txt' % (bucket, x) for x in id]
|
||||
files = ['results%g.txt' % x for x in id]
|
||||
c = ('gsutil cp ' + '%s ' * len(files) + '.') % tuple('gs://%s/results%g.txt' % (bucket, x) for x in id)
|
||||
os.system(c)
|
||||
else:
|
||||
files = list(Path(save_dir).glob('results*.txt'))
|
||||
assert len(files), 'No results.txt files found in %s, nothing to plot.' % os.path.abspath(save_dir)
|
||||
for fi, f in enumerate(files):
|
||||
try:
|
||||
results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
|
||||
n = results.shape[1] # number of rows
|
||||
x = range(start, min(stop, n) if stop else n)
|
||||
for i in range(10):
|
||||
y = results[i, x]
|
||||
if i in [0, 1, 2, 5, 6, 7]:
|
||||
y[y == 0] = np.nan # don't show zero loss values
|
||||
# y /= y[0] # normalize
|
||||
label = labels[fi] if len(labels) else f.stem
|
||||
ax[i].plot(x, y, marker='.', label=label, linewidth=2, markersize=8)
|
||||
ax[i].set_title(s[i])
|
||||
# if i in [5, 6, 7]: # share train and val loss y axes
|
||||
# ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
|
||||
except Exception as e:
|
||||
print('Warning: Plotting error for %s; %s' % (f, e))
|
||||
|
||||
ax[1].legend()
|
||||
fig.savefig(Path(save_dir) / 'results.png', dpi=200)
|
||||
|
||||
|
||||
def output_to_keypoint(output):
|
||||
# Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
|
||||
targets = []
|
||||
for i, o in enumerate(output):
|
||||
kpts = o[:,6:]
|
||||
o = o[:,:6]
|
||||
for index, (*box, conf, cls) in enumerate(o.detach().cpu().numpy()):
|
||||
targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf, *list(kpts.detach().cpu().numpy()[index])])
|
||||
return np.array(targets)
|
||||
|
||||
|
||||
def plot_skeleton_kpts(im, kpts, steps, orig_shape=None):
|
||||
#Plot the skeleton and keypointsfor coco datatset
|
||||
palette = np.array([[255, 128, 0], [255, 153, 51], [255, 178, 102],
|
||||
[230, 230, 0], [255, 153, 255], [153, 204, 255],
|
||||
[255, 102, 255], [255, 51, 255], [102, 178, 255],
|
||||
[51, 153, 255], [255, 153, 153], [255, 102, 102],
|
||||
[255, 51, 51], [153, 255, 153], [102, 255, 102],
|
||||
[51, 255, 51], [0, 255, 0], [0, 0, 255], [255, 0, 0],
|
||||
[255, 255, 255]])
|
||||
|
||||
skeleton = [[16, 14], [14, 12], [17, 15], [15, 13], [12, 13], [6, 12],
|
||||
[7, 13], [6, 7], [6, 8], [7, 9], [8, 10], [9, 11], [2, 3],
|
||||
[1, 2], [1, 3], [2, 4], [3, 5], [4, 6], [5, 7]]
|
||||
|
||||
pose_limb_color = palette[[9, 9, 9, 9, 7, 7, 7, 0, 0, 0, 0, 0, 16, 16, 16, 16, 16, 16, 16]]
|
||||
pose_kpt_color = palette[[16, 16, 16, 16, 16, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9]]
|
||||
radius = 5
|
||||
num_kpts = len(kpts) // steps
|
||||
|
||||
for kid in range(num_kpts):
|
||||
r, g, b = pose_kpt_color[kid]
|
||||
x_coord, y_coord = kpts[steps * kid], kpts[steps * kid + 1]
|
||||
if not (x_coord % 640 == 0 or y_coord % 640 == 0):
|
||||
if steps == 3:
|
||||
conf = kpts[steps * kid + 2]
|
||||
if conf < 0.5:
|
||||
continue
|
||||
cv2.circle(im, (int(x_coord), int(y_coord)), radius, (int(r), int(g), int(b)), -1)
|
||||
|
||||
for sk_id, sk in enumerate(skeleton):
|
||||
r, g, b = pose_limb_color[sk_id]
|
||||
pos1 = (int(kpts[(sk[0]-1)*steps]), int(kpts[(sk[0]-1)*steps+1]))
|
||||
pos2 = (int(kpts[(sk[1]-1)*steps]), int(kpts[(sk[1]-1)*steps+1]))
|
||||
if steps == 3:
|
||||
conf1 = kpts[(sk[0]-1)*steps+2]
|
||||
conf2 = kpts[(sk[1]-1)*steps+2]
|
||||
if conf1<0.5 or conf2<0.5:
|
||||
continue
|
||||
if pos1[0]%640 == 0 or pos1[1]%640==0 or pos1[0]<0 or pos1[1]<0:
|
||||
continue
|
||||
if pos2[0] % 640 == 0 or pos2[1] % 640 == 0 or pos2[0]<0 or pos2[1]<0:
|
||||
continue
|
||||
cv2.line(im, pos1, pos2, (int(r), int(g), int(b)), thickness=2)
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
# YOLOR PyTorch utils
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import torch.backends.cudnn as cudnn
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torchvision
|
||||
|
||||
try:
|
||||
import thop # for FLOPS computation
|
||||
except ImportError:
|
||||
thop = None
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def torch_distributed_zero_first(local_rank: int):
|
||||
"""
|
||||
Decorator to make all processes in distributed training wait for each local_master to do something.
|
||||
"""
|
||||
if local_rank not in [-1, 0]:
|
||||
torch.distributed.barrier()
|
||||
yield
|
||||
if local_rank == 0:
|
||||
torch.distributed.barrier()
|
||||
|
||||
|
||||
def init_torch_seeds(seed=0):
|
||||
# Speed-reproducibility tradeoff https://pytorch.org/docs/stable/notes/randomness.html
|
||||
torch.manual_seed(seed)
|
||||
if seed == 0: # slower, more reproducible
|
||||
cudnn.benchmark, cudnn.deterministic = False, True
|
||||
else: # faster, less reproducible
|
||||
cudnn.benchmark, cudnn.deterministic = True, False
|
||||
|
||||
|
||||
def date_modified(path=__file__):
|
||||
# return human-readable file modification date, i.e. '2021-3-26'
|
||||
t = datetime.datetime.fromtimestamp(Path(path).stat().st_mtime)
|
||||
return f'{t.year}-{t.month}-{t.day}'
|
||||
|
||||
|
||||
def git_describe(path=Path(__file__).parent): # path must be a directory
|
||||
# return human-readable git description, i.e. v5.0-5-g3e25f1e https://git-scm.com/docs/git-describe
|
||||
s = f'git -C {path} describe --tags --long --always'
|
||||
try:
|
||||
return subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).decode()[:-1]
|
||||
except subprocess.CalledProcessError as e:
|
||||
return '' # not a git repository
|
||||
|
||||
|
||||
def select_device(device='', batch_size=None):
|
||||
# device = 'cpu' or '0' or '0,1,2,3'
|
||||
s = f'YOLOR 🚀 {git_describe() or date_modified()} torch {torch.__version__} ' # string
|
||||
cpu = device.lower() == 'cpu'
|
||||
if cpu:
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # force torch.cuda.is_available() = False
|
||||
elif device: # non-cpu device requested
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = device # set environment variable
|
||||
assert torch.cuda.is_available(), f'CUDA unavailable, invalid device {device} requested' # check availability
|
||||
|
||||
cuda = not cpu and torch.cuda.is_available()
|
||||
if cuda:
|
||||
n = torch.cuda.device_count()
|
||||
if n > 1 and batch_size: # check that batch_size is compatible with device_count
|
||||
assert batch_size % n == 0, f'batch-size {batch_size} not multiple of GPU count {n}'
|
||||
space = ' ' * len(s)
|
||||
for i, d in enumerate(device.split(',') if device else range(n)):
|
||||
p = torch.cuda.get_device_properties(i)
|
||||
s += f"{'' if i == 0 else space}CUDA:{d} ({p.name}, {p.total_memory / 1024 ** 2}MB)\n" # bytes to MB
|
||||
else:
|
||||
s += 'CPU\n'
|
||||
|
||||
logger.info(s.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else s) # emoji-safe
|
||||
return torch.device('cuda:0' if cuda else 'cpu')
|
||||
|
||||
|
||||
def time_synchronized():
|
||||
# pytorch-accurate time
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.synchronize()
|
||||
return time.time()
|
||||
|
||||
|
||||
def profile(x, ops, n=100, device=None):
|
||||
# profile a pytorch module or list of modules. Example usage:
|
||||
# x = torch.randn(16, 3, 640, 640) # input
|
||||
# m1 = lambda x: x * torch.sigmoid(x)
|
||||
# m2 = nn.SiLU()
|
||||
# profile(x, [m1, m2], n=100) # profile speed over 100 iterations
|
||||
|
||||
device = device or torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
|
||||
x = x.to(device)
|
||||
x.requires_grad = True
|
||||
print(torch.__version__, device.type, torch.cuda.get_device_properties(0) if device.type == 'cuda' else '')
|
||||
print(f"\n{'Params':>12s}{'GFLOPS':>12s}{'forward (ms)':>16s}{'backward (ms)':>16s}{'input':>24s}{'output':>24s}")
|
||||
for m in ops if isinstance(ops, list) else [ops]:
|
||||
m = m.to(device) if hasattr(m, 'to') else m # device
|
||||
m = m.half() if hasattr(m, 'half') and isinstance(x, torch.Tensor) and x.dtype is torch.float16 else m # type
|
||||
dtf, dtb, t = 0., 0., [0., 0., 0.] # dt forward, backward
|
||||
try:
|
||||
flops = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 # GFLOPS
|
||||
except:
|
||||
flops = 0
|
||||
|
||||
for _ in range(n):
|
||||
t[0] = time_synchronized()
|
||||
y = m(x)
|
||||
t[1] = time_synchronized()
|
||||
try:
|
||||
_ = y.sum().backward()
|
||||
t[2] = time_synchronized()
|
||||
except: # no backward method
|
||||
t[2] = float('nan')
|
||||
dtf += (t[1] - t[0]) * 1000 / n # ms per op forward
|
||||
dtb += (t[2] - t[1]) * 1000 / n # ms per op backward
|
||||
|
||||
s_in = tuple(x.shape) if isinstance(x, torch.Tensor) else 'list'
|
||||
s_out = tuple(y.shape) if isinstance(y, torch.Tensor) else 'list'
|
||||
p = sum(list(x.numel() for x in m.parameters())) if isinstance(m, nn.Module) else 0 # parameters
|
||||
print(f'{p:12}{flops:12.4g}{dtf:16.4g}{dtb:16.4g}{str(s_in):>24s}{str(s_out):>24s}')
|
||||
|
||||
|
||||
def is_parallel(model):
|
||||
return type(model) in (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel)
|
||||
|
||||
|
||||
def intersect_dicts(da, db, exclude=()):
|
||||
# Dictionary intersection of matching keys and shapes, omitting 'exclude' keys, using da values
|
||||
return {k: v for k, v in da.items() if k in db and not any(x in k for x in exclude) and v.shape == db[k].shape}
|
||||
|
||||
|
||||
def initialize_weights(model):
|
||||
for m in model.modules():
|
||||
t = type(m)
|
||||
if t is nn.Conv2d:
|
||||
pass # nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
||||
elif t is nn.BatchNorm2d:
|
||||
m.eps = 1e-3
|
||||
m.momentum = 0.03
|
||||
elif t in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6]:
|
||||
m.inplace = True
|
||||
|
||||
|
||||
def find_modules(model, mclass=nn.Conv2d):
|
||||
# Finds layer indices matching module class 'mclass'
|
||||
return [i for i, m in enumerate(model.module_list) if isinstance(m, mclass)]
|
||||
|
||||
|
||||
def sparsity(model):
|
||||
# Return global model sparsity
|
||||
a, b = 0., 0.
|
||||
for p in model.parameters():
|
||||
a += p.numel()
|
||||
b += (p == 0).sum()
|
||||
return b / a
|
||||
|
||||
|
||||
def prune(model, amount=0.3):
|
||||
# Prune model to requested global sparsity
|
||||
import torch.nn.utils.prune as prune
|
||||
print('Pruning model... ', end='')
|
||||
for name, m in model.named_modules():
|
||||
if isinstance(m, nn.Conv2d):
|
||||
prune.l1_unstructured(m, name='weight', amount=amount) # prune
|
||||
prune.remove(m, 'weight') # make permanent
|
||||
print(' %.3g global sparsity' % sparsity(model))
|
||||
|
||||
|
||||
def fuse_conv_and_bn(conv, bn):
|
||||
# Fuse convolution and batchnorm layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/
|
||||
fusedconv = nn.Conv2d(conv.in_channels,
|
||||
conv.out_channels,
|
||||
kernel_size=conv.kernel_size,
|
||||
stride=conv.stride,
|
||||
padding=conv.padding,
|
||||
groups=conv.groups,
|
||||
bias=True).requires_grad_(False).to(conv.weight.device)
|
||||
|
||||
# prepare filters
|
||||
w_conv = conv.weight.clone().view(conv.out_channels, -1)
|
||||
w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps + bn.running_var)))
|
||||
fusedconv.weight.copy_(torch.mm(w_bn, w_conv).view(fusedconv.weight.shape))
|
||||
|
||||
# prepare spatial bias
|
||||
b_conv = torch.zeros(conv.weight.size(0), device=conv.weight.device) if conv.bias is None else conv.bias
|
||||
b_bn = bn.bias - bn.weight.mul(bn.running_mean).div(torch.sqrt(bn.running_var + bn.eps))
|
||||
fusedconv.bias.copy_(torch.mm(w_bn, b_conv.reshape(-1, 1)).reshape(-1) + b_bn)
|
||||
|
||||
return fusedconv
|
||||
|
||||
|
||||
def model_info(model, verbose=False, img_size=640):
|
||||
# Model information. img_size may be int or list, i.e. img_size=640 or img_size=[640, 320]
|
||||
n_p = sum(x.numel() for x in model.parameters()) # number parameters
|
||||
n_g = sum(x.numel() for x in model.parameters() if x.requires_grad) # number gradients
|
||||
if verbose:
|
||||
print('%5s %40s %9s %12s %20s %10s %10s' % ('layer', 'name', 'gradient', 'parameters', 'shape', 'mu', 'sigma'))
|
||||
for i, (name, p) in enumerate(model.named_parameters()):
|
||||
name = name.replace('module_list.', '')
|
||||
print('%5g %40s %9s %12g %20s %10.3g %10.3g' %
|
||||
(i, name, p.requires_grad, p.numel(), list(p.shape), p.mean(), p.std()))
|
||||
|
||||
try: # FLOPS
|
||||
from thop import profile
|
||||
stride = max(int(model.stride.max()), 32) if hasattr(model, 'stride') else 32
|
||||
img = torch.zeros((1, model.yaml.get('ch', 3), stride, stride), device=next(model.parameters()).device) # input
|
||||
flops = profile(deepcopy(model), inputs=(img,), verbose=False)[0] / 1E9 * 2 # stride GFLOPS
|
||||
img_size = img_size if isinstance(img_size, list) else [img_size, img_size] # expand if int/float
|
||||
fs = ', %.1f GFLOPS' % (flops * img_size[0] / stride * img_size[1] / stride) # 640x640 GFLOPS
|
||||
except (ImportError, Exception):
|
||||
fs = ''
|
||||
|
||||
logger.info(f"Model Summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}")
|
||||
|
||||
|
||||
def load_classifier(name='resnet101', n=2):
|
||||
# Loads a pretrained model reshaped to n-class output
|
||||
model = torchvision.models.__dict__[name](pretrained=True)
|
||||
|
||||
# ResNet model properties
|
||||
# input_size = [3, 224, 224]
|
||||
# input_space = 'RGB'
|
||||
# input_range = [0, 1]
|
||||
# mean = [0.485, 0.456, 0.406]
|
||||
# std = [0.229, 0.224, 0.225]
|
||||
|
||||
# Reshape output to n classes
|
||||
filters = model.fc.weight.shape[1]
|
||||
model.fc.bias = nn.Parameter(torch.zeros(n), requires_grad=True)
|
||||
model.fc.weight = nn.Parameter(torch.zeros(n, filters), requires_grad=True)
|
||||
model.fc.out_features = n
|
||||
return model
|
||||
|
||||
|
||||
def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416)
|
||||
# scales img(bs,3,y,x) by ratio constrained to gs-multiple
|
||||
if ratio == 1.0:
|
||||
return img
|
||||
else:
|
||||
h, w = img.shape[2:]
|
||||
s = (int(h * ratio), int(w * ratio)) # new size
|
||||
img = F.interpolate(img, size=s, mode='bilinear', align_corners=False) # resize
|
||||
if not same_shape: # pad/crop img
|
||||
h, w = [math.ceil(x * ratio / gs) * gs for x in (h, w)]
|
||||
return F.pad(img, [0, w - s[1], 0, h - s[0]], value=0.447) # value = imagenet mean
|
||||
|
||||
|
||||
def copy_attr(a, b, include=(), exclude=()):
|
||||
# Copy attributes from b to a, options to only include [...] and to exclude [...]
|
||||
for k, v in b.__dict__.items():
|
||||
if (len(include) and k not in include) or k.startswith('_') or k in exclude:
|
||||
continue
|
||||
else:
|
||||
setattr(a, k, v)
|
||||
|
||||
|
||||
class ModelEMA:
|
||||
""" Model Exponential Moving Average from https://github.com/rwightman/pytorch-image-models
|
||||
Keep a moving average of everything in the model state_dict (parameters and buffers).
|
||||
This is intended to allow functionality like
|
||||
https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage
|
||||
A smoothed version of the weights is necessary for some training schemes to perform well.
|
||||
This class is sensitive where it is initialized in the sequence of model init,
|
||||
GPU assignment and distributed training wrappers.
|
||||
"""
|
||||
|
||||
def __init__(self, model, decay=0.9999, updates=0):
|
||||
# Create EMA
|
||||
self.ema = deepcopy(model.module if is_parallel(model) else model).eval() # FP32 EMA
|
||||
# if next(model.parameters()).device.type != 'cpu':
|
||||
# self.ema.half() # FP16 EMA
|
||||
self.updates = updates # number of EMA updates
|
||||
self.decay = lambda x: decay * (1 - math.exp(-x / 2000)) # decay exponential ramp (to help early epochs)
|
||||
for p in self.ema.parameters():
|
||||
p.requires_grad_(False)
|
||||
|
||||
def update(self, model):
|
||||
# Update EMA parameters
|
||||
with torch.no_grad():
|
||||
self.updates += 1
|
||||
d = self.decay(self.updates)
|
||||
|
||||
msd = model.module.state_dict() if is_parallel(model) else model.state_dict() # model state_dict
|
||||
for k, v in self.ema.state_dict().items():
|
||||
if v.dtype.is_floating_point:
|
||||
v *= d
|
||||
v += (1. - d) * msd[k].detach()
|
||||
|
||||
def update_attr(self, model, include=(), exclude=('process_group', 'reducer')):
|
||||
# Update EMA attributes
|
||||
copy_attr(self.ema, model, include, exclude)
|
||||
|
||||
|
||||
class BatchNormXd(torch.nn.modules.batchnorm._BatchNorm):
|
||||
def _check_input_dim(self, input):
|
||||
# The only difference between BatchNorm1d, BatchNorm2d, BatchNorm3d, etc
|
||||
# is this method that is overwritten by the sub-class
|
||||
# This original goal of this method was for tensor sanity checks
|
||||
# If you're ok bypassing those sanity checks (eg. if you trust your inference
|
||||
# to provide the right dimensional inputs), then you can just use this method
|
||||
# for easy conversion from SyncBatchNorm
|
||||
# (unfortunately, SyncBatchNorm does not store the original class - if it did
|
||||
# we could return the one that was originally created)
|
||||
return
|
||||
|
||||
def revert_sync_batchnorm(module):
|
||||
# this is very similar to the function that it is trying to revert:
|
||||
# https://github.com/pytorch/pytorch/blob/c8b3686a3e4ba63dc59e5dcfe5db3430df256833/torch/nn/modules/batchnorm.py#L679
|
||||
module_output = module
|
||||
if isinstance(module, torch.nn.modules.batchnorm.SyncBatchNorm):
|
||||
new_cls = BatchNormXd
|
||||
module_output = BatchNormXd(module.num_features,
|
||||
module.eps, module.momentum,
|
||||
module.affine,
|
||||
module.track_running_stats)
|
||||
if module.affine:
|
||||
with torch.no_grad():
|
||||
module_output.weight = module.weight
|
||||
module_output.bias = module.bias
|
||||
module_output.running_mean = module.running_mean
|
||||
module_output.running_var = module.running_var
|
||||
module_output.num_batches_tracked = module.num_batches_tracked
|
||||
if hasattr(module, "qconfig"):
|
||||
module_output.qconfig = module.qconfig
|
||||
for name, child in module.named_children():
|
||||
module_output.add_module(name, revert_sync_batchnorm(child))
|
||||
del module
|
||||
return module_output
|
||||
|
||||
|
||||
class TracedModel(nn.Module):
|
||||
|
||||
def __init__(self, model=None, device=None, img_size=(640,640)):
|
||||
super(TracedModel, self).__init__()
|
||||
|
||||
print(" Convert model to Traced-model... ")
|
||||
self.stride = model.stride
|
||||
self.names = model.names
|
||||
self.model = model
|
||||
|
||||
self.model = revert_sync_batchnorm(self.model)
|
||||
self.model.to('cpu')
|
||||
self.model.eval()
|
||||
|
||||
self.detect_layer = self.model.model[-1]
|
||||
self.model.traced = True
|
||||
|
||||
rand_example = torch.rand(1, 3, img_size, img_size)
|
||||
|
||||
traced_script_module = torch.jit.trace(self.model, rand_example, strict=False)
|
||||
#traced_script_module = torch.jit.script(self.model)
|
||||
traced_script_module.save("traced_model.pt")
|
||||
print(" traced_script_module saved! ")
|
||||
self.model = traced_script_module
|
||||
self.model.to(device)
|
||||
self.detect_layer.to(device)
|
||||
print(" model is traced! \n")
|
||||
|
||||
def forward(self, x, augment=False, profile=False):
|
||||
out = self.model(x)
|
||||
out = self.detect_layer(out)
|
||||
return out
|
||||
|
|
@ -0,0 +1 @@
|
|||
# init
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import argparse
|
||||
|
||||
import yaml
|
||||
|
||||
from wandb_utils import WandbLogger
|
||||
|
||||
WANDB_ARTIFACT_PREFIX = 'wandb-artifact://'
|
||||
|
||||
|
||||
def create_dataset_artifact(opt):
|
||||
with open(opt.data) as f:
|
||||
data = yaml.load(f, Loader=yaml.SafeLoader) # data dict
|
||||
logger = WandbLogger(opt, '', None, data, job_type='Dataset Creation')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--data', type=str, default='data/coco.yaml', help='data.yaml path')
|
||||
parser.add_argument('--single-cls', action='store_true', help='train as single-class dataset')
|
||||
parser.add_argument('--project', type=str, default='YOLOR', help='name of W&B Project')
|
||||
opt = parser.parse_args()
|
||||
opt.resume = False # Explicitly disallow resume check for dataset upload job
|
||||
|
||||
create_dataset_artifact(opt)
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import yaml
|
||||
from tqdm import tqdm
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent)) # add utils/ to path
|
||||
from utils.datasets import LoadImagesAndLabels
|
||||
from utils.datasets import img2label_paths
|
||||
from utils.general import colorstr, xywh2xyxy, check_dataset
|
||||
|
||||
try:
|
||||
import wandb
|
||||
from wandb import init, finish
|
||||
except ImportError:
|
||||
wandb = None
|
||||
|
||||
WANDB_ARTIFACT_PREFIX = 'wandb-artifact://'
|
||||
|
||||
|
||||
def remove_prefix(from_string, prefix=WANDB_ARTIFACT_PREFIX):
|
||||
return from_string[len(prefix):]
|
||||
|
||||
|
||||
def check_wandb_config_file(data_config_file):
|
||||
wandb_config = '_wandb.'.join(data_config_file.rsplit('.', 1)) # updated data.yaml path
|
||||
if Path(wandb_config).is_file():
|
||||
return wandb_config
|
||||
return data_config_file
|
||||
|
||||
|
||||
def get_run_info(run_path):
|
||||
run_path = Path(remove_prefix(run_path, WANDB_ARTIFACT_PREFIX))
|
||||
run_id = run_path.stem
|
||||
project = run_path.parent.stem
|
||||
model_artifact_name = 'run_' + run_id + '_model'
|
||||
return run_id, project, model_artifact_name
|
||||
|
||||
|
||||
def check_wandb_resume(opt):
|
||||
process_wandb_config_ddp_mode(opt) if opt.global_rank not in [-1, 0] else None
|
||||
if isinstance(opt.resume, str):
|
||||
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
|
||||
if opt.global_rank not in [-1, 0]: # For resuming DDP runs
|
||||
run_id, project, model_artifact_name = get_run_info(opt.resume)
|
||||
api = wandb.Api()
|
||||
artifact = api.artifact(project + '/' + model_artifact_name + ':latest')
|
||||
modeldir = artifact.download()
|
||||
opt.weights = str(Path(modeldir) / "last.pt")
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
def process_wandb_config_ddp_mode(opt):
|
||||
with open(opt.data) as f:
|
||||
data_dict = yaml.load(f, Loader=yaml.SafeLoader) # data dict
|
||||
train_dir, val_dir = None, None
|
||||
if isinstance(data_dict['train'], str) and data_dict['train'].startswith(WANDB_ARTIFACT_PREFIX):
|
||||
api = wandb.Api()
|
||||
train_artifact = api.artifact(remove_prefix(data_dict['train']) + ':' + opt.artifact_alias)
|
||||
train_dir = train_artifact.download()
|
||||
train_path = Path(train_dir) / 'data/images/'
|
||||
data_dict['train'] = str(train_path)
|
||||
|
||||
if isinstance(data_dict['val'], str) and data_dict['val'].startswith(WANDB_ARTIFACT_PREFIX):
|
||||
api = wandb.Api()
|
||||
val_artifact = api.artifact(remove_prefix(data_dict['val']) + ':' + opt.artifact_alias)
|
||||
val_dir = val_artifact.download()
|
||||
val_path = Path(val_dir) / 'data/images/'
|
||||
data_dict['val'] = str(val_path)
|
||||
if train_dir or val_dir:
|
||||
ddp_data_path = str(Path(val_dir) / 'wandb_local_data.yaml')
|
||||
with open(ddp_data_path, 'w') as f:
|
||||
yaml.dump(data_dict, f)
|
||||
opt.data = ddp_data_path
|
||||
|
||||
|
||||
class WandbLogger():
|
||||
def __init__(self, opt, name, run_id, data_dict, job_type='Training'):
|
||||
# Pre-training routine --
|
||||
self.job_type = job_type
|
||||
self.wandb, self.wandb_run, self.data_dict = wandb, None if not wandb else wandb.run, data_dict
|
||||
# It's more elegant to stick to 1 wandb.init call, but useful config data is overwritten in the WandbLogger's wandb.init call
|
||||
if isinstance(opt.resume, str): # checks resume from artifact
|
||||
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
|
||||
run_id, project, model_artifact_name = get_run_info(opt.resume)
|
||||
model_artifact_name = WANDB_ARTIFACT_PREFIX + model_artifact_name
|
||||
assert wandb, 'install wandb to resume wandb runs'
|
||||
# Resume wandb-artifact:// runs here| workaround for not overwriting wandb.config
|
||||
self.wandb_run = wandb.init(id=run_id, project=project, resume='allow')
|
||||
opt.resume = model_artifact_name
|
||||
elif self.wandb:
|
||||
self.wandb_run = wandb.init(config=opt,
|
||||
resume="allow",
|
||||
project='YOLOR' if opt.project == 'runs/train' else Path(opt.project).stem,
|
||||
name=name,
|
||||
job_type=job_type,
|
||||
id=run_id) if not wandb.run else wandb.run
|
||||
if self.wandb_run:
|
||||
if self.job_type == 'Training':
|
||||
if not opt.resume:
|
||||
wandb_data_dict = self.check_and_upload_dataset(opt) if opt.upload_dataset else data_dict
|
||||
# Info useful for resuming from artifacts
|
||||
self.wandb_run.config.opt = vars(opt)
|
||||
self.wandb_run.config.data_dict = wandb_data_dict
|
||||
self.data_dict = self.setup_training(opt, data_dict)
|
||||
if self.job_type == 'Dataset Creation':
|
||||
self.data_dict = self.check_and_upload_dataset(opt)
|
||||
else:
|
||||
prefix = colorstr('wandb: ')
|
||||
print(f"{prefix}Install Weights & Biases for YOLOR logging with 'pip install wandb' (recommended)")
|
||||
|
||||
def check_and_upload_dataset(self, opt):
|
||||
assert wandb, 'Install wandb to upload dataset'
|
||||
check_dataset(self.data_dict)
|
||||
config_path = self.log_dataset_artifact(opt.data,
|
||||
opt.single_cls,
|
||||
'YOLOR' if opt.project == 'runs/train' else Path(opt.project).stem)
|
||||
print("Created dataset config file ", config_path)
|
||||
with open(config_path) as f:
|
||||
wandb_data_dict = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
return wandb_data_dict
|
||||
|
||||
def setup_training(self, opt, data_dict):
|
||||
self.log_dict, self.current_epoch, self.log_imgs = {}, 0, 16 # Logging Constants
|
||||
self.bbox_interval = opt.bbox_interval
|
||||
if isinstance(opt.resume, str):
|
||||
modeldir, _ = self.download_model_artifact(opt)
|
||||
if modeldir:
|
||||
self.weights = Path(modeldir) / "last.pt"
|
||||
config = self.wandb_run.config
|
||||
opt.weights, opt.save_period, opt.batch_size, opt.bbox_interval, opt.epochs, opt.hyp = str(
|
||||
self.weights), config.save_period, config.total_batch_size, config.bbox_interval, config.epochs, \
|
||||
config.opt['hyp']
|
||||
data_dict = dict(self.wandb_run.config.data_dict) # eliminates the need for config file to resume
|
||||
if 'val_artifact' not in self.__dict__: # If --upload_dataset is set, use the existing artifact, don't download
|
||||
self.train_artifact_path, self.train_artifact = self.download_dataset_artifact(data_dict.get('train'),
|
||||
opt.artifact_alias)
|
||||
self.val_artifact_path, self.val_artifact = self.download_dataset_artifact(data_dict.get('val'),
|
||||
opt.artifact_alias)
|
||||
self.result_artifact, self.result_table, self.val_table, self.weights = None, None, None, None
|
||||
if self.train_artifact_path is not None:
|
||||
train_path = Path(self.train_artifact_path) / 'data/images/'
|
||||
data_dict['train'] = str(train_path)
|
||||
if self.val_artifact_path is not None:
|
||||
val_path = Path(self.val_artifact_path) / 'data/images/'
|
||||
data_dict['val'] = str(val_path)
|
||||
self.val_table = self.val_artifact.get("val")
|
||||
self.map_val_table_path()
|
||||
if self.val_artifact is not None:
|
||||
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
|
||||
self.result_table = wandb.Table(["epoch", "id", "prediction", "avg_confidence"])
|
||||
if opt.bbox_interval == -1:
|
||||
self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1
|
||||
return data_dict
|
||||
|
||||
def download_dataset_artifact(self, path, alias):
|
||||
if isinstance(path, str) and path.startswith(WANDB_ARTIFACT_PREFIX):
|
||||
dataset_artifact = wandb.use_artifact(remove_prefix(path, WANDB_ARTIFACT_PREFIX) + ":" + alias)
|
||||
assert dataset_artifact is not None, "'Error: W&B dataset artifact doesn\'t exist'"
|
||||
datadir = dataset_artifact.download()
|
||||
return datadir, dataset_artifact
|
||||
return None, None
|
||||
|
||||
def download_model_artifact(self, opt):
|
||||
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
|
||||
model_artifact = wandb.use_artifact(remove_prefix(opt.resume, WANDB_ARTIFACT_PREFIX) + ":latest")
|
||||
assert model_artifact is not None, 'Error: W&B model artifact doesn\'t exist'
|
||||
modeldir = model_artifact.download()
|
||||
epochs_trained = model_artifact.metadata.get('epochs_trained')
|
||||
total_epochs = model_artifact.metadata.get('total_epochs')
|
||||
assert epochs_trained < total_epochs, 'training to %g epochs is finished, nothing to resume.' % (
|
||||
total_epochs)
|
||||
return modeldir, model_artifact
|
||||
return None, None
|
||||
|
||||
def log_model(self, path, opt, epoch, fitness_score, best_model=False):
|
||||
model_artifact = wandb.Artifact('run_' + wandb.run.id + '_model', type='model', metadata={
|
||||
'original_url': str(path),
|
||||
'epochs_trained': epoch + 1,
|
||||
'save period': opt.save_period,
|
||||
'project': opt.project,
|
||||
'total_epochs': opt.epochs,
|
||||
'fitness_score': fitness_score
|
||||
})
|
||||
model_artifact.add_file(str(path / 'last.pt'), name='last.pt')
|
||||
wandb.log_artifact(model_artifact,
|
||||
aliases=['latest', 'epoch ' + str(self.current_epoch), 'best' if best_model else ''])
|
||||
print("Saving model artifact on epoch ", epoch + 1)
|
||||
|
||||
def log_dataset_artifact(self, data_file, single_cls, project, overwrite_config=False):
|
||||
with open(data_file) as f:
|
||||
data = yaml.load(f, Loader=yaml.SafeLoader) # data dict
|
||||
nc, names = (1, ['item']) if single_cls else (int(data['nc']), data['names'])
|
||||
names = {k: v for k, v in enumerate(names)} # to index dictionary
|
||||
self.train_artifact = self.create_dataset_table(LoadImagesAndLabels(
|
||||
data['train']), names, name='train') if data.get('train') else None
|
||||
self.val_artifact = self.create_dataset_table(LoadImagesAndLabels(
|
||||
data['val']), names, name='val') if data.get('val') else None
|
||||
if data.get('train'):
|
||||
data['train'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'train')
|
||||
if data.get('val'):
|
||||
data['val'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'val')
|
||||
path = data_file if overwrite_config else '_wandb.'.join(data_file.rsplit('.', 1)) # updated data.yaml path
|
||||
data.pop('download', None)
|
||||
with open(path, 'w') as f:
|
||||
yaml.dump(data, f)
|
||||
|
||||
if self.job_type == 'Training': # builds correct artifact pipeline graph
|
||||
self.wandb_run.use_artifact(self.val_artifact)
|
||||
self.wandb_run.use_artifact(self.train_artifact)
|
||||
self.val_artifact.wait()
|
||||
self.val_table = self.val_artifact.get('val')
|
||||
self.map_val_table_path()
|
||||
else:
|
||||
self.wandb_run.log_artifact(self.train_artifact)
|
||||
self.wandb_run.log_artifact(self.val_artifact)
|
||||
return path
|
||||
|
||||
def map_val_table_path(self):
|
||||
self.val_table_map = {}
|
||||
print("Mapping dataset")
|
||||
for i, data in enumerate(tqdm(self.val_table.data)):
|
||||
self.val_table_map[data[3]] = data[0]
|
||||
|
||||
def create_dataset_table(self, dataset, class_to_id, name='dataset'):
|
||||
# TODO: Explore multiprocessing to slpit this loop parallely| This is essential for speeding up the the logging
|
||||
artifact = wandb.Artifact(name=name, type="dataset")
|
||||
img_files = tqdm([dataset.path]) if isinstance(dataset.path, str) and Path(dataset.path).is_dir() else None
|
||||
img_files = tqdm(dataset.img_files) if not img_files else img_files
|
||||
for img_file in img_files:
|
||||
if Path(img_file).is_dir():
|
||||
artifact.add_dir(img_file, name='data/images')
|
||||
labels_path = 'labels'.join(dataset.path.rsplit('images', 1))
|
||||
artifact.add_dir(labels_path, name='data/labels')
|
||||
else:
|
||||
artifact.add_file(img_file, name='data/images/' + Path(img_file).name)
|
||||
label_file = Path(img2label_paths([img_file])[0])
|
||||
artifact.add_file(str(label_file),
|
||||
name='data/labels/' + label_file.name) if label_file.exists() else None
|
||||
table = wandb.Table(columns=["id", "train_image", "Classes", "name"])
|
||||
class_set = wandb.Classes([{'id': id, 'name': name} for id, name in class_to_id.items()])
|
||||
for si, (img, labels, paths, shapes) in enumerate(tqdm(dataset)):
|
||||
height, width = shapes[0]
|
||||
labels[:, 2:] = (xywh2xyxy(labels[:, 2:].view(-1, 4))) * torch.Tensor([width, height, width, height])
|
||||
box_data, img_classes = [], {}
|
||||
for cls, *xyxy in labels[:, 1:].tolist():
|
||||
cls = int(cls)
|
||||
box_data.append({"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]},
|
||||
"class_id": cls,
|
||||
"box_caption": "%s" % (class_to_id[cls]),
|
||||
"scores": {"acc": 1},
|
||||
"domain": "pixel"})
|
||||
img_classes[cls] = class_to_id[cls]
|
||||
boxes = {"ground_truth": {"box_data": box_data, "class_labels": class_to_id}} # inference-space
|
||||
table.add_data(si, wandb.Image(paths, classes=class_set, boxes=boxes), json.dumps(img_classes),
|
||||
Path(paths).name)
|
||||
artifact.add(table, name)
|
||||
return artifact
|
||||
|
||||
def log_training_progress(self, predn, path, names):
|
||||
if self.val_table and self.result_table:
|
||||
class_set = wandb.Classes([{'id': id, 'name': name} for id, name in names.items()])
|
||||
box_data = []
|
||||
total_conf = 0
|
||||
for *xyxy, conf, cls in predn.tolist():
|
||||
if conf >= 0.25:
|
||||
box_data.append(
|
||||
{"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]},
|
||||
"class_id": int(cls),
|
||||
"box_caption": "%s %.3f" % (names[cls], conf),
|
||||
"scores": {"class_score": conf},
|
||||
"domain": "pixel"})
|
||||
total_conf = total_conf + conf
|
||||
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
|
||||
id = self.val_table_map[Path(path).name]
|
||||
self.result_table.add_data(self.current_epoch,
|
||||
id,
|
||||
wandb.Image(self.val_table.data[id][1], boxes=boxes, classes=class_set),
|
||||
total_conf / max(1, len(box_data))
|
||||
)
|
||||
|
||||
def log(self, log_dict):
|
||||
if self.wandb_run:
|
||||
for key, value in log_dict.items():
|
||||
self.log_dict[key] = value
|
||||
|
||||
def end_epoch(self, best_result=False):
|
||||
if self.wandb_run:
|
||||
wandb.log(self.log_dict)
|
||||
self.log_dict = {}
|
||||
if self.result_artifact:
|
||||
train_results = wandb.JoinedTable(self.val_table, self.result_table, "id")
|
||||
self.result_artifact.add(train_results, 'result')
|
||||
wandb.log_artifact(self.result_artifact, aliases=['latest', 'epoch ' + str(self.current_epoch),
|
||||
('best' if best_result else '')])
|
||||
self.result_table = wandb.Table(["epoch", "id", "prediction", "avg_confidence"])
|
||||
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
|
||||
|
||||
def finish_run(self):
|
||||
if self.wandb_run:
|
||||
if self.log_dict:
|
||||
wandb.log(self.log_dict)
|
||||
wandb.run.finish()
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
import cv2
|
||||
import torch
|
||||
import time
|
||||
import threading
|
||||
import numpy as np
|
||||
from numpy import random
|
||||
import sys
|
||||
import os
|
||||
from flask import Flask, Response, request
|
||||
import json
|
||||
|
||||
models_path = os.path.join(os.path.dirname(__file__), '..', 'Models/yolov7')
|
||||
sys.path.append(models_path)
|
||||
|
||||
from models.experimental import attempt_load
|
||||
from utils.general import non_max_suppression, scale_coords, check_img_size
|
||||
from utils.torch_utils import select_device, time_synchronized
|
||||
from utils.datasets import letterbox
|
||||
from utils.plots import plot_one_box
|
||||
|
||||
import uuid
|
||||
import paho.mqtt.client as mqtt
|
||||
mqtt_client = mqtt.Client(f"client_weed_detector_{uuid.uuid4()}")
|
||||
mqtt_client.connect("localhost", port=1883)
|
||||
|
||||
# Outras configurações
|
||||
json_data = None
|
||||
output_folder = 'Python/Output/'
|
||||
max_readings = int(sys.argv[1])
|
||||
porta = sys.argv[2]
|
||||
url = sys.argv[3]
|
||||
arquivoSaida = sys.argv[4]
|
||||
mostrar_linhas = sys.argv[5] == "1"
|
||||
mqtt_topic = sys.argv[6]
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def detect(weights, _camera_index, img_size=512, conf_thres=0.25, iou_thres=0.45):
|
||||
with torch.no_grad():
|
||||
# Initialize
|
||||
device = select_device('')
|
||||
half = device.type != 'cpu' # half precision only supported on CUDA
|
||||
|
||||
# Load model
|
||||
model = attempt_load(weights, map_location=device) # load FP32 model
|
||||
model.eval()
|
||||
stride = int(model.stride.max()) # model stride
|
||||
imgsz = check_img_size(img_size, s=stride) # check img_size
|
||||
if half:
|
||||
model.half() # to FP16
|
||||
|
||||
# Open camera
|
||||
cap = cv2.VideoCapture(_camera_index) # 0 for the first webcam device
|
||||
#cap.set(3, 640) # set video width
|
||||
#cap.set(4, 480) # set video height
|
||||
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
|
||||
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
|
||||
readings = []
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
# Padded resize
|
||||
img = letterbox(frame, imgsz, stride=stride)[0]
|
||||
|
||||
# Convert
|
||||
img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
|
||||
img = np.ascontiguousarray(img)
|
||||
|
||||
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)
|
||||
|
||||
# Inference
|
||||
t1 = time_synchronized()
|
||||
pred = model(img, augment=False)[0]
|
||||
t2 = time_synchronized()
|
||||
|
||||
# Apply NMS
|
||||
pred = non_max_suppression(pred, conf_thres, iou_thres, classes=None, agnostic=False)
|
||||
|
||||
current_readings = []
|
||||
# Process detections
|
||||
for i, det in enumerate(pred): # detections per frame
|
||||
if len(det):
|
||||
# Rescale boxes from img_size to frame size
|
||||
det[:, :4] = scale_coords(img.shape[2:], det[:, :4], frame.shape).round()
|
||||
|
||||
# Print results and draw boxes
|
||||
for *xyxy, conf, cls in reversed(det):
|
||||
x1, y1, x2, y2 = xyxy
|
||||
x = int(x1)
|
||||
y = int(y1)
|
||||
w = int(x2 - x1)
|
||||
h = int(y2 - y1)
|
||||
class_id = int(cls)
|
||||
confidence = float(conf)
|
||||
|
||||
detection_info = {
|
||||
'id': class_id,
|
||||
'descricao': model.module.names[class_id] if hasattr(model, 'module') else model.names[class_id],
|
||||
'x': x,
|
||||
'y': y,
|
||||
'largura': w,
|
||||
'altura': h,
|
||||
'confianca': confidence
|
||||
}
|
||||
current_readings.append(detection_info)
|
||||
|
||||
if mostrar_linhas:
|
||||
label = f'{model.names[class_id]} {confidence:.2f}'
|
||||
plot_one_box(xyxy, frame, label=label, color=[random.randint(0, 255) for _ in range(3)], line_thickness=3)
|
||||
|
||||
# Após a detecção:
|
||||
del img # Delete the image tensor to free up memory
|
||||
#if device.type == 'cuda':
|
||||
# torch.cuda.empty_cache() # Clear CUDA cache
|
||||
#gc.collect() # Run garbage collection
|
||||
|
||||
global json_data
|
||||
timestamp = time.time()
|
||||
json_data = {'timestamp': timestamp, 'x_max': width, 'y_max': height, 'objetos': current_readings}
|
||||
readings.append(json_data)
|
||||
|
||||
mqtt_client.publish(mqtt_topic, json.dumps(json_data).encode('utf-8'))
|
||||
|
||||
if not os.path.exists(output_folder):
|
||||
os.makedirs(output_folder)
|
||||
|
||||
if len(readings) > max_readings:
|
||||
readings.pop(0)
|
||||
|
||||
with open(output_folder + arquivoSaida, 'w') as file:
|
||||
json.dump(readings, file, indent=4)
|
||||
|
||||
ret, buffer = cv2.imencode('.jpg', frame)
|
||||
frame = buffer.tobytes()
|
||||
yield (b'--frame\r\n'
|
||||
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
|
||||
|
||||
|
||||
|
||||
# Esta função é para confirmar que script está pronto para execução
|
||||
def send_script_ready():
|
||||
mqtt_client.publish(mqtt_topic, "OK")
|
||||
|
||||
# Iniciar o servidor Flask em uma thread separada
|
||||
def run_flask_server():
|
||||
app.run(host='0.0.0.0', port=porta, threaded=True, debug=False)
|
||||
|
||||
@app.route('/' + url, methods=['GET'])
|
||||
def video_feed():
|
||||
weights_path = 'C:\\ZendionINC\\agrobot_base\\Treinamento\\models\\ervas_v7\\backup\\best.pt'
|
||||
conf_threshold = float(request.args.get('conf_threshold'))
|
||||
nms_threshold = float(request.args.get('nms_threshold'))
|
||||
camera_index = int(request.args.get('camera_index'))
|
||||
return Response(detect(weights_path, camera_index, 512), mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||
|
||||
|
||||
# Iniciar o servidor
|
||||
if __name__ == '__main__':
|
||||
# Cria trhead separada para informar que o script iniciou com sucesso
|
||||
mqtt_thread = threading.Thread(target=send_script_ready)
|
||||
mqtt_thread.start()
|
||||
|
||||
# Inicia o servidor Flask na thread principal
|
||||
run_flask_server()
|
||||
|
|
@ -15,6 +15,7 @@ namespace AgroBase.Services
|
|||
|
||||
public static string ScriptAPI = "api.py";
|
||||
public static string ScriptWeedDetector = "weed-detector.py";
|
||||
public static string ScriptWeedDetectorV7 = "weed-detector-v7.py";
|
||||
public static string ScriptGreenDetector = "green-detector.py";
|
||||
public static string ScriptStreetDetector = "street-detector.py";
|
||||
public static string ScriptMapConverter = "map-load.py";
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
[{"Momento":"2024-04-24T16:41:25.7857678-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.666666666666643,"BateriaConsumida":0.0,"DistanciaPercorrida":[],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:41:30.7885607-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.999999999999972,"BateriaConsumida":0.0,"DistanciaPercorrida":[],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:41:35.7954091-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-81.999999999999972,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:41:40.7926495-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.999999999999972,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:41:45.7946476-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.999999999999972,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:41:50.7876562-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.999999999999972,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:41:55.7816498-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-80.999999999999986,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:42:00.7876618-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:42:05.7826477-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.333333333333329,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:42:10.7846485-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.999999999999972,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:42:15.7906468-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-79.3333333333333,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:42:20.7896484-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.333333333333329,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:42:25.78165-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.666666666666643,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:42:30.792649-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.999999999999972,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-24T16:42:35.7894128-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":-78.666666666666643,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null}
|
||||
|
|
@ -0,0 +1 @@
|
|||
[{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0},{"Latitude":0.0,"Longitude":0.0,"DataHora":"0001-01-01T00:00:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":99.99,"Orientacao":0.0}]
|
||||
|
|
@ -0,0 +1 @@
|
|||
[{"Momento":"2024-04-25T09:06:35.5444721-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-25T09:06:40.5537095-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-25T09:06:45.5465152-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-25T09:06:50.5488174-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-25T09:06:55.546636-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-25T09:07:00.5484381-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-25T09:07:05.5544873-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-25T09:07:10.5508611-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-25T09:07:15.5696869-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-25T09:07:20.5560289-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null},{"Momento":"2024-04-25T09:07:24.4985838-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null}],{"Momento":"2024-04-25T09:07:25.8394796-03:00","VelocidadeMedia":0.0,"PorcentagemBateria":0.0,"BateriaConsumida":0.0,"DistanciaPercorrida":[0.0],"DistanciaEsquerda":0.0,"DistanciaDireita":0.0,"AnguloCarro":0.0,"AnguloCarroBussola":0.0,"SonarFrontal":null}]
|
||||
|
|
@ -0,0 +1 @@
|
|||
[{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:06:35.3281991-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:06:40.3447007-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:06:45.3319504-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:06:50.3290662-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:06:55.3349105-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:07:00.3356743-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:07:05.3362048-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:07:10.3531693-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:07:15.3682188-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:07:20.3690216-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:07:24.3653712-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0},{"Latitude":-22.339442335994335,"Longitude":-47.264067138109773,"DataHora":"2024-04-25T09:07:25.3681645-03:00","Altitude":0.0,"Velocidade":0.0,"NumeroSatelites":0,"PrecisaoHorizontal":0.0,"Orientacao":0.0}]
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
# init
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,272 @@
|
|||
import numpy as np
|
||||
import random
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from models.common import Conv, DWConv
|
||||
from utils.google_utils import attempt_download
|
||||
|
||||
|
||||
class CrossConv(nn.Module):
|
||||
# Cross Convolution Downsample
|
||||
def __init__(self, c1, c2, k=3, s=1, g=1, e=1.0, shortcut=False):
|
||||
# ch_in, ch_out, kernel, stride, groups, expansion, shortcut
|
||||
super(CrossConv, self).__init__()
|
||||
c_ = int(c2 * e) # hidden channels
|
||||
self.cv1 = Conv(c1, c_, (1, k), (1, s))
|
||||
self.cv2 = Conv(c_, c2, (k, 1), (s, 1), g=g)
|
||||
self.add = shortcut and c1 == c2
|
||||
|
||||
def forward(self, x):
|
||||
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
|
||||
|
||||
|
||||
class Sum(nn.Module):
|
||||
# Weighted sum of 2 or more layers https://arxiv.org/abs/1911.09070
|
||||
def __init__(self, n, weight=False): # n: number of inputs
|
||||
super(Sum, self).__init__()
|
||||
self.weight = weight # apply weights boolean
|
||||
self.iter = range(n - 1) # iter object
|
||||
if weight:
|
||||
self.w = nn.Parameter(-torch.arange(1., n) / 2, requires_grad=True) # layer weights
|
||||
|
||||
def forward(self, x):
|
||||
y = x[0] # no weight
|
||||
if self.weight:
|
||||
w = torch.sigmoid(self.w) * 2
|
||||
for i in self.iter:
|
||||
y = y + x[i + 1] * w[i]
|
||||
else:
|
||||
for i in self.iter:
|
||||
y = y + x[i + 1]
|
||||
return y
|
||||
|
||||
|
||||
class MixConv2d(nn.Module):
|
||||
# Mixed Depthwise Conv https://arxiv.org/abs/1907.09595
|
||||
def __init__(self, c1, c2, k=(1, 3), s=1, equal_ch=True):
|
||||
super(MixConv2d, self).__init__()
|
||||
groups = len(k)
|
||||
if equal_ch: # equal c_ per group
|
||||
i = torch.linspace(0, groups - 1E-6, c2).floor() # c2 indices
|
||||
c_ = [(i == g).sum() for g in range(groups)] # intermediate channels
|
||||
else: # equal weight.numel() per group
|
||||
b = [c2] + [0] * groups
|
||||
a = np.eye(groups + 1, groups, k=-1)
|
||||
a -= np.roll(a, 1, axis=1)
|
||||
a *= np.array(k) ** 2
|
||||
a[0] = 1
|
||||
c_ = np.linalg.lstsq(a, b, rcond=None)[0].round() # solve for equal weight indices, ax = b
|
||||
|
||||
self.m = nn.ModuleList([nn.Conv2d(c1, int(c_[g]), k[g], s, k[g] // 2, bias=False) for g in range(groups)])
|
||||
self.bn = nn.BatchNorm2d(c2)
|
||||
self.act = nn.LeakyReLU(0.1, inplace=True)
|
||||
|
||||
def forward(self, x):
|
||||
return x + self.act(self.bn(torch.cat([m(x) for m in self.m], 1)))
|
||||
|
||||
|
||||
class Ensemble(nn.ModuleList):
|
||||
# Ensemble of models
|
||||
def __init__(self):
|
||||
super(Ensemble, self).__init__()
|
||||
|
||||
def forward(self, x, augment=False):
|
||||
y = []
|
||||
for module in self:
|
||||
y.append(module(x, augment)[0])
|
||||
# y = torch.stack(y).max(0)[0] # max ensemble
|
||||
# y = torch.stack(y).mean(0) # mean ensemble
|
||||
y = torch.cat(y, 1) # nms ensemble
|
||||
return y, None # inference, train output
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ORT_NMS(torch.autograd.Function):
|
||||
'''ONNX-Runtime NMS operation'''
|
||||
@staticmethod
|
||||
def forward(ctx,
|
||||
boxes,
|
||||
scores,
|
||||
max_output_boxes_per_class=torch.tensor([100]),
|
||||
iou_threshold=torch.tensor([0.45]),
|
||||
score_threshold=torch.tensor([0.25])):
|
||||
device = boxes.device
|
||||
batch = scores.shape[0]
|
||||
num_det = random.randint(0, 100)
|
||||
batches = torch.randint(0, batch, (num_det,)).sort()[0].to(device)
|
||||
idxs = torch.arange(100, 100 + num_det).to(device)
|
||||
zeros = torch.zeros((num_det,), dtype=torch.int64).to(device)
|
||||
selected_indices = torch.cat([batches[None], zeros[None], idxs[None]], 0).T.contiguous()
|
||||
selected_indices = selected_indices.to(torch.int64)
|
||||
return selected_indices
|
||||
|
||||
@staticmethod
|
||||
def symbolic(g, boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold):
|
||||
return g.op("NonMaxSuppression", boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold)
|
||||
|
||||
|
||||
class TRT_NMS(torch.autograd.Function):
|
||||
'''TensorRT NMS operation'''
|
||||
@staticmethod
|
||||
def forward(
|
||||
ctx,
|
||||
boxes,
|
||||
scores,
|
||||
background_class=-1,
|
||||
box_coding=1,
|
||||
iou_threshold=0.45,
|
||||
max_output_boxes=100,
|
||||
plugin_version="1",
|
||||
score_activation=0,
|
||||
score_threshold=0.25,
|
||||
):
|
||||
batch_size, num_boxes, num_classes = scores.shape
|
||||
num_det = torch.randint(0, max_output_boxes, (batch_size, 1), dtype=torch.int32)
|
||||
det_boxes = torch.randn(batch_size, max_output_boxes, 4)
|
||||
det_scores = torch.randn(batch_size, max_output_boxes)
|
||||
det_classes = torch.randint(0, num_classes, (batch_size, max_output_boxes), dtype=torch.int32)
|
||||
return num_det, det_boxes, det_scores, det_classes
|
||||
|
||||
@staticmethod
|
||||
def symbolic(g,
|
||||
boxes,
|
||||
scores,
|
||||
background_class=-1,
|
||||
box_coding=1,
|
||||
iou_threshold=0.45,
|
||||
max_output_boxes=100,
|
||||
plugin_version="1",
|
||||
score_activation=0,
|
||||
score_threshold=0.25):
|
||||
out = g.op("TRT::EfficientNMS_TRT",
|
||||
boxes,
|
||||
scores,
|
||||
background_class_i=background_class,
|
||||
box_coding_i=box_coding,
|
||||
iou_threshold_f=iou_threshold,
|
||||
max_output_boxes_i=max_output_boxes,
|
||||
plugin_version_s=plugin_version,
|
||||
score_activation_i=score_activation,
|
||||
score_threshold_f=score_threshold,
|
||||
outputs=4)
|
||||
nums, boxes, scores, classes = out
|
||||
return nums, boxes, scores, classes
|
||||
|
||||
|
||||
class ONNX_ORT(nn.Module):
|
||||
'''onnx module with ONNX-Runtime NMS operation.'''
|
||||
def __init__(self, max_obj=100, iou_thres=0.45, score_thres=0.25, max_wh=640, device=None, n_classes=80):
|
||||
super().__init__()
|
||||
self.device = device if device else torch.device("cpu")
|
||||
self.max_obj = torch.tensor([max_obj]).to(device)
|
||||
self.iou_threshold = torch.tensor([iou_thres]).to(device)
|
||||
self.score_threshold = torch.tensor([score_thres]).to(device)
|
||||
self.max_wh = max_wh # if max_wh != 0 : non-agnostic else : agnostic
|
||||
self.convert_matrix = torch.tensor([[1, 0, 1, 0], [0, 1, 0, 1], [-0.5, 0, 0.5, 0], [0, -0.5, 0, 0.5]],
|
||||
dtype=torch.float32,
|
||||
device=self.device)
|
||||
self.n_classes=n_classes
|
||||
|
||||
def forward(self, x):
|
||||
boxes = x[:, :, :4]
|
||||
conf = x[:, :, 4:5]
|
||||
scores = x[:, :, 5:]
|
||||
if self.n_classes == 1:
|
||||
scores = conf # for models with one class, cls_loss is 0 and cls_conf is always 0.5,
|
||||
# so there is no need to multiplicate.
|
||||
else:
|
||||
scores *= conf # conf = obj_conf * cls_conf
|
||||
boxes @= self.convert_matrix
|
||||
max_score, category_id = scores.max(2, keepdim=True)
|
||||
dis = category_id.float() * self.max_wh
|
||||
nmsbox = boxes + dis
|
||||
max_score_tp = max_score.transpose(1, 2).contiguous()
|
||||
selected_indices = ORT_NMS.apply(nmsbox, max_score_tp, self.max_obj, self.iou_threshold, self.score_threshold)
|
||||
X, Y = selected_indices[:, 0], selected_indices[:, 2]
|
||||
selected_boxes = boxes[X, Y, :]
|
||||
selected_categories = category_id[X, Y, :].float()
|
||||
selected_scores = max_score[X, Y, :]
|
||||
X = X.unsqueeze(1).float()
|
||||
return torch.cat([X, selected_boxes, selected_categories, selected_scores], 1)
|
||||
|
||||
class ONNX_TRT(nn.Module):
|
||||
'''onnx module with TensorRT NMS operation.'''
|
||||
def __init__(self, max_obj=100, iou_thres=0.45, score_thres=0.25, max_wh=None ,device=None, n_classes=80):
|
||||
super().__init__()
|
||||
assert max_wh is None
|
||||
self.device = device if device else torch.device('cpu')
|
||||
self.background_class = -1,
|
||||
self.box_coding = 1,
|
||||
self.iou_threshold = iou_thres
|
||||
self.max_obj = max_obj
|
||||
self.plugin_version = '1'
|
||||
self.score_activation = 0
|
||||
self.score_threshold = score_thres
|
||||
self.n_classes=n_classes
|
||||
|
||||
def forward(self, x):
|
||||
boxes = x[:, :, :4]
|
||||
conf = x[:, :, 4:5]
|
||||
scores = x[:, :, 5:]
|
||||
if self.n_classes == 1:
|
||||
scores = conf # for models with one class, cls_loss is 0 and cls_conf is always 0.5,
|
||||
# so there is no need to multiplicate.
|
||||
else:
|
||||
scores *= conf # conf = obj_conf * cls_conf
|
||||
num_det, det_boxes, det_scores, det_classes = TRT_NMS.apply(boxes, scores, self.background_class, self.box_coding,
|
||||
self.iou_threshold, self.max_obj,
|
||||
self.plugin_version, self.score_activation,
|
||||
self.score_threshold)
|
||||
return num_det, det_boxes, det_scores, det_classes
|
||||
|
||||
|
||||
class End2End(nn.Module):
|
||||
'''export onnx or tensorrt model with NMS operation.'''
|
||||
def __init__(self, model, max_obj=100, iou_thres=0.45, score_thres=0.25, max_wh=None, device=None, n_classes=80):
|
||||
super().__init__()
|
||||
device = device if device else torch.device('cpu')
|
||||
assert isinstance(max_wh,(int)) or max_wh is None
|
||||
self.model = model.to(device)
|
||||
self.model.model[-1].end2end = True
|
||||
self.patch_model = ONNX_TRT if max_wh is None else ONNX_ORT
|
||||
self.end2end = self.patch_model(max_obj, iou_thres, score_thres, max_wh, device, n_classes)
|
||||
self.end2end.eval()
|
||||
|
||||
def forward(self, x):
|
||||
x = self.model(x)
|
||||
x = self.end2end(x)
|
||||
return x
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def attempt_load(weights, map_location=None):
|
||||
# Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a
|
||||
model = Ensemble()
|
||||
for w in weights if isinstance(weights, list) else [weights]:
|
||||
attempt_download(w)
|
||||
ckpt = torch.load(w, map_location=map_location) # load
|
||||
model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().fuse().eval()) # FP32 model
|
||||
|
||||
# Compatibility updates
|
||||
for m in model.modules():
|
||||
if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU]:
|
||||
m.inplace = True # pytorch 1.7.0 compatibility
|
||||
elif type(m) is nn.Upsample:
|
||||
m.recompute_scale_factor = None # torch 1.11.0 compatibility
|
||||
elif type(m) is Conv:
|
||||
m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility
|
||||
|
||||
if len(model) == 1:
|
||||
return model[-1] # return model
|
||||
else:
|
||||
print('Ensemble created with %s\n' % weights)
|
||||
for k in ['names', 'stride']:
|
||||
setattr(model, k, getattr(model[-1], k))
|
||||
return model # return ensemble
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,843 @@
|
|||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
|
||||
sys.path.append('./') # to run '$ python *.py' files in subdirectories
|
||||
logger = logging.getLogger(__name__)
|
||||
import torch
|
||||
from models.common import *
|
||||
from models.experimental import *
|
||||
from utils.autoanchor import check_anchor_order
|
||||
from utils.general import make_divisible, check_file, set_logging
|
||||
from utils.torch_utils import time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, \
|
||||
select_device, copy_attr
|
||||
from utils.loss import SigmoidBin
|
||||
|
||||
try:
|
||||
import thop # for FLOPS computation
|
||||
except ImportError:
|
||||
thop = None
|
||||
|
||||
|
||||
class Detect(nn.Module):
|
||||
stride = None # strides computed during build
|
||||
export = False # onnx export
|
||||
end2end = False
|
||||
include_nms = False
|
||||
concat = False
|
||||
|
||||
def __init__(self, nc=80, anchors=(), ch=()): # detection layer
|
||||
super(Detect, self).__init__()
|
||||
self.nc = nc # number of classes
|
||||
self.no = nc + 5 # number of outputs per anchor
|
||||
self.nl = len(anchors) # number of detection layers
|
||||
self.na = len(anchors[0]) // 2 # number of anchors
|
||||
self.grid = [torch.zeros(1)] * self.nl # init grid
|
||||
a = torch.tensor(anchors).float().view(self.nl, -1, 2)
|
||||
self.register_buffer('anchors', a) # shape(nl,na,2)
|
||||
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2)
|
||||
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv
|
||||
|
||||
def forward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](x[i]) # conv
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
y = x[i].sigmoid()
|
||||
if not torch.onnx.is_in_onnx_export():
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
else:
|
||||
xy, wh, conf = y.split((2, 2, self.nc + 1), 4) # y.tensor_split((2, 4, 5), 4) # torch 1.8.0
|
||||
xy = xy * (2. * self.stride[i]) + (self.stride[i] * (self.grid[i] - 0.5)) # new xy
|
||||
wh = wh ** 2 * (4 * self.anchor_grid[i].data) # new wh
|
||||
y = torch.cat((xy, wh, conf), 4)
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
if self.training:
|
||||
out = x
|
||||
elif self.end2end:
|
||||
out = torch.cat(z, 1)
|
||||
elif self.include_nms:
|
||||
z = self.convert(z)
|
||||
out = (z, )
|
||||
elif self.concat:
|
||||
out = torch.cat(z, 1)
|
||||
else:
|
||||
out = (torch.cat(z, 1), x)
|
||||
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _make_grid(nx=20, ny=20):
|
||||
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
|
||||
return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
|
||||
|
||||
def convert(self, z):
|
||||
z = torch.cat(z, 1)
|
||||
box = z[:, :, :4]
|
||||
conf = z[:, :, 4:5]
|
||||
score = z[:, :, 5:]
|
||||
score *= conf
|
||||
convert_matrix = torch.tensor([[1, 0, 1, 0], [0, 1, 0, 1], [-0.5, 0, 0.5, 0], [0, -0.5, 0, 0.5]],
|
||||
dtype=torch.float32,
|
||||
device=z.device)
|
||||
box @= convert_matrix
|
||||
return (box, score)
|
||||
|
||||
|
||||
class IDetect(nn.Module):
|
||||
stride = None # strides computed during build
|
||||
export = False # onnx export
|
||||
end2end = False
|
||||
include_nms = False
|
||||
concat = False
|
||||
|
||||
def __init__(self, nc=80, anchors=(), ch=()): # detection layer
|
||||
super(IDetect, self).__init__()
|
||||
self.nc = nc # number of classes
|
||||
self.no = nc + 5 # number of outputs per anchor
|
||||
self.nl = len(anchors) # number of detection layers
|
||||
self.na = len(anchors[0]) // 2 # number of anchors
|
||||
self.grid = [torch.zeros(1)] * self.nl # init grid
|
||||
a = torch.tensor(anchors).float().view(self.nl, -1, 2)
|
||||
self.register_buffer('anchors', a) # shape(nl,na,2)
|
||||
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2)
|
||||
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv
|
||||
|
||||
self.ia = nn.ModuleList(ImplicitA(x) for x in ch)
|
||||
self.im = nn.ModuleList(ImplicitM(self.no * self.na) for _ in ch)
|
||||
|
||||
def forward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](self.ia[i](x[i])) # conv
|
||||
x[i] = self.im[i](x[i])
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
|
||||
y = x[i].sigmoid()
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
return x if self.training else (torch.cat(z, 1), x)
|
||||
|
||||
def fuseforward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](x[i]) # conv
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
|
||||
y = x[i].sigmoid()
|
||||
if not torch.onnx.is_in_onnx_export():
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
else:
|
||||
xy, wh, conf = y.split((2, 2, self.nc + 1), 4) # y.tensor_split((2, 4, 5), 4) # torch 1.8.0
|
||||
xy = xy * (2. * self.stride[i]) + (self.stride[i] * (self.grid[i] - 0.5)) # new xy
|
||||
wh = wh ** 2 * (4 * self.anchor_grid[i].data) # new wh
|
||||
y = torch.cat((xy, wh, conf), 4)
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
if self.training:
|
||||
out = x
|
||||
elif self.end2end:
|
||||
out = torch.cat(z, 1)
|
||||
elif self.include_nms:
|
||||
z = self.convert(z)
|
||||
out = (z, )
|
||||
elif self.concat:
|
||||
out = torch.cat(z, 1)
|
||||
else:
|
||||
out = (torch.cat(z, 1), x)
|
||||
|
||||
return out
|
||||
|
||||
def fuse(self):
|
||||
print("IDetect.fuse")
|
||||
# fuse ImplicitA and Convolution
|
||||
for i in range(len(self.m)):
|
||||
c1,c2,_,_ = self.m[i].weight.shape
|
||||
c1_,c2_, _,_ = self.ia[i].implicit.shape
|
||||
self.m[i].bias += torch.matmul(self.m[i].weight.reshape(c1,c2),self.ia[i].implicit.reshape(c2_,c1_)).squeeze(1)
|
||||
|
||||
# fuse ImplicitM and Convolution
|
||||
for i in range(len(self.m)):
|
||||
c1,c2, _,_ = self.im[i].implicit.shape
|
||||
self.m[i].bias *= self.im[i].implicit.reshape(c2)
|
||||
self.m[i].weight *= self.im[i].implicit.transpose(0,1)
|
||||
|
||||
@staticmethod
|
||||
def _make_grid(nx=20, ny=20):
|
||||
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
|
||||
return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
|
||||
|
||||
def convert(self, z):
|
||||
z = torch.cat(z, 1)
|
||||
box = z[:, :, :4]
|
||||
conf = z[:, :, 4:5]
|
||||
score = z[:, :, 5:]
|
||||
score *= conf
|
||||
convert_matrix = torch.tensor([[1, 0, 1, 0], [0, 1, 0, 1], [-0.5, 0, 0.5, 0], [0, -0.5, 0, 0.5]],
|
||||
dtype=torch.float32,
|
||||
device=z.device)
|
||||
box @= convert_matrix
|
||||
return (box, score)
|
||||
|
||||
|
||||
class IKeypoint(nn.Module):
|
||||
stride = None # strides computed during build
|
||||
export = False # onnx export
|
||||
|
||||
def __init__(self, nc=80, anchors=(), nkpt=17, ch=(), inplace=True, dw_conv_kpt=False): # detection layer
|
||||
super(IKeypoint, self).__init__()
|
||||
self.nc = nc # number of classes
|
||||
self.nkpt = nkpt
|
||||
self.dw_conv_kpt = dw_conv_kpt
|
||||
self.no_det=(nc + 5) # number of outputs per anchor for box and class
|
||||
self.no_kpt = 3*self.nkpt ## number of outputs per anchor for keypoints
|
||||
self.no = self.no_det+self.no_kpt
|
||||
self.nl = len(anchors) # number of detection layers
|
||||
self.na = len(anchors[0]) // 2 # number of anchors
|
||||
self.grid = [torch.zeros(1)] * self.nl # init grid
|
||||
self.flip_test = False
|
||||
a = torch.tensor(anchors).float().view(self.nl, -1, 2)
|
||||
self.register_buffer('anchors', a) # shape(nl,na,2)
|
||||
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2)
|
||||
self.m = nn.ModuleList(nn.Conv2d(x, self.no_det * self.na, 1) for x in ch) # output conv
|
||||
|
||||
self.ia = nn.ModuleList(ImplicitA(x) for x in ch)
|
||||
self.im = nn.ModuleList(ImplicitM(self.no_det * self.na) for _ in ch)
|
||||
|
||||
if self.nkpt is not None:
|
||||
if self.dw_conv_kpt: #keypoint head is slightly more complex
|
||||
self.m_kpt = nn.ModuleList(
|
||||
nn.Sequential(DWConv(x, x, k=3), Conv(x,x),
|
||||
DWConv(x, x, k=3), Conv(x, x),
|
||||
DWConv(x, x, k=3), Conv(x,x),
|
||||
DWConv(x, x, k=3), Conv(x, x),
|
||||
DWConv(x, x, k=3), Conv(x, x),
|
||||
DWConv(x, x, k=3), nn.Conv2d(x, self.no_kpt * self.na, 1)) for x in ch)
|
||||
else: #keypoint head is a single convolution
|
||||
self.m_kpt = nn.ModuleList(nn.Conv2d(x, self.no_kpt * self.na, 1) for x in ch)
|
||||
|
||||
self.inplace = inplace # use in-place ops (e.g. slice assignment)
|
||||
|
||||
def forward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
if self.nkpt is None or self.nkpt==0:
|
||||
x[i] = self.im[i](self.m[i](self.ia[i](x[i]))) # conv
|
||||
else :
|
||||
x[i] = torch.cat((self.im[i](self.m[i](self.ia[i](x[i]))), self.m_kpt[i](x[i])), axis=1)
|
||||
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
x_det = x[i][..., :6]
|
||||
x_kpt = x[i][..., 6:]
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
kpt_grid_x = self.grid[i][..., 0:1]
|
||||
kpt_grid_y = self.grid[i][..., 1:2]
|
||||
|
||||
if self.nkpt == 0:
|
||||
y = x[i].sigmoid()
|
||||
else:
|
||||
y = x_det.sigmoid()
|
||||
|
||||
if self.inplace:
|
||||
xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i].view(1, self.na, 1, 1, 2) # wh
|
||||
if self.nkpt != 0:
|
||||
x_kpt[..., 0::3] = (x_kpt[..., ::3] * 2. - 0.5 + kpt_grid_x.repeat(1,1,1,1,17)) * self.stride[i] # xy
|
||||
x_kpt[..., 1::3] = (x_kpt[..., 1::3] * 2. - 0.5 + kpt_grid_y.repeat(1,1,1,1,17)) * self.stride[i] # xy
|
||||
#x_kpt[..., 0::3] = (x_kpt[..., ::3] + kpt_grid_x.repeat(1,1,1,1,17)) * self.stride[i] # xy
|
||||
#x_kpt[..., 1::3] = (x_kpt[..., 1::3] + kpt_grid_y.repeat(1,1,1,1,17)) * self.stride[i] # xy
|
||||
#print('=============')
|
||||
#print(self.anchor_grid[i].shape)
|
||||
#print(self.anchor_grid[i][...,0].unsqueeze(4).shape)
|
||||
#print(x_kpt[..., 0::3].shape)
|
||||
#x_kpt[..., 0::3] = ((x_kpt[..., 0::3].tanh() * 2.) ** 3 * self.anchor_grid[i][...,0].unsqueeze(4).repeat(1,1,1,1,self.nkpt)) + kpt_grid_x.repeat(1,1,1,1,17) * self.stride[i] # xy
|
||||
#x_kpt[..., 1::3] = ((x_kpt[..., 1::3].tanh() * 2.) ** 3 * self.anchor_grid[i][...,1].unsqueeze(4).repeat(1,1,1,1,self.nkpt)) + kpt_grid_y.repeat(1,1,1,1,17) * self.stride[i] # xy
|
||||
#x_kpt[..., 0::3] = (((x_kpt[..., 0::3].sigmoid() * 4.) ** 2 - 8.) * self.anchor_grid[i][...,0].unsqueeze(4).repeat(1,1,1,1,self.nkpt)) + kpt_grid_x.repeat(1,1,1,1,17) * self.stride[i] # xy
|
||||
#x_kpt[..., 1::3] = (((x_kpt[..., 1::3].sigmoid() * 4.) ** 2 - 8.) * self.anchor_grid[i][...,1].unsqueeze(4).repeat(1,1,1,1,self.nkpt)) + kpt_grid_y.repeat(1,1,1,1,17) * self.stride[i] # xy
|
||||
x_kpt[..., 2::3] = x_kpt[..., 2::3].sigmoid()
|
||||
|
||||
y = torch.cat((xy, wh, y[..., 4:], x_kpt), dim = -1)
|
||||
|
||||
else: # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953
|
||||
xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
if self.nkpt != 0:
|
||||
y[..., 6:] = (y[..., 6:] * 2. - 0.5 + self.grid[i].repeat((1,1,1,1,self.nkpt))) * self.stride[i] # xy
|
||||
y = torch.cat((xy, wh, y[..., 4:]), -1)
|
||||
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
return x if self.training else (torch.cat(z, 1), x)
|
||||
|
||||
@staticmethod
|
||||
def _make_grid(nx=20, ny=20):
|
||||
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
|
||||
return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
|
||||
|
||||
|
||||
class IAuxDetect(nn.Module):
|
||||
stride = None # strides computed during build
|
||||
export = False # onnx export
|
||||
end2end = False
|
||||
include_nms = False
|
||||
concat = False
|
||||
|
||||
def __init__(self, nc=80, anchors=(), ch=()): # detection layer
|
||||
super(IAuxDetect, self).__init__()
|
||||
self.nc = nc # number of classes
|
||||
self.no = nc + 5 # number of outputs per anchor
|
||||
self.nl = len(anchors) # number of detection layers
|
||||
self.na = len(anchors[0]) // 2 # number of anchors
|
||||
self.grid = [torch.zeros(1)] * self.nl # init grid
|
||||
a = torch.tensor(anchors).float().view(self.nl, -1, 2)
|
||||
self.register_buffer('anchors', a) # shape(nl,na,2)
|
||||
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2)
|
||||
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch[:self.nl]) # output conv
|
||||
self.m2 = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch[self.nl:]) # output conv
|
||||
|
||||
self.ia = nn.ModuleList(ImplicitA(x) for x in ch[:self.nl])
|
||||
self.im = nn.ModuleList(ImplicitM(self.no * self.na) for _ in ch[:self.nl])
|
||||
|
||||
def forward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](self.ia[i](x[i])) # conv
|
||||
x[i] = self.im[i](x[i])
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
x[i+self.nl] = self.m2[i](x[i+self.nl])
|
||||
x[i+self.nl] = x[i+self.nl].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
|
||||
y = x[i].sigmoid()
|
||||
if not torch.onnx.is_in_onnx_export():
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
else:
|
||||
xy, wh, conf = y.split((2, 2, self.nc + 1), 4) # y.tensor_split((2, 4, 5), 4) # torch 1.8.0
|
||||
xy = xy * (2. * self.stride[i]) + (self.stride[i] * (self.grid[i] - 0.5)) # new xy
|
||||
wh = wh ** 2 * (4 * self.anchor_grid[i].data) # new wh
|
||||
y = torch.cat((xy, wh, conf), 4)
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
return x if self.training else (torch.cat(z, 1), x[:self.nl])
|
||||
|
||||
def fuseforward(self, x):
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](x[i]) # conv
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
|
||||
y = x[i].sigmoid()
|
||||
if not torch.onnx.is_in_onnx_export():
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
else:
|
||||
xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i].data # wh
|
||||
y = torch.cat((xy, wh, y[..., 4:]), -1)
|
||||
z.append(y.view(bs, -1, self.no))
|
||||
|
||||
if self.training:
|
||||
out = x
|
||||
elif self.end2end:
|
||||
out = torch.cat(z, 1)
|
||||
elif self.include_nms:
|
||||
z = self.convert(z)
|
||||
out = (z, )
|
||||
elif self.concat:
|
||||
out = torch.cat(z, 1)
|
||||
else:
|
||||
out = (torch.cat(z, 1), x)
|
||||
|
||||
return out
|
||||
|
||||
def fuse(self):
|
||||
print("IAuxDetect.fuse")
|
||||
# fuse ImplicitA and Convolution
|
||||
for i in range(len(self.m)):
|
||||
c1,c2,_,_ = self.m[i].weight.shape
|
||||
c1_,c2_, _,_ = self.ia[i].implicit.shape
|
||||
self.m[i].bias += torch.matmul(self.m[i].weight.reshape(c1,c2),self.ia[i].implicit.reshape(c2_,c1_)).squeeze(1)
|
||||
|
||||
# fuse ImplicitM and Convolution
|
||||
for i in range(len(self.m)):
|
||||
c1,c2, _,_ = self.im[i].implicit.shape
|
||||
self.m[i].bias *= self.im[i].implicit.reshape(c2)
|
||||
self.m[i].weight *= self.im[i].implicit.transpose(0,1)
|
||||
|
||||
@staticmethod
|
||||
def _make_grid(nx=20, ny=20):
|
||||
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
|
||||
return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
|
||||
|
||||
def convert(self, z):
|
||||
z = torch.cat(z, 1)
|
||||
box = z[:, :, :4]
|
||||
conf = z[:, :, 4:5]
|
||||
score = z[:, :, 5:]
|
||||
score *= conf
|
||||
convert_matrix = torch.tensor([[1, 0, 1, 0], [0, 1, 0, 1], [-0.5, 0, 0.5, 0], [0, -0.5, 0, 0.5]],
|
||||
dtype=torch.float32,
|
||||
device=z.device)
|
||||
box @= convert_matrix
|
||||
return (box, score)
|
||||
|
||||
|
||||
class IBin(nn.Module):
|
||||
stride = None # strides computed during build
|
||||
export = False # onnx export
|
||||
|
||||
def __init__(self, nc=80, anchors=(), ch=(), bin_count=21): # detection layer
|
||||
super(IBin, self).__init__()
|
||||
self.nc = nc # number of classes
|
||||
self.bin_count = bin_count
|
||||
|
||||
self.w_bin_sigmoid = SigmoidBin(bin_count=self.bin_count, min=0.0, max=4.0)
|
||||
self.h_bin_sigmoid = SigmoidBin(bin_count=self.bin_count, min=0.0, max=4.0)
|
||||
# classes, x,y,obj
|
||||
self.no = nc + 3 + \
|
||||
self.w_bin_sigmoid.get_length() + self.h_bin_sigmoid.get_length() # w-bce, h-bce
|
||||
# + self.x_bin_sigmoid.get_length() + self.y_bin_sigmoid.get_length()
|
||||
|
||||
self.nl = len(anchors) # number of detection layers
|
||||
self.na = len(anchors[0]) // 2 # number of anchors
|
||||
self.grid = [torch.zeros(1)] * self.nl # init grid
|
||||
a = torch.tensor(anchors).float().view(self.nl, -1, 2)
|
||||
self.register_buffer('anchors', a) # shape(nl,na,2)
|
||||
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2)
|
||||
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv
|
||||
|
||||
self.ia = nn.ModuleList(ImplicitA(x) for x in ch)
|
||||
self.im = nn.ModuleList(ImplicitM(self.no * self.na) for _ in ch)
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
#self.x_bin_sigmoid.use_fw_regression = True
|
||||
#self.y_bin_sigmoid.use_fw_regression = True
|
||||
self.w_bin_sigmoid.use_fw_regression = True
|
||||
self.h_bin_sigmoid.use_fw_regression = True
|
||||
|
||||
# x = x.copy() # for profiling
|
||||
z = [] # inference output
|
||||
self.training |= self.export
|
||||
for i in range(self.nl):
|
||||
x[i] = self.m[i](self.ia[i](x[i])) # conv
|
||||
x[i] = self.im[i](x[i])
|
||||
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
|
||||
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
|
||||
|
||||
if not self.training: # inference
|
||||
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
|
||||
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
|
||||
|
||||
y = x[i].sigmoid()
|
||||
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
|
||||
#y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
|
||||
|
||||
|
||||
#px = (self.x_bin_sigmoid.forward(y[..., 0:12]) + self.grid[i][..., 0]) * self.stride[i]
|
||||
#py = (self.y_bin_sigmoid.forward(y[..., 12:24]) + self.grid[i][..., 1]) * self.stride[i]
|
||||
|
||||
pw = self.w_bin_sigmoid.forward(y[..., 2:24]) * self.anchor_grid[i][..., 0]
|
||||
ph = self.h_bin_sigmoid.forward(y[..., 24:46]) * self.anchor_grid[i][..., 1]
|
||||
|
||||
#y[..., 0] = px
|
||||
#y[..., 1] = py
|
||||
y[..., 2] = pw
|
||||
y[..., 3] = ph
|
||||
|
||||
y = torch.cat((y[..., 0:4], y[..., 46:]), dim=-1)
|
||||
|
||||
z.append(y.view(bs, -1, y.shape[-1]))
|
||||
|
||||
return x if self.training else (torch.cat(z, 1), x)
|
||||
|
||||
@staticmethod
|
||||
def _make_grid(nx=20, ny=20):
|
||||
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
|
||||
return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, cfg='yolor-csp-c.yaml', ch=3, nc=None, anchors=None): # model, input channels, number of classes
|
||||
super(Model, self).__init__()
|
||||
self.traced = False
|
||||
if isinstance(cfg, dict):
|
||||
self.yaml = cfg # model dict
|
||||
else: # is *.yaml
|
||||
import yaml # for torch hub
|
||||
self.yaml_file = Path(cfg).name
|
||||
with open(cfg) as f:
|
||||
self.yaml = yaml.load(f, Loader=yaml.SafeLoader) # model dict
|
||||
|
||||
# Define model
|
||||
ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels
|
||||
if nc and nc != self.yaml['nc']:
|
||||
logger.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
|
||||
self.yaml['nc'] = nc # override yaml value
|
||||
if anchors:
|
||||
logger.info(f'Overriding model.yaml anchors with anchors={anchors}')
|
||||
self.yaml['anchors'] = round(anchors) # override yaml value
|
||||
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist
|
||||
self.names = [str(i) for i in range(self.yaml['nc'])] # default names
|
||||
# print([x.shape for x in self.forward(torch.zeros(1, ch, 64, 64))])
|
||||
|
||||
# Build strides, anchors
|
||||
m = self.model[-1] # Detect()
|
||||
if isinstance(m, Detect):
|
||||
s = 256 # 2x min stride
|
||||
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
|
||||
check_anchor_order(m)
|
||||
m.anchors /= m.stride.view(-1, 1, 1)
|
||||
self.stride = m.stride
|
||||
self._initialize_biases() # only run once
|
||||
# print('Strides: %s' % m.stride.tolist())
|
||||
if isinstance(m, IDetect):
|
||||
s = 256 # 2x min stride
|
||||
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
|
||||
check_anchor_order(m)
|
||||
m.anchors /= m.stride.view(-1, 1, 1)
|
||||
self.stride = m.stride
|
||||
self._initialize_biases() # only run once
|
||||
# print('Strides: %s' % m.stride.tolist())
|
||||
if isinstance(m, IAuxDetect):
|
||||
s = 256 # 2x min stride
|
||||
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))[:4]]) # forward
|
||||
#print(m.stride)
|
||||
check_anchor_order(m)
|
||||
m.anchors /= m.stride.view(-1, 1, 1)
|
||||
self.stride = m.stride
|
||||
self._initialize_aux_biases() # only run once
|
||||
# print('Strides: %s' % m.stride.tolist())
|
||||
if isinstance(m, IBin):
|
||||
s = 256 # 2x min stride
|
||||
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
|
||||
check_anchor_order(m)
|
||||
m.anchors /= m.stride.view(-1, 1, 1)
|
||||
self.stride = m.stride
|
||||
self._initialize_biases_bin() # only run once
|
||||
# print('Strides: %s' % m.stride.tolist())
|
||||
if isinstance(m, IKeypoint):
|
||||
s = 256 # 2x min stride
|
||||
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
|
||||
check_anchor_order(m)
|
||||
m.anchors /= m.stride.view(-1, 1, 1)
|
||||
self.stride = m.stride
|
||||
self._initialize_biases_kpt() # only run once
|
||||
# print('Strides: %s' % m.stride.tolist())
|
||||
|
||||
# Init weights, biases
|
||||
initialize_weights(self)
|
||||
self.info()
|
||||
logger.info('')
|
||||
|
||||
def forward(self, x, augment=False, profile=False):
|
||||
if augment:
|
||||
img_size = x.shape[-2:] # height, width
|
||||
s = [1, 0.83, 0.67] # scales
|
||||
f = [None, 3, None] # flips (2-ud, 3-lr)
|
||||
y = [] # outputs
|
||||
for si, fi in zip(s, f):
|
||||
xi = scale_img(x.flip(fi) if fi else x, si, gs=int(self.stride.max()))
|
||||
yi = self.forward_once(xi)[0] # forward
|
||||
# cv2.imwrite(f'img_{si}.jpg', 255 * xi[0].cpu().numpy().transpose((1, 2, 0))[:, :, ::-1]) # save
|
||||
yi[..., :4] /= si # de-scale
|
||||
if fi == 2:
|
||||
yi[..., 1] = img_size[0] - yi[..., 1] # de-flip ud
|
||||
elif fi == 3:
|
||||
yi[..., 0] = img_size[1] - yi[..., 0] # de-flip lr
|
||||
y.append(yi)
|
||||
return torch.cat(y, 1), None # augmented inference, train
|
||||
else:
|
||||
return self.forward_once(x, profile) # single-scale inference, train
|
||||
|
||||
def forward_once(self, x, profile=False):
|
||||
y, dt = [], [] # outputs
|
||||
for m in self.model:
|
||||
if m.f != -1: # if not from previous layer
|
||||
x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers
|
||||
|
||||
if not hasattr(self, 'traced'):
|
||||
self.traced=False
|
||||
|
||||
if self.traced:
|
||||
if isinstance(m, Detect) or isinstance(m, IDetect) or isinstance(m, IAuxDetect) or isinstance(m, IKeypoint):
|
||||
break
|
||||
|
||||
if profile:
|
||||
c = isinstance(m, (Detect, IDetect, IAuxDetect, IBin))
|
||||
o = thop.profile(m, inputs=(x.copy() if c else x,), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPS
|
||||
for _ in range(10):
|
||||
m(x.copy() if c else x)
|
||||
t = time_synchronized()
|
||||
for _ in range(10):
|
||||
m(x.copy() if c else x)
|
||||
dt.append((time_synchronized() - t) * 100)
|
||||
print('%10.1f%10.0f%10.1fms %-40s' % (o, m.np, dt[-1], m.type))
|
||||
|
||||
x = m(x) # run
|
||||
|
||||
y.append(x if m.i in self.save else None) # save output
|
||||
|
||||
if profile:
|
||||
print('%.1fms total' % sum(dt))
|
||||
return x
|
||||
|
||||
def _initialize_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency
|
||||
# https://arxiv.org/abs/1708.02002 section 3.3
|
||||
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
|
||||
m = self.model[-1] # Detect() module
|
||||
for mi, s in zip(m.m, m.stride): # from
|
||||
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
|
||||
b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
|
||||
b.data[:, 5:] += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls
|
||||
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
|
||||
|
||||
def _initialize_aux_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency
|
||||
# https://arxiv.org/abs/1708.02002 section 3.3
|
||||
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
|
||||
m = self.model[-1] # Detect() module
|
||||
for mi, mi2, s in zip(m.m, m.m2, m.stride): # from
|
||||
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
|
||||
b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
|
||||
b.data[:, 5:] += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls
|
||||
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
|
||||
b2 = mi2.bias.view(m.na, -1) # conv.bias(255) to (3,85)
|
||||
b2.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
|
||||
b2.data[:, 5:] += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls
|
||||
mi2.bias = torch.nn.Parameter(b2.view(-1), requires_grad=True)
|
||||
|
||||
def _initialize_biases_bin(self, cf=None): # initialize biases into Detect(), cf is class frequency
|
||||
# https://arxiv.org/abs/1708.02002 section 3.3
|
||||
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
|
||||
m = self.model[-1] # Bin() module
|
||||
bc = m.bin_count
|
||||
for mi, s in zip(m.m, m.stride): # from
|
||||
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
|
||||
old = b[:, (0,1,2,bc+3)].data
|
||||
obj_idx = 2*bc+4
|
||||
b[:, :obj_idx].data += math.log(0.6 / (bc + 1 - 0.99))
|
||||
b[:, obj_idx].data += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
|
||||
b[:, (obj_idx+1):].data += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls
|
||||
b[:, (0,1,2,bc+3)].data = old
|
||||
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
|
||||
|
||||
def _initialize_biases_kpt(self, cf=None): # initialize biases into Detect(), cf is class frequency
|
||||
# https://arxiv.org/abs/1708.02002 section 3.3
|
||||
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
|
||||
m = self.model[-1] # Detect() module
|
||||
for mi, s in zip(m.m, m.stride): # from
|
||||
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
|
||||
b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
|
||||
b.data[:, 5:] += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls
|
||||
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
|
||||
|
||||
def _print_biases(self):
|
||||
m = self.model[-1] # Detect() module
|
||||
for mi in m.m: # from
|
||||
b = mi.bias.detach().view(m.na, -1).T # conv.bias(255) to (3,85)
|
||||
print(('%6g Conv2d.bias:' + '%10.3g' * 6) % (mi.weight.shape[1], *b[:5].mean(1).tolist(), b[5:].mean()))
|
||||
|
||||
# def _print_weights(self):
|
||||
# for m in self.model.modules():
|
||||
# if type(m) is Bottleneck:
|
||||
# print('%10.3g' % (m.w.detach().sigmoid() * 2)) # shortcut weights
|
||||
|
||||
def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers
|
||||
print('Fusing layers... ')
|
||||
for m in self.model.modules():
|
||||
if isinstance(m, RepConv):
|
||||
#print(f" fuse_repvgg_block")
|
||||
m.fuse_repvgg_block()
|
||||
elif isinstance(m, RepConv_OREPA):
|
||||
#print(f" switch_to_deploy")
|
||||
m.switch_to_deploy()
|
||||
elif type(m) is Conv and hasattr(m, 'bn'):
|
||||
m.conv = fuse_conv_and_bn(m.conv, m.bn) # update conv
|
||||
delattr(m, 'bn') # remove batchnorm
|
||||
m.forward = m.fuseforward # update forward
|
||||
elif isinstance(m, (IDetect, IAuxDetect)):
|
||||
m.fuse()
|
||||
m.forward = m.fuseforward
|
||||
self.info()
|
||||
return self
|
||||
|
||||
def nms(self, mode=True): # add or remove NMS module
|
||||
present = type(self.model[-1]) is NMS # last layer is NMS
|
||||
if mode and not present:
|
||||
print('Adding NMS... ')
|
||||
m = NMS() # module
|
||||
m.f = -1 # from
|
||||
m.i = self.model[-1].i + 1 # index
|
||||
self.model.add_module(name='%s' % m.i, module=m) # add
|
||||
self.eval()
|
||||
elif not mode and present:
|
||||
print('Removing NMS... ')
|
||||
self.model = self.model[:-1] # remove
|
||||
return self
|
||||
|
||||
def autoshape(self): # add autoShape module
|
||||
print('Adding autoShape... ')
|
||||
m = autoShape(self) # wrap model
|
||||
copy_attr(m, self, include=('yaml', 'nc', 'hyp', 'names', 'stride'), exclude=()) # copy attributes
|
||||
return m
|
||||
|
||||
def info(self, verbose=False, img_size=640): # print model information
|
||||
model_info(self, verbose, img_size)
|
||||
|
||||
|
||||
def parse_model(d, ch): # model_dict, input_channels(3)
|
||||
logger.info('\n%3s%18s%3s%10s %-40s%-30s' % ('', 'from', 'n', 'params', 'module', 'arguments'))
|
||||
anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']
|
||||
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors
|
||||
no = na * (nc + 5) # number of outputs = anchors * (classes + 5)
|
||||
|
||||
layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out
|
||||
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']): # from, number, module, args
|
||||
m = eval(m) if isinstance(m, str) else m # eval strings
|
||||
for j, a in enumerate(args):
|
||||
try:
|
||||
args[j] = eval(a) if isinstance(a, str) else a # eval strings
|
||||
except:
|
||||
pass
|
||||
|
||||
n = max(round(n * gd), 1) if n > 1 else n # depth gain
|
||||
if m in [nn.Conv2d, Conv, RobustConv, RobustConv2, DWConv, GhostConv, RepConv, RepConv_OREPA, DownC,
|
||||
SPP, SPPF, SPPCSPC, GhostSPPCSPC, MixConv2d, Focus, Stem, GhostStem, CrossConv,
|
||||
Bottleneck, BottleneckCSPA, BottleneckCSPB, BottleneckCSPC,
|
||||
RepBottleneck, RepBottleneckCSPA, RepBottleneckCSPB, RepBottleneckCSPC,
|
||||
Res, ResCSPA, ResCSPB, ResCSPC,
|
||||
RepRes, RepResCSPA, RepResCSPB, RepResCSPC,
|
||||
ResX, ResXCSPA, ResXCSPB, ResXCSPC,
|
||||
RepResX, RepResXCSPA, RepResXCSPB, RepResXCSPC,
|
||||
Ghost, GhostCSPA, GhostCSPB, GhostCSPC,
|
||||
SwinTransformerBlock, STCSPA, STCSPB, STCSPC,
|
||||
SwinTransformer2Block, ST2CSPA, ST2CSPB, ST2CSPC]:
|
||||
c1, c2 = ch[f], args[0]
|
||||
if c2 != no: # if not output
|
||||
c2 = make_divisible(c2 * gw, 8)
|
||||
|
||||
args = [c1, c2, *args[1:]]
|
||||
if m in [DownC, SPPCSPC, GhostSPPCSPC,
|
||||
BottleneckCSPA, BottleneckCSPB, BottleneckCSPC,
|
||||
RepBottleneckCSPA, RepBottleneckCSPB, RepBottleneckCSPC,
|
||||
ResCSPA, ResCSPB, ResCSPC,
|
||||
RepResCSPA, RepResCSPB, RepResCSPC,
|
||||
ResXCSPA, ResXCSPB, ResXCSPC,
|
||||
RepResXCSPA, RepResXCSPB, RepResXCSPC,
|
||||
GhostCSPA, GhostCSPB, GhostCSPC,
|
||||
STCSPA, STCSPB, STCSPC,
|
||||
ST2CSPA, ST2CSPB, ST2CSPC]:
|
||||
args.insert(2, n) # number of repeats
|
||||
n = 1
|
||||
elif m is nn.BatchNorm2d:
|
||||
args = [ch[f]]
|
||||
elif m is Concat:
|
||||
c2 = sum([ch[x] for x in f])
|
||||
elif m is Chuncat:
|
||||
c2 = sum([ch[x] for x in f])
|
||||
elif m is Shortcut:
|
||||
c2 = ch[f[0]]
|
||||
elif m is Foldcut:
|
||||
c2 = ch[f] // 2
|
||||
elif m in [Detect, IDetect, IAuxDetect, IBin, IKeypoint]:
|
||||
args.append([ch[x] for x in f])
|
||||
if isinstance(args[1], int): # number of anchors
|
||||
args[1] = [list(range(args[1] * 2))] * len(f)
|
||||
elif m is ReOrg:
|
||||
c2 = ch[f] * 4
|
||||
elif m is Contract:
|
||||
c2 = ch[f] * args[0] ** 2
|
||||
elif m is Expand:
|
||||
c2 = ch[f] // args[0] ** 2
|
||||
else:
|
||||
c2 = ch[f]
|
||||
|
||||
m_ = nn.Sequential(*[m(*args) for _ in range(n)]) if n > 1 else m(*args) # module
|
||||
t = str(m)[8:-2].replace('__main__.', '') # module type
|
||||
np = sum([x.numel() for x in m_.parameters()]) # number params
|
||||
m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
|
||||
logger.info('%3s%18s%3s%10.0f %-40s%-30s' % (i, f, n, np, t, args)) # print
|
||||
save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
|
||||
layers.append(m_)
|
||||
if i == 0:
|
||||
ch = []
|
||||
ch.append(c2)
|
||||
return nn.Sequential(*layers), sorted(save)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--cfg', type=str, default='yolor-csp-c.yaml', help='model.yaml')
|
||||
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
|
||||
parser.add_argument('--profile', action='store_true', help='profile model speed')
|
||||
opt = parser.parse_args()
|
||||
opt.cfg = check_file(opt.cfg) # check file
|
||||
set_logging()
|
||||
device = select_device(opt.device)
|
||||
|
||||
# Create model
|
||||
model = Model(opt.cfg).to(device)
|
||||
model.train()
|
||||
|
||||
if opt.profile:
|
||||
img = torch.rand(1, 3, 640, 640).to(device)
|
||||
y = model(img, profile=True)
|
||||
|
||||
# Profile
|
||||
# img = torch.rand(8 if torch.cuda.is_available() else 1, 3, 640, 640).to(device)
|
||||
# y = model(img, profile=True)
|
||||
|
||||
# Tensorboard
|
||||
# from torch.utils.tensorboard import SummaryWriter
|
||||
# tb_writer = SummaryWriter()
|
||||
# print("Run 'tensorboard --logdir=models/runs' to view tensorboard at http://localhost:6006/")
|
||||
# tb_writer.add_graph(model.model, img) # add model to tensorboard
|
||||
# tb_writer.add_image('test', img[0], dataformats='CWH') # add model to tensorboard
|
||||
|
|
@ -0,0 +1 @@
|
|||
# init
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# Activation functions
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
# SiLU https://arxiv.org/pdf/1606.08415.pdf ----------------------------------------------------------------------------
|
||||
class SiLU(nn.Module): # export-friendly version of nn.SiLU()
|
||||
@staticmethod
|
||||
def forward(x):
|
||||
return x * torch.sigmoid(x)
|
||||
|
||||
|
||||
class Hardswish(nn.Module): # export-friendly version of nn.Hardswish()
|
||||
@staticmethod
|
||||
def forward(x):
|
||||
# return x * F.hardsigmoid(x) # for torchscript and CoreML
|
||||
return x * F.hardtanh(x + 3, 0., 6.) / 6. # for torchscript, CoreML and ONNX
|
||||
|
||||
|
||||
class MemoryEfficientSwish(nn.Module):
|
||||
class F(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(ctx, x):
|
||||
ctx.save_for_backward(x)
|
||||
return x * torch.sigmoid(x)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
x = ctx.saved_tensors[0]
|
||||
sx = torch.sigmoid(x)
|
||||
return grad_output * (sx * (1 + x * (1 - sx)))
|
||||
|
||||
def forward(self, x):
|
||||
return self.F.apply(x)
|
||||
|
||||
|
||||
# Mish https://github.com/digantamisra98/Mish --------------------------------------------------------------------------
|
||||
class Mish(nn.Module):
|
||||
@staticmethod
|
||||
def forward(x):
|
||||
return x * F.softplus(x).tanh()
|
||||
|
||||
|
||||
class MemoryEfficientMish(nn.Module):
|
||||
class F(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(ctx, x):
|
||||
ctx.save_for_backward(x)
|
||||
return x.mul(torch.tanh(F.softplus(x))) # x * tanh(ln(1 + exp(x)))
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
x = ctx.saved_tensors[0]
|
||||
sx = torch.sigmoid(x)
|
||||
fx = F.softplus(x).tanh()
|
||||
return grad_output * (fx + x * sx * (1 - fx * fx))
|
||||
|
||||
def forward(self, x):
|
||||
return self.F.apply(x)
|
||||
|
||||
|
||||
# FReLU https://arxiv.org/abs/2007.11824 -------------------------------------------------------------------------------
|
||||
class FReLU(nn.Module):
|
||||
def __init__(self, c1, k=3): # ch_in, kernel
|
||||
super().__init__()
|
||||
self.conv = nn.Conv2d(c1, c1, k, 1, 1, groups=c1, bias=False)
|
||||
self.bn = nn.BatchNorm2d(c1)
|
||||
|
||||
def forward(self, x):
|
||||
return torch.max(x, self.bn(self.conv(x)))
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import numpy as np
|
||||
import onnx
|
||||
from onnx import shape_inference
|
||||
try:
|
||||
import onnx_graphsurgeon as gs
|
||||
except Exception as e:
|
||||
print('Import onnx_graphsurgeon failure: %s' % e)
|
||||
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class RegisterNMS(object):
|
||||
def __init__(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
precision: str = "fp32",
|
||||
):
|
||||
|
||||
self.graph = gs.import_onnx(onnx.load(onnx_model_path))
|
||||
assert self.graph
|
||||
LOGGER.info("ONNX graph created successfully")
|
||||
# Fold constants via ONNX-GS that PyTorch2ONNX may have missed
|
||||
self.graph.fold_constants()
|
||||
self.precision = precision
|
||||
self.batch_size = 1
|
||||
def infer(self):
|
||||
"""
|
||||
Sanitize the graph by cleaning any unconnected nodes, do a topological resort,
|
||||
and fold constant inputs values. When possible, run shape inference on the
|
||||
ONNX graph to determine tensor shapes.
|
||||
"""
|
||||
for _ in range(3):
|
||||
count_before = len(self.graph.nodes)
|
||||
|
||||
self.graph.cleanup().toposort()
|
||||
try:
|
||||
for node in self.graph.nodes:
|
||||
for o in node.outputs:
|
||||
o.shape = None
|
||||
model = gs.export_onnx(self.graph)
|
||||
model = shape_inference.infer_shapes(model)
|
||||
self.graph = gs.import_onnx(model)
|
||||
except Exception as e:
|
||||
LOGGER.info(f"Shape inference could not be performed at this time:\n{e}")
|
||||
try:
|
||||
self.graph.fold_constants(fold_shapes=True)
|
||||
except TypeError as e:
|
||||
LOGGER.error(
|
||||
"This version of ONNX GraphSurgeon does not support folding shapes, "
|
||||
f"please upgrade your onnx_graphsurgeon module. Error:\n{e}"
|
||||
)
|
||||
raise
|
||||
|
||||
count_after = len(self.graph.nodes)
|
||||
if count_before == count_after:
|
||||
# No new folding occurred in this iteration, so we can stop for now.
|
||||
break
|
||||
|
||||
def save(self, output_path):
|
||||
"""
|
||||
Save the ONNX model to the given location.
|
||||
Args:
|
||||
output_path: Path pointing to the location where to write
|
||||
out the updated ONNX model.
|
||||
"""
|
||||
self.graph.cleanup().toposort()
|
||||
model = gs.export_onnx(self.graph)
|
||||
onnx.save(model, output_path)
|
||||
LOGGER.info(f"Saved ONNX model to {output_path}")
|
||||
|
||||
def register_nms(
|
||||
self,
|
||||
*,
|
||||
score_thresh: float = 0.25,
|
||||
nms_thresh: float = 0.45,
|
||||
detections_per_img: int = 100,
|
||||
):
|
||||
"""
|
||||
Register the ``EfficientNMS_TRT`` plugin node.
|
||||
NMS expects these shapes for its input tensors:
|
||||
- box_net: [batch_size, number_boxes, 4]
|
||||
- class_net: [batch_size, number_boxes, number_labels]
|
||||
Args:
|
||||
score_thresh (float): The scalar threshold for score (low scoring boxes are removed).
|
||||
nms_thresh (float): The scalar threshold for IOU (new boxes that have high IOU
|
||||
overlap with previously selected boxes are removed).
|
||||
detections_per_img (int): Number of best detections to keep after NMS.
|
||||
"""
|
||||
|
||||
self.infer()
|
||||
# Find the concat node at the end of the network
|
||||
op_inputs = self.graph.outputs
|
||||
op = "EfficientNMS_TRT"
|
||||
attrs = {
|
||||
"plugin_version": "1",
|
||||
"background_class": -1, # no background class
|
||||
"max_output_boxes": detections_per_img,
|
||||
"score_threshold": score_thresh,
|
||||
"iou_threshold": nms_thresh,
|
||||
"score_activation": False,
|
||||
"box_coding": 0,
|
||||
}
|
||||
|
||||
if self.precision == "fp32":
|
||||
dtype_output = np.float32
|
||||
elif self.precision == "fp16":
|
||||
dtype_output = np.float16
|
||||
else:
|
||||
raise NotImplementedError(f"Currently not supports precision: {self.precision}")
|
||||
|
||||
# NMS Outputs
|
||||
output_num_detections = gs.Variable(
|
||||
name="num_dets",
|
||||
dtype=np.int32,
|
||||
shape=[self.batch_size, 1],
|
||||
) # A scalar indicating the number of valid detections per batch image.
|
||||
output_boxes = gs.Variable(
|
||||
name="det_boxes",
|
||||
dtype=dtype_output,
|
||||
shape=[self.batch_size, detections_per_img, 4],
|
||||
)
|
||||
output_scores = gs.Variable(
|
||||
name="det_scores",
|
||||
dtype=dtype_output,
|
||||
shape=[self.batch_size, detections_per_img],
|
||||
)
|
||||
output_labels = gs.Variable(
|
||||
name="det_classes",
|
||||
dtype=np.int32,
|
||||
shape=[self.batch_size, detections_per_img],
|
||||
)
|
||||
|
||||
op_outputs = [output_num_detections, output_boxes, output_scores, output_labels]
|
||||
|
||||
# Create the NMS Plugin node with the selected inputs. The outputs of the node will also
|
||||
# become the final outputs of the graph.
|
||||
self.graph.layer(op=op, name="batched_nms", inputs=op_inputs, outputs=op_outputs, attrs=attrs)
|
||||
LOGGER.info(f"Created NMS plugin '{op}' with attributes: {attrs}")
|
||||
|
||||
self.graph.outputs = op_outputs
|
||||
|
||||
self.infer()
|
||||
|
||||
def save(self, output_path):
|
||||
"""
|
||||
Save the ONNX model to the given location.
|
||||
Args:
|
||||
output_path: Path pointing to the location where to write
|
||||
out the updated ONNX model.
|
||||
"""
|
||||
self.graph.cleanup().toposort()
|
||||
model = gs.export_onnx(self.graph)
|
||||
onnx.save(model, output_path)
|
||||
LOGGER.info(f"Saved ONNX model to {output_path}")
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
# Auto-anchor utils
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import yaml
|
||||
from scipy.cluster.vq import kmeans
|
||||
from tqdm import tqdm
|
||||
|
||||
from utils.general import colorstr
|
||||
|
||||
|
||||
def check_anchor_order(m):
|
||||
# Check anchor order against stride order for YOLO Detect() module m, and correct if necessary
|
||||
a = m.anchor_grid.prod(-1).view(-1) # anchor area
|
||||
da = a[-1] - a[0] # delta a
|
||||
ds = m.stride[-1] - m.stride[0] # delta s
|
||||
if da.sign() != ds.sign(): # same order
|
||||
print('Reversing anchor order')
|
||||
m.anchors[:] = m.anchors.flip(0)
|
||||
m.anchor_grid[:] = m.anchor_grid.flip(0)
|
||||
|
||||
|
||||
def check_anchors(dataset, model, thr=4.0, imgsz=640):
|
||||
# Check anchor fit to data, recompute if necessary
|
||||
prefix = colorstr('autoanchor: ')
|
||||
print(f'\n{prefix}Analyzing anchors... ', end='')
|
||||
m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect()
|
||||
shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
||||
scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale
|
||||
wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float() # wh
|
||||
|
||||
def metric(k): # compute metric
|
||||
r = wh[:, None] / k[None]
|
||||
x = torch.min(r, 1. / r).min(2)[0] # ratio metric
|
||||
best = x.max(1)[0] # best_x
|
||||
aat = (x > 1. / thr).float().sum(1).mean() # anchors above threshold
|
||||
bpr = (best > 1. / thr).float().mean() # best possible recall
|
||||
return bpr, aat
|
||||
|
||||
anchors = m.anchor_grid.clone().cpu().view(-1, 2) # current anchors
|
||||
bpr, aat = metric(anchors)
|
||||
print(f'anchors/target = {aat:.2f}, Best Possible Recall (BPR) = {bpr:.4f}', end='')
|
||||
if bpr < 0.98: # threshold to recompute
|
||||
print('. Attempting to improve anchors, please wait...')
|
||||
na = m.anchor_grid.numel() // 2 # number of anchors
|
||||
try:
|
||||
anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
|
||||
except Exception as e:
|
||||
print(f'{prefix}ERROR: {e}')
|
||||
new_bpr = metric(anchors)[0]
|
||||
if new_bpr > bpr: # replace anchors
|
||||
anchors = torch.tensor(anchors, device=m.anchors.device).type_as(m.anchors)
|
||||
m.anchor_grid[:] = anchors.clone().view_as(m.anchor_grid) # for inference
|
||||
check_anchor_order(m)
|
||||
m.anchors[:] = anchors.clone().view_as(m.anchors) / m.stride.to(m.anchors.device).view(-1, 1, 1) # loss
|
||||
print(f'{prefix}New anchors saved to model. Update model *.yaml to use these anchors in the future.')
|
||||
else:
|
||||
print(f'{prefix}Original anchors better than new anchors. Proceeding with original anchors.')
|
||||
print('') # newline
|
||||
|
||||
|
||||
def kmean_anchors(path='./data/coco.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
|
||||
""" Creates kmeans-evolved anchors from training dataset
|
||||
|
||||
Arguments:
|
||||
path: path to dataset *.yaml, or a loaded dataset
|
||||
n: number of anchors
|
||||
img_size: image size used for training
|
||||
thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0
|
||||
gen: generations to evolve anchors using genetic algorithm
|
||||
verbose: print all results
|
||||
|
||||
Return:
|
||||
k: kmeans evolved anchors
|
||||
|
||||
Usage:
|
||||
from utils.autoanchor import *; _ = kmean_anchors()
|
||||
"""
|
||||
thr = 1. / thr
|
||||
prefix = colorstr('autoanchor: ')
|
||||
|
||||
def metric(k, wh): # compute metrics
|
||||
r = wh[:, None] / k[None]
|
||||
x = torch.min(r, 1. / r).min(2)[0] # ratio metric
|
||||
# x = wh_iou(wh, torch.tensor(k)) # iou metric
|
||||
return x, x.max(1)[0] # x, best_x
|
||||
|
||||
def anchor_fitness(k): # mutation fitness
|
||||
_, best = metric(torch.tensor(k, dtype=torch.float32), wh)
|
||||
return (best * (best > thr).float()).mean() # fitness
|
||||
|
||||
def print_results(k):
|
||||
k = k[np.argsort(k.prod(1))] # sort small to large
|
||||
x, best = metric(k, wh0)
|
||||
bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr
|
||||
print(f'{prefix}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr')
|
||||
print(f'{prefix}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, '
|
||||
f'past_thr={x[x > thr].mean():.3f}-mean: ', end='')
|
||||
for i, x in enumerate(k):
|
||||
print('%i,%i' % (round(x[0]), round(x[1])), end=', ' if i < len(k) - 1 else '\n') # use in *.cfg
|
||||
return k
|
||||
|
||||
if isinstance(path, str): # *.yaml file
|
||||
with open(path) as f:
|
||||
data_dict = yaml.load(f, Loader=yaml.SafeLoader) # model dict
|
||||
from utils.datasets import LoadImagesAndLabels
|
||||
dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
|
||||
else:
|
||||
dataset = path # dataset
|
||||
|
||||
# Get label wh
|
||||
shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
||||
wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)]) # wh
|
||||
|
||||
# Filter
|
||||
i = (wh0 < 3.0).any(1).sum()
|
||||
if i:
|
||||
print(f'{prefix}WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.')
|
||||
wh = wh0[(wh0 >= 2.0).any(1)] # filter > 2 pixels
|
||||
# wh = wh * (np.random.rand(wh.shape[0], 1) * 0.9 + 0.1) # multiply by random scale 0-1
|
||||
|
||||
# Kmeans calculation
|
||||
print(f'{prefix}Running kmeans for {n} anchors on {len(wh)} points...')
|
||||
s = wh.std(0) # sigmas for whitening
|
||||
k, dist = kmeans(wh / s, n, iter=30) # points, mean distance
|
||||
assert len(k) == n, print(f'{prefix}ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}')
|
||||
k *= s
|
||||
wh = torch.tensor(wh, dtype=torch.float32) # filtered
|
||||
wh0 = torch.tensor(wh0, dtype=torch.float32) # unfiltered
|
||||
k = print_results(k)
|
||||
|
||||
# Plot
|
||||
# k, d = [None] * 20, [None] * 20
|
||||
# for i in tqdm(range(1, 21)):
|
||||
# k[i-1], d[i-1] = kmeans(wh / s, i) # points, mean distance
|
||||
# fig, ax = plt.subplots(1, 2, figsize=(14, 7), tight_layout=True)
|
||||
# ax = ax.ravel()
|
||||
# ax[0].plot(np.arange(1, 21), np.array(d) ** 2, marker='.')
|
||||
# fig, ax = plt.subplots(1, 2, figsize=(14, 7)) # plot wh
|
||||
# ax[0].hist(wh[wh[:, 0]<100, 0],400)
|
||||
# ax[1].hist(wh[wh[:, 1]<100, 1],400)
|
||||
# fig.savefig('wh.png', dpi=200)
|
||||
|
||||
# Evolve
|
||||
npr = np.random
|
||||
f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
|
||||
pbar = tqdm(range(gen), desc=f'{prefix}Evolving anchors with Genetic Algorithm:') # progress bar
|
||||
for _ in pbar:
|
||||
v = np.ones(sh)
|
||||
while (v == 1).all(): # mutate until a change occurs (prevent duplicates)
|
||||
v = ((npr.random(sh) < mp) * npr.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
|
||||
kg = (k.copy() * v).clip(min=2.0)
|
||||
fg = anchor_fitness(kg)
|
||||
if fg > f:
|
||||
f, k = fg, kg.copy()
|
||||
pbar.desc = f'{prefix}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'
|
||||
if verbose:
|
||||
print_results(k)
|
||||
|
||||
return print_results(k)
|
||||
|
|
@ -0,0 +1 @@
|
|||
#init
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# AWS EC2 instance startup 'MIME' script https://aws.amazon.com/premiumsupport/knowledge-center/execute-user-data-ec2/
|
||||
# This script will run on every instance restart, not only on first start
|
||||
# --- DO NOT COPY ABOVE COMMENTS WHEN PASTING INTO USERDATA ---
|
||||
|
||||
Content-Type: multipart/mixed; boundary="//"
|
||||
MIME-Version: 1.0
|
||||
|
||||
--//
|
||||
Content-Type: text/cloud-config; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: attachment; filename="cloud-config.txt"
|
||||
|
||||
#cloud-config
|
||||
cloud_final_modules:
|
||||
- [scripts-user, always]
|
||||
|
||||
--//
|
||||
Content-Type: text/x-shellscript; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: attachment; filename="userdata.txt"
|
||||
|
||||
#!/bin/bash
|
||||
# --- paste contents of userdata.sh here ---
|
||||
--//
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Resume all interrupted trainings in yolor/ dir including DDP trainings
|
||||
# Usage: $ python utils/aws/resume.py
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
sys.path.append('./') # to run '$ python *.py' files in subdirectories
|
||||
|
||||
port = 0 # --master_port
|
||||
path = Path('').resolve()
|
||||
for last in path.rglob('*/**/last.pt'):
|
||||
ckpt = torch.load(last)
|
||||
if ckpt['optimizer'] is None:
|
||||
continue
|
||||
|
||||
# Load opt.yaml
|
||||
with open(last.parent.parent / 'opt.yaml') as f:
|
||||
opt = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
|
||||
# Get device count
|
||||
d = opt['device'].split(',') # devices
|
||||
nd = len(d) # number of devices
|
||||
ddp = nd > 1 or (nd == 0 and torch.cuda.device_count() > 1) # distributed data parallel
|
||||
|
||||
if ddp: # multi-GPU
|
||||
port += 1
|
||||
cmd = f'python -m torch.distributed.launch --nproc_per_node {nd} --master_port {port} train.py --resume {last}'
|
||||
else: # single-GPU
|
||||
cmd = f'python train.py --resume {last}'
|
||||
|
||||
cmd += ' > /dev/null 2>&1 &' # redirect output to dev/null and run in daemon thread
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
# AWS EC2 instance startup script https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
|
||||
# This script will run only once on first instance start (for a re-start script see mime.sh)
|
||||
# /home/ubuntu (ubuntu) or /home/ec2-user (amazon-linux) is working dir
|
||||
# Use >300 GB SSD
|
||||
|
||||
cd home/ubuntu
|
||||
if [ ! -d yolor ]; then
|
||||
echo "Running first-time script." # install dependencies, download COCO, pull Docker
|
||||
git clone -b main https://github.com/WongKinYiu/yolov7 && sudo chmod -R 777 yolov7
|
||||
cd yolov7
|
||||
bash data/scripts/get_coco.sh && echo "Data done." &
|
||||
sudo docker pull nvcr.io/nvidia/pytorch:21.08-py3 && echo "Docker done." &
|
||||
python -m pip install --upgrade pip && pip install -r requirements.txt && python detect.py && echo "Requirements done." &
|
||||
wait && echo "All tasks done." # finish background tasks
|
||||
else
|
||||
echo "Running re-start script." # resume interrupted runs
|
||||
i=0
|
||||
list=$(sudo docker ps -qa) # container list i.e. $'one\ntwo\nthree\nfour'
|
||||
while IFS= read -r id; do
|
||||
((i++))
|
||||
echo "restarting container $i: $id"
|
||||
sudo docker start $id
|
||||
# sudo docker exec -it $id python train.py --resume # single-GPU
|
||||
sudo docker exec -d $id python utils/aws/resume.py # multi-scenario
|
||||
done <<<"$list"
|
||||
fi
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,892 @@
|
|||
# YOLOR general utils
|
||||
|
||||
import glob
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import torchvision
|
||||
import yaml
|
||||
|
||||
from utils.google_utils import gsutil_getsize
|
||||
from utils.metrics import fitness
|
||||
from utils.torch_utils import init_torch_seeds
|
||||
|
||||
# Settings
|
||||
torch.set_printoptions(linewidth=320, precision=5, profile='long')
|
||||
np.set_printoptions(linewidth=320, formatter={'float_kind': '{:11.5g}'.format}) # format short g, %precision=5
|
||||
pd.options.display.max_columns = 10
|
||||
cv2.setNumThreads(0) # prevent OpenCV from multithreading (incompatible with PyTorch DataLoader)
|
||||
os.environ['NUMEXPR_MAX_THREADS'] = str(min(os.cpu_count(), 8)) # NumExpr max threads
|
||||
|
||||
|
||||
def set_logging(rank=-1):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
level=logging.INFO if rank in [-1, 0] else logging.WARN)
|
||||
|
||||
|
||||
def init_seeds(seed=0):
|
||||
# Initialize random number generator (RNG) seeds
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
init_torch_seeds(seed)
|
||||
|
||||
|
||||
def get_latest_run(search_dir='.'):
|
||||
# Return path to most recent 'last.pt' in /runs (i.e. to --resume from)
|
||||
last_list = glob.glob(f'{search_dir}/**/last*.pt', recursive=True)
|
||||
return max(last_list, key=os.path.getctime) if last_list else ''
|
||||
|
||||
|
||||
def isdocker():
|
||||
# Is environment a Docker container
|
||||
return Path('/workspace').exists() # or Path('/.dockerenv').exists()
|
||||
|
||||
|
||||
def emojis(str=''):
|
||||
# Return platform-dependent emoji-safe version of string
|
||||
return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str
|
||||
|
||||
|
||||
def check_online():
|
||||
# Check internet connectivity
|
||||
import socket
|
||||
try:
|
||||
socket.create_connection(("1.1.1.1", 443), 5) # check host accesability
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def check_git_status():
|
||||
# Recommend 'git pull' if code is out of date
|
||||
print(colorstr('github: '), end='')
|
||||
try:
|
||||
assert Path('.git').exists(), 'skipping check (not a git repository)'
|
||||
assert not isdocker(), 'skipping check (Docker image)'
|
||||
assert check_online(), 'skipping check (offline)'
|
||||
|
||||
cmd = 'git fetch && git config --get remote.origin.url'
|
||||
url = subprocess.check_output(cmd, shell=True).decode().strip().rstrip('.git') # github repo url
|
||||
branch = subprocess.check_output('git rev-parse --abbrev-ref HEAD', shell=True).decode().strip() # checked out
|
||||
n = int(subprocess.check_output(f'git rev-list {branch}..origin/master --count', shell=True)) # commits behind
|
||||
if n > 0:
|
||||
s = f"⚠️ WARNING: code is out of date by {n} commit{'s' * (n > 1)}. " \
|
||||
f"Use 'git pull' to update or 'git clone {url}' to download latest."
|
||||
else:
|
||||
s = f'up to date with {url} ✅'
|
||||
print(emojis(s)) # emoji-safe
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
def check_requirements(requirements='requirements.txt', exclude=()):
|
||||
# Check installed dependencies meet requirements (pass *.txt file or list of packages)
|
||||
import pkg_resources as pkg
|
||||
prefix = colorstr('red', 'bold', 'requirements:')
|
||||
if isinstance(requirements, (str, Path)): # requirements.txt file
|
||||
file = Path(requirements)
|
||||
if not file.exists():
|
||||
print(f"{prefix} {file.resolve()} not found, check failed.")
|
||||
return
|
||||
requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(file.open()) if x.name not in exclude]
|
||||
else: # list or tuple of packages
|
||||
requirements = [x for x in requirements if x not in exclude]
|
||||
|
||||
n = 0 # number of packages updates
|
||||
for r in requirements:
|
||||
try:
|
||||
pkg.require(r)
|
||||
except Exception as e: # DistributionNotFound or VersionConflict if requirements not met
|
||||
n += 1
|
||||
print(f"{prefix} {e.req} not found and is required by YOLOR, attempting auto-update...")
|
||||
print(subprocess.check_output(f"pip install '{e.req}'", shell=True).decode())
|
||||
|
||||
if n: # if packages updated
|
||||
source = file.resolve() if 'file' in locals() else requirements
|
||||
s = f"{prefix} {n} package{'s' * (n > 1)} updated per {source}\n" \
|
||||
f"{prefix} ⚠️ {colorstr('bold', 'Restart runtime or rerun command for updates to take effect')}\n"
|
||||
print(emojis(s)) # emoji-safe
|
||||
|
||||
|
||||
def check_img_size(img_size, s=32):
|
||||
# Verify img_size is a multiple of stride s
|
||||
new_size = make_divisible(img_size, int(s)) # ceil gs-multiple
|
||||
if new_size != img_size:
|
||||
print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size))
|
||||
return new_size
|
||||
|
||||
|
||||
def check_imshow():
|
||||
# Check if environment supports image displays
|
||||
try:
|
||||
assert not isdocker(), 'cv2.imshow() is disabled in Docker environments'
|
||||
cv2.imshow('test', np.zeros((1, 1, 3)))
|
||||
cv2.waitKey(1)
|
||||
cv2.destroyAllWindows()
|
||||
cv2.waitKey(1)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f'WARNING: Environment does not support cv2.imshow() or PIL Image.show() image displays\n{e}')
|
||||
return False
|
||||
|
||||
|
||||
def check_file(file):
|
||||
# Search for file if not found
|
||||
if Path(file).is_file() or file == '':
|
||||
return file
|
||||
else:
|
||||
files = glob.glob('./**/' + file, recursive=True) # find file
|
||||
assert len(files), f'File Not Found: {file}' # assert file was found
|
||||
assert len(files) == 1, f"Multiple files match '{file}', specify exact path: {files}" # assert unique
|
||||
return files[0] # return file
|
||||
|
||||
|
||||
def check_dataset(dict):
|
||||
# Download dataset if not found locally
|
||||
val, s = dict.get('val'), dict.get('download')
|
||||
if val and len(val):
|
||||
val = [Path(x).resolve() for x in (val if isinstance(val, list) else [val])] # val path
|
||||
if not all(x.exists() for x in val):
|
||||
print('\nWARNING: Dataset not found, nonexistent paths: %s' % [str(x) for x in val if not x.exists()])
|
||||
if s and len(s): # download script
|
||||
print('Downloading %s ...' % s)
|
||||
if s.startswith('http') and s.endswith('.zip'): # URL
|
||||
f = Path(s).name # filename
|
||||
torch.hub.download_url_to_file(s, f)
|
||||
r = os.system('unzip -q %s -d ../ && rm %s' % (f, f)) # unzip
|
||||
else: # bash script
|
||||
r = os.system(s)
|
||||
print('Dataset autodownload %s\n' % ('success' if r == 0 else 'failure')) # analyze return value
|
||||
else:
|
||||
raise Exception('Dataset not found.')
|
||||
|
||||
|
||||
def make_divisible(x, divisor):
|
||||
# Returns x evenly divisible by divisor
|
||||
return math.ceil(x / divisor) * divisor
|
||||
|
||||
|
||||
def clean_str(s):
|
||||
# Cleans a string by replacing special characters with underscore _
|
||||
return re.sub(pattern="[|@#!¡·$€%&()=?¿^*;:,¨´><+]", repl="_", string=s)
|
||||
|
||||
|
||||
def one_cycle(y1=0.0, y2=1.0, steps=100):
|
||||
# lambda function for sinusoidal ramp from y1 to y2
|
||||
return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1
|
||||
|
||||
|
||||
def colorstr(*input):
|
||||
# Colors a string https://en.wikipedia.org/wiki/ANSI_escape_code, i.e. colorstr('blue', 'hello world')
|
||||
*args, string = input if len(input) > 1 else ('blue', 'bold', input[0]) # color arguments, string
|
||||
colors = {'black': '\033[30m', # basic colors
|
||||
'red': '\033[31m',
|
||||
'green': '\033[32m',
|
||||
'yellow': '\033[33m',
|
||||
'blue': '\033[34m',
|
||||
'magenta': '\033[35m',
|
||||
'cyan': '\033[36m',
|
||||
'white': '\033[37m',
|
||||
'bright_black': '\033[90m', # bright colors
|
||||
'bright_red': '\033[91m',
|
||||
'bright_green': '\033[92m',
|
||||
'bright_yellow': '\033[93m',
|
||||
'bright_blue': '\033[94m',
|
||||
'bright_magenta': '\033[95m',
|
||||
'bright_cyan': '\033[96m',
|
||||
'bright_white': '\033[97m',
|
||||
'end': '\033[0m', # misc
|
||||
'bold': '\033[1m',
|
||||
'underline': '\033[4m'}
|
||||
return ''.join(colors[x] for x in args) + f'{string}' + colors['end']
|
||||
|
||||
|
||||
def labels_to_class_weights(labels, nc=80):
|
||||
# Get class weights (inverse frequency) from training labels
|
||||
if labels[0] is None: # no labels loaded
|
||||
return torch.Tensor()
|
||||
|
||||
labels = np.concatenate(labels, 0) # labels.shape = (866643, 5) for COCO
|
||||
classes = labels[:, 0].astype(np.int32) # labels = [class xywh]
|
||||
weights = np.bincount(classes, minlength=nc) # occurrences per class
|
||||
|
||||
# Prepend gridpoint count (for uCE training)
|
||||
# gpi = ((320 / 32 * np.array([1, 2, 4])) ** 2 * 3).sum() # gridpoints per image
|
||||
# weights = np.hstack([gpi * len(labels) - weights.sum() * 9, weights * 9]) ** 0.5 # prepend gridpoints to start
|
||||
|
||||
weights[weights == 0] = 1 # replace empty bins with 1
|
||||
weights = 1 / weights # number of targets per class
|
||||
weights /= weights.sum() # normalize
|
||||
return torch.from_numpy(weights)
|
||||
|
||||
|
||||
def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)):
|
||||
# Produces image weights based on class_weights and image contents
|
||||
class_counts = np.array([np.bincount(x[:, 0].astype(np.int32), minlength=nc) for x in labels])
|
||||
image_weights = (class_weights.reshape(1, nc) * class_counts).sum(1)
|
||||
# index = random.choices(range(n), weights=image_weights, k=1) # weight image sample
|
||||
return image_weights
|
||||
|
||||
|
||||
def coco80_to_coco91_class(): # converts 80-index (val2014) to 91-index (paper)
|
||||
# https://tech.amikelive.com/node-718/what-object-categories-labels-are-in-coco-dataset/
|
||||
# a = np.loadtxt('data/coco.names', dtype='str', delimiter='\n')
|
||||
# b = np.loadtxt('data/coco_paper.names', dtype='str', delimiter='\n')
|
||||
# x1 = [list(a[i] == b).index(True) + 1 for i in range(80)] # darknet to coco
|
||||
# x2 = [list(b[i] == a).index(True) if any(b[i] == a) else None for i in range(91)] # coco to darknet
|
||||
x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34,
|
||||
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
|
||||
64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90]
|
||||
return x
|
||||
|
||||
|
||||
def xyxy2xywh(x):
|
||||
# Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = (x[:, 0] + x[:, 2]) / 2 # x center
|
||||
y[:, 1] = (x[:, 1] + x[:, 3]) / 2 # y center
|
||||
y[:, 2] = x[:, 2] - x[:, 0] # width
|
||||
y[:, 3] = x[:, 3] - x[:, 1] # height
|
||||
return y
|
||||
|
||||
|
||||
def xywh2xyxy(x):
|
||||
# Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x
|
||||
y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y
|
||||
y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x
|
||||
y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y
|
||||
return y
|
||||
|
||||
|
||||
def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0):
|
||||
# Convert nx4 boxes from [x, y, w, h] normalized to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = w * (x[:, 0] - x[:, 2] / 2) + padw # top left x
|
||||
y[:, 1] = h * (x[:, 1] - x[:, 3] / 2) + padh # top left y
|
||||
y[:, 2] = w * (x[:, 0] + x[:, 2] / 2) + padw # bottom right x
|
||||
y[:, 3] = h * (x[:, 1] + x[:, 3] / 2) + padh # bottom right y
|
||||
return y
|
||||
|
||||
|
||||
def xyn2xy(x, w=640, h=640, padw=0, padh=0):
|
||||
# Convert normalized segments into pixel segments, shape (n,2)
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = w * x[:, 0] + padw # top left x
|
||||
y[:, 1] = h * x[:, 1] + padh # top left y
|
||||
return y
|
||||
|
||||
|
||||
def segment2box(segment, width=640, height=640):
|
||||
# Convert 1 segment label to 1 box label, applying inside-image constraint, i.e. (xy1, xy2, ...) to (xyxy)
|
||||
x, y = segment.T # segment xy
|
||||
inside = (x >= 0) & (y >= 0) & (x <= width) & (y <= height)
|
||||
x, y, = x[inside], y[inside]
|
||||
return np.array([x.min(), y.min(), x.max(), y.max()]) if any(x) else np.zeros((1, 4)) # xyxy
|
||||
|
||||
|
||||
def segments2boxes(segments):
|
||||
# Convert segment labels to box labels, i.e. (cls, xy1, xy2, ...) to (cls, xywh)
|
||||
boxes = []
|
||||
for s in segments:
|
||||
x, y = s.T # segment xy
|
||||
boxes.append([x.min(), y.min(), x.max(), y.max()]) # cls, xyxy
|
||||
return xyxy2xywh(np.array(boxes)) # cls, xywh
|
||||
|
||||
|
||||
def resample_segments(segments, n=1000):
|
||||
# Up-sample an (n,2) segment
|
||||
for i, s in enumerate(segments):
|
||||
s = np.concatenate((s, s[0:1, :]), axis=0)
|
||||
x = np.linspace(0, len(s) - 1, n)
|
||||
xp = np.arange(len(s))
|
||||
segments[i] = np.concatenate([np.interp(x, xp, s[:, i]) for i in range(2)]).reshape(2, -1).T # segment xy
|
||||
return segments
|
||||
|
||||
|
||||
def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
|
||||
# Rescale coords (xyxy) from img1_shape to img0_shape
|
||||
if ratio_pad is None: # calculate from img0_shape
|
||||
gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new
|
||||
pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # wh padding
|
||||
else:
|
||||
gain = ratio_pad[0][0]
|
||||
pad = ratio_pad[1]
|
||||
|
||||
coords[:, [0, 2]] -= pad[0] # x padding
|
||||
coords[:, [1, 3]] -= pad[1] # y padding
|
||||
coords[:, :4] /= gain
|
||||
clip_coords(coords, img0_shape)
|
||||
return coords
|
||||
|
||||
|
||||
def clip_coords(boxes, img_shape):
|
||||
# Clip bounding xyxy bounding boxes to image shape (height, width)
|
||||
boxes[:, 0].clamp_(0, img_shape[1]) # x1
|
||||
boxes[:, 1].clamp_(0, img_shape[0]) # y1
|
||||
boxes[:, 2].clamp_(0, img_shape[1]) # x2
|
||||
boxes[:, 3].clamp_(0, img_shape[0]) # y2
|
||||
|
||||
|
||||
def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7):
|
||||
# Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
|
||||
box2 = box2.T
|
||||
|
||||
# Get the coordinates of bounding boxes
|
||||
if x1y1x2y2: # x1, y1, x2, y2 = box1
|
||||
b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
|
||||
b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
|
||||
else: # transform from xywh to xyxy
|
||||
b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
|
||||
b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
|
||||
b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
|
||||
b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2
|
||||
|
||||
# Intersection area
|
||||
inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
|
||||
(torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
|
||||
|
||||
# Union Area
|
||||
w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps
|
||||
w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps
|
||||
union = w1 * h1 + w2 * h2 - inter + eps
|
||||
|
||||
iou = inter / union
|
||||
|
||||
if GIoU or DIoU or CIoU:
|
||||
cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width
|
||||
ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height
|
||||
if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
|
||||
c2 = cw ** 2 + ch ** 2 + eps # convex diagonal squared
|
||||
rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 +
|
||||
(b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center distance squared
|
||||
if DIoU:
|
||||
return iou - rho2 / c2 # DIoU
|
||||
elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
|
||||
v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / (h2 + eps)) - torch.atan(w1 / (h1 + eps)), 2)
|
||||
with torch.no_grad():
|
||||
alpha = v / (v - iou + (1 + eps))
|
||||
return iou - (rho2 / c2 + v * alpha) # CIoU
|
||||
else: # GIoU https://arxiv.org/pdf/1902.09630.pdf
|
||||
c_area = cw * ch + eps # convex area
|
||||
return iou - (c_area - union) / c_area # GIoU
|
||||
else:
|
||||
return iou # IoU
|
||||
|
||||
|
||||
|
||||
|
||||
def bbox_alpha_iou(box1, box2, x1y1x2y2=False, GIoU=False, DIoU=False, CIoU=False, alpha=2, eps=1e-9):
|
||||
# Returns tsqrt_he IoU of box1 to box2. box1 is 4, box2 is nx4
|
||||
box2 = box2.T
|
||||
|
||||
# Get the coordinates of bounding boxes
|
||||
if x1y1x2y2: # x1, y1, x2, y2 = box1
|
||||
b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
|
||||
b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
|
||||
else: # transform from xywh to xyxy
|
||||
b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
|
||||
b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
|
||||
b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
|
||||
b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2
|
||||
|
||||
# Intersection area
|
||||
inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
|
||||
(torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
|
||||
|
||||
# Union Area
|
||||
w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps
|
||||
w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps
|
||||
union = w1 * h1 + w2 * h2 - inter + eps
|
||||
|
||||
# change iou into pow(iou+eps)
|
||||
# iou = inter / union
|
||||
iou = torch.pow(inter/union + eps, alpha)
|
||||
# beta = 2 * alpha
|
||||
if GIoU or DIoU or CIoU:
|
||||
cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width
|
||||
ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height
|
||||
if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
|
||||
c2 = (cw ** 2 + ch ** 2) ** alpha + eps # convex diagonal
|
||||
rho_x = torch.abs(b2_x1 + b2_x2 - b1_x1 - b1_x2)
|
||||
rho_y = torch.abs(b2_y1 + b2_y2 - b1_y1 - b1_y2)
|
||||
rho2 = ((rho_x ** 2 + rho_y ** 2) / 4) ** alpha # center distance
|
||||
if DIoU:
|
||||
return iou - rho2 / c2 # DIoU
|
||||
elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
|
||||
v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2)
|
||||
with torch.no_grad():
|
||||
alpha_ciou = v / ((1 + eps) - inter / union + v)
|
||||
# return iou - (rho2 / c2 + v * alpha_ciou) # CIoU
|
||||
return iou - (rho2 / c2 + torch.pow(v * alpha_ciou + eps, alpha)) # CIoU
|
||||
else: # GIoU https://arxiv.org/pdf/1902.09630.pdf
|
||||
# c_area = cw * ch + eps # convex area
|
||||
# return iou - (c_area - union) / c_area # GIoU
|
||||
c_area = torch.max(cw * ch + eps, union) # convex area
|
||||
return iou - torch.pow((c_area - union) / c_area + eps, alpha) # GIoU
|
||||
else:
|
||||
return iou # torch.log(iou+eps) or iou
|
||||
|
||||
|
||||
def box_iou(box1, box2):
|
||||
# https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
|
||||
"""
|
||||
Return intersection-over-union (Jaccard index) of boxes.
|
||||
Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
|
||||
Arguments:
|
||||
box1 (Tensor[N, 4])
|
||||
box2 (Tensor[M, 4])
|
||||
Returns:
|
||||
iou (Tensor[N, M]): the NxM matrix containing the pairwise
|
||||
IoU values for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
# inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
return inter / (area1[:, None] + area2 - inter) # iou = inter / (area1 + area2 - inter)
|
||||
|
||||
|
||||
def wh_iou(wh1, wh2):
|
||||
# Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2
|
||||
wh1 = wh1[:, None] # [N,1,2]
|
||||
wh2 = wh2[None] # [1,M,2]
|
||||
inter = torch.min(wh1, wh2).prod(2) # [N,M]
|
||||
return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter)
|
||||
|
||||
|
||||
def box_giou(box1, box2):
|
||||
"""
|
||||
Return generalized intersection-over-union (Jaccard index) between two sets of boxes.
|
||||
Both sets of boxes are expected to be in ``(x1, y1, x2, y2)`` format with
|
||||
``0 <= x1 < x2`` and ``0 <= y1 < y2``.
|
||||
Args:
|
||||
boxes1 (Tensor[N, 4]): first set of boxes
|
||||
boxes2 (Tensor[M, 4]): second set of boxes
|
||||
Returns:
|
||||
Tensor[N, M]: the NxM matrix containing the pairwise generalized IoU values
|
||||
for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
union = (area1[:, None] + area2 - inter)
|
||||
|
||||
iou = inter / union
|
||||
|
||||
lti = torch.min(box1[:, None, :2], box2[:, :2])
|
||||
rbi = torch.max(box1[:, None, 2:], box2[:, 2:])
|
||||
|
||||
whi = (rbi - lti).clamp(min=0) # [N,M,2]
|
||||
areai = whi[:, :, 0] * whi[:, :, 1]
|
||||
|
||||
return iou - (areai - union) / areai
|
||||
|
||||
|
||||
def box_ciou(box1, box2, eps: float = 1e-7):
|
||||
"""
|
||||
Return complete intersection-over-union (Jaccard index) between two sets of boxes.
|
||||
Both sets of boxes are expected to be in ``(x1, y1, x2, y2)`` format with
|
||||
``0 <= x1 < x2`` and ``0 <= y1 < y2``.
|
||||
Args:
|
||||
boxes1 (Tensor[N, 4]): first set of boxes
|
||||
boxes2 (Tensor[M, 4]): second set of boxes
|
||||
eps (float, optional): small number to prevent division by zero. Default: 1e-7
|
||||
Returns:
|
||||
Tensor[N, M]: the NxM matrix containing the pairwise complete IoU values
|
||||
for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
union = (area1[:, None] + area2 - inter)
|
||||
|
||||
iou = inter / union
|
||||
|
||||
lti = torch.min(box1[:, None, :2], box2[:, :2])
|
||||
rbi = torch.max(box1[:, None, 2:], box2[:, 2:])
|
||||
|
||||
whi = (rbi - lti).clamp(min=0) # [N,M,2]
|
||||
diagonal_distance_squared = (whi[:, :, 0] ** 2) + (whi[:, :, 1] ** 2) + eps
|
||||
|
||||
# centers of boxes
|
||||
x_p = (box1[:, None, 0] + box1[:, None, 2]) / 2
|
||||
y_p = (box1[:, None, 1] + box1[:, None, 3]) / 2
|
||||
x_g = (box2[:, 0] + box2[:, 2]) / 2
|
||||
y_g = (box2[:, 1] + box2[:, 3]) / 2
|
||||
# The distance between boxes' centers squared.
|
||||
centers_distance_squared = (x_p - x_g) ** 2 + (y_p - y_g) ** 2
|
||||
|
||||
w_pred = box1[:, None, 2] - box1[:, None, 0]
|
||||
h_pred = box1[:, None, 3] - box1[:, None, 1]
|
||||
|
||||
w_gt = box2[:, 2] - box2[:, 0]
|
||||
h_gt = box2[:, 3] - box2[:, 1]
|
||||
|
||||
v = (4 / (torch.pi ** 2)) * torch.pow((torch.atan(w_gt / h_gt) - torch.atan(w_pred / h_pred)), 2)
|
||||
with torch.no_grad():
|
||||
alpha = v / (1 - iou + v + eps)
|
||||
return iou - (centers_distance_squared / diagonal_distance_squared) - alpha * v
|
||||
|
||||
|
||||
def box_diou(box1, box2, eps: float = 1e-7):
|
||||
"""
|
||||
Return distance intersection-over-union (Jaccard index) between two sets of boxes.
|
||||
Both sets of boxes are expected to be in ``(x1, y1, x2, y2)`` format with
|
||||
``0 <= x1 < x2`` and ``0 <= y1 < y2``.
|
||||
Args:
|
||||
boxes1 (Tensor[N, 4]): first set of boxes
|
||||
boxes2 (Tensor[M, 4]): second set of boxes
|
||||
eps (float, optional): small number to prevent division by zero. Default: 1e-7
|
||||
Returns:
|
||||
Tensor[N, M]: the NxM matrix containing the pairwise distance IoU values
|
||||
for every element in boxes1 and boxes2
|
||||
"""
|
||||
|
||||
def box_area(box):
|
||||
# box = 4xn
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
area1 = box_area(box1.T)
|
||||
area2 = box_area(box2.T)
|
||||
|
||||
inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
|
||||
union = (area1[:, None] + area2 - inter)
|
||||
|
||||
iou = inter / union
|
||||
|
||||
lti = torch.min(box1[:, None, :2], box2[:, :2])
|
||||
rbi = torch.max(box1[:, None, 2:], box2[:, 2:])
|
||||
|
||||
whi = (rbi - lti).clamp(min=0) # [N,M,2]
|
||||
diagonal_distance_squared = (whi[:, :, 0] ** 2) + (whi[:, :, 1] ** 2) + eps
|
||||
|
||||
# centers of boxes
|
||||
x_p = (box1[:, None, 0] + box1[:, None, 2]) / 2
|
||||
y_p = (box1[:, None, 1] + box1[:, None, 3]) / 2
|
||||
x_g = (box2[:, 0] + box2[:, 2]) / 2
|
||||
y_g = (box2[:, 1] + box2[:, 3]) / 2
|
||||
# The distance between boxes' centers squared.
|
||||
centers_distance_squared = (x_p - x_g) ** 2 + (y_p - y_g) ** 2
|
||||
|
||||
# The distance IoU is the IoU penalized by a normalized
|
||||
# distance between boxes' centers squared.
|
||||
return iou - (centers_distance_squared / diagonal_distance_squared)
|
||||
|
||||
|
||||
def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False,
|
||||
labels=()):
|
||||
"""Runs Non-Maximum Suppression (NMS) on inference results
|
||||
|
||||
Returns:
|
||||
list of detections, on (n,6) tensor per image [xyxy, conf, cls]
|
||||
"""
|
||||
|
||||
nc = prediction.shape[2] - 5 # number of classes
|
||||
xc = prediction[..., 4] > conf_thres # candidates
|
||||
|
||||
# Settings
|
||||
min_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and height
|
||||
max_det = 300 # maximum number of detections per image
|
||||
max_nms = 30000 # maximum number of boxes into torchvision.ops.nms()
|
||||
time_limit = 10.0 # seconds to quit after
|
||||
redundant = True # require redundant detections
|
||||
multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)
|
||||
merge = False # use merge-NMS
|
||||
|
||||
t = time.time()
|
||||
output = [torch.zeros((0, 6), device=prediction.device)] * prediction.shape[0]
|
||||
for xi, x in enumerate(prediction): # image index, image inference
|
||||
# Apply constraints
|
||||
# x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height
|
||||
x = x[xc[xi]] # confidence
|
||||
|
||||
# Cat apriori labels if autolabelling
|
||||
if labels and len(labels[xi]):
|
||||
l = labels[xi]
|
||||
v = torch.zeros((len(l), nc + 5), device=x.device)
|
||||
v[:, :4] = l[:, 1:5] # box
|
||||
v[:, 4] = 1.0 # conf
|
||||
v[range(len(l)), l[:, 0].long() + 5] = 1.0 # cls
|
||||
x = torch.cat((x, v), 0)
|
||||
|
||||
# If none remain process next image
|
||||
if not x.shape[0]:
|
||||
continue
|
||||
|
||||
# Compute conf
|
||||
if nc == 1:
|
||||
x[:, 5:] = x[:, 4:5] # for models with one class, cls_loss is 0 and cls_conf is always 0.5,
|
||||
# so there is no need to multiplicate.
|
||||
else:
|
||||
x[:, 5:] *= x[:, 4:5] # conf = obj_conf * cls_conf
|
||||
|
||||
# Box (center x, center y, width, height) to (x1, y1, x2, y2)
|
||||
box = xywh2xyxy(x[:, :4])
|
||||
|
||||
# Detections matrix nx6 (xyxy, conf, cls)
|
||||
if multi_label:
|
||||
i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
|
||||
x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
|
||||
else: # best class only
|
||||
conf, j = x[:, 5:].max(1, keepdim=True)
|
||||
x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]
|
||||
|
||||
# Filter by class
|
||||
if classes is not None:
|
||||
x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]
|
||||
|
||||
# Apply finite constraint
|
||||
# if not torch.isfinite(x).all():
|
||||
# x = x[torch.isfinite(x).all(1)]
|
||||
|
||||
# Check shape
|
||||
n = x.shape[0] # number of boxes
|
||||
if not n: # no boxes
|
||||
continue
|
||||
elif n > max_nms: # excess boxes
|
||||
x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence
|
||||
|
||||
# Batched NMS
|
||||
c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
|
||||
boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores
|
||||
i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS
|
||||
if i.shape[0] > max_det: # limit detections
|
||||
i = i[:max_det]
|
||||
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
|
||||
# update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
|
||||
iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
|
||||
weights = iou * scores[None] # box weights
|
||||
x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes
|
||||
if redundant:
|
||||
i = i[iou.sum(1) > 1] # require redundancy
|
||||
|
||||
output[xi] = x[i]
|
||||
if (time.time() - t) > time_limit:
|
||||
print(f'WARNING: NMS time limit {time_limit}s exceeded')
|
||||
break # time limit exceeded
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def non_max_suppression_kpt(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False,
|
||||
labels=(), kpt_label=False, nc=None, nkpt=None):
|
||||
"""Runs Non-Maximum Suppression (NMS) on inference results
|
||||
|
||||
Returns:
|
||||
list of detections, on (n,6) tensor per image [xyxy, conf, cls]
|
||||
"""
|
||||
if nc is None:
|
||||
nc = prediction.shape[2] - 5 if not kpt_label else prediction.shape[2] - 56 # number of classes
|
||||
xc = prediction[..., 4] > conf_thres # candidates
|
||||
|
||||
# Settings
|
||||
min_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and height
|
||||
max_det = 300 # maximum number of detections per image
|
||||
max_nms = 30000 # maximum number of boxes into torchvision.ops.nms()
|
||||
time_limit = 10.0 # seconds to quit after
|
||||
redundant = True # require redundant detections
|
||||
multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)
|
||||
merge = False # use merge-NMS
|
||||
|
||||
t = time.time()
|
||||
output = [torch.zeros((0,6), device=prediction.device)] * prediction.shape[0]
|
||||
for xi, x in enumerate(prediction): # image index, image inference
|
||||
# Apply constraints
|
||||
# x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height
|
||||
x = x[xc[xi]] # confidence
|
||||
|
||||
# Cat apriori labels if autolabelling
|
||||
if labels and len(labels[xi]):
|
||||
l = labels[xi]
|
||||
v = torch.zeros((len(l), nc + 5), device=x.device)
|
||||
v[:, :4] = l[:, 1:5] # box
|
||||
v[:, 4] = 1.0 # conf
|
||||
v[range(len(l)), l[:, 0].long() + 5] = 1.0 # cls
|
||||
x = torch.cat((x, v), 0)
|
||||
|
||||
# If none remain process next image
|
||||
if not x.shape[0]:
|
||||
continue
|
||||
|
||||
# Compute conf
|
||||
x[:, 5:5+nc] *= x[:, 4:5] # conf = obj_conf * cls_conf
|
||||
|
||||
# Box (center x, center y, width, height) to (x1, y1, x2, y2)
|
||||
box = xywh2xyxy(x[:, :4])
|
||||
|
||||
# Detections matrix nx6 (xyxy, conf, cls)
|
||||
if multi_label:
|
||||
i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
|
||||
x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
|
||||
else: # best class only
|
||||
if not kpt_label:
|
||||
conf, j = x[:, 5:].max(1, keepdim=True)
|
||||
x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]
|
||||
else:
|
||||
kpts = x[:, 6:]
|
||||
conf, j = x[:, 5:6].max(1, keepdim=True)
|
||||
x = torch.cat((box, conf, j.float(), kpts), 1)[conf.view(-1) > conf_thres]
|
||||
|
||||
|
||||
# Filter by class
|
||||
if classes is not None:
|
||||
x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]
|
||||
|
||||
# Apply finite constraint
|
||||
# if not torch.isfinite(x).all():
|
||||
# x = x[torch.isfinite(x).all(1)]
|
||||
|
||||
# Check shape
|
||||
n = x.shape[0] # number of boxes
|
||||
if not n: # no boxes
|
||||
continue
|
||||
elif n > max_nms: # excess boxes
|
||||
x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence
|
||||
|
||||
# Batched NMS
|
||||
c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
|
||||
boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores
|
||||
i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS
|
||||
if i.shape[0] > max_det: # limit detections
|
||||
i = i[:max_det]
|
||||
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
|
||||
# update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
|
||||
iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
|
||||
weights = iou * scores[None] # box weights
|
||||
x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes
|
||||
if redundant:
|
||||
i = i[iou.sum(1) > 1] # require redundancy
|
||||
|
||||
output[xi] = x[i]
|
||||
if (time.time() - t) > time_limit:
|
||||
print(f'WARNING: NMS time limit {time_limit}s exceeded')
|
||||
break # time limit exceeded
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def strip_optimizer(f='best.pt', s=''): # from utils.general import *; strip_optimizer()
|
||||
# Strip optimizer from 'f' to finalize training, optionally save as 's'
|
||||
x = torch.load(f, map_location=torch.device('cpu'))
|
||||
if x.get('ema'):
|
||||
x['model'] = x['ema'] # replace model with ema
|
||||
for k in 'optimizer', 'training_results', 'wandb_id', 'ema', 'updates': # keys
|
||||
x[k] = None
|
||||
x['epoch'] = -1
|
||||
x['model'].half() # to FP16
|
||||
for p in x['model'].parameters():
|
||||
p.requires_grad = False
|
||||
torch.save(x, s or f)
|
||||
mb = os.path.getsize(s or f) / 1E6 # filesize
|
||||
print(f"Optimizer stripped from {f},{(' saved as %s,' % s) if s else ''} {mb:.1f}MB")
|
||||
|
||||
|
||||
def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''):
|
||||
# Print mutation results to evolve.txt (for use with train.py --evolve)
|
||||
a = '%10s' * len(hyp) % tuple(hyp.keys()) # hyperparam keys
|
||||
b = '%10.3g' * len(hyp) % tuple(hyp.values()) # hyperparam values
|
||||
c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
|
||||
print('\n%s\n%s\nEvolved fitness: %s\n' % (a, b, c))
|
||||
|
||||
if bucket:
|
||||
url = 'gs://%s/evolve.txt' % bucket
|
||||
if gsutil_getsize(url) > (os.path.getsize('evolve.txt') if os.path.exists('evolve.txt') else 0):
|
||||
os.system('gsutil cp %s .' % url) # download evolve.txt if larger than local
|
||||
|
||||
with open('evolve.txt', 'a') as f: # append result
|
||||
f.write(c + b + '\n')
|
||||
x = np.unique(np.loadtxt('evolve.txt', ndmin=2), axis=0) # load unique rows
|
||||
x = x[np.argsort(-fitness(x))] # sort
|
||||
np.savetxt('evolve.txt', x, '%10.3g') # save sort by fitness
|
||||
|
||||
# Save yaml
|
||||
for i, k in enumerate(hyp.keys()):
|
||||
hyp[k] = float(x[0, i + 7])
|
||||
with open(yaml_file, 'w') as f:
|
||||
results = tuple(x[0, :7])
|
||||
c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
|
||||
f.write('# Hyperparameter Evolution Results\n# Generations: %g\n# Metrics: ' % len(x) + c + '\n\n')
|
||||
yaml.dump(hyp, f, sort_keys=False)
|
||||
|
||||
if bucket:
|
||||
os.system('gsutil cp evolve.txt %s gs://%s' % (yaml_file, bucket)) # upload
|
||||
|
||||
|
||||
def apply_classifier(x, model, img, im0):
|
||||
# applies a second stage classifier to yolo outputs
|
||||
im0 = [im0] if isinstance(im0, np.ndarray) else im0
|
||||
for i, d in enumerate(x): # per image
|
||||
if d is not None and len(d):
|
||||
d = d.clone()
|
||||
|
||||
# Reshape and pad cutouts
|
||||
b = xyxy2xywh(d[:, :4]) # boxes
|
||||
b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # rectangle to square
|
||||
b[:, 2:] = b[:, 2:] * 1.3 + 30 # pad
|
||||
d[:, :4] = xywh2xyxy(b).long()
|
||||
|
||||
# Rescale boxes from img_size to im0 size
|
||||
scale_coords(img.shape[2:], d[:, :4], im0[i].shape)
|
||||
|
||||
# Classes
|
||||
pred_cls1 = d[:, 5].long()
|
||||
ims = []
|
||||
for j, a in enumerate(d): # per item
|
||||
cutout = im0[i][int(a[1]):int(a[3]), int(a[0]):int(a[2])]
|
||||
im = cv2.resize(cutout, (224, 224)) # BGR
|
||||
# cv2.imwrite('test%i.jpg' % j, cutout)
|
||||
|
||||
im = im[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
|
||||
im = np.ascontiguousarray(im, dtype=np.float32) # uint8 to float32
|
||||
im /= 255.0 # 0 - 255 to 0.0 - 1.0
|
||||
ims.append(im)
|
||||
|
||||
pred_cls2 = model(torch.Tensor(ims).to(d.device)).argmax(1) # classifier prediction
|
||||
x[i] = x[i][pred_cls1 == pred_cls2] # retain matching class detections
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def increment_path(path, exist_ok=True, sep=''):
|
||||
# Increment path, i.e. runs/exp --> runs/exp{sep}0, runs/exp{sep}1 etc.
|
||||
path = Path(path) # os-agnostic
|
||||
if (path.exists() and exist_ok) or (not path.exists()):
|
||||
return str(path)
|
||||
else:
|
||||
dirs = glob.glob(f"{path}{sep}*") # similar paths
|
||||
matches = [re.search(rf"%s{sep}(\d+)" % path.stem, d) for d in dirs]
|
||||
i = [int(m.groups()[0]) for m in matches if m] # indices
|
||||
n = max(i) + 1 if i else 2 # increment number
|
||||
return f"{path}{sep}{n}" # update path
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
FROM gcr.io/google-appengine/python
|
||||
|
||||
# Create a virtualenv for dependencies. This isolates these packages from
|
||||
# system-level packages.
|
||||
# Use -p python3 or -p python3.7 to select python version. Default is version 2.
|
||||
RUN virtualenv /env -p python3
|
||||
|
||||
# Setting these environment variables are the same as running
|
||||
# source /env/bin/activate.
|
||||
ENV VIRTUAL_ENV /env
|
||||
ENV PATH /env/bin:$PATH
|
||||
|
||||
RUN apt-get update && apt-get install -y python-opencv
|
||||
|
||||
# Copy the application's requirements.txt and run pip to install all
|
||||
# dependencies into the virtualenv.
|
||||
ADD requirements.txt /app/requirements.txt
|
||||
RUN pip install -r /app/requirements.txt
|
||||
|
||||
# Add the application source code.
|
||||
ADD . /app
|
||||
|
||||
# Run a WSGI server to serve the application. gunicorn must be declared as
|
||||
# a dependency in requirements.txt.
|
||||
CMD gunicorn -b :$PORT main:app
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# add these requirements in your app on top of the existing ones
|
||||
pip==18.1
|
||||
Flask==1.0.2
|
||||
gunicorn==19.9.0
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
runtime: custom
|
||||
env: flex
|
||||
|
||||
service: yolorapp
|
||||
|
||||
liveness_check:
|
||||
initial_delay_sec: 600
|
||||
|
||||
manual_scaling:
|
||||
instances: 1
|
||||
resources:
|
||||
cpu: 1
|
||||
memory_gb: 4
|
||||
disk_size_gb: 20
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
# Google utils: https://cloud.google.com/storage/docs/reference/libraries
|
||||
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
import torch
|
||||
|
||||
|
||||
def gsutil_getsize(url=''):
|
||||
# gs://bucket/file size https://cloud.google.com/storage/docs/gsutil/commands/du
|
||||
s = subprocess.check_output(f'gsutil du {url}', shell=True).decode('utf-8')
|
||||
return eval(s.split(' ')[0]) if len(s) else 0 # bytes
|
||||
|
||||
|
||||
def attempt_download(file, repo='WongKinYiu/yolov7'):
|
||||
# Attempt file download if does not exist
|
||||
file = Path(str(file).strip().replace("'", '').lower())
|
||||
|
||||
if not file.exists():
|
||||
try:
|
||||
response = requests.get(f'https://api.github.com/repos/{repo}/releases/latest').json() # github api
|
||||
assets = [x['name'] for x in response['assets']] # release assets
|
||||
tag = response['tag_name'] # i.e. 'v1.0'
|
||||
except: # fallback plan
|
||||
assets = ['yolov7.pt', 'yolov7-tiny.pt', 'yolov7x.pt', 'yolov7-d6.pt', 'yolov7-e6.pt',
|
||||
'yolov7-e6e.pt', 'yolov7-w6.pt']
|
||||
tag = subprocess.check_output('git tag', shell=True).decode().split()[-1]
|
||||
|
||||
name = file.name
|
||||
if name in assets:
|
||||
msg = f'{file} missing, try downloading from https://github.com/{repo}/releases/'
|
||||
redundant = False # second download option
|
||||
try: # GitHub
|
||||
url = f'https://github.com/{repo}/releases/download/{tag}/{name}'
|
||||
print(f'Downloading {url} to {file}...')
|
||||
torch.hub.download_url_to_file(url, file)
|
||||
assert file.exists() and file.stat().st_size > 1E6 # check
|
||||
except Exception as e: # GCP
|
||||
print(f'Download error: {e}')
|
||||
assert redundant, 'No secondary mirror'
|
||||
url = f'https://storage.googleapis.com/{repo}/ckpt/{name}'
|
||||
print(f'Downloading {url} to {file}...')
|
||||
os.system(f'curl -L {url} -o {file}') # torch.hub.download_url_to_file(url, weights)
|
||||
finally:
|
||||
if not file.exists() or file.stat().st_size < 1E6: # check
|
||||
file.unlink(missing_ok=True) # remove partial downloads
|
||||
print(f'ERROR: Download failure: {msg}')
|
||||
print('')
|
||||
return
|
||||
|
||||
|
||||
def gdrive_download(id='', file='tmp.zip'):
|
||||
# Downloads a file from Google Drive. from yolov7.utils.google_utils import *; gdrive_download()
|
||||
t = time.time()
|
||||
file = Path(file)
|
||||
cookie = Path('cookie') # gdrive cookie
|
||||
print(f'Downloading https://drive.google.com/uc?export=download&id={id} as {file}... ', end='')
|
||||
file.unlink(missing_ok=True) # remove existing file
|
||||
cookie.unlink(missing_ok=True) # remove existing cookie
|
||||
|
||||
# Attempt file download
|
||||
out = "NUL" if platform.system() == "Windows" else "/dev/null"
|
||||
os.system(f'curl -c ./cookie -s -L "drive.google.com/uc?export=download&id={id}" > {out}')
|
||||
if os.path.exists('cookie'): # large file
|
||||
s = f'curl -Lb ./cookie "drive.google.com/uc?export=download&confirm={get_token()}&id={id}" -o {file}'
|
||||
else: # small file
|
||||
s = f'curl -s -L -o {file} "drive.google.com/uc?export=download&id={id}"'
|
||||
r = os.system(s) # execute, capture return
|
||||
cookie.unlink(missing_ok=True) # remove existing cookie
|
||||
|
||||
# Error check
|
||||
if r != 0:
|
||||
file.unlink(missing_ok=True) # remove partial
|
||||
print('Download error ') # raise Exception('Download error')
|
||||
return r
|
||||
|
||||
# Unzip if archive
|
||||
if file.suffix == '.zip':
|
||||
print('unzipping... ', end='')
|
||||
os.system(f'unzip -q {file}') # unzip
|
||||
file.unlink() # remove zip to free space
|
||||
|
||||
print(f'Done ({time.time() - t:.1f}s)')
|
||||
return r
|
||||
|
||||
|
||||
def get_token(cookie="./cookie"):
|
||||
with open(cookie) as f:
|
||||
for line in f:
|
||||
if "download" in line:
|
||||
return line.split()[-1]
|
||||
return ""
|
||||
|
||||
# def upload_blob(bucket_name, source_file_name, destination_blob_name):
|
||||
# # Uploads a file to a bucket
|
||||
# # https://cloud.google.com/storage/docs/uploading-objects#storage-upload-object-python
|
||||
#
|
||||
# storage_client = storage.Client()
|
||||
# bucket = storage_client.get_bucket(bucket_name)
|
||||
# blob = bucket.blob(destination_blob_name)
|
||||
#
|
||||
# blob.upload_from_filename(source_file_name)
|
||||
#
|
||||
# print('File {} uploaded to {}.'.format(
|
||||
# source_file_name,
|
||||
# destination_blob_name))
|
||||
#
|
||||
#
|
||||
# def download_blob(bucket_name, source_blob_name, destination_file_name):
|
||||
# # Uploads a blob from a bucket
|
||||
# storage_client = storage.Client()
|
||||
# bucket = storage_client.get_bucket(bucket_name)
|
||||
# blob = bucket.blob(source_blob_name)
|
||||
#
|
||||
# blob.download_to_filename(destination_file_name)
|
||||
#
|
||||
# print('Blob {} downloaded to {}.'.format(
|
||||
# source_blob_name,
|
||||
# destination_file_name))
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,227 @@
|
|||
# Model validation metrics
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from . import general
|
||||
|
||||
|
||||
def fitness(x):
|
||||
# Model fitness as a weighted combination of metrics
|
||||
w = [0.0, 0.0, 0.1, 0.9] # weights for [P, R, mAP@0.5, mAP@0.5:0.95]
|
||||
return (x[:, :4] * w).sum(1)
|
||||
|
||||
|
||||
def ap_per_class(tp, conf, pred_cls, target_cls, v5_metric=False, plot=False, save_dir='.', names=()):
|
||||
""" Compute the average precision, given the recall and precision curves.
|
||||
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
|
||||
# Arguments
|
||||
tp: True positives (nparray, nx1 or nx10).
|
||||
conf: Objectness value from 0-1 (nparray).
|
||||
pred_cls: Predicted object classes (nparray).
|
||||
target_cls: True object classes (nparray).
|
||||
plot: Plot precision-recall curve at mAP@0.5
|
||||
save_dir: Plot save directory
|
||||
# Returns
|
||||
The average precision as computed in py-faster-rcnn.
|
||||
"""
|
||||
|
||||
# Sort by objectness
|
||||
i = np.argsort(-conf)
|
||||
tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]
|
||||
|
||||
# Find unique classes
|
||||
unique_classes = np.unique(target_cls)
|
||||
nc = unique_classes.shape[0] # number of classes, number of detections
|
||||
|
||||
# Create Precision-Recall curve and compute AP for each class
|
||||
px, py = np.linspace(0, 1, 1000), [] # for plotting
|
||||
ap, p, r = np.zeros((nc, tp.shape[1])), np.zeros((nc, 1000)), np.zeros((nc, 1000))
|
||||
for ci, c in enumerate(unique_classes):
|
||||
i = pred_cls == c
|
||||
n_l = (target_cls == c).sum() # number of labels
|
||||
n_p = i.sum() # number of predictions
|
||||
|
||||
if n_p == 0 or n_l == 0:
|
||||
continue
|
||||
else:
|
||||
# Accumulate FPs and TPs
|
||||
fpc = (1 - tp[i]).cumsum(0)
|
||||
tpc = tp[i].cumsum(0)
|
||||
|
||||
# Recall
|
||||
recall = tpc / (n_l + 1e-16) # recall curve
|
||||
r[ci] = np.interp(-px, -conf[i], recall[:, 0], left=0) # negative x, xp because xp decreases
|
||||
|
||||
# Precision
|
||||
precision = tpc / (tpc + fpc) # precision curve
|
||||
p[ci] = np.interp(-px, -conf[i], precision[:, 0], left=1) # p at pr_score
|
||||
|
||||
# AP from recall-precision curve
|
||||
for j in range(tp.shape[1]):
|
||||
ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j], v5_metric=v5_metric)
|
||||
if plot and j == 0:
|
||||
py.append(np.interp(px, mrec, mpre)) # precision at mAP@0.5
|
||||
|
||||
# Compute F1 (harmonic mean of precision and recall)
|
||||
f1 = 2 * p * r / (p + r + 1e-16)
|
||||
if plot:
|
||||
plot_pr_curve(px, py, ap, Path(save_dir) / 'PR_curve.png', names)
|
||||
plot_mc_curve(px, f1, Path(save_dir) / 'F1_curve.png', names, ylabel='F1')
|
||||
plot_mc_curve(px, p, Path(save_dir) / 'P_curve.png', names, ylabel='Precision')
|
||||
plot_mc_curve(px, r, Path(save_dir) / 'R_curve.png', names, ylabel='Recall')
|
||||
|
||||
i = f1.mean(0).argmax() # max F1 index
|
||||
return p[:, i], r[:, i], ap, f1[:, i], unique_classes.astype('int32')
|
||||
|
||||
|
||||
def compute_ap(recall, precision, v5_metric=False):
|
||||
""" Compute the average precision, given the recall and precision curves
|
||||
# Arguments
|
||||
recall: The recall curve (list)
|
||||
precision: The precision curve (list)
|
||||
v5_metric: Assume maximum recall to be 1.0, as in YOLOv5, MMDetetion etc.
|
||||
# Returns
|
||||
Average precision, precision curve, recall curve
|
||||
"""
|
||||
|
||||
# Append sentinel values to beginning and end
|
||||
if v5_metric: # New YOLOv5 metric, same as MMDetection and Detectron2 repositories
|
||||
mrec = np.concatenate(([0.], recall, [1.0]))
|
||||
else: # Old YOLOv5 metric, i.e. default YOLOv7 metric
|
||||
mrec = np.concatenate(([0.], recall, [recall[-1] + 0.01]))
|
||||
mpre = np.concatenate(([1.], precision, [0.]))
|
||||
|
||||
# Compute the precision envelope
|
||||
mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
|
||||
|
||||
# Integrate area under curve
|
||||
method = 'interp' # methods: 'continuous', 'interp'
|
||||
if method == 'interp':
|
||||
x = np.linspace(0, 1, 101) # 101-point interp (COCO)
|
||||
ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate
|
||||
else: # 'continuous'
|
||||
i = np.where(mrec[1:] != mrec[:-1])[0] # points where x axis (recall) changes
|
||||
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) # area under curve
|
||||
|
||||
return ap, mpre, mrec
|
||||
|
||||
|
||||
class ConfusionMatrix:
|
||||
# Updated version of https://github.com/kaanakan/object_detection_confusion_matrix
|
||||
def __init__(self, nc, conf=0.25, iou_thres=0.45):
|
||||
self.matrix = np.zeros((nc + 1, nc + 1))
|
||||
self.nc = nc # number of classes
|
||||
self.conf = conf
|
||||
self.iou_thres = iou_thres
|
||||
|
||||
def process_batch(self, detections, labels):
|
||||
"""
|
||||
Return intersection-over-union (Jaccard index) of boxes.
|
||||
Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
|
||||
Arguments:
|
||||
detections (Array[N, 6]), x1, y1, x2, y2, conf, class
|
||||
labels (Array[M, 5]), class, x1, y1, x2, y2
|
||||
Returns:
|
||||
None, updates confusion matrix accordingly
|
||||
"""
|
||||
detections = detections[detections[:, 4] > self.conf]
|
||||
gt_classes = labels[:, 0].int()
|
||||
detection_classes = detections[:, 5].int()
|
||||
iou = general.box_iou(labels[:, 1:], detections[:, :4])
|
||||
|
||||
x = torch.where(iou > self.iou_thres)
|
||||
if x[0].shape[0]:
|
||||
matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy()
|
||||
if x[0].shape[0] > 1:
|
||||
matches = matches[matches[:, 2].argsort()[::-1]]
|
||||
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
|
||||
matches = matches[matches[:, 2].argsort()[::-1]]
|
||||
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
|
||||
else:
|
||||
matches = np.zeros((0, 3))
|
||||
|
||||
n = matches.shape[0] > 0
|
||||
m0, m1, _ = matches.transpose().astype(np.int16)
|
||||
for i, gc in enumerate(gt_classes):
|
||||
j = m0 == i
|
||||
if n and sum(j) == 1:
|
||||
self.matrix[gc, detection_classes[m1[j]]] += 1 # correct
|
||||
else:
|
||||
self.matrix[self.nc, gc] += 1 # background FP
|
||||
|
||||
if n:
|
||||
for i, dc in enumerate(detection_classes):
|
||||
if not any(m1 == i):
|
||||
self.matrix[dc, self.nc] += 1 # background FN
|
||||
|
||||
def matrix(self):
|
||||
return self.matrix
|
||||
|
||||
def plot(self, save_dir='', names=()):
|
||||
try:
|
||||
import seaborn as sn
|
||||
|
||||
array = self.matrix / (self.matrix.sum(0).reshape(1, self.nc + 1) + 1E-6) # normalize
|
||||
array[array < 0.005] = np.nan # don't annotate (would appear as 0.00)
|
||||
|
||||
fig = plt.figure(figsize=(12, 9), tight_layout=True)
|
||||
sn.set(font_scale=1.0 if self.nc < 50 else 0.8) # for label size
|
||||
labels = (0 < len(names) < 99) and len(names) == self.nc # apply names to ticklabels
|
||||
sn.heatmap(array, annot=self.nc < 30, annot_kws={"size": 8}, cmap='Blues', fmt='.2f', square=True,
|
||||
xticklabels=names + ['background FP'] if labels else "auto",
|
||||
yticklabels=names + ['background FN'] if labels else "auto").set_facecolor((1, 1, 1))
|
||||
fig.axes[0].set_xlabel('True')
|
||||
fig.axes[0].set_ylabel('Predicted')
|
||||
fig.savefig(Path(save_dir) / 'confusion_matrix.png', dpi=250)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def print(self):
|
||||
for i in range(self.nc + 1):
|
||||
print(' '.join(map(str, self.matrix[i])))
|
||||
|
||||
|
||||
# Plots ----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def plot_pr_curve(px, py, ap, save_dir='pr_curve.png', names=()):
|
||||
# Precision-recall curve
|
||||
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
|
||||
py = np.stack(py, axis=1)
|
||||
|
||||
if 0 < len(names) < 21: # display per-class legend if < 21 classes
|
||||
for i, y in enumerate(py.T):
|
||||
ax.plot(px, y, linewidth=1, label=f'{names[i]} {ap[i, 0]:.3f}') # plot(recall, precision)
|
||||
else:
|
||||
ax.plot(px, py, linewidth=1, color='grey') # plot(recall, precision)
|
||||
|
||||
ax.plot(px, py.mean(1), linewidth=3, color='blue', label='all classes %.3f mAP@0.5' % ap[:, 0].mean())
|
||||
ax.set_xlabel('Recall')
|
||||
ax.set_ylabel('Precision')
|
||||
ax.set_xlim(0, 1)
|
||||
ax.set_ylim(0, 1)
|
||||
plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
|
||||
fig.savefig(Path(save_dir), dpi=250)
|
||||
|
||||
|
||||
def plot_mc_curve(px, py, save_dir='mc_curve.png', names=(), xlabel='Confidence', ylabel='Metric'):
|
||||
# Metric-confidence curve
|
||||
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
|
||||
|
||||
if 0 < len(names) < 21: # display per-class legend if < 21 classes
|
||||
for i, y in enumerate(py):
|
||||
ax.plot(px, y, linewidth=1, label=f'{names[i]}') # plot(confidence, metric)
|
||||
else:
|
||||
ax.plot(px, py.T, linewidth=1, color='grey') # plot(confidence, metric)
|
||||
|
||||
y = py.mean(0)
|
||||
ax.plot(px, y, linewidth=3, color='blue', label=f'all classes {y.max():.2f} at {px[y.argmax()]:.3f}')
|
||||
ax.set_xlabel(xlabel)
|
||||
ax.set_ylabel(ylabel)
|
||||
ax.set_xlim(0, 1)
|
||||
ax.set_ylim(0, 1)
|
||||
plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
|
||||
fig.savefig(Path(save_dir), dpi=250)
|
||||
|
|
@ -0,0 +1,489 @@
|
|||
# Plotting utils
|
||||
|
||||
import glob
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import seaborn as sns
|
||||
import torch
|
||||
import yaml
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from scipy.signal import butter, filtfilt
|
||||
|
||||
from utils.general import xywh2xyxy, xyxy2xywh
|
||||
from utils.metrics import fitness
|
||||
|
||||
# Settings
|
||||
matplotlib.rc('font', **{'size': 11})
|
||||
matplotlib.use('Agg') # for writing to files only
|
||||
|
||||
|
||||
def color_list():
|
||||
# Return first 10 plt colors as (r,g,b) https://stackoverflow.com/questions/51350872/python-from-color-name-to-rgb
|
||||
def hex2rgb(h):
|
||||
return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))
|
||||
|
||||
return [hex2rgb(h) for h in matplotlib.colors.TABLEAU_COLORS.values()] # or BASE_ (8), CSS4_ (148), XKCD_ (949)
|
||||
|
||||
|
||||
def hist2d(x, y, n=100):
|
||||
# 2d histogram used in labels.png and evolve.png
|
||||
xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n)
|
||||
hist, xedges, yedges = np.histogram2d(x, y, (xedges, yedges))
|
||||
xidx = np.clip(np.digitize(x, xedges) - 1, 0, hist.shape[0] - 1)
|
||||
yidx = np.clip(np.digitize(y, yedges) - 1, 0, hist.shape[1] - 1)
|
||||
return np.log(hist[xidx, yidx])
|
||||
|
||||
|
||||
def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5):
|
||||
# https://stackoverflow.com/questions/28536191/how-to-filter-smooth-with-scipy-numpy
|
||||
def butter_lowpass(cutoff, fs, order):
|
||||
nyq = 0.5 * fs
|
||||
normal_cutoff = cutoff / nyq
|
||||
return butter(order, normal_cutoff, btype='low', analog=False)
|
||||
|
||||
b, a = butter_lowpass(cutoff, fs, order=order)
|
||||
return filtfilt(b, a, data) # forward-backward filter
|
||||
|
||||
|
||||
def plot_one_box(x, img, color=None, label=None, line_thickness=3):
|
||||
# Plots one bounding box on image img
|
||||
tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 # line/font thickness
|
||||
color = color or [random.randint(0, 255) for _ in range(3)]
|
||||
c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
|
||||
cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
|
||||
if label:
|
||||
tf = max(tl - 1, 1) # font thickness
|
||||
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
||||
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
|
||||
cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
|
||||
cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)
|
||||
|
||||
|
||||
def plot_one_box_PIL(box, img, color=None, label=None, line_thickness=None):
|
||||
img = Image.fromarray(img)
|
||||
draw = ImageDraw.Draw(img)
|
||||
line_thickness = line_thickness or max(int(min(img.size) / 200), 2)
|
||||
draw.rectangle(box, width=line_thickness, outline=tuple(color)) # plot
|
||||
if label:
|
||||
fontsize = max(round(max(img.size) / 40), 12)
|
||||
font = ImageFont.truetype("Arial.ttf", fontsize)
|
||||
txt_width, txt_height = font.getsize(label)
|
||||
draw.rectangle([box[0], box[1] - txt_height + 4, box[0] + txt_width, box[1]], fill=tuple(color))
|
||||
draw.text((box[0], box[1] - txt_height + 1), label, fill=(255, 255, 255), font=font)
|
||||
return np.asarray(img)
|
||||
|
||||
|
||||
def plot_wh_methods(): # from utils.plots import *; plot_wh_methods()
|
||||
# Compares the two methods for width-height anchor multiplication
|
||||
# https://github.com/ultralytics/yolov3/issues/168
|
||||
x = np.arange(-4.0, 4.0, .1)
|
||||
ya = np.exp(x)
|
||||
yb = torch.sigmoid(torch.from_numpy(x)).numpy() * 2
|
||||
|
||||
fig = plt.figure(figsize=(6, 3), tight_layout=True)
|
||||
plt.plot(x, ya, '.-', label='YOLOv3')
|
||||
plt.plot(x, yb ** 2, '.-', label='YOLOR ^2')
|
||||
plt.plot(x, yb ** 1.6, '.-', label='YOLOR ^1.6')
|
||||
plt.xlim(left=-4, right=4)
|
||||
plt.ylim(bottom=0, top=6)
|
||||
plt.xlabel('input')
|
||||
plt.ylabel('output')
|
||||
plt.grid()
|
||||
plt.legend()
|
||||
fig.savefig('comparison.png', dpi=200)
|
||||
|
||||
|
||||
def output_to_target(output):
|
||||
# Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
|
||||
targets = []
|
||||
for i, o in enumerate(output):
|
||||
for *box, conf, cls in o.cpu().numpy():
|
||||
targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf])
|
||||
return np.array(targets)
|
||||
|
||||
|
||||
def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=640, max_subplots=16):
|
||||
# Plot image grid with labels
|
||||
|
||||
if isinstance(images, torch.Tensor):
|
||||
images = images.cpu().float().numpy()
|
||||
if isinstance(targets, torch.Tensor):
|
||||
targets = targets.cpu().numpy()
|
||||
|
||||
# un-normalise
|
||||
if np.max(images[0]) <= 1:
|
||||
images *= 255
|
||||
|
||||
tl = 3 # line thickness
|
||||
tf = max(tl - 1, 1) # font thickness
|
||||
bs, _, h, w = images.shape # batch size, _, height, width
|
||||
bs = min(bs, max_subplots) # limit plot images
|
||||
ns = np.ceil(bs ** 0.5) # number of subplots (square)
|
||||
|
||||
# Check if we should resize
|
||||
scale_factor = max_size / max(h, w)
|
||||
if scale_factor < 1:
|
||||
h = math.ceil(scale_factor * h)
|
||||
w = math.ceil(scale_factor * w)
|
||||
|
||||
colors = color_list() # list of colors
|
||||
mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) # init
|
||||
for i, img in enumerate(images):
|
||||
if i == max_subplots: # if last batch has fewer images than we expect
|
||||
break
|
||||
|
||||
block_x = int(w * (i // ns))
|
||||
block_y = int(h * (i % ns))
|
||||
|
||||
img = img.transpose(1, 2, 0)
|
||||
if scale_factor < 1:
|
||||
img = cv2.resize(img, (w, h))
|
||||
|
||||
mosaic[block_y:block_y + h, block_x:block_x + w, :] = img
|
||||
if len(targets) > 0:
|
||||
image_targets = targets[targets[:, 0] == i]
|
||||
boxes = xywh2xyxy(image_targets[:, 2:6]).T
|
||||
classes = image_targets[:, 1].astype('int')
|
||||
labels = image_targets.shape[1] == 6 # labels if no conf column
|
||||
conf = None if labels else image_targets[:, 6] # check for confidence presence (label vs pred)
|
||||
|
||||
if boxes.shape[1]:
|
||||
if boxes.max() <= 1.01: # if normalized with tolerance 0.01
|
||||
boxes[[0, 2]] *= w # scale to pixels
|
||||
boxes[[1, 3]] *= h
|
||||
elif scale_factor < 1: # absolute coords need scale if image scales
|
||||
boxes *= scale_factor
|
||||
boxes[[0, 2]] += block_x
|
||||
boxes[[1, 3]] += block_y
|
||||
for j, box in enumerate(boxes.T):
|
||||
cls = int(classes[j])
|
||||
color = colors[cls % len(colors)]
|
||||
cls = names[cls] if names else cls
|
||||
if labels or conf[j] > 0.25: # 0.25 conf thresh
|
||||
label = '%s' % cls if labels else '%s %.1f' % (cls, conf[j])
|
||||
plot_one_box(box, mosaic, label=label, color=color, line_thickness=tl)
|
||||
|
||||
# Draw image filename labels
|
||||
if paths:
|
||||
label = Path(paths[i]).name[:40] # trim to 40 char
|
||||
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
||||
cv2.putText(mosaic, label, (block_x + 5, block_y + t_size[1] + 5), 0, tl / 3, [220, 220, 220], thickness=tf,
|
||||
lineType=cv2.LINE_AA)
|
||||
|
||||
# Image border
|
||||
cv2.rectangle(mosaic, (block_x, block_y), (block_x + w, block_y + h), (255, 255, 255), thickness=3)
|
||||
|
||||
if fname:
|
||||
r = min(1280. / max(h, w) / ns, 1.0) # ratio to limit image size
|
||||
mosaic = cv2.resize(mosaic, (int(ns * w * r), int(ns * h * r)), interpolation=cv2.INTER_AREA)
|
||||
# cv2.imwrite(fname, cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB)) # cv2 save
|
||||
Image.fromarray(mosaic).save(fname) # PIL save
|
||||
return mosaic
|
||||
|
||||
|
||||
def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''):
|
||||
# Plot LR simulating training for full epochs
|
||||
optimizer, scheduler = copy(optimizer), copy(scheduler) # do not modify originals
|
||||
y = []
|
||||
for _ in range(epochs):
|
||||
scheduler.step()
|
||||
y.append(optimizer.param_groups[0]['lr'])
|
||||
plt.plot(y, '.-', label='LR')
|
||||
plt.xlabel('epoch')
|
||||
plt.ylabel('LR')
|
||||
plt.grid()
|
||||
plt.xlim(0, epochs)
|
||||
plt.ylim(0)
|
||||
plt.savefig(Path(save_dir) / 'LR.png', dpi=200)
|
||||
plt.close()
|
||||
|
||||
|
||||
def plot_test_txt(): # from utils.plots import *; plot_test()
|
||||
# Plot test.txt histograms
|
||||
x = np.loadtxt('test.txt', dtype=np.float32)
|
||||
box = xyxy2xywh(x[:, :4])
|
||||
cx, cy = box[:, 0], box[:, 1]
|
||||
|
||||
fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True)
|
||||
ax.hist2d(cx, cy, bins=600, cmax=10, cmin=0)
|
||||
ax.set_aspect('equal')
|
||||
plt.savefig('hist2d.png', dpi=300)
|
||||
|
||||
fig, ax = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True)
|
||||
ax[0].hist(cx, bins=600)
|
||||
ax[1].hist(cy, bins=600)
|
||||
plt.savefig('hist1d.png', dpi=200)
|
||||
|
||||
|
||||
def plot_targets_txt(): # from utils.plots import *; plot_targets_txt()
|
||||
# Plot targets.txt histograms
|
||||
x = np.loadtxt('targets.txt', dtype=np.float32).T
|
||||
s = ['x targets', 'y targets', 'width targets', 'height targets']
|
||||
fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
|
||||
ax = ax.ravel()
|
||||
for i in range(4):
|
||||
ax[i].hist(x[i], bins=100, label='%.3g +/- %.3g' % (x[i].mean(), x[i].std()))
|
||||
ax[i].legend()
|
||||
ax[i].set_title(s[i])
|
||||
plt.savefig('targets.jpg', dpi=200)
|
||||
|
||||
|
||||
def plot_study_txt(path='', x=None): # from utils.plots import *; plot_study_txt()
|
||||
# Plot study.txt generated by test.py
|
||||
fig, ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True)
|
||||
# ax = ax.ravel()
|
||||
|
||||
fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True)
|
||||
# for f in [Path(path) / f'study_coco_{x}.txt' for x in ['yolor-p6', 'yolor-w6', 'yolor-e6', 'yolor-d6']]:
|
||||
for f in sorted(Path(path).glob('study*.txt')):
|
||||
y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T
|
||||
x = np.arange(y.shape[1]) if x is None else np.array(x)
|
||||
s = ['P', 'R', 'mAP@.5', 'mAP@.5:.95', 't_inference (ms/img)', 't_NMS (ms/img)', 't_total (ms/img)']
|
||||
# for i in range(7):
|
||||
# ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8)
|
||||
# ax[i].set_title(s[i])
|
||||
|
||||
j = y[3].argmax() + 1
|
||||
ax2.plot(y[6, 1:j], y[3, 1:j] * 1E2, '.-', linewidth=2, markersize=8,
|
||||
label=f.stem.replace('study_coco_', '').replace('yolo', 'YOLO'))
|
||||
|
||||
ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5],
|
||||
'k.-', linewidth=2, markersize=8, alpha=.25, label='EfficientDet')
|
||||
|
||||
ax2.grid(alpha=0.2)
|
||||
ax2.set_yticks(np.arange(20, 60, 5))
|
||||
ax2.set_xlim(0, 57)
|
||||
ax2.set_ylim(30, 55)
|
||||
ax2.set_xlabel('GPU Speed (ms/img)')
|
||||
ax2.set_ylabel('COCO AP val')
|
||||
ax2.legend(loc='lower right')
|
||||
plt.savefig(str(Path(path).name) + '.png', dpi=300)
|
||||
|
||||
|
||||
def plot_labels(labels, names=(), save_dir=Path(''), loggers=None):
|
||||
# plot dataset labels
|
||||
print('Plotting labels... ')
|
||||
c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes
|
||||
nc = int(c.max() + 1) # number of classes
|
||||
colors = color_list()
|
||||
x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height'])
|
||||
|
||||
# seaborn correlogram
|
||||
sns.pairplot(x, corner=True, diag_kind='auto', kind='hist', diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9))
|
||||
plt.savefig(save_dir / 'labels_correlogram.jpg', dpi=200)
|
||||
plt.close()
|
||||
|
||||
# matplotlib labels
|
||||
matplotlib.use('svg') # faster
|
||||
ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel()
|
||||
ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8)
|
||||
ax[0].set_ylabel('instances')
|
||||
if 0 < len(names) < 30:
|
||||
ax[0].set_xticks(range(len(names)))
|
||||
ax[0].set_xticklabels(names, rotation=90, fontsize=10)
|
||||
else:
|
||||
ax[0].set_xlabel('classes')
|
||||
sns.histplot(x, x='x', y='y', ax=ax[2], bins=50, pmax=0.9)
|
||||
sns.histplot(x, x='width', y='height', ax=ax[3], bins=50, pmax=0.9)
|
||||
|
||||
# rectangles
|
||||
labels[:, 1:3] = 0.5 # center
|
||||
labels[:, 1:] = xywh2xyxy(labels[:, 1:]) * 2000
|
||||
img = Image.fromarray(np.ones((2000, 2000, 3), dtype=np.uint8) * 255)
|
||||
for cls, *box in labels[:1000]:
|
||||
ImageDraw.Draw(img).rectangle(box, width=1, outline=colors[int(cls) % 10]) # plot
|
||||
ax[1].imshow(img)
|
||||
ax[1].axis('off')
|
||||
|
||||
for a in [0, 1, 2, 3]:
|
||||
for s in ['top', 'right', 'left', 'bottom']:
|
||||
ax[a].spines[s].set_visible(False)
|
||||
|
||||
plt.savefig(save_dir / 'labels.jpg', dpi=200)
|
||||
matplotlib.use('Agg')
|
||||
plt.close()
|
||||
|
||||
# loggers
|
||||
for k, v in loggers.items() or {}:
|
||||
if k == 'wandb' and v:
|
||||
v.log({"Labels": [v.Image(str(x), caption=x.name) for x in save_dir.glob('*labels*.jpg')]}, commit=False)
|
||||
|
||||
|
||||
def plot_evolution(yaml_file='data/hyp.finetune.yaml'): # from utils.plots import *; plot_evolution()
|
||||
# Plot hyperparameter evolution results in evolve.txt
|
||||
with open(yaml_file) as f:
|
||||
hyp = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
x = np.loadtxt('evolve.txt', ndmin=2)
|
||||
f = fitness(x)
|
||||
# weights = (f - f.min()) ** 2 # for weighted results
|
||||
plt.figure(figsize=(10, 12), tight_layout=True)
|
||||
matplotlib.rc('font', **{'size': 8})
|
||||
for i, (k, v) in enumerate(hyp.items()):
|
||||
y = x[:, i + 7]
|
||||
# mu = (y * weights).sum() / weights.sum() # best weighted result
|
||||
mu = y[f.argmax()] # best single result
|
||||
plt.subplot(6, 5, i + 1)
|
||||
plt.scatter(y, f, c=hist2d(y, f, 20), cmap='viridis', alpha=.8, edgecolors='none')
|
||||
plt.plot(mu, f.max(), 'k+', markersize=15)
|
||||
plt.title('%s = %.3g' % (k, mu), fontdict={'size': 9}) # limit to 40 characters
|
||||
if i % 5 != 0:
|
||||
plt.yticks([])
|
||||
print('%15s: %.3g' % (k, mu))
|
||||
plt.savefig('evolve.png', dpi=200)
|
||||
print('\nPlot saved as evolve.png')
|
||||
|
||||
|
||||
def profile_idetection(start=0, stop=0, labels=(), save_dir=''):
|
||||
# Plot iDetection '*.txt' per-image logs. from utils.plots import *; profile_idetection()
|
||||
ax = plt.subplots(2, 4, figsize=(12, 6), tight_layout=True)[1].ravel()
|
||||
s = ['Images', 'Free Storage (GB)', 'RAM Usage (GB)', 'Battery', 'dt_raw (ms)', 'dt_smooth (ms)', 'real-world FPS']
|
||||
files = list(Path(save_dir).glob('frames*.txt'))
|
||||
for fi, f in enumerate(files):
|
||||
try:
|
||||
results = np.loadtxt(f, ndmin=2).T[:, 90:-30] # clip first and last rows
|
||||
n = results.shape[1] # number of rows
|
||||
x = np.arange(start, min(stop, n) if stop else n)
|
||||
results = results[:, x]
|
||||
t = (results[0] - results[0].min()) # set t0=0s
|
||||
results[0] = x
|
||||
for i, a in enumerate(ax):
|
||||
if i < len(results):
|
||||
label = labels[fi] if len(labels) else f.stem.replace('frames_', '')
|
||||
a.plot(t, results[i], marker='.', label=label, linewidth=1, markersize=5)
|
||||
a.set_title(s[i])
|
||||
a.set_xlabel('time (s)')
|
||||
# if fi == len(files) - 1:
|
||||
# a.set_ylim(bottom=0)
|
||||
for side in ['top', 'right']:
|
||||
a.spines[side].set_visible(False)
|
||||
else:
|
||||
a.remove()
|
||||
except Exception as e:
|
||||
print('Warning: Plotting error for %s; %s' % (f, e))
|
||||
|
||||
ax[1].legend()
|
||||
plt.savefig(Path(save_dir) / 'idetection_profile.png', dpi=200)
|
||||
|
||||
|
||||
def plot_results_overlay(start=0, stop=0): # from utils.plots import *; plot_results_overlay()
|
||||
# Plot training 'results*.txt', overlaying train and val losses
|
||||
s = ['train', 'train', 'train', 'Precision', 'mAP@0.5', 'val', 'val', 'val', 'Recall', 'mAP@0.5:0.95'] # legends
|
||||
t = ['Box', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles
|
||||
for f in sorted(glob.glob('results*.txt') + glob.glob('../../Downloads/results*.txt')):
|
||||
results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
|
||||
n = results.shape[1] # number of rows
|
||||
x = range(start, min(stop, n) if stop else n)
|
||||
fig, ax = plt.subplots(1, 5, figsize=(14, 3.5), tight_layout=True)
|
||||
ax = ax.ravel()
|
||||
for i in range(5):
|
||||
for j in [i, i + 5]:
|
||||
y = results[j, x]
|
||||
ax[i].plot(x, y, marker='.', label=s[j])
|
||||
# y_smooth = butter_lowpass_filtfilt(y)
|
||||
# ax[i].plot(x, np.gradient(y_smooth), marker='.', label=s[j])
|
||||
|
||||
ax[i].set_title(t[i])
|
||||
ax[i].legend()
|
||||
ax[i].set_ylabel(f) if i == 0 else None # add filename
|
||||
fig.savefig(f.replace('.txt', '.png'), dpi=200)
|
||||
|
||||
|
||||
def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''):
|
||||
# Plot training 'results*.txt'. from utils.plots import *; plot_results(save_dir='runs/train/exp')
|
||||
fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True)
|
||||
ax = ax.ravel()
|
||||
s = ['Box', 'Objectness', 'Classification', 'Precision', 'Recall',
|
||||
'val Box', 'val Objectness', 'val Classification', 'mAP@0.5', 'mAP@0.5:0.95']
|
||||
if bucket:
|
||||
# files = ['https://storage.googleapis.com/%s/results%g.txt' % (bucket, x) for x in id]
|
||||
files = ['results%g.txt' % x for x in id]
|
||||
c = ('gsutil cp ' + '%s ' * len(files) + '.') % tuple('gs://%s/results%g.txt' % (bucket, x) for x in id)
|
||||
os.system(c)
|
||||
else:
|
||||
files = list(Path(save_dir).glob('results*.txt'))
|
||||
assert len(files), 'No results.txt files found in %s, nothing to plot.' % os.path.abspath(save_dir)
|
||||
for fi, f in enumerate(files):
|
||||
try:
|
||||
results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
|
||||
n = results.shape[1] # number of rows
|
||||
x = range(start, min(stop, n) if stop else n)
|
||||
for i in range(10):
|
||||
y = results[i, x]
|
||||
if i in [0, 1, 2, 5, 6, 7]:
|
||||
y[y == 0] = np.nan # don't show zero loss values
|
||||
# y /= y[0] # normalize
|
||||
label = labels[fi] if len(labels) else f.stem
|
||||
ax[i].plot(x, y, marker='.', label=label, linewidth=2, markersize=8)
|
||||
ax[i].set_title(s[i])
|
||||
# if i in [5, 6, 7]: # share train and val loss y axes
|
||||
# ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
|
||||
except Exception as e:
|
||||
print('Warning: Plotting error for %s; %s' % (f, e))
|
||||
|
||||
ax[1].legend()
|
||||
fig.savefig(Path(save_dir) / 'results.png', dpi=200)
|
||||
|
||||
|
||||
def output_to_keypoint(output):
|
||||
# Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
|
||||
targets = []
|
||||
for i, o in enumerate(output):
|
||||
kpts = o[:,6:]
|
||||
o = o[:,:6]
|
||||
for index, (*box, conf, cls) in enumerate(o.detach().cpu().numpy()):
|
||||
targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf, *list(kpts.detach().cpu().numpy()[index])])
|
||||
return np.array(targets)
|
||||
|
||||
|
||||
def plot_skeleton_kpts(im, kpts, steps, orig_shape=None):
|
||||
#Plot the skeleton and keypointsfor coco datatset
|
||||
palette = np.array([[255, 128, 0], [255, 153, 51], [255, 178, 102],
|
||||
[230, 230, 0], [255, 153, 255], [153, 204, 255],
|
||||
[255, 102, 255], [255, 51, 255], [102, 178, 255],
|
||||
[51, 153, 255], [255, 153, 153], [255, 102, 102],
|
||||
[255, 51, 51], [153, 255, 153], [102, 255, 102],
|
||||
[51, 255, 51], [0, 255, 0], [0, 0, 255], [255, 0, 0],
|
||||
[255, 255, 255]])
|
||||
|
||||
skeleton = [[16, 14], [14, 12], [17, 15], [15, 13], [12, 13], [6, 12],
|
||||
[7, 13], [6, 7], [6, 8], [7, 9], [8, 10], [9, 11], [2, 3],
|
||||
[1, 2], [1, 3], [2, 4], [3, 5], [4, 6], [5, 7]]
|
||||
|
||||
pose_limb_color = palette[[9, 9, 9, 9, 7, 7, 7, 0, 0, 0, 0, 0, 16, 16, 16, 16, 16, 16, 16]]
|
||||
pose_kpt_color = palette[[16, 16, 16, 16, 16, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9]]
|
||||
radius = 5
|
||||
num_kpts = len(kpts) // steps
|
||||
|
||||
for kid in range(num_kpts):
|
||||
r, g, b = pose_kpt_color[kid]
|
||||
x_coord, y_coord = kpts[steps * kid], kpts[steps * kid + 1]
|
||||
if not (x_coord % 640 == 0 or y_coord % 640 == 0):
|
||||
if steps == 3:
|
||||
conf = kpts[steps * kid + 2]
|
||||
if conf < 0.5:
|
||||
continue
|
||||
cv2.circle(im, (int(x_coord), int(y_coord)), radius, (int(r), int(g), int(b)), -1)
|
||||
|
||||
for sk_id, sk in enumerate(skeleton):
|
||||
r, g, b = pose_limb_color[sk_id]
|
||||
pos1 = (int(kpts[(sk[0]-1)*steps]), int(kpts[(sk[0]-1)*steps+1]))
|
||||
pos2 = (int(kpts[(sk[1]-1)*steps]), int(kpts[(sk[1]-1)*steps+1]))
|
||||
if steps == 3:
|
||||
conf1 = kpts[(sk[0]-1)*steps+2]
|
||||
conf2 = kpts[(sk[1]-1)*steps+2]
|
||||
if conf1<0.5 or conf2<0.5:
|
||||
continue
|
||||
if pos1[0]%640 == 0 or pos1[1]%640==0 or pos1[0]<0 or pos1[1]<0:
|
||||
continue
|
||||
if pos2[0] % 640 == 0 or pos2[1] % 640 == 0 or pos2[0]<0 or pos2[1]<0:
|
||||
continue
|
||||
cv2.line(im, pos1, pos2, (int(r), int(g), int(b)), thickness=2)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue