Skip to content

Commit bfb86dc

Browse files
committed
Enhance documentation & edge filtering
1 parent e93b3d8 commit bfb86dc

File tree

4 files changed

+66
-28
lines changed

4 files changed

+66
-28
lines changed

adafruit_pycamera/imageprocessing.py

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -70,29 +70,32 @@ def _np_convolve_same(arr, coeffs):
7070
EIGHT_BITS = 0b11111111
7171

7272

73-
def bitmap_as_array(bitmap):
73+
def _bitmap_as_array(bitmap):
7474
"""Create an array object that accesses the bitmap data"""
7575
if bitmap.width % 2:
7676
raise ValueError("Can only work on even-width bitmaps")
7777
return np.frombuffer(bitmap, dtype=np.uint16).reshape((bitmap.height, bitmap.width))
7878

7979

80-
def array_cast(arr, dtype):
80+
def _array_cast(arr, dtype):
8181
"""Cast an array to a given type and shape. The new type must match the original
8282
type's size in bytes."""
8383
return np.frombuffer(arr, dtype=dtype).reshape(arr.shape)
8484

8585

8686
def bitmap_to_components_rgb565(bitmap):
87-
"""Convert a RGB65_BYTESWAPPED image to int16 components in the [0,255] inclusive range
87+
"""Convert a RGB565_BYTESWAPPED image to int16 components in the [0,255] inclusive range
8888
8989
This requires higher memory than uint8, but allows more arithmetic on pixel values;
90-
converting back to bitmap clamps values to the appropriate range."""
91-
arr = bitmap_as_array(bitmap)
90+
converting back to bitmap clamps values to the appropriate range.
91+
92+
This only works on images whose width is a multiple of 2 pixels.
93+
"""
94+
arr = _bitmap_as_array(bitmap)
9295
arr.byteswap(inplace=True)
93-
r = array_cast(np.right_shift(arr, 8) & 0xF8, np.int16)
94-
g = array_cast(np.right_shift(arr, 3) & 0xFC, np.int16)
95-
b = array_cast(np.left_shift(arr, 3) & 0xF8, np.int16)
96+
r = _array_cast(np.right_shift(arr, 8) & 0xF8, np.int16)
97+
g = _array_cast(np.right_shift(arr, 3) & 0xFC, np.int16)
98+
b = _array_cast(np.left_shift(arr, 3) & 0xF8, np.int16)
9699
arr.byteswap(inplace=True)
97100
return r, g, b
98101

@@ -101,27 +104,27 @@ def bitmap_from_components_inplace_rgb565(
101104
bitmap, r, g, b
102105
): # pylint: disable=invalid-name
103106
"""Update a bitmap in-place with new RGB values"""
104-
dest = bitmap_as_array(bitmap)
105-
r = array_cast(np.maximum(np.minimum(r, 255), 0), np.uint16)
106-
g = array_cast(np.maximum(np.minimum(g, 255), 0), np.uint16)
107-
b = array_cast(np.maximum(np.minimum(b, 255), 0), np.uint16)
107+
dest = _bitmap_as_array(bitmap)
108+
r = _array_cast(np.maximum(np.minimum(r, 255), 0), np.uint16)
109+
g = _array_cast(np.maximum(np.minimum(g, 255), 0), np.uint16)
110+
b = _array_cast(np.maximum(np.minimum(b, 255), 0), np.uint16)
108111
dest[:] = np.left_shift(r & 0xF8, 8)
109112
dest[:] |= np.left_shift(g & 0xFC, 3)
110113
dest[:] |= np.right_shift(b, 3)
111114
dest.byteswap(inplace=True)
112115
return bitmap
113116

114117

115-
def as_flat(arr):
116-
"""Flatten an array, ensuring no copy is made"""
118+
def _as_flat(arr):
119+
"""Internal routine to flatten an array, ensuring no copy is made"""
117120
return np.frombuffer(arr, arr.dtype)
118121

119122

