iT邦幫忙

0

使用 C# Parallel.ForEach 進行平行處理 YOLO程式無預警中斷

css 2022-11-13 20:04:161601 瀏覽
  • 分享至 

  • xImage

各位大神好!小弟我想使用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秒的影片,最快近一分鐘了,還不如我人眼自己看..../images/emoticon/emoticon02.gif)

 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";

        }

https://ithelp.ithome.com.tw/upload/images/20221113/20151078Uq5Uq0S4l0.jpg

所以試試看若採行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,會發現執行時會不定時出現這個情況https://ithelp.ithome.com.tw/upload/images/20221113/20151078LXossoBum5.jpg

/images/emoticon/emoticon06.gif這是甚麼原因呢? 是否有解決的方案呢? 感謝各位幫我解惑⋯⋯

css iT邦新手 5 級 ‧ 2022-11-14 13:02:07 檢舉
我也嘗試在:
catch (AggregateException e)
{
parallelExceptions.Enqueue(e);
}
這裡設中斷點Debug,很明顯在之前就中斷卻沒捕獲任何訊息??
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 個回答

1
johncoc
iT邦新手 3 級 ‧ 2022-11-14 12:10:04
最佳解答

看起來是沒有讓螢幕上顯示文字

 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();
            
        }
看更多先前的回應...收起先前的回應...
css iT邦新手 5 級 ‧ 2022-11-14 13:30:59 檢舉

我之前在:

catch (AggregateException e)
   {
     parallelExceptions.Enqueue(e);
   }

這裡有設中斷點Debug,很明顯在之前就中斷,可惜沒丟出捕獲任何訊息,不知道為何.....

johncoc iT邦新手 3 級 ‧ 2022-11-14 14:29:38 檢舉

很明顯在之前就中斷 => 意思是有執行到前面那個中斷點?

css iT邦新手 5 級 ‧ 2022-11-14 15:11:21 檢舉

是的!! 我設定過好幾次,不同的地方設中斷點。包含在parrallel.foreach()中,會看到真得有執行,而且按F5會看到程式指令到處跳(我猜在平行處理),但就是會出現類似的畫面(不一定是哪行指令),僅僅這個訊息方塊,我不知道問題在哪https://ithelp.ithome.com.tw/upload/images/20221114/20151078Ni8P9PR647.jpg
我會認為這10個執行中的Task吐出某個錯誤訊息,但無法被捕獲就中斷。
另外在此處(parallelExceptions.Enqueue(e);)設的中斷點,很明顯從沒被執行。

johncoc iT邦新手 3 級 ‧ 2022-11-14 15:16:14 檢舉

warning 的那兩個訊息是什麼

css iT邦新手 5 級 ‧ 2022-11-14 17:28:20 檢舉

第一個是警告我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;
   }
johncoc iT邦新手 3 級 ‧ 2022-11-14 18:13:35 檢舉

try catch 如果中斷點設在catch裡面 ,只要try裡面的程式沒發生exception 就不會跑進catch =>也就不會被中斷(if else 也是同理)

johncoc iT邦新手 3 級 ‧ 2022-11-14 18:15:06 檢舉

你是不是把中斷點都設在不是必定執行的地方?

css iT邦新手 5 級 ‧ 2022-11-14 18:25:32 檢舉

疑!! 所以我的意思便是>>>>catch真沒被執行啊!!
看起來"try裡面的程式沒發生exception"

但 ==>他就是無聲無息結束!! 但不是正常執行完

是不是 我有誤會甚麼意思 麻煩再說明 感謝

johncoc iT邦新手 3 級 ‧ 2022-11-14 18:29:18 檢舉

就是這個意思,你再把中斷點設在必定執行的地方看看?

johncoc iT邦新手 3 級 ‧ 2022-11-14 18:30:02 檢舉

無聲無息結束!! => 你加個finally顯示訊息看看?

css iT邦新手 5 級 ‧ 2022-11-14 19:48:15 檢舉

感謝你的細心回答,以下我採用你的建議,但發現還是沒有任何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

css iT邦新手 5 級 ‧ 2022-11-14 20:10:57 檢舉

補充:我目前也懷疑平行處理遇到Marshal這些靜態物件,是不是會產生異常.......

johncoc iT邦新手 3 級 ‧ 2022-11-15 14:20:03 檢舉

記憶體有用很多嗎?

css iT邦新手 5 級 ‧ 2022-11-15 21:03:30 檢舉

剛剛解決了....
我目前的想法是類似以下的指令:

Marshal.Copy(imageData, 0, pnt, imageData.Length);

由於是靜態物件,所以會在平行執行時衝突,為了解決必須要設lock,但原來的程式有太多使用到Marshal靜態類別的地方,東鎖西鎖最後平行道也變成單向道/images/emoticon/emoticon23.gif

所以,改進第一步在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相當困難耶!!(我例外狀況設定也巡過了....)

johncoc iT邦新手 3 級 ‧ 2022-11-16 09:42:31 檢舉

deadlock好像都是執行中,不會跳出錯誤訊息
執行緒的互鎖與死鎖

css iT邦新手 5 級 ‧ 2022-11-16 11:39:58 檢舉

理解......感謝了/images/emoticon/emoticon41.gif

我要發表回答

立即登入回答