How to access kivy image(widget) without kv/id

Viewed 32

this is my first post in stackoverflow, be patient :D

I want to build a little picture editor with python/kivy. It is my first time using kivy. I got some experience in python.

I don´t know how to update/reload an added image (widget). For example if I want to setup the contrast of a picture, the picture needs to be reloaded. I want to avoid saving pictures to disk, thats why I use BytesIO -> speed.

My solution right now is to delete the old image(widget) and add a new one. But I get in trouble if want to add other widgets, like buttons. So right now the code only works with one widget :-/

I tried also with kv language, but while I use BytesIO for processing my Image I cant use the kv/id function. Source only accepts string (path/files). But I got ByteArray.

Please take a look at my code, may you can show me how to access an added widget and update it later on in the running app. Or maybe there is a solution with kv and BytesIO?

Most of the code is gathered from web and put together...


from kivy.app import App
from kivy.uix.image import Image as UixImage
import rawpy
import imageio
from PIL import Image, ExifTags, ImageEnhance
import colorcorrect.algorithm as cca
from colorcorrect.util import from_pil, to_pil
from kivy.core.image import Image as CoreImage
import io
from kivy.clock import Clock
from kivy.uix.widget import Widget
from kivy.uix.gridlayout import GridLayout
import kivy.uix.button as btn
import time
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.properties import NumericProperty, ObjectProperty


# used .raf (Fujifilm Raw) in this project
# https://filesamples.com/samples/image/raf/sample1.raf



class MyImageWidget(GridLayout):


    def __init__(self,**kwargs):
        super(MyImageWidget,self).__init__(**kwargs)
        self.cols = 1 #1 column grid layout

        self.contrast_counter = 0 # counter for contrast sweep

        # Load Image (+standard features)
        self.thumbnail_path = self.create_thumbnailpath() # set path to thumbnail (create thumbnail)
        self.image = Image.open(self.thumbnail_path) # open thumbnail
        self.image = self.set_size(self.image, 0.1) # minimize thumbail size for speed
        self.image = self.whitebalance_auto(self.image) # do whitebalance on thumbnail
        self.image = self.set_greyscale(self.image) # do black white conversion

        # add Image to widget (kivy_image)
        self.kivy_image = UixImage(source=self.thumbnail_path)
        self.add_widget(self.kivy_image) # <--- how can i access this widget later?

        # here a object is creatd for the widget, but how do I access it later on? 
        print(self.children)

        # TEST: reload Image in cycles (to see changing contrast effect on picture)
        Clock.schedule_interval(self.update_pic,0.025) # refreshrate of app (25ms)


    def update_pic(self, dt):
        new_image = self.process_image(self.image)
        
        self.remove_widget(self.kivy_image) # current widget need to be removed, so that contrast sweep is shown
        self.kivy_image = new_image
        self.add_widget(self.kivy_image) # new processed widget need to be added, so that contrast sweep is shown
        
        #self.kivy_image.reload() # <--- how can i reload the widget (kivy_image), to not need to remove the old widget?
        #print("reloaded")


    def create_thumbnailpath(self):
        # https://filesamples.com/samples/image/raf/sample1.raf
        path = 'lab2raw/sample1.raf'
        with rawpy.imread(path) as raw_image:
            thumbnail_path = self.create_thumbnail(raw_image) 
        return thumbnail_path

    def process_image(self,image):
        
        self.contrast_counter = self.contrast_counter + 0.02 # cycle through contrast levels 0.0 - 2.0

        if self.contrast_counter >= 2:
                self.contrast_counter = 0 # reset contrast level cycle
        
        #image = self.set_size(image, 0.2)
        #image = self.whitebalance_auto(image)
        #print_exif(image)
        #image = self.set_greyscale(image)
        #image = self.set_contrast(image,1.1)
        image = self.set_contrast(image,self.contrast_counter)
        #image = self.set_sharpness(image, 2.0)

        image_processed = self.create_kivy_image(image, 'png')
        return image_processed

    def create_kivy_image(self,image, file_extension):
        imgByteArr = io.BytesIO()
        image.save(imgByteArr, file_extension)
        # Return a Kivy image set from a bytes variable
        imgByteArr.seek(0)
        buf = io.BytesIO(imgByteArr.read())
        buf.seek(0)
        cim = CoreImage(buf, ext=file_extension)
        image_to_show = UixImage(source='')
        image_to_show.texture = cim.texture
        return image_to_show

    def whitebalance_auto(self,im):
        image = to_pil(cca.retinex_with_adjust(from_pil(im)))
        return image

    def create_thumbnail(self,raw_image):
        # Create JPG Thumbnail
        try:
            thumb = raw_image.extract_thumb()
        except rawpy.LibRawNoThumbnailError:
            print('no thumbnail found')
        except rawpy.LibRawUnsupportedThumbnailError:
            print('unsupported thumbnail')
        else:
            if thumb.format == rawpy.ThumbFormat.JPEG:
                with open('lab2raw/thumb.jpg', 'wb') as file:
                    file.write(thumb.data)
                    return 'lab2raw/thumb.jpg'
            elif thumb.format == rawpy.ThumbFormat.BITMAP:
                imageio.imsave('lab2raw/thumb.tiff', thumb.data)
                return 'lab2raw/thumb.tiff'
            
    def print_exif(self,image):
        img_exif = image.getexif()
        if img_exif is None:
            print('Sorry, image has no exif data.')
        else:
            for key, val in img_exif.items():
                if key in ExifTags.TAGS:
                    if ExifTags.TAGS[key]=="XMLPacket":
                        pass
                    elif ExifTags.TAGS[key]=="PrintImageMatching":
                        pass
                    else:
                        print(f'{ExifTags.TAGS[key]}:{val}')

    def set_greyscale (self,im):
        image = im.convert("L")
        return image

    def set_contrast (self,im, contrast_level):
        enhancer = ImageEnhance.Contrast(im)
        return_image = enhancer.enhance(contrast_level)
        return return_image

    def set_sharpness (self,im, sharpness_level):
        enhancer = ImageEnhance.Sharpness(im)
        image = enhancer.enhance(sharpness_level)
        return image

    def set_size(self,im, size_factor):
        width, height = im.size
        im = im.resize((round(width*size_factor),round(height*size_factor)),Image.ANTIALIAS)
        return im

    def save_jpg(self,im,quality, optimize, path):
        im.save(path,optimize=optimize, quality=quality)

