CSV文件转换为位图

3

我有一个填满数据的大型 CSV 文件,每一行前三个值都是无用的,而剩下的数值代表一条线上的像素点,多行数据组合成了一张图片。

例如: X,X,X,1,2,3,4,5 X,X,X,6,7,8,9,10 X,X,X,11,12,13,14,15

我想将它重构为灰度位图,以便在 picturebox 中显示并导出(还要进行许多其他调整,这里不再赘述)。

我已经苦苦挣扎了两天也没能解决这个问题。我尝试直接将 CSV 解析到 Image.SetPixel 中,但是遇到了无数问题。我尝试将 CSV 加载到二维数组中,但是数组长度获取和越界问题让我陷入了困境,最终放弃了这两次尝试,因为尝试修复问题只会让代码变得混乱。

有没有人有解决此类问题的经验?之前做过吗?有什么建议或代码片段吗?非常感谢,毫无疑问,我在这方面并不擅长。


最好将示例 CSV 文件附加到问题中。 - Evk
这是一个非常大的文件,我没有看到上传文件的地方。 - user3599976
这个例子在我看来已经足够好了。它展示了数据的格式,并且实际上可以用作5x3的图像。 - Nyerguds
我曾尝试直接将CSV解析到Image.SetPixel中,但遇到了无数问题。那么问题是什么呢?这是第一步操作,有点慢,但可以获得结果以检查数据。稍后你可以转到Lockbits以提高速度。当然,你必须正确解析并知道这些数字的含义! - TaW
1
@TaW 是的,缺乏任何代码有点不可靠,但无论如何我都会用8位来做这个,这就是为什么我已经回答了。虽然我能看到的唯一明显的问题是如果某些行是不完整的...否则第一行的分割结果应该给出图像宽度,而行数显然是高度... - Nyerguds
1个回答

1
这似乎是与8位图像有关的事情。8位图像的优点在于,您只需要具有一个一维字节数组和您的值,就可以将其直接加载到图像中,使用LockBitsMarshal.Copy 嗯,几乎是直接的。在8位图像上,您的像素值实际上并不是您的颜色,它们是对颜色调色板的引用,实际上包含您的颜色。但是,如果您希望图像上的0到255的值表示颜色(0,0,0)到(255,255,255),那么您需要做的就是生成一个具有这256个灰色所需颜色的调色板,这可以在一个简单的for循环中处理。
首先,处理CSV文件。如果这只是简单的“数字,逗号,数字”信息,您可以使用String.Split函数,但如果要处理包含引号和/或分割字符的引用块的特殊情况,则需要使用更高级/可靠的CSV解析器TextFieldParser。有关更多信息,请单击此处,但我认为对于这个问题,我们可以采用String.Split方法。
我在这里创建了一个名为startColumn的变量,以方便您的使用,但在您的情况下,它当然将是“3”。
public static Bitmap GrayImageFromCsv(String[] lines, Int32 startColumn, Int32 maxValue)
{
    // maxValue cannot exceed 255
    maxValue = Math.Min(maxValue, 255);
    // Read lines; this gives us the data, and the height.
    //String[] lines = File.ReadAllLines(path);
    if (lines == null || lines.Length == 0)
        return null;
    Int32 bottom = lines.Length;
    // Trim any empty lines from the start and end.
    while (bottom > 0 && lines[bottom - 1].Trim().Length == 0)
        bottom--;
    if (bottom == 0)
        return null;
    Int32 top = 0;
    while (top < bottom && lines[top].Trim().Length == 0)
        top++;
    Int32 height = bottom - top;
    // This removes the top-bottom stuff; the new array is compact.
    String[][] values = new String[height][];
    for (Int32 i = top; i < bottom; i++)
        values[i - top] = lines[i].Split(',');
    // Find width: maximum csv line length minus the amount of columns to skip.
    Int32 width = values.Max(line => line.Length) - startColumn;
    if (width <= 0)
        return null;
    // Create the array. Since it's 8-bit, this is one byte per pixel.
    Byte[] imageArray = new Byte[width*height];
    // Parse all values into the array
    // Y = lines, X = csv values
    for (Int32 y = 0; y < height; y++)
    {
        Int32 offset = y*width;
        // Skip indices before "startColumn". Target offset starts from the start of the line anyway.
        for (Int32 x = startColumn; x < values[y].Length; x++)
        {
            Int32 val;
            // Don't know if Trim is needed here. Depends on the file.
            if (Int32.TryParse(values[y][x].Trim(), out val))
                imageArray[offset] = (Byte) Math.Max(0, Math.Min(val, maxValue));
            offset++;
        }
    }
    // generate gray palette for the given range, by calculating the factor to multiply by.
    Double mulFactor = 255d / maxValue;
    Color[] palette = new Color[maxValue + 1];
    for (Int32 i = 0; i <= maxValue; i++)
    {
        // Away from zero rounding: 2.4 => 2 ; 2.5 => 3
        Byte g = (Byte)Math.Round(i * mulFactor, MidpointRounding.AwayFromZero);
        palette[i] = Color.FromArgb(g, g, g);
    }
    // Since the palette is incomplete, give the color fill arg as Color.White
    return BuildImage(imageArray, width, height, width, PixelFormat.Format8bppIndexed, palette, Color.White);
}

Called as:

String[] lines = File.ReadAllLines(path);
using (Bitmap img = GrayImageFromCsv(lines, 3, 15))
{
    // null = conversion failed. Could log/show warning.
    if (img != null)
        img.Save("fromcsv.png", ImageFormat.Png);
}

主处理结束时调用的BuildImage函数执行了上述“将字节数组加载到图像中”的操作。您可以在这里找到它。请注意,“stride”是图像一行上的字节数量。虽然对于8位图像,这与宽度相同,因为每个字节是一个像素,但对于其他格式,它将有所不同。

在网上查看,似乎只需要使用'System.Drawing'和'System.Drawing.Imaging'这两个库,而我已经拥有它们,所以我很困惑为什么它会说在当前上下文中不存在。 - user3599976
@user3599976 请阅读最后一段。我已经链接到了该函数(现在我对链接进行了一些编辑,以使其更加显眼)。在我的个人代码库中,它位于我的静态工具类“ImageUtils”中,但是显然您可以将其放在任何您想要的地方,并更改/删除那个“ImageUtils”部分。 - Nyerguds
非常感谢,经过修改以适应我的数据范围并添加手动偏移和乘数后,它的表现非常出色! - user3599976
我其实已经想通了,是我的大脑有些不太灵光,0x100 = 255,但还是非常感谢你提供这个更简洁的版本,你真的帮了我很多。 - user3599976
啊,关于从流中加载图像存在一些奇怪的问题。具体来说,您需要保持流处于打开状态,否则图像将会损坏。我之前在这里发布了有关此问题的完整清单笔记。 要想正确加载图像而没有任何限制,您应该在memstream和从该流中加载的图像的using块内加载它,就像那里展示的那样,然后以某种方式在其中创建一个真正干净的图像克隆。我通常使用我的可靠CloneImage函数 来实现。 - Nyerguds
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接