Coverage for pyguymer3/image/save_array_as_image.py: 83%
41 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
1#!/usr/bin/env python3
3# Define function ...
4def save_array_as_image(
5 img0,
6 fname,
7 /,
8 *,
9 calcAdaptive = True,
10 calcAverage = True,
11 calcNone = True,
12 calcPaeth = True,
13 calcSub = True,
14 calcUp = True,
15 chunksize = 1048576,
16 ct = "grey",
17 debug = __debug__,
18 dpi = None,
19 exiftoolPath = None,
20 form = "png",
21 gifsiclePath = None,
22 jpegtranPath = None,
23 modTime = None,
24 optipngPath = None,
25 pc_bot = 0.0,
26 pc_top = 0.0,
27 scale = False,
28 timeout = 60.0,
29):
30 """Save an array as an image
32 This function accepts a NumPy array, with optional scaling and/or colour
33 mapping, and saves it as an image. Currently only "png" and "ppm" formats
34 are available.
36 Parameters
37 ----------
38 img0 : numpy.ndarray
39 a 2D NumPy array of any type with shape (ny,nx)
40 fname : str
41 output file name
42 calcAdaptive : bool, optional
43 See :py:func:`pyguymer3.image.makePng` for the documentation.
44 calcAverage : bool, optional
45 See :py:func:`pyguymer3.image.makePng` for the documentation.
46 calcNone : bool, optional
47 See :py:func:`pyguymer3.image.makePng` for the documentation.
48 calcPaeth : bool, optional
49 See :py:func:`pyguymer3.image.makePng` for the documentation.
50 calcSub : bool, optional
51 See :py:func:`pyguymer3.image.makePng` for the documentation.
52 calcUp : bool, optional
53 See :py:func:`pyguymer3.image.makePng` for the documentation.
54 chunksize : int, optional
55 the size of the chunks of any files which are read in (in bytes)
56 ct : str, optional
57 the colour table to apply (the default is no colour mapping, i.e.,
58 greyscale)
59 debug : bool, optional
60 Print debug messages.
61 dpi : None or float or int, optional
62 See :py:func:`pyguymer3.image.makePng` for the documentation.
63 exiftoolPath : str, optional
64 the path to the "exiftool" binary (if not provided then Python will attempt to
65 find the binary itself)
66 form : str, optional
67 output image format
68 gifsiclePath : str, optional
69 the path to the "gifsicle" binary (if not provided then Python will attempt to
70 find the binary itself)
71 jpegtranPath : str, optional
72 the path to the "jpegtran" binary (if not provided then Python will attempt to
73 find the binary itself)
74 modTime : None or datetime.datetime, optional
75 See :py:func:`pyguymer3.image.makePng` for the documentation.
76 optipngPath : str, optional
77 the path to the "optipng" binary (if not provided then Python will attempt to
78 find the binary itself)
79 pc_bot : float, optional
80 the percentage to clip off the bottom of the histogram, if scaling is
81 requested (default 0.0)
82 pc_top : float, optional
83 the percentage to clip off the top of the histogram, if scaling is
84 requested (default 0.0)
85 scale : bool, optional
86 Does the input need scaling? If not, then the input array must be ≥ 0
87 and ≤ 255. (default False)
88 timeout : float, optional
89 the timeout for any requests/subprocess calls
91 Notes
92 -----
93 Copyright 2017 Thomas Guymer [1]_
95 References
96 ----------
97 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3
98 """
100 # Import modules ...
101 import json
102 import os
104 # Import special modules ...
105 try:
106 import numpy
107 except:
108 raise Exception("\"numpy\" is not installed; run \"pip install --user numpy\"") from None
110 # Import sub-functions ...
111 from .optimise_image import optimise_image
112 from .save_array_as_PPM import save_array_as_PPM
113 from .save_array_as_PNG import save_array_as_PNG
115 # Find image size ...
116 ny, nx = img0.shape # [px], [px]
118 # Load colour tables ...
119 with open(f"{os.path.dirname(__file__)}/../data/json/colourTables.json", "rt", encoding = "utf-8") as fObj:
120 cts = json.load(fObj)
122 # Create uint8 image that will be passed to the external function ...
123 img2 = numpy.empty((ny, nx, 3), dtype = numpy.uint8)
125 # Check if scaling is required ...
126 if scale:
127 # Check input values ...
128 if pc_bot < 0.0:
129 raise Exception("pc_bot < 0.0") from None
130 if pc_top < 0.0:
131 raise Exception("pc_top < 0.0") from None
132 if (pc_bot + pc_top) > 100.0:
133 raise Exception("(pc_bot + pc_top) > 100.0") from None
135 # Create copy and find the percentiles ...
136 img1 = img0.astype(numpy.float64)
137 p_lo = numpy.percentile(img1, pc_bot)
138 p_hi = numpy.percentile(img1, 100.0 - pc_top)
140 # Scale the image, clip scale image, convert scaled image to correct
141 # type, clean up ...
142 img1 = 255.0 * (img1 - p_lo) / (p_hi - p_lo)
143 numpy.place(img1, img1 > 255.0, 255.0)
144 numpy.place(img1, img1 < 0.0, 0.0)
145 for ix in range(nx):
146 for iy in range(ny):
147 img2[iy, ix, :] = cts[ct][img1[iy, ix].astype(numpy.uint8)][:]
148 else:
149 # Convert image to correct type ...
150 for ix in range(nx):
151 for iy in range(ny):
152 img2[iy, ix, :] = cts[ct][img0[iy, ix].astype(numpy.uint8)][:]
154 # Save image ...
155 match form:
156 case "png":
157 save_array_as_PNG(
158 img2,
159 fname,
160 calcAdaptive = calcAdaptive,
161 calcAverage = calcAverage,
162 calcNone = calcNone,
163 calcPaeth = calcPaeth,
164 calcSub = calcSub,
165 calcUp = calcUp,
166 debug = debug,
167 dpi = dpi,
168 modTime = modTime,
169 )
170 optimise_image(
171 fname,
172 chunksize = chunksize,
173 debug = debug,
174 exiftoolPath = exiftoolPath,
175 gifsiclePath = gifsiclePath,
176 jpegtranPath = jpegtranPath,
177 optipngPath = optipngPath,
178 pool = None,
179 strip = True,
180 timeout = timeout,
181 )
182 case "ppm":
183 save_array_as_PPM(
184 img2,
185 fname,
186 )
187 case _:
188 # Crash ...
189 raise ValueError(f"\"form\" is an unexpected value ({repr(form)})") from None