1#
2# The Python Imaging Library.
3#
4# SPIDER image file handling
5#
6# History:
7# 2004-08-02 Created BB
8# 2006-03-02 added save method
9# 2006-03-13 added support for stack images
10#
11# Copyright (c) 2004 by Health Research Inc. (HRI) RENSSELAER, NY 12144.
12# Copyright (c) 2004 by William Baxter.
13# Copyright (c) 2004 by Secret Labs AB.
14# Copyright (c) 2004 by Fredrik Lundh.
15#
16
17##
18# Image plugin for the Spider image format. This format is used
19# by the SPIDER software, in processing image data from electron
20# microscopy and tomography.
21##
22
23#
24# SpiderImagePlugin.py
25#
26# The Spider image format is used by SPIDER software, in processing
27# image data from electron microscopy and tomography.
28#
29# Spider home page:
30# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html
31#
32# Details about the Spider image format:
33# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html
34#
35from __future__ import annotations
36
37import os
38import struct
39import sys
40from typing import IO, TYPE_CHECKING, Any, Tuple, cast
41
42from . import Image, ImageFile
43
44
45def isInt(f: Any) -> int:
46 try:
47 i = int(f)
48 if f - i == 0:
49 return 1
50 else:
51 return 0
52 except (ValueError, OverflowError):
53 return 0
54
55
56iforms = [1, 3, -11, -12, -21, -22]
57
58
59# There is no magic number to identify Spider files, so just check a
60# series of header locations to see if they have reasonable values.
61# Returns no. of bytes in the header, if it is a valid Spider header,
62# otherwise returns 0
63
64
65def isSpiderHeader(t: tuple[float, ...]) -> int:
66 h = (99,) + t # add 1 value so can use spider header index start=1
67 # header values 1,2,5,12,13,22,23 should be integers
68 for i in [1, 2, 5, 12, 13, 22, 23]:
69 if not isInt(h[i]):
70 return 0
71 # check iform
72 iform = int(h[5])
73 if iform not in iforms:
74 return 0
75 # check other header values
76 labrec = int(h[13]) # no. records in file header
77 labbyt = int(h[22]) # total no. of bytes in header
78 lenbyt = int(h[23]) # record length in bytes
79 if labbyt != (labrec * lenbyt):
80 return 0
81 # looks like a valid header
82 return labbyt
83
84
85def isSpiderImage(filename: str) -> int:
86 with open(filename, "rb") as fp:
87 f = fp.read(92) # read 23 * 4 bytes
88 t = struct.unpack(">23f", f) # try big-endian first
89 hdrlen = isSpiderHeader(t)
90 if hdrlen == 0:
91 t = struct.unpack("<23f", f) # little-endian
92 hdrlen = isSpiderHeader(t)
93 return hdrlen
94
95
96class SpiderImageFile(ImageFile.ImageFile):
97 format = "SPIDER"
98 format_description = "Spider 2D image"
99 _close_exclusive_fp_after_loading = False
100
101 def _open(self) -> None:
102 # check header
103 n = 27 * 4 # read 27 float values
104 f = self.fp.read(n)
105
106 try:
107 self.bigendian = 1
108 t = struct.unpack(">27f", f) # try big-endian first
109 hdrlen = isSpiderHeader(t)
110 if hdrlen == 0:
111 self.bigendian = 0
112 t = struct.unpack("<27f", f) # little-endian
113 hdrlen = isSpiderHeader(t)
114 if hdrlen == 0:
115 msg = "not a valid Spider file"
116 raise SyntaxError(msg)
117 except struct.error as e:
118 msg = "not a valid Spider file"
119 raise SyntaxError(msg) from e
120
121 h = (99,) + t # add 1 value : spider header index starts at 1
122 iform = int(h[5])
123 if iform != 1:
124 msg = "not a Spider 2D image"
125 raise SyntaxError(msg)
126
127 self._size = int(h[12]), int(h[2]) # size in pixels (width, height)
128 self.istack = int(h[24])
129 self.imgnumber = int(h[27])
130
131 if self.istack == 0 and self.imgnumber == 0:
132 # stk=0, img=0: a regular 2D image
133 offset = hdrlen
134 self._nimages = 1
135 elif self.istack > 0 and self.imgnumber == 0:
136 # stk>0, img=0: Opening the stack for the first time
137 self.imgbytes = int(h[12]) * int(h[2]) * 4
138 self.hdrlen = hdrlen
139 self._nimages = int(h[26])
140 # Point to the first image in the stack
141 offset = hdrlen * 2
142 self.imgnumber = 1
143 elif self.istack == 0 and self.imgnumber > 0:
144 # stk=0, img>0: an image within the stack
145 offset = hdrlen + self.stkoffset
146 self.istack = 2 # So Image knows it's still a stack
147 else:
148 msg = "inconsistent stack header values"
149 raise SyntaxError(msg)
150
151 if self.bigendian:
152 self.rawmode = "F;32BF"
153 else:
154 self.rawmode = "F;32F"
155 self._mode = "F"
156
157 self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))]
158 self._fp = self.fp # FIXME: hack
159
160 @property
161 def n_frames(self) -> int:
162 return self._nimages
163
164 @property
165 def is_animated(self) -> bool:
166 return self._nimages > 1
167
168 # 1st image index is zero (although SPIDER imgnumber starts at 1)
169 def tell(self) -> int:
170 if self.imgnumber < 1:
171 return 0
172 else:
173 return self.imgnumber - 1
174
175 def seek(self, frame: int) -> None:
176 if self.istack == 0:
177 msg = "attempt to seek in a non-stack file"
178 raise EOFError(msg)
179 if not self._seek_check(frame):
180 return
181 self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
182 self.fp = self._fp
183 self.fp.seek(self.stkoffset)
184 self._open()
185
186 # returns a byte image after rescaling to 0..255
187 def convert2byte(self, depth: int = 255) -> Image.Image:
188 extrema = self.getextrema()
189 assert isinstance(extrema[0], float)
190 minimum, maximum = cast(Tuple[float, float], extrema)
191 m: float = 1
192 if maximum != minimum:
193 m = depth / (maximum - minimum)
194 b = -m * minimum
195 return self.point(lambda i: i * m + b).convert("L")
196
197 if TYPE_CHECKING:
198 from . import ImageTk
199
200 # returns a ImageTk.PhotoImage object, after rescaling to 0..255
201 def tkPhotoImage(self) -> ImageTk.PhotoImage:
202 from . import ImageTk
203
204 return ImageTk.PhotoImage(self.convert2byte(), palette=256)
205
206
207# --------------------------------------------------------------------
208# Image series
209
210
211# given a list of filenames, return a list of images
212def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None:
213 """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
214 if filelist is None or len(filelist) < 1:
215 return None
216
217 imglist = []
218 for img in filelist:
219 if not os.path.exists(img):
220 print(f"unable to find {img}")
221 continue
222 try:
223 with Image.open(img) as im:
224 im = im.convert2byte()
225 except Exception:
226 if not isSpiderImage(img):
227 print(f"{img} is not a Spider image file")
228 continue
229 im.info["filename"] = img
230 imglist.append(im)
231 return imglist
232
233
234# --------------------------------------------------------------------
235# For saving images in Spider format
236
237
238def makeSpiderHeader(im: Image.Image) -> list[bytes]:
239 nsam, nrow = im.size
240 lenbyt = nsam * 4 # There are labrec records in the header
241 labrec = int(1024 / lenbyt)
242 if 1024 % lenbyt != 0:
243 labrec += 1
244 labbyt = labrec * lenbyt
245 nvalues = int(labbyt / 4)
246 if nvalues < 23:
247 return []
248
249 hdr = [0.0] * nvalues
250
251 # NB these are Fortran indices
252 hdr[1] = 1.0 # nslice (=1 for an image)
253 hdr[2] = float(nrow) # number of rows per slice
254 hdr[3] = float(nrow) # number of records in the image
255 hdr[5] = 1.0 # iform for 2D image
256 hdr[12] = float(nsam) # number of pixels per line
257 hdr[13] = float(labrec) # number of records in file header
258 hdr[22] = float(labbyt) # total number of bytes in header
259 hdr[23] = float(lenbyt) # record length in bytes
260
261 # adjust for Fortran indexing
262 hdr = hdr[1:]
263 hdr.append(0.0)
264 # pack binary data into a string
265 return [struct.pack("f", v) for v in hdr]
266
267
268def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
269 if im.mode[0] != "F":
270 im = im.convert("F")
271
272 hdr = makeSpiderHeader(im)
273 if len(hdr) < 256:
274 msg = "Error creating Spider header"
275 raise OSError(msg)
276
277 # write the SPIDER header
278 fp.writelines(hdr)
279
280 rawmode = "F;32NF" # 32-bit native floating point
281 ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
282
283
284def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
285 # get the filename extension and register it with Image
286 filename_ext = os.path.splitext(filename)[1]
287 ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
288 Image.register_extension(SpiderImageFile.format, ext)
289 _save(im, fp, filename)
290
291
292# --------------------------------------------------------------------
293
294
295Image.register_open(SpiderImageFile.format, SpiderImageFile)
296Image.register_save(SpiderImageFile.format, _save_spider)
297
298if __name__ == "__main__":
299 if len(sys.argv) < 2:
300 print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]")
301 sys.exit()
302
303 filename = sys.argv[1]
304 if not isSpiderImage(filename):
305 print("input image must be in Spider format")
306 sys.exit()
307
308 with Image.open(filename) as im:
309 print(f"image: {im}")
310 print(f"format: {im.format}")
311 print(f"size: {im.size}")
312 print(f"mode: {im.mode}")
313 print("max, min: ", end=" ")
314 print(im.getextrema())
315
316 if len(sys.argv) > 2:
317 outfile = sys.argv[2]
318
319 # perform some image operation
320 im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
321 print(
322 f"saving a flipped version of {os.path.basename(filename)} "
323 f"as {outfile} "
324 )
325 im.save(outfile, SpiderImageFile.format)