Pillow/src/PIL/SpiderImagePlugin.py

325 lines
9.3 KiB
Python
Raw Normal View History

2010-07-31 06:52:47 +04:00
#
# The Python Imaging Library.
#
# SPIDER image file handling
#
# History:
# 2004-08-02 Created BB
# 2006-03-02 added save method
# 2006-03-13 added support for stack images
#
# Copyright (c) 2004 by Health Research Inc. (HRI) RENSSELAER, NY 12144.
# Copyright (c) 2004 by William Baxter.
# Copyright (c) 2004 by Secret Labs AB.
# Copyright (c) 2004 by Fredrik Lundh.
#
##
# Image plugin for the Spider image format. This format is is used
# by the SPIDER software, in processing image data from electron
# microscopy and tomography.
##
#
# SpiderImagePlugin.py
#
# The Spider image format is used by SPIDER software, in processing
# image data from electron microscopy and tomography.
#
# Spider home page:
2017-02-14 12:27:02 +03:00
# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html
2010-07-31 06:52:47 +04:00
#
# Details about the Spider image format:
2017-02-14 12:27:02 +03:00
# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html
2010-07-31 06:52:47 +04:00
#
2014-05-26 22:56:40 +04:00
import os
import struct
import sys
from PIL import Image, ImageFile
2010-07-31 06:52:47 +04:00
def isInt(f):
try:
i = int(f)
2019-03-21 16:28:20 +03:00
if f - i == 0:
2014-05-26 22:56:40 +04:00
return 1
else:
return 0
2018-01-06 13:58:05 +03:00
except (ValueError, OverflowError):
return 0
2010-07-31 06:52:47 +04:00
2018-03-03 12:54:00 +03:00
2014-05-26 22:56:40 +04:00
iforms = [1, 3, -11, -12, -21, -22]
2010-07-31 06:52:47 +04:00
# There is no magic number to identify Spider files, so just check a
# series of header locations to see if they have reasonable values.
2016-11-19 02:45:33 +03:00
# Returns no. of bytes in the header, if it is a valid Spider header,
2010-07-31 06:52:47 +04:00
# otherwise returns 0
2019-03-21 16:28:20 +03:00
2010-07-31 06:52:47 +04:00
def isSpiderHeader(t):
2019-03-21 16:28:20 +03:00
h = (99,) + t # add 1 value so can use spider header index start=1
2010-07-31 06:52:47 +04:00
# header values 1,2,5,12,13,22,23 should be integers
2014-05-26 22:56:40 +04:00
for i in [1, 2, 5, 12, 13, 22, 23]:
if not isInt(h[i]):
return 0
2010-07-31 06:52:47 +04:00
# check iform
iform = int(h[5])
2014-05-26 22:56:40 +04:00
if iform not in iforms:
return 0
2010-07-31 06:52:47 +04:00
# check other header values
2019-03-21 16:28:20 +03:00
labrec = int(h[13]) # no. records in file header
labbyt = int(h[22]) # total no. of bytes in header
lenbyt = int(h[23]) # record length in bytes
2014-05-26 22:56:40 +04:00
if labbyt != (labrec * lenbyt):
return 0
2010-07-31 06:52:47 +04:00
# looks like a valid header
return labbyt
2014-05-26 22:56:40 +04:00
2010-07-31 06:52:47 +04:00
def isSpiderImage(filename):
2019-03-21 16:28:20 +03:00
with open(filename, "rb") as fp:
f = fp.read(92) # read 23 * 4 bytes
t = struct.unpack(">23f", f) # try big-endian first
2010-07-31 06:52:47 +04:00
hdrlen = isSpiderHeader(t)
if hdrlen == 0:
2019-03-21 16:28:20 +03:00
t = struct.unpack("<23f", f) # little-endian
2010-07-31 06:52:47 +04:00
hdrlen = isSpiderHeader(t)
return hdrlen
class SpiderImageFile(ImageFile.ImageFile):
format = "SPIDER"
format_description = "Spider 2D image"
_close_exclusive_fp_after_loading = False
2010-07-31 06:52:47 +04:00
def _open(self):
# check header
n = 27 * 4 # read 27 float values
f = self.fp.read(n)
try:
self.bigendian = 1
2019-03-21 16:28:20 +03:00
t = struct.unpack(">27f", f) # try big-endian first
2010-07-31 06:52:47 +04:00
hdrlen = isSpiderHeader(t)
if hdrlen == 0:
self.bigendian = 0
2019-03-21 16:28:20 +03:00
t = struct.unpack("<27f", f) # little-endian
2010-07-31 06:52:47 +04:00
hdrlen = isSpiderHeader(t)
if hdrlen == 0:
raise SyntaxError("not a valid Spider file")
except struct.error as e:
raise SyntaxError("not a valid Spider file") from e
2010-07-31 06:52:47 +04:00
2019-03-21 16:28:20 +03:00
h = (99,) + t # add 1 value : spider header index starts at 1
2010-07-31 06:52:47 +04:00
iform = int(h[5])
if iform != 1:
raise SyntaxError("not a Spider 2D image")
2010-07-31 06:52:47 +04:00
self._size = int(h[12]), int(h[2]) # size in pixels (width, height)
2010-07-31 06:52:47 +04:00
self.istack = int(h[24])
self.imgnumber = int(h[27])
if self.istack == 0 and self.imgnumber == 0:
# stk=0, img=0: a regular 2D image
offset = hdrlen
self._nimages = 1
2010-07-31 06:52:47 +04:00
elif self.istack > 0 and self.imgnumber == 0:
# stk>0, img=0: Opening the stack for the first time
self.imgbytes = int(h[12]) * int(h[2]) * 4
self.hdrlen = hdrlen
self._nimages = int(h[26])
2010-07-31 06:52:47 +04:00
# Point to the first image in the stack
offset = hdrlen * 2
self.imgnumber = 1
elif self.istack == 0 and self.imgnumber > 0:
# stk=0, img>0: an image within the stack
offset = hdrlen + self.stkoffset
self.istack = 2 # So Image knows it's still a stack
else:
raise SyntaxError("inconsistent stack header values")
2010-07-31 06:52:47 +04:00
if self.bigendian:
self.rawmode = "F;32BF"
else:
self.rawmode = "F;32F"
self.mode = "F"
2019-03-21 16:28:20 +03:00
self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))]
2014-05-26 22:56:40 +04:00
self.__fp = self.fp # FIXME: hack
2010-07-31 06:52:47 +04:00
@property
def n_frames(self):
return self._nimages
2015-06-30 06:25:00 +03:00
@property
def is_animated(self):
return self._nimages > 1
2010-07-31 06:52:47 +04:00
# 1st image index is zero (although SPIDER imgnumber starts at 1)
def tell(self):
if self.imgnumber < 1:
return 0
else:
return self.imgnumber - 1
def seek(self, frame):
if self.istack == 0:
raise EOFError("attempt to seek in a non-stack file")
if not self._seek_check(frame):
return
2010-07-31 06:52:47 +04:00
self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
self.fp = self.__fp
self.fp.seek(self.stkoffset)
self._open()
# returns a byte image after rescaling to 0..255
def convert2byte(self, depth=255):
2015-04-24 11:24:52 +03:00
(minimum, maximum) = self.getextrema()
2010-07-31 06:52:47 +04:00
m = 1
2015-04-24 11:24:52 +03:00
if maximum != minimum:
2019-03-21 16:28:20 +03:00
m = depth / (maximum - minimum)
2015-04-24 11:24:52 +03:00
b = -m * minimum
2010-07-31 06:52:47 +04:00
return self.point(lambda i, m=m, b=b: i * m + b).convert("L")
# returns a ImageTk.PhotoImage object, after rescaling to 0..255
def tkPhotoImage(self):
2013-03-07 20:20:28 +04:00
from PIL import ImageTk
2019-03-21 16:28:20 +03:00
2010-07-31 06:52:47 +04:00
return ImageTk.PhotoImage(self.convert2byte(), palette=256)
2018-11-17 13:56:06 +03:00
def _close__fp(self):
try:
2019-01-04 04:29:23 +03:00
if self.__fp != self.fp:
self.__fp.close()
2018-11-17 13:56:06 +03:00
except AttributeError:
pass
finally:
self.__fp = None
2014-05-26 22:56:40 +04:00
2010-07-31 06:52:47 +04:00
# --------------------------------------------------------------------
# Image series
# given a list of filenames, return a list of images
def loadImageSeries(filelist=None):
2019-03-20 03:45:50 +03:00
"""create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
if filelist is None or len(filelist) < 1:
2010-07-31 06:52:47 +04:00
return
imglist = []
for img in filelist:
if not os.path.exists(img):
print("unable to find %s" % img)
2010-07-31 06:52:47 +04:00
continue
try:
Improve handling of file resources Follow Python's file object semantics. User code is responsible for closing resources (usually through a context manager) in a deterministic way. To achieve this, remove __del__ functions. These functions used to closed open file handlers in an attempt to silence Python ResourceWarnings. However, using __del__ has the following drawbacks: - __del__ isn't called until the object's reference count reaches 0. Therefore, resource handlers remain open or in use longer than necessary. - The __del__ method isn't guaranteed to execute on system exit. See the Python documentation: https://docs.python.org/3/reference/datamodel.html#object.__del__ > It is not guaranteed that __del__() methods are called for objects > that still exist when the interpreter exits. - Exceptions that occur inside __del__ are ignored instead of raised. This has the potential of hiding bugs. This is also in the Python documentation: > Warning: Due to the precarious circumstances under which __del__() > methods are invoked, exceptions that occur during their execution > are ignored, and a warning is printed to sys.stderr instead. Instead, always close resource handlers when they are no longer in use. This will close the file handler at a specified point in the user's code and not wait until the interpreter chooses to. It is always guaranteed to run. And, if an exception occurs while closing the file handler, the bug will not be ignored. Now, when code receives a ResourceWarning, it will highlight an area that is mishandling resources. It should not simply be silenced, but fixed by closing resources with a context manager. All warnings that were emitted during tests have been cleaned up. To enable warnings, I passed the `-Wa` CLI option to Python. This exposed some mishandling of resources in ImageFile.__init__() and SpiderImagePlugin.loadImageSeries(), they too were fixed.
2019-05-25 19:30:58 +03:00
with Image.open(img) as im:
im = im.convert2byte()
except Exception:
2010-07-31 06:52:47 +04:00
if not isSpiderImage(img):
print(img + " is not a Spider image file")
2010-07-31 06:52:47 +04:00
continue
2019-03-21 16:28:20 +03:00
im.info["filename"] = img
2010-07-31 06:52:47 +04:00
imglist.append(im)
return imglist
2014-05-26 22:56:40 +04:00
2010-07-31 06:52:47 +04:00
# --------------------------------------------------------------------
# For saving images in Spider format
2019-03-21 16:28:20 +03:00
2010-07-31 06:52:47 +04:00
def makeSpiderHeader(im):
2014-05-26 22:56:40 +04:00
nsam, nrow = im.size
2010-07-31 06:52:47 +04:00
lenbyt = nsam * 4 # There are labrec records in the header
2019-09-27 22:58:17 +03:00
labrec = int(1024 / lenbyt)
2014-05-26 22:56:40 +04:00
if 1024 % lenbyt != 0:
labrec += 1
2010-07-31 06:52:47 +04:00
labbyt = labrec * lenbyt
hdr = []
nvalues = int(labbyt / 4)
2010-07-31 06:52:47 +04:00
for i in range(nvalues):
hdr.append(0.0)
if len(hdr) < 23:
return []
# NB these are Fortran indices
2019-03-21 16:28:20 +03:00
hdr[1] = 1.0 # nslice (=1 for an image)
hdr[2] = float(nrow) # number of rows per slice
hdr[5] = 1.0 # iform for 2D image
hdr[12] = float(nsam) # number of pixels per line
2014-05-26 22:56:40 +04:00
hdr[13] = float(labrec) # number of records in file header
hdr[22] = float(labbyt) # total number of bytes in header
hdr[23] = float(lenbyt) # record length in bytes
2010-07-31 06:52:47 +04:00
# adjust for Fortran indexing
hdr = hdr[1:]
hdr.append(0.0)
# pack binary data into a string
hdrstr = []
for v in hdr:
2019-03-21 16:28:20 +03:00
hdrstr.append(struct.pack("f", v))
2010-07-31 06:52:47 +04:00
return hdrstr
2014-05-26 22:56:40 +04:00
2010-07-31 06:52:47 +04:00
def _save(im, fp, filename):
if im.mode[0] != "F":
2019-03-21 16:28:20 +03:00
im = im.convert("F")
2010-07-31 06:52:47 +04:00
hdr = makeSpiderHeader(im)
if len(hdr) < 256:
raise OSError("Error creating Spider header")
2010-07-31 06:52:47 +04:00
# write the SPIDER header
fp.writelines(hdr)
2014-05-26 22:56:40 +04:00
rawmode = "F;32NF" # 32-bit native floating point
2019-03-21 16:28:20 +03:00
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
2010-07-31 06:52:47 +04:00
2014-05-26 22:56:40 +04:00
2010-07-31 06:52:47 +04:00
def _save_spider(im, fp, filename):
# get the filename extension and register it with Image
2015-04-24 11:24:52 +03:00
ext = os.path.splitext(filename)[1]
Image.register_extension(SpiderImageFile.format, ext)
2010-07-31 06:52:47 +04:00
_save(im, fp, filename)
2019-03-21 16:28:20 +03:00
2010-07-31 06:52:47 +04:00
# --------------------------------------------------------------------
2018-03-03 12:54:00 +03:00
Image.register_open(SpiderImageFile.format, SpiderImageFile)
Image.register_save(SpiderImageFile.format, _save_spider)
2010-07-31 06:52:47 +04:00
if __name__ == "__main__":
2018-01-06 13:47:14 +03:00
if len(sys.argv) < 2:
2016-04-27 15:28:46 +03:00
print("Syntax: python SpiderImagePlugin.py [infile] [outfile]")
2010-07-31 06:52:47 +04:00
sys.exit()
filename = sys.argv[1]
if not isSpiderImage(filename):
print("input image must be in Spider format")
2010-07-31 06:52:47 +04:00
sys.exit()
2020-02-18 12:49:05 +03:00
with Image.open(filename) as im:
print("image: " + str(im))
print("format: " + str(im.format))
print("size: " + str(im.size))
print("mode: " + str(im.mode))
print("max, min: ", end=" ")
print(im.getextrema())
if len(sys.argv) > 2:
outfile = sys.argv[2]
# perform some image operation
im = im.transpose(Image.FLIP_LEFT_RIGHT)
print(
"saving a flipped version of %s as %s "
% (os.path.basename(filename), outfile)
)
im.save(outfile, SpiderImageFile.format)