API Reference

This section covers the core classes and functions.

Audio Playback and Triggering

audio_repr_sounddevice.py
  1#!/usr/bin/env python
  2# -*- coding: utf-8 -*-
  3
  4"""
  5Created: 2025-11-12
  6Author: Alberto Doimo
  7email: alberto.doimo@uni-konstanz.de
  8
  9Description
 10-----------
 11
 12Audio reproduction (using sounddevice) of:
 13    - camera + sync signal
 14    - Target localization/repulsion signal from external loudspeakers
 15
 16"""
 17
 18if __name__ == "__main__":
 19    ###############################################################################
 20    # Libraries import
 21    ###############################################################################
 22
 23    import sounddevice as sd
 24    import numpy as np
 25    import soundfile as sf
 26    from datetime import datetime
 27    import time
 28    from scipy import signal
 29
 30    ###############################################################################
 31    # SETUP PARAMETERS
 32    ###############################################################################
 33
 34    # Load wav files
 35
 36    # List available audio devices
 37    print("Available audio devices:")
 38    print(sd.query_devices())
 39
 40    # Select your device by index (change this number based on the list above)
 41    device_index = 20
 42    print("\ndevice_samplerate:", sd.query_devices(device_index)["default_samplerate"])
 43
 44    # Load the stereo wav file for channels 1-2
 45    tracking_signal, fs1 = sf.read("15_Hz_tracking_sync_signal_48000.wav")
 46
 47    multich_signal, fs2 = sf.read("multich-1000_chirp_100-4000hz_48khz.wav")
 48
 49    print(f"tracking_signal shape: {tracking_signal.shape}, fs1: {fs1}")
 50    print(f"multich_signal shape: {multich_signal.shape}, fs2: {fs2}")
 51
 52    if fs1 != fs2:
 53        # Resample multich_signal to match fs1
 54        if fs1 < fs2:
 55            print(f"Resampling from {fs2} Hz to {fs1} Hz ...")
 56            multich_signal = signal.resample(
 57                multich_signal, int(len(multich_signal) * fs1 / fs2)
 58            )
 59        else:
 60            print(f"Resampling from {fs1} Hz to {fs2} Hz ...")
 61            tracking_signal = signal.resample(
 62                tracking_signal, int(len(tracking_signal) * fs2 / fs1)
 63            )
 64
 65    # Find the maximum length to pad correctly
 66    len_track = tracking_signal.shape[0]
 67    len_multi = multich_signal.shape[0]
 68    # print(f"Original lengths: tracking_signal={len_track}, multich_signal={len_multi}")
 69    max_len = max(len_track, len_multi)
 70
 71    # Pad ONLY the time axis (axis 0), adding zeros to the end. (axis 1 gets 0 padding)
 72    track_padded = np.pad(
 73        tracking_signal, ((0, max_len - len_track), (0, 0)), mode="constant"
 74    )
 75    multi_padded = np.pad(
 76        multich_signal, ((0, max_len - len_multi), (0, 0)), mode="constant"
 77    )
 78
 79    # Use hstack now that they are strict columns: shape becomes (Samples, Total_Channels)
 80    output_sig = np.float32(np.hstack([track_padded, multi_padded]))
 81    print("output_sig shape", output_sig.shape)
 82
 83    # Save the combined output signal to a wav file
 84    output_filename = "combined_output.wav"
 85    sf.write(output_filename, output_sig, fs1)
 86    print(f"Output signal saved to: {output_filename}")
 87
 88    # ---- PLAYBACK WITH CTRL-C INTERRUPT ----
 89    print(f"\nPlaying audio on device:\n {sd.query_devices(device_index)['name']}\n")
 90    for i in range(3, 0, -1):
 91        print(f"Starting in {i} seconds... \n")
 92        time.sleep(1)
 93    sd.play(output_sig, samplerate=fs1, device=device_index)
 94
 95    for i in range(1, 30 * 60):
 96        time.sleep(1)
 97        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 98        minutes = ((i * 1) % 3600) // 60
 99        seconds = (i * 1) % 60
100        print(f"Time: {minutes:02d}m {seconds:02d}s")
101
102    try:
103        while True:
104            time.sleep(0.1)
105    except KeyboardInterrupt:
106        print("\nStopping playback...")
107        sd.stop()
108
109    print("Playback stopped by user.")

Store Camera Frames

This script is used to store frames from Basler camera after being triggerred by soundcard square wave signal.

