Tutorial.md 115 KB
Newer Older
lijian6's avatar
lijian6 committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
<img src="Images/Logo.png"  height="60" width="180" align=left>

<br />
<br />
<br />
<br />

<p align=center>  <font size=10> <b> MIGraphX Samples教程</b> </font> </p>
<p align=center>  <font size=6>  v2.4.2  </font> </p>

<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />

<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />

<p align=right>  <font size=4>  2023.02.06 </font> </p>



<div STYLE="page-break-after: always;"></div>
# 目录

[TOC]

<div STYLE="page-break-after: always;"></div>

# C++ Samples



## 分类器

### 模型简介

本示例使用了经典的mnist模型,模型下载地址:https://github.com/onnx/models/blob/main/vision/classification/mnist/model/mnist-12.onnx,模型结构如下图所示(可以通过netron (https://netron.app/) 查看),该模型的输入shape为[1,1,28,28] ,数据排布为NCHW,输出是10个类别的概率(未归一化)。

![image-20221212165226581](Images/Classifier_01.png)



### 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

1. 转换为单通道灰度图
2. resize到28x28
3. 将像素值归一化到[0.0, 1.0]
4. 转换数据排布为NCHW



本示例代码采用了OpenCV的cv::dnn::blobFromImages()函数实现了预处理操作:

```
ErrorCode Classifier::Classify(const std::vector<cv::Mat> &srcImages,std::vector<std::vector<ResultOfPrediction>> &predictions)
{
	...
	
    // 预处理
    cv::Mat inputBlob;
    cv::dnn::blobFromImages(srcImages,// 输入数据,支持多张图像
                    inputBlob, // 输出数据
                    scale, //  缩放系数,这里为1/255.0
                    inputSize, // 模型输入大小,这里为28x28
                    meanValue, // 均值,这里不需要减均值,所以设置为0.0
                    swapRB, // 单通道图像,这里设置为0
                    false);
                    
     ...
}
```

cv::dnn::blobFromImages()函数支持多个输入图像,首先将输入图像resize到inputSize,然后减去均值meanValue,最后乘以scale并转换为NCHW,最终将转换好的数据保存到inputBlob中,然后就可以输入到模型中执行推理了。



### 推理

完成预处理后,就可以执行推理了:

```
ErrorCode Classifier::Classify(const std::vector<cv::Mat> &srcImages,std::vector<std::vector<ResultOfPrediction>> &predictions)
{
	...
	
	// 预处理
	
	// 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> results = net.eval(inputData);

    // 获取输出节点的属性
    migraphx::argument result  = results[0]; // 获取第一个输出节点的数据
   	
   	...
   	
}
```

1. inputData表示MIGraphX的输入数据,inputData是一个映射关系,每个输入节点名都会对应一个输入数据,如果有多个输入,则需要为每个输入节点名创建数据,inputName表示输入节点名,这里为Input3,migraphx::argument{inputShape, (float*)inputBlob.data}表示该节点名对应的数据,这里是通过前面预处理的数据inputBlob来创建的,第一个参数表示数据的shape,第二个参数表示数据指针。
2. net.eval(inputData)返回模型的推理结果,由于这里只有一个输出节点,所以std::vector中只有一个数据,results[0]表示第一个输出节点,这里对应Plus214_Output_0节点,获取输出数据之后,就可以对输出数据进行各种操作了。
3. 由于该模型输出的是一个未归一化的概率,所以如果需要得到每一类的实际的概率值,还需要计算softmax。



### 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```
./MIGraphX_Samples 0
```

输出结果为:

```
...
========== 0 result ==========
label:0,confidence:0.000000
label:1,confidence:0.000034
label:2,confidence:0.000012
label:3,confidence:0.000169
label:4,confidence:0.000044
label:5,confidence:0.000001
label:6,confidence:0.000000
label:7,confidence:0.000725
label:8,confidence:0.000278
label:9,confidence:0.998737
```

由于示例图像为数字9,所以结果中label为9的概率最高。

![image-20221212173655309](Images/Classifier_02.png)



<div STYLE="page-break-after: always;"></div>

## SSD检测器

### 模型简介

SSD检测器示例使用了经典的SSD算法(https://arxiv.org/abs/1512.02325),原论文作者使用Caffe框架实现,本示例采用的是于仕琪老师开源的YuFaceDetectNet人脸检测模型,最初也是采用Caffe框架实现的,从下面的commit对应的工程中可以下载到Caffe模型:https://github.com/ShiqiYu/libfacedetection/commit/54b8e036b299b4763afa6a74af2502a8b13eb0ad,模型在libfacedetection/models/caffe目录下,该目录下同时提供了训练和部署的模型结构,本示例采用了yufacedetectnet-open-v2模型。



### 模型转换

由于MIGraphX不支持YuFaceDetectNet模型中的Permute,PriorBox和DetectionOutput这几个层,所以需要对模型做一些修改,基本的思路就是让MIGraphX不支持的层在CPU上运行。

![image-20221214191602193](Images/SSD_01.png)

图中黄色部分为MIGraphX不支持的层,导出onnx模型的时候需要删除这些层,将这些层都放到CPU上执行。  修改Resource/Models/Detector/SSD/yufacedetectnet-open-v2.prototxt文件的时候,直接删除不支持的层即可,修改后的文件为Resource/Models/Detector/SSD/yufacedetectnet-open-v2_onnx.prototxt文件,可以使用比较工具查看修改的部分,使用yufacedetectnet-open-v2_onnx.prototxt和yufacedetectnet-open-v2.caffemodel通过caffe-onnx(https://github.com/htshinichi/caffe-onnx)这个转换工具就可以将Caffe模型转换为onnx模型了。

注意:使用Caffe训练SSD模型的时候,需要去掉Normalize层,否则使用caffe-onnx工具转换模型的时候会失败,本示例中直接将yufacedetectnet-open-v2.prototxt中的Normalize层去掉了。



### SSD参数设置

在运行模型前,需要设置SSD模型的参数,Resource/Configuration.xml文件中的DetectorSSD节点对应了本示例使用的YuFaceDetectNet检测器的参数,下面看一下是如何根据yufacedetectnet-open-v2.prototxt设置Configuration.xml中的参数的,可以使用netscope (http://ethereon.github.io/netscope/#/editor ) 可视化.prototxt文件,方便观察模型结构,在yufacedetectnet-open-v2.prototxt中一共有4个priorbox层,分别为conv3_3_norm_mbox_priorbox,conv4_3_norm_mbox_priorbox,conv5_3_norm_mbox_priorbox,conv6_3_norm_mbox_priorbox,以conv3_3_norm_mbox_priorbox为例,其prototxt文件中的代码如下:

```
layer {
  name: "conv3_3_norm_mbox_priorbox"
  type: "PriorBox"
  bottom: "conv3_3_norm"
  bottom: "data"
  top: "conv3_3_norm_mbox_priorbox"
  prior_box_param {
    min_size: 10.0
    min_size: 16.0
    min_size: 24.0
    clip: false
    variance: 0.10000000149
    variance: 0.10000000149
    variance: 0.20000000298
    variance: 0.20000000298
    step: 8.0
    offset: 0.5
  }
}
```

一共有3个min_size:10,16,24,同时clip为fase且step为8,该层没有设置flip参数,所以采用默认值true(注:如果该层只有一个宽高比为1的anchor,则flip参数可以设置为false),由于该priorbox层没有设置其他宽高比,只包含一个宽高比为1的anchor,所以Configuration.xml中不需要设置宽高比,所以配置文件中对应的参数为:

```
<MinSize11>10</MinSize11>
<MinSize12>16</MinSize12>
<MinSize13>24</MinSize13>

<Flip1>0</Flip1>

<Clip1>0</Clip1>

<PriorBoxStepWidth1>8</PriorBoxStepWidth1>

<PriorBoxStepHeight1>8</PriorBoxStepHeight1>
```

如果你需要添加其他宽高比的anchor,在设置priorbox层参数的时候需要注意:Configuration.xml中AspectRatio参数设置的时候不需要包含1,因为程序中默认已经添加1了,同时需要忽略flip参数,比如现在有4个priorbox层,每一层的宽高比设置为0.3333和0.25且flip为true,则每一层只需要写0.3333和0.25即可,xml代码如下:

```
<AspectRatio11>0.3333</AspectRatio11>
<AspectRatio12>0.25</AspectRatio12>
<AspectRatio21>0.3333</AspectRatio21>
<AspectRatio22>0.25</AspectRatio22>
<AspectRatio31>0.3333</AspectRatio31>
<AspectRatio32>0.25</AspectRatio32>
<AspectRatio41>0.3333</AspectRatio41>
<AspectRatio42>0.25</AspectRatio42>
```

其他priorbox层参数的设置与conv3_3_norm_mbox_priorbox类似,这里需要注意:**Configuration.xml中priorbox层的顺序要与onnx文件中的输出节点顺序保持一致**,通过netron (https://netron.app/) 查看到onnx文件的输出顺序如下:

![image-20221214202259180](Images/SSD_02.png)

所以Configuration.xml中priorbox层的顺序为conv3_3_norm_mbox_priorbox,conv4_3_norm_mbox_priorbox,conv5_3_norm_mbox_priorbox,conv6_3_norm_mbox_priorbox,所以Configuration.xml中minisize参数设置如下:

```
<MinSize11>10</MinSize11> <!--conv3_3_norm_mbox_priorbox-->
<MinSize12>16</MinSize12>
<MinSize13>24</MinSize13>
<MinSize21>32</MinSize21> <!--conv4_3_norm_mbox_priorbox-->
<MinSize22>48</MinSize22>
<MinSize31>64</MinSize31> <!--conv5_3_norm_mbox_priorbox-->
<MinSize32>96</MinSize32>
<MinSize41>128</MinSize41> <!--conv6_3_norm_mbox_priorbox-->
<MinSize42>192</MinSize42>
<MinSize43>256</MinSize43>
```

设置好priorbox参数后,还需要设置DetectionOutput层的参数,由于本示例模型是一个人脸检测模型,所以只有两类目标(背景和人脸),所以ClassNumber为2,DetectionOutput层的其他参数可以根据实际情况做微调,本示例中采用如下设置:

```
<TopK>400</TopK>
<KeepTopK>200</KeepTopK>
<NMSThreshold>0.3</NMSThreshold>
<ConfidenceThreshold>0.9</ConfidenceThreshold>
```



### 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

1. 减去均值,本示例使用的模型不需要减均值
2. 转换数据排布为NCHW

本示例代码采用了OpenCV的cv::dnn::blobFromImage()函数实现了预处理操作:

```
ErrorCode DetectorSSD::Detect(const cv::Mat &srcImage,std::vector<ResultOfDetection> &resultsOfDetection)
{
	...

    // 预处理并转换为NCHW
    cv::Mat inputBlob;
    blobFromImage(srcImage,   // 输入数据
                    inputBlob, // 输出数据
                    scale, // 1
                    inputSize, // SSD输入大小,本示例为640x480
                    meanValue,// 本示例不需要减均值,这里设置为0
                    swapRB, // false
                    false);
    
    ...
 }
```



### 推理

模型转换成功并且设置好SSD参数之后就可以执行推理了,对于MIGraphX不支持的SSD层,需要在CPU上实现,示例代码Src/Detector/DetectorSSD.h中的PermuteLayer(),PriorBoxLayer(),DetectionOutputLayer()分别实现了对应的层。

```
ErrorCode DetectorSSD::Detect(const cv::Mat &srcImage,std::vector<ResultOfDetection> &resultsOfDetection)
{

    ...
 
    // 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> inferenceResults=net.eval(inputData);
    vector<vector<float>> regressions;
    vector<vector<float>> classifications;
    for(int i=0;i<ssdParameter.numberOfPriorBoxLayer;++i) // 执行Permute操作
    {
        int numberOfPriorBox=ssdParameter.detectInputChn[i]/(4*(ssdParameter.priorBoxHeight[i] * ssdParameter.priorBoxWidth[i]));

        // 回归
        std::vector<float> regression;
        migraphx::argument result0  = inferenceResults[2*i]; 
        result0.visit([&](auto output) { regression.assign(output.begin(), output.end()); });
        regression=PermuteLayer(regression,ssdParameter.priorBoxWidth[i],ssdParameter.priorBoxHeight[i],numberOfPriorBox*4);
        regressions.push_back(regression);
        
        // 分类
        std::vector<float> classification;
        migraphx::argument result1  = inferenceResults[2*i+1]; 
        result1.visit([&](auto output) { classification.assign(output.begin(), output.end()); });
        classification=PermuteLayer(classification,ssdParameter.priorBoxWidth[i],ssdParameter.priorBoxHeight[i],numberOfPriorBox*ssdParameter.classNum);
        classifications.push_back(classification);
    }

    // 对推理结果进行处理,得到最后SSD检测的结果
    GetResult(classifications,regressions,resultsOfDetection);

    // 转换到原图坐标
    for(int i=0;i<resultsOfDetection.size();++i)
    {
        float ratioOfWidth=(1.0*srcImage.cols)/inputSize.width;
        float ratioOfHeight=(1.0*srcImage.rows)/inputSize.height;

        resultsOfDetection[i].boundingBox.x*=ratioOfWidth;
        resultsOfDetection[i].boundingBox.width*=ratioOfWidth;
        resultsOfDetection[i].boundingBox.y*=ratioOfHeight;
        resultsOfDetection[i].boundingBox.height*=ratioOfHeight;
    }

    // 按照置信度排序
    sort(resultsOfDetection.begin(), resultsOfDetection.end(),CompareConfidence);

    return SUCCESS;

}
```

1. net.eval(inputData)返回推理结果,顺序与onnx输出保持一致,可以通过netron查看输出节点顺序,其中inferenceResults[2 * i]表示每个检测层的回归节点的输出,inferenceResults [2 * i + 1]表示每个检测层的分类节点的输出。

1. 经过PermuteLayer层处理之后的所有检测层数据通过GetResult()得到最后的输出结果,注意这里的输出结果还不是最后的检测结果,最后需要转换到原图坐标才能够得到最终的检测结果。





### 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```
./MIGraphX_Samples 1
```

会在当前目录生成检测结果图像Result.jpg

![image-20221214203849188](Images/SSD_03.png)



<div STYLE="page-break-after: always;"></div>

## RetinaFace人脸检测器

### 模型简介

RetinaFace是一个经典的人脸检测模型(https://arxiv.org/abs/1905.00641),采用了SSD架构。

![image-20221215140647406](Images/RetinaFace_01.png)

本示例采用了如下的开源实现:https://github.com/biubug6/Pytorch_Retinaface,作者提供了restnet50 和mobilenet0.25两个预训练模型,本示例使用了mobilenet0.25预训练模型,将mobilenet0.25预训练模型下载下来后,保存到Pytorch_Retinaface工程的weights目录。



### 模型转换

在将mobilenet0.25预训练模型转换为onnx文件的时候,本示例需要对作者提供的python代码做如下改变:

#### 修改models/retinaface.py

1. **将ClassHead类修改为如下实现**

   ```
   class ClassHead(nn.Module):
       def __init__(self,inchannels=512,num_anchors=3):
           super(ClassHead,self).__init__()
           self.num_anchors = num_anchors
           self.conv1x1 = nn.Conv2d(inchannels,self.num_anchors*2,kernel_size=(1,1),stride=1,padding=0)
   
       def forward(self,x):
           out = self.conv1x1(x)
           
           return out
   ```

   由于本示例的C++推理代码已经实现了permute操作,所以这里需要去掉out.permute(0,2,3,1).contiguous()。

2. **将BboxHead类修改为如下实现**

   ```
   class BboxHead(nn.Module):
       def __init__(self,inchannels=512,num_anchors=3):
           super(BboxHead,self).__init__()
           self.conv1x1 = nn.Conv2d(inchannels,num_anchors*4,kernel_size=(1,1),stride=1,padding=0)
   
       def forward(self,x):
           out = self.conv1x1(x)
   
           return out
   ```

   与ClassHead一样,需要去掉permute操作。

3. **将RetinaFace类的forward修改为如下实现**

   ```
   def forward(self,inputs):
           out = self.body(inputs)
   
           # FPN
           fpn = self.fpn(out)
   
           # SSH
           feature1 = self.ssh1(fpn[0])
           feature2 = self.ssh2(fpn[1])
           feature3 = self.ssh3(fpn[2])
           features = [feature1, feature2, feature3]
   
           bbox_regressions = [self.BboxHead[i](feature) for i, feature in enumerate(features)]
           classifications = [self.ClassHead[i](feature) for i, feature in enumerate(features)]
          
           output=(bbox_regressions[0],classifications[0],bbox_regressions[1],classifications[1],bbox_regressions[2],classifications[2])
   
           return output
   ```

   本示例去掉了landmark检测功能,所以需要去掉forward中的landmark部分,bbox_regressions和classifications需要删除torch.cat操作,同时需要修改output为(bbox_regressions[0],classifications[0],bbox_regressions[1],classifications[1],bbox_regressions[2],classifications[2])。



#### 修改data/config.py

将cfg_mnet中的'pretrain': True,修改为'pretrain': False,



#### 修改convert_to_onnx.py

导出onnx模型的时候,需要修改原来的output_names,可以直接删除torch.onnx._export()的output_names参数或者手动指定每个输出节点的名字,如果直接删除了output_names参数,则会生成一个随机名,本示例直接删除了output_names参数,同时本示例修改了onnx文件名output_onnx,修改后的main函数如下:

```
if __name__ == '__main__':
    torch.set_grad_enabled(False)
    cfg = None
    if args.network == "mobile0.25":
        cfg = cfg_mnet
    elif args.network == "resnet50":
        cfg = cfg_re50
    # net and model
    net = RetinaFace(cfg=cfg, phase = 'test')
    net = load_model(net, args.trained_model, args.cpu)
    net.eval()
    print('Finished loading model!')
    print(net)
    device = torch.device("cpu" if args.cpu else "cuda")
    net = net.to(device)

    # ------------------------ export -----------------------------
    output_onnx = 'mobilenet0.25_Final.onnx'
    print("==> Exporting model to ONNX format at '{}'".format(output_onnx))
    input_names = ["input0"]
    output_names = ["output0"]
    inputs = torch.randn(1, 3, args.long_side, args.long_side).to(device)

    torch_out = torch.onnx._export(net, inputs, output_onnx, export_params=True, verbose=False,
                                   input_names=input_names)
