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
« 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 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]_).
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``.
19 Returns
20 -------
21 stream : bytearray
22 The PNG image data stream.
24 Notes
25 -----
27 Copyright 2017 Thomas Guymer [1]_
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 """
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
41 # Import sub-functions ...
42 from .paethFilter import paethFilter
44 # **************************************************************************
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"
59 # **************************************************************************
61 # Create short-hands ...
62 ny, nx, nc = arrUint8.shape
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()
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
93 # Calculate scanline for "none" filter ...
94 for ix in range(nx):
95 scanline0[:, ix] = arrUint8[iy, ix, :]
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)
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)
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)
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)
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()
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()
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()
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()
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()
193 # Check that a best stream was found ...
194 assert len(bestStream) > 0, f"no best stream was found for scanline {iy:,d}"
196 # Add the best stream for this scanline to the total stream ...
197 stream += bestStream
199 # Return answer ...
200 return stream