Coverage for pyguymer3/image/makePngSrc/createStreamAdaptive.py: 95%

103 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 createStreamAdaptive( 

5 arrUint8, 

6 arrInt16, 

7 /, 

8) -> bytearray: 

9 """Create a PNG image data stream of an image using "adaptive" filtering (as 

10 suggested in the PNG specification [2]_). 

11 

12 Parameters 

13 ---------- 

14 arrUint8 : numpy.ndarray 

15 A "height * width * colour" unsigned 8-bit integer NumPy array. 

16 arrInt16 : numpy.ndarray 

17 A signed 16-bit integer NumPy array copy of ``arrUint8``. 

18 

19 Returns 

20 ------- 

21 stream : bytearray 

22 The PNG image data stream. 

23 

24 Notes 

25 ----- 

26 

27 Copyright 2017 Thomas Guymer [1]_ 

28 

29 References 

30 ---------- 

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

32 .. [2] PNG Specification (Third Edition), https://www.w3.org/TR/png-3/ 

33 """ 

34 

35 # Import special modules ... 

36 try: 

37 import numpy 

38 except: 

39 raise Exception("\"numpy\" is not installed; run \"pip install --user numpy\"") from None 

40 

41 # Import sub-functions ... 

42 from .paethFilter import paethFilter 

43 

44 # ************************************************************************** 

45 

46 # Check input ... 

47 assert arrUint8.dtype == "uint8", f"the NumPy array is not 8-bit (\"{arrUint8.dtype}\")" 

48 assert arrInt16.dtype == "int16", f"the NumPy array is not 16-bit (\"{arrInt16.dtype}\")" 

49 assert arrUint8.ndim == 3, f"the NumPy array is not 3D (\"{arrUint8.ndim:d}\")" 

50 match arrUint8.shape[2]: 

51 case 1: 

52 pass 

53 case 3: 

54 pass 

55 case _: 

56 raise ValueError(f"the NumPy array does not have either 1 or 3 colour channels (\"{arrUint8.shape[2]:d}\")") from None 

57 assert arrUint8.shape == arrInt16.shape, "the NumPy arrays do not have the same shape" 

58 

59 # ************************************************************************** 

60 

61 # Create short-hands ... 

62 ny, nx, nc = arrUint8.shape 

63 

64 # Initialize arrays and bytearray ... 

65 scanline0 = numpy.zeros( 

66 (nc, nx), 

67 dtype = numpy.uint8, 

68 ) 

69 scanline1 = numpy.zeros( 

70 (nc, nx), 

71 dtype = numpy.uint8, 

72 ) 

73 scanline2 = numpy.zeros( 

74 (nc, nx), 

75 dtype = numpy.uint8, 

76 ) 

77 scanline3 = numpy.zeros( 

78 (nc, nx), 

79 dtype = numpy.uint8, 

80 ) 

81 scanline4 = numpy.zeros( 

82 (nc, nx), 

83 dtype = numpy.uint8, 

84 ) 

85 stream = bytearray() 

86 

87 # Loop over scanlines ... 

88 for iy in range(ny): 

89 # Initialize best answer and figure-of-merit ... 

90 bestStream = bytearray() 

91 minTotal = numpy.iinfo("uint64").max 

92 

93 # Calculate scanline for "none" filter ... 

94 for ix in range(nx): 

95 scanline0[:, ix] = arrUint8[iy, ix, :] 

96 

97 # Calculate scanline for "sub" filter ... 

98 for ix in range(nx): 

99 for ic in range(nc): 

100 if ix == 0: 

101 p1 = numpy.int16(0) 

102 else: 

103 p1 = arrInt16[iy, ix - 1, ic] 

104 diff = arrInt16[iy, ix, ic] - p1 

105 diff = numpy.mod(diff, 256) 

106 scanline1[ic, ix] = diff.astype(numpy.uint8) 

107 

108 # Calculate scanline for "up" filter ... 