hardware_triggering_and_store_frames.py
  1#!/usr/bin/env python
  2# -*- coding: utf-8 -*-
  3
  4"""
  5Created: 2026-01-08
  6Author: Alberto Doimo
  7Email: alberto.doimo@uni-konstanz.de
  8
  9Description
 10-----------
 11Records images from a Basler ace2 camera triggered by an external hardware signal
 12(stereo signal on left channel output by soundcard).
 13
 14See also
 15--------
 16https://github.com/basler/pypylon/issues/842
 17"""
 18
 19#############################################################################
 20# Libraries import
 21#############################################################################
 22
 23import pypylon.pylon as pylon
 24import time
 25import datetime
 26import os
 27
 28if __name__ == "__main__":
 29
 30    ###############################################################################
 31    # SETUP PARAMETERS
 32    ###############################################################################
 33
 34    # Setup the camera (model = ace2 R a2A4508-20umBAS; Max frame rate = 19.4 fps)
 35    tl_factory = pylon.TlFactory.GetInstance()
 36    devices = tl_factory.EnumerateDevices()
 37    if not devices:
 38        print("No Basler camera found.")
 39
 40    camera = pylon.InstantCamera(tl_factory.CreateDevice(devices[0]))
 41    camera.Open()
 42
 43    # Set camera parameters
 44
 45    # Original image size
 46    original_width = 4504
 47    original_height = 4096
 48    # Crop size
 49    crop_w = 2560
 50    crop_h = 1600
 51
 52    # Crop the image to the desired size
 53    camera.Width.SetValue(crop_w)
 54    camera.Height.SetValue(crop_h)
 55
 56    # Center crop into the original image
 57    camera.BslCenterX.Execute()
 58    camera.BslCenterY.Execute()
 59
 60    # Set the upper limit of the camera's frame rate
 61    camera.AcquisitionFrameRateEnable.Value = True
 62    camera.AcquisitionFrameRate.Value = 1000
 63
 64    # Hardware Trigger Configuration
 65    camera.TriggerSelector.Value = "FrameStart"
 66    camera.TriggerSource.Value = "Line1"  # opto-coupled line
 67    camera.TriggerMode.Value = "On"
 68    camera.TriggerActivation.Value = "RisingEdge"
 69
 70    # setup for saving images
 71    img = pylon.PylonImage()
 72    camera.StartGrabbing(pylon.GrabStrategy_OneByOne)
 73    image_quality = 100
 74
 75    # Create folder for saving recordings
 76    timenow = datetime.datetime.now()
 77    time = timenow.strftime("%Y-%m-%d")
 78    time1 = timenow.strftime("%Y-%m-%d_%H-%M-%S")
 79
 80    # Create directory structure
 81    file_dir = os.path.dirname(os.path.abspath(__file__))
 82    save_path = "Data/"
 83    date_folder_name = str(time)
 84    hour_folder_name = str(time1)
 85    folder_path = os.path.join(file_dir, save_path, date_folder_name, hour_folder_name)
 86    os.makedirs(folder_path, exist_ok=True)
 87
 88    print("\nCamera is ready and waiting for a trigger signal on Line1...\n")
 89
 90    i = 0
 91    try:
 92        while camera.IsGrabbing():
 93            with camera.RetrieveResult(
 94                20000, pylon.TimeoutHandling_ThrowException
 95            ) as result:
 96                if i == 0:
 97                    print("Trigger received! Recording started...\n")
 98                    i += 1
 99                img.AttachGrabResultBuffer(result)
100
101                # The JPEG format that is used here supports adjusting the image
102                # quality (100 -> best quality, 0 -> poor quality).
103                ipo = pylon.ImagePersistenceOptions()
104                ipo.SetQuality(image_quality)
105
106                filename = f"{folder_path}/{datetime.datetime.now(datetime.timezone.utc).isoformat().replace(':', '-')}.jpeg"
107                img.Save(pylon.ImageFileFormat_Jpeg, filename, ipo)
108
109    except KeyboardInterrupt:
110        print("Interrupted by user (Ctrl+C).")
111    finally:
112        img.Release()
113        if camera.IsGrabbing():
114            camera.StopGrabbing()
115            print("Camera stopped.")
116        camera.Close()

Utilities module

Created: 2025-12-19 Author: Alberto Doimo email: alberto.doimo@uni-konstanz.de

Description:

Utilities for tracking ArUco markers and calculating robot positions and headings.

utilities_tracking.draw_closest_pair_line(frame, pair_centers, robot_names, reference_position, pixel_per_meters)

Draw a line between the closest pair of robots and display the distance.

This function identifies the pair of robots with the minimum distance between them, draws a white line connecting them on the frame, and displays the distance in millimeters at the midpoint of the line.

Parameters

framenp.ndarray

The video frame (image) on which to draw the heading arrows.

pair_centersdict

Dictionary mapping marker pairs (a, b) to their center coordinates in world coordinates (meters).

robot_namesdict

Dictionary mapping marker pairs (a, b) to robot names (Raspberry Pi last IP number).

reference_positiontuple or None

Reference position (x, y) for computing relative coordinates.

pixel_per_metersfloat

Conversion factor from meters to pixels.

Returns

list of dict

List of dictionaries containing pairwise distances. Each dictionary has: - ‘pair’: tuple of str, the names of the robot pair - ‘distance_m’: float, the distance between robots in meters

utilities_tracking.draw_heading_angles(frame, heading_vectors, pair_centers, robot_names)

Draw heading angles and arrows on a frame to visualize the direction to the closest robot. This function calculates the angle between each robot’s heading vector and the direction to its nearest neighboring robot. It draws arrows pointing to the closest robot and annotates the angle in degrees on the frame.

