数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一步都很简单,但合在一起跑一遍,你会彻底想明白一件平时容易忽略的事——图像文件在内存里到底长什么样,像素值是怎么被存储、计算和读出来的。这篇内容就把这套流程从头到尾拆开,包括原理、代码、输出结果,还有我踩过的几个坑。适合正在学数字图像处理的人,也适合做课设或毕设时想把基础环节快速跑通的同学。
1. 项目思路拆解:为什么这四步要凑在一起做
1.1 四个操作背后其实是同一条逻辑线
很多人第一次接触这些操作时会觉得它们是零散的:反白是一个滤镜,转灰度是一个功能,改调色板是一个偏门技巧,存txt更像是在应付作业。但把它们放在一起,你会发现它们全都在处理同一件事:像素值。
反白是直接修改图像矩阵里的数值;改调色板不碰像素值,改的是“如何解释这些数值”;彩色转灰度是把三维的像素矩阵降成二维;导出txt则是把最终矩阵变成人能直接读懂的纯文本。前面三个都是在改变或操作数据,最后一个是把数据“打印”出来。你亲手做完这四个步骤,相当于把一张图从“文件”到“矩阵”再到“可读文本”的整条链路走了一遍,这个认知比任何单独的操作都有价值。
我在设计这套流程的时候,特意让每一步都保留中间结果,而不是只输出最终效果。比如反白完保存一张,灰度化完再保存一张,调色板改完再保存一张,每一步都能单独打开看。这样做的好处是出了问题能立刻定位是哪一步不对,而不是最后拿着一个莫名其妙的结果无从下手。A同学跟我一起跑这套流程的时候,一开始就是偷懒只留最终文件,结果颜色不对,根本不知道是灰度化时通道顺序错了还是调色板那儿出了问题,后来老老实实每步留底,几分钟就找到问题了。
1.2 工具选型:Python + OpenCV + Pillow + NumPy
这套流程用到的工具,我首选的是Python搭配三件套:OpenCV、Pillow(PIL)、NumPy。为什么这么选?因为它们在图像处理上的分工刚好互补。
OpenCV在颜色空间转换、像素运算上非常成熟,一个cvtColor就能完成灰度化,bitwise_not可以直接做反白操作,性能也很好。但它对调色板这种索引色图像的支持比较别扭,很多接口不会直接暴露调色板给你改。这时候Pillow就是更好的选择,它原生支持P模式(索引色模式),可以直接getpalette()拿到调色板列表,改完再putpalette()写回去,非常直观。NumPy则是这一切的地基,图像在内存里本质就是一个多维数组,反白操作对一个数组做减法,你不可能用双层for循环去逐像素处理,那样又慢又容易错,直接用NumPy的向量化运算,一行代码就能处理整张图。
这三个工具装起来也简单,一个pip install opencv-python pillow numpy就全搞定了,不涉及额外环境。如果你用的是虚拟环境,记得装完之后确认一下import没问题,OpenCV在部分精简环境里缺少依赖库,会报奇怪的错误,最常见的就是缺了基础的动态链接库。我在某高校的机器上就碰到过这种问题,当时还以为是代码错了,排查了半天,最后发现是环境问题。
2. 图像反白实操:从“底片效果”到像素取反
2.1 反白的数学本质与应用场景
反白其实就是图像处理里的灰度反转,教科书上叫“反色变换”,也有人叫“负片效果”。它的数学定义很简单:对图像中每个像素点的亮度值V,用最大亮度值减去它。对于最常见的8位灰度图,像素值范围是0到255,反白就是new_value = 255 - old_value。黑色(0)反白后变成白色(255),白色变成黑色,中间灰度值变成它的互补值,128反白后还是128。
所以反白是一个典型的点操作,也叫逐点运算,每个像素的新值只跟它自己的旧值有关,完全不依赖周围的像素。这意味着你不需要看周围的像素来做判断,可以用向量化方式对整张图同时计算,速度极快。你可以用“照片底片”来理解:老式胶片上,亮的地方曝光多,底片上是黑的,洗出来才是亮的,数字负片本质上就是这个反转过程的功能模拟。
反白不只是一个视觉效果,它有实际用途。医学影像里看X光片时经常用灰度反转来突出某些组织;印刷行业做负片预览会用到它;图像增强里面,对某些太暗的区域先反白成亮的区域再处理,往往比直接拉伸暗部更方便。我自己的习惯是,拿到一张偏暗的图像,第一件事就是先反白看一眼,很多时候暗部的细节在反白之后反而看得更清楚,这一步成本极低,却能帮你快速判断图像数据的分布情况。
2.2 反白的两种实现与注意事项
第一种实现用OpenCV,一句话的事:
import cv2 img = cv2.imread("input.png") inverted = cv2.bitwise_not(img) # 逐位取反,等效于255 - img cv2.imwrite("inverted.png", inverted)第二种用NumPy,更直白地展示了数学本质:
import cv2 import numpy as np img = cv2.imread("input.png") inverted = 255 - img cv2.imwrite("inverted.png", inverted)两种做法效果完全一样,bitwise_not本质上就是按位取反,对8位无符号整数来说,按位取反的结果恰好等于255减去原值。我通常用第二种写法,因为它在代码里直接体现了公式,别人看你的代码更容易理解你在干什么。
这里有个非常容易翻车的点:图像的数据类型。如果图像是以uint8(0到255)读入的,那255 - img没问题。但如果图像被读成浮点类型,比如数值范围在0.0到1.0之间,那正确的反白应该是1.0 - img。你要是还写255 - img,得到的就是一片乱七八糟的数值,保存出来全白或者全黑。同理,处理16位深图像时要用65535 - img。判断图像类型很简单,print(img.dtype)一看就知道。
还有一个细节是反白只对单通道或三通道图有意义。如果你读入的是带透明通道的PNG(即RGBA四通道),直接用255 - img会把透明通道也反转,透明区域变成完全不透明。大多数情况下这不是你想要的效果。处理RGBA图时,要么先把透明通道拆出来,反转RGB三个通道后再合回去,要么先把图转换成RGB格式再反白。我一般在流程第一步就把所有输入统一转换成RGB,省得后面每个操作都要考虑通道数的问题。
3. 调色板信息修改:给图像“换颜料盘”
3.1 索引色图像与调色板的关系
这一步是四个操作里最容易让人困惑的,因为现在大家接触到的照片几乎都是真彩色图,压根没有调色板这个概念。要理解调色板,得先搞明白“索引色图像”是怎么一回事。
普通的真彩色图像,比如最常见的24位BMP或JPEG,每个像素直接保存RGB三个分量,一个像素占3个字节。而索引色图像里,每个像素保存的不是颜色本身,而是一个编号。比如像素值是5,意思就是“当前图像使用调色板上的第5号颜色”。调色板本身是一张颜色查找表,按顺序定义了0号颜色、1号颜色、2号颜色直到255号颜色,每一行的颜色都是一个RGB三元组。
用个生活化的类比:调色板就是你画画用的颜料盘,像素值记录的是“你用的是第几格颜料”,而不是颜料的实际颜色。调色板上1号格本来是红色,你把1号格换成蓝色,整张画里所有用1号格的区域就全部从红色变成蓝色,但你不需要去改画面的任何一个点。这就是修改调色板信息最核心的特点:像素值完全不变,变的是颜色的映射关系。
所以修改调色板信息之前,先确认你的图像确实是索引色模式。在Pillow里,图像模式是P才表示索引色。如果输入的是一张常规的JPEG照片,它在内存里是RGB模式,没有调色板可改。想改也行,先用convert("P")把真彩色图量化成索引色图,这个过程会自己生成一个调色板,然后再修改它。要注意的是,这个转换本身是有损的,256色调色板只能表示256种颜色,转完颜色会有一定偏差。
3.2 修改调色板的具体操作
用Pillow修改调色板的代码很简洁:
from PIL import Image img = Image.open("palette.png") if img.mode != "P": img = img.convert("P") palette = img.getpalette() # 返回一个列表,长度通常是768 # 列表顺序:第0个是0号颜色的R,第1个是0号颜色的G,第2个是0号颜色的B # 第3个开始是1号颜色,以此类推 # 示例:把0号颜色改成纯红色 palette[0] = 255 palette[1] = 0 palette[2] = 0 # 上面只改了3个值,如果只想改一个颜色,这样就够了 img.putpalette(palette) img.save("palette_modified.png")这里有一个需要注意的地方:getpalette()返回的列表长度并不总是768。如果图像实际用到的颜色数少于256,有些库实现会只返回实际颜色数对应的长度,比如图像只有16种颜色,返回的可能就是48个值(16乘以3)。但putpalette()需要完整的256色调色板,也就是768个值。我的习惯是先检查长度,不够768就补齐:
if len(palette) < 768: palette.extend([0] * (768 - len(palette)))补的数值没有严格规定,因为索引色图像里没有被用到的颜色索引,补了也不影响显示。但我习惯补成灰色而不是黑色,这样万一后面没注意某个索引,显示的也是能看出来有问题的灰色,而不是和背景黑色混淆。
还有一种更优雅的做法,是把调色板当成二维数组来处理:
import numpy as np palette = img.getpalette() palette_arr = np.array(palette).reshape(-1, 3) # 把10号颜色改成绿色 palette_arr[10] = [0, 255, 0] # 改完再展平回列表 img.putpalette(palette_arr.flatten().tolist())这样按索引改颜色更清晰,不会出现手数列表位置的尴尬。我用这个方法做一个批量实验:把调色板前50个颜色全部改成某种颜色,看看图像的变化,其实就是把低亮度区域全部映射成一个颜色。效果有点类似抠图或者海报化,但代码量非常小,几行就做完了。
3.3 换色实验:通道交换与索引染色
调色板改起来花样挺多的,我在这里多分享两个玩法,都是实际中用得上的思路。
一个是通道交换。把调色板数组reshape成(N, 3)之后,把第0列和第2列交换,相当于把红色通道和蓝色通道互换,图像立刻变成另一种色调。在真彩色图像上做通道交换你还要小心是RGB还是BGR顺序,但在调色板里规则就一个:第一列是R,第二列是G,第三列是B,怎么改自己完全能控制。
另一个玩法是给灰度图套伪彩色。灰度图转成P模式后,把调色板设置成渐变颜色,比如从黑色到红色再到黄色,灰度图立刻变成热力图风格。医学图像里常用的伪彩色就是这种思路:把灰度值映射成有颜色的调色板,让细节更容易被肉眼区分。做法是:
# 先准备一个渐变色调色板 new_palette = [] for i in range(256): t = i / 255 new_palette += [int(255 * t), 0, int(255 * (1 - t))] img.putpalette(new_palette)这段代码生成的调色板是从红色渐变到蓝色的,给灰度图套上之后,暗部偏蓝,亮部偏红,视觉区分度比单纯灰度图高很多。不过要留意,伪彩色只是改变了显示映射,并不会改变图像数据的亮度值,你导出的txt里保存的仍然是灰度值。
4. 彩色图像转灰度:别用简单平均糊弄
4.1 灰度化的原理与权重来源
彩色图像转灰度,看起来就是把R、G、B三个通道揉成一个值,但怎么揉是有讲究的。最简单的想法是取三个通道的平均值,gray = (R + G + B) / 3。这样能得到灰度图,但视觉效果往往偏暗、偏灰,细节也不够清楚。原因在于,人眼对三种颜色的敏感程度完全不同。
人类的视觉系统对绿色最敏感,因为视网膜上的感光细胞在绿色波段分布最密;对红色敏感度中等;对蓝色敏感度最低。如果三个通道平均分配权重,相当于把本来就难看清的蓝色细节和最容易看清的绿色细节一视同仁,结果就是图像失去了那种自然的明暗层次。这也是为什么标准灰度化公式里给绿色分配了最高权重。
行业通用的标准是ITU-R BT.601建议的加权公式:
Gray = 0.299 * R + 0.587 * G + 0.114 * B三个权重加起来刚好是1。0.587给了绿色,0.299给了红色,蓝色只有0.114。你直接用这个公式转出来的灰度图,看起来最接近人眼对原始彩图的明暗感受,暗部不会糊成一片,亮部也不会出现过曝感。
有一个细节值得注意:公式里的0.299这些系数,不是某个科学家拍脑袋定的,而是从大量视觉实验里统计出来的。它代表的是“标准观察者”对三种颜色的相对亮度感知。所以实际工程里不需要自己调,直接用现成系数就行。不少初学者会在网上看到另一个版本,0.2126、0.7152和0.0722,那是BT.709标准,用于高清视频和sRGB色彩空间,用在普通图像上也没大问题,但两个标准不要混着用。我的建议是跟随你用的库,OpenCV的COLOR_BGR2GRAY内部用的就是BT.601权重,直接用就好。
4.2 代码实现与效果对比
用OpenCV转灰度,代码是最简单的:
import cv2 img = cv2.imread("input.png") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) cv2.imwrite("gray.png", gray)cvtColor一行就完成了,但如果你想知道背后到底发生了什么,或者课程设计里要求手写算法,那可以手动实现一遍:
import cv2 import numpy as np img = cv2.imread("input.png") # OpenCV读入的通道顺序是BGR,不是RGB! b = img[:, :, 0].astype(np.float32) g = img[:, :, 1].astype(np.float32) r = img[:, :, 2].astype(np.float32) gray = 0.299 * r + 0.587 * g + 0.114 * b gray = gray.astype(np.uint8) cv2.imwrite("gray_manual.png", gray)这里必须强调一件事:OpenCV的imread读入图像后,通道顺序是BGR,不是我们习惯的RGB。所以手动实现灰度化公式的时候,取出来的第0通道是蓝色,第2通道才是红色,千万别写反。你写0.299 * b + 0.587 * g + 0.114 * r,虽然运算顺序一样结果数值也一样,因为这三个量是加在一起的,权重是绑定的,但如果你把权重跟通道对应错,比如把0.299给了蓝通道,那就大错特错了。结果会偏得一塌糊涂。
为了验证效果,我常用一个统一的办法:把原图、平均值法、加权法生成的灰度图放在同一视图里对比。平均值法的图明显偏灰,尤其在蓝色占比大的区域,比如天空、水面,会显得发暗发闷。加权法生成的图则更通透,亮部层次更丰富。这个差异在色彩丰富的照片上更明显,纯色块或者简单的图形上反而很难看出区别,所以做验证时记得选一张颜色丰富的图。
4.3 灰度化操作中容易犯的错
灰度化这一步的坑不算多,但有几个值得提一下。
第一个坑是前面说过的BGR和RGB顺序混淆。不只是手动实现时容易错,你在用Matplotlib显示图像时也会遇到。matplotlib默认按RGB顺序显示,你把OpenCV读进来的BGR图直接imshow,颜色会偏蓝红互换,人脸会变得像外星人。解决方法是显示前用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)转一次通道顺序。这件事跟灰度化本身无关,但在整套流程中经常一起出现,很多人会误以为是灰度化出了问题。
第二个坑是图像本来就是灰度图,你还去做灰度化。有些同学读入一张灰度图后没检查通道数,直接调cvtColor,OpenCV会报错,因为输入只有单通道。处理办法是先用img.ndim检查维度,如果是二维矩阵,说明已经是灰度图了,跳过这步。我习惯写一个判断分支,让函数既能处理彩色输入也能处理灰度输入。
第三个坑是数据类型被改成浮点后忘记转回去。手动实现灰度化时,为了保留小数精度,我把三个通道转成float32计算,但计算完如果不转回uint8就保存,OpenCV的imwrite会把浮点数按0到1的范围解释,结果就是保存的照片几乎全白。保存前必须astype(np.uint8)。这跟反白那边的坑是一样的:时刻清楚你的数据是什么类型、范围是多少。
5. 图像数据导出txt:让像素值变成“明文”
5.1 txt文件里的数据组织方式
把图像数据保存成txt,听起来原始,但实际场景里非常有用。比如你要把图像数据交给另一个程序处理,像C语言或MATLAB写的小工具,它们不一定方便直接读图片格式,txt是通用性最好的交换格式。再比如你想直观地检查一张图某个区域的像素值分布,用图片查看器是看不出来的,导出成txt后直接搜数值就行。我还在做数据可视化的Demo时遇到过一种情况:需要把图像数值导入到表格里画热力图,图片文件没法直接用,导出成txt再导入就要容易得多。
txt文件本身没有固定的格式要求,关键是定下一套规则,让后续读取的人或程序能明确知道每个数字代表什么。我通常这样组织:
- 第一行可以写注释,注明行列数和通道信息,比如
# shape: 240 320 3 - 如果是灰度图,每一行对应图像的一行像素,像素之间用空格或逗号分隔
- 如果是彩色图,每一个像素的多个通道值写在一起,用逗号隔开,像素之间用分号或空格隔开
我用得最多的是灰度图每行像素一行文本的格式,比如:
12 34 56 78 90 23 45 67这样安排的好处是,任何人拿到文件都能直观地看到图像矩阵的长相。保存出来之后还能顺便数一下行数,一行就是图像的一行,非常直观。彩色图稍微复杂一点,我一般会让每个像素输出三个通道值,并明确标注顺序。我用OpenCV处理时顺序是BGR,但我导出的txt里我会在文件头部注释里写清楚顺序,避免别人拿过去之后默认是RGB。
5.2 二维灰度图和三维彩图的写入方案
写txt文件的方法有两个层次:简单粗暴的和推荐使用的。
灰度图适合用numpy.savetxt,性能好、代码少:
import numpy as np gray = cv2.imread("gray.png", cv2.IMREAD_GRAYSCALE) np.savetxt("gray_data.txt", gray, fmt="%d", delimiter=" ")fmt="%d"表示按整数保存,不会出现128.0000000000这种冗长的小数。delimiter=" "控制像素之间的分隔符。
彩色图是三维矩阵,np.savetxt不支持直接保存三维数组,处理思路是把三维数据变换成二维再写。最直接的做法是把每个像素的三个通道拼接成一个字符串:
import cv2 img = cv2.imread("input.png") h, w, c = img.shape with open("color_data.txt", "w", encoding="utf-8") as f: f.write(f"# shape: {h} {w} {c}\n") f.write("# channel order: B G R\n") for row in img: line = " ".join( f"{pixel[0]},{pixel[1]},{pixel[2]}" for pixel in row ) f.write(line + "\n")这样每一行对应图像的一行像素,每个像素的三个通道用逗号连在一起,像素之间用空格隔开。后续解析的时候先按空格拆出像素,再按逗号拆出通道。这种格式比把三通道铺开成三列更紧凑,语义也更清楚。
如果你想要的是纯矩阵、不带任何注释的版本,用于程序自动读取,那就不写注释头。两种格式各有用途:给人看的加注释,给程序读的不加。
5.3 大图导出时的性能与精度控制
先说性能。图像是二维矩阵,一张1920乘1080的灰度图有207万多个数值,如果你用Python的双层for循环逐像素写入txt,会非常慢。我试过用最朴素的写法处理一张几百万像素的图,能等上几十秒,体验极差。原因在于Python每执行一次循环都有不小的解释器开销,几百万次循环叠加起来就是灾难。解决办法很简单:能向量化就不要循环,能用内置函数就不要手写。
np.savetxt内部是优化过的,处理几百万个数值只要一秒钟左右。彩色图用上面那种生成器表达式的方式写,也远比双层for循环快,因为join和生成器在C层面做了大量优化,但还是不如np.savetxt快。如果遇到特别大的图,比如上亿像素的卫星影像,我建议先对图像做降采样或者裁剪区域再导出,否则txt文件本身也会大到几百MB,普通文本编辑器根本打不开,写出来也没意义。
再讲精度控制。如果图像是uint8类型,导出整数值就够了。但如果图像已经转成浮点类型,比如归一化到0到1之间的浮点图,直接保存会出现一长串小数。需要在写入前决定保留几位小数,或者干脆乘255转回整数。我在处理灰度图时通常统一导出0到255的整数,这样数据在任何程序里都好处理。如果你要保留浮点精度,np.savetxt里可以用fmt="%.6f"控制小数位数。
还有一个细节:保存txt时如果要用Excel打开看,建议使用逗号作为分隔符,文件后缀改成.csv,这样Excel双击就能直接打开成表格。但如果你是按课程要求交txt文件,就按老师要求的格式来。我在自己的项目里一般黑白图用空格分隔的txt,彩色图用逗号分隔的csv,方便不同场景使用。
6. 完整演示流程:一张彩色图跑通全部操作
前面把四个操作拆开讲了一遍,现在把它们组合起来,形成一个完整的可运行脚本。我习惯分成四个函数,每个函数只做一件事,然后在一个主流程里按顺序调用。这样后面想单独复用某个环节,直接调用对应函数就行,不用在长脚本里翻找。
完整代码如下:
import cv2 import numpy as np from PIL import Image def invert_image(img): """反白:像素值取反""" if img.dtype != np.uint8: img = (img * 255).astype(np.uint8) return 255 - img def convert_to_gray(img): """彩色图转灰度:兼容已经是灰度图的情况""" if len(img.shape) == 2: return img if img.shape[2] == 4: img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) def modify_palette(input_path, output_path): """修改调色板:把0号颜色改成红色""" img = Image.open(input_path) if img.mode != "P": img = img.convert("P") palette = img.getpalette() if len(palette) < 768: palette.extend([0] * (768 - len(palette))) palette[0] = 255 palette[1] = 0 palette[2] = 0 img.putpalette(palette) img.save(output_path) def save_gray_to_txt(gray_img, output_path): """灰度图导出txt""" np.savetxt(output_path, gray_img, fmt="%d", delimiter=" ") if __name__ == "__main__": # 1. 读入彩色图 img = cv2.imread("input.png") # 2. 反白 inverted = invert_image(img) cv2.imwrite("inverted.png", inverted) # 3. 转灰度 gray = convert_to_gray(inverted) cv2.imwrite("gray.png", gray) # 4. 灰度图导出txt save_gray_to_txt(gray, "gray_data.txt") # 5. 调色板修改,单独读入一张索引色图 modify_palette("palette.png", "palette_modified.png")这个流程里有一个顺序上的考虑:我先对原图做反白,再对反白结果做灰度化,这样灰度图在视觉上就是原图的负片效果,跟原图的灰度和反白结果形成对比,方便验证每一步是否真的生效。你也可以把顺序换成先灰度再反白,结果数值上是等价的,因为灰度化是线性加权,反白是线性取反,两个线性操作谁先谁后结果一样。但我在流程里选择先反白,是想让灰度图能看出负片风格,更容易确认“反白确实生效了”。
关于中间文件的命名,我的习惯是加上明确的前缀:inverted.png、gray.png、gray_data.txt、palette_modified.png。这样不管过了多久,回头看目录也能一眼看出每个文件的来源。如果输出文件多了,最好再建一个output子目录统一存放,别和源代码混在一起。这个习惯看着琐碎,但等你需要反复调试、改参数的时候,清晰的目录结构能省下大量时间。
7. 常见问题与排查记录
7.1 读图读到None:路径与中文名
最典型的报错是cv2.imread之后图像变量是None,后面的代码一执行就报空指针错误。九成原因都是路径不对,要么文件不在当前目录,要么路径里包含中文。OpenCV在Windows上对包含中文的路径支持不太好,我踩过很多次。解决方案有三种:把文件路径里的中文改成英文;用相对路径切到正确目录;或者改用cv2.imdecode配合np.fromfile来读,这样能正确处理中文路径。前两个方案是首选,改个文件名就能解决的事没必要为它写额外代码。
7.2 反白之后颜色怪怪的:通道顺序在作怪
用OpenCV读入再反白,输出的PNG在Windows照片查看器里看颜色不对,红色变成蓝色、蓝变成红,这种情况几乎可以肯定是BGR和RGB的混淆。反白本身不会破坏通道关系,但如果中间某一步把图像传给Pillow或者Matplotlib显示过,它们默认按RGB解释,颜色就对不上了。排查方法很简单:把原图、反白图用同一种显示方式打开对比,如果原图单独看颜色正常,反白图单独看颜色不对,且整体呈互补色调,那就是通道顺序的锅。解决方法是统一通道顺序,我通常以OpenCV的BGR为准,保存前不再转换,因为最终保存成PNG时OpenCV会正确处理。
7.3 调色板改了却看不出变化:先确认图像模式
修改调色板后发现图像完全没变,常见原因有两个。第一个,图像本身不是P模式,你是在RGB模式下做操作,而RGB模式根本没有调色板,修改操作被忽略了。解决方法是先打印img.mode确认,再决定是否convert("P")。第二个,保存格式不支持调色板。你把修改后的图像保存成JPEG,JPEG本身是面向真彩色的格式,保存时会丢弃调色板信息,重新编码成RGB,你改的颜色自然就“消失”了。保存时应选择PNG或GIF,这两种格式原生支持调色板。我一般保存成PNG,兼容性最好。
7.4 保存的txt用Excel打开乱成一团
用np.savetxt保存的txt默认用空格或制表符分隔,Excel对这种格式的自动分列时而不灵,打开后所有数值挤在一列里。解决方法有两个:保存时用逗号分隔并改成.csv后缀,Excel双击就能自动分列;或者在Excel里用“数据—分列”功能,选择按空格分列。如果你的数据后续要交给别的程序处理,保持空格分隔的txt也没问题。这里再补充一个经验:导出的txt里如果包含汉字注释,某些文本编辑器会显示乱码,解决办法是写入文件时指定encoding="utf-8",并且避免写中文表头,英文注释最省心。
7.5 输出数值整体偏大或偏小:检查数据类型
保存txt后发现所有数值都很大,或者都是0和1,大概率是数据类型问题。浮点图数值范围0到1时,直接fmt="%d"保存,全都会变成0,因为整数格式把小数部分全丢了。此时应该用fmt="%.6f",或者先把数值乘255再转成uint8。反过来,如果图像是16位深,数值范围是0到65535,直接保存整数值没问题,但你在文本里看到大范围数值时别觉得奇怪,先确认原图的位深就行。
这套流程跑完,我最大的感触就是:数字图像处理里大部分算法本身不难,难的是数据格式和类型转换。你在反白时要知道图像是uint8还是浮点,在调色板操作时要知道图像有没有调色板,在灰度化时要知道通道顺序,在导出txt时要知道数值范围。这四个操作恰恰把最容易出问题的几个环节全都覆盖了。我每次跑这套流程都习惯把中间结果每步都输出一张图,检查一遍再继续下一步,这套习惯帮我避开了不少隐蔽的坑。你在自己做类似项目时,也建议保持这个习惯,反正输出一张图就是一行代码的事,但调试时省下的时间可以说是相当可观。