C#图片处理相关方法整理

本文主要是代码整理,将项目中用到的图像数据处理相关的方法进行了整理,方便以后直接拿来使用。当然,有些方法在实际使用可能需要经过调整和扩展,方可满足各种不同的应用场景。

1. byte[] 转 System.Drawing.Bitmap

代码图像信息的Byte数据转换
        /// <summary>
        /// byte带有图像信息
        /// </summary>
        /// <param name="byteImage"></param>
        /// <returns></returns>
        public static System.Drawing.Bitmap ByteToBitmap(byte[] byteImage)
        {
            using (MemoryStream stream = new MemoryStream(byteImage))
            {
                return new System.Drawing.Bitmap(stream);
                //return new Bitmap((Image)new Bitmap(stream));
            }
        }
原始ARGB格式的Byte数据
        public static Bitmap ByteToBitmap(int width, int height, byte[] imgBytes)
        {
            Bitmap bmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppRgb);
            BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height),
             ImageLockMode.WriteOnly, bmp.PixelFormat);
            //用Marshal的Copy方法,将刚才得到的内存字节数组复制到BitmapData中
            System.Runtime.InteropServices.Marshal.Copy(imgBytes, 0, bmpData.Scan0, imgBytes.Length);
            //解锁内存区域  
            bmp.UnlockBits(bmpData);
            return bmp;
        }

2. System.Drawing.Bitmap 转 byte[]

System.Drawing.Bitmap 转 byte[]
        /// <summary>
        /// 这里可自行决定生成什么格式的数据,Png,Jpg,gif等等
        /// </summary>
        /// <param name="bitmap"></param>
        /// <returns></returns>
        public static byte[] BitmapToByte(System.Drawing.Bitmap bitmap)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                bitmap.Save(stream, ImageFormat.Png);
                //bitmap.Save(stream, bitmap.RawFormat);
                byte[] byteImage = new Byte[stream.Length];
                byteImage = stream.ToArray();
                return byteImage;
            }
        }

3. 直接从图片文件读取数据

从文件读取数据
        public static byte[] GetPictureData(string imagepath)
        {
            using (FileStream fs = new FileStream(imagepath, FileMode.Open, FileAccess.Read, FileShare.Read))//可以是其他重载方法 
            {
                byte[] byData = new byte[fs.Length];
                fs.Read(byData, 0, byData.Length);
                fs.Close();
                fs.Dispose();
                return byData;
            }
        }

4.图像压缩

存在实际图片很大,传输时会耗时较长,可以适当压缩下在进行传输,只要保证显示效果即可。

压缩图像数据
        /// <summary>
        /// 压缩图像
        /// </summary>
        /// <param name="rawImgData"></param>
        /// <param name="quality">图像的压缩质量 0-100</param>
        /// <returns></returns>
        public static byte[] CompressionImage(byte[] rawImgData ,long quality)
        {
            if (rawImgData == null || rawImgData.Count() == 0)
                return rawImgData;
            using (MemoryStream stream = new MemoryStream(rawImgData))
            {
                using (var bitmap = new System.Drawing.Bitmap(stream))
                {
                    var codecInfo = GetEncoder(System.Drawing.Imaging.ImageFormat.Jpeg);
                    var myEncoder = System.Drawing.Imaging.Encoder.Quality;
                    var myEncoderParameters = new System.Drawing.Imaging.EncoderParameters(1);
                    var myEncoderParameter = new System.Drawing.Imaging.EncoderParameter(myEncoder, quality);
                    myEncoderParameters.Param[0] = myEn
                    using (var ms = new MemoryStream())
                    {
                        bitmap.Save(ms, codecInfo, myEncoderParameters);
                        myEncoderParameters.Dispose();
                        myEncoderParameter.Dispose();
                        return ms.ToArray();
                    }
                }
            } 
        }

        private static System.Drawing.Imaging.ImageCodecInfo GetEncoder(System.Drawing.Imaging.ImageFormat format)
        {
            var codecs = System.Drawing.Imaging.ImageCodecInfo.GetImageDecoders();
            return codecs.FirstOrDefault(codec => codec.FormatID == format.Guid);
        }

