kivy: "Exception: Shader didnt link" 当被 gpiozero 回调调用时,而不是被 kivy.uix.button 回调调用

kivy: "Exception: Shader didnt link" when called by gpiozero callback, but not by kivy.uix.button callback

我正在使用 kivy 中的摄像头编写 GUI,但不确定为什么我的代码无法运行。我有一个相机源,以及两种从中捕获图片的方法:一种由 gpiozero when_pressed 回调触发,另一种由 kivy.uix.button on_press 回调触发。

kivy.uix.button 回调成功捕获图像,但 gpiozero 回调显示 Exception: Shader didnt link, check info log.,无法保存图像,然后使相机画面变黑(尽管图像以后仍然可以使用成功选项捕获)。为什么一个回调有效而另一个无效?

这里是相关代码,对应终端输出。我用 # ALL CAPS COMMENTS 注释了终端输出。 (我的代码是受kivy docs camera example的启发,同样捕获成功)。

main.py

import kivy
#kivy.require('1.11.1')

# Uncomment these lines to see all the messages
#from kivy.logger import Logger
#import logging
#Logger.setLevel(logging.TRACE)

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.uix.camera import Camera
from kivy.core.window import Window
from gpiozero import Button as gpiozeroButton   # renamed to avoid conflict w/ kivy.uix.button
import time

Window.fullscreen = 'auto'  # uses display's current resolution

capture_btn  = gpiozeroButton(pin=13, pull_up=False)    # set up GPIO

class RootWidget(FloatLayout):

    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)
        capture_btn.when_pressed = self.capture    # initialize callback for GPIO button
        
    def capture(self):
        print('Capture step 1')
        camera = self.ids['camera']
        print('Capture step 2')
        timestr = time.strftime("%Y%m%d_%H%M%S")
        print('Capture step 3')
        camera.export_to_png("IMG_{}.png".format(timestr))
        print("Captured")


class LifterApp(App):

    def build(self):
        return RootWidget()


if __name__ == '__main__':
    LifterApp().run()

lifter.kv

# #:kivy 1.11.1

<RootWidget>:

    Camera:
        id: camera
        resolution: (640, 480)
        play: True
    Button:
        text: "capture"
        pos_hint: {'x':0.0, 'y':0.0}
        size_hint: (0.2, 0.2)
        on_press: root.capture()

终端输出(带注释)

