Local development and advanced debugging

Understanding the Code

To understand the code a bit better two simplified diagrams are given. Fist the UML Sequence Diagram:

_images/ablauf.drawio.png

It all starts with the run() entry point in the Application class. This call constructs and configures all the necessary resources and starts threads as required. The network server and the retrieval of frames from the camera have their own thread, symbolised by the activity rectangle in the diagram. Each frame calculation also has its own thread, represented by the while block in the diagram.

First the frame is taken from the video object via the implicit __next__() method with an iterator. Then the worker pool dispatches a worker with the frame and the find_ball_async() method. This worker returns to the callback ball_found_async_callback() after his job is done. The frame loop does not wait for the callback but continues with the for loop through the frames.

The frame data arrays are delivered via the piHSVArray class inside Videostream.__next__() in HSV format.

Call-Graph

This can also shown in a Flow-Chart-like clickable Call-Graph (you can zoom in the website to make it bigger):

digraph G { subgraph cluster_server { style=dashed; penwidth=2; color=black; node [shape=record,color=grey,target="_blank"]; label = "Server Pi"; s_app [ label="{Start App and load config|runner.py|Application.run()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/src.ballandhoop.html#module-src.ballandhoop.application" ] s_network_start[ label="{Init Network and wait for clients |init_network()|Server.__init__()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/index.html#src.network.Server" ] s_network_serial_start[ label="{Init Serial to D-Space|Serial.__init__()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/index.html#src.serial.SerialCom" ] s_network_serial_send[ label="{Send Serial to D-Space|Serial.write()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/index.html#src.serial.SerialCom.write" ] s_video_start [ label="{Start Camera| Videostream.__init__()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/src.ballandhoop.html#module-src.ballandhoop.videostream" ] s_video_get_pic [ label="{Recieve picture| Videostream.__next__()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/src.ballandhoop.html#src.ballandhoop.videostream.VideoStream.__next__" ] s_find_ball [ label="{Find ball and calc angle in given frame|hoop.find_ball()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/src.ballandhoop.html#src.ballandhoop.hoop.Hoop.find_ball" ] s_network_send[ label="{'Send' Message to save|<pre>NetworkInterface.preprocess_message()|<send>Server.send()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/index.html#src.network.Server.send" ] s_network_receive[ label="{Wait for messages|Server.__init__()|Server.run()|s.recv(1024)}" URL="https://lukas-staab.github.io/ball-and-hoop/code/index.html#src.network.Server.run" ] s_workerpool [ label="{Init Workerpool|multiprocessing.Pool()}" URL="https://docs.python.org/3/library/multiprocessing.html" ] s_worker_dispatch [ label="{Dispatch Worker| pool.apply_async()|hoop.find_ball_async()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/src.ballandhoop.html#src.ballandhoop.hoop.Hoop.find_ball_async" ] s_app -> s_network_start -> s_network_send -> s_network_serial_send s_network_start -> s_network_serial_start -> s_network_serial_send s_network_start-> s_network_receive -> s_network_serial_send s_app -> s_workerpool -> s_worker_dispatch -> s_find_ball -> s_network_send s_app -> s_video_start -> s_video_get_pic -> s_worker_dispatch } subgraph cluster_client { style=dashed; penwidth=2; color=black; label = "Client Pi"; node [shape=record,color=grey,target="_blank"]; c_app [ label="{Start App and load config|runner.py|Application.run()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/src.ballandhoop.html#module-src.ballandhoop.application" ] c_network_start[ label="{Init Network and Connect to Server|init_network()|Client.__init__()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/index.html#src.network.Client" ] c_video_start [ label="{Start Camera| Videostream.__init__()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/src.ballandhoop.html#module-src.ballandhoop.videostream" ] c_video_get_pic [ label="{Recieve picture| Videostream.__next__()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/src.ballandhoop.html#src.ballandhoop.videostream.VideoStream.__next__" ] c_find_ball [ label="{Find ball and calc angle in given frame|hoop.find_ball()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/src.ballandhoop.html#src.ballandhoop.hoop.Hoop.find_ball" ] c_network_send[ label="{Send Message to Server|<pre>NetworkInterface.preprocess_message()|<send>Client.send()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/index.html#src.network.Client.send" ] c_workerpool [ label="{Init Workerpool|multiprocessing.Pool()}" URL="https://docs.python.org/3/library/multiprocessing.html" ] c_worker_dispatch [ label="{Dispatch Worker| pool.apply_async()|hoop.find_ball_async()}" URL="https://lukas-staab.github.io/ball-and-hoop/code/src.ballandhoop.html#src.ballandhoop.hoop.Hoop.find_ball_async" ] c_app -> c_network_start -> c_network_send c_app -> c_workerpool -> c_worker_dispatch -> c_find_ball -> c_network_send c_app -> c_video_start -> c_video_get_pic -> c_worker_dispatch } subgraph cluster_lab { style=dashed; penwidth=2; color=black; label = "Lab Computer"; start [shape=Mdiamond]; node [shape=record,color=grey,target="_blank"]; l_config [ label="{Find Hoop and save to config|calibrate.py}" URL="https://lukas-staab.github.io/ball-and-hoop/config.html" ] start -> l_config [label="optional", constraint=false] l_config -> start l_pid [label="{Start Matlab Simulink Model}"] } start -> c_app start -> s_app start -> l_pid s_network_serial_send -> l_pid c_network_send:send -> s_network_receive [constraint=false] c_network_start -> s_network_start }