Parameters

framenp.ndarray

The video frame (image) on which to draw the heading arrows.

heading_vectorsdict

Dictionary mapping marker pairs (a, b) to normalized heading direction vectors in 2D space.

pair_centersdict

Dictionary mapping marker pairs (a, b) to their center coordinates in world coordinates (meters).

robot_namesdict

Dictionary mapping marker pairs (a, b) to robot names (Raspberry Pi last IP number).

Returns

angle_resultsdict
Dictionary mapping robot names to a dictionary containing:
  • ‘closest_robot’: Name of the closest robot.

  • ‘angle_deg’: Angle in degrees between the robot’s heading and the direction to the closest robot.

Notes

  • The function modifies the input frame by drawing arrows and text annotations in place.

  • The angle is calculated as the difference between the direction to the closest robot and the robot’s heading direction, normalized to the range [0, 360).

  • Arrows are drawn in red (BGR: 0, 0, 255) with a length of 100 pixels.

  • Text annotations displaying the angle are placed 40 pixels to the right and 20 pixels above the robot’s center position.

utilities_tracking.draw_heading_arrows(frame, pair_centers, robot_names, corners, ids, reference_position, pixel_per_meters)

Draw heading arrows on a frame based on marker positions and orientations.

This function visualizes the heading direction of robots by drawing arrows on the input frame. The heading direction is determined from ArUco marker corners, and arrows are scaled and positioned based on reference coordinates and pixel-to-meter conversion factors.

Parameters

framenp.ndarray

The video frame (image) on which to draw the heading arrows.

pair_centersdict

Dictionary mapping marker pairs (a, b) to their center coordinates in world coordinates (meters).

robot_namesdict

Dictionary mapping marker pairs (a, b) to robot names (Raspberry Pi last IP number).

cornersndarray

Array of corner coordinates for detected markers.

idsndarray or None

Array of detected marker IDs, shape (n, 1).

reference_positiontuple or np.ndarray, optional

Reference position (x, y) for computing relative coordinates.

pixel_per_metersfloat

Conversion factor from meters to pixels.

Returns

heading_vectorsdict

Dictionary mapping marker pairs (a, b) to normalized heading direction vectors in 2D space.

pixel_centersdict

Dictionary mapping marker pairs (a, b) to their pixel coordinates referred to the reference position

heading_angledict

Dictionary mapping robot names to heading angles in degrees, where 0° is vertical (top) and angles increase clockwise (0-360°).

Notes

  • Heading vectors are computed from ArUco marker corners (pt0 - pt3).

  • Arrows are drawn with a fixed length of 100 pixels.

utilities_tracking.draw_pair_centers(frame, pair_centers, robot_names, reference_position, pixel_per_meters)

Draw marker pair centers on a frame and circle of 100 px around the robot with optional labels.

Parameters

framenp.ndarray

The video frame (image) on which to draw the heading arrows.

pair_centersdict

Dictionary mapping marker pairs (a, b) to their center coordinates in world coordinates (meters).

robot_namesdict, optional

Dictionary mapping marker pairs (a, b) to robot names (Raspberry Pi last IP number).

reference_positiontuple or np.ndarray, optional

Reference position (x, y) for computing relative coordinates.

pixel_per_metersfloat

Conversion factor from meters to pixels.

utilities_tracking.get_marker_centers(corners, ids)

Get the center coordinates of each detected ArUco marker.

Parameters

cornerslist

List of detected marker corners from cv2.aruco.detectMarkers.

idslist

List of detected marker IDs from cv2.aruco.detectMarkers.

Returns

marker_centerslist

A list of (x, y) tuples representing the center of each marker.

utilities_tracking.get_pair_centers(marker_pairs, centers_dict, corners, ids, reference_position, pixel_per_meters)

Calculate the centers of marker pairs, with optional relative positioning.

This function computes the center point for each pair of markers. If both markers are detected, the center is the midpoint. If only one marker is detected, the center is placed 5 cm away from that marker in a direction parallel to the marker’s edge. Centers can be returned in pixel coordinates or relative to a reference position.

Parameters

marker_pairslist of tuple

List of (a, b) tuples representing marker pairs to process.

centers_dictdict

Dictionary mapping marker IDs to their center coordinates as (x, y) tuples.

cornersndarray

Array of corner coordinates for detected markers.

idsndarray or None

Array of detected marker IDs, shape (n, 1).

reference_positiontuple or None

Reference position (x, y) for computing relative coordinates.

pixel_per_metersfloat

Conversion factor from meters to pixels.

Returns

dict

Dictionary mapping marker pairs (a, b) to their center coordinates.

Notes

  • If both markers in a pair are in centers_dict, the center is their midpoint.

  • If only marker a is available, the center is placed 5 cm to the right (parallel to the marker’s edge defined by corners 0-1).

  • If only marker b is available, the center is placed 5 cm to the left (parallel to the marker’s edge).

  • Pairs where neither marker is available are skipped.