各位大神好!小弟我想使用C#透過yoloWrapper對MP4影片檔進行影像識別,目前如果順序逐一影格辨識是可運行的,但速度實在慢的感人!
所以想透過C#中Parallel.ForEach進行平行化處理,看是否可以再榨取一些速度。可惜幻想很美好,現實卻很骨感!! 採行這樣的方式,程式直接無預警結束,沒有丟出任何訊息。
在此用一個小測試畫面簡化說明問題,下面為該畫面類別我會使用的變數、結構
public partial class XForm : Form
{
const string CONFIG = @".\cfg\CFG20221109.cfg";
const string WEIGHTS = @".\weights\WEIGHTS20221109.weights";
const string NAMES = @".\data\NAMES20221109.names";
const String FILENAME = @"D:\1.mp4";
//每一幀所含的矩陣資料與編號
private struct Framecontainer
{
public Mat Image;
public int FrameId;
}
//每一幀所含的檢測框
private struct FrameResultContainer
{
public int FrameId;
public List<BboxResultContainer>
BboxResults;
}
//每一檢測框的資訊
private struct BboxResultContainer
{
public int classId;
public string Name;
public float Confidence;
public Rect RectBox;
}
private FrameResultContainer[] myFrameResults
= new FrameResultContainer[10];
private YoloWrapper.YoloWrapper myAIModel;
private Dictionary<int, string>
myNamesDic = new Dictionary<int, string>();
private int myClassNum;
在畫面初始化時,透過YoloWrapper封裝啟動DarkNet的Yolo Model
public XForm()
{
InitializeComponent();
//啟動yolo
this.myAIModel = new YoloWrapper.YoloWrapper
(CONFIG, WEIGHTS, 0);
//寫入名稱檔
var lines = File.ReadAllLines(NAMES);
this.myClassNum = lines.Length;
for (var i = 0; i < lines.Length; i++)
this.myNamesDic.Add(i, lines[i]);
toolStripStatusLabel1.Text = "啟動";
}
Detact這個Method主要在進行每一幀影格的辨識與後面的NMS處理
private FrameResultContainer Detact(Mat image, int currentframe)
{
FrameResultContainer frameresult
= new FrameResultContainer();
frameresult.FrameId = currentframe;
#region 影像識別處理
//影像格式轉換Mat->Bytes
int totalsize = image.Height *
image.Width *
image.Channels();
var bytes = new byte[totalsize];
BoundingBox[] bboxes;
try
{
//將影像資料轉為位元組陣列,再傳入Darknet核心進行識別
Marshal.Copy(image.Data, bytes, 0, bytes.Length);
bboxes = this.myAIModel.Detect(image.Width,
image.Height,bytes);
}
catch(Exception err)
{
throw;
}
#endregion
#region 影像識別後處理
if (bboxes.Length == 0) //該幀如果沒有檢測結果直接回傳
return frameresult;
List<int> classids = new List<int>();
List<float> confidences
= new List<float>();
List<string> names = new List<string>();
List<Rect> rectboxes = new List<Rect>();
//逐一讀取檢測框資訊
foreach (BoundingBox bbox in bboxes)
{
if (bbox.h == 0) break; //沒資料跳出
int classid = (int)bbox.track_id;
float confidence
= bbox.prob;
Rect rectbox = new Rect((int)bbox.x, (int)bbox.y,
(int)bbox.w, (int)bbox.h);
string name = myNamesDic[(int)bbox.obj_id];
classids.Add(classid);
confidences.Add(confidence);
names.Add(name);
rectboxes.Add(rectbox);
}
CvDnn.NMSBoxes(rectboxes, confidences,
0.9f, 0.3f, out int[] indences);//NMS演算
//後處理完成之顯示框儲存
frameresult.BboxResults
= new List<BboxResultContainer>();
foreach (int i in indences)
{
BboxResultContainer bboxresult
= new BboxResultContainer();
bboxresult.classId = classids[i];
bboxresult.Name = names[i];
bboxresult.Confidence = confidences[i];
bboxresult.RectBox = rectboxes[i];
frameresult.BboxResults.Add(bboxresult);
}
#endregion
return frameresult;
}
button1_Click順序化讀取影格進行辨識,以下測試程式碼僅採用10幀影格就花了1631ms(在實際程式中讀取分析10秒的影片,最快近一分鐘了,還不如我人眼自己看....)
private void button1_Click(object sender, EventArgs e)
{
//媒體捕獲器連結資料
VideoCapture ObjCap = new VideoCapture();
ObjCap.Open(FILENAME);
Mat image = new Mat();
int i = 0;
var mySW = new Stopwatch();
mySW.Start();
while (true)
{
ObjCap.Read(image);
//讀檔10幀影格完畢跳出
if (i==10)
break;
//逐幀檢測並回傳結果
this.myFrameResults[i]= this.Detact(image,i);
++i;
}
mySW.Stop();
toolStripStatusLabel1.Text = $"執行完成 in " +
$"{mySW.Elapsed.TotalMilliseconds:0}ms";
}
所以試試看若採行Parallel.ForEach進行平行化處理,看是否能使執行速度再加快
private void button2_Click(object sender, EventArgs e)
{
//媒體捕獲器連結資料
VideoCapture ObjCap = new VideoCapture();
ObjCap.Open(FILENAME);
Mat image = new Mat();
Framecontainer imgcontainer
= new Framecontainer();
List<Framecontainer> images
= new List<Framecontainer>();
int i = 0;
while (true)
{
ObjCap.Read(image);
//讀檔完畢跳出
if (images.Count==10)
break;
imgcontainer.Image = image.Clone();
imgcontainer.FrameId = i;
images.Add(imgcontainer);
i++;
}
try
{
var parallelExceptions = new ConcurrentQueue<Exception>();
Parallel.ForEach<Framecontainer>(images,(img) =>
{
try
{
//幀檢測並回傳結果
this.myFrameResults[img.FrameId]
= this.Detact(img.Image, img.FrameId);
}
catch (AggregateException e)
{
parallelExceptions.Enqueue(e);
}
if (parallelExceptions.Count > 0)
throw new AggregateException(parallelExceptions);
});
}
catch (AggregateException err)
{
foreach (Exception item in err.InnerExceptions)
{
Debug.WriteLine("異常型別:{0}{1}來自:" +
"{2}{3}異常內容:{4}", item.InnerException.GetType(),
Environment.NewLine, item.InnerException.Source,
Environment.NewLine, item.InnerException.Message);
}
}
images.Clear();
}
設了一堆try-catch程式就是因為本人完全掌握不住狀況,但是程式還就是悄悄結束,捕獲不到任何異常訊息......
若是設中斷點逐一執行Debug,會發現執行時會不定時出現這個情況
這是甚麼原因呢? 是否有解決的方案呢? 感謝各位幫我解惑⋯⋯
看起來是沒有讓螢幕上顯示文字
private void button2_Click(object sender, EventArgs e)
{
//媒體捕獲器連結資料
VideoCapture ObjCap = new VideoCapture();
ObjCap.Open(FILENAME);
Mat image = new Mat();
Framecontainer imgcontainer
= new Framecontainer();
List<Framecontainer> images
= new List<Framecontainer>();
int i = 0;
while (true)
{
ObjCap.Read(image);
//讀檔完畢跳出
if (images.Count==10)
break;
imgcontainer.Image = image.Clone();
imgcontainer.FrameId = i;
images.Add(imgcontainer);
i++;
}
try
{
var parallelExceptions = new ConcurrentQueue<Exception>();
Parallel.ForEach<Framecontainer>(images,(img) =>
{
try
{
//幀檢測並回傳結果
this.myFrameResults[img.FrameId]
= this.Detact(img.Image, img.FrameId);
}
catch (AggregateException e)
{
parallelExceptions.Enqueue(e);
}
if (parallelExceptions.Count > 0)
throw new AggregateException(parallelExceptions);
});
}
catch (AggregateException err)
{
foreach (Exception item in err.InnerExceptions)
{
Debug.WriteLine("異常型別:{0}{1}來自:" +
"{2}{3}異常內容:{4}", item.InnerException.GetType(),
Environment.NewLine, item.InnerException.Source,
Environment.NewLine, item.InnerException.Message);
}
}
//螢幕上顯示
toolStripStatusLabel2.Text = $"執行完成 in " +
$"{執行時間}ms";
images.Clear();
}
我之前在:
catch (AggregateException e)
{
parallelExceptions.Enqueue(e);
}
這裡有設中斷點Debug,很明顯在之前就中斷,可惜沒丟出捕獲任何訊息,不知道為何.....
很明顯在之前就中斷 => 意思是有執行到前面那個中斷點?
是的!! 我設定過好幾次,不同的地方設中斷點。包含在parrallel.foreach()中,會看到真得有執行,而且按F5會看到程式指令到處跳(我猜在平行處理),但就是會出現類似的畫面(不一定是哪行指令),僅僅這個訊息方塊,我不知道問題在哪
我會認為這10個執行中的Task吐出某個錯誤訊息,但無法被捕獲就中斷。
另外在此處(parallelExceptions.Enqueue(e);)設的中斷點,很明顯從沒被執行。
warning 的那兩個訊息是什麼
第一個是警告我item.InnerException.GetType()可能為Null
catch (AggregateException err)
{
foreach (Exception item in err.InnerExceptions)
{
Debug.WriteLine("異常型別:{0}{1}來自:" +
"{2}{3}異常內容:{4}",
item.InnerException.GetType(),//<=可能為Null
Environment.NewLine,
item.InnerException.Source,
Environment.NewLine,
item.InnerException.Message);
}
}
images.Clear();
}
第二個是警告在Detact這個Method
try
{
//將影像資料轉為位元組陣列,再傳入Darknet核心進行識別
Marshal.Copy(image.Data, bytes, 0, bytes.Length);
bboxes = this.myAIModel.Detect(image.Width,
image.Height, bytes);
}
catch(Exception err)//<=警告這個err沒使用
{
throw ;//<=後來使用了throw err; 一樣沒任何訊息
}
以下我將this.myAIModel.Detect()這個yolowrapper提供的程式顯示如下:
public BoundingBox[] Detect(int width,int height, byte []imageData)
{
var container = new BboxContainer();
var size = Marshal.SizeOf(imageData[0]) * imageData.Length;
var pnt = Marshal.AllocHGlobal(size);
try
{
Marshal.Copy(imageData,0, pnt, imageData.Length);
//這邊就是將影格資料丟入C的DLL去計算,
//結果就寫入BBOX container中
var count = DetectImage((uint)width,(uint)height,
pnt,imageData.Length,
ref container);
if (count == -1)
{
throw new NotSupportedException($"
{YoloLibraryName} has no OpenCV support");
}
}
catch (Exception exception)
{
return null;
}
finally
{
Marshal.FreeHGlobal(pnt);
}
return container.candidates;
}
try catch 如果中斷點設在catch裡面 ,只要try裡面的程式沒發生exception 就不會跑進catch =>也就不會被中斷(if else 也是同理)
你是不是把中斷點都設在不是必定執行的地方?
疑!! 所以我的意思便是>>>>catch真沒被執行啊!!
看起來"try裡面的程式沒發生exception"
但 ==>他就是無聲無息結束!! 但不是正常執行完
是不是 我有誤會甚麼意思 麻煩再說明 感謝
就是這個意思,你再把中斷點設在必定執行的地方看看?
無聲無息結束!! => 你加個finally顯示訊息看看?
感謝你的細心回答,以下我採用你的建議,但發現還是沒有任何Debug訊息被寫下。
Parallel.ForEach<Framecontainer>(images,(img) =>
{
try
{
this.myFrameResults[img.FrameId]
this.Detact(img.Image,
img.FrameId);//幀檢測並回傳結果
}
catch (AggregateException e)
{
parallelExceptions.Enqueue(e);
}
finally
{
throw new AggregateException("finally");
}
if (parallelExceptions.Count > 0)
throw new AggregateException(parallelExceptions);
});
我把中斷點設在finally內沒有執行到,依據fiannly的語法應該是每一次被執行完一定會被執行到,沒做到應該表示每一個平行的Task都沒執行到那就死翹翹了.......
另外,我也在try區段加中斷點,但還是會出現警告訊息方塊結束,但沒有拋出任何訊息給catch
補充:我目前也懷疑平行處理遇到Marshal這些靜態物件,是不是會產生異常.......
記憶體有用很多嗎?
剛剛解決了....
我目前的想法是類似以下的指令:
Marshal.Copy(imageData, 0, pnt, imageData.Length);
由於是靜態物件,所以會在平行執行時衝突,為了解決必須要設lock,但原來的程式有太多使用到Marshal靜態類別的地方,東鎖西鎖最後平行道也變成單向道
所以,改進第一步在yoloWrapper中多加一個Unsafe method,透過指標傳資料進yolo_cpp_dll.dll中
unsafe public BoundingBox[] Detect(int width, int height, byte* pnt, int totalsize)
{
int count;
BboxContainer container;
try
{
lock (_lock)
{
//在非安全模式下透過指標傳值,不用IntPtr
container = new BboxContainer();
count = DetectImage((uint)width, (uint)height,
pnt, totalsize, ref container);//<==但這還是瓶頸,顯卡太差??
}
if (count == -1)
throw new NotSupportedException(
$"{YoloLibraryName} has no OpenCV support");
}
catch (Exception exception)
{
return null;
}
finally
{
pnt=null;
}
return container.candidates;
}
}
相對應我也增加一個對接yolo_cpp_dll.dll窗口
[DllImport(YoloLibraryName, EntryPoint = "detect_mat")]
unsafe private static extern int DetectImage(uint width, uint height,byte* pArray,
int nSize, ref BboxContainer container);
之後我把Detact再做修改
//將detactmode = false 主要就跑這段減少交通管制
unsafe
{
//將影像資料指標傳入Darknet核心進行識別
byte* pnt = image.DataPointer;
bboxes = this.myAIModel.Detect(image.Width,
image.Height, pnt, totalsize);
}
全貌如下.....
private FrameResultContainer Detact(Mat image, int currentframe, bool detactmode = SAFEMODE)
{
FrameResultContainer frameresult
= new FrameResultContainer();
frameresult.FrameId = currentframe;
int totalsize = image.Height *
image.Width *
image.Channels();
BoundingBox[] bboxes;
#region 影像識別處理
if (detactmode == SAFEMODE)
{
//將影像資料轉為受保護的位元組陣列,
//再傳入Darknet核心進行識別
var bytes = new byte[totalsize];
Marshal.Copy(image.Data, bytes, 0, bytes.Length);
bboxes = this.myAIModel.Detect(image.Width,
image.Height, bytes);
}
else
{
//將detactmode = false 主要就跑這段減少交通管制
unsafe
{
//將影像資料指標傳入Darknet核心進行識別
byte* pnt = image.DataPointer;
bboxes = this.myAIModel.Detect(image.Width,
image.Height, pnt, totalsize);
}
}
#endregion
#region 影像識別後處理
if (bboxes.Length == 0) //該幀如果沒有檢測結果直接回傳
return frameresult;
List<int> classids = new List<int>();
List<float> confidences= new List<float>();
List<string> names = new List<string>();
List<Rect> rectboxes = new List<Rect>();
//逐一讀取檢測框資訊
foreach (BoundingBox bbox in bboxes)
{
if (bbox.h == 0) break; //沒資料跳出
int classid = (int)bbox.track_id;
float confidence= bbox.prob;
Rect rectbox = new Rect((int)bbox.x, (int)bbox.y,
(int)bbox.w(int)bbox.h);
string name = myNamesDic[(int)bbox.obj_id];
classids.Add(classid);
confidences.Add(confidence);
names.Add(name);
rectboxes.Add(rectbox);
}
int[] indences;
lock (_lock)
{
CvDnn.NMSBoxes(rectboxes, confidences,
0.9f, 0.3f, out indences);//NMS演算
}
//後處理完成之顯示框儲存
frameresult.BboxResults
= new List<BboxResultContainer>();
foreach (int i in indences)
{
BboxResultContainer bboxresult
= new BboxResultContainer();
bboxresult.classId = classids[i];
bboxresult.Name = names[i];
bboxresult.Confidence = confidences[i];
bboxresult.RectBox = rectboxes[i];
frameresult.BboxResults.Add(bboxresult);
}
#endregion
return frameresult;
}
之後就OK了。
省下來的時間很有限,我猜還是因為這裡
count = DetectImage((uint)width, (uint)height,
pnt, totalsize, ref container);
這應該進入yolo的核心了,一個影格80ms作判斷應該是演算法的極限了,再快應該就是強化硬體的問題了吧?
但是最初的問題似乎還是在,當共用物件衝突時,為什麼沒有丟出訊息呢??這樣Debug相當困難耶!!(我例外狀況設定也巡過了....)
deadlock好像都是執行中,不會跳出錯誤訊息
執行緒的互鎖與死鎖
理解......感謝了