```

注意:如果需要修改模型的输入大小,可以修改args.long_side参数,默认为640x640。



完成上述修改后,执行python convert_to_onnx.py命令就可以实现模型转换了,转换成功后会在当前目录生成mobilenet0.25_Final.onnx文件,下面就可以进行推理了。本示例将修改好的工程保存到了samples工程中的Resource/Models/Detector/RetinaFace目录中,在Pytorch_Retinaface目录中执行python convert_to_onnx.py命令可以直接生成onnx文件。



### 检测器参数设置

samples工程中的Resource/Configuration.xml文件的DetectorRetinaFace节点表示RetinaFace检测器的参数,这些参数是根据Pytorch_Retinaface工程中的data/config.py文件中的cfg_mnet来设置的,下面我们看一下是如何通过cfg_mnet来设置的。

2. **设置anchor大小**
   cfg_mnet的min_sizes表示每一个priorbox层的anchor大小,我们可以看到该模型一共有3个priorbox层,第一层anchor大小为16和32,第二层anchor大小为64和128,第三层anchor大小为256和512,注意:**Configuration.xml中priorbox层的顺序要与onnx文件中的输出节点顺序保持一致**,通过netron (https://netron.app/) 可以看到首先输出的是467和470节点,这两个节点对应的是特征图最大的检测层,所以对应的anchor大小为16和32,最后输出的是469和472节点,这两个节点对应的是特征图最小的检测层,所以对应的anchor大小为256和512,

   ![image-20221215153957174](Images/RetinaFace_02.png)
   
   
   所以Configuration.xml配置文件中的参数设置如下:
   
   ```
   <!--priorbox层的个数-->
   <PriorBoxLayerNumber>3</PriorBoxLayerNumber>
   
   <!--每个priorbox层的minisize-->
   <MinSize11>16</MinSize11>
   <MinSize12>32</MinSize12>
   <MinSize21>64</MinSize21>
   <MinSize22>128</MinSize22>
   <MinSize31>256</MinSize31>
   <MinSize32>512</MinSize32>
   ```
   
3. **设置Flip和Clip**
   cfg_mnet中的clip为False,所以Configuration.xml中对应的参数设置为0即可,由于只有一个宽高比为1的anchor,所以Flip设置为0。

   ```
   <Flip1>0</Flip1>
   <Flip2>0</Flip2>
   <Flip3>0</Flip3>
   
   <Clip1>0</Clip1>
   <Clip2>0</Clip2>
   <Clip3>0</Clip3>
   ```

4. **设置anchor的宽高比**
   由于RetinaFace只包含宽高比为1的anchor,所以这里不需要设置宽高比。

5. **设置每个priorbox层的步长**
   cfg_mnet中的steps表示每个priorbox层的步长,所以三个priorbox的步长依次为8,16,32,对应的Configuration.xml的设置如下:

   ```
   <!--每个priorbox层的step-->
   <PriorBoxStepWidth1>8</PriorBoxStepWidth1><!--第一个priorbox层的step的width-->
   <PriorBoxStepWidth2>16</PriorBoxStepWidth2>
   <PriorBoxStepWidth3>32</PriorBoxStepWidth3>
   
   <PriorBoxStepHeight1>8</PriorBoxStepHeight1><!--第一个priorbox层的step的height-->
   <PriorBoxStepHeight2>16</PriorBoxStepHeight2>
   <PriorBoxStepHeight3>32</PriorBoxStepHeight3>
   ```

6. **设置DetectionOutput层的参数**

   由于本示例模型是一个人脸检测模型,所以只有两类目标(背景和人脸),所以ClassNumber为2,DetectionOutput层的其他参数可以根据实际情况做微调,本示例中采用如下设置:

   ```
   <TopK>400</TopK>
   <KeepTopK>200</KeepTopK>
   <NMSThreshold>0.3</NMSThreshold>
   <ConfidenceThreshold>0.9</ConfidenceThreshold>
   ```



### 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

1. 减去均值,RetinaFace训练的时候对图像做了减均值的操作(train.py文件中的第38行),注意均值的顺序是BGR顺序。
2. 转换数据排布为NCHW



本示例代码采用了OpenCV的cv::dnn::blobFromImage()函数实现了预处理操作:

```
ErrorCode DetectorRetinaFace::Detect(const cv::Mat &srcImage,std::vector<ResultOfDetection> &resultsOfDetection)
{
	...

    // 预处理并转换为NCHW
    cv::Mat inputBlob;
    blobFromImage(srcImage,   // 输入数据
                    inputBlob, // 输出数据
                    scale, // 1
                    inputSize, // SSD输入大小,本示例为640x480
                    meanValue,// (104,117,123)
                    swapRB, // false
                    false);
    
    ...
 }
```



### 推理

模型转换成功并且设置好检测器参数之后就可以执行推理了。

```
ErrorCode DetectorRetinaFace::Detect(const cv::Mat &srcImage,std::vector<ResultOfDetection> &resultsOfDetection)
{

    ...
 
    // 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> inferenceResults=net.eval(inputData);
    vector<vector<float>> regressions;
    vector<vector<float>> classifications;
    for(int i=0;i<ssdParameter.numberOfPriorBoxLayer;++i) // 执行Permute操作
    {
        int numberOfPriorBox=ssdParameter.detectInputChn[i]/(4*(ssdParameter.priorBoxHeight[i] * ssdParameter.priorBoxWidth[i]));

        // BboxHead
        std::vector<float> regression;
        migraphx::argument result0  = inferenceResults[2*i]; 
        result0.visit([&](auto output) { regression.assign(output.begin(), output.end()); });
        regression=PermuteLayer(regression,ssdParameter.priorBoxWidth[i],ssdParameter.priorBoxHeight[i],numberOfPriorBox*4);
        regressions.push_back(regression);
        
        // ClassHead
        std::vector<float> classification;
        migraphx::argument result1  = inferenceResults[2*i+1]; 
        result1.visit([&](auto output) { classification.assign(output.begin(), output.end()); });
        classification=PermuteLayer(classification,ssdParameter.priorBoxWidth[i],ssdParameter.priorBoxHeight[i],numberOfPriorBox*ssdParameter.classNum);
        classifications.push_back(classification);
    }

    // 对推理结果进行处理,得到最后SSD检测的结果
    GetResult(classifications,regressions,resultsOfDetection);

    // 转换到原图坐标
    for(int i=0;i<resultsOfDetection.size();++i)
    {
        float ratioOfWidth=(1.0*srcImage.cols)/inputSize.width;
        float ratioOfHeight=(1.0*srcImage.rows)/inputSize.height;

        resultsOfDetection[i].boundingBox.x*=ratioOfWidth;
        resultsOfDetection[i].boundingBox.width*=ratioOfWidth;
        resultsOfDetection[i].boundingBox.y*=ratioOfHeight;
        resultsOfDetection[i].boundingBox.height*=ratioOfHeight;
    }

    // 按照置信度排序
    sort(resultsOfDetection.begin(), resultsOfDetection.end(),CompareConfidence);

    return SUCCESS;

}
```

1. net.eval(inputData)返回推理结果,顺序与onnx输出保持一致,可以通过netron查看输出节点顺序,其中inferenceResults[2 * i]表示每个检测层的BboxHead的输出,inferenceResults [2 * i + 1]表示每个检测层的ClassHead的输出。

1. 经过PermuteLayer处理之后的所有检测层数据通过GetResult()得到最后的输出结果,注意这里的输出结果还不是最后的检测结果,最后需要转换到原图坐标才能够得到最终的检测结果。



### 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```
./MIGraphX_Samples 2
```

会在当前目录生成检测结果图像Result.jpg

![image-20221215164140724](Images/RetinaFace_03.png)



<div STYLE="page-break-after: always;"></div>

## 动态shape(MTCNN人脸检测器)

### 模型简介

动态shape示例程序采用了MTCNN人脸检测器模型中的PNet模型(https://arxiv.org/abs/1604.02878),论文主页:https://kpzhang93.github.io/MTCNN_face_detection_alignment/index.html。MTCNN中的PNet是一个典型的动态shape模型,PNet网络结构如下图所示,PNet是一个全卷积网络,所以可以输入不同大小的图像。

![image-20221214101121257](Images/DynamicShape_01.png)

MTCNN检测器在检测之前会对输入图像构建一个图像金字塔,这个图像金字塔中包含了不同尺度的图像,然后将不同尺度的图像依次输入到PNet提取人脸候选区域,经过PNet提取出来的人脸候选区域再经过RNet和ONet进一步筛选和微调得到最后的检测结果。

![image-20221214104253947](Images/DynamicShape_02.png)



本示例对原始的PNet模型做了一些调整,训练的时候将输入图像大小修改为24x24,同时将第1个卷积和第3个卷积的步长修改为2。

### 模型转换

论文中作者提供的是Caffe训练好的模型,可以通过caffe-onnx这个工具(https://github.com/htshinichi/caffe-onnx)转换为onnx模型。



### 设置最大输入shape

由于PNet是一个动态shape模型,所以在推理之前需要设置最大输入shape,Resource/Configuration.xml文件中的DetectorMTCNN节点的MaxHeight和MaxWidth表示最大输入图像的高和宽,可以根据实际情况来定,本示例设置为512和512,程序中通过如下代码设置最大输入shape:

```
// 设置最大输入大小
migraphx::onnx_options onnx_options;
onnx_options.map_input_dims["input"]={1,3,maxHeight,maxWidth};
```



### 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

1. 减去均值127.5并乘以系数0.0078125
2. 转换数据排布为NCHW



本示例代码采用了OpenCV的cv::dnn::blobFromImage()函数实现了预处理操作:

```
cv::Mat inputBlob = blobFromImage(inputImage, 0.0078125, cv::Size(), cv::Scalar(127.5, 127.5, 127.5), false);
```



### 推理

完成预处理后,就可以执行推理了:

```
	...
	
	// 处理图像金字塔的每一级图像
	for (int k = 0; k < scales.size(); ++k)
    {
    	// 输入图像
        float scale = scales[k];
        cv::Mat inputImage;
        cv::Size inputSize=cv::Size(ceil(srcImage.cols*scale), ceil(srcImage.rows*scale));
        resize(srcImage, inputImage,inputSize, 0, 0, cv::INTER_LINEAR);
        
        // 每次输入shape发生变化后,需要对模型进行reshape
        std::vector<std::size_t> inputShapeOfReshape={1,3,inputSize.height,inputSize.width};
        std::unordered_map<std::string, std::vector<std::size_t>> inputShapeMap;
        inputShapeMap[inputName] = inputShapeOfReshape;
        net.reshape(inputShapeMap);

        // 预处理并转换为NCHW
		cv::Mat inputBlob = blobFromImage(inputImage, 0.0078125, cv::Size(), cv::Scalar(127.5, 127.5, 127.5), false);
        
        // 输入数据
        migraphx::parameter_map inputData;
        inputData[inputName]= migraphx::argument{migraphx::shape(inputShape.type(),inputShapeOfReshape), (float*)inputBlob.data};

        // 推理
        std::vector<migraphx::argument> results = net.eval(inputData);

        ...
    }
    
    ...
