Создание нескольких изображений Kivy Canvas и экспорт в png: как мне это сделать, управляя непредсказуемостью kivy.clock?

#python #python-3.x #kivy

#python #python-3.x #kivy

Вопрос:

Я создал упрощенный пример моего кода на Python ниже. По сути, программа генерирует крестики на изображении с позициями, продиктованными некоторым файлом данных. Затем он сохраняет изображение и холст в файл png. Он выполняет это несколько раз итеративно для разных строк в файле данных (ниже я только что использовал один «pos_list» для демонстрационных целей).

Моя проблема в том, что для использования export_to_png нужно разрешить экрану обновлять и отображать изменения в инструкциях canvas. Это означает, что мне нужно использовать Kivy Clock. ОДНАКО. Когда я планирую несколько событий часов, я не могу гарантировать, что они будут происходить в правильном порядке по какой-то причине (даже с тщательно подобранными таймингами). Это проблема, потому что, если png-файлы будут созданы до вступления в силу инструкций canvas, они будут неполными. Аналогично, конечная цель моего кода — создать PDF, содержащий все эти изображения (функция do_something_with_pages является заполнителем для этого), и если эта функция вызывается до создания изображений, она завершится ошибкой.

Я понимаю, что это сложная проблема, но было бы очень полезно получить представление о том, как заставить события часов вести себя или даже как это сделать без использования часов.

 from functools import partial
from kivy.app import App
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.clock import Clock
from kivy.graphics import *
from kivy.uix.label import Label

Builder.load_string('''
<NumberLabel>:
    size: 10, 15
    text_size: self.size
    color: 0, 0, 0, 1
    outline_color: 1, 1, 1
    outline_width: 1
    
<AScreen>:
    Button:
        size_hint: 0.2, 0.1
        pos_hint: {"x": 0.4, "y": 0.85}
        on_release:
            root.parent_function(8, '/some/image/file/path.png')
    Image:
        id: an_image
        size_hint: 0.8, 0.8
        pos_hint: {"x": 0.1, "y": 0}
''')

dir_path = '/some/directory'

class NumberLabel(Label):
    pass

Class AScreen(Screen):

    def parent_function(self, pages, path):

        t_interval = 0.7

        for i in range(pages):
            pos_list = [(0.2, 0.3), (0.15, 0.74)] # This is usually a list of 
                                                  # (x, y) relative 
                                                  # coordinates that is read 
                                                  # from a csv file and 
                                                  # varies per page, but here
                                                  # I have set it to 
                                                  # something fixed
            Clock.schedule_once(partial(self.draw_on_image, i, path, pos_list), t_interval * i)
            
        Clock.schedule_once(self.do_something_with_pages, (t_interval   0.18) * pages)
        
        

    def draw_on_image(self, page, path, pos_list, *args):
        # This function draws a number of canvas objects on the image and 
        # adds a label with a number for each object drawn.
        # It is scheduled with Clock so that the canvas may later be exported 
        # as an image before it is changed by the next iteration of the loop
        self.ids.an_image.source = path
        self.ids.an_image.canvas.after.clear()
        self.ids.an_image.clear_widgets()
        image_pos = self.ids.an_image.pos
        image_width = self.ids.an_image.width
        image_height = self.ids.an_image.height
        obj_num = 0
        for item in pos_list:
            new_pos = (image_pos[0]   item[0] * image_width, image_pos[1]   item[1] * image_height)
            x1, y1 = new_pos[0] - 3, new_pos[1] - 3
            x2, y2 = new_pos[0]   3, new_pos[1]   3
            with self.ids.an_image.canvas.after:
                Color(1, 1, 1)
                Line(points=[(x1,y1), (x2,y2)], width=2)
                Line(points=[(x1,y2), (x2,y1)], width=2)
                Color(0, 0, 0)
                Line(points=[(x1,y1), (x2,y2)], width=1)
                Line(points=[(x1,y2), (x2,y1)], width=1)
            obj_num  = 1
            l = NumberLabel(pos=(x1,y2), text=str(obj_num))
            self.ids.an_image.add_widget(l)

        Clock.schedule_once(partial(self.export_image, page), 0.002)
        
    def export_image(self, page, *args):
        # This MUST happen after the previous canvas changes show up on 
        # screen but before the next canvas changes are made
        png_path = '{dir}/image{num}.png'.format(dir=dir_path, num=page)
        self.ids.an_image.export_to_png(png_path)
        
    def do_something_with_pages(self, *args):
        # This function does something with all of the saved pngs so MUST 
        # only be called after all of the PNGs have been created
        pass
        
        
