diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/adafruit_pycamera/imageprocessing.py b/adafruit_pycamera/imageprocessing.py new file mode 100644 index 0000000..d7fa770 --- /dev/null +++ b/adafruit_pycamera/imageprocessing.py @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: 2024 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: MIT +"""Routines for performing image manipulation""" + +import bitmapfilter + +from adafruit_pycamera.ironbow import ironbow_palette + +sepia_weights = bitmapfilter.ChannelMixer( + 0.393, 0.769, 0.189, 0.349, 0.686, 0.168, 0.272, 0.534, 0.131 +) + + +def sepia(bitmap, mask=None): + """Apply a sepia filter to an image in place""" + bitmapfilter.mix(bitmap, sepia_weights, mask=mask) + return bitmap + + +negative_weights = bitmapfilter.ChannelScaleOffset(-1, 1, -1, 1, -1, 1) + + +def negative(bitmap, mask=None): + """Invert an image""" + bitmapfilter.mix(bitmap, negative_weights, mask=mask) + return bitmap + + +greyscale_weights = bitmapfilter.ChannelMixer( + 0.299, 0.587, 0.114, 0.299, 0.587, 0.114, 0.299, 0.587, 0.114 +) + + +def greyscale(bitmap, mask=None): + """Convert an image to greyscale""" + bitmapfilter.mix(bitmap, greyscale_weights, mask=mask) + return bitmap + + +def red_cast(bitmap, mask=None): + """Give an image a red cast by dividing G and B channels in half""" + bitmapfilter.mix(bitmap, bitmapfilter.ChannelScale(1, 0.5, 0.5), mask=mask) + return bitmap + + +def green_cast(bitmap, mask=None): + """Give an image a green cast by dividing R and B channels in half""" + bitmapfilter.mix(bitmap, bitmapfilter.ChannelScale(0.5, 1, 0.5), mask=mask) + return bitmap + + +def blue_cast(bitmap, mask=None): + """Give an image a blue cast by dividing R and G channels in half""" + bitmapfilter.mix(bitmap, bitmapfilter.ChannelScale(0.5, 0.5, 1), mask=mask) + return bitmap + + +def blur(bitmap, mask=None): + """Blur a bitmap""" + bitmapfilter.morph(bitmap, (1, 2, 1, 2, 4, 2, 1, 2, 1), mask=mask) + return bitmap + + +def sharpen(bitmap, mask=None): + """Sharpen a bitmap""" + bitmapfilter.morph(bitmap, (-1, -2, -1, -2, 13, -2, -1, -2, -1), mask=mask) + return bitmap + + +def emboss(bitmap, mask=None): + """Run an emboss filter on the bitmap""" + bitmapfilter.morph(bitmap, (-2, -1, 0, -1, 0, 1, 0, 1, 2), add=0.5, mask=mask) + + +def emboss_greyscale(bitmap, mask=None): + """Run an emboss filter on the bitmap in greyscale""" + greyscale(bitmap, mask=mask) + return emboss(bitmap, mask=mask) + + +def ironbow(bitmap, mask=None): + """Convert an image to false color using the 'ironbow palette'""" + return bitmapfilter.false_color(bitmap, ironbow_palette, mask=mask) diff --git a/adafruit_pycamera/ironbow.py b/adafruit_pycamera/ironbow.py new file mode 100644 index 0000000..11ce719 --- /dev/null +++ b/adafruit_pycamera/ironbow.py @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: 2024 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: MIT +"""The 'ironbow' palette used to convert images to false color""" + +import displayio + +ironbow_palette = displayio.Palette(256) +"""A palette often used to convert images to false color""" + +# fmt: off +for i, pi in enumerate( + [ + 0xFFFFFF, 0xFFFFFF, 0xFFFEFF, 0xF7FEF7, 0xF7FDF7, 0xF7FDF7, 0xF7FCF7, 0xEFFCEF, + 0xEFFFEF, 0xEFFFEF, 0xEFFAEF, 0xE7FAE7, 0xE7FDE7, 0xE7FDE7, 0xE7F8E7, 0xDEF8DE, + 0xDEFFDE, 0xDEFFDE, 0xDEFEDE, 0xD6FED6, 0xD6F5D6, 0xD6F5D6, 0xD6F4D6, 0xCEF4CE, + 0xCEFFCE, 0xCEFFCE, 0xCEFACE, 0xC6FAC6, 0xC6F5C6, 0xC6F5C6, 0xC6F0C6, 0xBDF0BD, + 0xBDBFBD, 0xBDBFBD, 0xBDBEBD, 0xB5BEB5, 0xB5BDB5, 0xB5BDB5, 0xB5BCB5, 0xB5BCB5, + 0xADAFAD, 0xADAFAD, 0xADAAAD, 0xADAAAD, 0xA5ADA5, 0xA5ADA5, 0xA5A8A5, 0xA5A8A5, + 0x9CBF9C, 0x9CBF9C, 0x9CBE9C, 0x9CBE9C, 0x94B594, 0x94B594, 0x94B494, 0x94B494, + 0x8CAF8C, 0x8CAF8C, 0x8CAA8C, 0x8CAA8C, 0x84A584, 0x84A584, 0x84A084, 0x84A084, + 0x7B7F7B, 0x7B7F7B, 0x7B7E7B, 0x7B7E7B, 0x737D73, 0x737D73, 0x737C73, 0x737C73, + 0x6B7F6B, 0x6B7F6B, 0x6B7A6B, 0x6B7A6B, 0x637D63, 0x637D63, 0x637863, 0x637863, + 0x5A5F5A, 0x5A5F5A, 0x5A5E5A, 0x5A5E5A, 0x525552, 0x525552, 0x525452, 0x525452, + 0x4A5F4A, 0x4A5F4A, 0x4A5A4A, 0x4A5A4A, 0x4A554A, 0x425542, 0x425042, 0x425042, + 0x423F42, 0x393F39, 0x393E39, 0x393E39, 0x393D39, 0x313D31, 0x313C31, 0x313C31, + 0x312F31, 0x292F29, 0x292A29, 0x292A29, 0x292D29, 0x212D21, 0x212821, 0x212821, + 0x211F21, 0x181F18, 0x181E18, 0x181E18, 0x181518, 0x101510, 0x101410, 0x101410, + 0x100F10, 0x080F08, 0x080A08, 0x080A08, 0x080508, 0x000500, 0x000000, 0x000000, + 0x000008, 0x000010, 0x000018, 0x080021, 0x080029, 0x080029, 0x080031, 0x100039, + 0x100042, 0x10004A, 0x180052, 0x18005A, 0x180063, 0x18006B, 0x21006B, 0x210073, + 0x21007B, 0x29007B, 0x31007B, 0x31007B, 0x39007B, 0x39007B, 0x42007B, 0x4A007B, + 0x4A0084, 0x520084, 0x520084, 0x5A0084, 0x630084, 0x630084, 0x6B0084, 0x6B0084, + 0x73008C, 0x7B008C, 0x7B008C, 0x84008C, 0x84058C, 0x8C058C, 0x94058C, 0x94058C, + 0x9C058C, 0x9C058C, 0xA5058C, 0xA5058C, 0xAD058C, 0xB5058C, 0xB50A8C, 0xBD0A8C, + 0xBD0A8C, 0xBD0F84, 0xC6147B, 0xC6157B, 0xC61573, 0xC61E6B, 0xCE1F6B, 0xCE2863, + 0xCE2863, 0xCE2D5A, 0xD62A52, 0xD62F52, 0xD62F4A, 0xDE3C42, 0xDE3D42, 0xDE3E39, + 0xDE3E31, 0xDE3F31, 0xDE5029, 0xE75529, 0xE75A29, 0xE75A21, 0xE75F21, 0xE75421, + 0xE75521, 0xE75E18, 0xE75F18, 0xE75F18, 0xEF7810, 0xEF7D10, 0xEF7A10, 0xEF7F08, + 0xEF7C08, 0xEF7D08, 0xEF7D08, 0xEF7E08, 0xEF7F08, 0xEFA008, 0xEFA508, 0xF7AA08, + 0xF7AF10, 0xF7B410, 0xF7B510, 0xF7BE10, 0xF7BF10, 0xF7A810, 0xF7A810, 0xF7AD10, + 0xF7AA10, 0xF7AF10, 0xF7BC10, 0xF7BD10, 0xF7BE10, 0xF7BF10, 0xF7BF10, 0xFFF018, + 0xFFF518, 0xFFFA18, 0xFFFF18, 0xFFF418, 0xFFF518, 0xFFFE18, 0xFFFE21, 0xFFFF21, + 0xFFF829, 0xFFFD31, 0xFFFD42, 0xFFFA52, 0xFFFA63, 0xFFFA6B, 0xFFFF7B, 0xFFFF8C, + 0xFFFC94, 0xFFFCA5, 0xFFFDB5, 0xFFFDBD, 0xFFFECE, 0xFFFEDE, 0xFFFFEF, 0xFFFF18, + ] +): + ironbow_palette[i] = pi +del i +del pi +del displayio diff --git a/docs/api.rst b/docs/api.rst index 811e43d..4e86a8c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,3 +6,7 @@ .. automodule:: adafruit_pycamera :members: +.. automodule:: adafruit_pycamera.imageprocessing + :members: +.. automodule:: adafruit_pycamera.ironbow + :members: diff --git a/docs/conf.py b/docs/conf.py index 66f51d2..3b6c67e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,6 +9,7 @@ import sys sys.path.insert(0, os.path.abspath("..")) +sys.path.insert(0, os.path.abspath("mock")) # -- General configuration ------------------------------------------------ @@ -32,11 +33,12 @@ "adafruit_debouncer", "adafruit_display_text", "adafruit_lis3dh", + "adafruit_ticks", "bitmaptools", + "bitmapfilter", "busdisplay", "busio", "digitalio", - "displayio", "espcamera", "fourwire", "micropython", @@ -44,6 +46,7 @@ "sdcardio", "storage", "terminalio", + "ulab", ] autodoc_preserve_defaults = True diff --git a/docs/mock/displayio.py b/docs/mock/displayio.py new file mode 100644 index 0000000..b4fd1c5 --- /dev/null +++ b/docs/mock/displayio.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2024 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + + +class Palette: + def __init__(self, i): + self._data = [0] * i + + def __setitem__(self, idx, value): + self._data[idx] = value diff --git a/examples/filter/code.py b/examples/filter/code.py new file mode 100644 index 0000000..94d90f3 --- /dev/null +++ b/examples/filter/code.py @@ -0,0 +1,113 @@ +# SPDX-FileCopyrightText: 2024 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +"""Effects Demonstration + +This will apply a nubmer of effects to a single image. + +Press any of the directional buttons to immediately apply a new effect. + +Otherwise, effects cycle every DISPLAY_INTERVAL milliseconds (default 2000 = 2 seconds) +""" + +import displayio +from jpegio import JpegDecoder +from adafruit_display_text.label import Label +from adafruit_ticks import ticks_less, ticks_ms, ticks_add, ticks_diff +from font_free_mono_bold_24 import FONT +import bitmapfilter + +from adafruit_pycamera import imageprocessing +from adafruit_pycamera import PyCameraBase + +effects = [ + ("blue cast", imageprocessing.blue_cast), + ("blur", imageprocessing.blur), + ("bright", lambda b: bitmapfilter.mix(b, bitmapfilter.ChannelScale(2.0, 2.0, 2.0))), + ("emboss", imageprocessing.emboss), + ("green cast", imageprocessing.green_cast), + ("greyscale", imageprocessing.greyscale), + ("ironbow", imageprocessing.ironbow), + ( + "low contrast", + lambda b: bitmapfilter.mix( + b, bitmapfilter.ChannelScaleOffset(0.5, 0.5, 0.5, 0.5, 0.5, 0.5) + ), + ), + ("negative", imageprocessing.negative), + ("red cast", imageprocessing.red_cast), + ("sepia", imageprocessing.sepia), + ("sharpen", imageprocessing.sharpen), + ("solarize", bitmapfilter.solarize), + ( + "swap r/b", + lambda b: bitmapfilter.mix( + b, bitmapfilter.ChannelMixer(0, 0, 1, 0, 1, 0, 1, 0, 0) + ), + ), +] + + +def cycle(seq): + while True: + for s in seq: + yield s + + +effects_cycle = iter(cycle(effects)) + + +DISPLAY_INTERVAL = 2000 # milliseconds + +decoder = JpegDecoder() + +pycam = PyCameraBase() +pycam.init_display() + + +def main(): + filename = "/cornell_box_208x208.jpg" + + bitmap = displayio.Bitmap(208, 208, 65535) + bitmap0 = displayio.Bitmap(208, 208, 65535) + decoder.open(filename) + decoder.decode(bitmap0) + + label = Label(font=FONT, x=0, y=8) + pycam.display.root_group = label + pycam.display.refresh() + + deadline = ticks_ms() + while True: + now = ticks_ms() + if pycam.up.fell: + deadline = now + + if pycam.down.fell: + deadline = now + + if pycam.left.fell: + deadline = now + + if pycam.right.fell: + deadline = now + + if ticks_less(deadline, now): + memoryview(bitmap)[:] = memoryview(bitmap0) + deadline = ticks_add(deadline, DISPLAY_INTERVAL) + + effect_name, effect = next(effects_cycle) # random.choice(effects) + print(effect) + print(f"applying {effect=}") + t0 = ticks_ms() + effect(bitmap) + t1 = ticks_ms() + dt = ticks_diff(t1, t0) + print(f"{dt}ms to apply effect") + pycam.blit(bitmap, x_offset=16) + label.text = f"{dt:4}ms: {effect_name}" + pycam.display.refresh() + + +main() diff --git a/examples/filter/cornell_box_208x208.jpg b/examples/filter/cornell_box_208x208.jpg new file mode 100644 index 0000000..20ab2c5 Binary files /dev/null and b/examples/filter/cornell_box_208x208.jpg differ diff --git a/examples/filter/cornell_box_208x208.jpg.license b/examples/filter/cornell_box_208x208.jpg.license new file mode 100644 index 0000000..2aa27ab --- /dev/null +++ b/examples/filter/cornell_box_208x208.jpg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2007 SeeSchloss + +SPDX-License-Identifier: CC0-1.0