```

1. for循环表示依次处理图像金字塔的每一级图像,由于每一级图像大小都不一样,所以推理之前需要对模型进行reshape操作,每一级图像经过预处理操作之后就可以输入到模型中执行推理了。

2. results表示推理返回的结果,这里表示conv4-2_Y和prob1_Y节点,results中的顺序与onnx中输出节点保持一致,可以通过netron (https://netron.app/) 查看,可以看出results中依次表示conv4-2_Y,prob1_Y节点,

   ![image-20221214201946030](Images/DynamicShape_04.png)

   返回的结果需要经过进一步的后处理操作才能得到最后的人脸候选区域。





### 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```
# 设置动态shape模式
export MIGRAPHX_DYNAMIC_SHAPE=1

# 运行示例
./MIGraphX_Samples 3
```

会在当前目录生成检测结果图像Result.jpg

![image-20221214112810405](Images/DynamicShape_03.png)



<div STYLE="page-break-after: always;"></div>

## YOLOV3检测器

### 模型简介

YOLOV3是由Joseph Redmon和Ali Farhadi在《YOLOv3: An Incremental Improvement》论文中提出的单阶段检测模型,算法首先通过特征提取网络对输入提取特征,backbone部分由YOLOV2时期的Darknet19进化至Darknet53加深了网络层数,引入了Resnet中的跨层加和操作;然后结合不同卷积层的特征实现多尺度训练,一共有13x13、26x26、52x52三种分辨率,分别用来预测大、中、小的物体;每种分辨率的特征图将输入图像分成不同数量的格子,每个格子预测B个bounding box,每个bounding box预测内容包括: Location(x, y, w, h)、Confidence Score和C个类别的概率,因此YOLOv3输出层的channel数为B*(5 + C)。YOLOv3的loss函数也有三部分组成:Location误差,Confidence误差和分类误差。

<img src="Images/YOLOV3_02.jpg" alt="YOLOV3_02" style="zoom:;" />

本示例采用如下的开源实现:https://github.com/ultralytics/yolov3, 作者在V9.6.0版本中提供多种不同的YOLOV3预训练模型,其中包括yolov3、yolov3-fixed、yolov3-spp、yolov3-tiny四个版本。本示例选择yolov3-tiny.pt预训练模型进行构建MIGraphX推理,下载YOLOV3的预训练模型yolov3-tiny.pt保存在Pytorch_YOLOV3工程的weights目录。

### 模型转换

官方提供的YOLOV3源码中包含导出onnx模型的程序,通过下面的步骤可以将yolov3-tiny.pt转换成onnx格式:

```
# 进入Pytorch_YOLOV3工程根目录
cd <path_to_Pytorch_YOLOV3>

# 环境配置,torch、torchvision手动安装
pip install -r requirements.txt

# 导出onnx模型
python export.py --weights yolov3.pt --imgsz 416 416 --include onnx
```

注意:官方源码提供的模型转换的程序中包含更多的功能,例如动态shape模型的导出,可根据需要进行添加相关参数。

### 检测器参数设置

samples工程中的Resource/Configuration.xml文件的DetectorYOLOV3节点表示YOLOV3检测器的参数,相关参数主要依据官方推理示例进行设置。各个参数含义如下:

- ModelPath:yolov3模型存放路径
- ClassNameFile:coco数据集类别文件存放路径
- UseFP16:是否使用FP16推理模式
- NumberOfClasses:检测类别数量
- ConfidenceThreshold:置信度阈值,用于判断anchor内的物体是否为正样本
- NMSThreshold:非极大值抑制阈值,用于消除重复框
- ObjectThreshold:用于判断anchor内部是否有物体

```
<ModelPath>"../Resource/Models/Detector/YOLOV3/yolov3-tiny.onnx"</ModelPath>
<ClassNameFile>"../Resource/Models/Detector/YOLOV3/coco.names"</ClassNameFile>
<UseFP16>0</UseFP16><!--是否使用FP16-->
<NumberOfClasses>80</NumberOfClasses><!--类别数(不包括背景类),COCO:80,VOC:20-->
<ConfidenceThreshold>0.2</ConfidenceThreshold>
<NMSThreshold>0.4</NMSThreshold>
<ObjectThreshold>0.4</ObjectThreshold>
```

### 模型初始化

模型初始化首先通过parse_onnx()函数加载YOLOV3的onnx模型,并可以通过program的get_parameter_shapes()函数获取网络的输入属性。完成模型加载之后需要使用compile()方法编译模型,编译模式使用migraphx::gpu::target{}设为GPU模式,编译过程主要基于MIGraphX IR完成各种优化。同时如果需要使用低精度量化进行推理,可以使用quantize_fp16()函数实现。

```
ErrorCode DetectorYOLOV3::Initialize(InitializationParameterOfDetector initializationParameterOfDetector)
{
    ...
    
    //模型加载
    net = migraphx::parse_onnx(modelPath);
    LOG_INFO(logFile,"succeed to load model: %s\n",GetFileName(modelPath).c_str());

    // 获取模型输入属性
    std::pair<std::string, migraphx::shape> inputAttribute=*(net.get_parameter_shapes().begin());
    inputName=inputAttribute.first;
    inputShape=inputAttribute.second;
    inputSize=cv::Size(inputShape.lens()[3],inputShape.lens()[2]);// NCHW

    // 设置模型为GPU模式
    migraphx::target gpuTarget = migraphx::gpu::target{};

    // 量化    
    if(useFP16)
    {
        migraphx::quantize_fp16(net);
    }

    // 编译模型
    migraphx::compile_options options;
    options.device_id=0; // 设置GPU设备,默认为0号设备(>=1.2版本中支持)
    options.offload_copy=true; // 设置offload_copy
    net.compile(gpuTarget,options);
    LOG_INFO(logFile,"succeed to compile model: %s\n",GetFileName(modelPath).c_str());

    ...
}
```

### 模型推理

#### 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

- 转换数据排布为NCHW
- 归一化[0.0, 1.0]
- 将输入数据的尺寸变换到YOLOV3输入大小(1,3,416,416)

```
ErrorCode DetectorYOLOV3::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{
   ...

    // 预处理并转换为NCHW
    cv::Mat inputBlob;
    blobFromImage(srcImage,   // 输入数据
                    inputBlob,  // 输出数据
                    1 / 255.0,  //归一化
                    inputSize,  //YOLOV3输入尺寸,本示例为416x416
                    Scalar(0, 0, 0),  //未减去均值
                    true,  //转换RB通道
                    false);
                    
    ...
}
```

#### 前向推理

完成图像预处理以及YOLOV3目标检测相关参数设置之后开始执行推理,利用migraphx推理计算得到YOLOV3模型的输出数据。

```
ErrorCode DetectorYOLOV3::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{

	...
    // 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> inferenceResults = net.eval(inputData);

    // 获取推理结果
    std::vector<cv::Mat> outs;
    migraphx::argument result = inferenceResults[0]; 

    // 转换为cv::Mat
    migraphx::shape outputShape = result.get_shape();
    int shape[]={outputShape.lens()[0],outputShape.lens()[1],outputShape.lens()[2]};
    cv::Mat out(3,shape,CV_32F);
    memcpy(out.data,result.data(),sizeof(float)*outputShape.elements());
    outs.push_back(out);
    
    ...
}
```

YOLOV3的MIGraphX推理结果inferenceResults是一个std::vector< migraphx::argument >类型,YOLOV3的onnx模型包含一个输出,所以result等于inferenceResults[0],result包含三个维度:outputShape.lens()[0]=1表示batch信息,outputShape.lens()[1]=10647表示生成anchor数量,outputShape.lens()[2]=85表示对每个anchor的预测信息。同时可将85拆分为4+1+80,前4个参数用于判断每一个特征点的回归参数,回归参数调整后可以获得预测框,第5个参数用于判断每一个特征点是否包含物体,最后80个参数用于判断每一个特征点所包含的物体种类。获取上述信息之后进行anchors筛选,筛选过程分为两个步骤:

- 第一步根据objectThreshold阈值进行筛选,大于该阈值则判断当前anchor内包含物体,小于该阈值则判断无物体;

- 第二步根据confidenceThreshold阈值进行筛选,当满足第一步阈值anchor的最大置信度得分maxClassScore大于该阈值,则进一步获取当前anchor的坐标信息和预测物体类别信息,小于该阈值则不做处理。

```
ErrorCode DetectorYOLOV3::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{
    ...
    
    //获取先验框的个数numProposal=10647
    int numProposal = outs[0].size[1];
    //获取每个anchor的预测信息数量numOut=85
    int numOut = outs[0].size[2];
    //变换输出的维度
    outs[0] = outs[0].reshape(0, numProposal);

    //生成先验框
    std::vector<float> confidences;
    std::vector<cv::Rect> boxes;
    std::vector<int> classIds;
    //原图尺寸与模型输入尺寸的缩放比例
    float ratioh = (float)srcImage.rows / inputSize.height, ratiow = (float)srcImage.cols / inputSize.width;

    //计算cx,cy,w,h,box_sore,class_sore
    int n = 0, rowInd = 0;
    float* pdata = (float*)outs[0].data;
    for (n = 0; n < numProposal; n++)
    {
        //获取当前anchor是否包含物体的概率值
        float boxScores = pdata[4];
        //第一次筛选,判断anchor内是否包含物体
        if (boxScores > yolov3Parameter.objectThreshold)
        {
            //获取每个anchor内部预测的80个类别概率信息
            cv::Mat scores = outs[0].row(rowInd).colRange(5, numOut);
            cv::Point classIdPoint;
            double maxClassScore;
            /获取80个类别中最大概率值和对应的类别ID
            cv::minMaxLoc(scores, 0, &maxClassScore, 0, &classIdPoint);
            maxClassScore *= boxScores;
            //第二次筛选,判断当前anchor的最大置信度得分是否满足阈值
            if (maxClassScore > yolov3Parameter.confidenceThreshold)
            {
                const int classIdx = classIdPoint.x;
                //将每个anchor坐标按缩放比例映射到原图
                float cx = pdata[0] * ratiow;
                float cy = pdata[1] * ratioh;
                float w = pdata[2] * ratiow;
                float h = pdata[3] * ratioh;
                //获取anchor的左上角坐标
                int left = int(cx - 0.5 * w);
                int top = int(cy - 0.5 * h);

                confidences.push_back((float)maxClassScore);
                boxes.push_back(cv::Rect(left, top, (int)(w), (int)(h)));
                classIds.push_back(classIdx);
            }
        }
        rowInd++;
        pdata += numOut;
    }

    ...
}
```

为了消除重叠锚框,输出最终的YOLOV3目标检测结果,执行非极大值抑制对筛选之后的anchor进行处理,最后保存检测结果到resultsOfDetection中。

```
ErrorCode DetectorYOLOV3::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{    
    ...
    //执行non maximum suppression消除冗余重叠boxes
    std::vector<int> indices;
    dnn::NMSBoxes(boxes, confidences, yolov3Parameter.confidenceThreshold, yolov3Parameter.nmsThreshold, indices);
    for (size_t i = 0; i < indices.size(); ++i)
    {
        int idx = indices[i];
        int classID=classIds[idx];
        string className=classNames[classID];
        float confidence=confidences[idx];
        cv::Rect box = boxes[idx];、
        //保存每个最终预测anchor的坐标值、置信度分数、类别ID
        ResultOfDetection result;
        result.boundingBox=box;
        result.confidence=confidence;// confidence
        result.classID=classID; // label
        result.className=className;
        resultsOfDetection.push_back(result);
    }
    ...
}
```

### 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```
./MIGraphX_Samples 4
```

会在当前目录生成检测结果图像Result.jpg。

![YOLOV3_01](Images/YOLOV3_01.jpg)



<div STYLE="page-break-after: always;"></div>

## YOLOV5检测器

### 模型简介

YOLOV5是一种单阶段目标检测算法,该算法在YOLOV4的基础上添加了一些新的改进思路,使其速度与精度都得到了极大的性能提升。具体包括:输入端的Mosaic数据增强、自适应锚框计算、自适应图片缩放操作;主干网络的Focus结构与CSP结构;Neck端的FPN+PAN结构;输出端的损失函数GIOU_Loss以及预测框筛选的DIOU_nms。网络结构如图所示。

![YOLOV5_01](Images/YOLOV5_01.jpg)

YOLOV5的官方源码地址:https://github.com/ultralytics/yolov5, 官方源码中具有YOLOV5n、YOLOV5s、YOLOV5m、YOLOV5l等不同的版本。本示例采用YOLOV5s版本进行MIGraphX推理示例构建,下载YOLOV5s的预训练模型yolov5s.pt保存在Pytorch_YOLOV5工程的weights目录。

### 模型转换

官方提供的YOLOV5源码中包含导出onnx模型的程序,通过下面的步骤可以将yolov5s.pt预训练模型转换成onnx格式:

```
# 进入Pytorch_YOLOV5工程根目录
cd <path_to_Pytorch_YOLOV5>

# 环境配置,torch、torchvision手动安装
pip install -r requirement.txt

