在 .NET MAUI 中将大型数据集显示为图像的最快方法是什么?

问题描述 投票:0回答:1

我正在使用 .NET MAUI 构建一个跨平台应用程序,需要显示大量数据。 数据以 2D float[,] 数组形式出现,每个点都有一个颜色,然后应显示在屏幕上。这就是我所追求的: Example of how the data should be displayed

因为数据集可能非常大(例如 2000 行 x 1000000 列),如果我尝试简单地将其完全加载为 Image 或 GraphicView,则需要很长时间。

所以我想做的就是一次绘制一列数据。我怎样才能实现这个目标?

到目前为止,我最成功的尝试是在视图后面的 C# 代码中使用 IDispatcherTimer 计时器:

protected override void OnAppearing()
{
    base.OnAppearing();

    string pathToTheDataset = Path.Combine(appFolderPath, "large_dataset");

    IDispatcherTimer timer;
    timer = Dispatcher.CreateTimer();
    timer.Interval = TimeSpan.FromMilliseconds(100);

    int columnNumber = 0;
    timer.Tick += (s, e) =>
    {
        AppendImage(pathToTheDataset, columnNumber, numberOfRows, numberOfColumns);
        Color[] column = ExtractOneColumn(pathToTheDataset, columnNumber);

        SkiaBitmapExportContext bitmap = new SkiaBitmapExportContext(width, height, 1.0f);
        ICanvas canvas = bitmap.Canvas;
        canvas.StrokeSize = 1;

        for (int i = 0; i < column.Length; i++)
        {
            canvas.StrokeColor = column[i]
            canvas.DrawRectangle(columnNumber, i, 1, 1)
        }

        var skImage = bitmap.SKImage;
        SKData encodedData = skImage.Encode(SKEncodedImageFormat.Png, 100)

        string imagePath = Path.Combine(AppFolder, "data.png");

        var bitmapImageStream = File.Open(imagePath, FileMode.Create, FileAccess.Write, FileShare.None);

        encodedData.SaveTo(bitmapImageStream);
        bitmapImageStream.Flush(true);
        bitmapImageStream.Dispose();

        dataImage.Source = ImageSource.FromFile(imagePath);

        columnNumber++;
        if (columnNumber == numberOfColumn - 1)
        {
            timer.Stop();
        }
    };

    timer.Start();
}

此代码的灵感来自于这篇文章的答案。

计时器允许我更新视图中图像的 ImageSource (x:Name = "dataImage"),但我不明白如何将下一列添加到该图像中的前一列。

我尝试将此代码分解为多个方法,以便它仅更新 SkiaBitmapExportContext 对象,但随后图像仍停留在第一列,尽管计时器按预期递增,没有错误/异常。

如有任何建议欢迎提出,谢谢。

c# .net cross-platform maui
1个回答
0
投票

我找到了一个可以在 Windows 和 Android 上运行的解决方案(我没有测试 iOS 和 Mac)。我在 512 行 x 55856 列的数据集上进行了测试,我可以在 7.2 秒内绘制图像。

诚然,该数据集比我在问题中指定的要小,但它是我目前可以使用的最大数据集。另外,一次性转储图像中的所有内容所需的时间为 7.2 秒,并且可以通过逐步更新图像来改善用户体验。

这是代码:

  1. 将像素写入字节数组。
private async Task WritePixelsAsync()
{
    pixelArray = new byte[numberOfColumns * numberOfRows * 4]; 
   // The 4 is because ARGB is 32 bits per pixel and 1 byte = 8 bits.

    int loopIndex = 0;
    await Task.Run(() =>
    {
        for (int i = 0; i < numberOfColumns * 4; i += 4)
        {
            for (int j = 0; j < numberOfRows; j++)
            {
                // Create a color scale, in my case I want a grey scale.
                int greyScale = (int)MathF.Round(data[loopIndex, j] / 256 + 128);
                greyScale = Math.Clamp(greyScale, 0, 255);

                //BGRA order because it is a bitmap.
                pixelArray[i + (numberOfColumns * 4 * j)] = (byte)greyScale;
                pixelArray[i + 1 + (numberOfColumns  * 4 * j)] = (byte)greyScale;
                pixelArray[i + 2 + (numberOfColumns * 4 * j)] = (byte)greyScale;
                pixelArray[i + 3 + (numberOfColumns * 4 * j)] = 255;
            }
            loopIndex++;
        }
    });
}
  1. 手动创建位图并将其放入MemoryStream中。
private static MemoryStream GenerateBitmapStreamFromPixelArray(int numberOfColumns, int numberOfRows, byte[] pixelArray)
{
    int bitsPerPixel = 32; // 32 bits per pixel (ARGB)
    int rowSize = numberOfColumns * (bitsPerPixel / 8);
    int imageSize = rowSize * numberOfRows;

    byte[] headerBytes = new byte[54]; // Header size is 54 bytes for bitmap format.
    
    // Bitmap file header (14 bytes)
    headerBytes[0] = 0x42; // Signature ('B')
    headerBytes[1] = 0x4D; // Signature ('M')
    BitConverter.GetBytes(14 + 40 + imageSize).CopyTo(headerBytes, 2); // File size
    BitConverter.GetBytes(54).CopyTo(headerBytes, 10); // Offset to image data

    // Bitmap info header (40 bytes)
    BitConverter.GetBytes(40).CopyTo(headerBytes, 14); // Info header size
    BitConverter.GetBytes(numberOfColumns).CopyTo(headerBytes, 18); // Image width
    BitConverter.GetBytes(numberOfRows).CopyTo(headerBytes, 22); // Image height
    headerBytes[26] = 1; // Number of color planes
    headerBytes[28] = (byte)bitsPerPixel; // Bits per pixel
    BitConverter.GetBytes(imageSize).CopyTo(headerBytes, 34); // Image size
    BitConverter.GetBytes(resolution).CopyTo(headerBytes, 38); // Horizontal resolution (pixels per meter)
    BitConverter.GetBytes(resolution).CopyTo(headerBytes, 42); // Vertical resolution (pixels per meter)

    // Reverse the order of rows in pixelArray because bitmap format wants data as "little endian" and not "big endian".
    byte[] reversedPixelArray = new byte[pixelArray.Length];

    for (int i = 0; i < numberOfRows; i++)
    {
        int sourceIndex = (numberOfRows - 1 - i) * rowSize;
        int targetIndex = i * rowSize;
        Array.Copy(pixelArray, sourceIndex, reversedPixelArray, targetIndex, rowSize);
    }

    // Append reversed pixel data to the header
    byte[] bmpData = new byte[headerBytes.Length + reversedPixelData.Length];
    headerBytes.CopyTo(bmpData, 0);
    reversedPixelData.CopyTo(bmpData, headerBytes.Length);

    MemoryStream stream = new(bmpData);
    return stream;
}

我使用这个维基百科页面来学习如何正确编写位图格式。

  1. 从 MemoryStream 更新图像的 ImageSource。
ImageSource.FromStream(() => GenerateBitmapStreamFromPixelArray(numberOfColumns, numberOfRows, pixelArray));

请注意,我发现了另一种使用 SKBitmap、SkiaImage 和 MemoryStream 组合的解决方案;它有点慢,但需要的代码少得多,如果您的数据集不像我的数据集那么大,也许值得研究。

© www.soinside.com 2019 - 2024. All rights reserved.