From a486f77b3e35176ecc2bbf3f42def5da37387096 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Tue, 30 Jan 2024 11:10:13 -0600 Subject: [PATCH 1/3] Add common blend mode functions --- adafruit_pycamera/imageprocessing.py | 120 +++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py index d7fa770..f8bdbd7 100644 --- a/adafruit_pycamera/imageprocessing.py +++ b/adafruit_pycamera/imageprocessing.py @@ -82,3 +82,123 @@ def emboss_greyscale(bitmap, mask=None): def ironbow(bitmap, mask=None): """Convert an image to false color using the 'ironbow palette'""" return bitmapfilter.false_color(bitmap, ironbow_palette, mask=mask) + + +# pylint: disable=invalid-name +def alphablend_func_factory(frac, nfrac=None): + """Create an alpha-blending function for a specific fractional value + + The resulting function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``. + """ + if nfrac is None: + nfrac = 1 - frac + + def inner(a, b): + return frac * a + nfrac * b + + return inner + + +def screen_func(a, b): + """The 'screen' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return 1 - (1 - a) * (1 - b) + + +def overlay_func(a, b): + """The 'overlay' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return 2 * a * b if a < 0.5 else 1 - 2 * (1 - a) * (1 - b) + + +def hard_light_func(a, b): + """The 'hard light' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return 2 * a * b if b < 0.5 else 1 - 2 * (1 - a) * (1 - b) + + +# illusions.hu formula version +def soft_light_func(a, b): + """The 'soft light' blend mode. + + There are various soft light blend functions. The "illusions.hu" variant of + soft light is used. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a ** (2 ** (2 * 0.5 - b)) + + +def color_dodge_func(a, b): + """The 'color dodge' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a / (1 - b) if b != 1 else 1 + + +def linear_dodge_func(a, b): + """The 'linear dodge' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a + b + + +def divide_func(a, b): + """The 'divide' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a / b if b else 1 + + +def multiply_func(a, b): + """The 'multiply' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a * b + + +def subtract_func(a, b): + """The 'subtract' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a - b + + +def color_burn_func(a, b): + """The 'color burn' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a * (1 - b) + + +def linear_burn_func(a, b): + """The 'linear burn' blend mode. + + This function can be used with ``bitmapfilter.blend`` and + ``bitmapfilter.blend_precompute``.""" + return a + b - 1 + + +darken_only_func = min +"""The 'darken only' blend mode. + +This function can be used with ``bitmapfilter.blend`` and +``bitmapfilter.blend_precompute``.""" +lighten_only_func = max +"""The 'screen' blend mode. + +This function can be used with ``bitmapfilter.blend`` and +``bitmapfilter.blend_precompute``.""" From 8741b57e26e8d70dfaae675925c2601b2e45f0e9 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Tue, 30 Jan 2024 12:06:22 -0600 Subject: [PATCH 2/3] Add blend examples this includes most of the blend functions listed on the related wikipedia article, though only a few are tested. --- adafruit_pycamera/imageprocessing.py | 2 +- examples/filter/code.py | 94 ++++++++++++++++++ examples/filter/testpattern_208x208.jpg | Bin 0 -> 6144 bytes .../filter/testpattern_208x208.jpg.license | 3 + 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 examples/filter/testpattern_208x208.jpg create mode 100644 examples/filter/testpattern_208x208.jpg.license diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py index f8bdbd7..98932aa 100644 --- a/adafruit_pycamera/imageprocessing.py +++ b/adafruit_pycamera/imageprocessing.py @@ -85,7 +85,7 @@ def ironbow(bitmap, mask=None): # pylint: disable=invalid-name -def alphablend_func_factory(frac, nfrac=None): +def alphablend_maker(frac, nfrac=None): """Create an alpha-blending function for a specific fractional value The resulting function can be used with ``bitmapfilter.blend`` and diff --git a/examples/filter/code.py b/examples/filter/code.py index 94d90f3..8d4598c 100644 --- a/examples/filter/code.py +++ b/examples/filter/code.py @@ -21,7 +21,95 @@ from adafruit_pycamera import imageprocessing from adafruit_pycamera import PyCameraBase + +blend_50_50 = bitmapfilter.blend_precompute(imageprocessing.alphablend_maker(0.5)) +screen = bitmapfilter.blend_precompute(imageprocessing.screen_func) +overlay = bitmapfilter.blend_precompute(imageprocessing.overlay_func) +hard_light = bitmapfilter.blend_precompute(imageprocessing.hard_light_func) +soft_light = bitmapfilter.blend_precompute(imageprocessing.soft_light_func) +color_dodge = bitmapfilter.blend_precompute(imageprocessing.color_dodge_func) +# linear_dodge = bitmapfilter.blend_precompute(imageprocessing.linear_dodge_func) +# divide = bitmapfilter.blend_precompute(imageprocessing.divide_func) +multiply = bitmapfilter.blend_precompute(imageprocessing.multiply_func) +# subtract = bitmapfilter.blend_precompute(imageprocessing.subtract_func) +# color_burn = bitmapfilter.blend_precompute(imageprocessing.color_burn_func) +# linear_burn = bitmapfilter.blend_precompute(imageprocessing.linear_burn_func) +# darken_only = bitmapfilter.blend_precompute(min) +# lighten_only = bitmapfilter.blend_precompute(max) + + +def blender(f): + def inner(b): + return bitmapfilter.blend(b, b, testpattern, f) + + return inner + + +def reverse_blender(f): + def inner(b): + return bitmapfilter.blend(b, testpattern, b, f) + + return inner + + +inverse_greyscale_weights = bitmapfilter.ChannelMixer( + 1 - 0.299, + 1 - 0.587, + 1 - 0.114, + 1 - 0.299, + 1 - 0.587, + 1 - 0.114, + 1 - 0.299, + 1 - 0.587, + 1 - 0.114, +) + +blur_more = [ + 4, + 15, + 24, + 15, + 4, + 15, + 61, + 97, + 61, + 15, + 24, + 97, + 154, + 97, + 24, + 15, + 61, + 97, + 61, + 15, + 4, + 15, + 24, + 15, + 4, +] + + +# "Sketch" filter based on +# https://www.freecodecamp.org/news/sketchify-turn-any-image-into-a-pencil-sketch-with-10-lines-of-code-cf67fa4f68ce/ +def sketch(b): + bitmapfilter.mix(b, inverse_greyscale_weights) + memoryview(auxbuffer)[:] = memoryview(b) + bitmapfilter.morph(auxbuffer, blur_more) + bitmapfilter.blend(b, auxbuffer, b, color_dodge) + bitmapfilter.mix(b, inverse_greyscale_weights) # get rid of magenta halos + return b + + effects = [ + ("sketch", sketch), + ("50/50", blender(blend_50_50)), + ("multiply", blender(multiply)), + ("soft light", blender(soft_light)), + ("hard_light", blender(hard_light)), ("blue cast", imageprocessing.blue_cast), ("blur", imageprocessing.blur), ("bright", lambda b: bitmapfilter.mix(b, bitmapfilter.ChannelScale(2.0, 2.0, 2.0))), @@ -65,6 +153,9 @@ def cycle(seq): pycam = PyCameraBase() pycam.init_display() +testpattern = displayio.Bitmap(208, 208, 65535) +auxbuffer = displayio.Bitmap(208, 208, 65535) + def main(): filename = "/cornell_box_208x208.jpg" @@ -74,6 +165,9 @@ def main(): decoder.open(filename) decoder.decode(bitmap0) + decoder.open("/testpattern_208x208.jpg") + decoder.decode(testpattern) + label = Label(font=FONT, x=0, y=8) pycam.display.root_group = label pycam.display.refresh() diff --git a/examples/filter/testpattern_208x208.jpg b/examples/filter/testpattern_208x208.jpg new file mode 100644 index 0000000000000000000000000000000000000000..94b01de2da807a7e3faf36b1d8410f1fdd52040f GIT binary patch literal 6144 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 literal 0 HcmV?d00001 diff --git a/examples/filter/testpattern_208x208.jpg.license b/examples/filter/testpattern_208x208.jpg.license new file mode 100644 index 0000000..746796b --- /dev/null +++ b/examples/filter/testpattern_208x208.jpg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries + +SPDX-License-Identifier: Unlicense From dd3a66504649813975e72dd20f8f3aa6cac172fc Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Tue, 30 Jan 2024 13:56:33 -0600 Subject: [PATCH 3/3] Add a boot.py to auto-create /sd at boot time --- examples/camera/boot.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/camera/boot.py diff --git a/examples/camera/boot.py b/examples/camera/boot.py new file mode 100644 index 0000000..83662bc --- /dev/null +++ b/examples/camera/boot.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +"""Automatically create the /sd mount point at boot time""" + +import os +import storage + +storage.remount("/", readonly=False) + +try: + os.mkdir("/sd") +except OSError: + pass # It's probably 'file exists', OK to ignore + +storage.remount("/", readonly=True)