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, 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 = [
158 ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))
159 ]
160 self._fp = self.fp # FIXME: hack
161
162 @property
163 def n_frames(self) -> int:
164 return self._nimages
165
166 @property
167 def is_animated(self) -> bool:
168 return self._nimages > 1
169
170 # 1st image index is zero (although SPIDER imgnumber starts at 1)
171 def tell(self) -> int:
172 if self.imgnumber < 1:
173 return 0
174 else:
175 return self.imgnumber - 1
176
177 def seek(self, frame: int) -> None:
178 if self.istack == 0:
179 msg = "attempt to seek in a non-stack file"
180 raise EOFError(msg)
181 if not self._seek_check(frame):
182 return
183 self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
184 self.fp = self._fp
185 self.fp.seek(self.stkoffset)
186 self._open()
187
188 # returns a byte image after rescaling to 0..255
189 def convert2byte(self, depth: int = 255) -> Image.Image:
190 extrema = self.getextrema()
191 assert isinstance(extrema[0], float)
192 minimum, maximum = cast(tuple[float, float], extrema)
193 m: float = 1
194 if maximum != minimum:
195 m = depth / (maximum - minimum)
196 b = -m * minimum
197 return self.point(lambda i: i * m + b).convert("L")
198
199 if TYPE_CHECKING:
200 from . import ImageTk
201
202 # returns a ImageTk.PhotoImage object, after rescaling to 0..255
203 def tkPhotoImage(self) -> ImageTk.PhotoImage:
204 from . import ImageTk
205
206 return ImageTk.PhotoImage(self.convert2byte(), palette=256)
207
208
209# --------------------------------------------------------------------
210# Image series
211
212
213# given a list of filenames, return a list of images
214def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None:
215 """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
216 if filelist is None or len(filelist) < 1:
217 return None
218
219 imglist = []
220 for img in filelist:
221 if not os.path.exists(img):
222 print(f"unable to find {img}")
223 continue
224 try:
225 with Image.open(img) as im:
226 im = im.convert2byte()
227 except Exception:
228 if not isSpiderImage(img):
229 print(f"{img} is not a Spider image file")
230 continue
231 im.info["filename"] = img
232 imglist.append(im)
233 return imglist
234
235
236# --------------------------------------------------------------------
237# For saving images in Spider format
238
239
240def makeSpiderHeader(im: Image.Image) -> list[bytes]:
241 nsam, nrow = im.size
242 lenbyt = nsam * 4 # There are labrec records in the header
243 labrec = int(1024 / lenbyt)
244 if 1024 % lenbyt != 0:
245 labrec += 1
246 labbyt = labrec * lenbyt
247 nvalues = int(labbyt / 4)
248 if nvalues < 23:
249 return []
250
251 hdr = [0.0] * nvalues
252
253 # NB these are Fortran indices
254 hdr[1] = 1.0 # nslice (=1 for an image)
255 hdr[2] = float(nrow) # number of rows per slice
256 hdr[3] = float(nrow) # number of records in the image
257 hdr[5] = 1.0 # iform for 2D image
258 hdr[12] = float(nsam) # number of pixels per line
259 hdr[13] = float(labrec) # number of records in file header
260 hdr[22] = float(labbyt) # total number of bytes in header
261 hdr[23] = float(lenbyt) # record length in bytes
262
263 # adjust for Fortran indexing
264 hdr = hdr[1:]
265 hdr.append(0.0)
266 # pack binary data into a string
267 return [struct.pack("f", v) for v in hdr]
268
269
270def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
271 if im.mode[0] != "F":
272 im = im.convert("F")
273
274 hdr = makeSpiderHeader(im)
275 if len(hdr) < 256:
276 msg = "Error creating Spider header"
277 raise OSError(msg)
278
279 # write the SPIDER header
280 fp.writelines(hdr)
281
282 rawmode = "F;32NF" # 32-bit native floating point
283 ImageFile._save(
284 im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
285 )
286
287
288def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
289 # get the filename extension and register it with Image
290 filename_ext = os.path.splitext(filename)[1]
291 ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
292 Image.register_extension(SpiderImageFile.format, ext)
293 _save(im, fp, filename)
294
295
296# --------------------------------------------------------------------
297
298
299Image.register_open(SpiderImageFile.format, SpiderImageFile)
300Image.register_save(SpiderImageFile.format, _save_spider)
301
302if __name__ == "__main__":
303 if len(sys.argv) < 2:
304 print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]")
305 sys.exit()
306
307 filename = sys.argv[1]
308 if not isSpiderImage(filename):
309 print("input image must be in Spider format")
310 sys.exit()
311
312 with Image.open(filename) as im:
313 print(f"image: {im}")
314 print(f"format: {im.format}")
315 print(f"size: {im.size}")
316 print(f"mode: {im.mode}")
317 print("max, min: ", end=" ")
318 print(im.getextrema())
319
320 if len(sys.argv) > 2:
321 outfile = sys.argv[2]
322
323 # perform some image operation
324 im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
325 print(
326 f"saving a flipped version of {os.path.basename(filename)} "
327 f"as {outfile} "
328 )
329 im.save(outfile, SpiderImageFile.format)