Spaces:
Sleeping
Sleeping
# | |
# 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 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: | |
# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html | |
# | |
# Details about the Spider image format: | |
# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html | |
# | |
from __future__ import annotations | |
import os | |
import struct | |
import sys | |
from typing import IO, TYPE_CHECKING, Any, Tuple, cast | |
from . import Image, ImageFile | |
def isInt(f: Any) -> int: | |
try: | |
i = int(f) | |
if f - i == 0: | |
return 1 | |
else: | |
return 0 | |
except (ValueError, OverflowError): | |
return 0 | |
iforms = [1, 3, -11, -12, -21, -22] | |
# There is no magic number to identify Spider files, so just check a | |
# series of header locations to see if they have reasonable values. | |
# Returns no. of bytes in the header, if it is a valid Spider header, | |
# otherwise returns 0 | |
def isSpiderHeader(t: tuple[float, ...]) -> int: | |
h = (99,) + t # add 1 value so can use spider header index start=1 | |
# header values 1,2,5,12,13,22,23 should be integers | |
for i in [1, 2, 5, 12, 13, 22, 23]: | |
if not isInt(h[i]): | |
return 0 | |
# check iform | |
iform = int(h[5]) | |
if iform not in iforms: | |
return 0 | |
# check other header values | |
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 | |
if labbyt != (labrec * lenbyt): | |
return 0 | |
# looks like a valid header | |
return labbyt | |
def isSpiderImage(filename: str) -> int: | |
with open(filename, "rb") as fp: | |
f = fp.read(92) # read 23 * 4 bytes | |
t = struct.unpack(">23f", f) # try big-endian first | |
hdrlen = isSpiderHeader(t) | |
if hdrlen == 0: | |
t = struct.unpack("<23f", f) # little-endian | |
hdrlen = isSpiderHeader(t) | |
return hdrlen | |
class SpiderImageFile(ImageFile.ImageFile): | |
format = "SPIDER" | |
format_description = "Spider 2D image" | |
_close_exclusive_fp_after_loading = False | |
def _open(self) -> None: | |
# check header | |
n = 27 * 4 # read 27 float values | |
f = self.fp.read(n) | |
try: | |
self.bigendian = 1 | |
t = struct.unpack(">27f", f) # try big-endian first | |
hdrlen = isSpiderHeader(t) | |
if hdrlen == 0: | |
self.bigendian = 0 | |
t = struct.unpack("<27f", f) # little-endian | |
hdrlen = isSpiderHeader(t) | |
if hdrlen == 0: | |
msg = "not a valid Spider file" | |
raise SyntaxError(msg) | |
except struct.error as e: | |
msg = "not a valid Spider file" | |
raise SyntaxError(msg) from e | |
h = (99,) + t # add 1 value : spider header index starts at 1 | |
iform = int(h[5]) | |
if iform != 1: | |
msg = "not a Spider 2D image" | |
raise SyntaxError(msg) | |
self._size = int(h[12]), int(h[2]) # size in pixels (width, height) | |
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 | |
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]) | |
# 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: | |
msg = "inconsistent stack header values" | |
raise SyntaxError(msg) | |
if self.bigendian: | |
self.rawmode = "F;32BF" | |
else: | |
self.rawmode = "F;32F" | |
self._mode = "F" | |
self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] | |
self._fp = self.fp # FIXME: hack | |
def n_frames(self) -> int: | |
return self._nimages | |
def is_animated(self) -> bool: | |
return self._nimages > 1 | |
# 1st image index is zero (although SPIDER imgnumber starts at 1) | |
def tell(self) -> int: | |
if self.imgnumber < 1: | |
return 0 | |
else: | |
return self.imgnumber - 1 | |
def seek(self, frame: int) -> None: | |
if self.istack == 0: | |
msg = "attempt to seek in a non-stack file" | |
raise EOFError(msg) | |
if not self._seek_check(frame): | |
return | |
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: int = 255) -> Image.Image: | |
extrema = self.getextrema() | |
assert isinstance(extrema[0], float) | |
minimum, maximum = cast(Tuple[float, float], extrema) | |
m: float = 1 | |
if maximum != minimum: | |
m = depth / (maximum - minimum) | |
b = -m * minimum | |
return self.point(lambda i: i * m + b).convert("L") | |
if TYPE_CHECKING: | |
from . import ImageTk | |
# returns a ImageTk.PhotoImage object, after rescaling to 0..255 | |
def tkPhotoImage(self) -> ImageTk.PhotoImage: | |
from . import ImageTk | |
return ImageTk.PhotoImage(self.convert2byte(), palette=256) | |
# -------------------------------------------------------------------- | |
# Image series | |
# given a list of filenames, return a list of images | |
def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None: | |
"""create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" | |
if filelist is None or len(filelist) < 1: | |
return None | |
imglist = [] | |
for img in filelist: | |
if not os.path.exists(img): | |
print(f"unable to find {img}") | |
continue | |
try: | |
with Image.open(img) as im: | |
im = im.convert2byte() | |
except Exception: | |
if not isSpiderImage(img): | |
print(f"{img} is not a Spider image file") | |
continue | |
im.info["filename"] = img | |
imglist.append(im) | |
return imglist | |
# -------------------------------------------------------------------- | |
# For saving images in Spider format | |
def makeSpiderHeader(im: Image.Image) -> list[bytes]: | |
nsam, nrow = im.size | |
lenbyt = nsam * 4 # There are labrec records in the header | |
labrec = int(1024 / lenbyt) | |
if 1024 % lenbyt != 0: | |
labrec += 1 | |
labbyt = labrec * lenbyt | |
nvalues = int(labbyt / 4) | |
if nvalues < 23: | |
return [] | |
hdr = [0.0] * nvalues | |
# NB these are Fortran indices | |
hdr[1] = 1.0 # nslice (=1 for an image) | |
hdr[2] = float(nrow) # number of rows per slice | |
hdr[3] = float(nrow) # number of records in the image | |
hdr[5] = 1.0 # iform for 2D image | |
hdr[12] = float(nsam) # number of pixels per line | |
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 | |
# adjust for Fortran indexing | |
hdr = hdr[1:] | |
hdr.append(0.0) | |
# pack binary data into a string | |
return [struct.pack("f", v) for v in hdr] | |
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |
if im.mode[0] != "F": | |
im = im.convert("F") | |
hdr = makeSpiderHeader(im) | |
if len(hdr) < 256: | |
msg = "Error creating Spider header" | |
raise OSError(msg) | |
# write the SPIDER header | |
fp.writelines(hdr) | |
rawmode = "F;32NF" # 32-bit native floating point | |
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) | |
def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |
# get the filename extension and register it with Image | |
filename_ext = os.path.splitext(filename)[1] | |
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext | |
Image.register_extension(SpiderImageFile.format, ext) | |
_save(im, fp, filename) | |
# -------------------------------------------------------------------- | |
Image.register_open(SpiderImageFile.format, SpiderImageFile) | |
Image.register_save(SpiderImageFile.format, _save_spider) | |
if __name__ == "__main__": | |
if len(sys.argv) < 2: | |
print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") | |
sys.exit() | |
filename = sys.argv[1] | |
if not isSpiderImage(filename): | |
print("input image must be in Spider format") | |
sys.exit() | |
with Image.open(filename) as im: | |
print(f"image: {im}") | |
print(f"format: {im.format}") | |
print(f"size: {im.size}") | |
print(f"mode: {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.Transpose.FLIP_LEFT_RIGHT) | |
print( | |
f"saving a flipped version of {os.path.basename(filename)} " | |
f"as {outfile} " | |
) | |
im.save(outfile, SpiderImageFile.format) | |