Estimator Replacement Plugin Example¶
This document provides a detailed analysis of the people count estimator replacement plugin
(luna_video_agent/plugins/plugin_examples/onnx_people_counter/people_count_onnx_plugin.py), which
demonstrates how to replace the standard SDK people counting estimator with a custom ONNX Runtime
implementation using density map models.
Overview¶
The example plugin replaces the built-in people counting estimator with a custom implementation that:
Uses ONNX Runtime for inference with density map models (CSRNet, MCNN)
Supports both CPU and GPU execution
Implements asynchronous batch processing
Extracts people counts and coordinate locations from density maps
Handles image cropping and coordinate transformation
Plugin architecture¶
The plugin consists of three main components:
ONNXPeopleCountEstimator - Custom estimator implementing the estimation logic
OnnxruntimeWrapper - Wrapper for asynchronous ONNX Runtime execution
PeopleCountReplacementPlugin - Plugin class that integrates the estimator into the SDK
Note
Since lunavl and sdk_loop are already dependencies of luna_video_agent, you can navigate
directly to lunavl.sdk.estimators.image_estimators.people_count.PeopleCountEstimatorV2 and
sdk_loop.estimators.estimators_collection.EstimatorsCollection from your IDE to inspect the
interfaces and return types this example implements against.
Asynchronous ONNX Runtime execution¶
Why asynchrony is needed¶
The service’s analytics pipeline (sdk_loop) is built on Python’s asyncio. When multiple frames
are estimated concurrently, blocking the event loop would:
Prevent other frames/streams from being processed
Reduce overall throughput
Cause latency spikes
The challenge with ONNX Runtime¶
ONNX Runtime’s standard session.run() method is synchronous and blocking. When called,
it blocks the calling thread until inference completes. This creates a problem:
Calling
session.run()directly in anasyncfunction would block the entire event loopSimply wrapping it in
async defdoesn’t make it non-blockingUsing
asyncio.to_thread()orrun_in_executor()would work but adds overhead
The solution: ONNX Runtime’s async API¶
ONNX Runtime provides a run_async() method that allows truly asynchronous execution:
def setFutureResult(future, result, error):
"""Set result to asyncio future."""
if not future.done():
future.set_result((error, result))
def ortCallback(result, waiter: tuple[asyncio.AbstractEventLoop, asyncio.Future], error: str):
"""Callback for onnx thread."""
loop = waiter[0]
future = waiter[1]
loop.call_soon_threadsafe(setFutureResult, future, result, error)
async def execute(output_names, input_feed, session: ort.InferenceSession, loop: asyncio.AbstractEventLoop):
"""Async execute onnx prediction using run_async."""
future = loop.create_future()
session.run_async(output_names=output_names, input_feed=input_feed,
callback=ortCallback, user_data=(loop, future))
error, res = await future
if error:
raise RuntimeError(f"ONNX inference error: {error}")
return res
How this works:
Create a Future:
loop.create_future()creates a Future object that will hold the resultStart async inference:
session.run_async()starts inference in ONNX Runtime’s thread poolRegister callback: When inference completes, ONNX Runtime calls
ortCallbackfrom its threadThread-safe result passing: The callback uses
loop.call_soon_threadsafe()to safely pass the result back to the asyncio event loopAwait result: The
await futuresuspends the coroutine until the result is ready, allowing other coroutines to run
Why this approach:
Non-blocking: The event loop is not blocked during inference
Efficient: No thread pool executor overhead or thread creation per request
Native support: Uses ONNX Runtime’s built-in async capabilities
Scalable: Multiple inferences can run concurrently without blocking each other
OnnxruntimeWrapper class¶
The wrapper encapsulates the async execution logic:
class OnnxruntimeWrapper:
"""Wrapper for asynchronous ONNX Runtime execution."""
def __init__(self, onnxSession):
self.onnxSession = onnxSession
async def forward(self, output_names, input_feed):
"""Execute inference asynchronously."""
loop = asyncio.get_running_loop()
return await execute(output_names, input_feed, self.onnxSession, loop)
This provides a clean interface: you call await wrapper.forward() and the inference
happens asynchronously without blocking the event loop.
Custom estimator implementation¶
ONNXPeopleCountEstimator class¶
The estimator class handles model initialization, inference, and postprocessing:
Initialization¶
def __init__(self, modelPath: Path, useGpu: bool = False, gpuDeviceId: int = 0, maxWorkers: int = 4) -> None:
# Setup execution providers
if useGpu:
providers = [
(
"CUDAExecutionProvider",
{
"device_id": gpuDeviceId,
"arena_extend_strategy": "kNextPowerOfTwo",
"gpu_mem_limit": 2 * 1024 * 1024 * 1024, # 2GB
"cudnn_conv_algo_search": "EXHAUSTIVE",
"do_copy_in_default_stream": True,
},
),
"CPUExecutionProvider",
]
else:
providers = ["CPUExecutionProvider"]
sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
self.session = ort.InferenceSession(
str(modelPath),
sess_options=sess_options,
providers=providers,
)
self.input_name = self.session.get_inputs()[0].name
self.output_name = self.session.get_outputs()[0].name
self.onnxWrapper = OnnxruntimeWrapper(self.session)
Key points:
Execution providers: CUDA for GPU, with CPU fallback
CUDA configuration:
device_id: Which GPU to usearena_extend_strategy: Memory allocation strategygpu_mem_limit: Maximum GPU memory usagecudnn_conv_algo_search: cuDNN algorithm selection (EXHAUSTIVE for best performance)
Graph optimization: Enable all ONNX Runtime optimizations
Provider fallback: If CUDA fails, automatically falls back to CPU
Batch estimation¶
async def estimateBatch(
self,
images: list[VLImage | ImageForPeopleEstimation | tuple[VLImage, Rect]],
estimationTargets: EstimationTargets = EstimationTargets.T1,
asyncEstimate: Literal[True] = True,
) -> list[PeopleCount]:
"""Estimate people count in multiple images asynchronously."""
if not asyncEstimate:
raise NotImplementedError("Synchronous estimation is not supported")
tasks = [self._estimate(image, estimationTargets) for image in images]
return await asyncio.gather(*tasks)
Key points:
Async by default: Only asynchronous mode is supported
Concurrent processing: All images are processed concurrently using
asyncio.gather()Return type: Returns
list[PeopleCount]matching the original estimator interface
Single image estimation¶
The _estimate() method processes a single image and supports the same input shapes as the original
PeopleCountEstimatorV2: a plain VLImage, an ImageForPeopleEstimation (image plus detection
area), or a (VLImage, Rect) tuple.
async def _estimate(
self, image: VLImage | ImageForPeopleEstimation | tuple[VLImage, Rect], estimationTargets: EstimationTargets
) -> PeopleCount:
if isinstance(image, VLImage):
vlImage = image
detectArea = None
elif isinstance(image, ImageForPeopleEstimation):
vlImage = image.image
detectArea = image.detectArea
elif (
isinstance(image, tuple)
and len(image) == 2
and isinstance(image[0], VLImage)
and isinstance(image[1], Rect)
):
vlImage, detectArea = image
else:
raise ValueError("Unsupported image format")
if detectArea is not None:
processedImage = vlImage.crop(detectArea)
else:
processedImage = vlImage
imgH, imgW = processedImage.rect.height, processedImage.rect.width
npArray = processedImage.asNPArray()
input_feed = {self.input_name: npArray}
outputs = await self.onnxWrapper.forward([self.output_name], input_feed)
estimation = self._postprocessModelOutput(outputs[0], imgH, imgW, estimationTargets)
if detectArea is not None and estimation.points.coordinates:
adjusted_coords = adjustCoordsToOriginal(estimation.points.coordinates, detectArea)
estimation = Estimation(estimation.count, adjusted_coords)
return PeopleCount(estimation)
Processing steps:
Input handling: Supports multiple input formats (
VLImage,ImageForPeopleEstimation, tuple)Image cropping: Applies detection area cropping if specified
Inference: Runs async ONNX inference
Postprocessing: Extracts count and coordinates from the density map
Coordinate adjustment: Maps coordinates back to original image space if cropped
Return: Creates a
PeopleCountobject matching the original estimator’s output format
Density map postprocessing¶
The plugin processes density map outputs to extract people counts and locations:
def _postprocessModelOutput(
self, modelOutput: np.ndarray, imgHeight: int, imgWidth: int, estimationTargets: EstimationTargets
) -> Estimation:
# For density map models (CSRNet, MCNN)
# Sum all values in density map to get count
count = int(np.sum(modelOutput))
count = max(0, count) # Ensure non-negative
# Extract coordinates if requested (T1)
if estimationTargets == EstimationTargets.T1:
coordinates = extractCoordinatesFromDensityMap(modelOutput, imgHeight, imgWidth, count)
else:
# T2: only count, no coordinates
coordinates = []
return Estimation(count, coordinates)
Density map interpretation:
Count extraction: Sum all density map values to get total people count
Coordinate extraction: Find peaks in density map and convert to image coordinates
Target modes:
T1: Extract both count and coordinates
T2: Extract only count
Coordinate extraction:
def extractCoordinatesFromDensityMap(
densityMap: np.ndarray, imgHeight: int, imgWidth: int, count: int
) -> list[tuple[int, int]]:
if count == 0:
return []
# Remove batch and channel dimensions
if densityMap.ndim == 4:
densityMap = densityMap[0, 0] # [height, width]
elif densityMap.ndim == 3:
densityMap = densityMap[0] # [height, width]
# Flatten and get top count positions
flatMap = densityMap.flatten()
topIndices = np.argsort(flatMap)[-count:] # Get top 'count' values
coordinates = []
mapH, mapW = densityMap.shape
imgH, imgW = imgHeight, imgWidth
for idx in topIndices:
yMap = idx // mapW
xMap = idx % mapW
# Scale coordinates from density map to original image size
xImg = int((xMap / mapW) * imgW)
yImg = int((yMap / mapH) * imgH)
coordinates.append((xImg, yImg))
return coordinates
Algorithm:
Normalize dimensions: Remove batch and channel dimensions
Find peaks: Use
np.argsort()to find the highest density valuesConvert indices: Transform flat array indices to 2D coordinates
Scale coordinates: Map from density map resolution to original image resolution
Plugin class implementation¶
PeopleCountReplacementPlugin class¶
The plugin class integrates the custom estimator:
class PeopleCountReplacementPlugin(EstimatorReplacementPlugin):
"""Plugin for replacing the default people count estimator with ONNX implementation."""
def close(self) -> None: ...
@property
def estimatorBrokerName(self) -> str:
"""Get the system name of the estimator to replace."""
return "peopleCountEstimator"
@property
def modelDirPath(self) -> Path:
"""Get the directory containing the model file."""
return Path(__file__).parent
@property
def modelName(self) -> str:
"""Get the model filename."""
return "csrnet_uint8.onnx"
@property
def modelPath(self) -> Path:
"""Get the full path to the model file."""
return self.modelDirPath / self.modelName
@property
def deviceClass(self) -> Literal["cpu", "gpu"]:
"""Get the device class for this estimator."""
estimatorDeviceType = self.appConfig.estimatorsSettings.peopleCountEstimator.deviceClass
if estimatorDeviceType == "global":
return self.globalDeviceClass
return estimatorDeviceType
def createReplacementEstimator(self) -> ONNXPeopleCountEstimator:
"""Create a new ONNX people count estimator instance."""
return ONNXPeopleCountEstimator(
modelPath=self.modelPath,
useGpu=True if self.deviceClass == "gpu" else False,
gpuDeviceId=self.defaultGpuDevice,
maxWorkers=4,
)
Configuration:
estimatorBrokerName: Identifies which estimator to replace in
EstimatorsCollectionModel path: Constructs path to the ONNX model file located next to the plugin
Device selection: Checks
estimatorsSettings.peopleCountEstimator.deviceClass, which is populated from the[LUNA_VIDEO_AGENT_PEOPLE_COUNT_ESTIMATOR_SETTINGS.RUNTIME_SETTINGS]section ofluna_video_agent/configs/config.conf(or the equivalent configurator entry). If set to “global”, it falls back to the global device class setting.Estimator creation: Instantiates
ONNXPeopleCountEstimatorwith appropriate settings
Usage in the service¶
Plugin structure¶
luna_video_agent/plugins/plugin_examples/onnx_people_counter/
├── __init__.py
├── people_count_onnx_plugin.py # Main plugin implementation
├── csrnet_uint8.onnx # ONNX model file
├── requirements.txt # Dependencies
├── requirements_gpu.txt # Dependencies for GPU
└── README.md # Documentation
Dependencies¶
The plugin requires the following Python packages:
onnxruntime>=1.22.0
The plugin requires the following Python packages for GPU execution:
onnxruntime-gpu==1.22.0
nvidia-cublas-cu12
nvidia-cudnn-cu12
nvidia-curand-cu12
nvidia-cufft-cu12
nvidia-cuda-runtime-cu12
Warning
These dependencies must be installed in the service environment before enabling the plugin.
The plugin will fail to load if onnxruntime is not available.
Integration steps¶
Copy plugin to container and install dependencies:
FROM my-registry/luna-video-agent:v.x.x.x # Copy plugin files COPY --chown=1001:0 plugin_examples/onnx_people_counter ./luna_video_agent/plugins/plugin_examples/onnx_people_counter # Install requirements.txt or requirements_gpu.txt dependencies BEFORE enabling the plugin RUN pip install --no-cache-dir -r ./luna_video_agent/plugins/plugin_examples/onnx_people_counter/requirements.txt
Important
The
pip installstep is mandatory. Without it, the plugin cannot importonnxruntimeand will fail during initialization.Note: Installing gpu dependencies requires to add cuda libraries folders to LD_LIBRARY_PATH.
ENV LD_LIBRARY_PATH="/srv/cudalibs:/srv/ipp_libs:/usr/lib/python3.13/site-packages/nvidia/cublas/lib:/usr/lib/python3.13/site-packages/nvidia/cudnn/lib:/usr/lib/python3.13/site-packages/nvidia/curand/lib:/usr/lib/python3.13/site-packages/nvidia/cufft/lib:/usr/lib/python3.13/site-packages/nvidia/cuda_runtime/lib:/usr/lib/python3.13/site-packages/nvidia/cuda_nvrtc/lib"
Adjust python version and library paths according to your environment. The above example is for Python 3.13.
Enable the plugin:
export LUNA_VIDEO_AGENT_ACTIVE_PLUGINS="plugins.plugin_examples.onnx_people_counter.people_count_onnx_plugin"
Restart the service:
docker restart <container_id>
Advanced topics¶
Performance optimization¶
Batch processing:
The plugin processes all images in a batch concurrently using asyncio.gather(). This allows:
Multiple inferences to run in parallel
Efficient use of GPU resources
Better throughput for multi-image requests
Memory management:
CUDA execution provider is configured with:
Memory limit: 2GB per estimator
Arena extension strategy: Powers of 2 for efficient allocation
Graph optimization: All ONNX Runtime optimizations enabled
Error handling¶
Async errors:
The execute() function checks for ONNX Runtime errors passed via callback:
error, res = await future
if error:
raise RuntimeError(f"ONNX inference error: {error}")
Input validation:
The estimator validates input formats and raises ValueError for unsupported types.
Extending the example¶
Different models:
To use a different density map model:
Replace
csrnet_uint8.onnxwith your modelAdjust
modelNamepropertyEnsure the model outputs a density map with compatible format
Custom postprocessing:
Override _postprocessModelOutput() to implement different postprocessing logic:
Different peak detection algorithms
Filtering of low-confidence detections
Custom coordinate extraction strategies
Additional estimators:
Follow the same pattern to replace other estimators:
Identify the estimator broker name in
EstimatorsCollectionFind the return type in
lunavl.sdk.estimatorsImplement your custom estimator returning compatible objects
Create a plugin class with the appropriate
estimatorBrokerName
Summary¶
This example demonstrates:
Async ONNX inference: Using
run_async()with callbacks and FuturesProper integration: Matching return types from the original estimator
Flexible configuration: Supporting both CPU and GPU execution
Complete implementation: From model loading to coordinate extraction
Production-ready: Error handling, resource management, and optimization
The key insight is the asynchronous execution pattern: ONNX Runtime’s run_async() method
with thread-safe callbacks allows true non-blocking inference that integrates seamlessly
with the service’s asyncio-based architecture.
Complete source files¶
__init__.py¶from .people_count_onnx_plugin import ( Estimation, ONNXPeopleCountEstimator, PeopleCountReplacementPlugin, )people_count_onnx_plugin.py¶import asyncio from pathlib import Path from typing import Literal import numpy as np import onnxruntime as ort from luna_plugins.estimator_plugins.estimator_replacement_plugin import EstimatorBase, EstimatorReplacementPlugin from lunavl.sdk.estimators.image_estimators.people_count import EstimationTargets, ImageForPeopleEstimation, PeopleCount from lunavl.sdk.image_utils.geometry import Rect, Vec2D from lunavl.sdk.image_utils.image import VLImage def setFutureResult(future, result, error): """Set result to asyncio future.""" if not future.done(): future.set_result((error, result)) def ortCallback(result, waiter: tuple[asyncio.AbstractEventLoop, asyncio.Future], error: str): """Callback for onnx thread.""" loop = waiter[0] future = waiter[1] loop.call_soon_threadsafe(setFutureResult, future, result, error) async def execute(output_names, input_feed, session: ort.InferenceSession, loop: asyncio.AbstractEventLoop): """Async execute onnx prediction using run_async. Args: output_names: name of the outputs input_feed: dictionary { input_name: numpy_array } session: ort session loop: current asyncio event loop Return: prediction result """ future = loop.create_future() session.run_async(output_names=output_names, input_feed=input_feed, callback=ortCallback, user_data=(loop, future)) error, res = await future if error: raise RuntimeError(f"ONNX inference error: {error}") return res class OnnxruntimeWrapper: """Wrapper for asynchronous ONNX Runtime execution.""" def __init__(self, onnxSession): self.onnxSession = onnxSession async def forward(self, output_names, input_feed): """Execute inference asynchronously. Args: output_names: List of output names input_feed: Dict with numpy array inputs Returns: List of numpy arrays (outputs) """ loop = asyncio.get_running_loop() return await execute(output_names, input_feed, self.onnxSession, loop) class Estimation: """Container for people counting estimation results. Holds the count of detected people and their coordinate positions. Attributes: count: The number of people detected in the image. points: Points object containing coordinate information. """ class Points: """Container for point coordinates in an estimation. Attributes: coordinates: List of (x, y) coordinate tuples representing people positions. """ def __init__(self, coordinates: list[tuple[int, int]]): """Initialize Points with coordinate data. Args: coordinates: List of (x, y) tuples representing point locations. """ self.coordinates = coordinates def getPoints(self): """Convert coordinates to Vec2D objects. Returns: List of Vec2D objects representing the point coordinates. """ return [Vec2D(x, y) for x, y in self.coordinates] def __init__(self, count: int, coordinates: list[tuple[int, int]]): """Initialize an Estimation with count and coordinates. Args: count: The number of people detected. coordinates: List of (x, y) tuples for each detected person. """ self.count = count self.points = self.Points(coordinates) def extractCoordinatesFromDensityMap( densityMap: np.ndarray, imgHeight: int, imgWidth: int, count: int ) -> list[tuple[int, int]]: """Extract person coordinates from a density map. Finds the top peak locations in a density map and converts them to image coordinates scaled to the original image dimensions. Args: densityMap: The density map output from the model. imgHeight: Height of the original image. imgWidth: Width of the original image. count: Number of people to extract coordinates for. Returns: List of (x, y) tuples representing person locations in image coordinates. """ if count == 0: return [] # Remove batch and channel dimensions if densityMap.ndim == 4: densityMap = densityMap[0, 0] # [height, width] elif densityMap.ndim == 3: densityMap = densityMap[0] # [height, width] # Flatten and get top count positions flatMap = densityMap.flatten() topIndices = np.argsort(flatMap)[-count:] # Get top 'count' values # Convert flat indices to 2D coordinates coordinates = [] mapH, mapW = densityMap.shape imgH, imgW = imgHeight, imgWidth for idx in topIndices: yMap = idx // mapW xMap = idx % mapW # Scale coordinates from density map to original image size xImg = int((xMap / mapW) * imgW) yImg = int((yMap / mapH) * imgH) coordinates.append((xImg, yImg)) return coordinates def adjustCoordsToOriginal(coords: list[tuple[int, int]], cropArea: Rect) -> list[tuple[int, int]]: """Adjust coordinates from cropped space back to original image space. Args: coords: List of (x, y) coordinates in cropped image space. cropArea: Rectangle defining the crop region offset. Returns: List of (x, y) coordinates adjusted to original image space. """ offsetX, offsetY = int(cropArea.x), int(cropArea.y) return [(x + offsetX, y + offsetY) for x, y in coords] class ONNXPeopleCountEstimator(EstimatorBase): """ONNX-based people counting estimator using density map models. This estimator uses ONNX Runtime to perform inference with density map models like CSRNet or MCNN for counting people in images. Attributes: onnxWrapper: Async ONNX Runtime wrapper. session: ONNX Runtime inference session. input_name: Name of the model's input node. output_name: Name of the model's output node. """ def __init__(self, modelPath: Path, useGpu: bool = False, gpuDeviceId: int = 0, maxWorkers: int = 4) -> None: """Initialize the ONNX people count estimator. Args: modelPath: Path to the ONNX model file. useGpu: Whether to use GPU acceleration. gpuDeviceId: GPU device ID to use if GPU is enabled. maxWorkers: Maximum number of worker threads for async operations. Raises: ImportError: If onnxruntime is not installed. """ # Setup execution providers if useGpu: providers = [ ( "CUDAExecutionProvider", { "device_id": gpuDeviceId, "arena_extend_strategy": "kNextPowerOfTwo", "gpu_mem_limit": 2 * 1024 * 1024 * 1024, # 2GB "cudnn_conv_algo_search": "EXHAUSTIVE", "do_copy_in_default_stream": True, }, ), "CPUExecutionProvider", ] else: providers = ["CPUExecutionProvider"] # Create ONNX Runtime session sess_options = ort.SessionOptions() sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL self.session = ort.InferenceSession( str(modelPath), sess_options=sess_options, providers=providers, ) # Get input/output names self.input_name = self.session.get_inputs()[0].name self.output_name = self.session.get_outputs()[0].name # Create async wrapper self.onnxWrapper = OnnxruntimeWrapper(self.session) def close(self): """Close the estimator and release resources.""" def _postprocessModelOutput( self, modelOutput: np.ndarray, imgHeight: int, imgWidth: int, estimationTargets: EstimationTargets ) -> Estimation: """Postprocess the model output to extract count and coordinates. Args: modelOutput: Raw output from the density map model. imgHeight: Height of the processed image. imgWidth: Width of the processed image. estimationTargets: Target type (T1 with coordinates, T2 count only). Returns: Estimation object containing count and optional coordinates. """ # For density map models (CSRNet, MCNN) # Sum all values in density map to get count count = int(np.sum(modelOutput)) count = max(0, count) # Ensure non-negative # Extract coordinates if requested (T1) if estimationTargets == EstimationTargets.T1: coordinates = extractCoordinatesFromDensityMap(modelOutput, imgHeight, imgWidth, count) else: # T2: only count, no coordinates coordinates = [] return Estimation(count, coordinates) async def _estimate( self, image: VLImage | ImageForPeopleEstimation | tuple[VLImage, Rect], estimationTargets: EstimationTargets ) -> PeopleCount: """Internal async method to estimate people count in a single image. Args: image: Input image in various supported formats. estimationTargets: Target type for estimation. Returns: PeopleCount object with estimation results. Raises: Exception: If image format is not supported. """ if isinstance(image, VLImage): vlImage = image detectArea = None elif isinstance(image, ImageForPeopleEstimation): vlImage = image.image detectArea = image.detectArea elif ( isinstance(image, tuple) and len(image) == 2 and isinstance(image[0], VLImage) and isinstance(image[1], Rect) ): vlImage, detectArea = image else: raise ValueError("Unsupported image format") # Crop image if detection area is specified if detectArea is not None: processedImage = vlImage.crop(detectArea) else: processedImage = vlImage # Get image dimensions imgH, imgW = processedImage.rect.height, processedImage.rect.width # Convert to numpy array npArray = processedImage.asNPArray() # Run async inference input_feed = {self.input_name: npArray} outputs = await self.onnxWrapper.forward([self.output_name], input_feed) # Postprocess output to get count and coordinates estimation = self._postprocessModelOutput(outputs[0], imgH, imgW, estimationTargets) # Adjust coordinates back to original image space if cropped if detectArea is not None and estimation.points.coordinates: adjusted_coords = adjustCoordsToOriginal(estimation.points.coordinates, detectArea) estimation = Estimation(estimation.count, adjusted_coords) return PeopleCount(estimation) async def estimateBatch( self, images: list[VLImage | ImageForPeopleEstimation | tuple[VLImage, Rect]], estimationTargets: EstimationTargets = EstimationTargets.T1, asyncEstimate: Literal[True] = True, ) -> list[PeopleCount]: """Estimate people count in multiple images asynchronously. Args: images: List of input images in various supported formats. estimationTargets: Target type (T1 with coordinates, T2 count only). Defaults to T1. asyncEstimate: Must be True (only async mode supported). Returns: List of PeopleCount objects with estimation results. """ print("estimate with ONNXPeopleCountEstimator: started") if not asyncEstimate: raise NotImplementedError("Synchronous estimation is not supported") # Process all images concurrently tasks = [self._estimate(image, estimationTargets) for image in images] print("estimate with ONNXPeopleCountEstimator: completed") return await asyncio.gather(*tasks) class PeopleCountReplacementPlugin(EstimatorReplacementPlugin): """Plugin for replacing the default people count estimator with ONNX implementation. This plugin integrates the ONNXPeopleCountEstimator into the Luna SDK, replacing the built-in estimator with a custom ONNX-based implementation. """ def close(self) -> None: ... @property def estimatorBrokerName(self) -> str: """Get the system name of the estimator to replace. Returns: The name "peopleCountEstimator". """ return "peopleCountEstimator" @property def modelDirPath(self) -> Path: """Get the directory containing the model file. Returns: Path to the directory containing this plugin file. """ return Path(__file__).parent @property def modelName(self) -> str: """Get the model filename. Returns: The ONNX model filename. """ return "csrnet_uint8.onnx" @property def modelPath(self) -> Path: """Get the full path to the model file. Returns: Complete path to the ONNX model file. """ return self.modelDirPath / self.modelName @property def deviceClass(self) -> Literal["cpu", "gpu"]: """Get the device class for this estimator. Checks the configuration for people counter device settings, falling back to global device class if set to "global". Returns: The device class, either "cpu" or "gpu". """ estimatorDeviceType = self.appConfig.estimatorsSettings.peopleCountEstimator.deviceClass if estimatorDeviceType == "global": return self.globalDeviceClass return estimatorDeviceType def createReplacementEstimator(self) -> ONNXPeopleCountEstimator: """Create a new ONNX people count estimator instance. Returns: Configured ONNXPeopleCountEstimator instance. """ return ONNXPeopleCountEstimator( modelPath=self.modelPath, useGpu=True if self.deviceClass == "gpu" else False, gpuDeviceId=self.defaultGpuDevice, maxWorkers=4, )