API Reference
This section covers the core classes and functions.
Audio Playback and Triggering
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.
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.