120123
def buffer_from_components_rgb888(r, g, b):
121124
"""Convert the individual color components to a single RGB888 buffer in memory"""
122-
r = as_flat(r)
123-
g = as_flat(g)
124-
b = as_flat(b)
125+
r = _as_flat(r)
126+
g = _as_flat(g)
127+
b = _as_flat(b)
125128
r = np.maximum(np.minimum(r, 0x3F), 0)
126129
g = np.maximum(np.minimum(g, 0x3F), 0)
127130
b = np.maximum(np.minimum(b, 0x3F), 0)
@@ -139,21 +142,26 @@ def symmetric_filter_inplace(data, coeffs, scale):
139142
many common kinds of image filters such as blur, sharpen, and edge detect.
140143
141144
Normally, scale is sum(coeffs)."""
142-
# First run the filter across each row
145+
row_filter_inplace(data, coeffs, scale)
146+
column_filter_inplace(data, coeffs, scale)
147+
148+
149+
def row_filter_inplace(data, coeffs, scale):
150+
"""Apply a filter to data in rows, changing it in place"""
143151
n_rows = data.shape[0]
144152
for i in range(n_rows):
145153
data[i, :] = _np_convolve_same(data[i, :], coeffs) // scale
146154

147-
# Run the filter across each column
155+
156+
def column_filter_inplace(data, coeffs, scale):
157+
"""Apply a filter to data in columns, changing it in place"""
148158
n_cols = data.shape[1]
149159
for i in range(n_cols):
150160
data[:, i] = _np_convolve_same(data[:, i], coeffs) // scale
151161

152-
return data
153-
154162

155163
def bitmap_symmetric_filter_inplace(bitmap, coeffs, scale):
156-
"""Apply a symmetric filter to an image, updating the original image"""
164+
"""Apply the same filter to an image by rows and then by columns, updating the original image"""
157165
r, g, b = bitmap_to_components_rgb565(bitmap)
158166
symmetric_filter_inplace(r, coeffs, scale)
159167
symmetric_filter_inplace(g, coeffs, scale)
@@ -262,14 +270,31 @@ def sharpen(bitmap):
262270
)
263271

264272

273+
def _edge_filter_component(data, coefficients):
274+
"""Internal filter to apply H+V edge detection to an image component"""
275+
data_copy = data[:]
276+
row_filter_inplace(data, coefficients, scale=1)
277+
column_filter_inplace(data_copy, coefficients, scale=1)
278+
data += data_copy
279+
data += 128
280+
281+
265282
def edgedetect(bitmap):
266283
"""Run an edge detection routine on a bitmap"""
267284
coefficients = np.array([-1, 0, 1])
268285
r, g, b = bitmap_to_components_rgb565(bitmap)
269-
symmetric_filter_inplace(r, coefficients, scale=1)
270-
r += 128
271-
symmetric_filter_inplace(g, coefficients, scale=1)
272-
g += 128
273-
symmetric_filter_inplace(b, coefficients, scale=1)
274-
b += 128
286+
_edge_filter_component(r, coefficients)
287+
_edge_filter_component(g, coefficients)
288+
_edge_filter_component(b, coefficients)
275289
return bitmap_from_components_inplace_rgb565(bitmap, r, g, b)
290+
291+
292+
def edgedetect_greyscale(bitmap):
293+
"""Run an edge detection routine on a bitmap in greyscale"""
294+
coefficients = np.array([-1, 0, 1])
295+
r, g, b = bitmap_to_components_rgb565(bitmap)
296+
luminance = np.right_shift(38 * r + 75 * g + 15 * b, 7)
297+
_edge_filter_component(luminance, coefficients)
298+
return bitmap_from_components_inplace_rgb565(
299+
bitmap, luminance, luminance, luminance
300+
)

docs/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
77
.. automodule:: adafruit_pycamera
88
:members:
9+
.. automodule:: adafruit_pycamera.imageprocessing
10+
:members:

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"sdcardio",
4545
"storage",
4646
"terminalio",
47+
"ulab",
4748
]
4849

4950
autodoc_preserve_defaults = True

examples/filter/code.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
imageprocessing.blue_cast,
2626
imageprocessing.blur,
2727
imageprocessing.edgedetect,
28+
imageprocessing.edgedetect_grayscale,
2829
imageprocessing.green_cast,
2930
imageprocessing.greyscale,
3031
imageprocessing.red_cast,
@@ -47,6 +48,15 @@ def random_choice(seq):
4748

4849

4950
def load_resized_image(bitmap, filename):
51+
"""Load an image at the best scale into a given bitmap
52+
53+
If the image can be scaled down until it fits within the bitmap, this routine
54+
does so, leaving equal space at the sides of the image (known as letterboxing
55+
or pillarboxing).
56+
57+
If the image cannot be scaled down, the most central part of the image is loaded
58+
into the bitmap."""
59+
5060
print(f"loading {filename}")
5161
bitmap.fill(0b01000_010000_01000) # fill with a middle grey
5262

0 commit comments

Comments
 (0)