# 转换模型
python export.py --weights ./weights/yolov5s.pt --imgsz 608 608 --include onnx
```

注意:官方源码提供的模型转换的程序中包含更多的功能,例如动态shape模型的导出,可根据需要进行添加相关参数。

### 检测器参数设置

samples工程中的Resource/Configuration.xml文件的DetectorYOLOV5节点表示YOLOV5检测器的参数,相关参数主要依据官方推理示例进行设置。各个参数含义如下:

- ModelPath:yolov5模型存放路径
- ClassNameFile:coco数据集类别文件存放路径
- UseFP16:是否使用FP16推理模式
- NumberOfClasses:检测类别数量
- ConfidenceThreshold:置信度阈值,用于判断anchor内的物体是否为正样本
- NMSThreshold:非极大值抑制阈值,用于消除重复框
- ObjectThreshold:用于判断anchor内部是否有物体

```
<ModelPath>"../Resource/Models/Detector/YOLOV5/YOLOV5s.onnx"</ModelPath>
<ClassNameFile>"../Resource/Models/Detector/YOLOV5/coco.names"</ClassNameFile>
<UseFP16>0</UseFP16><!--是否使用FP16-->
<NumberOfClasses>80</NumberOfClasses><!--类别数(不包括背景类),COCO:80,VOC:20-->
<ConfidenceThreshold>0.25</ConfidenceThreshold>
<NMSThreshold>0.5</NMSThreshold>
<ObjectThreshold>0.5</ObjectThreshold>
```

### 模型初始化

模型初始化首先通过parse_onnx()函数加载YOLOV5的onnx模型,并可以通过program的get_parameter_shapes()函数获取网络的输入属性。完成模型加载之后需要使用compile()方法编译模型,编译模式使用migraphx::gpu::target{}设为GPU模式,编译过程主要基于MIGraphX IR完成各种优化。同时如果需要使用低精度量化进行推理,可以使用quantize_fp16()函数实现。

```
ErrorCode DetectorYOLOV5::Initialize(InitializationParameterOfDetector initializationParameterOfDetector)
{
    ...
    
    //模型加载
    net = migraphx::parse_onnx(modelPath);
    LOG_INFO(logFile,"succeed to load model: %s\n",GetFileName(modelPath).c_str());

    // 获取模型输入属性
    std::pair<std::string, migraphx::shape> inputAttribute=*(net.get_parameter_shapes().begin());
    inputName=inputAttribute.first;
    inputShape=inputAttribute.second;
    inputSize=cv::Size(inputShape.lens()[3],inputShape.lens()[2]);// NCHW

    // 设置模型为GPU模式
    migraphx::target gpuTarget = migraphx::gpu::target{};

    // 量化    
    if(useFP16)
    {
        migraphx::quantize_fp16(net);
    }

    // 编译模型
    migraphx::compile_options options;
    options.device_id=0; // 设置GPU设备,默认为0号设备(>=1.2版本中支持)
    options.offload_copy=true; // 设置offload_copy
    net.compile(gpuTarget,options);
    LOG_INFO(logFile,"succeed to compile model: %s\n",GetFileName(modelPath).c_str());

    ...
}
```

### 模型推理

#### 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

1. 转换数据排布为NCHW
2. 归一化[0.0, 1.0]
3. 将输入数据的尺寸变换到YOLOV5输入大小(1,3,608,608)

```
ErrorCode DetectorYOLOV5::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{ 
  ...
  
    // 预处理并转换为NCHW
    cv::Mat inputBlob;
    blobFromImage(srcImage,   // 输入数据
                    inputBlob,  // 输出数据
                    1 / 255.0,  //归一化
                    inputSize,  //YOLOV5输入尺寸,本示例为608x608
                    Scalar(0, 0, 0),  //未减去均值
                    true,  //转换RB通道
                    false);
                    
   ...
}
```

#### 前向推理

完成图像预处理以及YOLOV5目标检测相关参数设置之后开始执行推理,利用migraphx推理计算得到YOLOV5模型的输出。

```
ErrorCode DetectorYOLOV5::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{

	...
    // 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> inferenceResults = net.eval(inputData);

    // 获取推理结果
    std::vector<cv::Mat> outs;
    migraphx::argument result = inferenceResults[0]; 

    // 转换为cv::Mat
    migraphx::shape outputShape = result.get_shape();
    int shape[]={outputShape.lens()[0],outputShape.lens()[1],outputShape.lens()[2]};
    cv::Mat out(3,shape,CV_32F);
    memcpy(out.data,result.data(),sizeof(float)*outputShape.elements());
    outs.push_back(out);
    
    ...
}
```

YOLOV5的MIGraphX推理结果inferenceResults是一个std::vector< migraphx::argument >类型,YOLOV5的onnx模型包含一个输出,所以result等于inferenceResults[0],result包含三个维度:outputShape.lens()[0]=1表示batch信息,outputShape.lens()[1]=22743表示生成anchor数量,outputShape.lens()[2]=85表示对每个anchor的预测信息。同时可将85拆分为4+1+80,前4个参数用于判断每一个特征点的回归参数,回归参数调整后可以获得预测框,第5个参数用于判断每一个特征点是否包含物体,最后80个参数用于判断每一个特征点所包含的物体种类。获取上述信息之后进行anchors筛选,筛选过程分为两个步骤:

- 第一步根据objectThreshold阈值进行筛选,大于该阈值则判断当前anchor内包含物体,小于该阈值则判断无物体
- 第二步根据confidenceThreshold阈值进行筛选,当满足第一步阈值anchor的最大置信度得分maxClassScore大于该阈值,则进一步获取当前anchor的坐标信息和预测物体类别信息,小于该阈值则不做处理。

```
ErrorCode DetectorYOLOV5::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{

	...
    //获取先验框的个数numProposal=22743
    numProposal = outs[0].size[1];
    //每个anchor的预测信息数量numOut=85
    numOut = outs[0].size[2];
    outs[0] = outs[0].reshape(0, numProposal);

    std::vector<float> confidences;
    std::vector<cv::Rect> boxes;
    std::vector<int> classIds;
    //原图尺寸与模型输入尺寸的缩放比例
    float ratioh = (float)srcImage.rows / inputSize.height, ratiow = (float)srcImage.cols / inputSize.width;

    //计算cx,cy,w,h,box_sore,class_sore
    int n = 0, rowInd = 0;
    float* pdata = (float*)outs[0].data;
    for (n = 0; n < numProposal; n++)
    {
        //获取当前anchor是否包含物体的概率值
        float boxScores = pdata[4];
        
        //第一次筛选,判断anchor内是否包含物体
        if (boxScores > yolov5Parameter.objectThreshold)
        {
            //获取每个anchor内部预测的80个类别概率信息
            cv::Mat scores = outs[0].row(rowInd).colRange(5, numOut);
            cv::Point classIdPoint;
            double maxClassScore;
            
            //获取80个类别中最大概率值和对应的类别ID
            cv::minMaxLoc(scores, 0, &maxClassScore, 0, &classIdPoint);
            maxClassScore *= boxScores;
            
            //第二次筛选,判断当前anchor的最大置信度得分是否满足阈值
            if (maxClassScore > yolov5Parameter.confidenceThreshold)
            {
                const int classIdx = classIdPoint.x;
                
                //将每个anchor坐标按缩放比例映射到原图
                float cx = pdata[0] * ratiow;
                float cy = pdata[1] * ratioh;
                float w = pdata[2] * ratiow;
                float h = pdata[3] * ratioh;
                //获取anchor的左上角坐标
                int left = int(cx - 0.5 * w);
                int top = int(cy - 0.5 * h);

                confidences.push_back((float)maxClassScore);
                boxes.push_back(cv::Rect(left, top, (int)(w), (int)(h)));
                classIds.push_back(classIdx);
            }
        }
        rowInd++;
        pdata += numOut;
    }
	
	...
}
```

为了消除重叠锚框,输出最终的YOLOV5目标检测结果,执行非极大值抑制对筛选之后的anchor进行处理,最后保存检测结果到resultsOfDetection中。

```
ErrorCode DetectorYOLOV5::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{

	...
    
    // 执行non maximum suppression消除冗余重叠boxes
    std::vector<int> indices;
    dnn::NMSBoxes(boxes, confidences, yolov5Parameter.confidenceThreshold, yolov5Parameter.nmsThreshold, indices);
    for (size_t i = 0; i < indices.size(); ++i)
    {
        int idx = indices[i];
        int classID=classIds[idx];
        string className=classNames[classID];
        float confidence=confidences[idx];
        cv::Rect box = boxes[idx];
		
        //保存每个最终预测anchor的坐标值、置信度分数、类别ID
        ResultOfDetection result;
        result.boundingBox=box;
        result.confidence=confidence;// confidence
        result.classID=classID; // label
        result.className=className;
        resultsOfDetection.push_back(result);
    }

    ...
}
```

### 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```
./MIGraphX_Samples 5
```

会在当前目录生成检测结果图像Result.jpg。

<img src="Images/YOLOV5_02.jpg" alt="YOLOV5_02" style="zoom:67%;" />



<div STYLE="page-break-after: always;"></div>

## YOLOV7检测器

### 模型简介

YOLOV7是2022年最新出现的一种YOLO系列目标检测模型,在论文 [YOLOv7: Trainable bag-of-freebies sets new state-of-the-art for real-time object detectors](https://arxiv.org/abs/2207.02696)中提出。

<img src="Images/YOLOV7_02.png" style="zoom:67%;" />

本示例采用YOLOv7的官方源码:https://github.com/WongKinYiu/yolov7, 作者提供了多个预训练模型,本示例使用yolov7-tiny.pt预训练模型,将yolov7-tiny.pt预训练模型下载下来后,保存到Pytorch_YOLOV7工程的weights目录。

### 模型转换

官方提供的YOLOV7源码中包含导出onnx模型的程序,通过下面的步骤可以将yolov7-tiny.pt预训练模型转换成onnx格式:

```
# 进入Pytorch_YOLOV7工程根目录
cd <path_to_Pytorch_YOLOV7>

# 安装程序运行的依赖,torch、torchvision需要手动安装
pip install -r requirement.txt

# 转换模型
python export.py --weights ./weight/yolov7-tiny.pt --img-size 640 640 
```

注意:如果需要修改onnx模型的输入大小,可以调整--img-size参数,同时模型输入batch默认为1,若想修改可以通过添加--batch-size设置,程序运行结束后,在当前目录下会生成onnx格式的YOLOV7模型,并将该模型保存到了samples工程中的Resource/Models/Detector/YOLOV7目录中,可以用来MIGraphX加载推理。

### 检测器参数设置

samples工程中的Resource/Configuration.xml文件的DetectorYOLOV7节点表示YOLOV7检测器的参数,相关参数主要依据官方推理示例进行设置。其中包括模型存放路径、类别名称文件、检测类别数量、置信度阈值、非极大值抑制阈值和判断先验框是否有物体阈值。

- ModelPath:yolov7模型存放路径
- ClassNameFile:coco数据集类别文件存放路径
- UseFP16:是否使用FP16推理模式
- NumberOfClasses:检测类别数量
- ConfidenceThreshold:置信度阈值,用于判断anchor内的物体是否为正样本
- NMSThreshold:非极大值抑制阈值,用于消除重复框
- ObjectThreshold:用于判断anchor内部是否有物体

```
<ModelPath>"../Resource/Models/Detector/YOLOV7/yolov7-tiny.onnx"</ModelPath>
<ClassNameFile>"../Resource/Models/Detector/YOLOV7/coco.names"</ClassNameFile>
<UseFP16>0</UseFP16><!--是否使用FP16-->
<NumberOfClasses>80</NumberOfClasses><!--类别数(不包括背景类),COCO:80,VOC:20-->
<ConfidenceThreshold>0.25</ConfidenceThreshold>
<NMSThreshold>0.5</NMSThreshold>
<ObjectThreshold>0.5</ObjectThreshold>
```

### 模型初始化

模型初始化首先通过parse_onnx()函数加载YOLOV7的onnx模型,并可以通过program的get_parameter_shapes()函数获取网络的输入属性。完成模型加载之后需要使用compile()方法编译模型,编译模式使用migraphx::gpu::target{}设为GPU模式,编译过程主要基于MIGraphX IR完成各种优化。同时如果需要使用低精度量化进行推理,可以使用quantize_fp16()函数实现。

```
ErrorCode DetectorYOLOV7::Initialize(InitializationParameterOfDetector initializationParameterOfDetector)
{
    ...
    
    //模型加载
    net = migraphx::parse_onnx(modelPath);
    LOG_INFO(logFile,"succeed to load model: %s\n",GetFileName(modelPath).c_str());

    // 获取模型输入属性
    std::pair<std::string, migraphx::shape> inputAttribute=*(net.get_parameter_shapes().begin());
    inputName=inputAttribute.first;
    inputShape=inputAttribute.second;
    inputSize=cv::Size(inputShape.lens()[3],inputShape.lens()[2]);// NCHW

    // 设置模型为GPU模式
    migraphx::target gpuTarget = migraphx::gpu::target{};

    // 量化    
    if(useFP16)
    {
        migraphx::quantize_fp16(net);
    }

    // 编译模型
    migraphx::compile_options options;
    options.device_id=0; // 设置GPU设备,默认为0号设备(>=1.2版本中支持)
    options.offload_copy=true; // 设置offload_copy
    net.compile(gpuTarget,options);
    LOG_INFO(logFile,"succeed to compile model: %s\n",GetFileName(modelPath).c_str());

    ...
}
```

### 模型推理

#### 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

- 转换数据排布为NCHW
- 归一化到[0.0, 1.0]
- 将输入数据的尺寸变换到YOLOV7输入大小(1,3,640,640)

```c++
ErrorCode DetectorYOLOV7::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{
	...
        
    // 预处理并转换为NCHW
    cv::Mat inputBlob;
    blobFromImage(srcImage, //输入数据
                    inputBlob, //输出数据
                    1 / 255.0, //缩放系数,这里为1/255.0
                    inputSize, //YOLOV7输入尺寸(640,640)
                    Scalar(0, 0, 0), // 均值,这里不需要减均值,所以设置为0.0
                    true, //转换RB通道
                    false); 
    ...
}
```

#### 前向推理

完成图像预处理以及yolov7目标检测相关参数设置之后开始执行推理,获取migraphx推理结果。

```
ErrorCode DetectorYOLOV7::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{
	...
	
    // 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> inferenceResults=net.eval(inputData);

    // 获取推理结果
    std::vector<cv::Mat> outs;
    migraphx::argument result = inferenceResults[0]; 

    // 转换为cv::Mat
    migraphx::shape outputShape = result.get_shape();
    int shape[]={outputShape.lens()[0],outputShape.lens()[1],outputShape.lens()[2]};
    cv::Mat out(4,shape,CV_32F);
    memcpy(out.data,result.data(),sizeof(float)*outputShape.elements());

    outs.push_back(out);

    ...

}
```

YOLOV7的MIGraphX推理结果inferenceResults是一个std::vector< migraphx::argument >类型,YOLOV7的onnx模型包含一个输出,所以result等于inferenceResults[0],result包含三个维度:outputShape.lens()[0]=1表示batch信息,outputShape.lens()[1]=25200表示生成anchor数量,outputShape.lens()[2]=85表示对每个anchor的预测信息。同时可将85拆分为4+1+80,前4个参数用于判断每一个特征点的回归参数,回归参数调整后可以获得预测框,第5个参数用于判断每一个特征点是否包含物体,最后80个参数用于判断每一个特征点所包含的物体种类。获取上述信息之后进行anchors筛选,筛选过程分为两个步骤:

- 第一步根据objectThreshold阈值进行筛选,大于该阈值则判断当前anchor内包含物体,小于该阈值则判断无物体
- 第二步根据confidenceThreshold阈值进行筛选,当满足第一步阈值anchor的最大置信度得分maxClassScore大于该阈值,则进一步获取当前anchor内部的物体类别和坐标信息,小于该阈值则不做处理。

```
ErrorCode DetectorYOLOV7::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{
	...
	
    //获取先验框的个数numProposal=25200
    int numProposal = outs[0].size[1];
    //每个anchor的预测信息数量numOut=85
    int numOut = outs[0].size[2];
    outs[0] = outs[0].reshape(0, numProposal);

    std::vector<float> confidence;
    std::vector<Rect> boxes
    std::vector<int> classIds
    //原图尺寸与模型输入尺寸的缩放比例
    float ratioh = (float)srcImage.rows / inputSize.height, ratiow = (float)srcImage.cols / inputSize.width;

   //计算cx,cy,w,h,box_sore,class_sore
    int n = 0, rowInd = 0;
    float* pdata = (float*)outs[0].data;
    for (n = 0; n < numProposal; n++)
    {
    	//获取是否包含物体的概率值
        float boxScores = pdata[4];
        
        //第一次筛选,判断anchor内是否包含物体
        if (boxScores > yolov7Parameter.objectThreshold)
        {
            //获取每个anchor内部预测的80个类别概率信息
            cv::Mat scores = outs[0].row(rowInd).colRange(5, numOut);
            cv::Point classIdPoint;
            double maxClassScore;
            
            //获取80个类别中最大概率值和对应的类别ID
            cv::minMaxLoc(scores, 0, &maxClassScore, 0, &classIdPoint);
            maxClassScore *= boxScores;
            
            //第二次筛选,判断当前anchor的最大置信度得分是否满足阈值
            if (maxClassScore > yolov7Parameter.confidenceThreshold)
            {
                const int classIdx = classIdPoint.x;
                
                //将每个anchor坐标按缩放比例映射到原图
                float cx = pdata[0] * ratiow;
                float cy = pdata[1] * ratioh;
                float w = pdata[2] * ratiow;
                float h = pdata[3] * ratioh;
                //获取anchor的左上角坐标
                int left = int(cx - 0.5 * w);
                int top = int(cy - 0.5 * h);

                confidences.push_back((float)maxClassScore);
                boxes.push_back(cv::Rect(left, top, (int)(w), (int)(h)));
                classIds.push_back(classIdx);
            }
        }
        rowInd++;
        pdata += numOut;
    }

    ...

}
```

为了消除重叠锚框,输出最终的YOLOV7目标检测结果,执行非极大值抑制对筛选之后的anchor进行处理,最后保存检测结果到resultsOfDetection中。

```
ErrorCode DetectorYOLOV7::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{
	...

    //执行non maximum suppression消除冗余重叠boxes
    std::vector<int> indices;
    dnn::NMSBoxes(boxes, confidences, yolov7Parameter.confidenceThreshold, yolov7Parameter.nmsThreshold, indices);
    for (size_t i = 0; i < indices.size(); ++i)
    {
        int idx = indices[i];
        int classID=classIds[idx];
        string className=classNames[classID];
        float confidence=confidences[idx];
        cv::Rect box = boxes[idx];
		
        //保存每个最终预测anchor的坐标值、置信度分数、类别ID
        ResultOfDetection result;
        result.boundingBox=box;
        result.confidence=confidence;// confidence
        result.classID=classID; // label
        result.className=className;
        resultsOfDetection.push_back(result);
    }
    
    ...
    
}
```

### 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```
./MIGraphX_Samples 6
```

会在当前目录生成检测结果图像Result.jpg。

<img src="Images/YOLOV7_03.jpg" alt="YOLOV7_03" style="zoom:67%;" />

<div STYLE="page-break-after: always;"></div>

## 图像分割

本示例采用了经典的Unet模型进行图像分割,模型下载地址:https://www.dropbox.com/s/3ntkhyk30x05uuv/unet_13_256.onnx,将unet_13_256.onnx文件保存在Resource\Models\Segmentation文件夹下。模型结构如下图所示(可以通过netron工具(https://netron.app/)查看具体的模型结构),该模型的输入shape为[batch_size,3,256,256],输出shape为[batch_size,1,256,256],数据排布为NCHW。

![Unet_Image_1](Images/Unet_Image_1.png)

### 参数设置

samples工程中的Resource/Configuration.xml文件的Unet节点表示图像分割模型Unet的参数,主要包括模型存放路径。

```xml
<!--Unet-->
<Unet>
	<ModelPath>"../Resource/Models/Segmentation/unet_13_256.onnx"</ModelPath>