5.System.Windows.Media.Imaging.BitmapSource 和 System.Drawing.Bitmap 相互转换

在WPF,WinForm等开发时,处理图像会大量应用到WriteableBitmap->BitmapSource->ImageSource这三个图像相关类,当然还有BitmapImage类,在需要对图像进行加工处理时自然也需要有转换成System.Drawing.Bitmap以便进一步处理。当然

BitmapSource 转 Bitmap
        static public System.Drawing.Bitmap WriteableBitmapToBitmap(WriteableBitmap wbm)
        {
            System.Drawing.Bitmap bitmap = null;
            using (MemoryStream stream = new MemoryStream())
            {
                PngBitmapEncoder encoder = new PngBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create(wbm));
                encoder.Save(stream);
                bitmap = new System.Drawing.Bitmap(stream);
            }
            return bitmap;
        }

        /// <summary>
        /// WriteableBitmap 转 System.Drawing.Bitmap
        /// </summary>
        /// <param name="wbmp"></param>
        /// <returns></returns>
        public static System.Drawing.Bitmap BitmapSourceToBitmap(WriteableBitmap wbmp)
        {
            //using (var outBitmap = new System.Drawing.Bitmap(wbmp.PixelWidth, wbmp.PixelHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
            //{
            //    System.Drawing.Imaging.BitmapData bitmapData = outBitmap.LockBits(new System.Drawing.Rectangle(System.Drawing.Point.Empty, outBitmap.Size), System.Drawing.Imaging.ImageLockMode.WriteOnly, outBitmap.PixelFormat);
            //    wbmp.CopyPixels(Int32Rect.Empty, bitmapData.Scan0, bitmapData.Height * bitmapData.Stride, bitmapData.Stride);
            //    outBitmap.UnlockBits(bitmapData);
            //    return outBitmap;
            //}
            var outBitmap1 = BitmapSource2Bitmap(wbmp);
            return outBitmap1;
         }

        public static System.Drawing.Bitmap BitmapSource2Bitmap(BitmapSource wbmp)
        {
            // 使用BitmapSource创建一个新的System.Drawing.Bitmap
            using (MemoryStream stream = new MemoryStream())
            {
                BitmapEncoder encoder = new BmpBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create(wbmp));
                encoder.Save(stream);
                stream.Position = 0; // 重置流的位置到开始处
                System.Drawing.Bitmap drawingBmp = new System.Drawing.Bitmap(stream);
                return drawingBmp;
            }
            
        }

 public static WriteableBitmap BitmapToImageSource(System.Drawing.Bitmap bitmap)
        {
            try
            {
                IntPtr intPtr = bitmap.GetHbitmap();
                BitmapSource imageSource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(intPtr, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
                return new WriteableBitmap(imageSource);
            }
            catch(Exception ex)
            {
                Console.WriteLine($"Bitmap convertTo WriteableBitmap error:{ex.ToString()}");
                return null;
            }
        }

6.修改图片,给已有的图片添加新内容

存在一种情况,需要在已有的图片上添加一个新的内容,比如某个位置填充一个长方形,一条线条,添加文字等等。这里需要用到GDI绘图类System.Drawing.Graphics

Graphics绘制多边形曲线,长方形
        /// <summary>
        /// 绘制曲线
        /// </summary>
        /// <param name="bitmap">原有的图像</param>
        /// <param name="curve">曲线需要的点集合</param>
        /// <param name="drawColor">绘制的颜色</param>
        /// <returns></returns>
        static public System.Drawing.Bitmap DrawCurveOnBitmap(System.Drawing.Bitmap bitmap, List<System.Drawing.PointF> curve, System.Drawing.Color drawColor)
        {
            if (bitmap == null)
                return null;

            System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmap);
            System.Drawing.Pen pen = new System.Drawing.Pen(drawColor, 1);
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 抗锯齿处理
            // 创建一个GraphicsPath对象并添加曲线
            using (System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath())
            {
                path.AddCurve(curve.ToArray());
                g.DrawPath(pen, path); // 使用黑色画笔绘制路径
            }
            g.Dispose();

            return bitmap;
        }

        /// <summary>
        /// 绘制多边形
        /// </summary>
        /// <param name="bitmap"></param>
        /// <param name="curve"></param>
        /// <returns></returns>
        static public System.Drawing.Bitmap DrawCurveOnBitmap(System.Drawing.Bitmap bitmap, List<System.Drawing.PointF> curve)
        {
            if (bitmap == null)
                return null;

            System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmap);
            System.Drawing.Pen pen = new System.Drawing.Pen(System.Drawing.Color.Yellow, 1);

            g.DrawPolygon(pen, curve.ToArray());
            g.Dispose();

            return bitmap;
        }

