Webcam Configurations: Windows Python Script for Setting Management
Webcam Configurations: Windows Python Script for Setting Management
Over the last month i have been using a new WebCam and found an issue with Microsoft Teams. In teams the webcam seem to reset one of its values every time it started. To address this, I had to use the control panel every time. After one morning, I did some vibe python coding while in a meeting and this is the result. A windows Python script designed to streamline the management of webcam settings through OpenCV, offering a robust solution for reading current configurations and applying saved profiles.
Purpose
The core function of this Python script is to programmatically read all discoverable webcam properties and save them to a YAML file, or load settings from such a file and apply them to a selected webcam. It directly addresses the problem of maintaining consistent webcam configurations across sessionsfferent and automates the setup process.
Use Cases
This script offers practical value in several scenarios:
- Automation of Setup: For applications requiring specific webcam settings (e.g., a particular resolution, focus, and exposure for a video recording booth or a computer vision system), the script can automatically apply a predefined configuration upon startup.
- Configuration Backup and Restore: Users can save their preferred webcam settings for different lighting conditions or tasks and restore them as needed. This is particularly useful if system updates or other software reset camera configurations.
- Compliance and Standardisation: In environments where multiple webcams must adhere to a standard configuration for consistent output (e.g., in educational settings for remote proctoring or in quality control imaging), this script can apply the standard settings profile to each device.
- Troubleshooting and Diagnostics: By reading all available camera properties, users can diagnose issues related to camera settings or compare configurations between different devices.
Prerequisites
For successful execution, the following are required:
- Python: Version 3.6 or newer.
- OpenCV (cv2): A comprehensive library for computer vision tasks. Install using
pip install opencv-python
. - PyYAML: A YAML parser and emitter for Python. Install using
pip install pyyaml
. - pygrabber: A Python library for grabbing images from video sources, used here for discovering camera names (primarily on Windows). Install using
pip install pygrabber
. - Permissions: Access to the webcam(s) connected to the system.
- Operating System: While core OpenCV functions are cross-platform, camera name detection via
pygrabber.dshow_graph
and the use ofcv2.CAP_DSHOW
are specific to Windows DirectShow. Functionality on other operating systems might be limited, particularly for camera name identification. - Environment Variables: No specific environment variables need to be configured.
- Network Access: Not required for script operation.
Requirements.txt file which can be used to create a virtual environment.
opencv-python
PyYAML
pygrabber
How the Script Works
The script operates in one of two modes: read
or apply
.
-
Initialization:
- The script begins by dynamically discovering all available
CAP_PROP_*
(camera property) constants within the installed OpenCV library. This ensures it can work with any property supported by the OpenCV version being used. - It then attempts to detect available cameras using OpenCV and, if possible (primarily on Windows via
pygrabber
), their descriptive names.
- The script begins by dynamically discovering all available
-
Camera Selection:
- The user can specify a target camera by its index or a unique part of its name via a command-line argument.
- If no camera is specified, or if the identifier is ambiguous, and multiple cameras are detected, the script presents an interactive list for the user to choose from.
-
read
Mode:- Once a camera is selected, the script queries the values of all discovered
CAP_PROP_*
properties for that camera. - Properties that return a value of
-1.0
are filtered out, as this often indicates an unsupported or unreadable property by the camera driver or hardware. - The successfully read settings (property name, its OpenCV integer ID, and its current value) are displayed in a formatted table in the console.
- These filtered settings, along with metadata (timestamp, camera index, detected camera name), are then saved to a YAML file. The filename is automatically generated by sanitising the camera’s detected name (e.g.,
Logitech_HD_Pro_Webcam_C920.yaml
).
- Once a camera is selected, the script queries the values of all discovered
-
apply
Mode:- The user must specify a YAML file (previously created by the
read
mode or manually crafted) containing the desired camera settings. - The script loads the settings from this YAML file. Property names are mapped back to their OpenCV integer IDs.
- It then attempts to apply these settings to the selected camera.
- A distinction is made for certain types of properties:
- Auto Mode Properties (e.g.,
CAP_PROP_AUTOFOCUS
,CAP_PROP_AUTO_EXPOSURE
): These are applied first, with a small delay to allow the camera to adjust. - Read-Only/Informational Properties (e.g.,
CAP_PROP_FRAME_WIDTH
,CAP_PROP_FPS
): These are skipped, as attempting to set them would typically fail or is nonsensical. - Other Writable Properties: These are applied sequentially.
- Auto Mode Properties (e.g.,
- The script provides a summary of how many settings were successfully applied, how many failed, and how many were skipped.
- A warning is issued if the metadata in the YAML file suggests it was created for a different camera than the one currently targeted.
- The user must specify a YAML file (previously created by the
The script includes helper functions for sanitising filenames, finding cameras by identifier, and formatting output tables. It uses cv2.CAP_DSHOW
as the backend for camera capture, which is generally preferred on Windows systems.
Parameter Reference
The script accepts the following command-line parameters:
Parameter | Data Type | Default Value | Description | Required |
---|---|---|---|---|
--mode |
string | None | Operation mode. Choices: ‘read’ (read and save settings) or ‘apply’ (load and apply settings). | Yes |
--file |
string | None | Path to the YAML file. Required for --mode apply to specify the settings file to load. Ignored for --mode read . |
Yes, if mode is apply |
--camera |
string | None | Camera identifier (index number or part of camera name, case-insensitive). If omitted, interactive selection is used if multiple cameras are found. | No |
Quick-Start Guide
-
Install Prerequisites:
pip install opencv-python pyyaml pygrabber
-
Save the Script: Save the script provided below as
webcam_manager.py
. -
Run the Script:
-
To read and save settings from the default/selected camera:
python webcam_manager.py --mode read
(This will create a YAML file, e.g.,
Your_Camera_Name.yaml
) -
To read and save settings from a specific camera (e.g., camera index 1 or name containing “Logitech”):
python webcam_manager.py --mode read --camera 1 python webcam_manager.py --mode read --camera "Logitech"
-
To apply settings from a YAML file to the default/selected camera:
python webcam_manager.py --mode apply --file "Your_Camera_Name.yaml"
-
To apply settings to a specific camera:
python webcam_manager.py --mode apply --file "Your_Camera_Name.yaml" --camera 1
-
The Script
#!/usr/bin/env python3
import cv2
import time
import sys
import yaml # For YAML handling
import argparse # For command-line arguments
import os # For checking file existence
import datetime # For timestamp
import re # For sanitizing filename
try:
# Attempt to import the necessary part from pygrabber
from pygrabber.dshow_graph import FilterGraph
except ImportError:
print("Error: pygrabber library not found.")
print("Please install it using: pip install pygrabber")
sys.exit(1)
# --- Dynamically Discover All OpenCV CAP_PROP_ Constants ---
print("Discovering available OpenCV properties...")
ALL_CV2_PROPERTIES = {} # Dictionary to store {ID: Name}
for name in dir(cv2):
if name.startswith('CAP_PROP_'):
prop_id = getattr(cv2, name)
if isinstance(prop_id, int): # Ensure it's an integer ID
# Store the first name found for a given ID, avoids alias issues if any
if prop_id not in ALL_CV2_PROPERTIES:
ALL_CV2_PROPERTIES[prop_id] = name
print(f"Found {len(ALL_CV2_PROPERTIES)} unique CAP_PROP_* properties.")
# Sort properties by ID for consistent ordering
SORTED_PROP_IDS = sorted(ALL_CV2_PROPERTIES.keys())
# --- Configuration ---
DEFAULT_YAML_FILE = 'webcam_settings.yaml' # (Used implicitly by argparse default help)
# List of properties known to be generally read-only or problematic to set blindly
# Used in apply function to avoid attempting to set them.
READ_ONLY_OR_INFO_PROPS = {
getattr(cv2, name, -999) for name in [
'CAP_PROP_FRAME_WIDTH', 'CAP_PROP_FRAME_HEIGHT', 'CAP_PROP_FPS',
'CAP_PROP_FOURCC', 'CAP_PROP_FORMAT', 'CAP_PROP_MODE',
'CAP_PROP_FRAME_COUNT', 'CAP_PROP_POS_MSEC', 'CAP_PROP_POS_FRAMES',
'CAP_PROP_POS_AVI_RATIO', 'CAP_PROP_BACKEND', 'CAP_PROP_SAR_NUM',
'CAP_PROP_SAR_DEN', 'CAP_PROP_ORIENTATION_META', 'CAP_PROP_CHANNEL',
'CAP_PROP_SETTINGS' # Special case: Triggers dialog, doesn't take a value well
] if hasattr(cv2, name)
}
# Properties related to auto modes, good to apply first
AUTO_MODE_PROPS = {
getattr(cv2, name, -999) for name in [
'CAP_PROP_AUTOFOCUS', 'CAP_PROP_AUTO_EXPOSURE', 'CAP_PROP_AUTO_WB'
] if hasattr(cv2, name)
}
# --- Helper Functions ---
def sanitize_filename(name):
"""Removes or replaces characters invalid for filenames."""
if not name:
name = "Unknown_Camera"
# Remove characters invalid in most file systems
sanitized = re.sub(r'[\<\>\:\"\/\\\|\?\*]', '_', name)
# Replace spaces with underscores
sanitized = re.sub(r'\s+', '_', sanitized)
# Remove leading/trailing underscores/whitespace
sanitized = sanitized.strip('_ ')
# Ensure filename is not empty
if not sanitized:
sanitized = "Sanitized_Camera_Name"
return sanitized + ".yaml"
def find_available_cameras_with_names(max_cameras_to_check=10):
"""Checks indices for available cameras and attempts to get their names."""
available_cameras = [] # List of dictionaries {'index': i, 'name': name_str}
print("Detecting available cameras...")
try:
# Get device names using pygrabber (relies on DirectShow)
device_names = FilterGraph().get_input_devices()
# print(f" (pygrabber found {len(device_names)} potential device names)") # Verbose
except Exception as e:
print(f" Warning: Could not get device names using pygrabber. Error: {e}")
device_names = []
# print(f"Checking OpenCV indices 0 to {max_cameras_to_check-1}...") # Verbose
for i in range(max_cameras_to_check):
cap = cv2.VideoCapture(i, cv2.CAP_DSHOW)
if cap.isOpened():
# Assign a default name including the index, useful if pygrabber fails
name = f"Camera Index {i} (Name N/A)"
if i < len(device_names):
name = device_names[i] # Use name from pygrabber if available
# print(f" Found camera - Index: {i}, Name: {name}") # Verbose
available_cameras.append({'index': i, 'name': name})
cap.release() # IMPORTANT: Release immediately
else:
pass # No camera at this index
if not available_cameras:
print("\nWarning: No cameras detected that OpenCV could open.")
if len(device_names) > 0:
print(" (pygrabber found names, but OpenCV couldn't open corresponding indices.)")
print(" (Possible causes: Camera in use, driver issues, permissions.)")
return available_cameras
def find_camera_by_identifier(identifier_str, cameras_list):
"""Tries to find a unique camera from the list based on index number or name substring."""
matched_cameras = []
try:
# Try interpreting as an index first
target_index = int(identifier_str)
for cam in cameras_list:
if cam['index'] == target_index:
matched_cameras.append(cam)
break # Exact index match found
except ValueError:
# If not an integer, treat as name (case-insensitive substring)
identifier_lower = identifier_str.lower()
for cam in cameras_list:
# Ensure name is treated as string for comparison
cam_name_str = str(cam.get('name', ''))
if identifier_lower in cam_name_str.lower():
matched_cameras.append(cam)
return matched_cameras # Return list of matches (0, 1, or more)
# --- Function to Print Settings Table ---
def print_settings_table(settings_dict_by_id):
"""Prints the camera settings (excluding those that returned -1.0) in a formatted table."""
if not settings_dict_by_id:
print("\nNo settings data to display (or all readable properties returned -1.0).")
return
print("\n--- Camera Properties (Excluding those reporting -1.0) ---") # Updated title
# Determine column widths dynamically (with minimums)
max_id_len = max((len(str(id)) for id in settings_dict_by_id.keys()), default=3)
max_name_len = max((len(ALL_CV2_PROPERTIES.get(id, f"UNKNOWN_{id}")) for id in settings_dict_by_id.keys()), default=20)
max_val_len = max((len(f"{val:.4f}" if isinstance(val, float) else str(val)) for val in settings_dict_by_id.values()), default=10)
id_width = max(3, max_id_len)
name_width = max(25, max_name_len)
val_width = max(15, max_val_len)
# Print header
header = f"| {'ID':<{id_width}} | {'Property Name':<{name_width}} | {'Value':<{val_width}} |"
separator = f"|{'-' * (id_width + 2)}|{'-' * (name_width + 2)}|{'-' * (val_width + 2)}|"
print(separator)
print(header)
print(separator)
# Print rows (iterates through the already filtered dictionary)
for prop_id in SORTED_PROP_IDS: # Iterate in sorted order to maintain consistency
if prop_id in settings_dict_by_id: # Only print if it was successfully read and not -1.0
prop_name = ALL_CV2_PROPERTIES.get(prop_id, f"UNKNOWN_{prop_id}")
value = settings_dict_by_id[prop_id]
# Format value nicely
if isinstance(value, float):
value_str = f"{value:<{val_width}.4f}" # Pad float, 4 decimal places
else:
value_str = f"{str(value):<{val_width}}" # Pad others
print(f"| {prop_id:<{id_width}} | {prop_name:<{name_width}} | {value_str} |")
print(separator)
print("---------------------------------------------------------")
# --- Function to Read ALL Current Camera Settings (With Filtering) ---
def read_current_settings(camera_index):
"""Reads current values for ALL discovered cv2.CAP_PROP_ properties,
filtering out those that return -1.0 (often indicating 'unsupported')."""
print(f"\nReading current settings from camera index: {camera_index}")
cap = cv2.VideoCapture(camera_index, cv2.CAP_DSHOW)
if not cap.isOpened():
print(f"Error: Could not open camera index {camera_index} to read settings.")
return None
current_settings_by_id = {}
print(f"Querying all {len(SORTED_PROP_IDS)} discovered properties...")
queried_count = 0
success_count = 0
skipped_count = 0
for prop_id in SORTED_PROP_IDS:
prop_name = ALL_CV2_PROPERTIES.get(prop_id, f"UNKNOWN_{prop_id}")
queried_count += 1
try:
value = cap.get(prop_id)
# --- FILTERING STEP ---
# Filter out properties returning -1.0, as they are often unsupported/unavailable.
# Note: This is a heuristic, a property could theoretically use -1.0 legitimately,
# but it's rare for typical webcam controls accessed this way.
if value != -1.0:
current_settings_by_id[prop_id] = value
success_count +=1
else:
# Property returned -1.0, considered unsupported/unavailable
skipped_count += 1
# Optional: Log skipped properties if verbose debugging is needed
# print(f" Skipping {prop_name} ({prop_id}): Value is -1.0")
# ----------------------
except Exception as e:
# Catch potential errors during the 'get' operation itself
print(f" Warning: Exception getting property {prop_name} (ID: {prop_id}). Error: {e}")
skipped_count += 1 # Count exceptions as skipped too
cap.release()
# Updated status message
print(f"Finished reading settings. Queried: {queried_count}, "
f"Stored (value != -1.0): {success_count}, Skipped/Error: {skipped_count}")
return current_settings_by_id
# --- Function to Save Settings to YAML (Operates on Filtered Data) ---
def save_settings_to_yaml(settings_dict_by_id, camera_info):
"""Saves the successfully read settings (value != -1.0) with metadata
to a YAML file named after the camera."""
if settings_dict_by_id is None or not camera_info: # Check if settings reading failed
print("Error: Cannot save settings. Invalid settings data or camera info.")
return
filename = sanitize_filename(camera_info.get('name', 'Unknown_Camera'))
print(f"\nGenerating filename: {filename}")
data_to_save = {
'metadata': {
'capture_time_utc': datetime.datetime.now(datetime.timezone.utc).isoformat(),
'capture_time_local': datetime.datetime.now().astimezone().isoformat(),
'camera_index': camera_info.get('index', 'N/A'),
'camera_name_detected': camera_info.get('name', 'N/A'),
# Updated comment reflecting the filtering
'script_comment': 'Camera settings captured by webcam_manager.py script (Filtered: value != -1.0)'
},
'settings': {}
}
# Populate settings using readable names, preserving the order read (sorted by ID)
print("Preparing filtered settings for saving...")
saved_count = 0
for prop_id in SORTED_PROP_IDS:
if prop_id in settings_dict_by_id: # Only save if it's in the filtered dictionary
prop_name = ALL_CV2_PROPERTIES.get(prop_id, f"UNKNOWN_{prop_id}")
data_to_save['settings'][prop_name] = settings_dict_by_id[prop_id]
saved_count += 1
# Updated print message to reflect filtered count
print(f"Saving metadata and {saved_count} settings (where value != -1.0) to YAML file: {filename}")
try:
with open(filename, 'w', encoding='utf-8') as file_handle:
yaml.dump(data_to_save, file_handle, default_flow_style=False, sort_keys=False, indent=2, allow_unicode=True)
print("Settings saved successfully.")
except Exception as e:
print(f"Error: Could not save settings to {filename}. Error: {e}")
# --- Function to Load Settings from YAML ---
def load_settings_from_yaml(filename):
"""Loads settings from a YAML file, expecting metadata and settings keys."""
print(f"\nLoading settings from YAML file: {filename}")
if not os.path.exists(filename):
print(f"Error: Settings file not found: {filename}")
return None, None # Return None for both settings and metadata
try:
with open(filename, 'r', encoding='utf-8') as file_handle:
loaded_data = yaml.safe_load(file_handle)
if not loaded_data or not isinstance(loaded_data, dict):
print(f"Error: YAML file '{filename}' is empty or invalid format.")
return None, None
metadata = loaded_data.get('metadata', {})
settings_by_name = loaded_data.get('settings', {})
if not settings_by_name:
print(f"Warning: No 'settings' section found in {filename}.")
# Still return metadata if present
return {}, metadata
# Display loaded metadata
print("--- Metadata from file ---")
if metadata:
# Nicer printing for metadata
for key, value in metadata.items():
# Capitalize words in key, replace underscores
display_key = key.replace('_', ' ').title()
print(f" {display_key}: {value}")
else:
print(" (No metadata section found)")
print("--------------------------")
# Convert property names from settings back to OpenCV integer IDs
settings_with_ids = {}
print("Converting loaded property names to OpenCV IDs...")
for name, value in settings_by_name.items():
# Find the property ID by looking up the name in our discovered map
prop_id = getattr(cv2, name, None) # More direct lookup
# Alternative using the map: prop_id = next((pid for pid, pname in ALL_CV2_PROPERTIES.items() if pname == name), None)
if prop_id is not None and isinstance(prop_id, int):
# Ensure value is not None before adding (handles cases where YAML might have null)
if value is not None:
settings_with_ids[prop_id] = value
# print(f" Loaded {name} (ID: {prop_id}): {value}") # Verbose
else:
print(f" Skipping property '{name}' due to null value in YAML.")
else:
print(f" Warning: Property name '{name}' from YAML file is not recognized in cv2 constants. Skipping.")
print("Settings loaded successfully.")
return settings_with_ids, metadata # Return both
except yaml.YAMLError as e:
print(f"Error: Could not parse YAML file {filename}. Error: {e}")
return None, None
except Exception as e:
print(f"Error: Could not load settings from {filename}. Error: {e}")
return None, None
# --- Function to Apply Settings to Camera ---
def apply_settings_to_camera(camera_index, settings_to_apply):
"""Applies the provided settings dictionary (using integer IDs) to the camera."""
if not settings_to_apply:
print("No valid settings loaded or provided to apply.")
return
print(f"\nApplying loaded settings to camera index: {camera_index}")
cap = cv2.VideoCapture(camera_index, cv2.CAP_DSHOW)
if not cap.isOpened():
print(f"Error: Could not open camera index {camera_index} to apply settings.")
return
settings_applied = 0
settings_failed = 0
# Separate settings: Auto modes, Regular, Read-only/Info (using the global sets)
auto_settings = {k: v for k, v in settings_to_apply.items() if k in AUTO_MODE_PROPS}
regular_settings = {k: v for k, v in settings_to_apply.items() if k not in AUTO_MODE_PROPS and k not in READ_ONLY_OR_INFO_PROPS}
print("Applying settings (Auto modes first)...")
# Apply Auto Mode settings first
for prop_id, value in auto_settings.items():
prop_name = ALL_CV2_PROPERTIES.get(prop_id, f"UNKNOWN_{prop_id}")
if value is None: print(f" Skipping {prop_name} due to None value."); continue
try: apply_value = int(value) if abs(int(value) - value) < 0.001 else float(value)
except (ValueError, TypeError): apply_value = value # Keep original if conversion fails
success = cap.set(prop_id, apply_value)
if success: settings_applied += 1
else: settings_failed += 1
time.sleep(0.15) # Delay for auto modes
# Apply Regular settings next (iterate based on sorted IDs for some consistency)
sorted_regular_ids = sorted(regular_settings.keys())
for prop_id in sorted_regular_ids:
value = regular_settings[prop_id] # Get value using the ID
prop_name = ALL_CV2_PROPERTIES.get(prop_id, f"UNKNOWN_{prop_id}")
if value is None: print(f" Skipping {prop_name} due to None value."); continue
try: apply_value = int(value) if abs(int(value) - value) < 0.001 else float(value)
except (ValueError, TypeError): apply_value = value
success = cap.set(prop_id, apply_value)
if success: settings_applied += 1
else: settings_failed += 1
time.sleep(0.05) # Shorter delay
# Report skipped read-only properties found in file
skipped_count = 0
for prop_id in settings_to_apply:
if prop_id in READ_ONLY_OR_INFO_PROPS:
skipped_count += 1
print("\n--- Application Summary ---")
print(f"Settings applied successfully: {settings_applied}")
print(f"Settings failed/unsupported: {settings_failed}")
if skipped_count > 0:
print(f"Settings skipped (read-only/info): {skipped_count}")
print("\nReleasing camera..."); cap.release(); print(f"Camera {camera_index} released.")
# --- Main Execution Logic ---
if __name__ == "__main__":
# Argument parsing setup
parser = argparse.ArgumentParser(
description='Read or apply webcam settings using OpenCV and YAML.',
formatter_class=argparse.RawTextHelpFormatter # Keep help text formatting
)
parser.add_argument(
'--mode',
choices=['read', 'apply'],
required=True,
help="'read': Read all settings (value != -1), print table, save to YAML.\n"
"'apply': Load settings from a specific YAML file and apply."
)
parser.add_argument(
'--file',
type=str,
default=None, # No default, required for apply
help="Specify the YAML file name to LOAD settings from.\n"
"(Required for --mode apply, ignored for --mode read)."
)
parser.add_argument(
'--camera',
type=str,
default=None,
help="Specify camera by index OR name/partial name (case-insensitive).\n"
"If omitted, interactive selection is used when multiple cameras found."
)
args = parser.parse_args()
# --- Validate Arguments ---
if args.mode == 'apply' and not args.file:
parser.error("--file argument is required when using --mode apply")
if args.mode == 'read' and args.file:
print("Info: --file argument is ignored when using --mode read. Filename is generated from camera name.")
# --- Discover Cameras ---
available_cameras = find_available_cameras_with_names()
if not available_cameras:
print("\nFatal Error: No cameras detected by OpenCV.")
sys.exit(1)
# --- Determine Target Camera ---
selected_camera_info = None # Will store the {'index': i, 'name': n} dict
if args.camera:
# Handle --camera argument
print(f"\nAttempting to find camera specified by argument: '{args.camera}'")
matches = find_camera_by_identifier(args.camera, available_cameras)
if len(matches) == 1:
selected_camera_info = matches[0]
elif len(matches) > 1:
print("Ambiguous camera specified via --camera - multiple matches found:")
for i, cam in enumerate(matches): print(f" {i+1}: Index {cam['index']}, Name: {cam['name']}")
print("Exiting. Please use a more specific identifier or run without --camera for interactive selection.")
sys.exit(1)
else:
print(f"Error: No camera found matching '{args.camera}'.")
print("Available cameras discovered:")
for cam in available_cameras: print(f" Index: {cam['index']}, Name: {cam['name']}")
sys.exit(1)
else:
# --- Interactive Selection ---
if len(available_cameras) == 1:
selected_camera_info = available_cameras[0]
print(f"\nOnly one camera found: Index {selected_camera_info['index']}, Name: {selected_camera_info['name']}. Selecting automatically.")
else:
print("\nAvailable cameras:")
# Display numbered list for interactive choice
for i, cam_info in enumerate(available_cameras):
print(f" {i+1}: Index {cam_info['index']}, Name: {cam_info['name']}")
while selected_camera_info is None: # Loop until valid selection
try:
user_input = input(f"Enter the number (1-{len(available_cameras)}) OR name/partial name of the camera: ")
if not user_input: continue # Skip empty input
# First, check if input matches one of the displayed numbers (1, 2, ...)
try:
selection_num = int(user_input)
if 1 <= selection_num <= len(available_cameras):
selected_camera_info = available_cameras[selection_num - 1]
# print(f"Selected by number: Index {selected_camera_info['index']}, Name: {selected_camera_info['name']}") # Verbose
break # Exit loop
else:
print(f"Error: Number out of range (1-{len(available_cameras)}).")
continue # Prompt again
except ValueError:
# If not a number, treat as name/partial name
pass # Continue to name matching below
# If not a valid selection number, try matching by name/index string
matches = find_camera_by_identifier(user_input, available_cameras)
if len(matches) == 1:
selected_camera_info = matches[0]
# print(f"Selected by name: Index {selected_camera_info['index']}, Name: {selected_camera_info['name']}") # Verbose
break # Exit loop
elif len(matches) > 1:
print("Ambiguous input - multiple cameras match:")
for cam in matches: print(f" Index: {cam['index']}, Name: {cam['name']}")
print("Please be more specific.")
# Stay in loop to prompt again
else:
print(f"No camera found matching '{user_input}'. Please try again.")
# Stay in loop to prompt again
except EOFError: print("\nInput closed."); sys.exit(1)
except KeyboardInterrupt: print("\nOperation cancelled by user."); sys.exit(1)
# --- Final Check and Action Execution ---
if not selected_camera_info:
print("\nFatal Error: Could not determine target camera.")
sys.exit(1)
chosen_index = selected_camera_info['index']
print(f"\nSelected Camera: Index={chosen_index}, Name='{selected_camera_info.get('name', 'N/A')}'")
if args.mode == 'read':
current_settings_by_id = read_current_settings(chosen_index)
if current_settings_by_id is not None: # Check if reading was successful
# Print the table BEFORE saving
print_settings_table(current_settings_by_id)
# Save the filtered settings
save_settings_to_yaml(current_settings_by_id, selected_camera_info)
elif args.mode == 'apply':
# args.file is guaranteed to be set here due to earlier check
settings_to_apply, loaded_metadata = load_settings_from_yaml(args.file) # Get settings and metadata
if settings_to_apply is not None: # Check if loading settings was successful (even if empty)
# Optional: Check if applying to the 'correct' camera based on metadata
if loaded_metadata and loaded_metadata.get('camera_name_detected') != selected_camera_info.get('name'):
print("\nWarning: Applying settings from a file potentially saved for a different camera:")
print(f" File metadata indicates camera: '{loaded_metadata.get('camera_name_detected', 'N/A')}' (Index: {loaded_metadata.get('camera_index', 'N/A')})")
print(f" You are targeting camera : '{selected_camera_info.get('name', 'N/A')}' (Index: {chosen_index})")
try:
confirm = input("Continue anyway? (y/N): ").lower().strip()
if confirm != 'y':
print("Aborted by user.")
sys.exit(0)
except (EOFError, KeyboardInterrupt): print("\nAborted by user."); sys.exit(0)
apply_settings_to_camera(chosen_index, settings_to_apply)
print("\nScript finished.")
Security Considerations
- Camera Access: The script requires access to system webcams. This is an inherent requirement for its function. Ensure that running the script aligns with organisational or personal privacy policies regarding camera usage.
- File I/O: The script reads from and writes to YAML files. The filenames are sanitised to prevent common path traversal or invalid character issues. However, users should ensure that the specified file paths for loading (
--file
argument) are from trusted sources, as YAML files can theoretically be crafted to exploit vulnerabilities in YAML parsers (thoughyaml.safe_load
is used here for mitigation). - No Privileged Operations: The script does not inherently require administrative or root privileges for basic OpenCV camera access or file operations in user-writable directories.
- No Hard-coded Sensitive Values: The script does not contain hard-coded passwords, API keys, or other sensitive credentials.
Limitations
- Operating System and Backend Dependency: Camera name detection relies on
pygrabber.dshow_graph
, andcv2.CAP_DSHOW
is used as the capture backend. These are primarily Windows-centric (DirectShow). On other operating systems (Linux, macOS), camera name detection might fail (falling back to “Camera Index X”), and a different OpenCV backend might be used by default ifCAP_DSHOW
is unavailable, potentially affecting which properties are accessible or how they behave. - Camera Driver and Hardware Variability: The success of reading or writing specific camera properties heavily depends on the camera’s hardware capabilities and the quality of its driver. Not all properties listed by OpenCV will be supported or settable by all cameras. The script’s filtering of
-1.0
values helps manage this, but some properties might accept a value when set viacap.set()
but not actually change the camera’s state, orcap.get()
might return a fixed or incorrect value. - Property Interpretation: Some OpenCV properties have values whose meanings are not universally standardised (e.g.,
CAP_PROP_MODE
) or can be camera-specific. The script treats these as numerical values. CAP_PROP_SETTINGS
: TheCAP_PROP_SETTINGS
property, which typically opens the camera’s driver-specific settings dialogue, is explicitly excluded from being set, as it does not behave like a standard property taking a numerical value.
Conclusion
The webcam_manager.py
script provides a practical and extensible tool for a users who require consistent and automated control over webcam settings. By leveraging OpenCV and YAML, it offers a clear and efficient method for capturing, storing, and reapplying camera configurations, solving a common pain point in various video-dependent applications. Its command-line interface supports automation, while the readable YAML format allows for easy inspection and manual adjustment of settings profiles if needed.