Python与16位PGM

5 投票
3 回答
7597 浏览
提问于 2025-04-17 01:54

我有一些16位的PGM图像,想在Python中读取。看起来PIL(Python Imaging Library)似乎不支持这种格式?

import Image
im = Image.open('test.pgm')
im.show()

显示的图像大致上是对的,但不太准确。图像中有一些黑色的条纹,而且报告显示图像的mode=L。我觉得这和我之前问过的关于16位TIFF文件的问题有关。16位的格式真的这么少见,以至于PIL根本不支持吗?有没有什么建议可以让我在Python中读取16位的PGM文件,使用PIL或者其他标准库,或者自己写代码?

3 个回答

1

这里有一个通用的 PNM/PAM 阅读器,它是基于 NumPy 和一个在 PyPNG 中未记录的功能制作的。

def read_pnm( filename, endian='>' ):
   fd = open(filename,'rb')
   format, width, height, samples, maxval = png.read_pnm_header( fd )
   pixels = numpy.fromfile( fd, dtype='u1' if maxval < 256 else endian+'u2' )
   return pixels.reshape(height,width,samples)

当然,写入这种图像格式通常不需要库的帮助...

4

你需要使用一种叫做 "L;16" 的模式;不过看起来PIL在加载PGM文件时,已经把 "L" 这个模式写死在了File.c里。如果你想读取16位的PGM文件,就得自己写一个解码器

不过,支持16位图像的功能似乎还不太稳定:

>>> im = Image.fromstring('I;16', (16, 16), '\xCA\xFE' * 256, 'raw', 'I;16') 
>>> im.getcolors()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.6/dist-packages/PIL/Image.py", line 866, in getcolors
    return self.im.getcolors(maxcolors)
ValueError: image has wrong mode

我觉得PIL可以读取16位的图像,但实际上存储和处理这些图像的功能还在实验阶段。

>>> im = Image.fromstring('L', (16, 16), '\xCA\xFE' * 256, 'raw', 'L;16') 
>>> im
<Image.Image image mode=L size=16x16 at 0x27B4440>
>>> im.getcolors()
[(256, 254)]

你看,它把 0xCAFE 的值当成了 0xFE,这其实是不太正确的。

2

下面的内容只需要用到 numpy 来加载图像,这个图像可以是8位或16位的原始PGM/PPM格式。我还展示了几种不同的查看图像的方法。其中一种使用PIL(import Image)的方式需要先把数据转换成8位。

#!/usr/bin/python2 -u

from __future__ import print_function
import sys, numpy

def read_pnm_from_stream( fd ):
   pnm = type('pnm',(object,),{}) ## create an empty container
   pnm.header = fd.readline()
   pnm.magic = pnm.header.split()[0]
   pnm.maxsample = 1 if ( pnm.magic == 'P4' ) else 0
   while ( len(pnm.header.split()) < 3+(1,0)[pnm.maxsample] ): s = fd.readline() ; pnm.header += s if ( len(s) and s[0] != '#' ) else ''
   pnm.width, pnm.height = [int(item) for item in pnm.header.split()[1:3]]
   pnm.samples = 3 if ( pnm.magic == 'P6' ) else 1
   if ( pnm.maxsample == 0 ): pnm.maxsample = int(pnm.header.split()[3])
   pnm.pixels = numpy.fromfile( fd, count=pnm.width*pnm.height*pnm.samples, dtype='u1' if pnm.maxsample < 256 else '>u2' )
   pnm.pixels = pnm.pixels.reshape(pnm.height,pnm.width) if pnm.samples==1 else pnm.pixels.reshape(pnm.height,pnm.width,pnm.samples)
   return pnm

if __name__ == '__main__':

## read image
 # src = read_pnm_from_stream( open(filename) )
   src = read_pnm_from_stream( sys.stdin )
 # print("src.header="+src.header.strip(), file=sys.stderr )
 # print("src.pixels="+repr(src.pixels), file=sys.stderr )

## write image
   dst=src
   dst.pixels = numpy.array([ dst.maxsample-i for i in src.pixels ],dtype=dst.pixels.dtype) ## example image processing
 # print("dst shape: "+str(dst.pixels.shape), file=sys.stderr )
   sys.stdout.write(("P5" if dst.samples==1 else "P6")+"\n"+str(dst.width)+" "+str(dst.height)+"\n"+str(dst.maxsample)+"\n");
   dst.pixels.tofile( sys.stdout ) ## seems to work, I'm not sure how it decides about endianness

## view using Image
   import Image
   viewable = dst.pixels if dst.pixels.dtype == numpy.dtype('u1') else numpy.array([ x>>8 for x in dst.pixels],dtype='u1')
   Image.fromarray(viewable).show()

## view using scipy
   import scipy.misc
   scipy.misc.toimage(dst.pixels).show()

使用说明

  • 我最终搞明白了“它是如何决定字节序的”——实际上,它在内存中以大端字节序存储图像(而不是本地字节序)。这种方式可能会让一些复杂的图像处理变慢——不过,Python的其他性能问题可能会让这个担忧变得不那么重要(见下文)。

  • 我在这里问了一个与字节序相关的问题 这里。我还遇到了一些有趣的关于字节序的困惑,因为我在用 pnmdepth 65535 预处理图像时,这并不适合测试字节序,因为低字节和高字节可能会是一样的(我一开始没注意到,因为 print(array) 输出的是十进制)。我应该也用 pnmgamma 来避免一些困惑。

  • 因为Python运行得比较慢,numpy 会在某些操作上想得比较“聪明”(见 广播)。使用 numpy 时的一个经验法则是 让numpy帮你处理循环(换句话说就是 不要自己写 for 循环)。上面代码中的有趣之处在于,它在进行“示例图像处理”时只部分遵循这个规则,因此那行代码的性能极度依赖于传给 reshape 的参数。

  • 下一个关于 numpy 字节序的大谜团:为什么 newbyteorder() 似乎 返回一个数组,而它的 文档却说它返回的是 dtype。如果你想用 dst.pixels=dst.pixels.byteswap(True).newbyteorder() 转换为本地字节序,这个问题就很重要。

  • 关于迁移到Python 3的提示:带有ASCII文本头的二进制输入,从标准输入读取

撰写回答