SpiderImagePlugin.py 10 KB

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