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.
|
|
|
|
#
|
|
|
|
|
|
|
|
##
|
2022-05-14 07:46:46 +03:00
|
|
|
# Image plugin for the Spider image format. This format is used
|
2010-07-31 06:52:47 +04:00
|
|
|
# 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
|
|
|
|
|
2019-07-06 23:40:53 +03:00
|
|
|
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):
|
2015-11-17 17:18:01 +03:00
|
|
|
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"
|
2017-03-15 02:16:38 +03:00
|
|
|
_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:
|
2012-10-11 07:52:53 +04:00
|
|
|
raise SyntaxError("not a valid Spider file")
|
2020-06-21 13:13:35 +03:00
|
|
|
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:
|
2012-10-11 07:52:53 +04:00
|
|
|
raise SyntaxError("not a Spider 2D image")
|
2010-07-31 06:52:47 +04:00
|
|
|
|
2018-09-30 05:58:02 +03: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
|
2015-04-15 03:43:05 +03:00
|
|
|
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
|
2015-04-15 03:43:05 +03:00
|
|
|
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:
|
2012-10-11 07:52:53 +04:00
|
|
|
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))]
|
2022-04-13 02:54:17 +03:00
|
|
|
self._fp = self.fp # FIXME: hack
|
2010-07-31 06:52:47 +04:00
|
|
|
|
2015-04-15 03:43:05 +03: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:
|
2016-02-23 16:08:39 +03:00
|
|
|
raise EOFError("attempt to seek in a non-stack file")
|
2017-09-30 06:32:43 +03:00
|
|
|
if not self._seek_check(frame):
|
|
|
|
return
|
2010-07-31 06:52:47 +04:00
|
|
|
self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
|
2022-04-13 02:54:17 +03:00
|
|
|
self.fp = self._fp
|
2010-07-31 06:52:47 +04:00
|
|
|
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)
|
|
|
|
|
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"""
|
2014-01-08 06:24:21 +04:00
|
|
|
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):
|
2020-07-16 12:43:29 +03:00
|
|
|
print(f"unable to find {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()
|
2018-11-17 00:51:52 +03:00
|
|
|
except Exception:
|
2010-07-31 06:52:47 +04:00
|
|
|
if not isSpiderImage(img):
|
2012-10-16 06:27:35 +04:00
|
|
|
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
|
2014-05-26 22:18:48 +04:00
|
|
|
nvalues = int(labbyt / 4)
|
2022-01-12 07:17:04 +03:00
|
|
|
if nvalues < 23:
|
|
|
|
return []
|
|
|
|
|
|
|
|
hdr = []
|
2010-07-31 06:52:47 +04:00
|
|
|
for i in range(nvalues):
|
|
|
|
hdr.append(0.0)
|
|
|
|
|
|
|
|
# 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
|
2022-01-12 09:29:25 +03:00
|
|
|
hdr[3] = float(nrow) # number of records in the image
|
2019-03-21 16:28:20 +03:00
|
|
|
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
|
2022-01-12 07:17:04 +03:00
|
|
|
return [struct.pack("f", v) for v in hdr]
|
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(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:
|
2019-09-30 17:56:31 +03:00
|
|
|
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]
|
2016-06-19 08:13:02 +03:00
|
|
|
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
|
|
|
|
2015-07-04 16:29:58 +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:
|
2021-05-08 05:37:06 +03:00
|
|
|
print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]")
|
2010-07-31 06:52:47 +04:00
|
|
|
sys.exit()
|
|
|
|
|
|
|
|
filename = sys.argv[1]
|
|
|
|
if not isSpiderImage(filename):
|
2012-10-16 06:27:35 +04:00
|
|
|
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
|
2022-01-15 01:02:31 +03:00
|
|
|
im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
2020-02-18 12:49:05 +03:00
|
|
|
print(
|
2020-07-16 12:43:29 +03:00
|
|
|
f"saving a flipped version of {os.path.basename(filename)} "
|
|
|
|
f"as {outfile} "
|
2020-02-18 12:49:05 +03:00
|
|
|
)
|
|
|
|
im.save(outfile, SpiderImageFile.format)
|