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