【问题标题】:How to get a pixel array from TBitmap?如何从 TBitmap 获取像素数组?
【发布时间】:2020-01-16 00:34:43
【问题描述】:

在相机应用程序中,位图像素数组是从流式相机中检索的。 通过将像素数组写入命名管道来捕获像素数组,在管道的另一端,ffmpeg 检索它们并创建一个 AVI 文件。

我需要创建一个自定义帧(启用自定义文本),并将其像素作为生成影片中的第一帧。

问题是我如何使用 TBitmap(为了方便)来

  1. 从头开始创建 X x Y 单色(8 位)位图,使用 自定义文本。我希望背景为白色,文本为 变黑。 (主要是想出这一步,见下文。)

  2. 检索我可以发送/写入管道的像素数组

第 1 步:以下代码创建一个 TBitmap 并在其上写入文本:

int w = 658;
int h = 492;
TBitmap* bm = new TBitmap();
bm->Width = w;
bm->Height = h;
bm->HandleType = bmDIB;
bm->PixelFormat = pf8bit;

bm->Canvas->Font->Name = "Tahoma";
bm->Canvas->Font->Size = 8;

int textY = 10;
string info("some Text");

bm->Canvas->TextOut(10, textY, info.c_str());

以上步骤基本结束。

写入/管道代码需要一个带有位图像素的字节数组;例如

unsigned long numWritten;
WriteFile(mPipeHandle, pImage, size, &numWritten, NULL);

其中 pImage 是指向无符号字符缓冲区(位图像素)的指针,大小是此缓冲区的长度。

更新: 使用生成的 TBitmap 和 TMemoryStream 将数据传输到 ffmpeg 管道不会生成正确的结果。我得到一个扭曲的图像,上面有 3 条对角线。

我收到的相机帧缓冲区的缓冲区大小正好是 323736,等于图像中的像素数,即 658x492。 注意 我的结论是这个“位图”没有被填充。 658 不能被四整除。

不过,我将生成的位图转储到内存流后得到的缓冲区大小为 325798,比预期的大 2062 个字节。正如@Spektre 在下面指出的那样,这种差异可能是填充?

使用以下代码获取像素数组;

ByteBuffer CustomBitmap::getPixArray()
{
    // --- Local variables --- //
    unsigned int iInfoHeaderSize=0;
    unsigned int iImageSize=0;
    BITMAPINFO *pBitmapInfoHeader;

    unsigned char *pBitmapImageBits;

    // First we call GetDIBSizes() to determine the amount of
    // memory that must be allocated before calling GetDIB()
    // NB: GetDIBSizes() is a part of the VCL.
    GetDIBSizes(mTheBitmap->Handle,
                iInfoHeaderSize,
                iImageSize);

    // Next we allocate memory according to the information
    // returned by GetDIBSizes()
    pBitmapInfoHeader = new BITMAPINFO[iInfoHeaderSize];
    pBitmapImageBits = new unsigned char[iImageSize];

    // Call GetDIB() to convert a device dependent bitmap into a
    // Device Independent Bitmap (a DIB).
    // NB: GetDIB() is a part of the VCL.
    GetDIB(mTheBitmap->Handle,
            mTheBitmap->Palette,
            pBitmapInfoHeader,
            pBitmapImageBits);

    delete []pBitmapInfoHeader;

    ByteBuffer buf;
    buf.buffer = pBitmapImageBits;
    buf.size = iImageSize;
    return buf;
}

所以最后的挑战似乎是获得一个与来自相机的字节数组大小相同的字节数组。如何从 TBitmap 代码中查找和删除填充字节??