Graphics类中提供了大量的方法,用来绘制不同的形状,点,直线,长方形,多边形,圆等等。

在图像上添加文字,合并文字到图像
        /// <summary>
        /// 在图像上添加文字,合并文字
        /// </summary>
        /// <param name="bytes"></param>
        /// <param name="TextString">需要添加的文字列表</param>
        /// <param name="PositionType">合并的位置,1=左上,2=右上,3=右下,4=左下,都是距离图像10个像素位置</param>
        /// <returns></returns>
        public static System.Drawing.Bitmap DrawTextOnBitmap(byte[] bytes, List<string> TextString,int PositionType, bool blackColor = false)
        {
            System.Drawing.Bitmap bitmap = ConvertByteToBitmap(bytes);
            if (bitmap == null)
                return null;
            //System.Drawing.Bitmap
            using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmap))
            {
                // 设置文字格式
                System.Drawing.Font font = new System.Drawing.Font("Microsoft YaHei", 14);
                System.Drawing.Brush brush = blackColor ? new System.Drawing.SolidBrush(System.Drawing.Color.Black) : new System.Drawing.SolidBrush(System.Drawing.Color.White);
                //System.Drawing.PointF point = new System.Drawing.PointF((bitmap.Width-100), (bitmap.Height - 40)); // 文字开始的位置

                if (PositionType == 3)
                {
                    TextString.Reverse();
                    //绘制多个文字
                    for (int i = 0; i < TextString.Count; i++)
                    {
                        System.Drawing.PointF point = new System.Drawing.PointF((bitmap.Width - 100), (bitmap.Height - 40 - (20 * i))); // 文字开始的位置                                   
                        g.DrawString(TextString[i], font, brush, point);// 绘制文字
                    }
                }
                else
                {
                    for (int i = 0; i < TextString.Count; i++)
                    {
                        System.Drawing.PointF point = new System.Drawing.PointF(5, 5); // 文字开始的位置                                                                               
                        g.DrawString(TextString[i], font, brush, point);// 绘制文字
                    }
                }
                
            }
            return bitmap;
        }

                                                              

7.图片裁剪

有时候我们需要对图像进行裁剪,即原有图像指定的位置开始,按照指定的宽高裁剪出一张新的图像。

裁剪图片
/// <summary>
        /// 切图
        /// </summary>
        /// <param name="bitmapSource">图源</param>
        /// <param name="cut">切割区域</param>
        /// <returns></returns>
        public static BitmapSource CutImage(BitmapSource bitmapSource, Int32Rect cut)
        {
            //计算Stride
            var stride = bitmapSource.Format.BitsPerPixel * cut.Width / 8;
            //声明字节数组
            byte[] data = new byte[cut.Height * stride];
            //调用CopyPixels
            bitmapSource.CopyPixels(cut, data, stride, 0);

            return BitmapSource.Create(cut.Width, cut.Height, 0, 0, PixelFormats.Bgr32, null, data, stride);
        }

使用示例:比如我们需要将一张已有的图片,从起始点0,0开始,按照指定的宽=100,高=50进行裁剪出新的图像,可以按照如下方式调用。