</Unet>
```

### 模型初始化

首先,通过parse_onnx()函数加载图像分割Unet的onnx模型,并可以通过program的get_parameter_shapes()函数获取网络的输入属性。完成模型加载之后需要使用compile()方法编译模型,编译模式使用migraphx::gpu::target{}设为GPU模式,编译过程主要基于MIGraphX IR完成各种优化。

```C++
ErrorCode Unet::Initialize(InitializationParameterOfSegmentation initParamOfSegmentationUnet)
{
    
    ...

    // 加载模型
    net = migraphx::parse_onnx(modelPath);       // 根据提供的模型地址,去加载模型文件
    LOG_INFO(logFile,"succeed to load model: %s\n",GetFileName(modelPath).c_str());

    // 获取模型输入属性
    std::pair<std::string, migraphx::shape> inputAttribute=*(net.get_parameter_shapes().begin());
    inputName=inputAttribute.first;
    inputShape=inputAttribute.second;
    inputSize=cv::Size(inputShape.lens()[3],inputShape.lens()[2]);

    // 设置模型为GPU模式
    migraphx::target gpuTarget = migraphx::gpu::target{};

    // 编译模型
    migraphx::compile_options options;
    options.device_id=0;                          // 设置GPU设备,默认为0号设备(>=1.2版本中支持)
    options.offload_copy=true;                    // 设置offload_copy
    net.compile(gpuTarget,options);               
    LOG_INFO(logFile,"succeed to compile model: %s\n",GetFileName(modelPath).c_str());

    ...
    
}
```

### 预处理

完成模型初始化之后,需要将输入数据进行如下预处理:

​        1.尺度变换,将图像resize到256x256大小

​        2.归一化,将数据归一化到[0.0, 1.0]之间

​        3.数据排布,将数据从HWC转换为NCHW

本示例代码主要通过opencv实现预处理操作:

```C++
ErrorCode Unet::Segmentation(const cv::Mat &srcImage, cv::Mat &maskImage)
{
    
    ...
        
    // 图像预处理,将图像resize到(256x256)大小,并转换为NCHW
    cv::Mat inputBlob;
    cv::dnn::blobFromImage(srcImage,    // 输入数据,支持多张图像
                    inputBlob,          // 输出数据
                    1 / 255.0,          // 缩放系数
                    inputSize,          // 模型输入大小,这里为256x256
                    Scalar(0, 0, 0),    // 均值,这里不需要减均值,所以设置为0
                    true,               // 通道转换,B通道与R通道互换,所以为true
                    false);
    ...
        
}
```

1.cv::dnn::blobFromImage()函数支持多个输入图像,首先将输入图像resize到inputSize大小,然后减去均值,其次乘以缩放系数1/255.0并转换为NCHW,最终将转换好的数据保存到inputBlob作为输入数据执行推理。

### 推理

完成图像预处理后,就可以执行推理,得到推理结果。

```c++
ErrorCode Unet::Segmentation(const cv::Mat &srcImage, cv::Mat &maskImage)
{
    ...
        
    // 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> results = net.eval(inputData);

    // 获取输出节点的属性
    migraphx::argument result = results[0];                 // 获取第一个输出节点的数据
    migraphx::shape outputShape=result.get_shape();         // 输出节点的shape
    std::vector<std::size_t> outputSize=outputShape.lens(); // 每一维大小,维度顺序为(N,C,H,W) 
    int numberOfOutput=outputShape.elements();              // 输出节点元素的个数
    float *data = (float *)result.data();                   // 输出节点数据指针

    // 计算sigmoid值,并且当大于0.996时,值为1,当小于0.996时,值为0,存储在value_mask[]数组中
    int value_mask[numberOfOutput];
    for(int i=0; i<numberOfOutput; ++i)
    {
        float num  = Sigmoid(data[i]);
        if (num > 0.996)
        {
            value_mask[i] = 1;
        }
        else
        {
            value_mask[i] = 0;
        }
    }

    // 将对应的value_mask[]数组中的值依次赋值到outputImage对应位置处
    cv::Mat outputImage = cv::Mat_<int>(Size(outputShape.lens()[3], outputShape.lens()[2]), CV_32S);
    for(int i=0;i<outputShape.lens()[2];++i)
    {
        for(int j=0;j<outputShape.lens()[3];++j){
            outputImage.at<int>(i,j)=value_mask[256*i+j];  // 其中,256代表了outputShape.lens()[3]的值
        }
    }
    // 将32S格式的数据转换为8U格式的数据
    outputImage.convertTo(maskImage, CV_8U, 255.0);
    ...
}
```

1.inputData表示MIGraphX的输入数据,这里表示为图像数据,inputName表示输入节点名,migraphx::argument{inputShape, (float*)inputBlob.data}表示该节点名对应的数据,这里是通过前面预处理的数据inputBlob来创建的,第一个参数表示数据的shape,第二个参数表示数据指针。

2.net.eval(inputData)返回模型的推理结果,由于这里只有一个输出节点,所以std::vector中只有一个数据,results[0]表示第一个输出节点,获取输出数据之后,就可以对输出数据执行相关后处理操作。

3.模型得到的推理结果并不能直接作为分割结果。首先,对推理结果计算sigmoid值,当计算值大于0.996时值为1,小于等于0.996时值为0,保存在数组value_mask中。其次,创建一个cv::Mat将value_mask数组中的值按行依次赋值到对应的位置。最后,将32S格式的数据转换为8U格式的数据,对应位置乘以255,得到最终的分割图像。

注:本次采用的模型权重onnx文件是通过使用具有普通背景的汽车图像来训练的。因此,“现实世界“图像的分割结果不完美是意料之中的。为了获得更好的结果,建议对现实世界示例数据集上的模型进行微调。

### 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```c++
./ MIGraphX_Samples 8
```

会在当前目录中生成分割结果图像Result.jpg

输出结果为:

![Unet_output_python](Images/Unet_output_c++.jpg)



<div STYLE="page-break-after: always;"></div>

## 超分辨率重建

### 模型简介

图像超分辨率重建技术是指设计并采用某种算法,使得可以通过观测到的低分辨率(Low Resolution, LR)图像重建出近似真实高分辨率(High Resolution , HR)图像的方法。本次部署的超分辨率模型是2016提出的ESPCN网络,主要通过一系列卷积层不断提取图像特征,最后通过sub-pixel亚像素卷积层提高图像分辨率,从而实现图像超分辨率的方法。ESPCN网络结构如下图所示
![Superresolution_01](Images/Superresolution_01.png)

本示例采用onnx官方提供的ESPCN模型示例:https://github.com/onnx/models/tree/main/vision/super_resolution/sub_pixel_cnn_2016,将super.onnx文件保存在Resource\Models\Super_Resolution文件夹下。

### 参数设置

samples工程中的Resource/Configuration.xml文件的Espcn节点表示超分辨率模型Espcn的参数,主要包括模型存放路径。

```xml
<!--超分辨率重建-->
<Espcn>
	<ModelPath>"../Resource/Models/Super_Resolution/super.onnx"</ModelPath>
</Espcn>
```

### 模型初始化

首先,通过parse_onnx()函数加载图像超分辨率ESPCN的onnx模型,并可以通过program的get_parameter_shapes()函数获取网络的输入属性。完成模型加载之后需要使用compile()方法编译模型,编译模式使用migraphx::gpu::target{}设为GPU模式,编译过程主要基于MIGraphX IR完成各种优化。

```C++
ErrorCode Espcn::Initialize(InitializationParameterOfSuperresolution initParamOfSuperresolutionESPCN)
{
    
    ...  
        
    // 加载模型
    net = migraphx::parse_onnx(modelPath);       // 根据提供的模型地址,去加载模型文件
    LOG_INFO(logFile,"succeed to load model: %s\n",GetFileName(modelPath).c_str());

    // 获取模型输入属性
    std::pair<std::string, migraphx::shape> inputAttribute=*(net.get_parameter_shapes().begin());
    inputName=inputAttribute.first;
    inputShape=inputAttribute.second;
    inputSize=cv::Size(inputShape.lens()[3],inputShape.lens()[2]);     

    // 设置模型为GPU模式
    migraphx::target gpuTarget = migraphx::gpu::target{};

    // 编译模型
    migraphx::compile_options options;
    options.device_id=0;                          // 设置GPU设备,默认为0号设备(>=1.2版本中支持)
    options.offload_copy=true;                    // 设置offload_copy
    net.compile(gpuTarget,options);               
    LOG_INFO(logFile,"succeed to compile model: %s\n",GetFileName(modelPath).c_str());
    
    ...
        
}
```

### 预处理

完成模型初始化之后,需要将输入数据进行如下预处理:

​      1.分离图像通道,变为B、G、R三个通道图像;

​      2.BGR格式通过公式转换为YCbCr格式,得到Y、Cb、Cr三通道图像;

​      3.Y通道图像resize到224x224,变为浮点型数据,并转换数据排布为NCHW;

本示例代码主要采用如下方式完成预处理操作:

```c++
ErrorCode Espcn::Super(const cv::Mat &srcImage, cv::Mat &superImage)
{
    ...
        
    // 图像预处理(分离图像通道,只处理Y通道的图像,进行模型推理)
    // 分离图像通道
    std::vector<cv::Mat> channels;
    cv::split(srcImage, channels);     // B通道=channels[0],G通道=channels[1],R通道=channels[2]

    // 将BGR格式转换为YCbCr格式,采用opencv官方转换公式
    cv::Mat Y_channels = 0.299*channels[2] + 0.587*channels[1] + 0.114*channels[0];
    cv::Mat Cb_channels = (channels[0]-Y_channels) * 0.564 + 128;
    cv::Mat Cr_channels = (channels[2]-Y_channels) * 0.713 + 128;

    // 将Y通道图像resize为224x224,变为浮点型数据,并转换数据排布为NCHW
    cv::Mat inputBlob;
    cv::dnn::blobFromImage(Y_channels,    // 输入数据,支持多张图像
                    inputBlob,            // 输出数据
                    1/255.0,              // 缩放系数
                    inputSize,            // 模型输入大小,这里为672x672
                    Scalar(0, 0, 0),      // 均值,这里不需要减均值,所以设置为0
                    false,                // 通道转换,因为只处理Y通道数据,不需要通道转换,所以设置为false
                    false);
    
     ...
         
}
```

1.在预处理过程中,重要的是BGR格式转换为YCbCr格式,主要是因为相较于色差,人类视觉对亮度变化更为敏感。因此,在处理的过程中感兴趣的不是颜色变化(存储在 CbCr 通道中的信息),而只是其亮度(Y 通道),所以需要将BGR转换为YCbCr,转换公式采用的是opencv官方提供的公式,链接:https://docs.opencv.org/3.4.11/de/d25/imgproc_color_conversions.html。

2.得到Y通道图像后,需要resize为固定尺寸并转换为NCHW,主要采用cv::dnn::blobFromImage()函数。首先将输入图像resize到inputSize,然后减去均值,其次乘以缩放系数1/255.0并转换为NCHW,最终将转换好的数据保存到inputBlob作为输入数据执行推理。

### 推理

完成图像预处理后,就可以执行推理,得到推理结果。

```C++
ErrorCode Espcn::Super(const cv::Mat &srcImage, cv::Mat &superImage)
{
    
    ...
        
    // 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> results = net.eval(inputData);

    // 获取输出节点的属性
    migraphx::argument result = results[0];                 // 获取第一个输出节点的数据
    migraphx::shape outputShape=result.get_shape();         // 输出节点的shape
    std::vector<std::size_t> outputSize=outputShape.lens(); // 每一维大小,维度顺序为(N,C,H,W) 
    int numberOfOutput=outputShape.elements();              // 输出节点元素的个数
    float *data = (float *)result.data();                   // 输出节点数据指针

    // 获取推理结果,是重建后每个像素点的像素值,创建一个cv::Mat,依次在对应位置处赋予像素值。
    cv::Mat output_Y = cv::Mat_<float>(Size(outputShape.lens()[3], outputShape.lens()[2]), CV_32F);   //规定尺寸大小
    for(int i=0;i<outputShape.lens()[2];++i)
    {
        for(int j=0;j<outputShape.lens()[3];++j){
            output_Y.at<float>(i,j)=data[672*i+j];   // 其中,672代表了outputShape.lens()[3]的值
        }
    }

    // 将32F格式的数据变为8U格式的数据
    cv::Mat output_Y8;
    output_Y.convertTo(output_Y8, CV_8U, 255.0);      // 转换为无符号8位像素数据类型,0-255是大多数图像和视频格式的正常范围

    ...
    
    // 将YCbCr转换为BGR
    cv::Mat B_output = output_Y8 + 1.773 * (Cb_output - 128);
    cv::Mat G_output = output_Y8 - 0.714 * (Cr_output - 128) - 0.344 * (Cb_output - 128);
    cv::Mat R_output = output_Y8 + 1.403 * (Cr_output - 128);

    // 将BGR三通道图像合并到一起
    std::vector<cv::Mat> channels2;
    channels2.push_back(B_output);
    channels2.push_back(G_output);
    channels2.push_back(R_output);
    cv::merge(channels2, superImage);
    
    return SUCCESS;
}
```

1.inputData表示MIGraphX的输入数据,这里表示为Y通道图像数据,inputName表示输入节点名,migraphx::argument{inputShape, (float*)inputBlob.data}表示该节点名对应的数据,这里通过前面的预处理数据inputBlob来创建的,第一个参数表示数据的shape,第二个参数表示数据指针。

2.net.eval(inputData)返回模型的推理结果,由于这里只有一个输出节点,所以std::vector中只有一个数据,results[0]表示第一个输出节点,获取输出数据之后,就可以对输出数据执行相关后处理操作。

3.推理结果为重建后的图像像素点值,创建一个cv::Mat,尺寸为672x672,通过output_Y.at<float>(i,j)=data[672*i+j]按行赋相应像素值。最后,将YCbCr转换为BGR,并将三通道合并到一起,得到最终的重建图像。

### 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```Python
./ MIGraphX_Samples 7
```

会在当前目录中生成检测结果图像Result.jpg。
结果如下,左图为原图,有图为重建过后的图像,分辨率提高了三倍。
![Superresolutin_02](Images/Superresolution_02.png)



<div STYLE="page-break-after: always;"></div>


# Python Samples



## 分类器
本示例使用了经典的mnist模型,模型下载地址:https://github.com/onnx/models/blob/main/vision/classification/mnist/model/mnist-12.onnx,模型结构如下图所示(可以通过netron (https://netron.app/) 查看),该模型的输入shape为[1,1,28,28] ,数据排布为NCHW,输出是10个类别的概率(未归一化)。

![image-20221212165226581](Images/Classifier_01.png)



### 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

1. 转换为单通道灰度图
2. resize到28x28
3. 将像素值归一化到[0.0, 1.0]
4. 转换数据排布为NCHW



本示例代码采用了OpenCV实现了预处理操作:

```
def Preprocessing(pathOfImage):
    gray = cv2.imread(pathOfImage,cv2.IMREAD_GRAYSCALE)
    gray = cv2.resize(gray, (28,28)).astype(np.float32)/255
    input = np.reshape(gray, (1,1,28,28))
    return input