class Manager(ScreenManager):
    pass

class MainApp(App):
    def build(self):
        self.title = 'Example'
        self.sm = Manager()
        return self.sm

if __name__ == "__main__":
    app = MainApp()
    app.run()
  

Ответ №1:

Вам не нужно так много использовать Clock.schedule_once() в своем коде. Вы можете внести изменения в изображение, а затем просто вызвать Clock.schedule_once() , чтобы разрешить kivy обновить изображение.

Я опубликовал измененную версию AScreen class ниже. Модификация используется только Clock.schedule_once() в одном месте. Затем я добавил pages переменную, которая будет передаваться по стеку, чтобы можно было определить, когда все файлы были экспортированы. Смотрите комментарии в коде:

 class AScreen(Screen):

    def parent_function(self, pages, path):

        t_interval = 0.7

        for i in range(pages):
            pos_list = [(0.2, 0.3), (0.15, 0.74)] # This is usually a list of
                                                  # (x, y) relative
                                                  # coordinates that is read
                                                  # from a csv file and
                                                  # varies per page, but here
                                                  # I have set it to
                                                  # something fixed

            # do not need Clock.schedule_once() here. We are already on main thread
            self.draw_on_image( i, pages, path, pos_list)   # pass pages, so we can track status

    def draw_on_image(self, page, pages, path, pos_list, *args):
        # This function draws a number of canvas objects on the image and
        # adds a label with a number for each object drawn.
        # It is scheduled with Clock so that the canvas may later be exported
        # as an image before it is changed by the next iteration of the loop
        self.ids.an_image.source = path
        self.ids.an_image.canvas.after.clear()
        self.ids.an_image.clear_widgets()
        image_pos = self.ids.an_image.pos
        image_width = self.ids.an_image.width
        image_height = self.ids.an_image.height
        obj_num = 0
        for item in pos_list:
            new_pos = (image_pos[0]   item[0] * image_width, image_pos[1]   item[1] * image_height)
            x1, y1 = new_pos[0] - 3, new_pos[1] - 3
            x2, y2 = new_pos[0]   3, new_pos[1]   3
            with self.ids.an_image.canvas.after:
                Color(1, 1, 1)
                Line(points=[(x1,y1), (x2,y2)], width=2)
                Line(points=[(x1,y2), (x2,y1)], width=2)
                Color(0, 0, 0)
                Line(points=[(x1,y1), (x2,y2)], width=1)
                Line(points=[(x1,y2), (x2,y1)], width=1)
            obj_num  = 1
            l = NumberLabel(pos=(x1,y2), text=str(obj_num))
            self.ids.an_image.add_widget(l)

        # use Clock.schedule_once() to give main thread a chance to update image
        Clock.schedule_once(partial(self.export_image, page, pages), 0.002)  # pass pages again

    def export_image(self, page, pages, *args):
        # This MUST happen after the previous canvas changes show up on
        # screen but before the next canvas changes are made
        png_path = '{dir}/image{num}.png'.format(dir=dir_path, num=page)
        self.ids.an_image.export_to_png(png_path)
        print('exported', png_path)
        if page == pages - 1:   # use page and pages to determine if all files are done
            self.do_something_with_pages()

    def do_something_with_pages(self, *args):
        print('do something')
        # This function does something with all of the saved pngs so MUST
        # only be called after all of the PNGs have been created
        pass
  

Комментарии:

1. Большое спасибо, Джон. Передача переменной pages для определения того, когда все файлы были экспортированы, определенно помогла улучшить ситуацию! Однако остальное работает не так, как вы ожидаете. К сожалению, вызовы функции export_image не выполняются до завершения всех вызовов draw_on_image . Это означает, что холст меняется несколько раз при циклическом просмотре страниц, а затем все изображения экспортируются как одно и то же изображение (последнее). Вот почему я почувствовал, что мне нужно также включить функцию draw_on_image в запланированную задачу, что, по общему признанию, довольно неуклюже.