调用示例
BitmapImage bitmapImage = new BitmapImage(new Uri("pack://application:,,,/Controls;component/Icons/default/image.png替换成你的图片路径", UriKind.Absolute));
            BitmapSource imageSource= ImageHelper.CutImage(bitmapImage, new Int32Rect(0, 0, 100, 50);

这里在引深一步,应道到实际场景,当我们在UI界面上有一个随着鼠标调整来实时显示图片的一部分的场景时,就可以根据鼠标位置实时剪切,当然另外一个方法就是在图片加上一个遮盖实时调整遮盖控件。我这里以WPF中使用场景为例。假若界面有长方形控件Rectangle,这个Rectangle控件需要使用一张图片进行填充,(图片很大),Rectangle可调整大小,Rectangle调整大小时图像要实时显示对应的位置不分。
前端XMAL代码,这里关键就是CurrentBrush这个绑定

使用示例代码
<Rectangle
                        x:Name="RectangleDemo"
                        Canvas.Left="{Binding LocationX}"
                        Canvas.Top="{Binding LocationY}"
                        Width="{Binding WidthX}"
                        Height="{Binding HeightY}"
                        Fill="{Binding CurrentBrush}"
                        Opacity="0.5"
                        StrokeThickness="2"
                        Visibility="{Binding IsVirtualStentChecked, Converter={StaticResource booleanToVisibilityConverter}}" />


//后端调用示例
            CurrentBrush= new DrawingBrush();
            BitmapImage bitmapImage = new BitmapImage(new Uri("pack://application:,,,/Controls;component/Icons/default/111.png替换成你的图片", UriKind.Absolute));
            BitmapSource imageSource= ImageHelper.CutImage(bitmapImage, new Int32Rect(0, 0, (int)WidthX, (int)WidthY));
            VirtualStentBrush.Drawing= new ImageDrawing(imageSource, new Rect(new Point(0, 0), new Vector(0.5, 0.5)));

8.最后,在附带几个图像数据格式转换的函数。
图像中经常使用的数据格式byte[], short[],ushort[]

short和byte转换
        //输入byte[],返回IntPtr
        public static IntPtr ByteArrToIntPtr(byte[] array)
        {
            return Marshal.UnsafeAddrOfPinnedArrayElement(array, 0);
        }

        public static IntPtr ShortArrToIntPtr(short[] shortArr)
        {
            return Marshal.UnsafeAddrOfPinnedArrayElement(ShortArrToByteArr(shortArr), 0);
        }

        public static byte[] ShortArrToByteArr(short[] shortArr)
        {
            int shortSize = sizeof(short) * shortArr.Length;
            byte[] bytArr = new byte[shortSize];
            //申请一块非托管内存
            IntPtr ptr = Marshal.AllocHGlobal(shortSize);
            //复制shortArr数组到该内存块
            Marshal.Copy(shortArr, 0, ptr, shortArr.Length);
            //复制回bytArr数组
            Marshal.Copy(ptr, bytArr, 0, bytArr.Length);
            //释放申请的非托管内存
            Marshal.FreeHGlobal(ptr);
            return bytArr;
        }

        public static short[] ByteArrToShortArr(byte[] byteArr)
        {
            int shortSize = byteArr.Length;
            short[] shortArr = new short[shortSize / 2];
            //申请一块非托管内存
            IntPtr ptr = Marshal.AllocHGlobal(shortSize);
            //复制byte数组到该内存块
            Marshal.Copy(byteArr, 0, ptr, byteArr.Length);
            //复制回shortArr数组
            Marshal.Copy(ptr, shortArr, 0, shortArr.Length);
            //释放申请的非托管内存
            Marshal.FreeHGlobal(ptr);
            return shortArr;
        }

9.新增图片灰度化处理

此小点的内容主要来自博文https://www.cnblogs.com/huangxincheng/archive/2013/01/04/2845050.html
灰度化的RGB权重设置如下,差不多就是RGB的3:6:1的比例:
Gary(i,j)=0.299R(i,j)+0.587G(i,j)+0.114*B(i,j);

灰度化处理-原始版本每个像素都去单独处理
static void Main(string[] args)
{
    Bitmap bitmap = new Bitmap(Environment.CurrentDirectory + "//1.jpg");
    for (int i = 0; i < bitmap.Width; i++)
    {
        for (int j = 0; j < bitmap.Height; j++)
        {
            //取图片当前的像素点
            var color = bitmap.GetPixel(i, j);
            var gray = (int)(color.R * 0.299 + color.G * 0.587 + color.B * 0.114);
            //重新设置当前的像素点
            bitmap.SetPixel(i, j, Color.FromArgb(gray, gray, gray));
        }
    }
    bitmap.Save(Environment.CurrentDirectory + "//2.jpg");
}

第二版是基于字节的角度进行自考,减少循环次数,注意一个问题就是:比如图片的width=21px,一个像素点占用3个字节,但是21个像素点不一定就占用63个字节,这是因为系统基于性能考虑,在每一行中存放着一个“未用区域”,来确保图片每行的byte数是4的倍数,那么如何去读某一行的字节数呢?C#里面有一个Stride属性就可以用来获取。(这个Stride之前只知道这样用,但至于为什么终于是知道了。)

灰度化处理-第一版优化
static void Main(string[] args)
        {
            Bitmap bitmap = new Bitmap(Environment.CurrentDirectory + "//1.jpg");
 
            //定义锁定bitmap的rect的指定范围区域
            Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
            //加锁区域像素
            var bitmapData = bitmap.LockBits(rect, ImageLockMode.ReadWrite, bitmap.PixelFormat);
            //位图的首地址
            var ptr = bitmapData.Scan0;
            //stride:扫描行
            int len = bitmapData.Stride * bitmap.Height;
            var bytes = new byte[len];
            //锁定区域的像素值copy到byte数组中
            Marshal.Copy(ptr, bytes, 0, len);
            for (int i = 0; i < bitmap.Height; i++)
            {
                for (int j = 0; j < bitmap.Width * 3; j = j + 3)
                {
                    var color = bytes[i * bitmapData.Stride + j + 2] * 0.299
                                + bytes[i * bitmapData.Stride + j + 1] * 0.597
                                + bytes[i * bitmapData.Stride + j] * 0.114;
 
                    bytes[i * bitmapData.Stride + j]
                             = bytes[i * bitmapData.Stride + j + 1]
                             = bytes[i * bitmapData.Stride + j + 2] = (byte)color;
                }
            }
 
            //copy回位图
            Marshal.Copy(bytes, 0, ptr, len);
            //解锁
            bitmap.UnlockBits(bitmapData);
            bitmap.Save(Environment.CurrentDirectory + "//3.jpg");
        }

一般来说,做到上面的优化代码即可,当然,还可以用指针继续优化,指针是C#中对C++的沿用,自然就需要对C++中指针一定的了解,原理可能都知道,但很多C#程序员不见得会用以及能用好,我自己就是如此。这里的代码需要设置VS中项目属性里的 容许不安全代码选项。

灰度化处理-应用指针的计算,第二版优化
private unsafe void GrayScaleImg(Bitmap Img)
{
    int X, Y , Stride, Width, Height;
    byte* Pointer;
    byte Value;
    BitmapData BmpData = Img.LockBits(new Rectangle(0, 0, Img.Width, Img.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
    Width = BmpData.Width; Height = BmpData.Height;
    Stride = BmpData.Stride;          //  这个值并不一定就等于width*height*色深/8
    for (Y = 0; Y < Height; Y++)
    {
        Pointer = (byte*)BmpData.Scan0 + Y * Stride;                  // 必须在某个地方开启unsafe功能,其实C#中的unsafe很safe,搞的好吓人。
        for (X = 0; X < Width; X++)
        {
            Value = (byte)((*Pointer * 299 + *(Pointer + 1) * 587 + *(Pointer + 2) * 114) / 1000);        //优化算法是通过移位实现的。
            *(Pointer) = Value;
            *(Pointer + 1) = Value;
            *(Pointer + 2) = Value;
            Pointer += 3;
        }
    }
    Img.UnlockBits(BmpData);
}
posted @ 2025-04-16 11:43  奔四的大龄码农  阅读(98)  评论(0)    收藏  举报