```



### 推理

完成预处理后,就可以执行推理了:

```
if __name__ == '__main__':
    ...
    
    # 预处理
    pathOfImage ="../../Resource/Images/9.jpg"
    image = Preprocessing(pathOfImage)

    # 推理
    results = model.run({inputName: migraphx.argument(image)}) # 推理结果,list类型

    # 获取输出节点属性
    result=results[0] # 获取第一个输出节点的数据,migraphx.argument类型
    outputShape=result.get_shape() # 输出节点的shape,migraphx.shape类型
    outputSize=outputShape.lens() # 每一维大小,维度顺序为(N,C,H,W),list类型
    numberOfOutput=outputShape.elements() # 输出节点元素的个数

    # 计算softmax
    result=results[0].tolist() # 将migraphx.argument转换为list
    result=np.array(result)
    scores=Softmax(result)

    # 打印10个类别的概率
    print(scores)
```

1. Preprocessing()函数返回输入数据(numpy类型),然后通过{inputName: migraphx.argument(image)}构造一个字典输入模型执行推理,如果模型有多个输入,则在字典中需要添加多个输入数据。
2. model.run()返回模型的推理结果,返回结果是一个list类型,results[0]表示第一个输出节点的输出,是一个migraphx.argument类型,由于示例模型只有一个输出节点,所以results[0]对应Plus214_Output_0节点,如果想将migraphx.argument类型转换为list类型,可以通过tolist()方法实现。
3. 由于该模型输出的是一个未归一化的概率,所以如果需要得到每一类的实际的概率值,还需要计算softmax。



### 运行示例



1. 参考《MIGraphX教程》中的安装方法安装MIGraphX并设置好PYTHONPATH
2. 安装依赖:

```
# 进入migraphx samples工程根目录
cd <path_to_migraphx_samples> 

# 进入示例程序目录
cd Python/Classifier

# 安装依赖
pip install -r requirements.txt
```

3. 在Python/Classifier目录下执行如下命令运行该示例程序:

```
# 运行示例
python Classifier.py
```

输出结果为:

```
...
[1.59622257e-07 3.41601745e-05 1.17416399e-05 1.69055674e-04
 4.37055434e-05 1.19463621e-06 5.51138837e-11 7.25694010e-04
 2.78295036e-04 9.98735994e-01]
```

输出结果中,每个值分别对应每个label的实际概率,比如1.59622257e-07表示label为0的概率,由于示例图像为数字9,所以结果中label为9的概率最高。

![image-20221212173655309](Images/Classifier_02.png)



<div STYLE="page-break-after: always;"></div>

## RetinaFace人脸检测器

### 模型简介

RetinaFace是一个经典的人脸检测模型(https://arxiv.org/abs/1905.00641),采用了SSD架构。

![image-20221215140647406](Images/RetinaFace_01.png)

本示例采用了如下的开源实现:https://github.com/biubug6/Pytorch_Retinaface,作者提供了restnet50 和mobilenet0.25两个预训练模型,本示例使用了mobilenet0.25预训练模型,将mobilenet0.25预训练模型下载下来后,保存到Pytorch_Retinaface工程的weights目录。

### 模型转换

通过下面的步骤可以将mobilenet0.25预训练转换成onnx文件:

1. 修改data/config.py:将cfg_mnet中的'pretrain': True,修改为'pretrain': False,

2. 执行如下命令就可以将weights目录下的mobilenet0.25_Final.pth模型转换为onnx文件了

   ```
   # 进入Pytorch_Retinaface工程根目录
   cd <path_to_Pytorch_Retinaface>
   
   # 转换模型
   python convert_to_onnx.py
   ```

   注意:如果需要修改模型的输入大小,可以修改args.long_side参数,默认为640x640。

   模型转换成功后,会在当前目录生成FaceDetector.onnx文件,利用该模型就可以使用MIGraphX进行推理了,本示例在samples工程中的Python/RetinaFace目录中提供了已经修改好的代码,在该目录下执行python convert_to_onnx.py可以直接生成onnx文件。

### 推理

Pytorch_Retinaface工程提供了原始Pytorch版本的推理测试代码detect.py,我们只需要将其中使用Pytorch推理的部分转换为MIGraphX推理就可以了,samples工程中的Python/RetinaFace/detect.py文件为已经转换好的推理代码,下面我们看一下是如何转换的:

1. 将加载模型部分修改为migraphx的方式加载

   ```
   # 加载模型
   model = migraphx.parse_onnx("./FaceDetector.onnx")
   ```

2. 模型加载成功后,需要通过model.compile进行编译,可以通过device_id设置使用哪一块设备

   ```
   model.compile(t=migraphx.get_target("gpu"),device_id=0)
   ```

3. 编译成功后,就可以输入图像进行推理了,由于本示例使用的onnx模型的输入大小是640x640,所以对于输入图像需要先resize到640x640

   ```
   # resize到onnx模型输入大小
   image_path = "./curve/test.jpg"
   img_raw = cv2.imread(image_path, cv2.IMREAD_COLOR)
   img_raw = cv2.resize(img_raw, (640,640))
   ```

4. 预处理部分跟作者的代码保持一致即可,这部分不需要修改

5. 下面是最关键的一步,将pytorch推理net(img)转换为MIGraphX推理migraphx_run(model,args.cpu,img),其中migraphx_run实现如下:

   ```
   def migraphx_run(model,cpu,data_tensor):
       # 将输入的tensor数据转换为numpy
       if cpu:
           data_numpy=data_tensor.cpu().numpy()
           device = torch.device("cpu")
       else:
           data_numpy=data_tensor.detach().cpu().numpy()
           device = torch.device("cuda")
       
       img_data = np.zeros(data_numpy.shape).astype("float32")
       for i in range(data_numpy.shape[0]):
           img_data[i, :, :, :] = data_numpy[i, :, :, :]
   
       # 执行推理
       result = model.run({model.get_parameter_names()[0]: migraphx.argument(img_data)})
   
       # 将结果转换为tensor
       result0=torch.from_numpy(np.array(result[0], copy=False)).to(device)
       result1=torch.from_numpy(np.array(result[1], copy=False)).to(device)
       result2=torch.from_numpy(np.array(result[2], copy=False)).to(device)
   
       return (result0,result1,result2)
   ```

   首先需要将tensor数据转换为numpy,转换好的数据保存在img_data中,然后通过{model.get_parameter_names()[0]: migraphx.argument(img_data)}创建MIGraphX的输入数据,并使用model.run执行推理,result为推理返回的结果,然后通过torch.from_numpy的方式转换为tensor类型并返回,为了保持与Pytorch推理结果的格式一致,转换的时候需要注意输出结果的顺序,MIGraphX的输出结果顺序与onnx中保持一致,可以通过netron (https://netron.app/) 查看:

   ![image-20221215200807445](Images/RetinaFace_04.png)

   所以第一个输出结果对应pytorch结果中的loc,第二个对应conf, 第三个对应landms,所以返回的结果是(result0,result1,result2)。

6. 推理执行成功后,需要执行后处理才能得到最终的检测结果,由于我们模型推理输出的格式与原始的Pytorch模型输出是一致的,所以后处理可以直接使用原来的,不需要修改。

### 运行示例

1. 参考《MIGraphX教程》中的安装方法安装MIGraphX并设置好PYTHONPATH
2. 安装Pytorch
3. 安装依赖:

```
# 进入migraphx samples工程根目录
cd <path_to_migraphx_samples> 

# 进入示例程序目录
cd Python/RetinaFace

# 安装依赖
pip install -r requirements.txt
```

4. 运行示例:

```
python detect.py
```

会在当前目录生成检测结果图像test.jpg
![image-20221215202711987](Images/RetinaFace_05.png)



<div STYLE="page-break-after: always;"></div>

## YOLOV3检测器

### 模型简介

YOLOV3是由Joseph Redmon和Ali Farhadi在《YOLOv3: An Incremental Improvement》论文中提出的单阶段检测模型,算法基本思想首先通过特征提取网络对输入提取特征,backbone部分由YOLOV2时期的Darknet19进化至Darknet53加深了网络层数,引入了Resnet中的跨层加和操作;然后结合不同卷积层的特征实现多尺度训练,一共有13x13、26x26、52x52三种分辨率,分别用来预测大、中、小的物体;每种分辨率的特征图将输入图像分成不同数量的格子,每个格子预测B个bounding box,每个bounding box预测内容包括: Location(x, y, w, h)、Confidence Score和C个类别的概率,因此YOLOv3输出层的channel数为B*(5 + C)。YOLOv3的loss函数也有三部分组成:Location误差,Confidence误差和分类误差。

<img src="Images/YOLOV3_02.jpg" alt="YOLOV3_02"  />

本示例采用如下的开源实现:https://github.com/ultralytics/yolov3,作者在V9.6.0版本中提供多种不同的YOLOV3预训练模型,其中包括yolov3、yolov3-fixed、yolov3-spp、yolov3-tiny四个版本。本示例选择yolov3-tiny.pt预训练模型进行构建MIGraphX推理,下载YOLOV3的预训练模型yolov3-tiny.pt保存在Pytorch_YOLOV3工程的weights目录。

### 环境配置

运行YOLOV3模型的Python示例首先需要进行环境配置,包括安装torch、torchvision以及程序运行所需要的依赖。

1、安装torch、torchvision

2、安装程序运行的依赖

```
# 进入Pytorch_YOLOV3工程根目录
cd <path_to_Pytorch_YOLOV3>

# 安装程序运行的依赖
pip install -r requirement.txt
```

### 模型转换

官方提供的YOLOV3源码中包含导出onnx模型的程序,通过下面的步骤可以将yolov3-tiny.pt预训练模型转换成onnx格式:

```
# 进入Pytorch_YOLOV3工程根目录
cd <path_to_Pytorch_YOLOV3>

# 转换模型
python export.py --weights ./weights/yolov3-tiny.pt --imgsz 416 416 --include onnx
```

注意:官方源码提供的模型转换的程序中包含更多的功能,例如动态shape模型的导出,可根据需要进行添加相关参数。

### 模型推理

#### 预处理

待检测图片输入模型进行检测之前需要进行预处理,主要包括调整输入的尺寸,归一化等操作。

1. 转换数据排布为NCHW
2. 归一化[0.0, 1.0]
3. 调整输入数据的尺寸为(1,3,416,416)

```
def prepare_input(self, image):
    self.img_height, self.img_width = image.shape[:2]
    input_img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    # 调整图像的尺寸
    input_img = cv2.resize(input_img, (self.inputWidth, self.inputHeight))
    # 维度转换HWC->CHW
    input_img = input_img.transpose(2, 0, 1)
    # 维度拓展,增加batch维度
    input_img = np.expand_dims(input_img, 0)
    input_img = np.ascontiguousarray(input_img)
    input_img = input_img.astype(np.float32)
    # 归一化
    input_img = input_img / 255

    return input_img
