Bitmap 图像灰度变换原理浅析
上篇文章《拥抱 C/C++ : Android JNI 的使用》里提到调用 native 方法直接修改 bitmap 像素缓冲区,从而实现将彩色图片显示为灰度图片的方法。这篇文章将介绍该操作的实现原理。
开始先不讲关于 Bitmap 的相关细节,先从计算机底层存储与运算原理讲起。总所周知,计算机只识别 0 和 1,无论是八进制、十进制、十六进制,在底层都会被转换为二进制。有几个单位与概念要提及一下:
计量单位
bit(位)
计算机表示信息的最小单位,也是最小的存储单位,只有两种状态:0 和 1。即二进制位。
平时常见的 32 位出流程就是一次最多能处理 32 位的数据,也就是 4 个 byte(字节)。同理,64 位处理器一次最多能处理 64 位的数据,即 8 个字节。
byte(字节)
- 1 KB = 1024 Byte
- 1 MB = 1024 KB
- 1 GB = 1024 MB
通常一个字节由 8 个二进制位(bit)组成。
一个十六进制数需要由 4 个二进制组成,即一个字节可以标识 2 个十六进制数。
基本数据类型的长度
对 C/C++ 而言,不同的操作平台分配给基本数据类型的长度(字节)是不一样的,比如 char*
指针变量在 32 位编译器里是 4 个字节(32 位的寻址空间是 2^32, 即 32 个 bit,也就是 4 个字节。64 位编译器同理),在 64 位编译器里是 8 个字节。
而 Java 是跨平台语言,JVM 里的基础数据类型的字节长度是一致的。各基本数据类型长度如下:
int:4 个字节
short:2 个字节。
long:8 个字节。
byte:1 个字节。
float:4 个字节。
double:8 个字节。
char:2 个字节。
boolean:boolean 属于布尔类型,在存储的时候不使用字节,仅使用 1 位来存储,范围仅为 0 和 1,其字面量为 true 和 false。
基本数据类型的取值范围
以最常见的 int 为例,Java 中 int 是 4 个字节,那 int 的取值范围是多少呢?熟悉 api 的同学都知道,Integer
类里定义了 MAX_VALUE = 0x7fffffff
,那就来推算一下 Java 定义的这个值对不对(大雾
int 占 4 个字节 32 位,因此就是 8 位数的十六进制。因为 int 值有正负之分,所以最高位表示符号,0 代表正数,1 代表负数。显而易见,int 能表示的最大值的二进制为 0111 1111 1111 1111 1111 1111 1111 1111 ,最高位 0,后面跟 31 个 1。换算成十六进制就是 0x7FFFFFFF,该值与 Jdk 中定义的相同,可见 Jdk 还是很严谨的(2333),Java 大法好!同理,最小值的二进制为 1111 1111 1111 1111 1111 1111 1111,换算成十六进制就是 0xFFFFFFFF,再对照一下 Jdk 中定义的最小值 MIN_VALUE = 0x80000000
。纳尼?Jdk 有 bug!(2333)
想都不用想,肯定是我自己有 bug,那为什么推算出的和 Jdk 中定义的不符呢。其实是二进制表示方法不对而已。二进制除了上述可直观计算得出的逢二进一的原码外,另外还有几种表示方法。
原码 反码 补码
原码很直观易懂,但也有其缺点,就比如最高位为符号位为这个槽点,就诞生了 0000 ~ 0000,1000 ~ 000,分别代表 +0 和 -0。至于数学里有没有 +0 和 -0,二者参与运算是怎么个计算法,我读书少我也不清楚。但这说明了一个问题,使用原码存储和运算会存在二义性。计算机在运算时使用的并非原码而是补码。补码和反码的计算公式如下:
正数
原码、反码、补码都相同负数
反码:原码保留符号位,其他位取反
补码:反码+1补码转原码
如果符号位为1,其余各位取反,然后再整个数加1。
上面提到的 +0 (0000 ~ 0000),其补码也为 000 ~ 0000,而 -0(1000 ~ 0000),其反码为 1111 ~ 1111,补码为反码 + 1 ,为 0000 ~ 0000,可见补码消除了关于 0 的二义性,使用补码并不会存在两个 0。
回到上面推算的 int 值得最小值 1111 ~ 1111,其反码为 1000 ~ 0000,补码为 1000 ~ 0001,转换为十六进制为 0x80000001。而这与 Jdk 规定的最小值 MIN_VALUE = 0x80000000
并不相同,说明还遗漏了什么。再回看补码,除了消除二义性,还有个好处是可以把减法当做加法。都知道 01111 ~ 1111 代表正数的最大值,最高位只代表符号,那么将其由 0 变 1,用 1111 ~ 1111 来代表负数的最大值从某种角度上也说得通,补码(1111 ~ 1111)
= 十进制(-1)
,将 补码(1111 ~ 1111)
往前迭代 1 位(做 + 1 的运算),舍弃溢出位,得到 补码(0000 ~ 0000)
= 十进制(0)
,符合 -1 + 1 = 0 的运算结果。将 补码(1111 ~ 1111)
往后迭代 1 位,得到 补码(1111 ~ 1110)
= 原码(1000~ 0010)
= 十进制(-2)
,符合 -1 - 1 = -2 的运算结果。则同理,将负数最大值 补码(1111 ~ 1111)
一直往后迭代,直到无法再小,则最小值应为 补码(1000 ~ 000)
= 原码(1000 ~ 000)
= 十进制(-0)
= 十六进制(0x80000000)
。也就是原码空出来的那个代表 -0 的数,被计算机用来表示 int 的最小值。
Bitmap 像素
提及 Bitmap ,先介绍一下 Android 中Bitmap
类中定义的枚举类 Config
里的几个值,也是比较多见的 Android 中的 Biamap 显示参数。
Bitmap 参数
ARGB_4444
四个通道 A(透明度)、R(红色)、G(绿色)、B(蓝色)各占 4 位,总共 16 位,即每个像素占用 2 个字节。ARGB_8888
四个通道各占 8 位,总共 32 位,每个像素占用 4 个字节。因为 RGB 通道精度更高,所以颜色显示更丰富,同时占用内存也更大。RGB_565
没有透明度信息,RGB 通道各占用 5 位、6 位、5 位,总共 16 位,每个像素占用 2 个字节。
知道了每个像素占用的字节长度,就可以计算一张图片显示时所占用的内存大小,以 ARGB_8888 为例,一张像素为 16 * 16 的图片占用的内存为:16 * 16 * 4 = 1024 byte,即 1 KB。
轻松愉快又简单!可梦想很美好,显示很骨感。在 Android 中,在不压缩计算的情况下(例如显示 assets 目录下的图片),内存大小就是上面计算所得,但因为 Android 中的图片一般存放在不同的资源目录:
资源目录对应的 dpi
mdpi -> 120 dpi
mdpi -> 160 dpi
hdpi -> 240 dpi
xdpi -> 320 dpi
xxdpi -> 480 dpi
xxxdpi -> 640 dpi
Android 中显示不同的资源目录图片时,会对图片做缩放处理,缩放比例为 设备dpi / 资源目录对应 dpi
,以 小米8SE 为例,设备屏幕密度为 440 dpi,该设备显示存放在 xxdpi(480dpi)目录中的像素为 300 * 300 的图片时,实际显示图片的宽和高将换算为 440 / 480 * 300
(结果四舍五入),计算得到图片在手机显示的宽高为 275,再根据计算所得实际的图片宽高计算所占内存:
275 * 275 * 4 = 302500(byte)
可以调用 Bitmap
类自带的方法 getByteCount()
方法验证一下。
顺带提一下,Android 中 Bitmap 的占用内存大小与显示图片的容器(例如 Android 上的 ImageView)尺寸无关。
Bitmap 像素的定义
介绍完 Bitmap 内存占用大小后,回到 Bitmap 本身来。Bitmap 将图像定义为由像素组成,以 ARGB_8888 为例,上面提到过,A/R/G/B 各占 8 位,各由两个十六进制数表示,依次排列,比如常见的色值 #FF234567,即各通道值为:透明度 alpha 0xFF,红色 red 0x23,绿色 green 0x45,蓝色 blue 0x67。
因此一张分辨率 100 * 100 的彩色图片,无非就是 100 * 100 个像素,每个像素显示对应的颜色,所有像素组合在一起便成了彩色的图片。所以只要拿到了 Bitmap,想要如何修改图像的显示,只要对各个像素显示的颜色做相应的处理就好了。
彩色转换为灰色的计算方式暂且不提。要改变图像的显示,首要任务是获取到各像素点的颜色。
Android 中可以调用 Bitmap
类自带的方法获取到具体某个点的像素颜色:
int color = bitmap.getPixel(200, 300);
那么问题来了,如何才能从一个 int 值中获取各个通道(RGB)的颜色呢?
从像素中提取各通道色值
老司机们可能秒懂,这个简单,Color
类自带的方法就可以做到:
int redColor = Color.red(color);
再看一下该方法的实现:
@IntRange(from = 0, to = 255)
public static int red(int color) {
return (color >> 16) & 0xFF;
}
其实计算方法也很简单,用到了位运算,那就顺带回顾一下位运算。
位运算符
从最低位到最高位一一对齐,每一位都做运算(也是对补码做运算),各运算符含义如下:
&
与
都是 1,则结果为1。否则为 0。|
或
都是 0,则结果为0。否则为 1。~
取反
对数的每一位取反。^
异或
数值相同,则结果为 0,不为 1。>>
右移
从 0 位起整体向右移动,空出的高位正数补 0,负数补1。>>>
无符号右移
从 0 位起(连符号位)整体向右移动,空出的高位一律补 0。
对于正数而言,>>和>>>没区别。<<
左移
整体向左移动,右边的空位一律补 0。
现在再来回看上面提到的取色方法:
// Color
public static int red(int color) {
return (color >> 16) & 0xFF;
}
还以 #FF234567
为例,转换为二进制为
1111 1111 | 0010 0011 | 0100 0101 | 0110 0111
(这里我用了 |
符号方便划分),其中 第二阵列 0010 0011
,即右起第 17 ~25 位代表红色色值。将二进制右移 16位,等同于舍弃了红色右边 的 16 位用于存储绿色、蓝色的色值,得到 0000 0000 | 0000 0000 | 1111 1111 | 0010 0011
,再与 0xFF
即二进制 1111 1111
做与运算,运算时高位为空则补0,与 0 做 &
与运算结果必为0,等同于与舍弃了右边代表透明度的高八位,最终得到红色的色值 0010 0011
。
取红色色值也还有另一种解法:
(color & 0x00FF0000) >> 16
先和 0x00FF0000
做与运算,舍弃除红色外所有色值,再右移 16 位得到该值。这种解法与上述的只不过是运算顺序不同,殊途同归。
至此,获取到了色值,想要怎么改变图片的显示就是算法上的事了,各凭本事各显神通。
今天的分享就到这,如有纰漏欢迎指正,下篇博客见。
Bitmap 图像灰度变换原理浅析的更多相关文章
- Android Bitmap变迁与原理解析(4.x-8.x)
App开发不可避免的要和图片打交道,由于其占用内存非常大,管理不当很容易导致内存不足,最后OOM,图片的背后其实是Bitmap,它是Android中最能吃内存的对象之一,也是很多OOM的元凶,不过,在 ...
- Atitit 图像金字塔原理与概率 attilax的理解总结qb23
Atitit 图像金字塔原理与概率 attilax的理解总结qb23 1.1. 高斯金字塔 ( Gaussianpyramid): 拉普拉斯金字塔 (Laplacianpyramid):1 1.2 ...
- HTTP长连接和短连接原理浅析
原文出自:HTTP长连接和短连接原理浅析
- Javascript自执行匿名函数(function() { })()的原理浅析
匿名函数就是没有函数名的函数.这篇文章主要介绍了Javascript自执行匿名函数(function() { })()的原理浅析的相关资料,需要的朋友可以参考下 函数是JavaScript中最灵活的一 ...
- [转帖]Git数据存储的原理浅析
Git数据存储的原理浅析 https://segmentfault.com/a/1190000016320008 写作背景 进来在闲暇的时间里在看一些关系P2P网络的拓扑发现的内容,重点关注了Ma ...
- Android-Binder原理浅析
Android-Binder原理浅析 学习自 <Android开发艺术探索> 写在前头 在上一章,我们简单的了解了一下Binder并且通过 AIDL完成了一个IPC的DEMO.你可能会好奇 ...
- Dubbo学习(一) Dubbo原理浅析
一.初入Dubbo Dubbo学习文档: http://dubbo.incubator.apache.org/books/dubbo-user-book/ http://dubbo.incubator ...
- 沉淀,再出发:docker的原理浅析
沉淀,再出发:docker的原理浅析 一.前言 在我们使用docker的时候,很多情况下我们对于一些概念的理解是停留在名称和用法的地步,如果更进一步理解了docker的本质,我们的技术一定会有质的进步 ...
- 阻塞和唤醒线程——LockSupport功能简介及原理浅析
目录 1.LockSupport功能简介 1.1 使用wait,notify阻塞唤醒线程 1.2 使用LockSupport阻塞唤醒线程 2. LockSupport的其他特色 2.1 可以先唤醒线程 ...
随机推荐
- 盘点腾讯Linux、 C++后台开发面试题,做好充足准备,不怕被Pass
一.C/C++ const 多态 什么类不能被继承 二.网络 网络的字节序 网络知识 TCP三次握手 各种细节 timewait状态 TCP与UDP的区别 概念 适用范围 TCP四次挥 ...
- [BUGCASE]CI框架的post方法对url做了防xss攻击的处理引发的文件编码错误
一.问题描述 出现问题的链接: http://adm.apply.wechat.com/admin/index.php/order/detail?country=others&st=1& ...
- linq 查询的结果会开辟新的内存吗?
一:背景 1. 讲故事 昨天群里有位朋友问:linq 查询的结果会开辟新的内存吗?如果开了,那是对原序列集里面元素的深拷贝还是仅仅拷贝其引用? 其实这个问题我觉得问的挺好,很多初学 C# 的朋友或多或 ...
- CoProcessFunction实战三部曲之二:状态处理
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- Spring Boot 2.x 多数据源配置之 JPA 篇
场景假设:现有电商业务,商品和库存分别放在不同的库 配置数据库连接 app: datasource: first: driver-class-name: com.mysql.cj.jdbc.Drive ...
- NOIp2020游记
Day 1 考点还是在南航,第三次去已经没有什么新鲜感了,满脑子都是NOIp能不能考好.考前奶了一波这次必考最短路,于是在试机的时候打了一遍Dij和SPFA的板子,信心满满的上场了. 考试右后方是Ki ...
- moviepy音视频剪辑:视频半自动追踪人脸打马赛克
一.引言 在<moviepy1.03音视频剪辑:使用manual_tracking和headblur实现追踪人脸打马赛克>介绍了使用手动跟踪跟踪人脸移动轨迹和使用headblur对人脸进行 ...
- 第8.17节 Python __repr__方法和__str__方法、内置函数repr和str的异同点对比剖析
一. 引言 记得刚开始学习Python学习字符串相关内容的时候,查了很多资料,也做了些测试,对repr和str这两个函数的返回值老猿一直没有真正理解,因为测试发现这两个函数基本上输出时一样的.到现在老 ...
- PyQt(Python+Qt)学习随笔
老猿Python博文目录 老猿Python博客地址 PyQt学习随笔 PyQt(Python+Qt)帮助文档官网及文档下载 PyQt(Python+Qt)学习随笔:PyQt帮助文档导入assistan ...
- PyQt(Python+Qt)学习随笔:使用QtWidgets.qApp实现在程序中随时访问应用的方法
在PyQt应用中,任何一个应用在启动时必须创建一个基于QtWidgets.QApplication或其派生类对应的应用对象,该对象用于处理事件. 如果需要在应用代码中的任何位置都能访问该应用对象,可以 ...