[INFO   ] [Logger      ] Record log in /home/pi/.kivy/logs/kivy_20-12-30_27.txt
[INFO   ] [Kivy        ] v1.11.1
[INFO   ] [Kivy        ] Installed at "/usr/local/lib/python3.7/dist-packages/kivy/__init__.py"
[INFO   ] [Python      ] v3.7.3 (default, Dec 20 2019, 18:57:59) 
[GCC 8.3.0]
[INFO   ] [Python      ] Interpreter at "/usr/bin/python3"
[INFO   ] [Factory     ] 184 symbols loaded
[INFO   ] [Image       ] Providers: img_tex, img_dds, img_sdl2, img_pil, img_gif (img_ffpyplayer ignored)
[INFO   ] [Text        ] Provider: sdl2(['text_pango'] ignored)
[INFO   ] [Camera      ] Provider: picamera
[INFO   ] [Window      ] Provider: sdl2(['window_egl_rpi'] ignored)
[INFO   ] [GL          ] Using the "OpenGL" graphics system
[INFO   ] [GL          ] Backend used <sdl2>
[INFO   ] [GL          ] OpenGL version <b'3.1 Mesa 19.3.2'>
[INFO   ] [GL          ] OpenGL vendor <b'VMware, Inc.'>
[INFO   ] [GL          ] OpenGL renderer <b'llvmpipe (LLVM 9.0.1, 128 bits)'>
[INFO   ] [GL          ] OpenGL parsed version: 3, 1
[INFO   ] [GL          ] Shading version <b'1.40'>
[INFO   ] [GL          ] Texture max size <8192>
[INFO   ] [GL          ] Texture max units <32>
[INFO   ] [Window      ] auto add sdl2 input provider
[INFO   ] [Window      ] virtual keyboard not allowed, single mode, not docked
[INFO   ] [ProbeSysfs  ] device match: /dev/input/event0
[INFO   ] [MTD         ] Read event from </dev/input/event0>
[INFO   ] [ProbeSysfs  ] device match: /dev/input/event0
[INFO   ] [HIDInput    ] Read event from </dev/input/event0>
[INFO   ] [Base        ] Start application main loop
[INFO   ] [MTD         ] </dev/input/event0> range position X is 0 - 800
[INFO   ] [MTD         ] </dev/input/event0> range position Y is 0 - 480
[INFO   ] [HIDMotionEvent] using <WaveShare WS170120>
[INFO   ] [MTD         ] </dev/input/event0> range touch major is 0 - 0
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range ABS X position is 0 - 800
[INFO   ] [MTD         ] </dev/input/event0> range touch minor is 0 - 0
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range ABS Y position is 0 - 480
[INFO   ] [MTD         ] </dev/input/event0> range pressure is 0 - 255
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range ABS pressure is 0 - 255
[INFO   ] [MTD         ] </dev/input/event0> axes invertion: X is 0, Y is 0
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range position X is 0 - 800
[INFO   ] [MTD         ] </dev/input/event0> rotation set to 0
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range position Y is 0 - 480
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range pressure is 0 - 255
[INFO   ] [GL          ] NPOT texture support is available
Capture step 1 # TRIGGERED BY KIVY.UIX.BUTTON ON_PRESS CALLBACK
Capture step 2
Capture step 3
Captured
Capture step 1 # TRIGGERED BY GPIOZERO WHEN_PRESSED CALLBACK
Capture step 2
Capture step 3
 Traceback (most recent call last):
   File "/usr/lib/python3/dist-packages/gpiozero/pins/rpigpio.py", line 244, in _call_when_changed
     super(RPiGPIOPin, self)._call_when_changed()
   File "/usr/lib/python3/dist-packages/gpiozero/pins/local.py", line 143, in _call_when_changed
     self.state if state is None else state)
   File "/usr/lib/python3/dist-packages/gpiozero/pins/pi.py", line 293, in _call_when_changed
     method(ticks, state)
   File "/usr/lib/python3/dist-packages/gpiozero/input_devices.py", line 197, in _pin_changed
     self._fire_events(ticks, bool(self._state_to_value(state)))
   File "/usr/lib/python3/dist-packages/gpiozero/mixins.py", line 368, in _fire_events
     self._fire_activated()
   File "/usr/lib/python3/dist-packages/gpiozero/mixins.py", line 397, in _fire_activated
     super(HoldMixin, self)._fire_activated()
   File "/usr/lib/python3/dist-packages/gpiozero/mixins.py", line 344, in _fire_activated
     self.when_activated()
   File "main.py", line 35, in capture
     camera.export_to_png("IMG_{}.png".format(timestr))
   File "/usr/local/lib/python3.7/dist-packages/kivy/uix/widget.py", line 727, in export_to_png
     self.export_as_image().save(filename, flipped=False)
   File "/usr/local/lib/python3.7/dist-packages/kivy/uix/widget.py", line 744, in export_as_image
     with_stencilbuffer=True)
   File "kivy/graphics/fbo.pyx", line 152, in kivy.graphics.fbo.Fbo.__init__
   File "kivy/graphics/instructions.pyx", line 777, in kivy.graphics.instructions.RenderContext.__init__
   File "kivy/graphics/shader.pyx", line 184, in kivy.graphics.shader.Shader.__init__
   File "kivy/graphics/shader.pyx", line 701, in kivy.graphics.shader.Shader.vs.__set__
   File "kivy/graphics/shader.pyx", line 557, in kivy.graphics.shader.Shader.build_vertex
   File "kivy/graphics/shader.pyx", line 587, in kivy.graphics.shader.Shader.link_program
 Exception: Shader didnt link, check info log.
