前往我的主页以获得更好的阅读体验C#图像压缩相关方法总结 - DearXuan的主页https://blog.dearxuan.com/2022/02/07/C-%E5%9B%BE%E5%83%8F%E5%8E%8B%E7%BC%A9%E7%9B%B8%E5%85%B3%E6%96%B9%E6%B3%95%E6%80%BB%E7%BB%93/

前言

本文所描述的所有内容和算法,均未使用任何外部库,且已经在开源压缩软件PicSizer中使用

PicSizer是我独立编写的批量图片压缩软件,主要功能是实现网页图片的压缩。因此所有的算法都是优先考虑网页显示的。如果你对图片压缩感兴趣,可以前往Gitee查看源码,或者点击这里下载最新发行版。软件完全开源,大小仅不到 1 MB,可放心使用,删除后不会有残留。

线程管理

本节需要的命名空间:

using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;

多线程是充分利用CPU的一种方法,但是如果线程数量超出了CPU的逻辑处理器数量,就会适得其反。且大量的图形计算和IO操作也会导致程序卡顿,因此在PicSizer我选择了默认2个线程,最多10个线程

在使用C#自带的ThreadPool时,我发现即使就开一个线程,也会有严重的卡顿,因此我采用自己实现的线程池

线程池

实现线程池的具体思路是:先创建指定数量的线程,然后通过死循环不断地从一个数组中读取图片进行压缩,直到结束。

该过程非常简单,下面给出代码

//开始压缩
for (int i = 0; i < 10; i++)
{//创建一个高优先级线程并立即执行Thread thread = new Thread(() =>{//压缩图片的代码}){Priority = ThreadPriority.Highest};//线程启动thread.Start();
}
//压缩完毕
//其它代码

当压缩结束后,应当做一些“善后”工作,而实际情况是,10个线程刚创建玩,函数就结束了,为了让函数能够等待这10个压缩线程,我们可以使用WaitHandle,它通过创建独占资源来避免同时访问,这里我们可以利用它的“忙则等待”特性,在子线程中独占某个资源,结束后释放这些资源,而主线程就会因为资源被其它线程占用而进入等待,直到全部子线程都结束才能继续运行

private static List<WaitHandle> waitHandles = new List<WaitHandle>();public static void StartThreadsPool()
{//清空所有独占资源waitHandles.Clear();//创建10个子线程for (int i = 0; i < 10; i++){//创建一个独占资源ManualResetEvent manual = new ManualResetEvent(false);//添加到数组中waitHandles.Add(manual);//创建一个新线程Thread thread = new Thread(() =>{//将独占资源传递给一个子线程DoInThread(manual);}){Priority = ThreadPriority.Normal};thread.Start();}//等待数组中的全部资源都被释放才继续执行WaitHandle.WaitAll(waitHandles.ToArray());//善后工作//......
}public static void DoInThread(ManualResetEvent manualResetEvent)
{int index;//获取下一站图片的序号,如果是-1则表示没有图片了while ((index = GetNext()) != -1){//压缩图片}//循环结束,释放资源manualResetEvent.Set();return;
}

线程同步

当两个线程对同一个资源进行“写”操作时,就需要考虑到线程同步问题。本文中,我们希望10个线程共用一个函数来获取下一张图片在数组里的下标,这里显然用到了“写”操作,因此需要用到线程同步,即每次仅允许一个线程访问

C#的实现方式非常简单,只需要在函数上面加上一句就行

[MethodImpl(MethodImplOptions.Synchronized)]
public static int GetIndex()
{//获取下标
}

图片读写

本节需要的命名空间:

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

从文件读取

Bitmap bitmap = new Bitmap("文件路径");

写入到硬盘

bitmap.Save("导出路径", imageFormat);

其中imageFormat是输出的格式,注意该格式并不等同于后缀,一个“*.png”文件不一定就是PNG图片

imageFormat有多种选择,如果你想要导出BMP图片,则可以这样写

bitmap.Save(path, ImageFormat.Bmp);

内存流读写