Here the full code of the diagram above for a more detailed deep dive:

def run(self, ball_hsv: dict):
    """
    This method runs the main loop method for tracking and network init. Administers the thread-workers and gives
    them jobs. Each core gets one thread-worker. They will calculate the results of the frames after each other.
    The thread-workers are especially needed if the application is running under high per frame cpu,
    which can happen with high fps or high resolution settings.
    :param ball_hsv: if this parameter is set, the ball hsv in config is overwritten
    :type ball_hsv: dict
    """
    # saves the new colors to the config
    self.save_col_and_add_from_config('ball', ball_hsv)
    # give config to object constructors to initialize like defined in config
    # ** does flatten the array to arguments, with their corresponding keys as argument names
    hoop = Hoop(**self.get_cfg('hoop'))
    video = VideoStream(**self.get_cfg('camera'))
    # the network needs object context for better access in the async callback method from the workers
    self.network = init_network(**self.get_cfg('network'))
    # start network
    with self.network:
        try:
            # start thread-worker pool
            pool = multiprocessing.Pool(processes=os.cpu_count())
            # count the number of frames, this will be important to reconstruct original frame order
            i = 0
            # iterate over the video frames (most likely infinitely)
            for frame in video:
                # increase frame counter
                i = i + 1
                # log the time of frame start (and delivery later)
                self.timings[i] = time.time()

                debug_dir_path = None
                # if in debugging mode save every 30th frame in this folder for that frame
                if self.verbose and i % 30 == 0:
                    debug_dir_path = './storage/debug/' + str(i) + "/"
                    os.makedirs(debug_dir_path, exist_ok=True)
                # normal loop:
                # send the task to the next available thread-worker, from the pool
                # the threads will call hoop.find_ball(frame=frame, cols=ball_hsv, iterations=0)
                # search for the ball in the frame with the given color borders
                pool.apply_async(hoop.find_ball_async,
                                 args=(i, frame, self.local_config()['ball'], debug_dir_path),
                                 callback=self.ball_found_async_callback,
                                 error_callback=self.ball_search_error_callback)
        except KeyboardInterrupt:
            # break potential infinite loop
            pass

        finally:
            print('Closing resources, worker and so on')
            video.close()
            pool.terminate()
            pool.close()

Local development

to be able to develope this codebase on a local (non-pi) machine you need to do some extra steps.

  • make sure you have a Serial port, or a deactivated serial communication (see Configuration)

  • make sure you have mocked video material from a pi to make your calculations on (more on that later)

If you want better code completion it is advised also to install the picamera module. Usually this is not possible, but there is a way to achieve it.

export READTHEDOCS=True
pip install picamera # should work now

Getting Mocked Video material

To record a ‘video’ there is the debug.py file, to take a video you can use the following command:

python debug.py --vid dirname 300 60 1

Where

  • dirname is the name of the directory in storage/faker/

  • 300 is the amount of frames which will be collected, defaults to 10

  • 60 is the framerate the frames will be collected, defaults to 60

  • 1 is the resolution_no the pictures will be taken in, defaults to 1 (320x240)

To use this videos (only a lot of pictures which will be interpreted as a raw video stream) add the following config:

lyoga: # my pc hostname
  # some parts omitted
   camera:
      faker_path: fetch/rpi3.lan/faker/runtest # loads this video directory instead of the camera (for the ball search)
      # some parts omitted
   hoop:
      # some parts omitted
      faker_path: storage/cheat2.png # one picture which will be used for the configure routine (hoop+ball once)
   network:
      is_server: true
      serial:
         active: false # has to be false if you do not have a serial com