```

其中模型输入的inputWidth、inputHeight通过migraphx对输入模型进行解析获取,代码位置见YOLOV3类初始化位置。

```
class YOLOv3:
    def __init__(self, path, obj_thres=0.5, conf_thres=0.25, iou_thres=0.5):
        self.objectThreshold = obj_thres
        self.confThreshold = conf_thres
        self.nmsThreshold = iou_thres

        # 获取模型检测的类别信息
        self.classNames = list(map(lambda x: x.strip(), open('./weights/coco.names', 'r').readlines()))

        # 解析推理模型
        self.model = migraphx.parse_onnx(path)

        # 获取模型的输入name
        self.inputName = self.model.get_parameter_names()[0]

        # 获取模型的输入尺寸
        inputShape = self.model.get_parameter_shapes()[self.inputName].lens()
        self.inputHeight = int(inputShape[2])
        self.inputWidth = int(inputShape[3])
```

#### 推理

输入图片预处理完成之后开始进行推理,首先需要利用migraphx进行编译,然后对输入数据进行前向计算得到模型的输出result,在detect函数中调用定义的process_output函数对result进行后处理,得到图片中含有物体的anchor坐标信息、类别置信度、类别ID。

```
def detect(self, image):
    # 输入图片预处理
    input_img = self.prepare_input(image)

    # 模型编译
    self.model.compile(t=migraphx.get_target("gpu"), device_id=0)  # device_id: 设置GPU设备,默认为0号设备
    print("Success to compile")
    # 执行推理
    print("Start to inference")
    start = time.time()
    result = self.model.run({self.model.get_parameter_names()[0]: migraphx.argument(input_img)})
    print('net forward time: {:.4f}'.format(time.time() - start))
    # 模型输出结果后处理
    boxes, scores, class_ids = self.process_output(result)

    return boxes, scores, class_ids
```

其中对migraphx推理输出result进行后处理,首先需要对生成的anchor根据是否有物体阈值objectThreshold、置信度阈值confThreshold进行筛选,相关过程定义在process_output函数中。获取筛选后的anchor的坐标信息之后,需要将坐标映射到原图中的位置,相关过程定义在rescale_boxes函数中。

```
def process_output(self, output):
    predictions = np.squeeze(output[0])

    # 筛选包含物体的anchor
    obj_conf = predictions[:, 4]
    predictions = predictions[obj_conf > self.objectThreshold]
    obj_conf = obj_conf[obj_conf > self.objectThreshold]

    # 筛选大于置信度阈值的anchor
    predictions[:, 5:] *= obj_conf[:, np.newaxis]
    scores = np.max(predictions[:, 5:], axis=1)
    valid_scores = scores > self.confThreshold
    predictions = predictions[valid_scores]
    scores = scores[valid_scores]

    # 获取最高置信度分数对应的类别ID
    class_ids = np.argmax(predictions[:, 5:], axis=1)

    # 获取每个物体对应的anchor
    boxes = self.extract_boxes(predictions)

    # 执行非极大值抑制消除冗余anchor
    indices = cv2.dnn.NMSBoxes(boxes.tolist(), scores.tolist(), self.confThreshold, self.nmsThreshold).flatten()

    return boxes[indices], scores[indices], class_ids[indices]

def rescale_boxes(self, boxes):
    # 对anchor尺寸进行变换
    input_shape = np.array([self.inputWidth, self.inputHeight, self.inputWidth, self.inputHeight])
    boxes = np.divide(boxes, input_shape, dtype=np.float32)
    boxes *= np.array([self.img_width, self.img_height, self.img_width, self.img_height])
    return boxes
```

根据获取的detect函数输出的boxes、scores、class_ids信息在原图进行结果可视化,包括用绘制图片中检测到的物体位置、类别和置信度分数,得到最终的YOLOV3目标检测结果输出。

```
def draw_detections(self, image, boxes, scores, class_ids):
    for box, score, class_id in zip(boxes, scores, class_ids):
        cx, cy, w, h = box.astype(int)

        # 绘制检测物体框
        cv2.rectangle(image, (cx, cy), (cx + w, cy + h), (0, 255, 255), thickness=2)
        label = self.classNames[class_id]
        label = f'{label} {int(score * 100)}%'
        labelSize, baseLine = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
        cv2.putText(image, label, (cx, cy - 10), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), thickness=2)
    return image
```

### 运行示例

1.参考《MIGraphX教程》中的安装方法安装MIGraphX并设置好PYTHONPATH

2.运行示例

```
# 进入migraphx samples工程根目录
cd <path_to_migraphx_samples> 

#进入示例程序目录
cd Python/Detector/YOLOV3

# 运行示例
python detect_migraphx.py --imgpath ./data/images/dog.jpg --modelpath ./weights/yolov3-tiny.onnx --objectThreshold 0.4 --confThreshold 0.2 --nmsThreshold 0.4
```

输入参数中可根据需要进行修改,程序运行结束会在当前目录生成YOLOV3检测结果图片。

![YOLOV3_03](Images/YOLOV3_03.jpg)



<div STYLE="page-break-after: always;"></div>

## YOLOV5检测器

### 模型简介

YOLOV5是一种单阶段目标检测算法,该算法在YOLOV4的基础上添加了一些新的改进思路,使其速度与精度都得到了极大的性能提升。具体包括:输入端的Mosaic数据增强、自适应锚框计算、自适应图片缩放操作;主干网络的Focus结构与CSP结构;Neck端的FPN+PAN结构;输出端的损失函数GIOU_Loss以及预测框筛选的DIOU_nms。网络结构如图所示。

<img src="Images/YOLOV5_01.jpg" alt="YOLOV5_01" style="zoom: 67%;" />

YOLOV5的官方源码地址:https://github.com/ultralytics/yolov5,官方源码中具有YOLOV5n、YOLOV5s、YOLOV5m、YOLOV5l等不同的版本。本示例采用YOLOV5s版本进行MIGraphX推理示例构建,下载YOLOV5s的预训练模型yolov5s.pt保存在Pytorch_YOLOV5工程的weights目录。

### 环境配置

运行YOLOV5模型的Python示例首先需要进行环境配置,包括安装torch、torchvision以及程序运行所需要的依赖。

1、安装torch、torchvision

2、安装程序运行的依赖

```
# 进入Pytorch_YOLOV5工程根目录
cd <path_to_Pytorch_YOLOV5>

# 安装程序运行的依赖
pip install -r requirement.txt
```

### 模型转换

官方提供的YOLOV5源码中包含导出onnx模型的程序,通过下面的步骤可以将yolov5s.pt预训练模型转换成onnx格式:

```
# 进入Pytorch_YOLOV5工程根目录
cd <path_to_Pytorch_YOLOV5>

# 转换模型
python export.py --weights ./weights/yolov5s.pt --imgsz 608 608 --include onnx
```

注意:官方源码提供的模型转换的程序中包含更多的功能,例如动态shape模型的导出,可根据需要进行添加相关参数。

### 模型推理

#### 预处理

待检测图片输入模型进行检测之前需要进行预处理,主要包括调整输入的尺寸,归一化等操作。

1. 转换数据排布为NCHW
2. 归一化[0.0, 1.0]
3. 调整输入数据的尺寸为(1,3,608,608)

```
def prepare_input(self, image):
    self.img_height, self.img_width = image.shape[:2]
    input_img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    # 调整图像的尺寸
    input_img = cv2.resize(input_img, (self.inputWidth, self.inputHeight))
    # 维度转换HWC->CHW
    input_img = input_img.transpose(2, 0, 1)
    # 维度拓展,增加batch维度
    input_img = np.expand_dims(input_img, 0)
    input_img = np.ascontiguousarray(input_img)
    input_img = input_img.astype(np.float32)
    # 归一化
    input_img = input_img / 255

    return input_img
```

其中模型输入的inputWidth、inputHeight通过migraphx对输入模型进行解析获取,代码位置见YOLOV5类初始化位置。

```
class YOLOv5:
    def __init__(self, path, obj_thres=0.5, conf_thres=0.25, iou_thres=0.5):
        self.objectThreshold = obj_thres
        self.confThreshold = conf_thres
        self.nmsThreshold = iou_thres

        # 获取模型检测的类别信息
        self.classNames = list(map(lambda x: x.strip(), open('./weights/coco.names', 'r').readlines()))

        # 解析推理模型
        self.model = migraphx.parse_onnx(path)

        # 获取模型的输入name
        self.inputName = self.model.get_parameter_names()[0]

        # 获取模型的输入尺寸
        inputShape = self.model.get_parameter_shapes()[self.inputName].lens()
        self.inputHeight = int(inputShape[2])
        self.inputWidth = int(inputShape[3])
```

#### 推理

输入图片预处理完成之后开始进行推理,首先需要利用migraphx进行编译,然后对输入数据进行前向计算得到模型的输出result,在detect函数中调用定义的process_output函数对result进行后处理,得到图片中含有物体的anchor坐标信息、类别置信度、类别ID。

```
def detect(self, image):
    # 输入图片预处理
    input_img = self.prepare_input(image)

    # 模型编译
    self.model.compile(t=migraphx.get_target("gpu"), device_id=0)  # device_id: 设置GPU设备,默认为0号设备
    print("Success to compile")
    # 执行推理
    print("Start to inference")
    start = time.time()
    result = self.model.run({self.model.get_parameter_names()[0]: migraphx.argument(input_img)})
    print('net forward time: {:.4f}'.format(time.time() - start))
    # 模型输出结果后处理
    boxes, scores, class_ids = self.process_output(result)

    return boxes, scores, class_ids
```

其中对migraphx推理输出result进行后处理,首先需要对生成的anchor根据是否有物体阈值objectThreshold、置信度阈值confThreshold进行筛选,相关过程定义在process_output函数中。获取筛选后的anchor的坐标信息之后,需要将坐标映射到原图中的位置,相关过程定义在rescale_boxes函数中。

```
def process_output(self, output):
    predictions = np.squeeze(output[0])

    # 筛选包含物体的anchor
    obj_conf = predictions[:, 4]
    predictions = predictions[obj_conf > self.objectThreshold]
    obj_conf = obj_conf[obj_conf > self.objectThreshold]

    # 筛选大于置信度阈值的anchor
    predictions[:, 5:] *= obj_conf[:, np.newaxis]
    scores = np.max(predictions[:, 5:], axis=1)
    valid_scores = scores > self.confThreshold
    predictions = predictions[valid_scores]
    scores = scores[valid_scores]

    # 获取最高置信度分数对应的类别ID
    class_ids = np.argmax(predictions[:, 5:], axis=1)

    # 获取每个物体对应的anchor
    boxes = self.extract_boxes(predictions)

    # 执行非极大值抑制消除冗余anchor
    indices = cv2.dnn.NMSBoxes(boxes.tolist(), scores.tolist(), self.confThreshold, self.nmsThreshold).flatten()

    return boxes[indices], scores[indices], class_ids[indices]

def rescale_boxes(self, boxes):
    # 对anchor尺寸进行变换
    input_shape = np.array([self.inputWidth, self.inputHeight, self.inputWidth, self.inputHeight])
    boxes = np.divide(boxes, input_shape, dtype=np.float32)
    boxes *= np.array([self.img_width, self.img_height, self.img_width, self.img_height])
    return boxes
```

根据获取的detect函数输出的boxes、scores、class_ids信息在原图进行结果可视化,包括用绘制图片中检测到的物体位置、类别和置信度分数,得到最终的YOLOV5目标检测结果输出。

```
def draw_detections(self, image, boxes, scores, class_ids):
    for box, score, class_id in zip(boxes, scores, class_ids):
        cx, cy, w, h = box.astype(int)

        # 绘制检测物体框
        cv2.rectangle(image, (cx, cy), (cx + w, cy + h), (0, 255, 255), thickness=2)
        label = self.classNames[class_id]
        label = f'{label} {int(score * 100)}%'
        labelSize, baseLine = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
        cv2.putText(image, label, (cx, cy - 10), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), thickness=2)
    return image
```

### 运行示例

1.参考《MIGraphX教程》中的安装方法安装MIGraphX并设置好PYTHONPATH

2.运行示例

```
# 进入migraphx samples工程根目录
cd <path_to_migraphx_samples> 

#进入示例程序目录
cd Python/Detector/YOLOV5

# 运行示例
python detect_migraphx.py --imgpath ./data/images/bus.jpg --modelpath ./weights/yolov5s.onnx --objectThreshold 0.5 --confThreshold 0.25 --nmsThreshold 0.5
```

输入参数中可根据需要进行修改,程序运行结束会在当前目录生成YOLOV5检测结果图片。

<img src="Images/YOLOV5_03.jpg" alt="YOLOV5_02" style="zoom:67%;" />



<div STYLE="page-break-after: always;"></div>

## YOLOV7检测器

### 模型简介

YOLOV7是2022年最新出现的一种YOLO系列目标检测模型,在论文 [YOLOv7: Trainable bag-of-freebies sets new state-of-the-art for real-time object detectors](https://arxiv.org/abs/2207.02696)中提出。

<img src="Images/YOLOV7_02.png" alt="YOLOV7_02" style="zoom:67%;" />

本示例采用YOLOv7的官方源码:https://github.com/WongKinYiu/yolov7,作者提供了多个预训练模型,本示例使用yolov7-tiny.pt预训练模型,将yolov7-tiny.pt预训练模型下载下来后,保存到Pytorch_YOLOV7工程的weights目录。

### 环境配置

运行YOLOV7模型的Python示例首先需要进行环境配置,包括安装torch、torchvision以及程序运行所需要的依赖。

1、安装torch、torchvision

2、安装程序运行的依赖

```
# 进入Pytorch_YOLOV7工程根目录
cd <path_to_Pytorch_YOLOV7>

# 安装程序运行的依赖
pip install -r requirement.txt
```

### 模型转换

通过下面的步骤可以将yolov7-tiny.pt预训练模型转换成onnx格式:

```
# 进入Pytorch_YOLOV7工程根目录
cd <path_to_Pytorch_YOLOV7>

#转换模型
python export.py --weights ./weight/yolov7-tiny.pt --img-size 640 640 
```

注意:如果需要修改onnx模型的输入大小,可以调整--img-size参数,同时模型输入batch默认为1,若想修改可以通过添加--batch-size设置,程序运行结束后,在当前目录下会生成onnx格式的YOLOV7模型,并将该模型保存到了samples工程中的Resource/Models/Detector/YOLOV7目录中,可以用来MIGraphX加载推理。

### 模型推理

#### 预处理

待检测图片输入模型进行检测之前需要进行预处理,主要包括调整输入的尺寸,归一化等操作。

1. 转换数据排布为NCHW
2. 调整输入数据的尺寸为(1,3,640,640)

```python
def prepare_input(self, image):
    self.img_height, self.img_width = image.shape[:2]
    input_img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    # 调整图像的尺寸为YOLOV7的输入尺寸
    input_img = cv2.resize(input_img, (self.inputWidth, self.inputHeight))
    # 调整输入数据的维度
    input_img = input_img.transpose(2, 0, 1)
    # 拓展维度,增加batch维度
    input_img = np.expand_dims(input_img, 0)
    input_img = np.ascontiguousarray(input_img)
    input_img = input_img.astype(np.float32)
    # 归一化[0.0, 1.0]
    input_img = input_img / 255

    return input_img
