在 Picturebox 上显示从相机检索到的位图的实验

Experiment on displaying a Bitmap retrieved from a camera on a Picturebox

在我的代码中,我使用指向非托管对象的指针从相机中检索帧,对其进行一些计算,然后在图片框控件上将其可视化。
在我进一步了解这个应用程序的所有细节之前,我想确保这个过程的基本代码是好的。 我特别想:
- 保持执行时间最短并避免不必要的操作,例如 复制不必要的图像。我只想保留必要的 操作
- 了解每帧计算过程中的延迟是否会对图像的显示方式产生不利影响(即,如果它没有按照我的预期打印)或跳过某些图像
- 防止更严重的错误,例如由于内存或线程管理或图像显示引起的错误。
为此,我设置了几行实验代码(如下),但我无法解释我发现的结果。如果你有 OpenCv 的可执行文件,你可以自己尝试一下。

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Threading;

public partial class FormX : Form
{
private delegate void setImageCallback();
Bitmap _bmp;
Bitmap _bmp_draw;
bool _exit;
double _x;
IntPtr _ImgBuffer;

bool buffercopy;
bool copyBitmap;
bool refresh;

public FormX()
{
    InitializeComponent();
    _x = 10.1;

    // set experimemental parameters
    buffercopy = false;
    copyBitmap = false;
    refresh = true;
}

private void buttonStart_Click(object sender, EventArgs e)
{
    Thread camThread = new Thread(new ThreadStart(Cycle));
    camThread.Start();
}

private void buttonStop_Click(object sender, EventArgs e)
{
    _exit = true;
}

private void Cycle()
{
    _ImgBuffer = IntPtr.Zero;
    _exit = false;

    IntPtr vcap = cvCreateCameraCapture(0);
    while (!_exit)
    {
        IntPtr frame = cvQueryFrame(vcap);

        if (buffercopy)
        {
            UnmanageCopy(frame);
            _bmp = SharedBitmap(_ImgBuffer);
        }
        else
        { _bmp = SharedBitmap(frame); }

        // make calculations
        int N = 1000000; /*1000000*/
        for (int i = 0; i < N; i++)
            _x = Math.Sin(0.999999 * _x);

        ShowFrame();
    }

    cvReleaseImage(ref _ImgBuffer);
    cvReleaseCapture(ref vcap);
}


private void ShowFrame()
{
    if (pbCam.InvokeRequired)
    {
        this.Invoke(new setImageCallback(ShowFrame));
    }
    else
    {
        Pen RectangleDtPen = new Pen(Color.Azure, 3);

        if (copyBitmap)
        {
            if (_bmp_draw != null) _bmp_draw.Dispose();
            //_bmp_draw = new Bitmap(_bmp); // deep copy
            _bmp_draw = _bmp.Clone(new Rectangle(0, 0, _bmp.Width, _bmp.Height), _bmp.PixelFormat);
        }
        else
        {
            _bmp_draw = _bmp;  // add reference to the same object
        }

        Graphics g = Graphics.FromImage(_bmp_draw);
        String drawString = _x.ToString();
        Font drawFont = new Font("Arial", 56);
        SolidBrush drawBrush = new SolidBrush(Color.Red);
        PointF drawPoint = new PointF(10.0F, 10.0F);
        g.DrawString(drawString, drawFont, drawBrush, drawPoint);
        drawPoint = new PointF(10.0F, 300.0F);
        g.DrawString(drawString, drawFont, drawBrush, drawPoint);
        g.DrawRectangle(RectangleDtPen, 12, 12, 200, 400);
        g.Dispose();

        pbCam.Image = _bmp_draw;
        if (refresh) pbCam.Refresh();
    }
}

public void UnmanageCopy(IntPtr f)
{
    if (_ImgBuffer == IntPtr.Zero)
        _ImgBuffer = cvCloneImage(f);
    else
        cvCopy(f, _ImgBuffer, IntPtr.Zero);
}

// only works with 3 channel images from camera! (to keep code minimal)
public Bitmap SharedBitmap(IntPtr ipl)
{
    // gets unmanaged data from pointer to IplImage:
    IntPtr scan0;
    int step;
    Size size;
    OpenCvCall.cvGetRawData(ipl, out scan0, out step, out size);
    return new Bitmap(size.Width, size.Height, step, PixelFormat.Format24bppRgb, scan0);
}

// based on older version of OpenCv. Change dll name if different
[DllImport( "opencv_highgui246", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr cvCreateCameraCapture(int index);

[DllImport("opencv_highgui246", CallingConvention = CallingConvention.Cdecl)]
public static extern void cvReleaseCapture(ref IntPtr capture);

[DllImport("opencv_highgui246", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr cvQueryFrame(IntPtr capture);

[DllImport("opencv_core246", CallingConvention = CallingConvention.Cdecl)]
public static extern void cvGetRawData(IntPtr arr, out IntPtr data, out int step, out Size roiSize);

[DllImport("opencv_core246", CallingConvention = CallingConvention.Cdecl)]
public static extern void cvCopy(IntPtr src, IntPtr dst, IntPtr mask);

[DllImport("opencv_core246", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr cvCloneImage(IntPtr src);

[DllImport("opencv_core246", CallingConvention = CallingConvention.Cdecl)]
public static extern void cvReleaseImage(ref IntPtr image);
}

结果[双核 2 Duo T6600 2.2 GHz]:

A。缓冲区复制=假;复制位图=假;刷新=假;
这是更简单的配置。依次检索每一帧,进行操作(实际上它们是基于同一帧,这里只是计算),然后将计算结果打印在图像上,最后显示在图片框上。
OpenCv 文档说:

OpenCV 1.x functions cvRetrieveFrame and cv.RetrieveFrame return image stored inside the video capturing structure. It is not allowed to modify or release the image! You can copy the frame using cvCloneImage() and then do whatever you want with the copy.

但这并不妨碍我们做实验。
如果计算不密集(迭代次数少,N),一切都很好,我们操纵非托管帧检索器拥有的图像缓冲区这一事实在这里不会造成问题。
原因可能是他们建议不要触及缓冲区,以防人们修改它的结构(而不​​是它的值)或在没有意识到的情况下异步执行操作。现在我们依次检索帧并修改它们的内容。
如果增加 N(N=1000000 或更多),当每秒帧数不高时,例如在人造光和低曝光下,一切似乎都正常,但过一会儿视频就会滞后并且图形会在上面留下深刻印象正在闪烁。使用更高的帧速率,即使视频仍然流畅,闪烁也会从一开始就出现。
这是因为在控件上显示图像(或刷新或其他)的机制在某种程度上是异步的,当图片框正在获取其数据缓冲区时,它同时被相机修改,删除图形?
还是有其他原因?
为什么图像会以这种方式滞后,即我预计由于计算而导致的延迟只会在计算尚未完成时跳过相机接收到的帧,并且实际上只会降低帧速率;或者,所有帧都已接收并且由于计算而导致的延迟使系统处理几分钟前获得的图像,因为要处理的图像队列会随着时间的推移而增加。
相反,观察到的行为似乎是两者的混合体:有几秒钟的延迟,但随着捕获过程的进行,延迟似乎并没有增加多少。

B。缓冲区复制=真;复制位图=假;刷新=假;
在这里,我按照 OpenCv 文档的建议将缓冲区的深层副本复制到第二个缓冲区中。
没有什么改变。在 运行 期间,第二个缓冲区不会更改其在内存中的地址。

C。缓冲区复制=假;复制位图=真;刷新=假;
现在,每次在内存中分配新的 space 时都会分配位图的(深)副本。
闪烁效果消失了,但在一定时间后仍会出现滞后现象。

D。缓冲区复制=假;复制位图=假;刷新=真;
像之前一样。

请帮我解释一下这些结果!

我认为您的 UnmanageCopy 方法仍然存在问题,因为您仅在第一次调用此方法时克隆图像,然后再复制它。我相信你需要做一个 cvCloneImage(f) every 次,因为 copy 只执行浅拷贝,而不是像你想的那样深拷贝。

如果我可以这么坦率,理解你问题的所有细节有点乏味,但让我提出几点来帮助你分析你的结果。

在案例A中,你说你直接在缓冲区上执行计算。文档说你不应该这样做,所以如果你这样做,你可能会得到未定义的结果。 OpenCV 假设你不会碰它,所以它可能会做一些事情,比如突然删除那部分内存,让其他应用程序处理它,等等。它可能看起来有效,但你永远无法确定,所以不要这样做 *拍打你的手腕* 特别是,如果你的处理需要很长时间,相机可能会在你处理过程中覆盖缓冲区。

你应该这样做的方法是在做任何事情之前复制缓冲区。这会给你一段记忆,你可以随心所欲地做任何事。您可以创建一个引用此内存的位图,并在您不再需要时手动释放内存。

如果您的处理速率(每秒处理的帧数)小于相机每秒捕获的帧数,您必须预料到会丢失一些帧。如果您想显示处理过的图像的实时视图,它会滞后并且没有简单的解决方法。如果您的应用程序处理流畅的视频至关重要(例如,如果您正在跟踪一个对象,这可能是必要的),那么请考虑将视频存储到磁盘,这样您就不必实时处理。也可以考虑多线程一次处理多帧,但是实时取景会有延迟。

顺便问一下,您没有使用 EmguCV 有什么特别的原因吗?它具有相机抽象和一个系统,该系统在相机捕捉到新帧时引发事件。这样,您就不需要在后台线程上连续调用 cvQueryFrame