class MyApp(App):
    def build(self):
        return MyImageWidget()
 
if __name__ == "__main__":
    MyApp().run()

1 Answers

You don't have to delete self.kivy_image and create it again but you have to replace .texture instead of full Image

self.kivy_image.texture = new_image.texture

And this means you can also reduce code in create_kivy_image. You can return CoreImage instead of UixImage. You may also use one io.BufferIO

    def create_kivy_image(self, image, file_extension):
        buf = io.BytesIO()
        image.save(buf, file_extension)
        buf.seek(0)
        return CoreImage(buf, ext=file_extension)

    def update_pic(self, dt):
        new_image = self.process_image(self.image)
        
        self.kivy_image.texture = new_image.texture

You could also use io.BytesIo() to create thumbnail and you wouldn't have to write in file. I could test it only with JPG but I don't know if it is correct for BITMAP

    def raw_to_pil(self, path):
        """Load RAW and convert to PIL.Image."""
        
        with rawpy.imread(path) as raw_image:
            try:
                thumb = raw_image.extract_thumb()
            except rawpy.LibRawNoThumbnailError:
                print('no thumbnail found')
            except rawpy.LibRawUnsupportedThumbnailError:
                print('unsupported thumbnail')
            else:

                if thumb.format == rawpy.ThumbFormat.JPEG:
                    print('thumbnail JPG')
                    return Image.open(io.BytesIO(thumb.data)) 

                elif thumb.format == rawpy.ThumbFormat.BITMAP:                
                    print('thumbnail BITMAP')
                    buf = io.BytesIO()
                    imageio.imsave(buf, thumb.data, 'tiff')
                    buf.seek(0)
                    return Image.open(buf) 

My full code for tests:

import io
import time
import rawpy
import imageio
from PIL import Image, ExifTags, ImageEnhance
import colorcorrect.algorithm as cca
from colorcorrect.util import from_pil, to_pil

from kivy.app import App
from kivy.clock import Clock
from kivy.core.image import Image as CoreImage
from kivy.uix.image import Image as UixImage
from kivy.uix.widget import Widget
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.lang import Builder
from kivy.properties import ObjectProperty
from kivy.properties import NumericProperty, ObjectProperty

# used .raf (Fujifilm Raw) in this project
# https://filesamples.com/samples/image/raf/sample1.raf