109 for ix in range(nx): 

110 for ic in range(nc): 

111 if iy == 0: 

112 p1 = numpy.int16(0) 

113 else: 

114 p1 = arrInt16[iy - 1, ix, ic] 

115 diff = arrInt16[iy, ix, ic] - p1 

116 diff = numpy.mod(diff, 256) 

117 scanline2[ic, ix] = diff.astype(numpy.uint8) 

118 

119 # Calculate scanline for "average" filter ... 

120 for ix in range(nx): 

121 for ic in range(nc): 

122 if ix == 0: 

123 p1 = numpy.int16(0) 

124 else: 

125 p1 = arrInt16[iy, ix - 1, ic] 

126 if iy == 0: 

127 p2 = numpy.int16(0) 

128 else: 

129 p2 = arrInt16[iy - 1, ix, ic] 

130 diff = arrInt16[iy, ix, ic] - ((p1 + p2) // numpy.int16(2)) 

131 diff = numpy.mod(diff, 256) 

132 scanline3[ic, ix] = diff.astype(numpy.uint8) 

133 

134 # Calculate scanline for "Paeth" filter ... 

135 for ix in range(nx): 

136 for ic in range(nc): 

137 if ix == 0: 

138 p1 = numpy.int16(0) 

139 else: 

140 p1 = arrInt16[iy, ix - 1, ic] 

141 if iy == 0: 

142 p2 = numpy.int16(0) 

143 else: 

144 p2 = arrInt16[iy - 1, ix, ic] 

145 if ix == 0 or iy == 0: 

146 p3 = numpy.int16(0) 

147 else: 

148 p3 = arrInt16[iy - 1, ix - 1, ic] 

149 diff = arrInt16[iy, ix, ic] - paethFilter(p1, p2, p3) 

150 diff = numpy.mod(diff, 256) 

151 scanline4[ic, ix] = diff.astype(numpy.uint8) 

152 

153 # Check if the "none" filter is likely to be the best stream ... 

154 if scanline0.sum() < minTotal: 

155 bestStream = bytearray() 

156 bestStream += numpy.uint8(0).tobytes() 

157 for ix in range(nx): 

158 bestStream += scanline0[:, ix].tobytes() 

159 minTotal = scanline0.sum() 

160 

161 # Check if the "sub" filter is likely to be the best stream ... 

162 if scanline1.sum() < minTotal: 

163 bestStream = bytearray() 

164 bestStream += numpy.uint8(1).tobytes() 

165 for ix in range(nx): 

166 bestStream += scanline1[:, ix].tobytes() 

167 minTotal = scanline1.sum() 

168 

169 # Check if the "up" filter is likely to be the best stream ... 

170 if scanline2.sum() < minTotal: 

171 bestStream = bytearray() 

172 bestStream += numpy.uint8(2).tobytes() 

173 for ix in range(nx): 

174 bestStream += scanline2[:, ix].tobytes() 

175 minTotal = scanline2.sum() 

176 

177 # Check if the "average" filter is likely to be the best stream ... 

178 if scanline3.sum() < minTotal: 

179 bestStream = bytearray() 

180 bestStream += numpy.uint8(3).tobytes() 

181 for ix in range(nx): 

182 bestStream += scanline3[:, ix].tobytes() 

183 minTotal = scanline3.sum() 

184 

185 # Check if the "Paeth" filter is likely to be the best stream ... 

186 if scanline4.sum() < minTotal: 

187 bestStream = bytearray() 

188 bestStream += numpy.uint8(4).tobytes() 

189 for ix in range(nx): 

190 bestStream += scanline4[:, ix].tobytes() 

191 minTotal = scanline4.sum() 

192 

193 # Check that a best stream was found ... 

194 assert len(bestStream) > 0, f"no best stream was found for scanline {iy:,d}" 

195 

196 # Add the best stream for this scanline to the total stream ... 

197 stream += bestStream 

198 

199 # Return answer ... 

200 return stream