32-bit BMP转PNG之C#版

.Net库中默认的Bitmap类是不支持读取32-bit BMP的, 注意是不支持直接读, 本身是可以处理ARGB结构的. 既然这样那就自己一个个字节读吧, 反正BMP又不需要解码一个一个像素挨个读取便是.

不过读取了每个像素的各个分量值后如果挨个setPixel()的效率是不可忍受的. 且不说双层循环本来效率就很差, setPixel()本来就不快. 还好微软自己也觉得这速度太不靠谱, 留下了用指针直接操作的方法.

主要代码如下:

using (FileStream fs = new FileStream(file, FileMode.Open))
{
    //判断像素深度, 仅对32-bit BMP操作
    fs.Position = 0x1C;
    byte[] byteDepth = new byte[4];
    fs.Read(byteDepth, 0, 4);
    int depth = BitConverter.ToInt32(byteDepth, 0);
    if (depth != 32)
    {
        Console.Write("Not 32-bit bmp file.n");
        continue;
    }
    else
    {
        DateTime dt = DateTime.Now;
    
        //Pixel数据区开始位置
        fs.Position = 0xA;
        byte[] bytePixelArrayStart = new byte[4];
        fs.Read(bytePixelArrayStart, 0, 4);
        int posPixelArrayStart = BitConverter.ToInt32(bytePixelArrayStart, 0);
    
        //图片宽度
        fs.Position = 0x12;
        byte[] byteWidth = new byte[4];
        fs.Read(byteWidth, 0, 4);
        int width = BitConverter.ToInt32(byteWidth, 0);
    
        //图片高度
        fs.Position = 0x16;
        byte[] byteHeight = new byte[4];
        fs.Read(byteHeight, 0, 4);
        int height = BitConverter.ToInt32(byteHeight, 0);

        fs.Position = posPixelArrayStart;
        //建立一个与原图等大的Bitmap对象, 设置像素格式为32-bitARGB
        using (Bitmap bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb))
        {
            //获取该Bitmap对象的BitmapData数据
            BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, bmp.PixelFormat);
            //获取数据开始的指针
            IntPtr ptr = bmpData.Scan0;
            //计算数据区的长度
            int imgLen = Math.Abs(bmpData.Stride) * bmp.Height;
            byte[] pixels = new byte[imgLen];
            
            //逐个Byte读取值并写入对应像素的各个通道
            for (int counter = 0; counter < pixels.Length; counter++)
            {
                byte[] color = new byte[1];
                fs.Read(color, 0, 1);
                pixels[counter] = color[0];
            }
            
            //直接将新的Pixel数据复制入BitmapData对象, 覆盖原有数据
            System.Runtime.InteropServices.Marshal.Copy(pixels, 0, ptr, imgLen);
            bmp.UnlockBits(bmpData);
    
            //这样生成的图时y方向上下颠倒的, 需要做一次翻转
            //这样的翻转不会导致图片质量的损失
            bmp.RotateFlip(RotateFlipType.RotateNoneFlipY);
    
            //转换成PNG格式并写入新文件
            string pngFile = string.Format(@"{0}{1}.png", Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file));
            bmp.Save(pngFile, ImageFormat.Png);
    
            Console.Write("Used: {0}ms.n", (DateTime.Now - dt).TotalMilliseconds);
        }
    }
}

测试表明使用指针直接操作Pixel数据后速度有显著提升.

另有个小工具叫AlphaConv也可以实现32-bit BMP和PNG互转, Delphi写的, 调的libpng, 效率应该不错, 可惜只有图形界面.

有空来试试直接用C调libpng来实现一下, 看能比.Net快多少><.

Advertisement

C#控制台输出实时重定向

.Net Framework中的Process类和ProcessStartInfo类可以方便地控制进程的启动, 还可以方便地将控制台程序的输出重定向到Stream中, 然后显示到TextBox中或者记录到文件中. 不过要实时显示每一行的输出还得用点技巧, 方法如下.

启动进程前的准备.