Ответ №2:

ХОРОШО, вдохновленный ответом Джона Андерсона, мне удалось его взломать. Публикация ниже для всех, кому это может понадобиться.

Я не думаю, что можно добиться желаемого результата, используя одно изображение и обновляя исходный код и холст для каждой «страницы». Вместо этого я создал отдельное изображение (в модальном представлении) для каждой «страницы», а затем экспортировал каждое из этих изображений в конце.

 from functools import partial
from kivy.app import App
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.clock import Clock
from kivy.graphics import *
from kivy.uix.label import Label
from kivy.uix.modalview import ModalView

Builder.load_string('''
<NumberLabel>:
    size: 10, 15
    text_size: self.size
    color: 0, 0, 0, 1
    outline_color: 1, 1, 1
    outline_width: 1
    
<AScreen>:
    Button:
        size_hint: 0.2, 0.1
        pos_hint: {"x": 0.4, "y": 0.85}
        on_release:
            root.parent_function(8, '/some/image/file/path.png')

<AModalView>:
    size_hint: 0.8, 0.8
    pos_hint: {"x": 0.1, "y": 0.1}
    FloatLayout:
        Image:
            id: an_image
''')

dir_path = '/some/directory'

class NumberLabel(Label):
    pass

class AModalView(ModalView):
    pass

Class AScreen(Screen):

    def parent_function(self, pages, path):

        self.popup_list = []

        for i in range(pages):
            pos_list = [(0.2, 0.3), (0.15, 0.74)] # This is usually a list of 
                                                  # (x, y) relative 
                                                  # coordinates that is read 
                                                  # from a csv file and 
                                                  # varies per page, but here
                                                  # I have set it to 
                                                  # something fixed
            self.draw_on_image(i, path, pos_list)
            
        Clock.schedule_once(partial(self.do_something_with_pages, pages), 0)
        

    def draw_on_image(self, page, path, pos_list, *args):
        # This function creates a popup with an image inside it.
        # It then draws a number of canvas objects on the image and 
        # adds a label with a number for each object drawn.
        self.popup_list.append(AModalView())
        self.popup_list[page - 1].open()
        self.popup_list[page - 1].ids.an_image.source = path
        image_pos = self.popup_list[page - 1].ids.an_image.pos
        image_width = self.popup_list[page - 1].ids.an_image.width
        image_height = self.popup_list[page - 1].ids.an_image.height
        obj_num = 0
        for item in pos_list:
            new_pos = (image_pos[0]   item[0] * image_width, image_pos[1]   item[1] * image_height)
            x1, y1 = new_pos[0] - 3, new_pos[1] - 3
            x2, y2 = new_pos[0]   3, new_pos[1]   3
            with self.popup_list[page - 1].ids.an_image.canvas.after:
                Color(1, 1, 1)
                Line(points=[(x1,y1), (x2,y2)], width=2)
                Line(points=[(x1,y2), (x2,y1)], width=2)
                Color(0, 0, 0)
                Line(points=[(x1,y1), (x2,y2)], width=1)
                Line(points=[(x1,y2), (x2,y1)], width=1)
            obj_num  = 1
            l = NumberLabel(pos=(x1,y2), text=str(obj_num))
            self.popup_list[page - 1].ids.an_image.add_widget(l)
        
    def do_something_with_pages(self, pages, *args):
        # export to png for the image on each popup created, then dismiss all the 
        # popups
        for page in range(pages):
            png_path = '{dir}/image{num}.png'.format(dir=dir_path, num=page)
            self.popup_list[page - 1].ids.an_image.export_to_png(png_path)
            self.popup_list[page - 1].dismiss()
        # Now do something with all the pngs
        
        
class Manager(ScreenManager):
    pass

class MainApp(App):
    def build(self):
        self.title = 'Example'
        self.sm = Manager()
        return self.sm

if __name__ == "__main__":
    app = MainApp()
    app.run()