class MyImageWidget(GridLayout):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        
        self.cols = 1              # 1 column grid layout
        self.contrast_counter = 0  # counter for contrast sweep

        # add Image to widget (kivy_image)
        self.kivy_image = UixImage()       # empty image
        self.add_widget(self.kivy_image)

        self.image = self.raw_to_pil('lab2raw/sample1.raf')        
        
        if self.image:  # can be `None`
            self.image = self.set_size(self.image, 0.1) # minimize thumbail size for speed
            #self.image = self.whitebalance_auto(self.image) # do whitebalance on thumbnail
            self.image = self.set_greyscale(self.image) # do black white conversion
            core_image = self.pil_to_kivy(self.image, 'png')
            self.kivy_image.texture = core_image.texture

        # TEST: reload Image in cycles (to see changing contrast effect on picture)
        Clock.schedule_interval(self.update_pic, 0.025) # refreshrate of app (25ms)

    def update_pic(self, dt):
        if self.image:  # can be `None`
            pil_image = self.process_image(self.image)
            core_image = self.pil_to_kivy(pil_image, 'png')
            self.kivy_image.texture = core_image.texture

    def process_image(self, image):
        """Process PIL.Image without converting to other formats."""
        
        self.contrast_counter += 0.02 # cycle through contrast levels 0.0 - 2.0

        if self.contrast_counter >= 2:
            self.contrast_counter = 0 # reset contrast level cycle
        
        #image = self.set_size(image, 0.2)
        #image = self.whitebalance_auto(image)
        #image = self.set_greyscale(image)
        #image = self.set_contrast(image, 1.1)
        image = self.set_contrast(image, self.contrast_counter)
        #image = self.set_sharpness(image, 2.0)

        #self.print_exif(image)
        
        return image

    def pil_to_kivy(self, image, file_extension):
        """Convert PIL.Image to CoreImage (to get later .texture)."""
    
        buf = io.BytesIO()
        image.save(buf, file_extension)
        buf.seek(0)
        
        return CoreImage(buf, ext=file_extension)

    def raw_to_pil(self, path):
        """Load RAW and convert to PIL.Image. Can return `None`."""

        with rawpy.imread(path) as raw_image:
            try:
                thumb = raw_image.extract_thumb()
            except rawpy.LibRawNoThumbnailError:
                print('no thumbnail found')
            except rawpy.LibRawUnsupportedThumbnailError:
                print('unsupported thumbnail')
            else:
                if thumb.format == rawpy.ThumbFormat.JPEG:
                    print('thumbnail JPG')
                    buf = io.BytesIO(thumb.data)
                    return Image.open(buf) 
                elif thumb.format == rawpy.ThumbFormat.BITMAP:                
                    print('thumbnail BITMAP')
                    buf = io.BytesIO()
                    imageio.imsave(buf, thumb.data, 'tiff')
                    buf.seek(0)
                    return Image.open(buf) 
            
            #return Image.new('RGB', (1500, 1500), 'white')   # create empty Image
        
        
    def print_exif(self, im):
        im_exif = im.getexif()
        
        if im_exif is None:
            print('Sorry, image has no exif data.')
        else:
            for key, val in im_exif.items():
                if key in ExifTags.TAGS:
                    if ExifTags.TAGS[key] == "XMLPacket":
                        print('pass (XMLPacket)')
                        pass
                    elif ExifTags.TAGS[key] == "PrintImageMatching":
                        print('pass (PrintImageMatching)')
                        pass
                    else:
                        print(f'{ExifTags.TAGS[key]}:{val}')

    # --- 
    
    def whitebalance_auto(self, im):
        return to_pil(cca.retinex_with_adjust(from_pil(im)))

    def set_greyscale(self, im):
        return im.convert("L")

    def set_contrast(self, im, contrast_level):
        return ImageEnhance.Contrast(im).enhance(contrast_level)

    def set_sharpness(self, im, sharpness_level):
        return ImageEnhance.Sharpness(im).enhance(sharpness_level)
        
    def set_size(self, im, factor):
        return im.resize((round(im.width*factor), round(im.height*factor)), Image.ANTIALIAS)
        
    def save_jpg(self, im, quality, optimize, path):
        im.save(path, optimize=optimize, quality=quality)

class MyApp(App):
    def build(self):
        return MyImageWidget()
 
if __name__ == "__main__":
    MyApp().run()
Related