3 Commits

Author SHA1 Message Date
Roman Meshko 23c8ee6bbe Update files 2026-04-23 17:41:14 +00:00
Roman Meshko a78e483a33 Skip GSD and size filtering without altitude 2026-04-23 20:37:25 +03:00
Roman Meshko 5cfcdb5fd5 Skip GSD and size filtering without altitude 2026-04-19 21:50:48 +03:00
21 changed files with 168 additions and 70 deletions
+3 -18
View File
@@ -8,24 +8,9 @@ labels:
steps: steps:
- name: build-push - name: build-push
image: docker image: docker
environment:
REGISTRY_HOST:
from_secret: registry_host
REGISTRY_USER:
from_secret: registry_user
REGISTRY_TOKEN:
from_secret: registry_token
commands: commands:
- echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin - if [ "$CI_COMMIT_BRANCH" = "main" ]; then export TAG=arm; else export TAG=${CI_COMMIT_BRANCH}-arm; fi
- export TAG=${CI_COMMIT_BRANCH}-arm - docker build -f Dockerfile.jetson -t localhost:5000/detections:$TAG .
- export BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) - docker push localhost:5000/detections:$TAG
- |
docker build -f Dockerfile.jetson \
--build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \
--label org.opencontainers.image.revision=$CI_COMMIT_SHA \
--label org.opencontainers.image.created=$BUILD_DATE \
--label org.opencontainers.image.source=$CI_REPO_URL \
-t $REGISTRY_HOST/azaion/detections:$TAG .
- docker push $REGISTRY_HOST/azaion/detections:$TAG
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
-2
View File
@@ -1,6 +1,4 @@
FROM nvcr.io/nvidia/l4t-jetpack:r36.2.0 FROM nvcr.io/nvidia/l4t-jetpack:r36.2.0
ARG CI_COMMIT_SHA=unknown
ENV AZAION_REVISION=$CI_COMMIT_SHA
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
python3 python3-pip python3-dev gcc \ python3 python3-pip python3-dev gcc \
+2 -2
View File
@@ -5,7 +5,7 @@
- Detections with confidence below `probability_threshold` (default: 0.25) are filtered out. - Detections with confidence below `probability_threshold` (default: 0.25) are filtered out.
- Overlapping detections with containment ratio > `tracking_intersection_threshold` (default: 0.6) are deduplicated, keeping the higher-confidence detection. - Overlapping detections with containment ratio > `tracking_intersection_threshold` (default: 0.6) are deduplicated, keeping the higher-confidence detection.
- Tile duplicate detections are identified when all bounding box coordinates differ by less than 0.01 (TILE_DUPLICATE_CONFIDENCE_THRESHOLD). - Tile duplicate detections are identified when all bounding box coordinates differ by less than 0.01 (TILE_DUPLICATE_CONFIDENCE_THRESHOLD).
- Physical size filtering: detections exceeding `max_object_size_meters` for their class (defined in classes.json, range 220 meters) are removed. - Physical size filtering: detections exceeding `max_object_size_meters` for their class (defined in classes.json, range 220 meters) are removed when ground sampling distance can be computed from camera settings.
## Video Processing ## Video Processing
@@ -17,7 +17,7 @@
- Images ≤ 1.5× model dimensions (1280×1280): processed as single frame. - Images ≤ 1.5× model dimensions (1280×1280): processed as single frame.
- Larger images: tiled based on ground sampling distance. Tile physical size: 25 meters (METERS_IN_TILE). Tile overlap: `big_image_tile_overlap_percent` (default: 20%). - Larger images: tiled based on ground sampling distance. Tile physical size: 25 meters (METERS_IN_TILE). Tile overlap: `big_image_tile_overlap_percent` (default: 20%).
- GSD calculation: `sensor_width * altitude / (focal_length * image_width)`. - GSD calculation: `sensor_width * altitude / (focal_length * image_width)` when `altitude` is provided.
## API ## API
@@ -36,7 +36,7 @@ Media path is resolved from the Annotations service via `GET /api/media/{media_i
| tracking_intersection_threshold | float | 0.6 | Overlap ratio for NMS deduplication | | tracking_intersection_threshold | float | 0.6 | Overlap ratio for NMS deduplication |
| model_batch_size | int | 8 | Inference batch size | | model_batch_size | int | 8 | Inference batch size |
| big_image_tile_overlap_percent | int | 20 | Tile overlap for large images (0-100%) | | big_image_tile_overlap_percent | int | 20 | Tile overlap for large images (0-100%) |
| altitude | float | 400 | Camera altitude in meters | | altitude | float | optional | Camera altitude in meters. When omitted, GSD-based size filtering and image tiling are skipped. |
| focal_length | float | 24 | Camera focal length in mm | | focal_length | float | 24 | Camera focal length in mm |
| sensor_width | float | 23.5 | Camera sensor width in mm | | sensor_width | float | 23.5 | Camera sensor width in mm |
@@ -1 +1,3 @@
center_x,center_y,width,height,label,confidence_min center_x,center_y,width,height,label,confidence_min
0.104737,0.375955,0.154999,0.144996,Truck,0.73
0.576021,0.728599,0.1495,0.142923,Truck,0.83
1 center_x center_y width height label confidence_min
2 0.104737 0.375955 0.154999 0.144996 Truck 0.73
3 0.576021 0.728599 0.1495 0.142923 Truck 0.83
@@ -1 +1,4 @@
center_x,center_y,width,height,label,confidence_min center_x,center_y,width,height,label,confidence_min
0.214701,0.770089,0.274945,0.28434,ArmorVehicle,0.73
0.588816,0.581807,0.269273,0.209632,ArmorVehicle,0.49
0.314031,0.56274,0.039824,0.060367,MilitaryMan,0.32
1 center_x center_y width height label confidence_min
2 0.214701 0.770089 0.274945 0.28434 ArmorVehicle 0.73
3 0.588816 0.581807 0.269273 0.209632 ArmorVehicle 0.49
4 0.314031 0.56274 0.039824 0.060367 MilitaryMan 0.32
@@ -1 +1,2 @@
center_x,center_y,width,height,label,confidence_min center_x,center_y,width,height,label,confidence_min
0.465599,0.20807,0.137469,0.194655,ArmorVehicle,0.77
1 center_x center_y width height label confidence_min
2 0.465599 0.20807 0.137469 0.194655 ArmorVehicle 0.77
@@ -37,21 +37,18 @@ For videos, the additional field:
### Images ### Images
| # | Input File | Description | Expected Result File | Expected Detection Count | Notes | | # | Input File | Description | Expected Result File | Expected Detection Count | Notes |
|---|------------|-------------|---------------------|-------------------------|-------| |---|------------|-------------|---------------------|---|-------|
| 1 | `image_small.jpg` | 1280×720 aerial, contains detectable objects | `image_small_expected.csv` | ? | Primary test image for single-frame detection | | 1 | `image_small.jpg`| 1280×720 aerial, contains detectable objects| `image_small_expected.csv`| 1 | Primary test image for single-frame detection|
| 2 | `image_large.JPG` | 6252×4168 aerial, triggers GSD-based tiling | `image_large_expected.csv` | ? | Coordinates normalized to full image (not tile) | | 2 | `image_large.JPG`| 6252×4168 aerial, triggers GSD-based tiling| `image_large_expected.csv`| 3 | Coordinates normalized to full image (not tile)|
| 3 | `image_dense01.jpg` | 1280×720 dense scene, many clustered objects | `image_dense01_expected.csv` | ? | Used for dedup and max-detection-cap tests | | 3 | `image_different_types.jpg`| 900×1600, varied object classes| `image_different_types_expected.csv`| 2 | Must contain multiple distinct class labels|
| 4 | `image_dense02.jpg` | 1920×1080 dense scene variant | `image_dense02_expected.csv` | ? | Borderline tiling, dedup variant | | 4 | `image_empty_scene.jpg`| 1920×1080, no detectable objects| `image_empty_scene_expected.csv`| 0 | CSV has headers only — zero detections expected|
| 5 | `image_different_types.jpg` | 900×1600, varied object classes | `image_different_types_expected.csv` | ? | Must contain multiple distinct class labels |
| 6 | `image_empty_scene.jpg` | 1920×1080, no detectable objects | `image_empty_scene_expected.csv` | 0 | CSV has headers only — zero detections expected |
### Videos ### Videos
| # | Input File | Description | Expected Result File | Notes | | # | Input File | Description | Expected Result File | Notes |
|---|------------|-------------|---------------------|-------| |---|------------|-------------|---------------------|-------|
| 7 | `video_test01.mp4` | Standard test video | `video_test01_expected.csv` | Primary async/SSE/video test. List key-frame detections. | | 5 | `video_test01.mp4` | Standard test video | `video_test01_expected.csv` | Primary async/SSE/video test. List key-frame detections. |
| 8 | `video_1.mp4` | Video variant | `video_1_expected.csv` | Secondary local fixture for resilience and concurrent-style validation. | | 6 | `video_1_faststart.mp4` | Faststart video variant | `video_1_faststart_expected.csv` | Streaming compatibility variant. Separate long-video overflow fixture is not currently present in local fixtures. |
| 9 | `video_1_faststart.mp4` | Faststart video variant | `video_1_faststart_expected.csv` | Streaming compatibility variant. Separate long-video overflow fixture is not currently present in local fixtures. |
## How to Fill ## How to Fill
@@ -1 +1,81 @@
time_sec,center_x,center_y,width,height,label,confidence_min time_sec,center_x,center_y,width,height,label,confidence_min
0,0.289802,0.512695,0.077206,0.072666,ArmorVehicle,0.89
10,0.753483,0.198423,0.057173,0.070441,ArmorVehicle,0.72
108,0.621403,0.45839,0.026638,0.037245,Smoke,0.58
110,0.637573,0.465766,0.027141,0.040499,Smoke,0.58
112,0.610838,0.480663,0.0283,0.041891,Smoke,0.72
114,0.60922,0.486313,0.026929,0.042859,Smoke,0.68
116,0.615395,0.492105,0.028959,0.041361,Smoke,0.67
118,0.612622,0.49208,0.027159,0.039772,Smoke,0.61
120,0.488272,0.653743,0.028969,0.045788,Smoke,0.67
122,0.276008,0.692814,0.061306,0.10955,Smoke,0.74
127,0.5029,0.24006,0.116369,0.208533,Smoke,0.74
130,0.350402,0.278989,0.122529,0.179202,Smoke,0.7
132,0.525031,0.469289,0.103297,0.193989,Smoke,0.48
134,0.475175,0.52287,0.099367,0.178105,Smoke,0.27
136,0.042595,0.458038,0.073769,0.201478,Smoke,0.72
138,0.398975,0.280201,0.132485,0.076752,Smoke,0.3
14,0.956159,0.68494,0.075992,0.034521,Trenches,0.62
149,0.115298,0.585385,0.037456,0.030006,Trenches,0.64
149,0.149621,0.602923,0.039252,0.032263,Trenches,0.73
149,0.534584,0.082995,0.03861,0.036615,Trenches,0.51
151,0.363107,0.353294,0.036212,0.030509,Trenches,0.52
151,0.395121,0.377565,0.03571,0.034257,Trenches,0.52
153,0.540678,0.549795,0.04493,0.040794,Trenches,0.55
155,0.124097,0.466852,0.074959,0.051802,Trenches,0.32
163,0.687639,0.261656,0.116238,0.125195,TyreTracks,0.43
168,0.378062,0.769232,0.032526,0.027289,Trenches,0.46
174,0.435846,0.904734,0.03305,0.03245,Trenches,0.27
176,0.942677,0.589411,0.024974,0.052446,Trenches,0.33
181,0.612998,0.232146,0.08565,0.060314,Smoke,0.52
191,0.390291,0.036179,0.054409,0.033613,Trenches,0.39
193,0.413883,0.208317,0.053158,0.031181,Trenches,0.76
193,0.430828,0.171384,0.030674,0.029638,Trenches,0.27
193,0.484856,0.182248,0.04184,0.063353,Trenches,0.65
193,0.491091,0.316695,0.031371,0.024474,Trenches,0.37
195,0.556304,0.101112,0.046918,0.036026,Smoke,0.71
197,0.519331,0.120967,0.044832,0.039692,Smoke,0.67
199,0.176271,0.118214,0.047294,0.053538,Smoke,0.63
2,0.653329,0.376178,0.078665,0.081241,ArmorVehicle,0.89
202,0.171963,0.138687,0.043301,0.04702,Smoke,0.46
214,0.413986,0.12009,0.050383,0.046863,Smoke,0.5
219,0.364435,0.552301,0.028486,0.0314,ArmorVehicle,0.72
240,0.502753,0.535129,0.036657,0.033246,Trenches,0.32
242,0.019098,0.607661,0.038072,0.024671,Trenches,0.27
242,0.02406,0.680742,0.044204,0.023859,Trenches,0.37
242,0.027672,0.720891,0.033973,0.04163,Trenches,0.65
242,0.041207,0.628713,0.021846,0.044712,Trenches,0.29
242,0.053195,0.682519,0.027647,0.038862,Trenches,0.65
242,0.84993,0.631653,0.046269,0.042584,Trenches,0.47
244,0.017405,0.7017,0.034866,0.023287,Trenches,0.35
244,0.018653,0.733875,0.03717,0.025044,Trenches,0.48
244,0.032765,0.621231,0.064777,0.023041,Trenches,0.45
244,0.05303,0.734179,0.034922,0.041688,Trenches,0.7
244,0.070526,0.649712,0.023196,0.061101,Trenches,0.44
244,0.076041,0.69735,0.028326,0.039375,Trenches,0.65
246,0.025853,0.893559,0.051857,0.02183,Trenches,0.33
246,0.055409,0.908165,0.020511,0.040618,Trenches,0.32
252,0.735668,0.309579,0.026179,0.044193,Trenches,0.61
252,0.760952,0.371567,0.026674,0.062061,Trenches,0.52
282,0.566367,0.010329,0.036472,0.020469,Smoke,0.49
284,0.341962,0.011659,0.03561,0.023243,Smoke,0.59
287,0.423856,0.023553,0.03954,0.044283,Smoke,0.42
290,0.397077,0.010032,0.037649,0.019987,Smoke,0.29
292,0.415613,0.011793,0.036875,0.023537,Smoke,0.37
294,0.385953,0.009416,0.037843,0.018655,Smoke,0.39
30,0.343423,0.763509,0.098184,0.079214,TyreTracks,0.82
32,0.438461,0.687812,0.09794,0.07654,TyreTracks,0.85
4,0.197519,0.416036,0.067097,0.083232,ArmorVehicle,0.83
41,0.700711,0.590761,0.10644,0.088566,TyreTracks,0.74
47,0.830646,0.762364,0.108269,0.091804,TyreTracks,0.37
47,0.698529,0.00825,0.022697,0.01672,Vehicle,0.34
49,0.349817,0.663337,0.075551,0.061795,TyreTracks,0.77
6,0.276864,0.60931,0.060284,0.066828,ArmorVehicle,0.8
67,0.846456,0.527966,0.03052,0.058466,Trenches,0.32
69,0.68478,0.514788,0.025761,0.053171,Trenches,0.46
71,0.368416,0.460942,0.030577,0.050027,Trenches,0.78
73,0.177122,0.455578,0.031931,0.048597,Trenches,0.36
8,0.379446,0.434878,0.071124,0.077504,ArmorVehicle,0.55
87,0.128406,0.138269,0.040148,0.032909,Smoke,0.56
90,0.118965,0.089119,0.037023,0.033037,Smoke,0.32
95,0.570426,0.373717,0.031503,0.021361,Trenches,0.48
1 time_sec center_x center_y width height label confidence_min
2 0 0.289802 0.512695 0.077206 0.072666 ArmorVehicle 0.89
3 10 0.753483 0.198423 0.057173 0.070441 ArmorVehicle 0.72
4 108 0.621403 0.45839 0.026638 0.037245 Smoke 0.58
5 110 0.637573 0.465766 0.027141 0.040499 Smoke 0.58
6 112 0.610838 0.480663 0.0283 0.041891 Smoke 0.72
7 114 0.60922 0.486313 0.026929 0.042859 Smoke 0.68
8 116 0.615395 0.492105 0.028959 0.041361 Smoke 0.67
9 118 0.612622 0.49208 0.027159 0.039772 Smoke 0.61
10 120 0.488272 0.653743 0.028969 0.045788 Smoke 0.67
11 122 0.276008 0.692814 0.061306 0.10955 Smoke 0.74
12 127 0.5029 0.24006 0.116369 0.208533 Smoke 0.74
13 130 0.350402 0.278989 0.122529 0.179202 Smoke 0.7
14 132 0.525031 0.469289 0.103297 0.193989 Smoke 0.48
15 134 0.475175 0.52287 0.099367 0.178105 Smoke 0.27
16 136 0.042595 0.458038 0.073769 0.201478 Smoke 0.72
17 138 0.398975 0.280201 0.132485 0.076752 Smoke 0.3
18 14 0.956159 0.68494 0.075992 0.034521 Trenches 0.62
19 149 0.115298 0.585385 0.037456 0.030006 Trenches 0.64
20 149 0.149621 0.602923 0.039252 0.032263 Trenches 0.73
21 149 0.534584 0.082995 0.03861 0.036615 Trenches 0.51
22 151 0.363107 0.353294 0.036212 0.030509 Trenches 0.52
23 151 0.395121 0.377565 0.03571 0.034257 Trenches 0.52
24 153 0.540678 0.549795 0.04493 0.040794 Trenches 0.55
25 155 0.124097 0.466852 0.074959 0.051802 Trenches 0.32
26 163 0.687639 0.261656 0.116238 0.125195 TyreTracks 0.43
27 168 0.378062 0.769232 0.032526 0.027289 Trenches 0.46
28 174 0.435846 0.904734 0.03305 0.03245 Trenches 0.27
29 176 0.942677 0.589411 0.024974 0.052446 Trenches 0.33
30 181 0.612998 0.232146 0.08565 0.060314 Smoke 0.52
31 191 0.390291 0.036179 0.054409 0.033613 Trenches 0.39
32 193 0.413883 0.208317 0.053158 0.031181 Trenches 0.76
33 193 0.430828 0.171384 0.030674 0.029638 Trenches 0.27
34 193 0.484856 0.182248 0.04184 0.063353 Trenches 0.65
35 193 0.491091 0.316695 0.031371 0.024474 Trenches 0.37
36 195 0.556304 0.101112 0.046918 0.036026 Smoke 0.71
37 197 0.519331 0.120967 0.044832 0.039692 Smoke 0.67
38 199 0.176271 0.118214 0.047294 0.053538 Smoke 0.63
39 2 0.653329 0.376178 0.078665 0.081241 ArmorVehicle 0.89
40 202 0.171963 0.138687 0.043301 0.04702 Smoke 0.46
41 214 0.413986 0.12009 0.050383 0.046863 Smoke 0.5
42 219 0.364435 0.552301 0.028486 0.0314 ArmorVehicle 0.72
43 240 0.502753 0.535129 0.036657 0.033246 Trenches 0.32
44 242 0.019098 0.607661 0.038072 0.024671 Trenches 0.27
45 242 0.02406 0.680742 0.044204 0.023859 Trenches 0.37
46 242 0.027672 0.720891 0.033973 0.04163 Trenches 0.65
47 242 0.041207 0.628713 0.021846 0.044712 Trenches 0.29
48 242 0.053195 0.682519 0.027647 0.038862 Trenches 0.65
49 242 0.84993 0.631653 0.046269 0.042584 Trenches 0.47
50 244 0.017405 0.7017 0.034866 0.023287 Trenches 0.35
51 244 0.018653 0.733875 0.03717 0.025044 Trenches 0.48
52 244 0.032765 0.621231 0.064777 0.023041 Trenches 0.45
53 244 0.05303 0.734179 0.034922 0.041688 Trenches 0.7
54 244 0.070526 0.649712 0.023196 0.061101 Trenches 0.44
55 244 0.076041 0.69735 0.028326 0.039375 Trenches 0.65
56 246 0.025853 0.893559 0.051857 0.02183 Trenches 0.33
57 246 0.055409 0.908165 0.020511 0.040618 Trenches 0.32
58 252 0.735668 0.309579 0.026179 0.044193 Trenches 0.61
59 252 0.760952 0.371567 0.026674 0.062061 Trenches 0.52
60 282 0.566367 0.010329 0.036472 0.020469 Smoke 0.49
61 284 0.341962 0.011659 0.03561 0.023243 Smoke 0.59
62 287 0.423856 0.023553 0.03954 0.044283 Smoke 0.42
63 290 0.397077 0.010032 0.037649 0.019987 Smoke 0.29
64 292 0.415613 0.011793 0.036875 0.023537 Smoke 0.37
65 294 0.385953 0.009416 0.037843 0.018655 Smoke 0.39
66 30 0.343423 0.763509 0.098184 0.079214 TyreTracks 0.82
67 32 0.438461 0.687812 0.09794 0.07654 TyreTracks 0.85
68 4 0.197519 0.416036 0.067097 0.083232 ArmorVehicle 0.83
69 41 0.700711 0.590761 0.10644 0.088566 TyreTracks 0.74
70 47 0.830646 0.762364 0.108269 0.091804 TyreTracks 0.37
71 47 0.698529 0.00825 0.022697 0.01672 Vehicle 0.34
72 49 0.349817 0.663337 0.075551 0.061795 TyreTracks 0.77
73 6 0.276864 0.60931 0.060284 0.066828 ArmorVehicle 0.8
74 67 0.846456 0.527966 0.03052 0.058466 Trenches 0.32
75 69 0.68478 0.514788 0.025761 0.053171 Trenches 0.46
76 71 0.368416 0.460942 0.030577 0.050027 Trenches 0.78
77 73 0.177122 0.455578 0.031931 0.048597 Trenches 0.36
78 8 0.379446 0.434878 0.071124 0.077504 ArmorVehicle 0.55
79 87 0.128406 0.138269 0.040148 0.032909 Smoke 0.56
80 90 0.118965 0.089119 0.037023 0.033037 Smoke 0.32
81 95 0.570426 0.373717 0.031503 0.021361 Trenches 0.48
@@ -1 +1,5 @@
time_sec,center_x,center_y,width,height,label,confidence_min time_sec,center_x,center_y,width,height,label,confidence_min
0,0.289857,0.512138,0.07729,0.072891,ArmorVehicle,0.89
2,0.653617,0.376064,0.078636,0.08118,ArmorVehicle,0.89
4,0.197561,0.416208,0.068112,0.084079,ArmorVehicle,0.85
6,0.27662,0.609538,0.059521,0.067212,ArmorVehicle,0.8
1 time_sec center_x center_y width height label confidence_min
2 0 0.289857 0.512138 0.07729 0.072891 ArmorVehicle 0.89
3 2 0.653617 0.376064 0.078636 0.08118 ArmorVehicle 0.89
4 4 0.197561 0.416208 0.068112 0.084079 ArmorVehicle 0.85
5 6 0.27662 0.609538 0.059521 0.067212 ArmorVehicle 0.8
+2 -2
View File
@@ -20,7 +20,7 @@ Data class holding all AI recognition configuration parameters, with factory met
| `tracking_intersection_threshold` | double | 0.6 | IoU threshold for overlapping detection removal | | `tracking_intersection_threshold` | double | 0.6 | IoU threshold for overlapping detection removal |
| `model_batch_size` | int | 1 | Batch size for inference | | `model_batch_size` | int | 1 | Batch size for inference |
| `big_image_tile_overlap_percent` | int | 20 | Tile overlap percentage for large image splitting | | `big_image_tile_overlap_percent` | int | 20 | Tile overlap percentage for large image splitting |
| `altitude` | double | 400 | Camera altitude in meters | | `altitude` | double? | optional | Camera altitude in meters. When missing, GSD-based filtering is disabled |
| `focal_length` | double | 24 | Camera focal length in mm | | `focal_length` | double | 24 | Camera focal length in mm |
| `sensor_width` | double | 23.5 | Camera sensor width in mm | | `sensor_width` | double | 23.5 | Camera sensor width in mm |
@@ -51,7 +51,7 @@ Data class holding all AI recognition configuration parameters, with factory met
## Configuration ## Configuration
Camera/altitude parameters (`altitude`, `focal_length`, `sensor_width`) are used for ground sampling distance calculation in aerial image processing. Camera/altitude parameters (`altitude`, `focal_length`, `sensor_width`) are used for ground sampling distance calculation in aerial image processing. If `altitude` is missing, the service skips GSD-based size filtering and does not tile large images by physical size.
## External Integrations ## External Integrations
-6
View File
@@ -8,12 +8,9 @@
| classes-json | `classes.json` (repo root) | 19 detection classes with Id, Name, Color, MaxSizeM | All tests | Volume mount to detections `/app/classes.json` | Container restart | | classes-json | `classes.json` (repo root) | 19 detection classes with Id, Name, Color, MaxSizeM | All tests | Volume mount to detections `/app/classes.json` | Container restart |
| image-small | `input_data/image_small.jpg` | JPEG 1280×720 — below tiling threshold (1920×1920) | FT-P-01..03, 05, 07, 13..15, FT-N-03, 06, NFT-PERF-01..02, NFT-RES-01, 03, NFT-SEC-01, NFT-RES-LIM-01 | Volume mount to consumer `/media/` | N/A (read-only) | | image-small | `input_data/image_small.jpg` | JPEG 1280×720 — below tiling threshold (1920×1920) | FT-P-01..03, 05, 07, 13..15, FT-N-03, 06, NFT-PERF-01..02, NFT-RES-01, 03, NFT-SEC-01, NFT-RES-LIM-01 | Volume mount to consumer `/media/` | N/A (read-only) |
| image-large | `input_data/image_large.JPG` | JPEG 6252×4168 — above tiling threshold, triggers GSD tiling | FT-P-04, 16, NFT-PERF-03 | Volume mount to consumer `/media/` | N/A (read-only) | | image-large | `input_data/image_large.JPG` | JPEG 6252×4168 — above tiling threshold, triggers GSD tiling | FT-P-04, 16, NFT-PERF-03 | Volume mount to consumer `/media/` | N/A (read-only) |
| image-dense-01 | `input_data/image_dense01.jpg` | JPEG 1280×720 — dense scene with many clustered objects | FT-P-06, NFT-RES-LIM-03 | Volume mount to consumer `/media/` | N/A (read-only) |
| image-dense-02 | `input_data/image_dense02.jpg` | JPEG 1920×1080 — dense scene variant, borderline tiling | FT-P-06 (variant) | Volume mount to consumer `/media/` | N/A (read-only) |
| image-different-types | `input_data/image_different_types.jpg` | JPEG 900×1600 — varied object classes for class variant tests | FT-P-13 | Volume mount to consumer `/media/` | N/A (read-only) | | image-different-types | `input_data/image_different_types.jpg` | JPEG 900×1600 — varied object classes for class variant tests | FT-P-13 | Volume mount to consumer `/media/` | N/A (read-only) |
| image-empty-scene | `input_data/image_empty_scene.jpg` | JPEG 1920×1080 — clean scene with no detectable objects | Edge case (zero detections) | Volume mount to consumer `/media/` | N/A (read-only) | | image-empty-scene | `input_data/image_empty_scene.jpg` | JPEG 1920×1080 — clean scene with no detectable objects | Edge case (zero detections) | Volume mount to consumer `/media/` | N/A (read-only) |
| video-test-01 | `input_data/video_test01.mp4` | MP4 video — standard async/SSE/video detection tests | FT-P-08..12, FT-N-04, 07, NFT-PERF-04, NFT-RES-02, NFT-SEC-03 | Volume mount to consumer `/media/` | N/A (read-only) | | video-test-01 | `input_data/video_test01.mp4` | MP4 video — standard async/SSE/video detection tests | FT-P-08..12, FT-N-04, 07, NFT-PERF-04, NFT-RES-02, NFT-SEC-03 | Volume mount to consumer `/media/` | N/A (read-only) |
| video-1 | `input_data/video_1.mp4` | MP4 video — local variant for concurrent and resilience-style tests | NFT-RES-02 (variant), NFT-RES-04 | Volume mount to consumer `/media/` | N/A (read-only) |
| video-1-faststart | `input_data/video_1_faststart.mp4` | MP4 video — faststart/local streaming variant | Streaming compatibility checks | Volume mount to consumer `/media/` | N/A (read-only) | | video-1-faststart | `input_data/video_1_faststart.mp4` | MP4 video — faststart/local streaming variant | Streaming compatibility checks | Volume mount to consumer `/media/` | N/A (read-only) |
| empty-image | Generated at build time | Zero-byte file | FT-N-01 | Generated in e2e/fixtures/ | N/A | | empty-image | Generated at build time | Zero-byte file | FT-N-01 | Generated in e2e/fixtures/ | N/A |
| corrupt-image | Generated at build time | Random binary garbage (not valid image format) | FT-N-02 | Generated in e2e/fixtures/ | N/A | | corrupt-image | Generated at build time | Random binary garbage (not valid image format) | FT-N-02 | Generated in e2e/fixtures/ | N/A |
@@ -31,12 +28,9 @@ Each test run starts with fresh containers (`docker compose down -v && docker co
| azaion.onnx | `_docs/00_problem/input_data/azaion.onnx` | YOLO ONNX detection model | All detection tests | | azaion.onnx | `_docs/00_problem/input_data/azaion.onnx` | YOLO ONNX detection model | All detection tests |
| image_small.jpg | `_docs/00_problem/input_data/image_small.jpg` | 1280×720 aerial image | Single-frame detection, health, negative, perf tests | | image_small.jpg | `_docs/00_problem/input_data/image_small.jpg` | 1280×720 aerial image | Single-frame detection, health, negative, perf tests |
| image_large.JPG | `_docs/00_problem/input_data/image_large.JPG` | 6252×4168 aerial image | Tiling tests | | image_large.JPG | `_docs/00_problem/input_data/image_large.JPG` | 6252×4168 aerial image | Tiling tests |
| image_dense01.jpg | `_docs/00_problem/input_data/image_dense01.jpg` | Dense scene 1280×720 | Dedup, detection cap tests |
| image_dense02.jpg | `_docs/00_problem/input_data/image_dense02.jpg` | Dense scene 1920×1080 | Dedup variant |
| image_different_types.jpg | `_docs/00_problem/input_data/image_different_types.jpg` | Varied classes 900×1600 | Class variant tests | | image_different_types.jpg | `_docs/00_problem/input_data/image_different_types.jpg` | Varied classes 900×1600 | Class variant tests |
| image_empty_scene.jpg | `_docs/00_problem/input_data/image_empty_scene.jpg` | Empty scene 1920×1080 | Zero-detection edge case | | image_empty_scene.jpg | `_docs/00_problem/input_data/image_empty_scene.jpg` | Empty scene 1920×1080 | Zero-detection edge case |
| video_test01.mp4 | `_docs/00_problem/input_data/video_test01.mp4` | Standard video | Async, SSE, video, perf tests | | video_test01.mp4 | `_docs/00_problem/input_data/video_test01.mp4` | Standard video | Async, SSE, video, perf tests |
| video_1.mp4 | `_docs/00_problem/input_data/video_1.mp4` | Video variant | Resilience, concurrent tests |
| video_1_faststart.mp4 | `_docs/00_problem/input_data/video_1_faststart.mp4` | Faststart video variant | Streaming compatibility checks | | video_1_faststart.mp4 | `_docs/00_problem/input_data/video_1_faststart.mp4` | Faststart video variant | Streaming compatibility checks |
| classes.json | repo root `classes.json` | 19 detection classes | All tests | | classes.json | repo root `classes.json` | 19 detection classes | All tests |
@@ -27,8 +27,6 @@ e2e/
├── fixtures/ ├── fixtures/
│ ├── image_small.jpg (1280×720 JPEG, aerial, detectable objects) │ ├── image_small.jpg (1280×720 JPEG, aerial, detectable objects)
│ ├── image_large.JPG (6252×4168 JPEG, triggers tiling) │ ├── image_large.JPG (6252×4168 JPEG, triggers tiling)
│ ├── image_dense01.jpg (1280×720 JPEG, dense scene, clustered objects)
│ ├── image_dense02.jpg (1920×1080 JPEG, dense scene variant)
│ ├── image_different_types.jpg (900×1600 JPEG, varied object classes) │ ├── image_different_types.jpg (900×1600 JPEG, varied object classes)
│ ├── image_empty_scene.jpg (1920×1080 JPEG, no detectable objects) │ ├── image_empty_scene.jpg (1920×1080 JPEG, no detectable objects)
│ ├── video_short01.mp4 (short MP4 with moving objects) │ ├── video_short01.mp4 (short MP4 with moving objects)
@@ -130,8 +128,6 @@ Two Docker Compose profiles:
| `reset_mocks` | function (autouse) | Calls `POST /mock/reset` on both mocks before each test | | `reset_mocks` | function (autouse) | Calls `POST /mock/reset` on both mocks before each test |
| `image_small` | session | Reads `image_small.jpg` from `/media/` volume | | `image_small` | session | Reads `image_small.jpg` from `/media/` volume |
| `image_large` | session | Reads `image_large.JPG` from `/media/` volume | | `image_large` | session | Reads `image_large.JPG` from `/media/` volume |
| `image_dense` | session | Reads `image_dense01.jpg` from `/media/` volume |
| `image_dense_02` | session | Reads `image_dense02.jpg` from `/media/` volume |
| `image_different_types` | session | Reads `image_different_types.jpg` from `/media/` volume | | `image_different_types` | session | Reads `image_different_types.jpg` from `/media/` volume |
| `image_empty_scene` | session | Reads `image_empty_scene.jpg` from `/media/` volume | | `image_empty_scene` | session | Reads `image_empty_scene.jpg` from `/media/` volume |
| `video_short_path` | session | Path to `video_short01.mp4` on `/media/` volume | | `video_short_path` | session | Path to `video_short01.mp4` on `/media/` volume |
@@ -150,8 +146,6 @@ Two Docker Compose profiles:
| classes.json | repo root `classes.json` | JSON (19 objects with Id, Name, Color, MaxSizeM) | All tests (volume mount to detections) | | classes.json | repo root `classes.json` | JSON (19 objects with Id, Name, Color, MaxSizeM) | All tests (volume mount to detections) |
| image_small.jpg | `input_data/image_small.jpg` | JPEG 1280×720 | Health, single image, filtering, negative, performance tests | | image_small.jpg | `input_data/image_small.jpg` | JPEG 1280×720 | Health, single image, filtering, negative, performance tests |
| image_large.JPG | `input_data/image_large.JPG` | JPEG 6252×4168 | Tiling tests, performance tests | | image_large.JPG | `input_data/image_large.JPG` | JPEG 6252×4168 | Tiling tests, performance tests |
| image_dense01.jpg | `input_data/image_dense01.jpg` | JPEG 1280×720 dense scene | Dedup tests, detection cap tests |
| image_dense02.jpg | `input_data/image_dense02.jpg` | JPEG 1920×1080 dense scene | Dedup variant |
| image_different_types.jpg | `input_data/image_different_types.jpg` | JPEG 900×1600 varied classes | Weather mode class variant tests | | image_different_types.jpg | `input_data/image_different_types.jpg` | JPEG 900×1600 varied classes | Weather mode class variant tests |
| image_empty_scene.jpg | `input_data/image_empty_scene.jpg` | JPEG 1920×1080 empty | Zero-detection edge case | | image_empty_scene.jpg | `input_data/image_empty_scene.jpg` | JPEG 1920×1080 empty | Zero-detection edge case |
| video_short01.mp4 | `input_data/video_short01.mp4` | MP4 short video | Async, SSE, video processing tests | | video_short01.mp4 | `input_data/video_short01.mp4` | MP4 short video | Async, SSE, video processing tests |
-11
View File
@@ -228,17 +228,6 @@ def image_small():
def image_large(): def image_large():
return _read_media("image_large.JPG") return _read_media("image_large.JPG")
@pytest.fixture(scope="session")
def image_dense():
return _read_media("image_dense01.jpg")
@pytest.fixture(scope="session")
def image_dense_02():
return _read_media("image_dense02.jpg")
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def image_different_types(): def image_different_types():
return _read_media("image_different_types.jpg") return _read_media("image_different_types.jpg")
+1
View File
@@ -12,6 +12,7 @@ cdef class AIRecognitionConfig:
cdef public int model_batch_size cdef public int model_batch_size
cdef public bint has_altitude
cdef public double altitude cdef public double altitude
cdef public double focal_length cdef public double focal_length
cdef public double sensor_width cdef public double sensor_width
+4 -3
View File
@@ -25,7 +25,8 @@ cdef class AIRecognitionConfig:
self.big_image_tile_overlap_percent = big_image_tile_overlap_percent self.big_image_tile_overlap_percent = big_image_tile_overlap_percent
self.altitude = altitude self.has_altitude = altitude is not None
self.altitude = 0.0 if altitude is None else float(altitude)
self.focal_length = focal_length self.focal_length = focal_length
self.sensor_width = sensor_width self.sensor_width = sensor_width
@@ -36,7 +37,7 @@ cdef class AIRecognitionConfig:
f'frame_period_recognition : {self.frame_period_recognition}, ' f'frame_period_recognition : {self.frame_period_recognition}, '
f'big_image_tile_overlap_percent: {self.big_image_tile_overlap_percent}, ' f'big_image_tile_overlap_percent: {self.big_image_tile_overlap_percent}, '
f'model_batch_size: {self.model_batch_size}, ' f'model_batch_size: {self.model_batch_size}, '
f'altitude: {self.altitude}, ' f'altitude: {self.altitude if self.has_altitude else None}, '
f'focal_length: {self.focal_length}, ' f'focal_length: {self.focal_length}, '
f'sensor_width: {self.sensor_width}' f'sensor_width: {self.sensor_width}'
) )
@@ -56,7 +57,7 @@ cdef class AIRecognitionConfig:
data.get("big_image_tile_overlap_percent", 20), data.get("big_image_tile_overlap_percent", 20),
data.get("altitude", 400), data.get("altitude", None),
data.get("focal_length", 24), data.get("focal_length", 24),
data.get("sensor_width", 23.5) data.get("sensor_width", 23.5)
) )
+1 -1
View File
@@ -7,6 +7,6 @@ cdef class Detection:
cdef class Annotation: cdef class Annotation:
cdef public str name cdef public str name
cdef public str original_media_name cdef public str original_media_name
cdef long time cdef public long time
cdef public list[Detection] detections cdef public list[Detection] detections
cdef public bytes image cdef public bytes image
+17 -2
View File
@@ -311,13 +311,22 @@ cdef class Inference:
cdef double ground_sampling_distance cdef double ground_sampling_distance
cdef int model_h, model_w cdef int model_h, model_w
cdef int img_h, img_w cdef int img_h, img_w
cdef bint has_gsd
model_h, model_w = self.engine.get_input_shape() model_h, model_w = self.engine.get_input_shape()
img_h, img_w, _ = frame.shape img_h, img_w, _ = frame.shape
ground_sampling_distance = ai_config.sensor_width * ai_config.altitude / (ai_config.focal_length * img_w) has_gsd = ai_config.has_altitude and ai_config.focal_length > 0 and ai_config.sensor_width > 0 and img_w > 0
constants_inf.log(<str>f'ground sampling distance: {ground_sampling_distance}') ground_sampling_distance = 0.0
if has_gsd:
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}')
else:
constants_inf.log(<str>'ground sampling distance: skipped (altitude unavailable)')
if img_h <= 1.5 * model_h and img_w <= 1.5 * model_w: if img_h <= 1.5 * model_h and img_w <= 1.5 * model_w:
all_frame_data.append((frame, original_media_name, f'{original_media_name}_000000', ground_sampling_distance)) all_frame_data.append((frame, original_media_name, f'{original_media_name}_000000', ground_sampling_distance))
else: else:
if not has_gsd:
all_frame_data.append((frame, original_media_name, f'{original_media_name}_000000', ground_sampling_distance))
return
tile_size = int(constants_inf.METERS_IN_TILE / ground_sampling_distance) tile_size = int(constants_inf.METERS_IN_TILE / ground_sampling_distance)
constants_inf.log(<str> f'calc tile size: {tile_size}') constants_inf.log(<str> f'calc tile size: {tile_size}')
res = self.split_to_tiles(frame, original_media_name, tile_size, ai_config.big_image_tile_overlap_percent) res = self.split_to_tiles(frame, original_media_name, tile_size, ai_config.big_image_tile_overlap_percent)
@@ -410,6 +419,12 @@ cdef class Inference:
if annotation.detections: if annotation.detections:
constants_inf.log(<str> f'Initial ann: {annotation}') constants_inf.log(<str> f'Initial ann: {annotation}')
if ground_sampling_distance <= 0:
if not annotation.detections:
return <bint>False
constants_inf.log(<str>'Skipping physical-size filtering (ground sampling distance unavailable)')
return <bint>True
cdef list[Detection] valid_detections = [] cdef list[Detection] valid_detections = []
for det in annotation.detections: for det in annotation.detections:
m_w = det.w * img_w * ground_sampling_distance m_w = det.w * img_w * ground_sampling_distance
+14 -6
View File
@@ -15,6 +15,7 @@ import cv2
import jwt as pyjwt import jwt as pyjwt
import numpy as np import numpy as np
import requests as http_requests import requests as http_requests
from loguru import logger
from fastapi import Body, Depends, FastAPI, File, Form, HTTPException, Request, UploadFile from fastapi import Body, Depends, FastAPI, File, Form, HTTPException, Request, UploadFile
from fastapi.responses import Response, StreamingResponse from fastapi.responses import Response, StreamingResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -163,7 +164,7 @@ class AIConfigDto(BaseModel):
tracking_intersection_threshold: float = 0.6 tracking_intersection_threshold: float = 0.6
model_batch_size: int = 8 model_batch_size: int = 8
big_image_tile_overlap_percent: int = 20 big_image_tile_overlap_percent: int = 20
altitude: float = 400 altitude: Optional[float] = None
focal_length: float = 24 focal_length: float = 24
sensor_width: float = 23.5 sensor_width: float = 23.5
@@ -270,7 +271,8 @@ def _post_media_record(payload: dict, bearer: str) -> bool:
timeout=30, timeout=30,
) )
return r.status_code in (200, 201) return r.status_code in (200, 201)
except Exception: except Exception as exc:
logger.warning(f"Failed to create media record in annotations service: {exc}")
return False return False
@@ -284,7 +286,8 @@ def _put_media_status(media_id: str, media_status: int, bearer: str) -> bool:
timeout=30, timeout=30,
) )
return r.status_code in (200, 204) return r.status_code in (200, 204)
except Exception: except Exception as exc:
logger.warning(f"Failed to update media status in annotations service for {media_id}: {exc}")
return False return False
@@ -332,10 +335,13 @@ def _post_annotation_to_service(token_mgr: TokenManager, media_id: str,
try: try:
token = token_mgr.get_valid_token() token = token_mgr.get_valid_token()
image_b64 = base64.b64encode(annotation.image).decode() if annotation.image else None image_b64 = base64.b64encode(annotation.image).decode() if annotation.image else None
total_seconds = int(annotation.time // 1000) if annotation.time else 0
hours, remainder = divmod(total_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
payload = { payload = {
"mediaId": media_id, "mediaId": media_id,
"source": 0, "source": 0,
"videoTime": f"00:00:{annotation.time // 1000:02d}" if annotation.time else "00:00:00", "videoTime": f"{hours:02d}:{minutes:02d}:{seconds:02d}",
"detections": [d.model_dump() for d in dtos], "detections": [d.model_dump() for d in dtos],
} }
if image_b64: if image_b64:
@@ -346,8 +352,10 @@ def _post_annotation_to_service(token_mgr: TokenManager, media_id: str,
headers={"Authorization": f"Bearer {token}"}, headers={"Authorization": f"Bearer {token}"},
timeout=30, timeout=30,
) )
except Exception: except Exception as exc:
pass logger.warning(
f"Failed to post annotation to annotations service for media {media_id}: {exc}"
)
def _cleanup_channel(channel_id: str): def _cleanup_channel(channel_id: str):
+9
View File
@@ -5,6 +5,15 @@ def test_ai_config_from_dict_defaults():
assert cfg.model_batch_size == 8 assert cfg.model_batch_size == 8
assert cfg.frame_period_recognition == 4 assert cfg.frame_period_recognition == 4
assert cfg.frame_recognition_seconds == 2 assert cfg.frame_recognition_seconds == 2
assert cfg.has_altitude is False
def test_ai_config_from_dict_altitude_override_sets_flag():
from inference import ai_config_from_dict
cfg = ai_config_from_dict({"altitude": 400})
assert cfg.has_altitude is True
assert cfg.altitude == 400
def test_ai_config_from_dict_overrides(): def test_ai_config_from_dict_overrides():
+17
View File
@@ -118,6 +118,23 @@ def test_resolve_media_for_detect_override_wins():
assert "paths" not in cfg assert "paths" not in cfg
def test_resolve_media_for_detect_omits_altitude_when_not_provided():
# Arrange
import main
tm = main.TokenManager(_access_jwt(), "")
mock_ann = MagicMock()
mock_ann.fetch_user_ai_settings.return_value = {"probabilityThreshold": 0.2}
mock_ann.fetch_media_path.return_value = "/m/v.mp4"
with patch("main.annotations_client", mock_ann):
# Act
cfg, path = main._resolve_media_for_detect("vid-2", tm, None)
# Assert
assert "altitude" not in cfg
assert cfg["probability_threshold"] == 0.2
assert path == "/m/v.mp4"
def test_resolve_media_for_detect_raises_when_no_media_path(): def test_resolve_media_for_detect_raises_when_no_media_path():
# Arrange # Arrange
import main import main