//设置待启动程序的路径和要传递的参数
ProcessStartInfo info = new ProcessStartInfo(path, args));

//将标准输出流重定向, 需同时把UseShellExecute设置为false
info.RedirectStandardOutput = true;

//不使用操作系统外壳程序启动
info.UseShellExecute = false;

//不创建窗体
info.CreateNoWindow = true;

为了做到实时捕获, 需在进程结束前一直搜索输出流, 如有新的一行便读取并引发一个事件以让主程序窗体能够响应并将这行添加至TextBox.

if (process.Start())
{
    while (!process.HasExited)
    {
        string line = process.StandardOutput.ReadLine();
        if (line != null)
            OutputLineGet(line);
    }
    process.WaitForExit();
}

为了不让窗体假死, 将调用上述循环放至另一个的单独线程中.

public void Start()
{
    new System.Threading.Thread(new System.Threading.ThreadStart(this.StartConsoleProgram)).Start();
}


然后便是窗体响应该事件, 注意到是跨线程调用GUI, 需用委托.

public MainForm()
{
  InitializeComponent();

  //注册事件
  this.OutputLineGet += new OnOutputLineGetDelegate(MainForm_OutputLineGet);
}

public delegate void OnOutputLineGetDelegate(string line);
public event OnOutputLineGetDelegate OutputLineGet;

private void MainForm_OutputLineGet(string line)
{
    if (this.InvokeRequired)
    {
        this.Invoke(new OnOutputLineGetDelegate(MainForm_OutputLineGet), line);
    }
    else
    {
        txtLog.AppendText(line);

        //添加换行符
        txtLog.AppendText("rn");
    }
}

这样便能一行一行实时捕获输出了, 而且主程序也可以响应窗体消息不至于假死.

C++判断输入结束的讨论

先看2段代码:

[cpp title=”Method 1″]

int a;
while(cin>>a)
{
cout<<a<<endl;
}

[/cpp]

[cpp title=”Method 2″]

int a;
while(cin.good())
{
cin>>a;
cout<<a<<endl;
}

[/cpp]

乍一看, 两者似乎都能判断出输入结束, 然而, 当输入出现错误时, 看下列例子:

input:
1 2 a
output 1:
1
2
output 2:
1
2
2

第二种方法却多了一个输出. 仔细观察, 发现两者从输入流读取和判断结束的顺序存在不同:

Method 1:
从输入流读取->判断流的状态->输出->…
Method 2:
判断流的状态->从输入流读取->输出->…

当循环进行到第三次时, Method 1先进行读取, 发现’a’不满足int, 于是返回0, 跳出循环, 故有2次输出. 而Method 2 先进行判断, 由于此时刚刚成功读完第二个数据, 第三个还未读, 流的状态cin.good()为true, 故进入第三个循环, 尝试读入一个数, 发现’a’不满足int, 于是a的值不会被覆盖, a的值仍为2, 故又输出一个2. 之后由于刚才读取失败置cin.good()为false, 跳出循环.
可见, 用cin.good()判断输入结束是可能存在问题的, 需作如下修改:

[cpp title=”Method 3″]
int a;
cin>>a;
while(cin.good())
{
cout<<a<<endl;
}

[/cpp]

此时先进行读取再判断流的状态, 可以正常判断输入结束.

另外, 参考http://www.cplusplus.com的解释, 如下情况时cin.good()会被置false:

“The function returns true if none of the stream’s error flags (eofbit, failbit and badbit) are set.”

即eofbit, failbit, badbit中任何一个被触发即置cin.good()为false.
注意到cin.bad()并不是cin.good()的反面, 它只对应badbit.
关于这三个标志何时被触发有下表:

flag error
eofbit The end of the source of characters is reached during its operations.
failbit The input obtained could not be interpreted as an element of the appropriate type.

Notice that some eofbit cases will also set failbit.

badbit An error other than the above happened.

输入结束将导致eofbit和failbit, 输入类型不符将导致failbit, 其它将导致badbit.

吐槽: 这货害我在ZOJ上浪费了不下30次submit…