我正在使用 .NET MAUI 构建一个跨平台应用程序,需要显示大量数据。 数据以 2D float[,] 数组形式出现,每个点都有一个颜色,然后应显示在屏幕上。这就是我所追求的:
因为数据集可能非常大(例如 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 对象,但随后图像仍停留在第一列,尽管计时器按预期递增,没有错误/异常。
如有任何建议欢迎提出,谢谢。
我找到了一个可以在 Windows 和 Android 上运行的解决方案(我没有测试 iOS 和 Mac)。我在 512 行 x 55856 列的数据集上进行了测试,我可以在 7.2 秒内绘制图像。
诚然,该数据集比我在问题中指定的要小,但它是我目前可以使用的最大数据集。另外,一次性转储图像中的所有内容所需的时间为 7.2 秒,并且可以通过逐步更新图像来改善用户体验。
这是代码:
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++;
}
});
}
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;
}
我使用这个维基百科页面来学习如何正确编写位图格式。
ImageSource.FromStream(() => GenerateBitmapStreamFromPixelArray(numberOfColumns, numberOfRows, pixelArray));
请注意,我发现了另一种使用 SKBitmap、SkiaImage 和 MemoryStream 组合的解决方案;它有点慢,但需要的代码少得多,如果您的数据集不像我的数据集那么大,也许值得研究。