^C[INFO   ] [Base        ] Leaving application in progress...
 Traceback (most recent call last):
   File "main.py", line 46, in <module>
     LifterApp().run()
   File "/usr/local/lib/python3.7/dist-packages/kivy/app.py", line 855, in run
     runTouchApp()
   File "/usr/local/lib/python3.7/dist-packages/kivy/base.py", line 504, in runTouchApp
     EventLoop.window.mainloop()
   File "/usr/local/lib/python3.7/dist-packages/kivy/core/window/window_sdl2.py", line 747, in mainloop
     self._mainloop()
   File "/usr/local/lib/python3.7/dist-packages/kivy/core/window/window_sdl2.py", line 479, in _mainloop
     EventLoop.idle()
   File "/usr/local/lib/python3.7/dist-packages/kivy/base.py", line 339, in idle
     Clock.tick()
   File "/usr/local/lib/python3.7/dist-packages/kivy/clock.py", line 591, in tick
     self._process_events()
   File "kivy/_clock.pyx", line 384, in kivy._clock.CyClockBase._process_events
   File "kivy/_clock.pyx", line 414, in kivy._clock.CyClockBase._process_events
   File "kivy/_clock.pyx", line 412, in kivy._clock.CyClockBase._process_events
   File "kivy/_clock.pyx", line 167, in kivy._clock.ClockEvent.tick
   File "/usr/local/lib/python3.7/dist-packages/kivy/core/camera/camera_picamera.py", line 71, in _update
     self._camera.capture(output, self._format, use_video_port=True)
   File "/usr/lib/python3/dist-packages/picamera/camera.py", line 1421, in capture
     if not encoder.wait(self.CAPTURE_TIMEOUT):
   File "/usr/lib/python3/dist-packages/picamera/encoders.py", line 393, in wait
     result = self.event.wait(timeout)
   File "/usr/lib/python3.7/threading.py", line 552, in wait
     signaled = self._cond.wait(timeout)
   File "/usr/lib/python3.7/threading.py", line 300, in wait
     gotit = waiter.acquire(True, timeout)
 KeyboardInterrupt

我创建了一个解决方法,但它很糟糕,所以我更喜欢更好的解决方案...

...或至少解释为什么 gpiozero when_pressed 表现不佳。

我的解决方法是使用私有 __capture() 函数,它充当 Clock.schedule_once(self.capture) 的包装器。 capture_btn.when_pressed 将使用此私有包装器回调来避免直接调用有问题的代码,并且 capture() 的任何其他用途将照常使用 public 一个,因为在不与 [= 交互时它可以正常工作11=].

In the following code I would prefer for capture() and __capture() to have swapped names since that would fit better with the philosophy of private functions, but unfortunately that's throwing AttributeError: 'RootWidget' object has no attribute '__capture' when I try that and I don't know how to fix that (using _RootWidget__capture isn't helping). The lines commented with ## illustrate my preferred but nonfunctional way.

main.py

import kivy
#kivy.require('1.11.1')

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.uix.camera import Camera
from kivy.core.window import Window
from kivy.clock import Clock
from gpiozero import Button as gpiozeroButton   # renamed to avoid conflict w/ kivy.uix.button
import time

Window.fullscreen = 'auto'  # uses display's current resolution

capture_btn = gpiozeroButton(pin=13, pull_up=False)    # set up GPIO

class RootWidget(FloatLayout):

    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)
##      capture_btn.when_pressed = self.capture
        capture_btn.when_pressed = self.__capture    # initialize callback for GPIO button
    
##  def __capture(self):
    def capture(self, dt=None):
        print('Capture step 1')
        camera = self.ids['camera']
        print('Capture step 2')
        timestr = time.strftime("%Y%m%d_%H%M%S")
        print('Capture step 3')
        camera.export_to_png("IMG_{}.png".format(timestr))
        print("Captured")

##  def capture(self):
    def __capture(self):    # FIXME this function shouldn't be needed (see function description)
        '''
        Since camera.export_to_png() throws "Exception: Shader didnt link" when called
        by a gpiozero when_pressed callback but not when scheduled using kivy.clock, I
        decided to create this wrapper function to circumvent the issue (I'm not sure 
        of the underlying problem, but this is a workaround in the meantime).
        '''
##      Clock.schedule_once(self.__capture)
        Clock.schedule_once(self.capture)


class LifterApp(App):

    def build(self):
        return RootWidget()


if __name__ == '__main__':
    LifterApp().run()

lifter.kv

lifter.kv与问题中的文字没有变化。

您可以使用 kivy.clock.mainthread 装饰器来替换您的 __wrapper 函数。

from kivy.clock import mainthread

...

class RootWidget(FloatLayout):
   ...

   @mainthread
   def capture(self):
        ...

此外,这也摆脱了您不需要的 dt 参数,因为它保留了函数的签名。

最初的问题当然是管理 when_pressed 的代码在一个线程中运行,因此在任何 opengl 操作中都表现不佳(你得到一个“shader didn't link”,但是你还不如在另一台计算机上崩溃,或者什么都没有),因为 opengl 不应该从多个线程使用,主线程装饰器通过使用 Clock 委托给主线程,避免了这个问题。