【问题讨论】:

    标签: c++ bitmap c++builder vcl


    【解决方案1】:

    TBitmap 有一个PixelFormat 属性来设置位深度。

    TBitmap 有一个HandleType 属性来控制是创建 DDB 还是 DIB。 DIB 是默认设置。

    由于您在不同系统之间传递 BMP,因此您确实应该使用 DIB 而不是 DDB,以避免像素数据的任何损坏/误解。

    另外,这行代码:

    Image1->Picture->Bitmap->Handle = bm->Handle;
    

    应该改为:

    Image1->Picture->Bitmap->Assign(bm);
    // or:
    // Image1->Picture->Bitmap = bm;
    

    或者这个:

    Image1->Picture->Assign(bm);
    

    不管怎样,之后不要忘记delete bm;,因为TPicture复制输入TBitmap,它不会获得所有权。

    要将 BMP 数据作为字节缓冲区获取,您可以使用 TBitmap::SaveToStream() 方法,将其保存到 TMemoryStream。或者,如果您只需要像素数据,而不是完整的 BMP 数据(即,没有 BMP 标头 - 请参阅 Bitmap Storage),您可以使用 Win32 GetDiBits() 函数,它以 DIB 格式输出像素。您无法获得 DDB 像素的字节缓冲区,因为它们取决于它们被渲染到的设备。 DDB 只能在内存中与HDCs 一起使用,您不能传递它们。但是,一旦有了最终设备可以将其渲染到,您就可以将 DIB 转换为 DDB。

    换句话说,从相机获取像素,将它们保存到 DIB,根据需要传递(即通过管道),然后用它做任何你需要的事情 - 保存到文件,转换为 DDB在屏幕上渲染等。

    【讨论】:

    • hmm bmDIB 是默认的吗?他们改了吗?我记得 BCB5 次,如果不将 Graphics::TBitmap->HandleType 设置为 bmDIBScanLine[] 属性会引发访问冲突......或者通过再次设置它,bmp 会以某种方式重新计算以允许 ScanLine[] 访问?
    • @RemeLebeau 感谢您的信息。我最初的问题对 DIB 和 DDB 感到困惑。我改变了,所以 TBitmap 现在是一个 DIB,我可以正确地创建和写入文本。将其保存到文件可以确认这一点。但是,将像素作为 Bytearray 似乎是个问题。当 TBitmap 由于某种原因是 DIB 时,上面的 getPixArray 代码失败!
    • @Spektre 我不知道,也许我对默认的HandleType 有误,毕竟它可能是bmDDBbmDIB 是 0,bmDDB 是 1,HandleType 上面有 nodefault。但是看看HandleType getter,我发现它实际上是根据几个标准计算出来的,所以我想它可以是任何一种方式。
    • @TotteKarlsson 使用ScanLine[y] 属性...您可以通过它访问像素以进行读写...如果您将值存储到指针数组中一次,那么它也很快见@987654326 @ 你只需要使用与你的像素格式长度相同的变量类型即可。
    【解决方案2】:

    这只是现有答案的一个插件(在 OP 编辑​​后提供附加信息)

    位图文件格式在每一行上都有对齐字节(因此每行末尾通常有一些不是像素的字节)直到某个 ByteLength(存在于 bmp 标头中)。那些会产生像线一样的倾斜和对角线。在您的情况下,大小差异是每行 4 个字节:

    (xs + align)*ys  + header = size
    (658+     4)*492 + 94     = 325798
    

    但要注意对齐大小取决于图像宽度和 bmp 标题...

    试试这个:

        // create bmp
        Graphics::TBitmap *bmp=new Graphics::TBitmap;
    //  bmp->Assign(???);       // a) copy image from ???
        bmp->SetSize(658,492);  // b) in case you use Assign do not change resolution
        bmp->HandleType=bmDIB;
        bmp->PixelFormat=pf8bit;
    //  bmp->Canvas->Draw(0,0,???); // b) copy image from ???
        // here render your text using
        bmp->Canvas->Brush->Style=bsSolid;
        bmp->Canvas->Brush->Color=clWhite;
        bmp->Canvas->Font->Color=clBlack;
        bmp->Canvas->Font->Name = "Tahoma";
        bmp->Canvas->Font->Size = 8;
        bmp->Canvas->TextOutA(5,5,"Text");
        // Byte data
        for (int y=0;y<bmp->Height;y++)
         {
         BYTE *p=(BYTE*)bmp->ScanLine[y]; // pf8bit -> BYTE*
         // here send/write/store ... bmp->Width bytes from p[]
         }
    //  Canvas->Draw(0,0,bmp);  // just renfder it on Form
        delete bmp; bmp=NULL;
    

    将像素数组访问的 GDI winapi 调用(bitblt 等...)与 VCL bmDIB 位图混合可能会导致问题和资源泄漏(因此退出时出现错误),并且它也比使用 ScanLine[] 更慢(如果编码正确)所以我强烈建议尽可能使用原生 VCL 函数(就像我在上面的示例中所做的那样)而不是 GDI/winapi 调用。

    更多信息见:

    您还提到您的图像来源是相机。如果您使用pf8bit,则意味着它的调色板索引颜色相对缓慢且丑陋,如果使用原生 GDI 算法(从真/高彩色相机图像转换)以获得更好的转换,请参阅:

    【讨论】:

    • 从相机到 AVI 的位图流(据我现在了解)是使用 DIB 的。我的这条代码链效果很好。我当前的问题是生成一个单独的自定义位图作为 AVI 中的第一帧。此位图上将有自定义文本。由于它只有一帧,因此效率并不重要。所以听起来 TBitmap 生成的位图与相机生成的位图有不同的填充,因为像素阵列对于相同的分辨率具有不同的大小?
    • @TotteKarlsson 没有样本数据我只能猜测......我敢打赌你的相机给你的是原始图像而不是位图......
    • 你说得对!我能够复制 Pixelbuffer,跳过填充字节,而且效果很好!也就是说,相机的像素是在没有填充字节的情况下传递的。
    猜你喜欢
    • 2016-01-16
    • 2011-09-25
    • 2012-12-07
    • 2021-10-20
    • 2014-03-19
    • 2014-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多