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

1#!/usr/bin/env python3 

2 

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 

31 

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. 

35 

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 

90 

91 Notes 

92 ----- 

93 Copyright 2017 Thomas Guymer [1]_ 

94 

95 References 

96 ---------- 

97 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3 

98 """ 

99 

100 # Import modules ... 

101 import json 

102 import os 

103 

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 

109 

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 

114 

115 # Find image size ... 

116 ny, nx = img0.shape # [px], [px] 

117 

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) 

121 

122 # Create uint8 image that will be passed to the external function ... 

123 img2 = numpy.empty((ny, nx, 3), dtype = numpy.uint8) 

124 

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 

134 

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) 

139 

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)][:] 

153 

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