add altitude + camera spec component and calc tile size by this

also restrict detections to be no bigger than in classes.json
This commit is contained in:
Oleksandr Bezdieniezhnykh
2025-09-23 01:48:10 +03:00
parent b0e4b467c1
commit fde9a9f418
36 changed files with 715 additions and 222 deletions
+4 -1
View File
@@ -9,11 +9,14 @@ cdef class AIRecognitionConfig:
cdef public double tracking_intersection_threshold
cdef public int big_image_tile_overlap_percent
cdef public int tile_size
cdef public bytes file_data
cdef public list[str] paths
cdef public int model_batch_size
cdef public double altitude
cdef public double focal_length
cdef public double sensor_width
@staticmethod
cdef from_msgpack(bytes data)
+17 -4
View File
@@ -15,7 +15,10 @@ cdef class AIRecognitionConfig:
model_batch_size,
big_image_tile_overlap_percent,
tile_size
altitude,
focal_length,
sensor_width
):
self.frame_period_recognition = frame_period_recognition
self.frame_recognition_seconds = frame_recognition_seconds
@@ -30,7 +33,10 @@ cdef class AIRecognitionConfig:
self.model_batch_size = model_batch_size
self.big_image_tile_overlap_percent = big_image_tile_overlap_percent
self.tile_size = tile_size
self.altitude = altitude
self.focal_length = focal_length
self.sensor_width = sensor_width
def __str__(self):
return (f'frame_seconds : {self.frame_recognition_seconds}, distance_confidence : {self.tracking_distance_confidence}, '
@@ -39,7 +45,11 @@ cdef class AIRecognitionConfig:
f'frame_period_recognition : {self.frame_period_recognition}, '
f'big_image_tile_overlap_percent: {self.big_image_tile_overlap_percent}, '
f'paths: {self.paths}, '
f'model_batch_size: {self.model_batch_size}')
f'model_batch_size: {self.model_batch_size}, '
f'altitude: {self.altitude}, '
f'focal_length: {self.focal_length}, '
f'sensor_width: {self.sensor_width}'
)
@staticmethod
cdef from_msgpack(bytes data):
@@ -58,5 +68,8 @@ cdef class AIRecognitionConfig:
unpacked.get("m_bs"),
unpacked.get("ov_p", 20),
unpacked.get("tile_size", 550),
unpacked.get("cam_a", 400),
unpacked.get("cam_fl", 24),
unpacked.get("cam_sw", 23.5)
)
+1 -1
View File
@@ -48,7 +48,7 @@ cdef class Annotation:
return f"{self.name}: No detections"
detections_str = ", ".join(
f"class: {d.cls} {d.confidence * 100:.1f}% ({d.x:.2f}, {d.y:.2f})"
f"class: {d.cls} {d.confidence * 100:.1f}% ({d.x:.2f}, {d.y:.2f}) ({d.w:.2f}, {d.h:.2f})"
for d in self.detections
)
return f"{self.name}: {detections_str}"
+2 -1
View File
@@ -1,7 +1,7 @@
echo Build Cython app
set CURRENT_DIR=%cd%
REM Change to the parent directory of the current location
REM Change to the file's directory
cd /d %~dp0
echo remove dist folder:
@@ -58,5 +58,6 @@ robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "onnx_engi
robocopy "dist\azaion-inference\_internal" "..\dist-dlls\_internal" /E
robocopy "dist\azaion-inference" "..\dist-azaion" "azaion-inference.exe"
robocopy "." "..\dist-azaion" "classes.json"
cd /d %CURRENT_DIR%
+21
View File
@@ -0,0 +1,21 @@
[
{ "Id": 0, "Name": "ArmorVehicle", "ShortName": "Броня", "Color": "#ff0000", "MaxSizeM": 8 },
{ "Id": 1, "Name": "Truck", "ShortName": "Вантаж.", "Color": "#00ff00", "MaxSizeM": 8 },
{ "Id": 2, "Name": "Vehicle", "ShortName": "Машина", "Color": "#0000ff", "MaxSizeM": 7 },
{ "Id": 3, "Name": "Atillery", "ShortName": "Арта", "Color": "#ffff00", "MaxSizeM": 14 },
{ "Id": 4, "Name": "Shadow", "ShortName": "Тінь", "Color": "#ff00ff", "MaxSizeM": 9 },
{ "Id": 5, "Name": "Trenches", "ShortName": "Окопи", "Color": "#00ffff", "MaxSizeM": 10 },
{ "Id": 6, "Name": "MilitaryMan", "ShortName": "Військов", "Color": "#188021", "MaxSizeM": 2 },
{ "Id": 7, "Name": "TyreTracks", "ShortName": "Накати", "Color": "#800000", "MaxSizeM": 5 },
{ "Id": 8, "Name": "AdditArmoredTank", "ShortName": "Танк.захист", "Color": "#008000", "MaxSizeM": 7 },
{ "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080", "MaxSizeM": 8 },
{ "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#a52a2a", "MaxSizeM": 12 },
{ "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000", "MaxSizeM": 3 },
{ "Id": 12, "Name": "CamouflageNet", "ShortName": "Сітка", "Color": "#87ceeb", "MaxSizeM": 14 },
{ "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f", "MaxSizeM": 8 },
{ "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff", "MaxSizeM": 15 },
{ "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1", "MaxSizeM": 20 },
{ "Id": 16, "Name": "Caponier", "ShortName": "Капонір", "Color": "#ffa500", "MaxSizeM": 10 },
{ "Id": 17, "Name": "Ammo", "ShortName": "БК", "Color": "#33658a", "MaxSizeM": 2 },
{ "Id": 18, "Name": "Protect.Struct", "ShortName": "Зуби.драк", "Color": "#969647", "MaxSizeM": 2 }
]
+17 -2
View File
@@ -14,8 +14,23 @@ cdef str MODELS_FOLDER
cdef int SMALL_SIZE_KB
cdef str SPLIT_SUFFIX
cdef int TILE_DUPLICATE_CONFIDENCE_THRESHOLD
cdef double TILE_DUPLICATE_CONFIDENCE_THRESHOLD
cdef int METERS_IN_TILE
cdef log(str log_message)
cdef logerror(str error)
cdef format_time(int ms)
cdef format_time(int ms)
cdef dict[int, AnnotationClass] annotations_dict
cdef class AnnotationClass:
cdef public int id
cdef public str name
cdef public str color
cdef public int max_object_size_meters
cdef enum WeatherMode:
Norm = 0
Wint = 20
Night = 40
+32 -1
View File
@@ -1,3 +1,4 @@
import json
import sys
from loguru import logger
@@ -13,7 +14,37 @@ cdef str MODELS_FOLDER = "models"
cdef int SMALL_SIZE_KB = 3
cdef str SPLIT_SUFFIX = "!split!"
cdef int TILE_DUPLICATE_CONFIDENCE_THRESHOLD = 5
cdef double TILE_DUPLICATE_CONFIDENCE_THRESHOLD = 0.01
cdef int METERS_IN_TILE = 25
cdef class AnnotationClass:
def __init__(self, id, name, color, max_object_size_meters):
self.id = id
self.name = name
self.color = color
self.max_object_size_meters = max_object_size_meters
def __str__(self):
return f'{self.id} {self.name} {self.color} {self.max_object_size_meters}'
cdef int weather_switcher_increase = 20
WEATHER_MODE_NAMES = {
Norm: "Norm",
Wint: "Wint",
Night: "Night"
}
with open('classes.json', 'r', encoding='utf-8') as f:
j = json.loads(f.read())
annotations_dict = {}
for i in range(0, weather_switcher_increase * 3, weather_switcher_increase):
for cl in j:
id = i + cl['Id']
mode_name = WEATHER_MODE_NAMES.get(i, "Unknown")
name = cl['Name'] if i == 0 else f'{cl["Name"]}({mode_name})'
annotations_dict[id] = AnnotationClass(id, name, cl['Color'], cl['MaxSizeM'])
logger.remove()
log_format = "[{time:HH:mm:ss} {level}] {message}"
+2 -2
View File
@@ -31,7 +31,7 @@ cdef class Inference:
cdef run_inference(self, RemoteCommand cmd)
cdef _process_video(self, RemoteCommand cmd, AIRecognitionConfig ai_config, str video_name)
cdef _process_images(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list[str] image_paths)
cdef _process_images_inner(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list frame_data)
cdef _process_images_inner(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list frame_data, double ground_sampling_distance)
cdef on_annotation(self, RemoteCommand cmd, Annotation annotation)
cdef split_to_tiles(self, frame, path, tile_size, overlap_percent)
cdef stop(self)
@@ -43,5 +43,5 @@ cdef class Inference:
cdef split_list_extend(self, lst, chunk_size)
cdef bint is_valid_video_annotation(self, Annotation annotation, AIRecognitionConfig ai_config)
cdef bint is_valid_image_annotation(self, Annotation annotation)
cdef bint is_valid_image_annotation(self, Annotation annotation, double ground_sampling_distance, frame_shape)
cdef remove_tiled_duplicates(self, Annotation annotation)
+45 -11
View File
@@ -315,18 +315,24 @@ cdef class Inference:
constants_inf.logerror(<str>f'Failed to read image {path}')
continue
original_media_name = Path(<str> path).stem.replace(" ", "")
ground_sampling_distance = ai_config.sensor_width * ai_config.altitude / (ai_config.focal_length * img_w)
constants_inf.log(<str>f'ground sampling distance: {ground_sampling_distance}')
if img_h <= 1.5 * self.model_height and img_w <= 1.5 * self.model_width:
frame_data.append((frame, original_media_name, f'{original_media_name}_000000'))
else:
res = self.split_to_tiles(frame, path, ai_config.tile_size, ai_config.big_image_tile_overlap_percent)
tile_size = int(constants_inf.METERS_IN_TILE / ground_sampling_distance)
constants_inf.log(<str> f'calc tile size: {tile_size}')
res = self.split_to_tiles(frame, path, tile_size, ai_config.big_image_tile_overlap_percent)
frame_data.extend(res)
if len(frame_data) > self.engine.get_batch_size():
for chunk in self.split_list_extend(frame_data, self.engine.get_batch_size()):
self._process_images_inner(cmd, ai_config, chunk)
self._process_images_inner(cmd, ai_config, chunk, ground_sampling_distance)
self.send_detection_status(cmd.client_id)
for chunk in self.split_list_extend(frame_data, self.engine.get_batch_size()):
self._process_images_inner(cmd, ai_config, chunk)
self._process_images_inner(cmd, ai_config, chunk, ground_sampling_distance)
self.send_detection_status(cmd.client_id)
cdef send_detection_status(self, client_id):
@@ -369,7 +375,7 @@ cdef class Inference:
results.append((tile, original_media_name, name))
return results
cdef _process_images_inner(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list frame_data):
cdef _process_images_inner(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list frame_data, double ground_sampling_distance):
cdef list frames, original_media_names, names
cdef Annotation annotation
cdef int i
@@ -381,7 +387,7 @@ cdef class Inference:
list_detections = self.postprocess(outputs, ai_config)
for i in range(len(list_detections)):
annotation = Annotation(names[i], original_media_names[i], 0, list_detections[i])
if self.is_valid_image_annotation(annotation):
if self.is_valid_image_annotation(annotation, ground_sampling_distance, frames[i].shape):
constants_inf.log(<str> f'Detected {annotation}')
_, image = cv2.imencode('.jpg', frames[i])
annotation.image = image.tobytes()
@@ -391,6 +397,7 @@ cdef class Inference:
self.stop_signal = True
cdef remove_tiled_duplicates(self, Annotation annotation):
# Parse tile info from the annotation name
right = annotation.name.rindex('!')
left = annotation.name.index(constants_inf.SPLIT_SUFFIX) + len(constants_inf.SPLIT_SUFFIX)
tile_size_str, x_str, y_str = annotation.name[left:right].split('_')
@@ -398,19 +405,46 @@ cdef class Inference:
x = int(x_str)
y = int(y_str)
# This will be our new, filtered list of detections
cdef list[Detection] unique_detections = []
existing_abs_detections = self._tile_detections.setdefault(annotation.original_media_name, [])
for det in annotation.detections:
# Calculate the absolute position and size of the detection
x1 = det.x * tile_size
y1 = det.y * tile_size
det_abs = Detection(x + x1, y + y1, det.w * tile_size, det.h * tile_size, det.cls, det.confidence)
detections = self._tile_detections.setdefault(annotation.original_media_name, [])
if det_abs in detections:
annotation.detections.remove(det)
else:
detections.append(det_abs)
cdef bint is_valid_image_annotation(self, Annotation annotation):
# If it's not a duplicate, keep it and update the cache
if det_abs not in existing_abs_detections:
unique_detections.append(det)
existing_abs_detections.append(det_abs)
annotation.detections = unique_detections
cdef bint is_valid_image_annotation(self, Annotation annotation, double ground_sampling_distance, frame_shape):
if constants_inf.SPLIT_SUFFIX in annotation.name:
self.remove_tiled_duplicates(annotation)
img_h, img_w, _ = frame_shape
if annotation.detections:
constants_inf.log(<str> f'Initial ann: {annotation}')
cdef list[Detection] valid_detections = []
for det in annotation.detections:
m_w = det.w * img_w * ground_sampling_distance
m_h = det.h * img_h * ground_sampling_distance
max_size = constants_inf.annotations_dict[det.cls].max_object_size_meters
if m_w <= max_size and m_h <= max_size:
valid_detections.append(det)
constants_inf.log(<str> f'Kept ({m_w} {m_h}) <= {max_size}. class: {constants_inf.annotations_dict[det.cls].name}')
else:
constants_inf.log(<str> f'Removed ({m_w} {m_h}) > {max_size}. class: {constants_inf.annotations_dict[det.cls].name}')
# Replace the old list with the new, filtered one
annotation.detections = valid_detections
if not annotation.detections:
return False
return True