CoolFace
Apppublic

Aluode/PerceptionLabPortable

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