```

其中模型输入的inputWidth、inputHeight通过migraphx对输入模型进行解析获取,代码位置见YOLOV7类初始化位置。

```
class YOLOv7:
    def __init__(self, path, obj_thres=0.5, conf_thres=0.7, iou_thres=0.5):
        self.objectThreshold = obj_thres
        self.confThreshold = conf_thres
        self.nmsThreshold = iou_thres
        
        # 获取模型检测的类别信息
        self.classNames = list(map(lambda x: x.strip(), open('./weights/coco.names', 'r').readlines()))

        # 解析推理模型
        self.model = migraphx.parse_onnx(path)
      
        # 获取模型的输入name
        self.inputName = self.model.get_parameter_names()[0]
        
        # 获取模型的输入尺寸
        inputShape = self.model.get_parameter_shapes()[self.inputName].lens()
        self.inputHeight = int(inputShape[2])
        self.inputWidth = int(inputShape[3])
```

#### 推理

输入图片预处理完成之后开始进行推理,首先需要利用migraphx进行编译,然后对输入数据进行前向计算得到模型的输出result,在detect函数中调用定义的process_output函数对result进行后处理,得到图片中含有物体的anchor坐标信息、类别置信度、类别ID。

```
def detect(self, image):
    # 输入图片预处理
    input_img = self.prepare_input(image)

    # 模型编译
    self.model.compile(t=migraphx.get_target("gpu"), device_id=0)  # device_id: 设置GPU设备,默认为0号设备
    print("Success to compile")
    # 执行推理
    print("Start to inference")
    start = time.time()
    result = self.model.run({self.model.get_parameter_names()[0]: migraphx.argument(input_img)})
    print('net forward time: {:.4f}'.format(time.time() - start))
    # 模型输出结果后处理
    boxes, scores, class_ids = self.process_output(result)

    return boxes, scores, class_ids
```

其中对migraphx推理输出result进行后处理,首先需要对生成的anchor根据是否有物体阈值objectThreshold、置信度阈值confThreshold进行筛选,相关过程定义在process_output函数中。获取筛选后的anchor的坐标信息之后,需要将坐标映射到原图中的位置,相关过程定义在rescale_boxes函数中。

```
def process_output(self, output):
    predictions = np.squeeze(output[0])

    # 筛选包含物体的anchor
    obj_conf = predictions[:, 4]
    predictions = predictions[obj_conf > self.objectThreshold]
    obj_conf = obj_conf[obj_conf > self.objectThreshold]

    # 筛选大于置信度阈值的anchor
    predictions[:, 5:] *= obj_conf[:, np.newaxis]
    scores = np.max(predictions[:, 5:], axis=1)
    valid_scores = scores > self.confThreshold
    predictions = predictions[valid_scores]
    scores = scores[valid_scores]

    # 获取最高置信度分数对应的类别ID
    class_ids = np.argmax(predictions[:, 5:], axis=1)

    # 获取每个物体对应的anchor
    boxes = self.extract_boxes(predictions)

    # 执行非极大值抑制消除冗余anchor
    indices = cv2.dnn.NMSBoxes(boxes.tolist(), scores.tolist(), self.confThreshold, self.nmsThreshold).flatten()

    return boxes[indices], scores[indices], class_ids[indices]
        
def rescale_boxes(self, boxes):
    # 对anchor尺寸进行变换
    input_shape = np.array([self.inputWidth, self.inputHeight, self.inputWidth, self.inputHeight])
    boxes = np.divide(boxes, input_shape, dtype=np.float32)
    boxes *= np.array([self.img_width, self.img_height, self.img_width, self.img_height])
    return boxes
```

根据获取的detect函数输出的boxes、scores、class_ids信息在原图进行结果可视化,包括用绘制图片中检测到的物体位置、类别和置信度分数,得到最终的YOLOV7目标检测结果输出。

```
def draw_detections(self, image, boxes, scores, class_ids):
    for box, score, class_id in zip(boxes, scores, class_ids):
        cx, cy, w, h = box.astype(int)

        # 绘制检测物体框
        cv2.rectangle(image, (cx, cy), (cx + w, cy + h), (0, 255, 255), thickness=2)
        label = self.classNames[class_id]
        label = f'{label} {int(score * 100)}%'
        labelSize, baseLine = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
        cv2.putText(image, label, (cx, cy - 10), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), thickness=2)
    return image
```

### 运行示例

1.参考《MIGraphX教程》中的安装方法安装MIGraphX并设置好PYTHONPATH

2.运行示例

```
# 进入migraphx samples工程根目录
cd <path_to_migraphx_samples> 

#进入示例程序目录
cd Python/Detector/YOLOV7

# 运行示例
python detect_migraphx.py --imgpath ./inference/images/bus.jpg --modelpath ./weights/yolov7-tiny.onnx --objectThreshold 0.5 --confThreshold 0.25 --nmsThreshold 0.5
```

输入参数中可根据需要进行修改,程序运行结束会在当前目录生成YOLOV7检测结果图片。

<img src="Images/YOLOV7_01.jpg" alt="YOLOV7_01" style="zoom:67%;" />



<div STYLE="page-break-after: always;"></div>

## 图像分割

本示例采用了经典的Unet模型进行图像分割,模型下载地址:https://www.dropbox.com/s/3ntkhyk30x05uuv/unet_13_256.onnx,将unet_13_256.onnx文件保存在Resource\Models\Segmentation文件夹下。模型结构如下图所示(可以通过netron工具(https://netron.app/)查看模型结构),该模型的输入shape为[batch_size,3,256,256],输出shape为[batch_size,1,256,256],数据排布为NCHW。

![Unet_Image_1](Images/Unet_Image_1.png)

### 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

​      1.尺度变换,将图像resize到256x256大小

​      2.归一化,将数据归一化到[0.0, 1.0]之间

​      3.数据排布,将数据从HWC转换为CHW,再将维度转换为NCHW

本示例代码通过如下方式实现预处理操作:

```python
def Preprocessing(pil_img, newW, newH):
    assert newW > 0 and newH > 0, 'Scale is too small'  # 判断规定的尺寸是否为空
    img_nd = cv2.resize(pil_img, (newW, newH))          # 将图像尺寸修改为256x256
    img_nd = cv2.cvtColor(img_nd, cv2.COLOR_BGR2RGB)    # BGR转换为RGB

    img_trans = img_nd.transpose((2, 0, 1))             # HWC转换为CHW
    if img_trans.max() > 1:                             # 保证数据处于0-1之间的浮点数
        img_trans = img_trans / 255.0  
    img_trans = np.expand_dims(img_trans, 0)            # CHW转换为NCHW
    img = img_trans.astype(np.float32)                  # 转换成浮点型数据
    return img
```

### 推理

完成图像预处理后,就可以执行推理,得到推理结果。

```python
    # 加载模型
    model = migraphx.parse_onnx("../../Resource/Models/Segmentation/unet_13_256.onnx")

    # 编译模型
    model.compile(migraphx.get_target("gpu"), device_id=0)   #device_id: 设置GPU设备,默认为0号设备

    # 图像预处理
    img = cv2.imread("../../Resource/Images/car1.jpeg")
    input_img = Preprocessing(img, 256, 256)

    # 模型推理
    mask = model.run({'inputs':input_img})      
    output_mask = np.array(mask[0])[0]          # 获取推理结果,shape为(1,256,256)
    probs = Sigmoid(output_mask)                # 计算sigmoid值

    # 0/1像素值,当概率值大于0.996时,值为255,小于等于0.996时,值为0
    output_mask[probs > 0.996] = 255            
    output_mask[probs <= 0.996] = 0

    output = output_mask.astype(np.uint8)[0]  # 将浮点数类型转换为无符号整型,shape为(256,256)
    cv2.imwrite("output.jpg", output)         # 保存图像分割结果

```

1.Preprocessing函数返回预处理后的数据(numpy类型),然后通过model.run({'inputs':input_img})得到推理结果,因为只有输入一张图片,所以通过mask[0]获取第一个输出节点的数据即可。

2.模型得到的推理结果并不能直接作为分割结果。首先,需要计算sigmoid值,当概率值大于0.996时值为255,小于等于0.996时值为0。其次,将数据类型转换为无符号整形。最终,保存结果得到分割图像。

注:本次采用的模型权重onnx文件是通过使用具有普通背景的汽车图像来训练的。因此,“现实世界“图像的分割结果不完美是意料之中的。为了获得更好的结果,建议对现实世界示例数据集上的模型进行微调。

### 运行示例

1.参考《MIGraphX教程》中的安装方法安装MIGraphX并设置好PYTHONPATH

2.安装依赖:

```Python
# 进入migraphx samples工程根目录
cd <path_to_migraphx_samples> 

# 进入示例程序目录
cd Python/Segmentation

# 安装依赖
pip install -r requirements.txt
```

3.在Python/Segmentation目录下执行如下命令运行该示例程序:

```
python Unet.py
```

输出结果为:

![Unet_output_python](Images/Unet_output_python.jpg)



<div STYLE="page-break-after: always;"></div>

## 超分辨率重建

### 模型简介

图像超分辨率重建技术是指设计并采用某种算法,使得可以通过观测到的低分辨率(Low Resolution, LR)图像重建出近似真实高分辨率(High Resolution , HR)图像的方法。本次部署的超分模型是2016提出的ESPCN网络,主要通过一系列卷积层不断提取图像特征,最后通过sub-pixel亚像素卷积层提高图像分辨率,从而实现图像超分辨率的方法。ESPCN网络结构如下图所示:
![Superresolution_01](Images/Superresolution_01.png)

本示例采用onnx官方提供的ESPCN模型示例:https://github.com/onnx/models/tree/main/vision/super_resolution/sub_pixel_cnn_2016,将super.onnx文件保存在Resource\Models\Super_Resolution文件夹下。

### 预处理

在将数据输入到模型之前,需要对输入图像做如下预处理操作:

​      1.图像尺寸resize到224x224

​      2.分离图像通道,变为B、G、R三个通道图像;

​      3.BGR格式通过公式转换为YCrCb格式,得到Y、Cb、Cr三通道图像;

​      4.只处理Y通道图像,转换数据排布为NCHW;

本示例代码采用了OpenCV实现预处理操作:

```python
def bgr_to_ycbcr(B, G, R):
    Y = 0.299 * R + 0.587 * G + 0.114 * B
    Cr = (R - Y) * 0.713 + 128
    Cb = (B - Y) * 0.564 + 128
    return Y, Cr, Cb

def Preprocessing(pathOfImage):
    orig_img = cv2.imread(pathOfImage, cv2.IMREAD_COLOR) # cv2.IMREAD_COLOR:彩色图,cv2.IMREAD_GRAYSCALE:灰度图
    img = cv2.resize(orig_img, (224,224))                # 调整图像为固定尺寸224x224
    bImg, gImg, rImg = cv2.split(img)                    # 分离图片
    img_y_0, img_cr, img_cb = bgr_to_ycbcr(bImg, gImg, rImg) # bgr转换为YCbCr
    img_ndarray = np.asarray(img_y_0)                    # 复制img_y_0对象,转换为array([224,224])
    img_y = np.reshape(img_ndarray, (1,1,224,224))       # 改变数组形状,满足模型输入,变为(1,1,224,224) 转换为(N,C,H,W)格式
    img_y = img_y.astype(np.float32) / 255.0             # unit8转换成float32
    return img_y, img_cr, img_cb
```

### 推理

完成图像预处理后,就开始执行推理,得到推理结果。

```python
if __name__ == '__main__':
    # 加载模型
    model = migraphx.parse_onnx("../../Resource/Models/Super/super.onnx")
    inputName = model.get_parameter_names()
    inputShape = model.get_parameter_shapes()
    print("inputName:{0} \ninputShape:{1}".format(inputName, inputShape))

    # 编译模型
    model.compile(t=migraphx.get_target("gpu"), device_id=0)  # device_id: 设置GPU设备,默认为0号设备

    # 图像预处理
    pathOfImage = "../../Resource/Images/cat.jpg"
    img_Y, img_cr, img_cb = Preprocessing(pathOfImage)

    # 模型推理结果
    results = model.run({"input": img_Y})

    # 获取输出节点属性
    result = results[0]                      # 获取第一个输出节点的数据,migraphx.argument类型
    outputShape = result.get_shape()         # 输出节点的shape,migraphx.shape类型
    outputSize = outputShape.lens()          # 每一维大小,维度顺序为(N,C,H,W)
    numberOfOutput = outputShape.elements()  # 输出节点元素的个数

    data = np.array(results[0])[0]
    img_out_y = np.uint8((data * 255.0).clip(0, 255)[0]) # 将浮点型数据转换为0-255之间整型数据
    
    # 将YCbCr格式转换为BGR格式,再将三通道合并到一起,得到重建图像
    img_cr = cv2.resize(img_cr, (672, 672))
    img_cb = cv2.resize(img_cb, (672, 672))
    final_img = ycbcr_to_bgr(img_out_y, img_cr, img_cb)
    cv2.imwrite("output.jpg", final_img)     # 保存超分辨率重建图像的结果
```

 1.Preprocessing函数返回Y、Cr、Cb三通道图像,只将Y通道图像作为输入数据,通过执行model.run({"input": img_Y})得到推理结果,因为只有输入一张图片,所以只通过results[0]获取第一个输出节点的数据即可。
 2.该模型只处理Y通道图像,得到推理结果之后,再通过ycrcb_to_bgr()函数将YCrCb格式转换为BGR格式,转换公式采用的是opencv官方提供的公式,链接:https://docs.opencv.org/3.4.11/de/d25/imgproc_color_conversions.html,最后再将三通道合并到一起,得到最终的重建图像。

### 运行示例

1.参考《MIGraphX教程》中的安装方法安装MIGraphX并设置好PYTHONPATH

2.安装依赖:

```
# 进入migraphx samples工程根目录
cd <path_to_migraphx_samples>

# 进入示例程序目录
cd Python/Super_Resolution

# 安装依赖
pip install -r requirements.txt
```

3.在Python/Super_Resolution目录下执行如下命令运行该示例程序:

```python
# 运行示例
python Espcn.py
```

输出结果如下,左图为原图,右图为重建过后的图像,分辨率提高了三倍。

![Superresolution_03](Images/Superresolution_03.png)