如果想要获取输出之后的文件大小,你可以直接把Bitmap保存到磁盘里,然后读取。但是在接下来的算法里,需要大量输出文件,并且这些文件都是一次性的,频繁读写硬盘会造成硬盘寿命降低,同时效率也非常低。我们可以在内存中模拟输出文件,然后读取内存中的文件大小。

//创建一个内存流
MemoryStream memoryStream = new MemoryStream();
//把Bitmap写入到内存
bitmap.Save(memoryStream, imageFormat);
//摧毁内存流
memoryStream.Dispose();

现在我们可以定义一个函数,用它来计算Bitmap以指定格式输出到内存中的大小

public static long LengthOfBitmapInMemory(Bitmap bitmap, ImageFormat imageFormat)
{MemoryStream memoryStream = null;try{memoryStream = new MemoryStream();bitmap.Save(memoryStream, imageFormat);return memoryStream.Length >> 10;//此处的位移仅用于单位换算,可以去掉}finally{//及时摧毁内存流memoryStream?.Dispose();}
}

ICON文件结构

对于ICON的详细物理结构,可以前往微软文档查看

ICON文件主要分为:标头、数据段,像素段

标头保存了该文件的基本信息,例如文件类型、包含的图标数量(ICON里可以保存多个图标)

每个数据段都对应了一个图标,它保存着图标相关信息,例如尺寸、色域、像素的偏移

像素段保存着每个图标的具体像素值

C#自带的Icon类并不能保存到硬盘,我们需要自己按位写入,下面给出另存为Ico的代码

private static void SaveAsIcon(Bitmap bitmap, string path, byte size)
{Image image = null;FileStream fileStream = null;BinaryWriter writer = null;try{image = new Bitmap(bitmap, size, size);fileStream = new FileStream(path, FileMode.Create);writer = new BinaryWriter(fileStream);//ICON文件标头(0x0)writer.Write((short)0);//预留位,必须为0writer.Write((short)1);//资源类型(1表示ICON)writer.Write((short)1);//该文件里有几个资源//ICON文件数据段(0x6)writer.Write((byte)size);//宽度,偏移0x6writer.Write((byte)size);//高度,偏移0x7writer.Write((byte)0);//像素位数(0表示 >=8bpp)writer.Write((byte)0);//预留位,必须为0writer.Write((short)0);//色彩画板(我也不知道啥用)writer.Write((short)32);//位深度,32位颜色writer.Write((int)0);//像素段长度,目前还不知道具体长度,先用0代替writer.Write((int)0x16);//该数据段对应的像素段偏移,由于共一张图片,所以偏移一定是0x16//ICON文件像素段(偏移0x16)image.Save(fileStream, ImageFormat.Png);//现在知道了像素段的长度,所以控制指针往回移动,再次写入writer.Seek(0xE, SeekOrigin.Begin);//像素段长度是目前整个文件流的长度减去标头和数据段的长度,即 Length-22writer.Write((int)fileStream.Length - 22);}finally{writer?.Dispose();fileStream?.Dispose();image?.Dispose();}
}

考虑到写入的数据大部分都是固定的,所以我把文件标头和数据段保存为一个byte数组,下次只需要先写入这个数组,然后通过偏移修改相关字段的数据就可以了

//标头和数据段数组
private static readonly byte[] _ICON_HEADER = new byte[] { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 22, 0, 0, 0 };private static void SaveAsIcon(Bitmap bitmap, string path, byte size)
{Image image = null;FileStream fileStream = null;BinaryWriter writer = null;try{image = new Bitmap(bitmap, size, size);fileStream = new FileStream(path, FileMode.CreateNew);writer = new BinaryWriter(fileStream);//写入标头byte数组writer.Write(_ICON_HEADER);//写入像素段image.Save(fileStream, ImageFormat.Png);//偏移0x6处为图片宽度writer.Seek(0x6, SeekOrigin.Begin);writer.Write(size);//偏移0x7处为图片高度writer.Seek(0x7, SeekOrigin.Begin);writer.Write(size);//偏移0xE处为图片主体部分长度writer.Seek(0xE, SeekOrigin.Begin);writer.Write((int)fileStream.Length - 22);}finally{writer?.Dispose();fileStream?.Dispose();image?.Dispose();}
}

图像预处理

本节需要的命名空间:

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

缩放

Bitmap的缩放有两种方式,最简单的方法仅需要一行代码

Bitmap bitmap = new Bitmap(oldBitmap, width, height);

缩放本身并不难,但是在实践中,我们通常不希望图片尺寸过大,也不希望过小,因为浏览器会自动放大尺寸较小的图片,造成模糊。因此我们可以设置一个基准尺寸,如果图片比它大,就缩放到和它相同的大小,否则不缩放

int LimitWidth = 1920;
int LimitHeight = 1080;public static Bitmap Scale(Bitmap bitmap)
{int width = bitmap.Width;int height = bitmap.Height;//求出比值float widthByMin = (float)width / LimitWidth;float heightByMin = (float)height / LimitHeight;//求出较小者float min = Math.Min(widthByMin, heightByMin);//如果较小者大于1,则说明图片尺寸超过限制if(min > 1){//按照较小者来放缩,这样可以保证长和宽中有一个恰好是限制值,另一个略大于限制值width = (int)(width / min);height = (int)(height / min);return new Bitmap(bitmap, width, height);}//图片没有被缩放,返回原图return bitmap;
}

居中裁剪

假设图片原本的尺寸是 500×600,我们想要把他裁剪成 1000×1000的大小,则第一步应该先得到图片的裁剪区尺寸,即 500×500,然后将图片裁剪为 500×500 的大小,最后放大到 1000×1000

首先应求出限制尺寸需要被缩放的比值,这个比值实际上就是上一个代码块里的min,这里不再重复叙述

第二部是将Bitmap和比值传递到一个函数里,进行裁剪

private static Bitmap CenterCutBitmap(Bitmap bitmap, float scale)
{//将限制尺寸乘上比值,就可以得到Bitmap的裁剪区尺寸//width和height是bitmap上的需要裁剪的区域的宽和高int final_width = (int)(LimitWidth * scale);int final_height = (int)(LimitHeight * scale);//bitmap的裁剪区域左上角位置int left = (bitmap.Width - final_width) / 2;int top = (bitmap.Height - final_height) / 2;//创建一个新Bitmap,用于保存裁剪后的图片Bitmap newBitmap = new Bitmap(LimitWidth, LimitHeight, PixelFormat.Format24bppRgb);//在新的Bitmap上绘图Graphics g = Graphics.FromImage(newBitmap);//使用最高画笔品质g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;g.DrawImage(bitmap,//该参数是在新Bitmap上绘图的尺寸,应当填满整个newBitmapnew Rectangle(0, 0, LimitWidth, LimitHeight),//该参数是老Bitmap上取色的尺寸,应当只截取中间部分new Rectangle(left, top, final_width, final_height),GraphicsUnit.Pixel);g.Dispose();bitmap.Dispose();return newBitmap;
}

压缩方法

本节需要的命名空间:

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

画质压缩

对于JPEG图片,我们可以调节它的画质,更低的画质意味着更小的体积

首先应获取编码参数

//获取JPEG的编解码器
public static ImageCodecInfo _Info_JPEG = Encoder.GetEncoderInfo("image/jpeg");public static System.Drawing.Imaging.Encoder encoder = System.Drawing.Imaging.Encoder.Quality;
public static EncoderParameter[] parameterList = new EncoderParameter[101];//该方法根据指定的画质返回编码信息数组,这个数组在压缩JPEG时需要用到
public static EncoderParameters GetEncoderParameters(long value)
{EncoderParameters encoderParameters = new EncoderParameters(1);encoderParameters.Param[0] = GetParameter(value);return encoderParameters;
}//该方法根据参数返回包含指定画质的编码信息,value的范围是: [0,100]
public static EncoderParameter GetParameter(long value)
{int v = (int)value;//为了提高性能,可以将使用过的编码信息保存起来,仅当数组中没有时才重新获取if (parameterList[v] == null){parameterList[v] = new EncoderParameter(encoder, value);}return parameterList[v];
}//获取图像编解码器
public static ImageCodecInfo GetEncoderInfo(string type)
{int j;ImageCodecInfo[] encoders;encoders = ImageCodecInfo.GetImageEncoders();for (j = 0; j < encoders.Length; ++j){if (encoders[j].MimeType == type){return encoders[j];}}return null;
}

现在我们就可以使用这个编码信息来压缩JPEG图像

public static void CompressionByValue(string file)
{Bitmap bitmap = null;try{bitmap = new Bitmap(file);//创建一个编码信息数组并作为参数传入EncoderParameters encoderParameters = new EncoderParameters(1);//获取画质为50时候的编码信息encoderParameters.Param[0] = GetParameter(50L);//保存到硬盘bitmap.Save("保存路径", _Info_JPEG, encoderParameters);}finally{bitmap?.Dispose();}
}

位深度压缩

对于非JPEG类型的图片,由于其本身并没有提供可修改的参数,所以无法通过画质来减小体积,这时我们可以通过减少色域的方式

在C#中表示像素格式的类是PixelFormat,下面是4个常见的像素格式

public static PixelFormat[] pixelFormats = new PixelFormat[]
{PixelFormat.Format8bppIndexed,PixelFormat.Format16bppArgb1555,PixelFormat.Format32bppArgb,PixelFormat.Format64bppArgb
};

位深度越低,意味着储存一个像素所需的字节越少,文件体积也就越小。但是储存像素的字节少了,一个像素点能够表示的颜色范围就变少了,可能造成部分颜色显示异常,修改位深度非常简单,只需要一行代码

//用指定的位深度复制Bitmap
Bitmap newBitmap = oldBitmap.Clone(new Rectangle(oldBitmap.Width, oldBitmap.Height), pixelFormat);

该方法对所有图片均有效

缩放压缩

在浏览器中,我们可以通过适当地修改html标签来让图片显示为指定的尺寸,如果图片较小或较大,浏览器会自动为我们缩放。因此我们可以通过减小图片的尺寸来较小体积,而不必考虑它的实际显示效果

这种方法唯一的缺点就是放大后的图片会变模糊,但是比起位深度压缩带来的颜色异常,这种损失是可以接受的

压缩至指定大小

严格的说,压缩到指定的大小几乎是不可能的,我们所能做到的是压缩到不超过指定大小的最佳情况,对于画质压缩,位深度压缩,缩放压缩,都可以通过调节参数使其

以画质压缩为例,画质可被分为101个等级(0~100),首先创建一个数组,用于储存各个画质下的文件大小

long[] sizeList = new long[101];

通过常识可知文件大小和画质是呈正比的,所以我们可以通过二分查找的方式,来快速找到不超过给定大小的最高画质

//限定最大体积为1024KB
long LimitSize = 1024;//使用二分查找的方式获取不超过给定值的最大画质
private static bool Compress(string file)
{using (Bitmap bitmap = new Bitmap(file)){long left = 0L, right = 100L, mid = 0L;long[] sizeList = new long[101];//进入二分查找while (left < right - 1){//计算中间值mid = (left + right) / 2;//求出mid对应的文件体积sizeList[mid] = GetBitmapSize(bitmap, mid);//即使当前体积已经符合要求了,仍然要继续查找,因为目标是找到符合要求的最高画质if (sizeList[mid] <= LimitSize){left = mid;}else{right = mid;}}//此时left就是所能选到的最高画质if (sizeList[left] == 0){sizeList[left] = GetBitmapSize(bitmap, left);}//left对应的文件体积仍然可能超出限制,因此要加一个判断if (sizeList[left] <= LimitSize){bitmap.Save("保存路径");return true;}else{return false;}}
}

这里只给出了按画质压缩的例子,实际上对于另外两种压缩方式也是适用的。对于位深度压缩,可以将不同的像素格式列为一个数组进行查找;对于缩放压缩,可以调整缩放比为 0.01~1.00来进行查找

C#图像压缩相关方法总结相关推荐

  1. Android-----View绘制流程以及invalidate()等相关方法分析 .

    引自:http://blog.csdn.net/qinjuning/article/details/7110211 前言: 本文是我读<Android内核剖析>第13章----View工作 ...

  2. 采用编码器-解码器匹配语义分割的图像压缩

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 小白导读 论文是学术研究的精华和未来发展的明灯.小白决心每天为大家 ...

  3. Thread和Object类中关于线程的相关方法

    Thread和Object类中线程的7个重要方法概览 类 方法名称 简介 Thread sleep相关 相关表示sleep所有的重载方法,参数不同,实际作用大同小异 Thread join() 等待其 ...

  4. Google发布新的图像压缩技术,最高可节省75%带宽

    Google发布新的图像压缩技术,最高可节省75%带宽 在社交网络上,每天都有难以计数的图片被人们分享.存储.但有一个现实的问题是,大量的照片由于网络限制被人为压缩降低了画质.而Apple在2010年 ...

  5. Android中View绘制流程以及invalidate()等相关方法分析

                                                                                                        ...

  6. UIView 的布局与绘制显示相关方法调用时机

    UIView是我们经常使用的一个基本控件,不知道你是否对以下方法在什么时候调用感到疑惑?虽然他们和布局都有关系. setNeedLayout layoutSubViews layoutIfNeed 另 ...

  7. CVPR 2021 双目图像压缩最新进展

    CVPR 2021 图像压缩最新进展 有预训练: 首先分享的主题为:图像处理 (实际内容为图像压缩) 两位讲者分别是来自北航的邓欣和鹏城实验室的柏园超. 论文一 论文名称:Deep Homograph ...

  8. Python二维离散余弦变换图像压缩应用(基于OpenCV)

    参考:https://blog.csdn.net/James_Ray_Murphy/article/details/79163705 离散余弦变换的特征是将物体信息能量汇聚到低频成分,高频成分接近0, ...

  9. ArcGIS Engine中空间参照(地理坐标)相关方法总结

    转自原文 ArcGIS Engine中空间参照(地理坐标)相关方法总结 1.创建空间参考 /// <summary> /// 根据prj文件创建空间参考 /// </summary& ...

  10. jQuery 1.6 源码学习(六)——core.js[6]之jQuery对象/数组操作相关方法

    本文将分析jQuery对象操作相关方法(包括静态和实例方法): merge方法,代码如下: //此方法用于合并两个jQuery对象(因为jQuery对象中有length属性)或者数组,//这个方法非常 ...

最新文章

  1. 方差分析(Analysis of Variance,ANOVA)是什么?方差分析的形式有哪些?
  2. 题目1132:与7无关的数
  3. CTF的一道安卓逆向
  4. 面试离职理由_面试一个月都没有成功,是种什么样的神奇体验?
  5. ASP.NET Web API身份验证和授权
  6. mysql 内置功能 存储过程介绍
  7. 计算机选修课学什么,计算机专业都学什么 主要课程有什么
  8. [Git]Git远程仓库
  9. vnpy策略回测如何设置滑点手续费和size
  10. Oracle之唯一性约束(UNIQUEConstraint)用法详解
  11. http状态码 以及请求响应头相关
  12. PureLayout,使用纯代码写AutoLayout
  13. FFMPEG音视频解码流程MP4音视频文件流读取(转)
  14. 城市生活污水处理技术现状及活性炭的应用
  15. python获取word页数_用程序获取word页码方法汇总
  16. power_supply子系统
  17. 【天光学术】古代文学论文该怎么写?为你步步解说!
  18. StatusBarUtil 状态栏工具类
  19. Matlab信号添加噪声及信噪比SNR的计算
  20. 企业舆情监测系统工作流程

热门文章

  1. Style之flag标签
  2. 1 三角函数的正交性
  3. [Win] 免登录百度网盘高速下载器 ENFI下载器 v1.3.1
  4. python离线语音转文字
  5. Spring JdbcTemplate 多参数查询,以及like模糊查询处理方式
  6. 计算机word设置斜框线,Word绘制多线斜线表头技巧-word技巧-电脑技巧收藏家
  7. linux 查找mysql rpm包位置_linux如何查看rpm包的安装路径办法
  8. ZDM按横断面水位线河道开挖
  9. IEEEtran模板使用Bibtex插入DOI不显示问题
  10. BT文件分享服务器,bt是什么意思服务(bt资源库)