Compare commits

...

6 Commits

Author SHA1 Message Date
LittleSam129
04ce66fdfe Lab028: add video packetization and CRC reassembly 2026-07-27 13:46:01 +03:00
LittleSam129
78258b5355 Lab027: optimize synchronized ROI video stream 2026-07-24 17:54:27 +03:00
LittleSam129
6d49036066 Lab026: benchmark low-bitrate video and 4-bit encoding 2026-07-24 11:56:41 +03:00
LittleSam129
557b082f2b lab025_wfm_audio_100_1mhz 2026-07-22 17:29:07 +03:00
LittleSam129
f3a11b29b3 Merge branch 'repair/project-layout' 2026-07-22 14:20:11 +03:00
LittleSam129
35cb25fccc Create lab024a_pluto_rx_100mhz.npy 2026-07-22 11:24:05 +03:00
43 changed files with 14024 additions and 1 deletions

View File

@@ -33,3 +33,24 @@
```text ```text
git version 2.51.1.windows.1 git version 2.51.1.windows.1
```
---
# Запись 002
## Дата
27 июля 2026 года
## Тема
Завершение Lab028: пакетирование синхронного BASE + ROI.
## Выполнено
- Реализован фиксированный бинарный заголовок размером 32 байта.
- Добавлены packet CRC32 для заголовка и payload и object CRC32 для полного JPEG.
- Исследованы размеры payload 64, 128, 256, 512 и 1024 байта.
- Размер payload 512 байт принят как рабочий кандидат.
- Окончательный выбор размера пакета перенесён в Lab029.

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

View File

@@ -0,0 +1,29 @@
Lab025. Приём и программная WFM-демодуляция
URI Pluto+: ip:192.168.2.1
Частота станции: 100100000 Гц
RX LO: 100350000 Гц
Цифровое смещение: +250000 Гц
Частота дискретизации RX: 2400000 Гц
Полоса RX: 1500000 Гц
Количество рабочих буферов: 128
Количество IQ-сэмплов: 8388608
Длительность IQ-записи: 3.495253 с
Частота канального сигнала после децимации: 240000 Гц
Длительность итогового WAV: 3.445250 с
Частота WAV: 48000 Гц
Тип PCM: signed PCM16, mono
RMS итогового аудио: 0.389148
Пиковая амплитуда: 0.974792
Отсчёты на границе PCM16: 0.000000 %
Выходные файлы:
WAV: data\processed\lab025\lab025_wfm_audio_100_1mhz.wav
Радиоспектр: data\processed\lab025\lab025_rf_spectrum.png
Звуковая волна: data\processed\lab025\lab025_audio_waveform.png
Спектр звука: data\processed\lab025\lab025_audio_spectrum.png
Отчёт: data\processed\lab025\lab025_report.txt
Использован только приёмный канал RX1.
Передатчики TX1 и TX2 не использовались.
Необработанные IQ-сэмплы на диск не сохранялись.

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -0,0 +1,33 @@
experiment,profile_name,width,height,target_fps,actual_fps,color_mode,jpeg_quality,source_duration_s,selected_frames,total_bytes,mean_frame_bytes,median_frame_bytes,p95_frame_bytes,max_frame_bytes,payload_bitrate_bps,payload_bitrate_kbps,source_file_bitrate_kbps,reduction_ratio,percent_of_source,mean_psnr_db
fps_sweep,fps_30,640,360,30.000000,30.000000,grayscale,50,20.766667,623,10219569,16403.803,16282.000,18471.500,21081,3936912.616,3936.912616,3001.342536,0.762359,131.171720,37.349661
fps_sweep,fps_10,640,360,10.000000,10.016051,grayscale,50,20.766667,208,3641634,17507.856,17509.500,18958.400,21081,1402876.661,1402.876661,3001.342536,2.139420,46.741638,36.369592
fps_sweep,fps_5,640,360,5.000000,5.008026,grayscale,50,20.766667,104,1822387,17522.952,17547.000,18910.400,21081,702043.146,702.043146,3001.342536,4.275154,23.390970,36.352519
fps_sweep,fps_2,640,360,2.000000,2.022472,grayscale,50,20.766667,42,737435,17557.976,17476.500,18926.800,21081,284084.109,284.084109,3001.342536,10.564979,9.465234,36.319782
fps_sweep,fps_1,640,360,1.000000,1.011236,grayscale,50,20.766667,21,369317,17586.524,17483.000,18935.000,21081,142273.002,142.273002,3001.342536,21.095658,4.740312,36.287711
fps_sweep,fps_0_5,640,360,0.500000,0.529695,grayscale,50,20.766667,11,191626,17420.545,17040.000,19774.000,21081,73820.610,73.820610,3001.342536,40.657244,2.459586,36.371684
fps_sweep,fps_0_2,640,360,0.200000,0.240770,grayscale,50,20.766667,5,91496,18299.200,18467.000,20651.800,21081,35247.255,35.247255,3001.342536,85.151100,1.174383,35.860330
resolution_sweep,resolution_1280x720,1280,720,5.000000,5.008026,grayscale,50,20.766667,104,5506628,52948.346,52634.000,57821.150,73994,2121333.419,2121.333419,3001.342536,1.414838,70.679484,39.607731
resolution_sweep,resolution_960x540,960,540,5.000000,5.008026,grayscale,50,20.766667,104,3411900,32806.731,32634.500,35501.600,41897,1314375.602,1314.375602,3001.342536,2.283474,43.792922,38.632041
resolution_sweep,resolution_640x360,640,360,5.000000,5.008026,grayscale,50,20.766667,104,1822387,17522.952,17547.000,18910.400,21081,702043.146,702.043146,3001.342536,4.275154,23.390970,36.352519
resolution_sweep,resolution_480x270,480,270,5.000000,5.008026,grayscale,50,20.766667,104,1120944,10778.308,10786.000,11463.200,12395,431824.334,431.824334,3001.342536,6.950378,14.387706,36.172087
resolution_sweep,resolution_320x180,320,180,5.000000,5.008026,grayscale,50,20.766667,104,590018,5673.250,5679.000,5995.750,6191,227294.254,227.294254,3001.342536,13.204656,7.573086,35.069392
resolution_sweep,resolution_160x90,160,90,5.000000,5.008026,grayscale,50,20.766667,104,210050,2019.712,2017.000,2103.400,2156,80918.138,80.918138,3001.342536,37.091097,2.696065,33.585930
quality_sweep,quality_color_q90,640,360,5.000000,5.008026,color,90,20.766667,104,5486654,52756.288,52645.500,57012.750,66227,2113638.780,2113.638780,3001.342536,1.419988,70.423111,39.694424
quality_sweep,quality_color_q70,640,360,5.000000,5.008026,color,70,20.766667,104,2901304,27897.154,27815.500,30080.600,33877,1117677.303,1117.677303,3001.342536,2.685339,37.239245,36.029412
quality_sweep,quality_color_q50,640,360,5.000000,5.008026,color,50,20.766667,104,2137169,20549.702,20514.000,22026.250,24396,823307.480,823.307480,3001.342536,3.645470,27.431307,34.439250
quality_sweep,quality_color_q30,640,360,5.000000,5.008026,color,30,20.766667,104,1572112,15116.462,15105.500,16167.600,17386,605629.021,605.629021,3001.342536,4.955744,20.178604,32.766254
quality_sweep,quality_color_q20,640,360,5.000000,5.008026,color,20,20.766667,104,1224535,11774.375,11797.000,12633.050,13098,471730.979,471.730979,3001.342536,6.362403,15.717332,31.298703
quality_sweep,quality_grayscale_q90,640,360,5.000000,5.008026,grayscale,90,20.766667,104,4697004,45163.500,44959.000,48937.750,57535,1809439.743,1809.439743,3001.342536,1.658714,60.287679,42.574562
quality_sweep,quality_grayscale_q70,640,360,5.000000,5.008026,grayscale,70,20.766667,104,2484781,23892.125,23853.000,25744.950,29385,957219.005,957.219005,3001.342536,3.135482,31.893028,38.031656
quality_sweep,quality_grayscale_q50,640,360,5.000000,5.008026,grayscale,50,20.766667,104,1822387,17522.952,17547.000,18910.400,21081,702043.146,702.043146,3001.342536,4.275154,23.390970,36.352519
quality_sweep,quality_grayscale_q30,640,360,5.000000,5.008026,grayscale,30,20.766667,104,1328179,12770.952,12821.500,13834.150,14903,511658.042,511.658042,3001.342536,5.865915,17.047639,34.701951
quality_sweep,quality_grayscale_q20,640,360,5.000000,5.008026,grayscale,20,20.766667,104,1017957,9788.048,9832.500,10597.450,11027,392150.369,392.150369,3001.342536,7.653550,13.065832,33.304362
candidate_profiles,candidate_640x360_gray_5fps_q50,640,360,5.000000,5.008026,grayscale,50,20.766667,104,1822387,17522.952,17547.000,18910.400,21081,702043.146,702.043146,3001.342536,4.275154,23.390970,36.352519
candidate_profiles,candidate_640x360_gray_2fps_q40,640,360,2.000000,2.022472,grayscale,40,20.766667,42,639531,15226.929,15187.500,16381.750,18122,246368.283,246.368283,3001.342536,12.182341,8.208603,35.588581
candidate_profiles,candidate_320x180_color_5fps_q50,320,180,5.000000,5.008026,color,50,20.766667,104,717676,6900.731,6919.500,7309.850,7475,276472.295,276.472295,3001.342536,10.855853,9.211621,32.413850
candidate_profiles,candidate_320x180_gray_5fps_q50,320,180,5.000000,5.008026,grayscale,50,20.766667,104,590018,5673.250,5679.000,5995.750,6191,227294.254,227.294254,3001.342536,13.204656,7.573086,35.069392
candidate_profiles,candidate_320x180_gray_2fps_q40,320,180,2.000000,2.022472,grayscale,40,20.766667,42,208403,4961.976,4965.000,5224.700,5400,80283.660,80.283660,3001.342536,37.384227,2.674925,34.362114
candidate_profiles,candidate_320x180_gray_1fps_q35,320,180,1.000000,1.011236,grayscale,35,20.766667,21,96733,4606.333,4611.000,4836.000,4992,37264.719,37.264719,3001.342536,80.541129,1.241602,34.035117
candidate_profiles,candidate_320x180_gray_0_5fps_q35,320,180,0.500000,0.529695,grayscale,35,20.766667,11,50124,4556.727,4493.000,4914.000,4992,19309.406,19.309406,3001.342536,155.434223,0.643359,34.165852
candidate_profiles,candidate_320x180_gray_0_2fps_q35,320,180,0.200000,0.240770,grayscale,35,20.766667,5,23618,4723.600,4815.000,4960.800,4992,9098.427,9.098427,3001.342536,329.874884,0.303145,33.741044
candidate_profiles,candidate_160x90_gray_1fps_q30,160,90,1.000000,1.011236,grayscale,30,20.766667,21,32743,1559.190,1561.000,1623.000,1632,12613.676,12.613676,3001.342536,237.943530,0.420268,32.031480
1 experiment profile_name width height target_fps actual_fps color_mode jpeg_quality source_duration_s selected_frames total_bytes mean_frame_bytes median_frame_bytes p95_frame_bytes max_frame_bytes payload_bitrate_bps payload_bitrate_kbps source_file_bitrate_kbps reduction_ratio percent_of_source mean_psnr_db
2 fps_sweep fps_30 640 360 30.000000 30.000000 grayscale 50 20.766667 623 10219569 16403.803 16282.000 18471.500 21081 3936912.616 3936.912616 3001.342536 0.762359 131.171720 37.349661
3 fps_sweep fps_10 640 360 10.000000 10.016051 grayscale 50 20.766667 208 3641634 17507.856 17509.500 18958.400 21081 1402876.661 1402.876661 3001.342536 2.139420 46.741638 36.369592
4 fps_sweep fps_5 640 360 5.000000 5.008026 grayscale 50 20.766667 104 1822387 17522.952 17547.000 18910.400 21081 702043.146 702.043146 3001.342536 4.275154 23.390970 36.352519
5 fps_sweep fps_2 640 360 2.000000 2.022472 grayscale 50 20.766667 42 737435 17557.976 17476.500 18926.800 21081 284084.109 284.084109 3001.342536 10.564979 9.465234 36.319782
6 fps_sweep fps_1 640 360 1.000000 1.011236 grayscale 50 20.766667 21 369317 17586.524 17483.000 18935.000 21081 142273.002 142.273002 3001.342536 21.095658 4.740312 36.287711
7 fps_sweep fps_0_5 640 360 0.500000 0.529695 grayscale 50 20.766667 11 191626 17420.545 17040.000 19774.000 21081 73820.610 73.820610 3001.342536 40.657244 2.459586 36.371684
8 fps_sweep fps_0_2 640 360 0.200000 0.240770 grayscale 50 20.766667 5 91496 18299.200 18467.000 20651.800 21081 35247.255 35.247255 3001.342536 85.151100 1.174383 35.860330
9 resolution_sweep resolution_1280x720 1280 720 5.000000 5.008026 grayscale 50 20.766667 104 5506628 52948.346 52634.000 57821.150 73994 2121333.419 2121.333419 3001.342536 1.414838 70.679484 39.607731
10 resolution_sweep resolution_960x540 960 540 5.000000 5.008026 grayscale 50 20.766667 104 3411900 32806.731 32634.500 35501.600 41897 1314375.602 1314.375602 3001.342536 2.283474 43.792922 38.632041
11 resolution_sweep resolution_640x360 640 360 5.000000 5.008026 grayscale 50 20.766667 104 1822387 17522.952 17547.000 18910.400 21081 702043.146 702.043146 3001.342536 4.275154 23.390970 36.352519
12 resolution_sweep resolution_480x270 480 270 5.000000 5.008026 grayscale 50 20.766667 104 1120944 10778.308 10786.000 11463.200 12395 431824.334 431.824334 3001.342536 6.950378 14.387706 36.172087
13 resolution_sweep resolution_320x180 320 180 5.000000 5.008026 grayscale 50 20.766667 104 590018 5673.250 5679.000 5995.750 6191 227294.254 227.294254 3001.342536 13.204656 7.573086 35.069392
14 resolution_sweep resolution_160x90 160 90 5.000000 5.008026 grayscale 50 20.766667 104 210050 2019.712 2017.000 2103.400 2156 80918.138 80.918138 3001.342536 37.091097 2.696065 33.585930
15 quality_sweep quality_color_q90 640 360 5.000000 5.008026 color 90 20.766667 104 5486654 52756.288 52645.500 57012.750 66227 2113638.780 2113.638780 3001.342536 1.419988 70.423111 39.694424
16 quality_sweep quality_color_q70 640 360 5.000000 5.008026 color 70 20.766667 104 2901304 27897.154 27815.500 30080.600 33877 1117677.303 1117.677303 3001.342536 2.685339 37.239245 36.029412
17 quality_sweep quality_color_q50 640 360 5.000000 5.008026 color 50 20.766667 104 2137169 20549.702 20514.000 22026.250 24396 823307.480 823.307480 3001.342536 3.645470 27.431307 34.439250
18 quality_sweep quality_color_q30 640 360 5.000000 5.008026 color 30 20.766667 104 1572112 15116.462 15105.500 16167.600 17386 605629.021 605.629021 3001.342536 4.955744 20.178604 32.766254
19 quality_sweep quality_color_q20 640 360 5.000000 5.008026 color 20 20.766667 104 1224535 11774.375 11797.000 12633.050 13098 471730.979 471.730979 3001.342536 6.362403 15.717332 31.298703
20 quality_sweep quality_grayscale_q90 640 360 5.000000 5.008026 grayscale 90 20.766667 104 4697004 45163.500 44959.000 48937.750 57535 1809439.743 1809.439743 3001.342536 1.658714 60.287679 42.574562
21 quality_sweep quality_grayscale_q70 640 360 5.000000 5.008026 grayscale 70 20.766667 104 2484781 23892.125 23853.000 25744.950 29385 957219.005 957.219005 3001.342536 3.135482 31.893028 38.031656
22 quality_sweep quality_grayscale_q50 640 360 5.000000 5.008026 grayscale 50 20.766667 104 1822387 17522.952 17547.000 18910.400 21081 702043.146 702.043146 3001.342536 4.275154 23.390970 36.352519
23 quality_sweep quality_grayscale_q30 640 360 5.000000 5.008026 grayscale 30 20.766667 104 1328179 12770.952 12821.500 13834.150 14903 511658.042 511.658042 3001.342536 5.865915 17.047639 34.701951
24 quality_sweep quality_grayscale_q20 640 360 5.000000 5.008026 grayscale 20 20.766667 104 1017957 9788.048 9832.500 10597.450 11027 392150.369 392.150369 3001.342536 7.653550 13.065832 33.304362
25 candidate_profiles candidate_640x360_gray_5fps_q50 640 360 5.000000 5.008026 grayscale 50 20.766667 104 1822387 17522.952 17547.000 18910.400 21081 702043.146 702.043146 3001.342536 4.275154 23.390970 36.352519
26 candidate_profiles candidate_640x360_gray_2fps_q40 640 360 2.000000 2.022472 grayscale 40 20.766667 42 639531 15226.929 15187.500 16381.750 18122 246368.283 246.368283 3001.342536 12.182341 8.208603 35.588581
27 candidate_profiles candidate_320x180_color_5fps_q50 320 180 5.000000 5.008026 color 50 20.766667 104 717676 6900.731 6919.500 7309.850 7475 276472.295 276.472295 3001.342536 10.855853 9.211621 32.413850
28 candidate_profiles candidate_320x180_gray_5fps_q50 320 180 5.000000 5.008026 grayscale 50 20.766667 104 590018 5673.250 5679.000 5995.750 6191 227294.254 227.294254 3001.342536 13.204656 7.573086 35.069392
29 candidate_profiles candidate_320x180_gray_2fps_q40 320 180 2.000000 2.022472 grayscale 40 20.766667 42 208403 4961.976 4965.000 5224.700 5400 80283.660 80.283660 3001.342536 37.384227 2.674925 34.362114
30 candidate_profiles candidate_320x180_gray_1fps_q35 320 180 1.000000 1.011236 grayscale 35 20.766667 21 96733 4606.333 4611.000 4836.000 4992 37264.719 37.264719 3001.342536 80.541129 1.241602 34.035117
31 candidate_profiles candidate_320x180_gray_0_5fps_q35 320 180 0.500000 0.529695 grayscale 35 20.766667 11 50124 4556.727 4493.000 4914.000 4992 19309.406 19.309406 3001.342536 155.434223 0.643359 34.165852
32 candidate_profiles candidate_320x180_gray_0_2fps_q35 320 180 0.200000 0.240770 grayscale 35 20.766667 5 23618 4723.600 4815.000 4960.800 4992 9098.427 9.098427 3001.342536 329.874884 0.303145 33.741044
33 candidate_profiles candidate_160x90_gray_1fps_q30 160 90 1.000000 1.011236 grayscale 30 20.766667 21 32743 1559.190 1561.000 1623.000 1632 12613.676 12.613676 3001.342536 237.943530 0.420268 32.031480

View File

@@ -0,0 +1,82 @@
Lab026. Исследование уменьшения видеопотока для радиоуправления ровером
Цель: исследовать уменьшение полезной нагрузки видеоканала для передачи через ретранслятор.
Исходный файл: data\raw\lab026_rover_source.mp4
Размер исходного файла: 7790985 байт
Исходное разрешение: 1280x720
Исходный FPS: 30.000000
Число кадров: 623
Длительность: 20.766667 с
Приблизительный исходный файловый битрейт: 3001.343 кбит/с
Исследуется независимое покадровое JPEG-сжатие. JPEG хранится и декодируется только в памяти.
Рассчитанный bitrate учитывает только JPEG payload.
Не учитываются заголовки радиопакетов, CRC, FEC, преамбула, интервалы, повторы и команды управления.
Результаты fps_sweep:
fps_30: 640x360, 30 кадр/с, grayscale, Q=50, кадров=623, payload=3936.913 кбит/с, PSNR=37.350 дБ, уменьшение=0.76x
fps_10: 640x360, 10 кадр/с, grayscale, Q=50, кадров=208, payload=1402.877 кбит/с, PSNR=36.370 дБ, уменьшение=2.14x
fps_5: 640x360, 5 кадр/с, grayscale, Q=50, кадров=104, payload=702.043 кбит/с, PSNR=36.353 дБ, уменьшение=4.28x
fps_2: 640x360, 2 кадр/с, grayscale, Q=50, кадров=42, payload=284.084 кбит/с, PSNR=36.320 дБ, уменьшение=10.56x
fps_1: 640x360, 1 кадр/с, grayscale, Q=50, кадров=21, payload=142.273 кбит/с, PSNR=36.288 дБ, уменьшение=21.10x
fps_0_5: 640x360, 0.5 кадр/с, grayscale, Q=50, кадров=11, payload=73.821 кбит/с, PSNR=36.372 дБ, уменьшение=40.66x
fps_0_2: 640x360, 0.2 кадр/с, grayscale, Q=50, кадров=5, payload=35.247 кбит/с, PSNR=35.860 дБ, уменьшение=85.15x
Результаты resolution_sweep:
resolution_1280x720: 1280x720, 5 кадр/с, grayscale, Q=50, кадров=104, payload=2121.333 кбит/с, PSNR=39.608 дБ, уменьшение=1.41x
resolution_960x540: 960x540, 5 кадр/с, grayscale, Q=50, кадров=104, payload=1314.376 кбит/с, PSNR=38.632 дБ, уменьшение=2.28x
resolution_640x360: 640x360, 5 кадр/с, grayscale, Q=50, кадров=104, payload=702.043 кбит/с, PSNR=36.353 дБ, уменьшение=4.28x
resolution_480x270: 480x270, 5 кадр/с, grayscale, Q=50, кадров=104, payload=431.824 кбит/с, PSNR=36.172 дБ, уменьшение=6.95x
resolution_320x180: 320x180, 5 кадр/с, grayscale, Q=50, кадров=104, payload=227.294 кбит/с, PSNR=35.069 дБ, уменьшение=13.20x
resolution_160x90: 160x90, 5 кадр/с, grayscale, Q=50, кадров=104, payload=80.918 кбит/с, PSNR=33.586 дБ, уменьшение=37.09x
Результаты quality_sweep:
quality_color_q90: 640x360, 5 кадр/с, color, Q=90, кадров=104, payload=2113.639 кбит/с, PSNR=39.694 дБ, уменьшение=1.42x
quality_color_q70: 640x360, 5 кадр/с, color, Q=70, кадров=104, payload=1117.677 кбит/с, PSNR=36.029 дБ, уменьшение=2.69x
quality_color_q50: 640x360, 5 кадр/с, color, Q=50, кадров=104, payload=823.307 кбит/с, PSNR=34.439 дБ, уменьшение=3.65x
quality_color_q30: 640x360, 5 кадр/с, color, Q=30, кадров=104, payload=605.629 кбит/с, PSNR=32.766 дБ, уменьшение=4.96x
quality_color_q20: 640x360, 5 кадр/с, color, Q=20, кадров=104, payload=471.731 кбит/с, PSNR=31.299 дБ, уменьшение=6.36x
quality_grayscale_q90: 640x360, 5 кадр/с, grayscale, Q=90, кадров=104, payload=1809.440 кбит/с, PSNR=42.575 дБ, уменьшение=1.66x
quality_grayscale_q70: 640x360, 5 кадр/с, grayscale, Q=70, кадров=104, payload=957.219 кбит/с, PSNR=38.032 дБ, уменьшение=3.14x
quality_grayscale_q50: 640x360, 5 кадр/с, grayscale, Q=50, кадров=104, payload=702.043 кбит/с, PSNR=36.353 дБ, уменьшение=4.28x
quality_grayscale_q30: 640x360, 5 кадр/с, grayscale, Q=30, кадров=104, payload=511.658 кбит/с, PSNR=34.702 дБ, уменьшение=5.87x
quality_grayscale_q20: 640x360, 5 кадр/с, grayscale, Q=20, кадров=104, payload=392.150 кбит/с, PSNR=33.304 дБ, уменьшение=7.65x
Результаты candidate_profiles:
candidate_640x360_gray_5fps_q50: 640x360, 5 кадр/с, grayscale, Q=50, кадров=104, payload=702.043 кбит/с, PSNR=36.353 дБ, уменьшение=4.28x
candidate_640x360_gray_2fps_q40: 640x360, 2 кадр/с, grayscale, Q=40, кадров=42, payload=246.368 кбит/с, PSNR=35.589 дБ, уменьшение=12.18x
candidate_320x180_color_5fps_q50: 320x180, 5 кадр/с, color, Q=50, кадров=104, payload=276.472 кбит/с, PSNR=32.414 дБ, уменьшение=10.86x
candidate_320x180_gray_5fps_q50: 320x180, 5 кадр/с, grayscale, Q=50, кадров=104, payload=227.294 кбит/с, PSNR=35.069 дБ, уменьшение=13.20x
candidate_320x180_gray_2fps_q40: 320x180, 2 кадр/с, grayscale, Q=40, кадров=42, payload=80.284 кбит/с, PSNR=34.362 дБ, уменьшение=37.38x
candidate_320x180_gray_1fps_q35: 320x180, 1 кадр/с, grayscale, Q=35, кадров=21, payload=37.265 кбит/с, PSNR=34.035 дБ, уменьшение=80.54x
candidate_320x180_gray_0_5fps_q35: 320x180, 0.5 кадр/с, grayscale, Q=35, кадров=11, payload=19.309 кбит/с, PSNR=34.166 дБ, уменьшение=155.43x
candidate_320x180_gray_0_2fps_q35: 320x180, 0.2 кадр/с, grayscale, Q=35, кадров=5, payload=9.098 кбит/с, PSNR=33.741 дБ, уменьшение=329.87x
candidate_160x90_gray_1fps_q30: 160x90, 1 кадр/с, grayscale, Q=30, кадров=21, payload=12.614 кбит/с, PSNR=32.031 дБ, уменьшение=237.94x
Candidate-профили с payload bitrate не более 100 кбит/с:
candidate_320x180_gray_2fps_q40: 320x180, 2 кадр/с, grayscale, Q=40, кадров=42, payload=80.284 кбит/с, PSNR=34.362 дБ, уменьшение=37.38x
candidate_320x180_gray_1fps_q35: 320x180, 1 кадр/с, grayscale, Q=35, кадров=21, payload=37.265 кбит/с, PSNR=34.035 дБ, уменьшение=80.54x
candidate_320x180_gray_0_5fps_q35: 320x180, 0.5 кадр/с, grayscale, Q=35, кадров=11, payload=19.309 кбит/с, PSNR=34.166 дБ, уменьшение=155.43x
candidate_320x180_gray_0_2fps_q35: 320x180, 0.2 кадр/с, grayscale, Q=35, кадров=5, payload=9.098 кбит/с, PSNR=33.741 дБ, уменьшение=329.87x
candidate_160x90_gray_1fps_q30: 160x90, 1 кадр/с, grayscale, Q=30, кадров=21, payload=12.614 кбит/с, PSNR=32.031 дБ, уменьшение=237.94x
Candidate-профили с payload bitrate не более 50 кбит/с:
candidate_320x180_gray_1fps_q35: 320x180, 1 кадр/с, grayscale, Q=35, кадров=21, payload=37.265 кбит/с, PSNR=34.035 дБ, уменьшение=80.54x
candidate_320x180_gray_0_5fps_q35: 320x180, 0.5 кадр/с, grayscale, Q=35, кадров=11, payload=19.309 кбит/с, PSNR=34.166 дБ, уменьшение=155.43x
candidate_320x180_gray_0_2fps_q35: 320x180, 0.2 кадр/с, grayscale, Q=35, кадров=5, payload=9.098 кбит/с, PSNR=33.741 дБ, уменьшение=329.87x
candidate_160x90_gray_1fps_q30: 160x90, 1 кадр/с, grayscale, Q=30, кадров=21, payload=12.614 кбит/с, PSNR=32.031 дБ, уменьшение=237.94x
Candidate-профили с payload bitrate не более 20 кбит/с:
candidate_320x180_gray_0_5fps_q35: 320x180, 0.5 кадр/с, grayscale, Q=35, кадров=11, payload=19.309 кбит/с, PSNR=34.166 дБ, уменьшение=155.43x
candidate_320x180_gray_0_2fps_q35: 320x180, 0.2 кадр/с, grayscale, Q=35, кадров=5, payload=9.098 кбит/с, PSNR=33.741 дБ, уменьшение=329.87x
candidate_160x90_gray_1fps_q30: 160x90, 1 кадр/с, grayscale, Q=30, кадров=21, payload=12.614 кбит/с, PSNR=32.031 дБ, уменьшение=237.94x
Candidate-профили с payload bitrate не более 10 кбит/с:
candidate_320x180_gray_0_2fps_q35: 320x180, 0.2 кадр/с, grayscale, Q=35, кадров=5, payload=9.098 кбит/с, PSNR=33.741 дБ, уменьшение=329.87x
Профиль с минимальным измеренным битрейтом:
candidate_320x180_gray_0_2fps_q35: 320x180, 0.2 кадр/с, grayscale, Q=35, кадров=5, payload=9.098 кбит/с, PSNR=33.741 дБ, уменьшение=329.87x
Автоматические метрики не заменяют визуальную оценку пригодности изображения для управления ровером.
Сравнительное изображение: data\processed\lab026\lab026_candidate_profiles.png
Следующий шаг: пользователь должен визуально выбрать пригодные candidate-профили.

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,31 @@
experiment,profile_name,mode,width,height,pixel_count,target_fps,actual_fps,bits_per_pixel_before_compression,maximum_gray_levels,maximum_colors,selected_frames,total_payload_bytes,mean_frame_bytes,median_frame_bytes,p95_frame_bytes,max_frame_bytes,payload_bitrate_bps,payload_bitrate_kbps,baseline_bitrate_kbps,difference_from_baseline_kbps,percent_of_baseline_bitrate,compression_ratio_from_unpacked_source,mean_psnr_db,header_bytes_per_frame,palette_bytes_per_frame,notes
four_bit_resolution_sweep,320x180_gray8_jpeg_2fps,gray8_jpeg,320,180,57600,2.000000,2.022472,8,256,0,42,208403,4961.976,4965.000,5224.700,5400,80283.660,80.283660,80.283660,-0.000000,100.000000,11.608278,34.362114,0,0,"Grayscale 8 bit, JPEG quality 40."
four_bit_resolution_sweep,320x180_gray4_jpeg_2fps,gray4_jpeg,320,180,57600,2.000000,2.022472,4,16,0,42,229518,5464.714,5466.000,5727.250,5822,88417.849,88.417849,80.283660,8.134189,110.131812,10.540350,32.588941,0,0,"Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40."
four_bit_resolution_sweep,320x180_gray4_packed_zlib_2fps,gray4_packed_zlib,320,180,57600,2.000000,2.022472,4,16,0,42,441074,10501.762,10444.000,11194.300,12317,169916.148,169.916148,80.283660,89.632488,211.644745,5.484794,34.397012,16,0,"Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header."
four_bit_resolution_sweep,320x180_color16_png_2fps,color16_png,320,180,57600,2.000000,2.022472,4,0,16,42,371520,8845.714,8607.500,10578.350,10794,143121.669,143.121669,80.283660,62.838009,178.269986,19.534884,25.384724,0,0,"Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG."
four_bit_resolution_sweep,320x180_color16_packed_zlib_2fps,color16_packed_zlib,320,180,57600,2.000000,2.022472,4,0,16,42,364892,8687.905,8452.000,10414.800,10652,140568.347,140.568347,80.283660,60.284687,175.089609,19.889721,25.384724,16,48,"Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette."
four_bit_resolution_sweep,400x225_gray8_jpeg_2fps,gray8_jpeg,400,225,90000,2.000000,2.022472,8,256,0,42,297273,7077.929,7063.500,7473.000,7841,114519.294,114.519294,80.283660,34.235634,142.643340,12.715585,34.942886,0,0,"Grayscale 8 bit, JPEG quality 40."
four_bit_resolution_sweep,400x225_gray4_jpeg_2fps,gray4_jpeg,400,225,90000,2.000000,2.022472,4,16,0,42,332268,7911.143,7920.500,8353.700,8554,128000.514,128.000514,80.283660,47.716854,159.435324,11.376359,32.883090,0,0,"Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40."
four_bit_resolution_sweep,400x225_gray4_packed_zlib_2fps,gray4_packed_zlib,400,225,90000,2.000000,2.022472,4,16,0,42,636862,15163.381,15120.500,16266.050,18356,245340.096,245.340096,80.283660,165.056436,305.591569,5.935352,34.399552,16,0,"Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header."
four_bit_resolution_sweep,400x225_color16_png_2fps,color16_png,400,225,90000,2.000000,2.022472,4,0,16,42,528261,12577.643,12153.000,15191.650,16128,203503.435,203.503435,80.283660,123.219775,253.480515,21.466661,25.344339,0,0,"Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG."
four_bit_resolution_sweep,400x225_color16_packed_zlib_2fps,color16_packed_zlib,400,225,90000,2.000000,2.022472,4,0,16,42,520725,12398.214,11977.000,15015.100,15966,200600.321,200.600321,80.283660,120.316661,249.864444,21.777330,25.344339,16,48,"Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette."
four_bit_resolution_sweep,448x252_gray8_jpeg_2fps,gray8_jpeg,448,252,112896,2.000000,2.022472,8,256,0,42,353578,8418.524,8413.000,8885.950,9512,136209.823,136.209823,80.283660,55.926163,169.660705,13.410427,35.301071,0,0,"Grayscale 8 bit, JPEG quality 40."
four_bit_resolution_sweep,448x252_gray4_jpeg_2fps,gray4_jpeg,448,252,112896,2.000000,2.022472,4,16,0,42,397126,9455.381,9467.500,9996.400,10450,152985.939,152.985939,80.283660,72.702279,190.556757,11.939868,33.060439,0,0,"Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40."
four_bit_resolution_sweep,448x252_gray4_packed_zlib_2fps,gray4_packed_zlib,448,252,112896,2.000000,2.022472,4,16,0,42,762082,18144.810,18111.500,19504.750,22433,293578.941,293.578941,80.283660,213.295281,365.677076,6.221945,34.399841,16,0,"Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header."
four_bit_resolution_sweep,448x252_color16_png_2fps,color16_png,448,252,112896,2.000000,2.022472,4,0,16,42,626516,14917.048,14409.500,18239.600,18695,241354.478,241.354478,80.283660,161.070818,300.627149,22.704761,25.369963,0,0,"Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG."
four_bit_resolution_sweep,448x252_color16_packed_zlib_2fps,color16_packed_zlib,448,252,112896,2.000000,2.022472,4,0,16,42,618164,14718.190,14221.500,18022.000,18517,238137.014,238.137014,80.283660,157.853354,296.619529,23.011524,25.369963,16,48,"Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette."
four_bit_resolution_sweep,480x270_gray8_jpeg_2fps,gray8_jpeg,480,270,129600,2.000000,2.022472,8,256,0,42,393752,9375.048,9365.500,9919.950,10678,151686.164,151.686164,80.283660,71.402504,188.937779,13.823930,35.412999,0,0,"Grayscale 8 bit, JPEG quality 40."
four_bit_resolution_sweep,480x270_gray4_jpeg_2fps,gray4_jpeg,480,270,129600,2.000000,2.022472,4,16,0,42,443419,10557.595,10575.500,11152.550,11737,170819.518,170.819518,80.283660,90.535858,212.769969,12.275523,33.099458,0,0,"Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40."
four_bit_resolution_sweep,480x270_gray4_packed_zlib_2fps,gray4_packed_zlib,480,270,129600,2.000000,2.022472,4,16,0,42,854354,20341.762,20276.500,21857.900,25369,329125.136,329.125136,80.283660,248.841476,409.952830,6.371130,34.401420,16,0,"Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header."
four_bit_resolution_sweep,480x270_color16_png_2fps,color16_png,480,270,129600,2.000000,2.022472,4,0,16,42,701871,16711.214,16249.500,20469.850,21005,270383.692,270.383692,80.283660,190.100032,336.785458,23.265814,25.368870,0,0,"Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG."
four_bit_resolution_sweep,480x270_color16_packed_zlib_2fps,color16_packed_zlib,480,270,129600,2.000000,2.022472,4,0,16,42,693190,16504.524,16007.500,20249.750,20809,267039.486,267.039486,80.283660,186.755826,332.619971,23.557178,25.368870,16,48,"Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette."
four_bit_resolution_sweep,560x315_gray8_jpeg_2fps,gray8_jpeg,560,315,176400,2.000000,2.022472,8,256,0,42,507535,12084.167,12068.500,12860.900,13999,195519.101,195.519101,80.283660,115.235441,243.535361,14.597614,35.954946,0,0,"Grayscale 8 bit, JPEG quality 40."
four_bit_resolution_sweep,560x315_gray4_jpeg_2fps,gray4_jpeg,560,315,176400,2.000000,2.022472,4,16,0,42,575350,13698.810,13674.500,14558.450,15458,221643.660,221.643660,80.283660,141.360000,276.075679,12.877031,33.337152,0,0,"Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40."
four_bit_resolution_sweep,560x315_gray4_packed_zlib_2fps,gray4_packed_zlib,560,315,176400,2.000000,2.022472,4,16,0,42,1093024,26024.381,25851.500,28055.650,33171,421068.636,421.068636,80.283660,340.784976,524.476133,6.778259,34.403027,16,0,"Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header."
four_bit_resolution_sweep,560x315_color16_png_2fps,color16_png,560,315,176400,2.000000,2.022472,4,0,16,42,894967,21308.738,20851.500,25906.100,27345,344770.594,344.770594,80.283660,264.486934,429.440554,24.834882,25.323519,0,0,"Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG."
four_bit_resolution_sweep,560x315_color16_packed_zlib_2fps,color16_packed_zlib,560,315,176400,2.000000,2.022472,4,0,16,42,885156,21075.143,20622.000,25686.650,27166,340991.075,340.991075,80.283660,260.707415,424.732848,25.110150,25.323519,16,48,"Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette."
four_bit_resolution_sweep,640x360_gray8_jpeg_2fps,gray8_jpeg,640,360,230400,2.000000,2.022472,8,256,0,42,639531,15226.929,15187.500,16381.750,18122,246368.283,246.368283,80.283660,166.084623,306.872261,15.131088,35.588581,0,0,"Grayscale 8 bit, JPEG quality 40."
four_bit_resolution_sweep,640x360_gray4_jpeg_2fps,gray4_jpeg,640,360,230400,2.000000,2.022472,4,16,0,42,726335,17293.690,17220.500,18557.350,19938,279808.026,279.808026,80.283660,199.524366,348.524252,13.322778,33.048695,0,0,"Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40."
four_bit_resolution_sweep,640x360_gray4_packed_zlib_2fps,gray4_packed_zlib,640,360,230400,2.000000,2.022472,4,16,0,42,1394769,33208.786,32974.500,35959.800,43514,537310.690,537.310690,80.283660,457.027030,669.265315,6.937923,34.404228,16,0,"Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header."
four_bit_resolution_sweep,640x360_color16_png_2fps,color16_png,640,360,230400,2.000000,2.022472,4,0,16,42,1140488,27154.476,26267.500,33424.950,36017,439353.323,439.353323,80.283660,359.069663,547.251237,25.454367,25.259621,0,0,"Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG."
four_bit_resolution_sweep,640x360_color16_packed_zlib_2fps,color16_packed_zlib,640,360,230400,2.000000,2.022472,4,0,16,42,1128711,26874.071,25978.500,33021.450,35603,434816.437,434.816437,80.283660,354.532777,541.600167,25.719958,25.259621,16,48,"Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette."
1 experiment profile_name mode width height pixel_count target_fps actual_fps bits_per_pixel_before_compression maximum_gray_levels maximum_colors selected_frames total_payload_bytes mean_frame_bytes median_frame_bytes p95_frame_bytes max_frame_bytes payload_bitrate_bps payload_bitrate_kbps baseline_bitrate_kbps difference_from_baseline_kbps percent_of_baseline_bitrate compression_ratio_from_unpacked_source mean_psnr_db header_bytes_per_frame palette_bytes_per_frame notes
2 four_bit_resolution_sweep 320x180_gray8_jpeg_2fps gray8_jpeg 320 180 57600 2.000000 2.022472 8 256 0 42 208403 4961.976 4965.000 5224.700 5400 80283.660 80.283660 80.283660 -0.000000 100.000000 11.608278 34.362114 0 0 Grayscale 8 bit, JPEG quality 40.
3 four_bit_resolution_sweep 320x180_gray4_jpeg_2fps gray4_jpeg 320 180 57600 2.000000 2.022472 4 16 0 42 229518 5464.714 5466.000 5727.250 5822 88417.849 88.417849 80.283660 8.134189 110.131812 10.540350 32.588941 0 0 Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40.
4 four_bit_resolution_sweep 320x180_gray4_packed_zlib_2fps gray4_packed_zlib 320 180 57600 2.000000 2.022472 4 16 0 42 441074 10501.762 10444.000 11194.300 12317 169916.148 169.916148 80.283660 89.632488 211.644745 5.484794 34.397012 16 0 Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header.
5 four_bit_resolution_sweep 320x180_color16_png_2fps color16_png 320 180 57600 2.000000 2.022472 4 0 16 42 371520 8845.714 8607.500 10578.350 10794 143121.669 143.121669 80.283660 62.838009 178.269986 19.534884 25.384724 0 0 Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG.
6 four_bit_resolution_sweep 320x180_color16_packed_zlib_2fps color16_packed_zlib 320 180 57600 2.000000 2.022472 4 0 16 42 364892 8687.905 8452.000 10414.800 10652 140568.347 140.568347 80.283660 60.284687 175.089609 19.889721 25.384724 16 48 Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette.
7 four_bit_resolution_sweep 400x225_gray8_jpeg_2fps gray8_jpeg 400 225 90000 2.000000 2.022472 8 256 0 42 297273 7077.929 7063.500 7473.000 7841 114519.294 114.519294 80.283660 34.235634 142.643340 12.715585 34.942886 0 0 Grayscale 8 bit, JPEG quality 40.
8 four_bit_resolution_sweep 400x225_gray4_jpeg_2fps gray4_jpeg 400 225 90000 2.000000 2.022472 4 16 0 42 332268 7911.143 7920.500 8353.700 8554 128000.514 128.000514 80.283660 47.716854 159.435324 11.376359 32.883090 0 0 Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40.
9 four_bit_resolution_sweep 400x225_gray4_packed_zlib_2fps gray4_packed_zlib 400 225 90000 2.000000 2.022472 4 16 0 42 636862 15163.381 15120.500 16266.050 18356 245340.096 245.340096 80.283660 165.056436 305.591569 5.935352 34.399552 16 0 Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header.
10 four_bit_resolution_sweep 400x225_color16_png_2fps color16_png 400 225 90000 2.000000 2.022472 4 0 16 42 528261 12577.643 12153.000 15191.650 16128 203503.435 203.503435 80.283660 123.219775 253.480515 21.466661 25.344339 0 0 Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG.
11 four_bit_resolution_sweep 400x225_color16_packed_zlib_2fps color16_packed_zlib 400 225 90000 2.000000 2.022472 4 0 16 42 520725 12398.214 11977.000 15015.100 15966 200600.321 200.600321 80.283660 120.316661 249.864444 21.777330 25.344339 16 48 Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette.
12 four_bit_resolution_sweep 448x252_gray8_jpeg_2fps gray8_jpeg 448 252 112896 2.000000 2.022472 8 256 0 42 353578 8418.524 8413.000 8885.950 9512 136209.823 136.209823 80.283660 55.926163 169.660705 13.410427 35.301071 0 0 Grayscale 8 bit, JPEG quality 40.
13 four_bit_resolution_sweep 448x252_gray4_jpeg_2fps gray4_jpeg 448 252 112896 2.000000 2.022472 4 16 0 42 397126 9455.381 9467.500 9996.400 10450 152985.939 152.985939 80.283660 72.702279 190.556757 11.939868 33.060439 0 0 Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40.
14 four_bit_resolution_sweep 448x252_gray4_packed_zlib_2fps gray4_packed_zlib 448 252 112896 2.000000 2.022472 4 16 0 42 762082 18144.810 18111.500 19504.750 22433 293578.941 293.578941 80.283660 213.295281 365.677076 6.221945 34.399841 16 0 Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header.
15 four_bit_resolution_sweep 448x252_color16_png_2fps color16_png 448 252 112896 2.000000 2.022472 4 0 16 42 626516 14917.048 14409.500 18239.600 18695 241354.478 241.354478 80.283660 161.070818 300.627149 22.704761 25.369963 0 0 Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG.
16 four_bit_resolution_sweep 448x252_color16_packed_zlib_2fps color16_packed_zlib 448 252 112896 2.000000 2.022472 4 0 16 42 618164 14718.190 14221.500 18022.000 18517 238137.014 238.137014 80.283660 157.853354 296.619529 23.011524 25.369963 16 48 Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette.
17 four_bit_resolution_sweep 480x270_gray8_jpeg_2fps gray8_jpeg 480 270 129600 2.000000 2.022472 8 256 0 42 393752 9375.048 9365.500 9919.950 10678 151686.164 151.686164 80.283660 71.402504 188.937779 13.823930 35.412999 0 0 Grayscale 8 bit, JPEG quality 40.
18 four_bit_resolution_sweep 480x270_gray4_jpeg_2fps gray4_jpeg 480 270 129600 2.000000 2.022472 4 16 0 42 443419 10557.595 10575.500 11152.550 11737 170819.518 170.819518 80.283660 90.535858 212.769969 12.275523 33.099458 0 0 Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40.
19 four_bit_resolution_sweep 480x270_gray4_packed_zlib_2fps gray4_packed_zlib 480 270 129600 2.000000 2.022472 4 16 0 42 854354 20341.762 20276.500 21857.900 25369 329125.136 329.125136 80.283660 248.841476 409.952830 6.371130 34.401420 16 0 Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header.
20 four_bit_resolution_sweep 480x270_color16_png_2fps color16_png 480 270 129600 2.000000 2.022472 4 0 16 42 701871 16711.214 16249.500 20469.850 21005 270383.692 270.383692 80.283660 190.100032 336.785458 23.265814 25.368870 0 0 Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG.
21 four_bit_resolution_sweep 480x270_color16_packed_zlib_2fps color16_packed_zlib 480 270 129600 2.000000 2.022472 4 0 16 42 693190 16504.524 16007.500 20249.750 20809 267039.486 267.039486 80.283660 186.755826 332.619971 23.557178 25.368870 16 48 Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette.
22 four_bit_resolution_sweep 560x315_gray8_jpeg_2fps gray8_jpeg 560 315 176400 2.000000 2.022472 8 256 0 42 507535 12084.167 12068.500 12860.900 13999 195519.101 195.519101 80.283660 115.235441 243.535361 14.597614 35.954946 0 0 Grayscale 8 bit, JPEG quality 40.
23 four_bit_resolution_sweep 560x315_gray4_jpeg_2fps gray4_jpeg 560 315 176400 2.000000 2.022472 4 16 0 42 575350 13698.810 13674.500 14558.450 15458 221643.660 221.643660 80.283660 141.360000 276.075679 12.877031 33.337152 0 0 Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40.
24 four_bit_resolution_sweep 560x315_gray4_packed_zlib_2fps gray4_packed_zlib 560 315 176400 2.000000 2.022472 4 16 0 42 1093024 26024.381 25851.500 28055.650 33171 421068.636 421.068636 80.283660 340.784976 524.476133 6.778259 34.403027 16 0 Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header.
25 four_bit_resolution_sweep 560x315_color16_png_2fps color16_png 560 315 176400 2.000000 2.022472 4 0 16 42 894967 21308.738 20851.500 25906.100 27345 344770.594 344.770594 80.283660 264.486934 429.440554 24.834882 25.323519 0 0 Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG.
26 four_bit_resolution_sweep 560x315_color16_packed_zlib_2fps color16_packed_zlib 560 315 176400 2.000000 2.022472 4 0 16 42 885156 21075.143 20622.000 25686.650 27166 340991.075 340.991075 80.283660 260.707415 424.732848 25.110150 25.323519 16 48 Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette.
27 four_bit_resolution_sweep 640x360_gray8_jpeg_2fps gray8_jpeg 640 360 230400 2.000000 2.022472 8 256 0 42 639531 15226.929 15187.500 16381.750 18122 246368.283 246.368283 80.283660 166.084623 306.872261 15.131088 35.588581 0 0 Grayscale 8 bit, JPEG quality 40.
28 four_bit_resolution_sweep 640x360_gray4_jpeg_2fps gray4_jpeg 640 360 230400 2.000000 2.022472 4 16 0 42 726335 17293.690 17220.500 18557.350 19938 279808.026 279.808026 80.283660 199.524366 348.524252 13.322778 33.048695 0 0 Grayscale quantized to 16 levels, restored to uint8, JPEG quality 40.
29 four_bit_resolution_sweep 640x360_gray4_packed_zlib_2fps gray4_packed_zlib 640 360 230400 2.000000 2.022472 4 16 0 42 1394769 33208.786 32974.500 35959.800 43514 537310.690 537.310690 80.283660 457.027030 669.265315 6.937923 34.404228 16 0 Two 4-bit grayscale indices per byte, zlib level 9, 16-byte frame header.
30 four_bit_resolution_sweep 640x360_color16_png_2fps color16_png 640 360 230400 2.000000 2.022472 4 0 16 42 1140488 27154.476 26267.500 33424.950 36017 439353.323 439.353323 80.283660 359.069663 547.251237 25.454367 25.259621 0 0 Pillow palette image, maximum 16 colors, no dithering, optimized indexed PNG.
31 four_bit_resolution_sweep 640x360_color16_packed_zlib_2fps color16_packed_zlib 640 360 230400 2.000000 2.022472 4 0 16 42 1128711 26874.071 25978.500 33021.450 35603 434816.437 434.816437 80.283660 354.532777 541.600167 25.719958 25.259621 16 48 Two 4-bit palette indices per byte, zlib level 9, 16-byte header and fixed 48-byte palette.

View File

@@ -0,0 +1,90 @@
Lab026B. Увеличение разрешения кадра за счёт глубины цвета 4 бита
Цель: проверить увеличение разрешения при битрейте, близком к контрольным 80.283660 кбит/с.
Исходное видео: data\raw\lab026_rover_source.mp4
Размер исходного видео: 7790985 байт
Исходное разрешение: 1280×720
Исходный FPS: 30.000000
Число кадров: 623
Длительность: 20.766667 с
4-битная яркость: индекс 0...15, то есть 16 уровней серого на пиксель.
4-битный индексированный цвет: один индекс 0...15 на пиксель и палитра максимум из 16 RGB-цветов.
Это не RGB444: RGB444 требует по 4 бита на каждый из трёх каналов, то есть 12 бит на пиксель.
Режимы:
gray8_jpeg — контрольный grayscale JPEG Q40.
gray4_jpeg — 16 уровней серого, восстановление uint8, JPEG Q40.
gray4_packed_zlib — 4-битные индексы, два на байт, zlib level 9 и заголовок 16 байт.
color16_png — палитра максимум 16 цветов, без дизеринга, индексированный PNG в памяти.
color16_packed_zlib — 4-битные индексы, zlib level 9, заголовок 16 байт и фиксированная палитра 48 байт.
Контрольный baseline: 80.283660 кбит/с.
CRC, FEC, радиозаголовки и пакетирование пока не учтены.
Статические метрики не учитывают задержку видеоканала.
Полные результаты 30 профилей:
320x180_gray8_jpeg_2fps: mode=gray8_jpeg, 320x180, frames=42, actual_fps=2.022472, total=208403 bytes, mean=4961.976, median=4965.000, p95=5224.700, max=5400, bitrate=80.283660 kbit/s, baseline_diff=-0.000000, baseline_percent=100.000%, compression=11.608x, PSNR=34.362114 dB
320x180_gray4_jpeg_2fps: mode=gray4_jpeg, 320x180, frames=42, actual_fps=2.022472, total=229518 bytes, mean=5464.714, median=5466.000, p95=5727.250, max=5822, bitrate=88.417849 kbit/s, baseline_diff=8.134189, baseline_percent=110.132%, compression=10.540x, PSNR=32.588941 dB
320x180_gray4_packed_zlib_2fps: mode=gray4_packed_zlib, 320x180, frames=42, actual_fps=2.022472, total=441074 bytes, mean=10501.762, median=10444.000, p95=11194.300, max=12317, bitrate=169.916148 kbit/s, baseline_diff=89.632488, baseline_percent=211.645%, compression=5.485x, PSNR=34.397012 dB
320x180_color16_png_2fps: mode=color16_png, 320x180, frames=42, actual_fps=2.022472, total=371520 bytes, mean=8845.714, median=8607.500, p95=10578.350, max=10794, bitrate=143.121669 kbit/s, baseline_diff=62.838009, baseline_percent=178.270%, compression=19.535x, PSNR=25.384724 dB
320x180_color16_packed_zlib_2fps: mode=color16_packed_zlib, 320x180, frames=42, actual_fps=2.022472, total=364892 bytes, mean=8687.905, median=8452.000, p95=10414.800, max=10652, bitrate=140.568347 kbit/s, baseline_diff=60.284687, baseline_percent=175.090%, compression=19.890x, PSNR=25.384724 dB
400x225_gray8_jpeg_2fps: mode=gray8_jpeg, 400x225, frames=42, actual_fps=2.022472, total=297273 bytes, mean=7077.929, median=7063.500, p95=7473.000, max=7841, bitrate=114.519294 kbit/s, baseline_diff=34.235634, baseline_percent=142.643%, compression=12.716x, PSNR=34.942886 dB
400x225_gray4_jpeg_2fps: mode=gray4_jpeg, 400x225, frames=42, actual_fps=2.022472, total=332268 bytes, mean=7911.143, median=7920.500, p95=8353.700, max=8554, bitrate=128.000514 kbit/s, baseline_diff=47.716854, baseline_percent=159.435%, compression=11.376x, PSNR=32.883090 dB
400x225_gray4_packed_zlib_2fps: mode=gray4_packed_zlib, 400x225, frames=42, actual_fps=2.022472, total=636862 bytes, mean=15163.381, median=15120.500, p95=16266.050, max=18356, bitrate=245.340096 kbit/s, baseline_diff=165.056436, baseline_percent=305.592%, compression=5.935x, PSNR=34.399552 dB
400x225_color16_png_2fps: mode=color16_png, 400x225, frames=42, actual_fps=2.022472, total=528261 bytes, mean=12577.643, median=12153.000, p95=15191.650, max=16128, bitrate=203.503435 kbit/s, baseline_diff=123.219775, baseline_percent=253.481%, compression=21.467x, PSNR=25.344339 dB
400x225_color16_packed_zlib_2fps: mode=color16_packed_zlib, 400x225, frames=42, actual_fps=2.022472, total=520725 bytes, mean=12398.214, median=11977.000, p95=15015.100, max=15966, bitrate=200.600321 kbit/s, baseline_diff=120.316661, baseline_percent=249.864%, compression=21.777x, PSNR=25.344339 dB
448x252_gray8_jpeg_2fps: mode=gray8_jpeg, 448x252, frames=42, actual_fps=2.022472, total=353578 bytes, mean=8418.524, median=8413.000, p95=8885.950, max=9512, bitrate=136.209823 kbit/s, baseline_diff=55.926163, baseline_percent=169.661%, compression=13.410x, PSNR=35.301071 dB
448x252_gray4_jpeg_2fps: mode=gray4_jpeg, 448x252, frames=42, actual_fps=2.022472, total=397126 bytes, mean=9455.381, median=9467.500, p95=9996.400, max=10450, bitrate=152.985939 kbit/s, baseline_diff=72.702279, baseline_percent=190.557%, compression=11.940x, PSNR=33.060439 dB
448x252_gray4_packed_zlib_2fps: mode=gray4_packed_zlib, 448x252, frames=42, actual_fps=2.022472, total=762082 bytes, mean=18144.810, median=18111.500, p95=19504.750, max=22433, bitrate=293.578941 kbit/s, baseline_diff=213.295281, baseline_percent=365.677%, compression=6.222x, PSNR=34.399841 dB
448x252_color16_png_2fps: mode=color16_png, 448x252, frames=42, actual_fps=2.022472, total=626516 bytes, mean=14917.048, median=14409.500, p95=18239.600, max=18695, bitrate=241.354478 kbit/s, baseline_diff=161.070818, baseline_percent=300.627%, compression=22.705x, PSNR=25.369963 dB
448x252_color16_packed_zlib_2fps: mode=color16_packed_zlib, 448x252, frames=42, actual_fps=2.022472, total=618164 bytes, mean=14718.190, median=14221.500, p95=18022.000, max=18517, bitrate=238.137014 kbit/s, baseline_diff=157.853354, baseline_percent=296.620%, compression=23.012x, PSNR=25.369963 dB
480x270_gray8_jpeg_2fps: mode=gray8_jpeg, 480x270, frames=42, actual_fps=2.022472, total=393752 bytes, mean=9375.048, median=9365.500, p95=9919.950, max=10678, bitrate=151.686164 kbit/s, baseline_diff=71.402504, baseline_percent=188.938%, compression=13.824x, PSNR=35.412999 dB
480x270_gray4_jpeg_2fps: mode=gray4_jpeg, 480x270, frames=42, actual_fps=2.022472, total=443419 bytes, mean=10557.595, median=10575.500, p95=11152.550, max=11737, bitrate=170.819518 kbit/s, baseline_diff=90.535858, baseline_percent=212.770%, compression=12.276x, PSNR=33.099458 dB
480x270_gray4_packed_zlib_2fps: mode=gray4_packed_zlib, 480x270, frames=42, actual_fps=2.022472, total=854354 bytes, mean=20341.762, median=20276.500, p95=21857.900, max=25369, bitrate=329.125136 kbit/s, baseline_diff=248.841476, baseline_percent=409.953%, compression=6.371x, PSNR=34.401420 dB
480x270_color16_png_2fps: mode=color16_png, 480x270, frames=42, actual_fps=2.022472, total=701871 bytes, mean=16711.214, median=16249.500, p95=20469.850, max=21005, bitrate=270.383692 kbit/s, baseline_diff=190.100032, baseline_percent=336.785%, compression=23.266x, PSNR=25.368870 dB
480x270_color16_packed_zlib_2fps: mode=color16_packed_zlib, 480x270, frames=42, actual_fps=2.022472, total=693190 bytes, mean=16504.524, median=16007.500, p95=20249.750, max=20809, bitrate=267.039486 kbit/s, baseline_diff=186.755826, baseline_percent=332.620%, compression=23.557x, PSNR=25.368870 dB
560x315_gray8_jpeg_2fps: mode=gray8_jpeg, 560x315, frames=42, actual_fps=2.022472, total=507535 bytes, mean=12084.167, median=12068.500, p95=12860.900, max=13999, bitrate=195.519101 kbit/s, baseline_diff=115.235441, baseline_percent=243.535%, compression=14.598x, PSNR=35.954946 dB
560x315_gray4_jpeg_2fps: mode=gray4_jpeg, 560x315, frames=42, actual_fps=2.022472, total=575350 bytes, mean=13698.810, median=13674.500, p95=14558.450, max=15458, bitrate=221.643660 kbit/s, baseline_diff=141.360000, baseline_percent=276.076%, compression=12.877x, PSNR=33.337152 dB
560x315_gray4_packed_zlib_2fps: mode=gray4_packed_zlib, 560x315, frames=42, actual_fps=2.022472, total=1093024 bytes, mean=26024.381, median=25851.500, p95=28055.650, max=33171, bitrate=421.068636 kbit/s, baseline_diff=340.784976, baseline_percent=524.476%, compression=6.778x, PSNR=34.403027 dB
560x315_color16_png_2fps: mode=color16_png, 560x315, frames=42, actual_fps=2.022472, total=894967 bytes, mean=21308.738, median=20851.500, p95=25906.100, max=27345, bitrate=344.770594 kbit/s, baseline_diff=264.486934, baseline_percent=429.441%, compression=24.835x, PSNR=25.323519 dB
560x315_color16_packed_zlib_2fps: mode=color16_packed_zlib, 560x315, frames=42, actual_fps=2.022472, total=885156 bytes, mean=21075.143, median=20622.000, p95=25686.650, max=27166, bitrate=340.991075 kbit/s, baseline_diff=260.707415, baseline_percent=424.733%, compression=25.110x, PSNR=25.323519 dB
640x360_gray8_jpeg_2fps: mode=gray8_jpeg, 640x360, frames=42, actual_fps=2.022472, total=639531 bytes, mean=15226.929, median=15187.500, p95=16381.750, max=18122, bitrate=246.368283 kbit/s, baseline_diff=166.084623, baseline_percent=306.872%, compression=15.131x, PSNR=35.588581 dB
640x360_gray4_jpeg_2fps: mode=gray4_jpeg, 640x360, frames=42, actual_fps=2.022472, total=726335 bytes, mean=17293.690, median=17220.500, p95=18557.350, max=19938, bitrate=279.808026 kbit/s, baseline_diff=199.524366, baseline_percent=348.524%, compression=13.323x, PSNR=33.048695 dB
640x360_gray4_packed_zlib_2fps: mode=gray4_packed_zlib, 640x360, frames=42, actual_fps=2.022472, total=1394769 bytes, mean=33208.786, median=32974.500, p95=35959.800, max=43514, bitrate=537.310690 kbit/s, baseline_diff=457.027030, baseline_percent=669.265%, compression=6.938x, PSNR=34.404228 dB
640x360_color16_png_2fps: mode=color16_png, 640x360, frames=42, actual_fps=2.022472, total=1140488 bytes, mean=27154.476, median=26267.500, p95=33424.950, max=36017, bitrate=439.353323 kbit/s, baseline_diff=359.069663, baseline_percent=547.251%, compression=25.454x, PSNR=25.259621 dB
640x360_color16_packed_zlib_2fps: mode=color16_packed_zlib, 640x360, frames=42, actual_fps=2.022472, total=1128711 bytes, mean=26874.071, median=25978.500, p95=33021.450, max=35603, bitrate=434.816437 kbit/s, baseline_diff=354.532777, baseline_percent=541.600%, compression=25.720x, PSNR=25.259621 dB
Профили 7090 кбит/с:
320x180_gray8_jpeg_2fps: mode=gray8_jpeg, 320x180, frames=42, actual_fps=2.022472, total=208403 bytes, mean=4961.976, median=4965.000, p95=5224.700, max=5400, bitrate=80.283660 kbit/s, baseline_diff=-0.000000, baseline_percent=100.000%, compression=11.608x, PSNR=34.362114 dB
320x180_gray4_jpeg_2fps: mode=gray4_jpeg, 320x180, frames=42, actual_fps=2.022472, total=229518 bytes, mean=5464.714, median=5466.000, p95=5727.250, max=5822, bitrate=88.417849 kbit/s, baseline_diff=8.134189, baseline_percent=110.132%, compression=10.540x, PSNR=32.588941 dB
Профили до baseline:
320x180_gray8_jpeg_2fps: mode=gray8_jpeg, 320x180, frames=42, actual_fps=2.022472, total=208403 bytes, mean=4961.976, median=4965.000, p95=5224.700, max=5400, bitrate=80.283660 kbit/s, baseline_diff=-0.000000, baseline_percent=100.000%, compression=11.608x, PSNR=34.362114 dB
Профили до 100 кбит/с:
320x180_gray8_jpeg_2fps: mode=gray8_jpeg, 320x180, frames=42, actual_fps=2.022472, total=208403 bytes, mean=4961.976, median=4965.000, p95=5224.700, max=5400, bitrate=80.283660 kbit/s, baseline_diff=-0.000000, baseline_percent=100.000%, compression=11.608x, PSNR=34.362114 dB
320x180_gray4_jpeg_2fps: mode=gray4_jpeg, 320x180, frames=42, actual_fps=2.022472, total=229518 bytes, mean=5464.714, median=5466.000, p95=5727.250, max=5822, bitrate=88.417849 kbit/s, baseline_diff=8.134189, baseline_percent=110.132%, compression=10.540x, PSNR=32.588941 dB
Цветные color16-профили до 100 кбит/с:
Нет.
Профили больше 320x180 и не выше baseline:
Нет.
Максимальное разрешение до baseline:
320x180_gray8_jpeg_2fps: mode=gray8_jpeg, 320x180, frames=42, actual_fps=2.022472, total=208403 bytes, mean=4961.976, median=4965.000, p95=5224.700, max=5400, bitrate=80.283660 kbit/s, baseline_diff=-0.000000, baseline_percent=100.000%, compression=11.608x, PSNR=34.362114 dB
Максимальное разрешение до 100 кбит/с:
320x180_gray8_jpeg_2fps: mode=gray8_jpeg, 320x180, frames=42, actual_fps=2.022472, total=208403 bytes, mean=4961.976, median=4965.000, p95=5224.700, max=5400, bitrate=80.283660 kbit/s, baseline_diff=-0.000000, baseline_percent=100.000%, compression=11.608x, PSNR=34.362114 dB
320x180_gray4_jpeg_2fps: mode=gray4_jpeg, 320x180, frames=42, actual_fps=2.022472, total=229518 bytes, mean=5464.714, median=5466.000, p95=5727.250, max=5822, bitrate=88.417849 kbit/s, baseline_diff=8.134189, baseline_percent=110.132%, compression=10.540x, PSNR=32.588941 dB
Сравнение gray4 и color16:
Минимальный gray4 bitrate: 88.417849 кбит/с.
Минимальный color16 bitrate: 140.568347 кбит/с.
Размер палитры packed color16: 48 байт на кадр.
Размер компактного заголовка packed-режимов: 16 байт на кадр.
Нельзя автоматически объявлять профиль безопасным для управления ровером по одной статической метрике.
Сравнительное изображение: data\processed\lab026b\lab026b_equal_bitrate_candidates.png
Следующий шаг: ручная визуальная оценка пользователем.

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,10 @@
profile_name,base_width,base_height,base_fps,base_quality,roi_enabled,roi_x_min,roi_y_min,roi_x_max,roi_y_max,roi_width,roi_height,roi_fps,roi_quality,source_duration_s,selected_base_frames,selected_roi_frames,actual_base_fps,actual_roi_fps,total_base_bytes,total_roi_bytes,total_payload_bytes,mean_base_frame_bytes,mean_roi_frame_bytes,p95_base_frame_bytes,p95_roi_frame_bytes,max_base_frame_bytes,max_roi_frame_bytes,base_bitrate_kbps,roi_bitrate_kbps,total_payload_bitrate_kbps,baseline_bitrate_kbps,difference_from_baseline_kbps,percent_of_baseline,mean_base_jpeg_psnr_db,mean_roi_jpeg_psnr_db,mean_reconstructed_full_psnr_db,mean_reconstructed_roi_psnr_db
baseline_320x180_gray_2fps_q40,320,180,2.000000,40,0,0.000000,0.000000,0.000000,0.000000,0,0,0.000000,0,20.766667,42,0,2.022472,0.000000,208368,0,208368,4961.143,0.000,5226.950,0.000,5390,0,80.270177,0.000000,80.270177,80.283660,-0.013483,99.983205,34.364165,,20.757991,
base240_1fps_q25_roi320_2fps_q35,240,135,1.000000,25,1,0.200000,0.420000,0.800000,1.000000,320,180,2.000000,35,20.766667,21,42,1.011236,2.022472,50858,191270,242128,2421.810,4554.048,2554.000,5044.500,2569,5172,19.592167,73.683467,93.275634,80.283660,12.991974,116.182588,32.370927,34.103745,19.657187,19.574377
base240_0_5fps_q25_roi320_2fps_q35,240,135,0.500000,25,1,0.200000,0.420000,0.800000,1.000000,320,180,2.000000,35,20.766667,11,42,0.529695,2.022472,26348,191270,217618,2395.273,4554.048,2561.500,5044.500,2569,5172,10.150112,73.683467,83.833579,80.283660,3.549919,104.421721,32.474572,34.103745,18.613817,19.574377
base160_1fps_q25_roi320_2fps_q35,160,90,1.000000,25,1,0.200000,0.420000,0.800000,1.000000,320,180,2.000000,35,20.766667,21,42,1.011236,2.022472,29882,191270,221152,1422.952,4554.048,1484.000,5044.500,1492,5172,11.511525,73.683467,85.194992,80.283660,4.911332,106.117474,31.458773,34.103745,19.701304,19.574377
base240_1fps_q25_roi448_1fps_q30,240,135,1.000000,25,1,0.200000,0.420000,0.800000,1.000000,448,252,1.000000,30,20.766667,21,21,1.011236,1.011236,50858,147787,198645,2421.810,7037.476,2554.000,7860.000,2569,8507,19.592167,56.932392,76.524559,80.283660,-3.759101,95.317725,32.370927,34.666932,19.098790,17.871555
base240_0_5fps_q25_roi448_1fps_q30,240,135,0.500000,25,1,0.200000,0.420000,0.800000,1.000000,448,252,1.000000,30,20.766667,11,21,0.529695,1.011236,26348,147787,174135,2395.273,7037.476,2561.500,7860.000,2569,8507,10.150112,56.932392,67.082504,80.283660,-13.201156,83.556858,32.474572,34.666932,18.113877,17.871555
base320_1fps_q30_roi320_1fps_q35,320,180,1.000000,30,1,0.200000,0.420000,0.800000,1.000000,320,180,1.000000,35,20.766667,21,21,1.011236,1.011236,87901,95367,183268,4185.762,4541.286,4391.000,5048.000,4537,5172,33.862343,36.738491,70.600835,80.283660,-9.682825,87.939233,33.539230,34.103514,19.121457,17.888190
base240_1fps_q20_roi320_1fps_q30,240,135,1.000000,20,1,0.200000,0.420000,0.800000,1.000000,320,180,1.000000,30,20.766667,21,21,1.011236,1.011236,44598,86458,131056,2123.714,4117.048,2231.000,4551.000,2251,4614,17.180610,33.306453,50.487063,80.283660,-29.796597,62.885851,31.643522,33.598412,19.107784,17.873870
base160_0_5fps_q20_roi320_2fps_q30,160,90,0.500000,20,1,0.200000,0.420000,0.800000,1.000000,320,180,2.000000,30,20.766667,11,42,0.529695,2.022472,13820,173455,187275,1256.364,4129.881,1326.000,4548.450,1347,4614,5.323917,66.820546,72.144462,80.283660,-8.139198,89.861950,30.823518,33.595566,18.664976,19.549559
1 profile_name base_width base_height base_fps base_quality roi_enabled roi_x_min roi_y_min roi_x_max roi_y_max roi_width roi_height roi_fps roi_quality source_duration_s selected_base_frames selected_roi_frames actual_base_fps actual_roi_fps total_base_bytes total_roi_bytes total_payload_bytes mean_base_frame_bytes mean_roi_frame_bytes p95_base_frame_bytes p95_roi_frame_bytes max_base_frame_bytes max_roi_frame_bytes base_bitrate_kbps roi_bitrate_kbps total_payload_bitrate_kbps baseline_bitrate_kbps difference_from_baseline_kbps percent_of_baseline mean_base_jpeg_psnr_db mean_roi_jpeg_psnr_db mean_reconstructed_full_psnr_db mean_reconstructed_roi_psnr_db
2 baseline_320x180_gray_2fps_q40 320 180 2.000000 40 0 0.000000 0.000000 0.000000 0.000000 0 0 0.000000 0 20.766667 42 0 2.022472 0.000000 208368 0 208368 4961.143 0.000 5226.950 0.000 5390 0 80.270177 0.000000 80.270177 80.283660 -0.013483 99.983205 34.364165 20.757991
3 base240_1fps_q25_roi320_2fps_q35 240 135 1.000000 25 1 0.200000 0.420000 0.800000 1.000000 320 180 2.000000 35 20.766667 21 42 1.011236 2.022472 50858 191270 242128 2421.810 4554.048 2554.000 5044.500 2569 5172 19.592167 73.683467 93.275634 80.283660 12.991974 116.182588 32.370927 34.103745 19.657187 19.574377
4 base240_0_5fps_q25_roi320_2fps_q35 240 135 0.500000 25 1 0.200000 0.420000 0.800000 1.000000 320 180 2.000000 35 20.766667 11 42 0.529695 2.022472 26348 191270 217618 2395.273 4554.048 2561.500 5044.500 2569 5172 10.150112 73.683467 83.833579 80.283660 3.549919 104.421721 32.474572 34.103745 18.613817 19.574377
5 base160_1fps_q25_roi320_2fps_q35 160 90 1.000000 25 1 0.200000 0.420000 0.800000 1.000000 320 180 2.000000 35 20.766667 21 42 1.011236 2.022472 29882 191270 221152 1422.952 4554.048 1484.000 5044.500 1492 5172 11.511525 73.683467 85.194992 80.283660 4.911332 106.117474 31.458773 34.103745 19.701304 19.574377
6 base240_1fps_q25_roi448_1fps_q30 240 135 1.000000 25 1 0.200000 0.420000 0.800000 1.000000 448 252 1.000000 30 20.766667 21 21 1.011236 1.011236 50858 147787 198645 2421.810 7037.476 2554.000 7860.000 2569 8507 19.592167 56.932392 76.524559 80.283660 -3.759101 95.317725 32.370927 34.666932 19.098790 17.871555
7 base240_0_5fps_q25_roi448_1fps_q30 240 135 0.500000 25 1 0.200000 0.420000 0.800000 1.000000 448 252 1.000000 30 20.766667 11 21 0.529695 1.011236 26348 147787 174135 2395.273 7037.476 2561.500 7860.000 2569 8507 10.150112 56.932392 67.082504 80.283660 -13.201156 83.556858 32.474572 34.666932 18.113877 17.871555
8 base320_1fps_q30_roi320_1fps_q35 320 180 1.000000 30 1 0.200000 0.420000 0.800000 1.000000 320 180 1.000000 35 20.766667 21 21 1.011236 1.011236 87901 95367 183268 4185.762 4541.286 4391.000 5048.000 4537 5172 33.862343 36.738491 70.600835 80.283660 -9.682825 87.939233 33.539230 34.103514 19.121457 17.888190
9 base240_1fps_q20_roi320_1fps_q30 240 135 1.000000 20 1 0.200000 0.420000 0.800000 1.000000 320 180 1.000000 30 20.766667 21 21 1.011236 1.011236 44598 86458 131056 2123.714 4117.048 2231.000 4551.000 2251 4614 17.180610 33.306453 50.487063 80.283660 -29.796597 62.885851 31.643522 33.598412 19.107784 17.873870
10 base160_0_5fps_q20_roi320_2fps_q30 160 90 0.500000 20 1 0.200000 0.420000 0.800000 1.000000 320 180 2.000000 30 20.766667 11 42 0.529695 2.022472 13820 173455 187275 1256.364 4129.881 1326.000 4548.450 1347 4614 5.323917 66.820546 72.144462 80.283660 -8.139198 89.861950 30.823518 33.595566 18.664976 19.549559

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -0,0 +1,74 @@
Lab027. Многомасштабная передача изображения: общий кадр и область интереса
Цель: проверить совместную передачу общего кадра и фиксированной ROI при payload около 80100 кбит/с.
Исходное видео: data\raw\lab026_rover_source.mp4
Размер файла: 7790985 байт
Исходное разрешение: 1280×720
Исходный FPS: 30.000000
Число кадров: 623
Длительность: 20.766667 с
BASE: полный grayscale-кадр пониженного разрешения с собственной частотой и JPEG quality.
ROI: фиксированная центральная нижняя область исходного grayscale-кадра с независимыми разрешением, частотой и JPEG quality.
Нормализованные координаты ROI: x=0.20...0.80, y=0.42...1.00.
Пиксельные координаты ROI исходного кадра: x=256...1024, y=302...720.
Контрольный профиль: 320×180 grayscale, 2 fps, JPEG Q40.
Контрольный baseline: 80.283660 кбит/с.
Результаты всех девяти профилей:
baseline_320x180_gray_2fps_q40: BASE=320x180, 2.000 fps, Q40, base_frames=42, base=80.270177 kbit/s; ROI=0x0, 0.000 fps, Q0, roi_frames=0, roi=0.000000 kbit/s; total=80.270177 kbit/s, baseline_diff=-0.013483, baseline_percent=99.983%, BASE_JPEG_PSNR=34.364165 dB, ROI_JPEG_PSNR=нет dB, full_reconstructed_PSNR=20.757991 dB, ROI_reconstructed_PSNR=нет dB
base240_1fps_q25_roi320_2fps_q35: BASE=240x135, 1.000 fps, Q25, base_frames=21, base=19.592167 kbit/s; ROI=320x180, 2.000 fps, Q35, roi_frames=42, roi=73.683467 kbit/s; total=93.275634 kbit/s, baseline_diff=12.991974, baseline_percent=116.183%, BASE_JPEG_PSNR=32.370927 dB, ROI_JPEG_PSNR=34.103745 dB, full_reconstructed_PSNR=19.657187 dB, ROI_reconstructed_PSNR=19.574377 dB
base240_0_5fps_q25_roi320_2fps_q35: BASE=240x135, 0.500 fps, Q25, base_frames=11, base=10.150112 kbit/s; ROI=320x180, 2.000 fps, Q35, roi_frames=42, roi=73.683467 kbit/s; total=83.833579 kbit/s, baseline_diff=3.549919, baseline_percent=104.422%, BASE_JPEG_PSNR=32.474572 dB, ROI_JPEG_PSNR=34.103745 dB, full_reconstructed_PSNR=18.613817 dB, ROI_reconstructed_PSNR=19.574377 dB
base160_1fps_q25_roi320_2fps_q35: BASE=160x90, 1.000 fps, Q25, base_frames=21, base=11.511525 kbit/s; ROI=320x180, 2.000 fps, Q35, roi_frames=42, roi=73.683467 kbit/s; total=85.194992 kbit/s, baseline_diff=4.911332, baseline_percent=106.117%, BASE_JPEG_PSNR=31.458773 dB, ROI_JPEG_PSNR=34.103745 dB, full_reconstructed_PSNR=19.701304 dB, ROI_reconstructed_PSNR=19.574377 dB
base240_1fps_q25_roi448_1fps_q30: BASE=240x135, 1.000 fps, Q25, base_frames=21, base=19.592167 kbit/s; ROI=448x252, 1.000 fps, Q30, roi_frames=21, roi=56.932392 kbit/s; total=76.524559 kbit/s, baseline_diff=-3.759101, baseline_percent=95.318%, BASE_JPEG_PSNR=32.370927 dB, ROI_JPEG_PSNR=34.666932 dB, full_reconstructed_PSNR=19.098790 dB, ROI_reconstructed_PSNR=17.871555 dB
base240_0_5fps_q25_roi448_1fps_q30: BASE=240x135, 0.500 fps, Q25, base_frames=11, base=10.150112 kbit/s; ROI=448x252, 1.000 fps, Q30, roi_frames=21, roi=56.932392 kbit/s; total=67.082504 kbit/s, baseline_diff=-13.201156, baseline_percent=83.557%, BASE_JPEG_PSNR=32.474572 dB, ROI_JPEG_PSNR=34.666932 dB, full_reconstructed_PSNR=18.113877 dB, ROI_reconstructed_PSNR=17.871555 dB
base320_1fps_q30_roi320_1fps_q35: BASE=320x180, 1.000 fps, Q30, base_frames=21, base=33.862343 kbit/s; ROI=320x180, 1.000 fps, Q35, roi_frames=21, roi=36.738491 kbit/s; total=70.600835 kbit/s, baseline_diff=-9.682825, baseline_percent=87.939%, BASE_JPEG_PSNR=33.539230 dB, ROI_JPEG_PSNR=34.103514 dB, full_reconstructed_PSNR=19.121457 dB, ROI_reconstructed_PSNR=17.888190 dB
base240_1fps_q20_roi320_1fps_q30: BASE=240x135, 1.000 fps, Q20, base_frames=21, base=17.180610 kbit/s; ROI=320x180, 1.000 fps, Q30, roi_frames=21, roi=33.306453 kbit/s; total=50.487063 kbit/s, baseline_diff=-29.796597, baseline_percent=62.886%, BASE_JPEG_PSNR=31.643522 dB, ROI_JPEG_PSNR=33.598412 dB, full_reconstructed_PSNR=19.107784 dB, ROI_reconstructed_PSNR=17.873870 dB
base160_0_5fps_q20_roi320_2fps_q30: BASE=160x90, 0.500 fps, Q20, base_frames=11, base=5.323917 kbit/s; ROI=320x180, 2.000 fps, Q30, roi_frames=42, roi=66.820546 kbit/s; total=72.144462 kbit/s, baseline_diff=-8.139198, baseline_percent=89.862%, BASE_JPEG_PSNR=30.823518 dB, ROI_JPEG_PSNR=33.595566 dB, full_reconstructed_PSNR=18.664976 dB, ROI_reconstructed_PSNR=19.549559 dB
Профили до 80.283660 кбит/с:
baseline_320x180_gray_2fps_q40: BASE=320x180, 2.000 fps, Q40, base_frames=42, base=80.270177 kbit/s; ROI=0x0, 0.000 fps, Q0, roi_frames=0, roi=0.000000 kbit/s; total=80.270177 kbit/s, baseline_diff=-0.013483, baseline_percent=99.983%, BASE_JPEG_PSNR=34.364165 dB, ROI_JPEG_PSNR=нет dB, full_reconstructed_PSNR=20.757991 dB, ROI_reconstructed_PSNR=нет dB
base240_1fps_q25_roi448_1fps_q30: BASE=240x135, 1.000 fps, Q25, base_frames=21, base=19.592167 kbit/s; ROI=448x252, 1.000 fps, Q30, roi_frames=21, roi=56.932392 kbit/s; total=76.524559 kbit/s, baseline_diff=-3.759101, baseline_percent=95.318%, BASE_JPEG_PSNR=32.370927 dB, ROI_JPEG_PSNR=34.666932 dB, full_reconstructed_PSNR=19.098790 dB, ROI_reconstructed_PSNR=17.871555 dB
base240_0_5fps_q25_roi448_1fps_q30: BASE=240x135, 0.500 fps, Q25, base_frames=11, base=10.150112 kbit/s; ROI=448x252, 1.000 fps, Q30, roi_frames=21, roi=56.932392 kbit/s; total=67.082504 kbit/s, baseline_diff=-13.201156, baseline_percent=83.557%, BASE_JPEG_PSNR=32.474572 dB, ROI_JPEG_PSNR=34.666932 dB, full_reconstructed_PSNR=18.113877 dB, ROI_reconstructed_PSNR=17.871555 dB
base320_1fps_q30_roi320_1fps_q35: BASE=320x180, 1.000 fps, Q30, base_frames=21, base=33.862343 kbit/s; ROI=320x180, 1.000 fps, Q35, roi_frames=21, roi=36.738491 kbit/s; total=70.600835 kbit/s, baseline_diff=-9.682825, baseline_percent=87.939%, BASE_JPEG_PSNR=33.539230 dB, ROI_JPEG_PSNR=34.103514 dB, full_reconstructed_PSNR=19.121457 dB, ROI_reconstructed_PSNR=17.888190 dB
base240_1fps_q20_roi320_1fps_q30: BASE=240x135, 1.000 fps, Q20, base_frames=21, base=17.180610 kbit/s; ROI=320x180, 1.000 fps, Q30, roi_frames=21, roi=33.306453 kbit/s; total=50.487063 kbit/s, baseline_diff=-29.796597, baseline_percent=62.886%, BASE_JPEG_PSNR=31.643522 dB, ROI_JPEG_PSNR=33.598412 dB, full_reconstructed_PSNR=19.107784 dB, ROI_reconstructed_PSNR=17.873870 dB
base160_0_5fps_q20_roi320_2fps_q30: BASE=160x90, 0.500 fps, Q20, base_frames=11, base=5.323917 kbit/s; ROI=320x180, 2.000 fps, Q30, roi_frames=42, roi=66.820546 kbit/s; total=72.144462 kbit/s, baseline_diff=-8.139198, baseline_percent=89.862%, BASE_JPEG_PSNR=30.823518 dB, ROI_JPEG_PSNR=33.595566 dB, full_reconstructed_PSNR=18.664976 dB, ROI_reconstructed_PSNR=19.549559 dB
Профили до 100 кбит/с:
baseline_320x180_gray_2fps_q40: BASE=320x180, 2.000 fps, Q40, base_frames=42, base=80.270177 kbit/s; ROI=0x0, 0.000 fps, Q0, roi_frames=0, roi=0.000000 kbit/s; total=80.270177 kbit/s, baseline_diff=-0.013483, baseline_percent=99.983%, BASE_JPEG_PSNR=34.364165 dB, ROI_JPEG_PSNR=нет dB, full_reconstructed_PSNR=20.757991 dB, ROI_reconstructed_PSNR=нет dB
base240_1fps_q25_roi320_2fps_q35: BASE=240x135, 1.000 fps, Q25, base_frames=21, base=19.592167 kbit/s; ROI=320x180, 2.000 fps, Q35, roi_frames=42, roi=73.683467 kbit/s; total=93.275634 kbit/s, baseline_diff=12.991974, baseline_percent=116.183%, BASE_JPEG_PSNR=32.370927 dB, ROI_JPEG_PSNR=34.103745 dB, full_reconstructed_PSNR=19.657187 dB, ROI_reconstructed_PSNR=19.574377 dB
base240_0_5fps_q25_roi320_2fps_q35: BASE=240x135, 0.500 fps, Q25, base_frames=11, base=10.150112 kbit/s; ROI=320x180, 2.000 fps, Q35, roi_frames=42, roi=73.683467 kbit/s; total=83.833579 kbit/s, baseline_diff=3.549919, baseline_percent=104.422%, BASE_JPEG_PSNR=32.474572 dB, ROI_JPEG_PSNR=34.103745 dB, full_reconstructed_PSNR=18.613817 dB, ROI_reconstructed_PSNR=19.574377 dB
base160_1fps_q25_roi320_2fps_q35: BASE=160x90, 1.000 fps, Q25, base_frames=21, base=11.511525 kbit/s; ROI=320x180, 2.000 fps, Q35, roi_frames=42, roi=73.683467 kbit/s; total=85.194992 kbit/s, baseline_diff=4.911332, baseline_percent=106.117%, BASE_JPEG_PSNR=31.458773 dB, ROI_JPEG_PSNR=34.103745 dB, full_reconstructed_PSNR=19.701304 dB, ROI_reconstructed_PSNR=19.574377 dB
base240_1fps_q25_roi448_1fps_q30: BASE=240x135, 1.000 fps, Q25, base_frames=21, base=19.592167 kbit/s; ROI=448x252, 1.000 fps, Q30, roi_frames=21, roi=56.932392 kbit/s; total=76.524559 kbit/s, baseline_diff=-3.759101, baseline_percent=95.318%, BASE_JPEG_PSNR=32.370927 dB, ROI_JPEG_PSNR=34.666932 dB, full_reconstructed_PSNR=19.098790 dB, ROI_reconstructed_PSNR=17.871555 dB
base240_0_5fps_q25_roi448_1fps_q30: BASE=240x135, 0.500 fps, Q25, base_frames=11, base=10.150112 kbit/s; ROI=448x252, 1.000 fps, Q30, roi_frames=21, roi=56.932392 kbit/s; total=67.082504 kbit/s, baseline_diff=-13.201156, baseline_percent=83.557%, BASE_JPEG_PSNR=32.474572 dB, ROI_JPEG_PSNR=34.666932 dB, full_reconstructed_PSNR=18.113877 dB, ROI_reconstructed_PSNR=17.871555 dB
base320_1fps_q30_roi320_1fps_q35: BASE=320x180, 1.000 fps, Q30, base_frames=21, base=33.862343 kbit/s; ROI=320x180, 1.000 fps, Q35, roi_frames=21, roi=36.738491 kbit/s; total=70.600835 kbit/s, baseline_diff=-9.682825, baseline_percent=87.939%, BASE_JPEG_PSNR=33.539230 dB, ROI_JPEG_PSNR=34.103514 dB, full_reconstructed_PSNR=19.121457 dB, ROI_reconstructed_PSNR=17.888190 dB
base240_1fps_q20_roi320_1fps_q30: BASE=240x135, 1.000 fps, Q20, base_frames=21, base=17.180610 kbit/s; ROI=320x180, 1.000 fps, Q30, roi_frames=21, roi=33.306453 kbit/s; total=50.487063 kbit/s, baseline_diff=-29.796597, baseline_percent=62.886%, BASE_JPEG_PSNR=31.643522 dB, ROI_JPEG_PSNR=33.598412 dB, full_reconstructed_PSNR=19.107784 dB, ROI_reconstructed_PSNR=17.873870 dB
base160_0_5fps_q20_roi320_2fps_q30: BASE=160x90, 0.500 fps, Q20, base_frames=11, base=5.323917 kbit/s; ROI=320x180, 2.000 fps, Q30, roi_frames=42, roi=66.820546 kbit/s; total=72.144462 kbit/s, baseline_diff=-8.139198, baseline_percent=89.862%, BASE_JPEG_PSNR=30.823518 dB, ROI_JPEG_PSNR=33.595566 dB, full_reconstructed_PSNR=18.664976 dB, ROI_reconstructed_PSNR=19.549559 dB
Профиль с максимальным ROI-разрешением до 100 кбит/с:
base240_1fps_q25_roi448_1fps_q30: BASE=240x135, 1.000 fps, Q25, base_frames=21, base=19.592167 kbit/s; ROI=448x252, 1.000 fps, Q30, roi_frames=21, roi=56.932392 kbit/s; total=76.524559 kbit/s, baseline_diff=-3.759101, baseline_percent=95.318%, BASE_JPEG_PSNR=32.370927 dB, ROI_JPEG_PSNR=34.666932 dB, full_reconstructed_PSNR=19.098790 dB, ROI_reconstructed_PSNR=17.871555 dB
Профиль с максимальным ROI PSNR до 100 кбит/с:
base240_1fps_q25_roi320_2fps_q35: BASE=240x135, 1.000 fps, Q25, base_frames=21, base=19.592167 kbit/s; ROI=320x180, 2.000 fps, Q35, roi_frames=42, roi=73.683467 kbit/s; total=93.275634 kbit/s, baseline_diff=12.991974, baseline_percent=116.183%, BASE_JPEG_PSNR=32.370927 dB, ROI_JPEG_PSNR=34.103745 dB, full_reconstructed_PSNR=19.657187 dB, ROI_reconstructed_PSNR=19.574377 dB
Профиль с максимальным full-frame PSNR до 100 кбит/с:
baseline_320x180_gray_2fps_q40: BASE=320x180, 2.000 fps, Q40, base_frames=42, base=80.270177 kbit/s; ROI=0x0, 0.000 fps, Q0, roi_frames=0, roi=0.000000 kbit/s; total=80.270177 kbit/s, baseline_diff=-0.013483, baseline_percent=99.983%, BASE_JPEG_PSNR=34.364165 dB, ROI_JPEG_PSNR=нет dB, full_reconstructed_PSNR=20.757991 dB, ROI_reconstructed_PSNR=нет dB
Разделение битрейта BASE/ROI:
baseline_320x180_gray_2fps_q40: BASE=80.270177 кбит/с, ROI=0.000000 кбит/с, total=80.270177 кбит/с.
base240_1fps_q25_roi320_2fps_q35: BASE=19.592167 кбит/с, ROI=73.683467 кбит/с, total=93.275634 кбит/с.
base240_0_5fps_q25_roi320_2fps_q35: BASE=10.150112 кбит/с, ROI=73.683467 кбит/с, total=83.833579 кбит/с.
base160_1fps_q25_roi320_2fps_q35: BASE=11.511525 кбит/с, ROI=73.683467 кбит/с, total=85.194992 кбит/с.
base240_1fps_q25_roi448_1fps_q30: BASE=19.592167 кбит/с, ROI=56.932392 кбит/с, total=76.524559 кбит/с.
base240_0_5fps_q25_roi448_1fps_q30: BASE=10.150112 кбит/с, ROI=56.932392 кбит/с, total=67.082504 кбит/с.
base320_1fps_q30_roi320_1fps_q35: BASE=33.862343 кбит/с, ROI=36.738491 кбит/с, total=70.600835 кбит/с.
base240_1fps_q20_roi320_1fps_q30: BASE=17.180610 кбит/с, ROI=33.306453 кбит/с, total=50.487063 кбит/с.
base160_0_5fps_q20_roi320_2fps_q30: BASE=5.323917 кбит/с, ROI=66.820546 кбит/с, total=72.144462 кбит/с.
Фиксированная ROI не гарантирует попадания препятствия в область интереса.
PSNR не заменяет ручную оценку пригодности изображения.
Нельзя автоматически объявлять профиль безопасным для управления ровером.
Радиопротокол, радиозаголовки, CRC, FEC, фрагментация и повторные передачи не учтены.
Сравнительное изображение: data\processed\lab027\lab027_candidate_profiles.png
Следующий шаг: ручная оценка пользователем.

View File

@@ -0,0 +1,5 @@
profile_name,update_mode,base_width,base_height,base_fps,base_quality,roi_width,roi_height,roi_fps,roi_quality,source_duration_s,selected_base_frames,selected_roi_frames,synchronized_composite_frames,total_base_bytes,total_roi_bytes,total_payload_bytes,mean_base_frame_bytes,mean_roi_frame_bytes,mean_composite_frame_bytes,p95_composite_frame_bytes,max_composite_frame_bytes,base_bitrate_kbps,roi_bitrate_kbps,total_payload_bitrate_kbps,timestamp_mismatch_count,frame_id_mismatch_count
async_reference,asynchronous,240,135,1.000000,25,320,180,2.000000,35,20.766667,21,42,N/A,50858,191270,242128,2421.810,4554.048,5764.952,7477.600,7741,19.592167,73.683467,93.275634,N/A,N/A
sync_base160_q25_roi320_q35,synchronous,160,90,2.000000,25,320,180,2.000000,35,20.766667,42,42,42,59958,191270,251228,1427.571,4554.048,5981.619,6519.900,6656,23.097785,73.683467,96.781252,0,0
sync_base240_q20_roi320_q30,synchronous,240,135,2.000000,20,320,180,2.000000,30,20.766667,42,42,42,89696,173455,263151,2135.619,4129.881,6265.500,6780.400,6842,34.553836,66.820546,101.374382,0,0
sync_base240_q25_roi320_q35,synchronous,240,135,2.000000,25,320,180,2.000000,35,20.766667,42,42,42,102283,191270,293553,2435.310,4554.048,6989.357,7572.350,7741,39.402761,73.683467,113.086228,0,0
1 profile_name update_mode base_width base_height base_fps base_quality roi_width roi_height roi_fps roi_quality source_duration_s selected_base_frames selected_roi_frames synchronized_composite_frames total_base_bytes total_roi_bytes total_payload_bytes mean_base_frame_bytes mean_roi_frame_bytes mean_composite_frame_bytes p95_composite_frame_bytes max_composite_frame_bytes base_bitrate_kbps roi_bitrate_kbps total_payload_bitrate_kbps timestamp_mismatch_count frame_id_mismatch_count
2 async_reference asynchronous 240 135 1.000000 25 320 180 2.000000 35 20.766667 21 42 N/A 50858 191270 242128 2421.810 4554.048 5764.952 7477.600 7741 19.592167 73.683467 93.275634 N/A N/A
3 sync_base160_q25_roi320_q35 synchronous 160 90 2.000000 25 320 180 2.000000 35 20.766667 42 42 42 59958 191270 251228 1427.571 4554.048 5981.619 6519.900 6656 23.097785 73.683467 96.781252 0 0
4 sync_base240_q20_roi320_q30 synchronous 240 135 2.000000 20 320 180 2.000000 30 20.766667 42 42 42 89696 173455 263151 2135.619 4129.881 6265.500 6780.400 6842 34.553836 66.820546 101.374382 0 0
5 sync_base240_q25_roi320_q35 synchronous 240 135 2.000000 25 320 180 2.000000 35 20.766667 42 42 42 102283 191270 293553 2435.310 4554.048 6989.357 7572.350 7741 39.402761 73.683467 113.086228 0 0

View File

@@ -0,0 +1,44 @@
Lab027C. Динамическое сравнение асинхронного и синхронного BASE + ROI
Цель: визуально сравнить ранее выбранный асинхронный профиль BASE 1 fps + ROI 2 fps с тремя синхронными профилями BASE 2 fps + ROI 2 fps.
Исходное видео: data\raw\lab026_rover_source.mp4
Исходное разрешение: 1280x720
Исходный FPS: 30.000000
Число кадров: 623
Длительность: 20.766667 с
ROI: x=0.20...0.80, y=0.42...1.00; пиксели x=256...1024, y=302...720.
Результат ручной оценки предыдущей Lab027:
- пользователь выбрал вариант B;
- граница ROI не мешает;
- обновление BASE один раз в секунду мешает;
- рассинхронное движение BASE и ROI мешает;
- принято решение сравнить с синхронным обновлением 2 fps.
async_reference: BASE и ROI обновляются независимо. Единого logical_frame_id нет; используются отдельные BASE ID и ROI ID. Mismatch для этого режима неприменим.
Синхронные профили: BASE и ROI формируются из одного source frame, получают единые composite ID и timestamp и атомарно заменяют отображаемый составной кадр.
Фактические результаты четырёх профилей:
async_reference: mode=asynchronous, BASE=240x135, 1.000 fps, Q25, base_frames=21, base_bytes=50858, base_bitrate=19.592167 kbit/s; ROI=320x180, 2.000 fps, Q35, roi_frames=42, roi_bytes=191270, roi_bitrate=73.683467 kbit/s; total_bytes=242128, total_bitrate=93.275634 kbit/s, sync_frames=N/A, timestamp_mismatch=N/A, frame_id_mismatch=N/A
sync_base160_q25_roi320_q35: mode=synchronous, BASE=160x90, 2.000 fps, Q25, base_frames=42, base_bytes=59958, base_bitrate=23.097785 kbit/s; ROI=320x180, 2.000 fps, Q35, roi_frames=42, roi_bytes=191270, roi_bitrate=73.683467 kbit/s; total_bytes=251228, total_bitrate=96.781252 kbit/s, sync_frames=42, timestamp_mismatch=0, frame_id_mismatch=0
sync_base240_q20_roi320_q30: mode=synchronous, BASE=240x135, 2.000 fps, Q20, base_frames=42, base_bytes=89696, base_bitrate=34.553836 kbit/s; ROI=320x180, 2.000 fps, Q30, roi_frames=42, roi_bytes=173455, roi_bitrate=66.820546 kbit/s; total_bytes=263151, total_bitrate=101.374382 kbit/s, sync_frames=42, timestamp_mismatch=0, frame_id_mismatch=0
sync_base240_q25_roi320_q35: mode=synchronous, BASE=240x135, 2.000 fps, Q25, base_frames=42, base_bytes=102283, base_bitrate=39.402761 kbit/s; ROI=320x180, 2.000 fps, Q35, roi_frames=42, roi_bytes=191270, roi_bitrate=73.683467 kbit/s; total_bytes=293553, total_bitrate=113.086228 kbit/s, sync_frames=42, timestamp_mismatch=0, frame_id_mismatch=0
Количество обновлений:
async_reference: BASE=21, ROI=42, synchronized=N/A.
sync_base160_q25_roi320_q35: BASE=42, ROI=42, synchronized=42.
sync_base240_q20_roi320_q30: BASE=42, ROI=42, synchronized=42.
sync_base240_q25_roi320_q35: BASE=42, ROI=42, synchronized=42.
Mismatch-проверка:
async_reference: timestamp=N/A, frame_id=N/A.
sync_base160_q25_roi320_q35: timestamp=0, frame_id=0.
sync_base240_q20_roi320_q30: timestamp=0, frame_id=0.
sync_base240_q25_roi320_q35: timestamp=0, frame_id=0.
Preview: data\raw\lab027c_previews\lab027c_synchronous_roi_preview.mp4
Формат: MP4/mp4v.
Радиопротокол, CRC, FEC, фрагментация и служебный трафик пока не учитываются.
Программа не выбирает лучший профиль и не объявляет режим безопасным автоматически.
Следующий шаг: ручной выбор пользователем после просмотра preview-видео.

View File

@@ -0,0 +1,5 @@
profile_name,base_width,base_height,fps,base_quality,roi_width,roi_height,roi_quality,source_duration_s,selected_base_frames,selected_roi_frames,synchronized_composite_frames,total_base_bytes,total_roi_bytes,total_payload_bytes,mean_base_frame_bytes,mean_roi_frame_bytes,mean_composite_frame_bytes,p95_composite_frame_bytes,max_composite_frame_bytes,base_bitrate_kbps,roi_bitrate_kbps,total_payload_bitrate_kbps,channel_rate_fec_2_3_kbps,channel_rate_fec_1_2_kbps,timestamp_mismatch_count,frame_id_mismatch_count
sync_2fps_base_q20_roi_q30,240,135,2.000000,20,320,180,30,20.766667,42,42,42,89696,173455,263151,2135.619,4129.881,6265.500,6780.400,6842,34.553836,66.820546,101.374382,167.267730,223.023640,0,0
sync_3fps_base_q20_roi_q30,240,135,3.000000,20,320,180,30,20.766667,63,63,63,133076,249436,382512,2112.317,3959.302,6071.619,6640.800,6842,51.265233,96.090915,147.356148,243.137644,324.183525,0,0
sync_3fps_base_q23_roi_q33,240,135,3.000000,23,320,180,33,20.766667,63,63,63,144649,264895,409544,2296.016,4204.683,6500.698,7125.400,7383,55.723531,102.046228,157.769759,260.320103,347.093470,0,0
sync_3fps_base_q25_roi_q35,240,135,3.000000,25,320,180,35,20.766667,63,63,63,151516,274486,426002,2405.016,4356.921,6761.937,7421.700,7741,58.368925,105.740995,164.109920,270.781368,361.041823,0,0
1 profile_name base_width base_height fps base_quality roi_width roi_height roi_quality source_duration_s selected_base_frames selected_roi_frames synchronized_composite_frames total_base_bytes total_roi_bytes total_payload_bytes mean_base_frame_bytes mean_roi_frame_bytes mean_composite_frame_bytes p95_composite_frame_bytes max_composite_frame_bytes base_bitrate_kbps roi_bitrate_kbps total_payload_bitrate_kbps channel_rate_fec_2_3_kbps channel_rate_fec_1_2_kbps timestamp_mismatch_count frame_id_mismatch_count
2 sync_2fps_base_q20_roi_q30 240 135 2.000000 20 320 180 30 20.766667 42 42 42 89696 173455 263151 2135.619 4129.881 6265.500 6780.400 6842 34.553836 66.820546 101.374382 167.267730 223.023640 0 0
3 sync_3fps_base_q20_roi_q30 240 135 3.000000 20 320 180 30 20.766667 63 63 63 133076 249436 382512 2112.317 3959.302 6071.619 6640.800 6842 51.265233 96.090915 147.356148 243.137644 324.183525 0 0
4 sync_3fps_base_q23_roi_q33 240 135 3.000000 23 320 180 33 20.766667 63 63 63 144649 264895 409544 2296.016 4204.683 6500.698 7125.400 7383 55.723531 102.046228 157.769759 260.320103 347.093470 0 0
5 sync_3fps_base_q25_roi_q35 240 135 3.000000 25 320 180 35 20.766667 63 63 63 151516 274486 426002 2405.016 4356.921 6761.937 7421.700 7741 58.368925 105.740995 164.109920 270.781368 361.041823 0 0

View File

@@ -0,0 +1,37 @@
Lab027D. Синхронный BASE + ROI при 2 и 3 fps
Цель: сравнить текущий синхронный профиль 2 fps с тремя профилями 3 fps при разных JPEG Quality.
Результат ручной оценки Lab027C:
- лучший из показанных вариантов — нижний левый;
- синхронизация BASE и ROI устранила рассогласование;
- 2 fps недостаточно;
- BASE 160x90 слишком грубый;
- BASE 240x135 Q20 немного недостаточен по качеству.
Причина повышения FPS: ручная оценка показала, что синхронное обновление устраняет рассогласование, но частота 2 fps недостаточна для динамической сцены.
Исходное видео: data\raw\lab026_rover_source.mp4
Разрешение: 1280x720
FPS: 30.000000
Кадров: 623
Длительность: 20.766667 с
ROI: x=0.20...0.80, y=0.42...1.00; пиксели x=256...1024, y=302...720.
Все профили синхронные: BASE и ROI формируются из одного source frame, получают единый timestamp и composite frame ID и публикуются атомарно.
Фактические результаты:
sync_2fps_base_q20_roi_q30: 2.000 fps; BASE=240x135, Q20, frames=42, bytes=89696, bitrate=34.553836 kbit/s; ROI=320x180, Q30, frames=42, bytes=173455, bitrate=66.820546 kbit/s; total_bytes=263151, payload=101.374382 kbit/s; channel FEC 2/3=167.267730 kbit/s; channel FEC 1/2=223.023640 kbit/s; sync_frames=42; timestamp_mismatch=0; frame_id_mismatch=0
sync_3fps_base_q20_roi_q30: 3.000 fps; BASE=240x135, Q20, frames=63, bytes=133076, bitrate=51.265233 kbit/s; ROI=320x180, Q30, frames=63, bytes=249436, bitrate=96.090915 kbit/s; total_bytes=382512, payload=147.356148 kbit/s; channel FEC 2/3=243.137644 kbit/s; channel FEC 1/2=324.183525 kbit/s; sync_frames=63; timestamp_mismatch=0; frame_id_mismatch=0
sync_3fps_base_q23_roi_q33: 3.000 fps; BASE=240x135, Q23, frames=63, bytes=144649, bitrate=55.723531 kbit/s; ROI=320x180, Q33, frames=63, bytes=264895, bitrate=102.046228 kbit/s; total_bytes=409544, payload=157.769759 kbit/s; channel FEC 2/3=260.320103 kbit/s; channel FEC 1/2=347.093470 kbit/s; sync_frames=63; timestamp_mismatch=0; frame_id_mismatch=0
sync_3fps_base_q25_roi_q35: 3.000 fps; BASE=240x135, Q25, frames=63, bytes=151516, bitrate=58.368925 kbit/s; ROI=320x180, Q35, frames=63, bytes=274486, bitrate=105.740995 kbit/s; total_bytes=426002, payload=164.109920 kbit/s; channel FEC 2/3=270.781368 kbit/s; channel FEC 1/2=361.041823 kbit/s; sync_frames=63; timestamp_mismatch=0; frame_id_mismatch=0
Оценки channel rate иллюстративны: к payload добавлено 10% служебных данных, затем применена оценка FEC 2/3 или FEC 1/2.
Эти значения не являются окончательной архитектурой радиоканала.
Preview: data\raw\lab027d_previews\lab027d_fps_quality_preview.mp4
Формат: MP4/mp4v.
Предупреждение: оценки пока не включают окончательную модуляцию, полосу сигнала, интерливинг, повторы и команды управления.
Программа не выбирает лучший профиль автоматически.
Следующий шаг: ручной выбор пользователя после просмотра preview-видео.

View File

@@ -0,0 +1,2 @@
profile_name,playback_speed_factor,source_width,source_height,source_fps,source_frames,source_duration_s,output_width,output_height,output_fps,output_frames,output_duration_s,composite_fps,base_width,base_height,base_quality,roi_width,roi_height,roi_quality,selected_composite_frames,total_base_bytes,total_roi_bytes,total_payload_bytes,mean_base_frame_bytes,mean_roi_frame_bytes,mean_composite_frame_bytes,p95_composite_frame_bytes,max_composite_frame_bytes,base_bitrate_kbps,roi_bitrate_kbps,total_payload_bitrate_kbps,channel_rate_fec_2_3_kbps,channel_rate_fec_1_2_kbps,timestamp_mismatch_count,frame_id_mismatch_count
sync_3fps_base240_q23_roi320_q33_speed_0_5x,0.500000,1280,720,30.000000,623,20.766667,1280,720,30.000000,1246,41.533333,3.000000,240,135,23,320,180,33,125,286715,524962,811677,2293.720,4199.696,6493.416,7125.400,7890,55.226003,101.116276,156.342279,257.964761,343.953014,0,0
1 profile_name playback_speed_factor source_width source_height source_fps source_frames source_duration_s output_width output_height output_fps output_frames output_duration_s composite_fps base_width base_height base_quality roi_width roi_height roi_quality selected_composite_frames total_base_bytes total_roi_bytes total_payload_bytes mean_base_frame_bytes mean_roi_frame_bytes mean_composite_frame_bytes p95_composite_frame_bytes max_composite_frame_bytes base_bitrate_kbps roi_bitrate_kbps total_payload_bitrate_kbps channel_rate_fec_2_3_kbps channel_rate_fec_1_2_kbps timestamp_mismatch_count frame_id_mismatch_count
2 sync_3fps_base240_q23_roi320_q33_speed_0_5x 0.500000 1280 720 30.000000 623 20.766667 1280 720 30.000000 1246 41.533333 3.000000 240 135 23 320 180 33 125 286715 524962 811677 2293.720 4199.696 6493.416 7125.400 7890 55.226003 101.116276 156.342279 257.964761 343.953014 0 0

View File

@@ -0,0 +1,50 @@
Lab027E. Операторское preview при playback 0.5x
Результаты ручной оценки Lab027D:
- 3 fps достаточно на исходном быстром видео;
- Q20/Q30 приемлем;
- Q23/Q33 приемлем;
- разница Q23/Q33 и Q25/Q35 практически отсутствует.
Предварительно выбран профиль Q23/Q33.
От Q25/Q35 предварительно отказались, поскольку визуальный выигрыш почти отсутствует.
Профиль не считается окончательно утверждённым до ручного просмотра этого preview.
Исходная сцена замедлена для имитации более медленного движения ровера и оценки удобства управления.
Коэффициент playback: 0.500x.
Предупреждение: реальная скорость исходного автомобиля неизвестна.
Исходное видео:
- путь: data\raw\lab026_rover_source.mp4;
- разрешение: 1280x720;
- FPS: 30.000000;
- кадров: 623;
- длительность: 20.766667 с.
Выходное preview:
- разрешение: 1280x720;
- FPS: 30.000000;
- кадров: 1246;
- длительность: 41.533333 с;
- composite FPS: 3.000000.
Профиль:
- BASE 240x135 Q23;
- ROI 320x180 Q33;
- составных обновлений: 125.
Фактические скорости:
- BASE: 55.226003 kbit/s;
- ROI: 101.116276 kbit/s;
- total payload: 156.342279 kbit/s;
- channel rate FEC 2/3: 257.964761 kbit/s;
- channel rate FEC 1/2: 343.953014 kbit/s.
Оценки канальной скорости иллюстративны и не являются окончательной архитектурой радиоканала.
Mismatch-проверка:
- timestamp mismatch: 0;
- frame ID mismatch: 0.
Preview: data\raw\lab027e_previews\lab027e_operator_view_0_5x.mp4
Формат: MP4/mp4v.
Следующий шаг: ручная оценка удобства управления пользователем.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,6 @@
max_payload_bytes,composite_frames,mean_base_packets_per_frame,mean_roi_packets_per_frame,mean_total_packets_per_frame,max_total_packets_per_frame,packets_per_second,jpeg_payload_bytes,jpeg_payload_bitrate_kbps,header_bytes_per_second,header_bitrate_kbps,wire_bytes,wire_bitrate_kbps,service_data_percent,efficiency_percent,mean_wire_packet_bytes,p95_wire_packet_bytes,max_wire_packet_bytes
64,63,36.428571,66.174603,102.603175,117,311.268058,409544,157.769759,9960.577849,79.684623,616392,237.454382,33.557866,66.442134,95.357673,96.000000,96
128,63,18.428571,33.269841,51.698413,59,156.837881,409544,157.769759,5018.812199,40.150498,513768,197.920257,20.286199,79.713801,157.742708,160.000000,160
256,63,9.428571,16.920635,26.349206,30,79.935795,409544,157.769759,2557.945425,20.463563,462664,178.233323,11.481334,88.518666,278.713253,288.000000,288
512,63,5.000000,8.682540,13.682540,15,41.508828,409544,157.769759,1328.282504,10.626260,437128,168.396019,6.310280,93.689720,507.109049,544.000000,544
1024,63,3.000000,4.587302,7.587302,8,23.017657,409544,157.769759,736.565008,5.892520,424840,163.662279,3.600414,96.399586,888.786611,1056.000000,1056
1 max_payload_bytes composite_frames mean_base_packets_per_frame mean_roi_packets_per_frame mean_total_packets_per_frame max_total_packets_per_frame packets_per_second jpeg_payload_bytes jpeg_payload_bitrate_kbps header_bytes_per_second header_bitrate_kbps wire_bytes wire_bitrate_kbps service_data_percent efficiency_percent mean_wire_packet_bytes p95_wire_packet_bytes max_wire_packet_bytes
2 64 63 36.428571 66.174603 102.603175 117 311.268058 409544 157.769759 9960.577849 79.684623 616392 237.454382 33.557866 66.442134 95.357673 96.000000 96
3 128 63 18.428571 33.269841 51.698413 59 156.837881 409544 157.769759 5018.812199 40.150498 513768 197.920257 20.286199 79.713801 157.742708 160.000000 160
4 256 63 9.428571 16.920635 26.349206 30 79.935795 409544 157.769759 2557.945425 20.463563 462664 178.233323 11.481334 88.518666 278.713253 288.000000 288
5 512 63 5.000000 8.682540 13.682540 15 41.508828 409544 157.769759 1328.282504 10.626260 437128 168.396019 6.310280 93.689720 507.109049 544.000000 544
6 1024 63 3.000000 4.587302 7.587302 8 23.017657 409544 157.769759 736.565008 5.892520 424840 163.662279 3.600414 96.399586 888.786611 1056.000000 1056

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -0,0 +1,52 @@
Lab028. Пакетирование синхронного BASE + ROI
Профиль и исходные данные
- Видео: data\raw\lab026_rover_source.mp4
- Источник: 1280x720, 30.000000 fps, 623 кадров, 20.766667 с.
- ROI: x=0.20...0.80, y=0.42...1.00; пиксели x=256...1024, y=302...720.
- Синхронные составные кадры: 63 при 3.000 fps.
- BASE: 240x135, grayscale JPEG Q23; средний размер 2296.016 B, min/max 2122/2476 B.
- ROI: 320x180, grayscale JPEG Q33; средний размер 4204.683 B, min/max 3457/4938 B.
- BASE и ROI формируются из одного source frame и имеют общий composite_frame_id.
Бинарный заголовок
- struct format: !4sBBHIHHHHIII
- Размер: 32 байта; network byte order; padding нет.
- Layout: magic[4] @0, version:u8 @4, object_type:u8 @5, header_size:u16 @6, composite_frame_id:u32 @8, fragment_index:u16 @12, fragment_count:u16 @14, payload_length:u16 @16, flags:u16 @18, jpeg_size:u32 @20, object_crc32:u32 @24, packet_crc32:u32 @28.
- object_type: 1=BASE, 2=ROI; flags зарезервирован и равен 0.
CRC32
- object_crc32 = zlib.crc32(полный JPEG) & 0xFFFFFFFF; значение помещается во все фрагменты объекта и проверяется после полной сборки.
- packet_crc32: сначала поле packet_crc32 заголовка обнуляется, затем CRC считается как zlib.crc32(header_with_zero_crc + payload) & 0xFFFFFFFF.
- Таким образом packet CRC защищает все поля заголовка и payload.
Функциональные проверки
- PASS header_serialization_round_trip: fixed 32-byte header round trip is exact
- PASS ordered_lossless_transfer: BASE and ROI match original JPEG bytes
- PASS random_packet_order: arbitrary packet order reconstructed correctly
- PASS exact_duplicate_packets: 2 exact duplicates ignored
- PASS packet_crc_corruption: payload bit flip rejected; composite not emitted
- PASS missing_fragment: ROI fragment 10 reported missing
- PASS object_crc_corruption: valid packet CRCs still failed full-object CRC
- PASS adjacent_frame_isolation: two interleaved frame IDs remained independent
- PASS base_without_roi_atomicity: complete BASE retained while composite stayed unpublished
Результаты размеров packet payload
payload | BASE pkt/frame | ROI pkt/frame | all pkt/frame | max pkt/frame | pkt/s | JPEG kbit/s | headers B/s | wire kbit/s | service % | efficiency % | packet mean/p95/max
-------:|---------------:|--------------:|--------------:|--------------:|------:|------------:|------------:|------------:|----------:|-------------:|--------------------:
64 | 36.429 | 66.175 | 102.603 | 117 | 311.268 | 157.770 | 9960.578 | 237.454 | 33.558 | 66.442 | 95.4/96.0/96
128 | 18.429 | 33.270 | 51.698 | 59 | 156.838 | 157.770 | 5018.812 | 197.920 | 20.286 | 79.714 | 157.7/160.0/160
256 | 9.429 | 16.921 | 26.349 | 30 | 79.936 | 157.770 | 2557.945 | 178.233 | 11.481 | 88.519 | 278.7/288.0/288
512 | 5.000 | 8.683 | 13.683 | 15 | 41.509 | 157.770 | 1328.283 | 168.396 | 6.310 | 93.690 | 507.1/544.0/544
1024 | 3.000 | 4.587 | 7.587 | 8 | 23.018 | 157.770 | 736.565 | 163.662 | 3.600 | 96.400 | 888.8/1056.0/1056
JPEG payload bitrate одинаков для всех строк, потому что исходные JPEG не меняются при выборе размера фрагмента.
Wire bitrate включает только JPEG payload и 32-байтные заголовки каждого пакета.
Wire bitrate пока НЕ включает FEC, преамбулу, синхронизацию, модуляцию, интервалы, повторные передачи, команды управления и телеметрию.
Лучший размер packet payload автоматически не выбирается: таблица показывает только транспортный компромисс.
Артефакты
- CSV: data\processed\lab028\lab028_packet_payload_results.csv
- Overhead/efficiency: data\processed\lab028\lab028_overhead_efficiency.png
- Packet count/wire bitrate: data\processed\lab028\lab028_packets_wire_bitrate.png
- Промежуточные JPEG и пакеты сохранялись только в памяти; бинарные дампы не создавались.

View File

@@ -0,0 +1,405 @@
# Исследование сжатия видеопотока в Lab026Lab027
## 1. Назначение исследований
Lab026, Lab026B и Lab027 исследуют способы уменьшения полезной нагрузки
видеоканала для системы управления ровером через ретранслятор.
На рассматриваемом этапе измеряется только полезная нагрузка,
формируемая средствами покадрового кодирования изображения. Исследования
не включают радиопередачу и не определяют профиль как автоматически
безопасный для управления.
Последовательность исследований:
```text
Lab026
|
+-- разрешение кадра
+-- частота кадров
+-- color / grayscale
+-- JPEG Quality
|
v
Контрольный профиль: 320x180, grayscale, 2 fps, JPEG Q40
|
+--> Lab026B: 4-битная яркость и индексированный цвет
|
+--> Lab027: раздельная передача общего кадра BASE и области ROI
```
## 2. Исходное видео
Во всех трёх лабораторных использован один файл:
```text
data/raw/lab026_rover_source.mp4
```
| Параметр | Значение |
|---|---:|
| Размер файла | 7 790 985 байт |
| Разрешение | 1280x720 |
| Частота кадров | 30.000000 кадр/с |
| Число кадров | 623 |
| Длительность | 20.766667 с |
| Приблизительный файловый битрейт | 3001.343 кбит/с |
## 3. Метод покадрового JPEG-сжатия
Lab026 и JPEG-режимы последующих лабораторных используют независимое
покадровое сжатие:
```text
исходный кадр
|
+--> изменение разрешения
|
+--> преобразование color / grayscale по параметрам профиля
|
+--> cv2.imencode(..., JPEG Quality)
|
+--> размер JPEG в байтах
|
+--> cv2.imdecode(...)
|
+--> расчёт PSNR
```
JPEG кодируется и декодируется в памяти. Отдельные JPEG-файлы на диск
не сохраняются. Межкадровое предсказание не используется.
Кадры для заданной частоты выбираются по временной шкале. В общем виде:
```text
frame_time = frame_index / source_fps
кадр выбирается, если:
frame_time + 1e-9 >= next_sample_time
после выбора:
next_sample_time += 1 / target_fps
```
## 4. Lab026: разрешение, FPS, grayscale и JPEG Quality
### 4.1. Изменение FPS
Условия серии: 640x360, grayscale, JPEG Q50.
| Целевой FPS | Выбрано кадров | Payload, кбит/с | PSNR, дБ |
|---:|---:|---:|---:|
| 30 | 623 | 3936.913 | 37.350 |
| 10 | 208 | 1402.877 | 36.370 |
| 5 | 104 | 702.043 | 36.353 |
| 2 | 42 | 284.084 | 36.320 |
| 1 | 21 | 142.273 | 36.288 |
| 0.5 | 11 | 73.821 | 36.372 |
| 0.2 | 5 | 35.247 | 35.860 |
### 4.2. Изменение разрешения
Условия серии: 5 кадр/с, grayscale, JPEG Q50.
| Разрешение | Выбрано кадров | Payload, кбит/с | PSNR, дБ |
|---|---:|---:|---:|
| 1280x720 | 104 | 2121.333 | 39.608 |
| 960x540 | 104 | 1314.376 | 38.632 |
| 640x360 | 104 | 702.043 | 36.353 |
| 480x270 | 104 | 431.824 | 36.172 |
| 320x180 | 104 | 227.294 | 35.069 |
| 160x90 | 104 | 80.918 | 33.586 |
### 4.3. Color, grayscale и JPEG Quality
Условия серии: 640x360, 5 кадр/с.
| Режим | JPEG Quality | Payload, кбит/с | PSNR, дБ |
|---|---:|---:|---:|
| color | 90 | 2113.639 | 39.694 |
| color | 70 | 1117.677 | 36.029 |
| color | 50 | 823.307 | 34.439 |
| color | 30 | 605.629 | 32.766 |
| color | 20 | 471.731 | 31.299 |
| grayscale | 90 | 1809.440 | 42.575 |
| grayscale | 70 | 957.219 | 38.032 |
| grayscale | 50 | 702.043 | 36.353 |
| grayscale | 30 | 511.658 | 34.702 |
| grayscale | 20 | 392.150 | 33.304 |
### 4.4. Контрольный профиль
В качестве контрольного профиля принят:
| Параметр | Значение |
|---|---:|
| Разрешение | 320x180 |
| Цветовой режим | grayscale |
| Частота | 2 кадр/с |
| JPEG Quality | 40 |
| Выбрано кадров | 42 |
| Payload bitrate | 80.283660 кбит/с |
| Средний PSNR | 34.362114 дБ |
Этот профиль используется как baseline в Lab026B и как контрольная
точка для Lab027.
## 5. Lab026B: глубина 4 бита на пиксель
### 5.1. Исследованные представления
Под 4-битной яркостью понимается индекс `0...15`, то есть 16 уровней
серого. Восстановленное значение формируется по тому же индексу в
диапазоне `0...255`.
Под 4-битным индексированным цветом понимается один индекс `0...15`
на пиксель и палитра не более 16 RGB-цветов на кадр. Это не RGB444:
RGB444 использует по 4 бита на каждый из трёх каналов, то есть 12 бит
на пиксель.
Исследованы пять режимов:
| Режим | Представление |
|---|---|
| `gray8_jpeg` | Контрольный grayscale JPEG Q40 |
| `gray4_jpeg` | 16 уровней серого, восстановление в `uint8`, JPEG Q40 |
| `gray4_packed_zlib` | Два 4-битных индекса в байте, zlib level 9, заголовок 16 байт |
| `color16_png` | До 16 цветов, без дизеринга, индексированный PNG |
| `color16_packed_zlib` | 4-битные индексы, zlib level 9, заголовок 16 байт, палитра 48 байт |
### 5.2. Результаты при 320x180 и 2 кадр/с
| Режим | Payload, кбит/с | Средний PSNR, дБ |
|---|---:|---:|
| `gray8_jpeg` | 80.283660 | 34.362114 |
| `gray4_jpeg` | 88.417849 | 32.588941 |
| `gray4_packed_zlib` | 169.916148 | 34.397012 |
| `color16_png` | 143.121669 | 25.384724 |
| `color16_packed_zlib` | 140.568347 | 25.384724 |
### 5.3. Зафиксированный отрицательный результат
В Lab026B уменьшение глубины до 4 бит не позволило повысить разрешение
кадра в пределах 100 кбит/с.
До 100 кбит/с прошли только два профиля:
| Профиль | Разрешение | Payload, кбит/с | Средний PSNR, дБ |
|---|---|---:|---:|
| `320x180_gray8_jpeg_2fps` | 320x180 | 80.283660 | 34.362114 |
| `320x180_gray4_jpeg_2fps` | 320x180 | 88.417849 | 32.588941 |
Профилей с разрешением больше 320x180 и payload не более
80.283660 кбит/с не получено. Цветных `color16`-профилей до
100 кбит/с также не получено.
## 6. Lab027: многомасштабная передача BASE + ROI
### 6.1. Принцип
В Lab027 один профиль состоит из двух независимых JPEG-потоков:
```text
исходный полноразмерный кадр
|
+--> BASE
| полный grayscale-кадр
| собственные разрешение, FPS и JPEG Quality
|
+--> ROI
вырезка из исходного кадра
собственные разрешение, FPS и JPEG Quality
приёмная реконструкция 640x360:
декодированный BASE
|
+--> масштабирование до 640x360
|
+--> наложение последнего декодированного ROI
в геометрическое положение области интереса
```
BASE и ROI обновляются независимо по временной шкале. Между
обновлениями удерживается последнее декодированное состояние каждого
потока.
### 6.2. Фиксированная ROI
Нормализованные координаты:
```text
x_min = 0.20
x_max = 0.80
y_min = 0.42
y_max = 1.00
```
Для исходного кадра 1280x720 используются координаты:
```text
x = 256...1024
y = 302...720
```
Область занимает центральные 60% ширины и нижние 58% высоты кадра.
ROI фиксирована и не использует автоматическое распознавание дороги,
нейронные сети или детекторы объектов.
### 6.3. Результаты ROI-профилей
| Профиль | BASE | ROI | BASE, кбит/с | ROI, кбит/с | Total, кбит/с | Full PSNR, дБ | ROI PSNR, дБ |
|---|---|---|---:|---:|---:|---:|---:|
| `base240_1fps_q25_roi320_2fps_q35` | 240x135, 1 FPS, Q25 | 320x180, 2 FPS, Q35 | 19.592167 | 73.683467 | 93.275634 | 19.657187 | 19.574377 |
| `base240_0_5fps_q25_roi320_2fps_q35` | 240x135, 0.5 FPS, Q25 | 320x180, 2 FPS, Q35 | 10.150112 | 73.683467 | 83.833579 | 18.613817 | 19.574377 |
| `base160_1fps_q25_roi320_2fps_q35` | 160x90, 1 FPS, Q25 | 320x180, 2 FPS, Q35 | 11.511525 | 73.683467 | 85.194992 | 19.701304 | 19.574377 |
| `base240_1fps_q25_roi448_1fps_q30` | 240x135, 1 FPS, Q25 | 448x252, 1 FPS, Q30 | 19.592167 | 56.932392 | 76.524559 | 19.098790 | 17.871555 |
| `base240_0_5fps_q25_roi448_1fps_q30` | 240x135, 0.5 FPS, Q25 | 448x252, 1 FPS, Q30 | 10.150112 | 56.932392 | 67.082504 | 18.113877 | 17.871555 |
| `base320_1fps_q30_roi320_1fps_q35` | 320x180, 1 FPS, Q30 | 320x180, 1 FPS, Q35 | 33.862343 | 36.738491 | 70.600835 | 19.121457 | 17.888190 |
| `base240_1fps_q20_roi320_1fps_q30` | 240x135, 1 FPS, Q20 | 320x180, 1 FPS, Q30 | 17.180610 | 33.306453 | 50.487063 | 19.107784 | 17.873870 |
| `base160_0_5fps_q20_roi320_2fps_q30` | 160x90, 0.5 FPS, Q20 | 320x180, 2 FPS, Q30 | 5.323917 | 66.820546 | 72.144462 | 18.664976 | 19.549559 |
Контрольный baseline при повторении в Lab027 составил
80.270177 кбит/с. Контрольное значение из Lab026, используемое для
сравнения, равно 80.283660 кбит/с.
### 6.4. Основной режим
В качестве основного режима в этом описании зафиксирован профиль:
```text
base160_1fps_q25_roi320_2fps_q35
```
| Компонент | Параметры |
|---|---|
| BASE | 160x90, 1 кадр/с, JPEG Q25 |
| ROI | 320x180, 2 кадр/с, JPEG Q35 |
| BASE bitrate | 11.511525 кбит/с |
| ROI bitrate | 73.683467 кбит/с |
| Total payload bitrate | 85.194992 кбит/с |
| Mean reconstructed full PSNR | 19.701304 дБ |
| Mean reconstructed ROI PSNR | 19.574377 дБ |
### 6.5. Аварийный режим
В качестве аварийного режима в этом описании зафиксирован профиль:
```text
base240_1fps_q20_roi320_1fps_q30
```
| Компонент | Параметры |
|---|---|
| BASE | 240x135, 1 кадр/с, JPEG Q20 |
| ROI | 320x180, 1 кадр/с, JPEG Q30 |
| BASE bitrate | 17.180610 кбит/с |
| ROI bitrate | 33.306453 кбит/с |
| Total payload bitrate | 50.487063 кбит/с |
| Mean reconstructed full PSNR | 19.107784 дБ |
| Mean reconstructed ROI PSNR | 17.873870 дБ |
## 7. Расчёт payload bitrate
Для одного JPEG-потока:
```text
total_payload_bytes = сумма размеров выбранных JPEG-кадров
payload_bitrate_bps =
total_payload_bytes * 8 / source_duration_seconds
payload_bitrate_kbps =
payload_bitrate_bps / 1000
```
Для Lab027:
```text
total_payload_bytes =
total_base_bytes + total_roi_bytes
base_bitrate_kbps =
total_base_bytes * 8 / source_duration_seconds / 1000
roi_bitrate_kbps =
total_roi_bytes * 8 / source_duration_seconds / 1000
total_payload_bitrate_kbps =
base_bitrate_kbps + roi_bitrate_kbps
```
Lab026B использует тот же принцип подсчёта полезной нагрузки, но размер
кадра определяется выбранным режимом: JPEG, индексированный PNG либо
упакованные и сжатые zlib индексы вместе с предусмотренными заголовком
и палитрой.
## 8. Неучтённые накладные расходы радиоканала
В приведённые значения payload bitrate пока не включены:
- заголовки радиопакетов и служебные поля радиопротокола;
- преамбула;
- CRC;
- FEC;
- фрагментация кадров по радиопакетам;
- межпакетные интервалы;
- повторные передачи;
- команды управления.
Значения также не включают оценку задержки полного тракта. PSNR и
статические сравнительные изображения не заменяют ручную оценку
пригодности изображения. Фиксированная ROI не гарантирует попадания
препятствия в область интереса.
## 9. Итоговый рабочий видеорежим после Lab027CLab027E
Динамические сравнения Lab027C и Lab027D подтвердили, что раздельное
асинхронное обновление BASE и ROI заметно оператору. Синхронное
формирование обеих частей из одного исходного кадра устранило их
визуальное рассогласование. Частота 2 кадр/с оказалась недостаточной,
а 3 кадр/с по результатам ручной оценки признаны приемлемыми.
В качестве рабочего компромисса выбран следующий профиль:
| Компонент | Рабочие параметры |
|---|---|
| BASE | 240x135, grayscale, JPEG Q23 |
| ROI | 320x180, grayscale, JPEG Q33 |
| Обновление | синхронное и атомарное |
| Частота составного изображения | 3 кадр/с |
| JPEG payload | 156.342279 кбит/с |
| Оценка с 10% overhead и FEC 2/3 | 257.964761 кбит/с |
| Оценка с 10% overhead и FEC 1/2 | 343.953014 кбит/с |
Расчёт иллюстративных канальных скоростей:
```text
channel_rate_fec_2_3_kbps =
payload_kbps * 1.10 / (2 / 3)
channel_rate_fec_1_2_kbps =
payload_kbps * 1.10 / (1 / 2)
```
JPEG Q20 для BASE и Q30 для ROI признаны приемлемыми. Профиль Q23/Q33
выбран как рабочий компромисс качества и потока. Визуальная разница
между Q23/Q33 и Q25/Q35 практически отсутствует, поэтому увеличение
потока для Q25/Q35 не дало заметного преимущества при ручной оценке.
При скорости ровера 25 км/ч линейная скорость равна примерно
6.944 м/с. При трёх составных кадрах в секунду ровер проходит:
```text
6.944 м/с / 3 кадр/с = 2.315 м/кадр
```
То есть между соседними составными изображениями ровер перемещается
примерно на 2.31 м. Эта оценка не включает задержку кодирования,
радиоканала, ретранслятора, декодирования и реакции оператора.
Lab027E дополнительно использовала playback 0.5x для локальной
визуальной имитации более медленного движения сцены. Замедление
применялось только при создании операторского preview и не является
частью передаваемого радиоканального формата. Реальная скорость
автомобиля в исходном видео неизвестна.

416
protocol/video_packet.py Normal file
View File

@@ -0,0 +1,416 @@
"""
Transport packets for synchronous BASE + ROI JPEG composite frames.
The wire header has a fixed 32-byte, padding-free network-byte-order layout::
!4sBBHIHHHHIII
Offset Size Field
0 4 magic (b"SRV1")
4 1 version
5 1 object_type (1=BASE, 2=ROI)
6 2 header_size
8 4 composite_frame_id
12 2 fragment_index
14 2 fragment_count
16 2 payload_length
18 2 flags (reserved, must be zero)
20 4 jpeg_size
24 4 object_crc32
28 4 packet_crc32
packet_crc32 is calculated over the complete header with packet_crc32 set to
zero, followed by the packet payload. object_crc32 is calculated over the
complete JPEG before fragmentation and checked after reassembly.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import IntEnum
import struct
import zlib
MAGIC = b"SRV1"
VERSION = 1
HEADER_FORMAT = "!4sBBHIHHHHIII"
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
MAX_PAYLOAD_LENGTH = 0xFFFF
MAX_FRAGMENT_COUNT = 0xFFFF
MAX_JPEG_SIZE = 0xFFFFFFFF
class ObjectType(IntEnum):
"""JPEG object carried by a packet."""
BASE = 1
ROI = 2
class VideoPacketError(ValueError):
"""Base class for video transport validation errors."""
class PacketCRCError(VideoPacketError):
"""The packet header or payload failed its CRC32 check."""
class ObjectCRCError(VideoPacketError):
"""A completely reassembled JPEG failed its CRC32 check."""
class ObjectConsistencyError(VideoPacketError):
"""Fragments for one object contain inconsistent metadata or data."""
@dataclass(frozen=True)
class VideoPacket:
"""Decoded and CRC-checked transport packet."""
composite_frame_id: int
object_type: ObjectType
fragment_index: int
fragment_count: int
jpeg_size: int
object_crc32: int
payload: bytes
packet_crc32: int = 0
@dataclass(frozen=True)
class CompositeFrame:
"""Atomically reassembled BASE and ROI JPEGs for one source update."""
composite_frame_id: int
base_jpeg: bytes
roi_jpeg: bytes
def crc32(data: bytes) -> int:
"""Return an unsigned IEEE CRC32."""
return zlib.crc32(data) & 0xFFFFFFFF
def _validate_packet_fields(packet: VideoPacket) -> None:
if not 0 <= packet.composite_frame_id <= 0xFFFFFFFF:
raise VideoPacketError("composite_frame_id is outside uint32")
if packet.object_type not in (ObjectType.BASE, ObjectType.ROI):
raise VideoPacketError("unsupported object_type")
if not 1 <= packet.fragment_count <= MAX_FRAGMENT_COUNT:
raise VideoPacketError("fragment_count is outside uint16")
if not 0 <= packet.fragment_index < packet.fragment_count:
raise VideoPacketError("fragment_index is outside fragment_count")
if not 1 <= len(packet.payload) <= MAX_PAYLOAD_LENGTH:
raise VideoPacketError("payload length is outside uint16")
if not 1 <= packet.jpeg_size <= MAX_JPEG_SIZE:
raise VideoPacketError("jpeg_size is outside uint32")
if len(packet.payload) > packet.jpeg_size:
raise VideoPacketError("payload is larger than the JPEG object")
if not 0 <= packet.object_crc32 <= 0xFFFFFFFF:
raise VideoPacketError("object_crc32 is outside uint32")
def _pack_header(packet: VideoPacket, packet_crc32: int) -> bytes:
return struct.pack(
HEADER_FORMAT,
MAGIC,
VERSION,
int(packet.object_type),
HEADER_SIZE,
packet.composite_frame_id,
packet.fragment_index,
packet.fragment_count,
len(packet.payload),
0,
packet.jpeg_size,
packet.object_crc32,
packet_crc32,
)
def encode_packet(packet: VideoPacket) -> bytes:
"""Serialize a packet and calculate its packet CRC32."""
if not isinstance(packet, VideoPacket):
raise TypeError("packet must be VideoPacket")
_validate_packet_fields(packet)
header_with_zero_crc = _pack_header(packet, 0)
packet_crc32 = crc32(header_with_zero_crc + packet.payload)
return _pack_header(packet, packet_crc32) + packet.payload
def decode_packet(wire_packet: bytes) -> VideoPacket:
"""Deserialize a packet and validate its fixed header and packet CRC32."""
if not isinstance(wire_packet, (bytes, bytearray)):
raise TypeError("wire_packet must be bytes or bytearray")
wire_packet = bytes(wire_packet)
if len(wire_packet) < HEADER_SIZE + 1:
raise VideoPacketError("packet is shorter than header plus payload")
(
magic,
version,
object_type_value,
header_size,
composite_frame_id,
fragment_index,
fragment_count,
payload_length,
flags,
jpeg_size,
object_crc32,
received_packet_crc32,
) = struct.unpack(HEADER_FORMAT, wire_packet[:HEADER_SIZE])
if magic != MAGIC:
raise VideoPacketError("invalid packet magic")
if version != VERSION:
raise VideoPacketError("unsupported packet version")
if header_size != HEADER_SIZE:
raise VideoPacketError("invalid fixed header size")
if flags != 0:
raise VideoPacketError("reserved flags must be zero")
try:
object_type = ObjectType(object_type_value)
except ValueError as error:
raise VideoPacketError("unsupported object_type") from error
if len(wire_packet) != HEADER_SIZE + payload_length:
raise VideoPacketError("packet length does not match payload_length")
packet = VideoPacket(
composite_frame_id=composite_frame_id,
object_type=object_type,
fragment_index=fragment_index,
fragment_count=fragment_count,
jpeg_size=jpeg_size,
object_crc32=object_crc32,
payload=wire_packet[HEADER_SIZE:],
packet_crc32=received_packet_crc32,
)
_validate_packet_fields(packet)
calculated_packet_crc32 = crc32(
_pack_header(packet, 0) + packet.payload
)
if calculated_packet_crc32 != received_packet_crc32:
raise PacketCRCError(
"packet CRC mismatch: "
f"received 0x{received_packet_crc32:08X}, "
f"calculated 0x{calculated_packet_crc32:08X}"
)
return packet
def packetize_jpeg(
jpeg: bytes,
composite_frame_id: int,
object_type: ObjectType,
max_payload_length: int,
) -> list[bytes]:
"""Split one non-empty JPEG object into serialized packets."""
if not isinstance(jpeg, (bytes, bytearray)):
raise TypeError("jpeg must be bytes or bytearray")
jpeg = bytes(jpeg)
if not jpeg:
raise ValueError("JPEG object must not be empty")
if len(jpeg) > MAX_JPEG_SIZE:
raise ValueError("JPEG object is larger than uint32")
if not 1 <= max_payload_length <= MAX_PAYLOAD_LENGTH:
raise ValueError("max_payload_length is outside uint16")
if not 0 <= composite_frame_id <= 0xFFFFFFFF:
raise ValueError("composite_frame_id is outside uint32")
try:
object_type = ObjectType(object_type)
except ValueError as error:
raise ValueError("unsupported object_type") from error
fragment_count = (
len(jpeg) + max_payload_length - 1
) // max_payload_length
if fragment_count > MAX_FRAGMENT_COUNT:
raise ValueError("JPEG needs more than 65535 fragments")
object_crc32 = crc32(jpeg)
packets = []
for fragment_index in range(fragment_count):
start = fragment_index * max_payload_length
payload = jpeg[start:start + max_payload_length]
packets.append(
encode_packet(
VideoPacket(
composite_frame_id=composite_frame_id,
object_type=object_type,
fragment_index=fragment_index,
fragment_count=fragment_count,
jpeg_size=len(jpeg),
object_crc32=object_crc32,
payload=payload,
)
)
)
return packets
@dataclass
class _ObjectAssembly:
composite_frame_id: int
object_type: ObjectType
fragment_count: int
jpeg_size: int
object_crc32: int
fragments: dict[int, bytes] = field(default_factory=dict)
@classmethod
def from_packet(cls, packet: VideoPacket) -> "_ObjectAssembly":
return cls(
composite_frame_id=packet.composite_frame_id,
object_type=packet.object_type,
fragment_count=packet.fragment_count,
jpeg_size=packet.jpeg_size,
object_crc32=packet.object_crc32,
)
def add(self, packet: VideoPacket) -> bool:
metadata = (
packet.composite_frame_id,
packet.object_type,
packet.fragment_count,
packet.jpeg_size,
packet.object_crc32,
)
expected = (
self.composite_frame_id,
self.object_type,
self.fragment_count,
self.jpeg_size,
self.object_crc32,
)
if metadata != expected:
raise ObjectConsistencyError(
"fragment metadata conflicts with object assembly"
)
existing = self.fragments.get(packet.fragment_index)
if existing is not None:
if existing != packet.payload:
raise ObjectConsistencyError(
"different payload for an existing fragment index"
)
return False
self.fragments[packet.fragment_index] = packet.payload
return True
def missing_fragments(self) -> tuple[int, ...]:
return tuple(
index
for index in range(self.fragment_count)
if index not in self.fragments
)
def assemble_if_complete(self) -> bytes | None:
if self.missing_fragments():
return None
jpeg = b"".join(
self.fragments[index]
for index in range(self.fragment_count)
)
if len(jpeg) != self.jpeg_size:
raise ObjectConsistencyError(
"reassembled JPEG length does not match jpeg_size"
)
calculated_object_crc32 = crc32(jpeg)
if calculated_object_crc32 != self.object_crc32:
raise ObjectCRCError(
"object CRC mismatch: "
f"received 0x{self.object_crc32:08X}, "
f"calculated 0x{calculated_object_crc32:08X}"
)
return jpeg
class CompositeReassembler:
"""Reassemble interleaved BASE/ROI packets and publish atomic frames."""
def __init__(self) -> None:
self._objects: dict[
tuple[int, ObjectType], _ObjectAssembly
] = {}
self._completed: dict[
int, dict[ObjectType, bytes]
] = {}
self._finalized_frame_ids: set[int] = set()
self.duplicate_packets = 0
def ingest(self, wire_packet: bytes) -> CompositeFrame | None:
"""Accept one packet and return a frame only when BASE and ROI exist."""
packet = decode_packet(wire_packet)
if packet.composite_frame_id in self._finalized_frame_ids:
self.duplicate_packets += 1
return None
key = (packet.composite_frame_id, packet.object_type)
assembly = self._objects.get(key)
if assembly is None:
assembly = _ObjectAssembly.from_packet(packet)
self._objects[key] = assembly
if not assembly.add(packet):
self.duplicate_packets += 1
return None
jpeg = assembly.assemble_if_complete()
if jpeg is None:
return None
frame_parts = self._completed.setdefault(
packet.composite_frame_id, {}
)
frame_parts[packet.object_type] = jpeg
if not all(
object_type in frame_parts
for object_type in (ObjectType.BASE, ObjectType.ROI)
):
return None
frame = CompositeFrame(
composite_frame_id=packet.composite_frame_id,
base_jpeg=frame_parts[ObjectType.BASE],
roi_jpeg=frame_parts[ObjectType.ROI],
)
self._finalized_frame_ids.add(packet.composite_frame_id)
self._completed.pop(packet.composite_frame_id, None)
for object_type in (ObjectType.BASE, ObjectType.ROI):
self._objects.pop(
(packet.composite_frame_id, object_type), None
)
return frame
def missing_fragments(
self,
composite_frame_id: int,
object_type: ObjectType,
) -> tuple[int, ...] | None:
"""Return missing indexes, or None if this object has not started."""
assembly = self._objects.get(
(composite_frame_id, ObjectType(object_type))
)
if assembly is None:
return None
return assembly.missing_fragments()
def object_is_complete(
self,
composite_frame_id: int,
object_type: ObjectType,
) -> bool:
"""Report whether one CRC-validated object awaits its counterpart."""
return ObjectType(object_type) in self._completed.get(
composite_frame_id, {}
)

View File

@@ -0,0 +1,875 @@
"""
Lab025. Приём и программная WFM-демодуляция FM-радиостанции.
Схема подключения:
антенна 40860 МГц -> RX1
Используется только приёмный канал RX1. Передатчики TX1 и TX2
не используются. Необработанные IQ-сэмплы на диск не сохраняются.
"""
from pathlib import Path
import adi
import matplotlib
import numpy as np
from scipy import signal
from scipy.io import wavfile
# Для лабораторной графики сохраняется в файлы без блокирующих окон.
matplotlib.use("Agg")
import matplotlib.pyplot as plt
# ---------------------------------------------------------------------
# Параметры приёмника Pluto+
# ---------------------------------------------------------------------
PLUTO_URI = "ip:192.168.2.1"
STATION_FREQUENCY_HZ = 100_100_000
LO_OFFSET_HZ = 250_000
RX_LO_FREQUENCY_HZ = (
STATION_FREQUENCY_HZ + LO_OFFSET_HZ
)
SAMPLE_RATE_HZ = 2_400_000
RX_BANDWIDTH_HZ = 1_500_000
RX_BUFFER_SIZE = 65_536
DISCARD_BUFFER_COUNT = 3
CAPTURE_BUFFER_COUNT = 128
# ---------------------------------------------------------------------
# Параметры обработки сигнала
# ---------------------------------------------------------------------
CHANNEL_DECIMATION = 10
CHANNEL_SAMPLE_RATE_HZ = 240_000
AUDIO_CUTOFF_HZ = 15_000
AUDIO_SAMPLE_RATE_HZ = 48_000
DEEMPHASIS_TIME_CONSTANT_SECONDS = 50e-6
AUDIO_TRANSIENT_DURATION_SECONDS = 0.05
AUDIO_REFERENCE_PERCENTILE = 99.5
AUDIO_TARGET_LEVEL = 0.85
MINIMUM_AUDIO_REFERENCE_PEAK = 1e-12
RF_WELCH_MAXIMUM_SAMPLE_COUNT = 1_048_576
RF_WELCH_SEGMENT_LENGTH = 8_192
RF_WELCH_OVERLAP_LENGTH = 4_096
AUDIO_WELCH_SEGMENT_LENGTH = 8_192
# ---------------------------------------------------------------------
# Выходные файлы
# ---------------------------------------------------------------------
OUTPUT_DIRECTORY = Path("data/processed/lab025")
WAV_FILE_PATH = (
OUTPUT_DIRECTORY / "lab025_wfm_audio_100_1mhz.wav"
)
RF_SPECTRUM_FILE_PATH = (
OUTPUT_DIRECTORY / "lab025_rf_spectrum.png"
)
AUDIO_WAVEFORM_FILE_PATH = (
OUTPUT_DIRECTORY / "lab025_audio_waveform.png"
)
AUDIO_SPECTRUM_FILE_PATH = (
OUTPUT_DIRECTORY / "lab025_audio_spectrum.png"
)
REPORT_FILE_PATH = OUTPUT_DIRECTORY / "lab025_report.txt"
def configure_receiver() -> adi.Pluto:
"""
Подключается к Pluto+ и настраивает только приёмный канал RX1.
Частота гетеродина смещена на 250 кГц выше частоты станции.
Благодаря этому полезный сигнал не совпадает с аппаратным DC-пиком
в центре комплексной полосы приёмника.
"""
print("Подключение к Pluto+...")
sdr = adi.Pluto(uri=PLUTO_URI)
# Используем только первый приёмный канал RX1.
sdr.rx_enabled_channels = [0]
sdr.sample_rate = SAMPLE_RATE_HZ
sdr.rx_lo = RX_LO_FREQUENCY_HZ
sdr.rx_rf_bandwidth = RX_BANDWIDTH_HZ
# Медленная АРУ подходит для приёма вещательной FM-станции.
sdr.gain_control_mode_chan0 = "slow_attack"
sdr.rx_buffer_size = RX_BUFFER_SIZE
return sdr
def receive_samples(sdr: adi.Pluto) -> np.ndarray:
"""
Получает последовательность комплексных IQ-буферов.
Первые буферы отбрасываются, поскольку после настройки приёмника
в них могут присутствовать переходные процессы АРУ и фильтров.
Рабочие буферы объединяются только в оперативной памяти.
"""
print()
print(
"Отбрасывание переходных буферов: "
f"{DISCARD_BUFFER_COUNT}..."
)
for _ in range(DISCARD_BUFFER_COUNT):
_ = sdr.rx()
print("Получение рабочих IQ-буферов...")
received_buffers: list[np.ndarray] = []
for buffer_number in range(1, CAPTURE_BUFFER_COUNT + 1):
received_buffer = np.asarray(
sdr.rx(),
dtype=np.complex64,
)
if received_buffer.size == 0:
raise RuntimeError(
"Pluto+ вернул пустой рабочий IQ-буфер."
)
received_buffers.append(received_buffer)
if (
buffer_number % 16 == 0
or buffer_number == CAPTURE_BUFFER_COUNT
):
print(
f" Принято {buffer_number:3d}/"
f"{CAPTURE_BUFFER_COUNT} буферов"
)
if not received_buffers:
raise RuntimeError("Pluto+ не вернул IQ-сэмплы.")
combined_samples = np.concatenate(received_buffers)
if combined_samples.size == 0:
raise RuntimeError("После объединения получен пустой IQ-массив.")
if not np.all(np.isfinite(combined_samples)):
raise RuntimeError("В IQ-сэмплах обнаружены NaN или Inf.")
return np.asarray(combined_samples, dtype=np.complex64)
def shift_station_to_baseband(
samples: np.ndarray,
sample_rate_hz: float,
frequency_shift_hz: float,
) -> np.ndarray:
"""
Переносит выбранную станцию в центр цифровой полосы.
При положительном смещении комплексный генератор переносит сигнал,
расположенный на отрицательной относительной частоте, к 0 Гц.
"""
if samples.size == 0:
raise RuntimeError(
"Невозможно выполнить цифровой перенос пустого IQ-массива."
)
sample_indices = np.arange(
len(samples),
dtype=np.float64,
)
digital_oscillator = np.exp(
1j
* 2.0
* np.pi
* frequency_shift_hz
* sample_indices
/ sample_rate_hz
).astype(np.complex64)
centered_samples = samples * digital_oscillator
if not np.all(np.isfinite(centered_samples)):
raise RuntimeError(
"После цифрового переноса обнаружены NaN или Inf."
)
return np.asarray(centered_samples, dtype=np.complex64)
def extract_wfm_channel(
centered_samples: np.ndarray,
) -> np.ndarray:
"""
Фильтрует WFM-канал и понижает частоту до 240 кГц.
Полигармоническая передискретизация одновременно выполняет
низкочастотную фильтрацию и децимацию в десять раз.
"""
calculated_sample_rate_hz = (
SAMPLE_RATE_HZ / CHANNEL_DECIMATION
)
if not np.isclose(
calculated_sample_rate_hz,
CHANNEL_SAMPLE_RATE_HZ,
):
raise RuntimeError(
"Частота канального сигнала после децимации "
"не равна 240 кГц."
)
channel_samples = signal.resample_poly(
centered_samples,
up=1,
down=CHANNEL_DECIMATION,
window=("kaiser", 8.0),
)
if channel_samples.size == 0:
raise RuntimeError("После выделения WFM-канала массив пуст.")
if not np.all(np.isfinite(channel_samples)):
raise RuntimeError(
"После выделения WFM-канала обнаружены NaN или Inf."
)
return np.asarray(channel_samples, dtype=np.complex64)
def demodulate_fm(
channel_samples: np.ndarray,
) -> np.ndarray:
"""
Выполняет частотную демодуляцию по фазовой разности отсчётов.
Результат представляет монофонический композитный FM-сигнал
до звуковой фильтрации и коррекции предыскажений.
"""
if channel_samples.size < 2:
raise RuntimeError(
"Недостаточно канальных отсчётов для FM-демодуляции."
)
phase_difference = np.angle(
channel_samples[1:]
* np.conj(channel_samples[:-1])
).astype(np.float64)
demodulated = phase_difference - np.mean(phase_difference)
if demodulated.size == 0:
raise RuntimeError("FM-дискриминатор вернул пустой массив.")
if not np.all(np.isfinite(demodulated)):
raise RuntimeError(
"После FM-демодуляции обнаружены NaN или Inf."
)
return demodulated
def lowpass_audio(
demodulated_samples: np.ndarray,
sample_rate_hz: float,
) -> np.ndarray:
"""
Выделяет монофонический звук L+R в полосе до 15 кГц.
Фильтр подавляет стереопилот 19 кГц, стереоразностную часть,
RDS и внеполосный высокочастотный шум.
"""
audio_sos = signal.butter(
6,
AUDIO_CUTOFF_HZ,
btype="lowpass",
fs=sample_rate_hz,
output="sos",
)
filtered_audio = signal.sosfiltfilt(
audio_sos,
demodulated_samples,
)
if filtered_audio.size == 0:
raise RuntimeError("Звуковой фильтр вернул пустой массив.")
if not np.all(np.isfinite(filtered_audio)):
raise RuntimeError(
"После звукового фильтра обнаружены NaN или Inf."
)
return np.asarray(filtered_audio, dtype=np.float64)
def apply_deemphasis(
audio_samples: np.ndarray,
sample_rate_hz: float,
time_constant_seconds: float,
) -> np.ndarray:
"""
Выполняет европейскую FM-коррекцию предыскажений 50 мкс.
Используется устойчивый однополюсный рекурсивный фильтр,
реализованный функцией scipy.signal.lfilter.
"""
alpha = np.exp(
-1.0
/ (
sample_rate_hz
* time_constant_seconds
)
)
deemphasized = signal.lfilter(
[1.0 - alpha],
[1.0, -alpha],
audio_samples,
)
deemphasized = deemphasized - np.mean(deemphasized)
if deemphasized.size == 0:
raise RuntimeError("De-emphasis вернул пустой массив.")
if not np.all(np.isfinite(deemphasized)):
raise RuntimeError(
"После de-emphasis обнаружены NaN или Inf."
)
return np.asarray(deemphasized, dtype=np.float64)
def resample_audio_to_output_rate(
audio_samples: np.ndarray,
) -> np.ndarray:
"""
Преобразует звуковой сигнал с 240 кГц в выходные 48 кГц.
"""
output_decimation = 5
calculated_output_rate_hz = (
CHANNEL_SAMPLE_RATE_HZ / output_decimation
)
if not np.isclose(
calculated_output_rate_hz,
AUDIO_SAMPLE_RATE_HZ,
):
raise RuntimeError(
"Рассчитанная частота WAV не равна 48 кГц."
)
output_audio = signal.resample_poly(
audio_samples,
up=1,
down=output_decimation,
)
if output_audio.size == 0:
raise RuntimeError(
"После преобразования в 48 кГц получен пустой массив."
)
if not np.all(np.isfinite(output_audio)):
raise RuntimeError(
"После преобразования звука обнаружены NaN или Inf."
)
return np.asarray(output_audio, dtype=np.float64)
def convert_audio_to_pcm16(
audio_samples: np.ndarray,
) -> np.ndarray:
"""
Удаляет переходный участок, нормализует звук и создаёт PCM16.
Опорный уровень определяется по 99,5-му процентилю модуля,
поэтому единичный выброс не делает весь WAV слишком тихим.
"""
if audio_samples.size == 0:
raise RuntimeError("Невозможно нормализовать пустой звук.")
centered_audio = audio_samples - np.mean(audio_samples)
transient_sample_count = int(
round(
AUDIO_TRANSIENT_DURATION_SECONDS
* AUDIO_SAMPLE_RATE_HZ
)
)
if centered_audio.size <= transient_sample_count:
raise RuntimeError(
"Звуковой массив короче переходного участка 0,05 с."
)
centered_audio = centered_audio[transient_sample_count:]
reference_peak = float(
np.percentile(
np.abs(centered_audio),
AUDIO_REFERENCE_PERCENTILE,
)
)
if (
not np.isfinite(reference_peak)
or reference_peak <= MINIMUM_AUDIO_REFERENCE_PEAK
):
raise RuntimeError(
"Сигнал слишком мал для безопасной нормализации PCM16."
)
normalized_audio = (
centered_audio
/ reference_peak
* AUDIO_TARGET_LEVEL
)
normalized_audio = np.clip(
normalized_audio,
-1.0,
1.0,
)
pcm16_audio = np.round(
normalized_audio * np.iinfo(np.int16).max
).astype(np.int16)
if pcm16_audio.size == 0:
raise RuntimeError("После преобразования PCM16 массив пуст.")
return pcm16_audio
def calculate_rf_spectrum(
samples: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
"""
Рассчитывает ограниченный спектр исходного IQ методом Уэлча.
Для графика используется не более 1 048 576 отсчётов, поэтому
полная восьмимиллионная запись не передаётся в одну большую FFT.
"""
analysis_sample_count = min(
len(samples),
RF_WELCH_MAXIMUM_SAMPLE_COUNT,
)
analysis_samples = samples[:analysis_sample_count]
relative_frequencies_hz, power_density = signal.welch(
analysis_samples,
fs=SAMPLE_RATE_HZ,
window="hann",
nperseg=RF_WELCH_SEGMENT_LENGTH,
noverlap=RF_WELCH_OVERLAP_LENGTH,
detrend=False,
return_onesided=False,
scaling="density",
)
relative_frequencies_hz = np.fft.fftshift(
relative_frequencies_hz
)
power_density = np.fft.fftshift(power_density)
minimum_positive_value = np.finfo(np.float64).tiny
power_db = 10.0 * np.log10(
power_density + minimum_positive_value
)
power_db -= np.max(power_db)
return relative_frequencies_hz, power_db
def create_rf_spectrum_graph(samples: np.ndarray) -> None:
"""
Сохраняет спектр до цифрового переноса станции в центр.
"""
frequencies_hz, power_db = calculate_rf_spectrum(samples)
figure, axes = plt.subplots(figsize=(13, 7))
axes.plot(
frequencies_hz / 1e3,
power_db,
linewidth=0.8,
)
expected_station_offset_hz = -LO_OFFSET_HZ
axes.axvline(
expected_station_offset_hz / 1e3,
color="tab:red",
linestyle="--",
linewidth=1.4,
label=(
"Ожидаемая станция 100,100 МГц "
"(-250 кГц)"
),
)
axes.set_title(
"Lab025. Спектр принятого IQ до цифрового переноса\n"
"RX LO = 100,350 МГц; станция = 100,100 МГц"
)
axes.set_xlabel("Относительная частота относительно RX LO, кГц")
axes.set_ylabel("Относительная мощность, дБ")
axes.set_ylim(-90, 5)
axes.grid(True)
axes.legend()
figure.tight_layout()
figure.savefig(RF_SPECTRUM_FILE_PATH, dpi=160)
plt.close(figure)
def create_audio_waveform_graph(pcm16_audio: np.ndarray) -> None:
"""
Сохраняет первые 0,1 секунды нормированного звука.
"""
displayed_sample_count = min(
len(pcm16_audio),
int(round(0.1 * AUDIO_SAMPLE_RATE_HZ)),
)
displayed_audio = (
pcm16_audio[:displayed_sample_count].astype(np.float64)
/ np.iinfo(np.int16).max
)
time_seconds = (
np.arange(displayed_sample_count, dtype=np.float64)
/ AUDIO_SAMPLE_RATE_HZ
)
figure, axes = plt.subplots(figsize=(12, 5))
axes.plot(time_seconds, displayed_audio, linewidth=0.8)
axes.set_title("Lab025. Первые 0,1 секунды демодулированного звука")
axes.set_xlabel("Время, с")
axes.set_ylabel("Нормированная амплитуда")
axes.set_ylim(-1.05, 1.05)
axes.grid(True)
figure.tight_layout()
figure.savefig(AUDIO_WAVEFORM_FILE_PATH, dpi=160)
plt.close(figure)
def create_audio_spectrum_graph(pcm16_audio: np.ndarray) -> None:
"""
Рассчитывает методом Уэлча и сохраняет односторонний спектр WAV.
"""
normalized_audio = (
pcm16_audio.astype(np.float64)
/ np.iinfo(np.int16).max
)
segment_length = min(
AUDIO_WELCH_SEGMENT_LENGTH,
len(normalized_audio),
)
frequencies_hz, power_density = signal.welch(
normalized_audio,
fs=AUDIO_SAMPLE_RATE_HZ,
window="hann",
nperseg=segment_length,
noverlap=segment_length // 2,
detrend="constant",
return_onesided=True,
scaling="density",
)
minimum_positive_value = np.finfo(np.float64).tiny
power_db = 10.0 * np.log10(
power_density + minimum_positive_value
)
power_db -= np.max(power_db)
figure, axes = plt.subplots(figsize=(12, 6))
axes.plot(frequencies_hz / 1e3, power_db, linewidth=0.9)
axes.set_xlim(0, 20)
axes.set_ylim(-100, 5)
axes.set_title("Lab025. Спектр демодулированного монофонического звука")
axes.set_xlabel("Частота, кГц")
axes.set_ylabel("Относительная спектральная плотность, дБ")
axes.grid(True)
figure.tight_layout()
figure.savefig(AUDIO_SPECTRUM_FILE_PATH, dpi=160)
plt.close(figure)
def calculate_audio_statistics(
pcm16_audio: np.ndarray,
) -> tuple[float, float, float]:
"""
Возвращает нормированные RMS, пик и процент предельных отсчётов.
"""
normalized_audio = (
pcm16_audio.astype(np.float64)
/ np.iinfo(np.int16).max
)
rms_value = float(
np.sqrt(np.mean(normalized_audio ** 2))
)
peak_value = float(np.max(np.abs(normalized_audio)))
clipped_sample_count = int(
np.count_nonzero(
(pcm16_audio == np.iinfo(np.int16).min)
| (pcm16_audio == np.iinfo(np.int16).max)
)
)
clipping_percentage = (
100.0
* clipped_sample_count
/ len(pcm16_audio)
)
return rms_value, peak_value, clipping_percentage
def save_report(
iq_sample_count: int,
iq_duration_seconds: float,
pcm16_audio: np.ndarray,
audio_rms: float,
audio_peak: float,
clipping_percentage: float,
) -> None:
"""
Создаёт текстовый отчёт с параметрами приёма и WAV-файла.
"""
wav_duration_seconds = (
len(pcm16_audio) / AUDIO_SAMPLE_RATE_HZ
)
report_lines = [
"Lab025. Приём и программная WFM-демодуляция",
"",
f"URI Pluto+: {PLUTO_URI}",
f"Частота станции: {STATION_FREQUENCY_HZ} Гц",
f"RX LO: {RX_LO_FREQUENCY_HZ} Гц",
f"Цифровое смещение: +{LO_OFFSET_HZ} Гц",
f"Частота дискретизации RX: {SAMPLE_RATE_HZ} Гц",
f"Полоса RX: {RX_BANDWIDTH_HZ} Гц",
f"Количество рабочих буферов: {CAPTURE_BUFFER_COUNT}",
f"Количество IQ-сэмплов: {iq_sample_count}",
f"Длительность IQ-записи: {iq_duration_seconds:.6f} с",
(
"Частота канального сигнала после децимации: "
f"{CHANNEL_SAMPLE_RATE_HZ} Гц"
),
f"Длительность итогового WAV: {wav_duration_seconds:.6f} с",
f"Частота WAV: {AUDIO_SAMPLE_RATE_HZ} Гц",
"Тип PCM: signed PCM16, mono",
f"RMS итогового аудио: {audio_rms:.6f}",
f"Пиковая амплитуда: {audio_peak:.6f}",
f"Отсчёты на границе PCM16: {clipping_percentage:.6f} %",
"",
"Выходные файлы:",
f"WAV: {WAV_FILE_PATH}",
f"Радиоспектр: {RF_SPECTRUM_FILE_PATH}",
f"Звуковая волна: {AUDIO_WAVEFORM_FILE_PATH}",
f"Спектр звука: {AUDIO_SPECTRUM_FILE_PATH}",
f"Отчёт: {REPORT_FILE_PATH}",
"",
"Использован только приёмный канал RX1.",
"Передатчики TX1 и TX2 не использовались.",
"Необработанные IQ-сэмплы на диск не сохранялись.",
]
REPORT_FILE_PATH.write_text(
"\n".join(report_lines),
encoding="utf-8",
)
def main() -> None:
"""
Выполняет полный цикл приёма, WFM-демодуляции и сохранения WAV.
"""
OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True)
sdr = None
try:
sdr = configure_receiver()
print()
print("Параметры RX:")
print(f" URI: {PLUTO_URI}")
print(
f" Станция: "
f"{STATION_FREQUENCY_HZ / 1e6:.3f} МГц"
)
print(
f" RX LO: "
f"{sdr.rx_lo / 1e6:.3f} МГц"
)
print(
f" Частота дискретизации: "
f"{sdr.sample_rate / 1e6:.3f} Мвыб/с"
)
print(
f" Полоса RX: "
f"{sdr.rx_rf_bandwidth / 1e6:.3f} МГц"
)
print(
f" Режим усиления: "
f"{sdr.gain_control_mode_chan0}"
)
print(f" Размер буфера: {sdr.rx_buffer_size}")
print(" Активный канал: RX1")
samples = receive_samples(sdr)
iq_sample_count = len(samples)
iq_duration_seconds = iq_sample_count / SAMPLE_RATE_HZ
print()
print(f"Количество IQ-сэмплов: {iq_sample_count}")
print(f"Длительность записи: {iq_duration_seconds:.3f} с")
# Удаляем остаточную комплексную постоянную составляющую.
samples = samples - np.mean(samples)
print()
print("Цифровой перенос станции в центр полосы...")
centered_samples = shift_station_to_baseband(
samples=samples,
sample_rate_hz=SAMPLE_RATE_HZ,
frequency_shift_hz=LO_OFFSET_HZ,
)
print("Фильтрация WFM-канала и децимация до 240 кГц...")
channel_samples = extract_wfm_channel(centered_samples)
print("FM-демодуляция...")
demodulated_samples = demodulate_fm(channel_samples)
print("Звуковой low-pass фильтр 015 кГц...")
filtered_audio = lowpass_audio(
demodulated_samples=demodulated_samples,
sample_rate_hz=CHANNEL_SAMPLE_RATE_HZ,
)
print("De-emphasis 50 мкс...")
deemphasized_audio = apply_deemphasis(
audio_samples=filtered_audio,
sample_rate_hz=CHANNEL_SAMPLE_RATE_HZ,
time_constant_seconds=(
DEEMPHASIS_TIME_CONSTANT_SECONDS
),
)
print("Преобразование звука в 48 кГц...")
output_audio = resample_audio_to_output_rate(
deemphasized_audio
)
pcm16_audio = convert_audio_to_pcm16(output_audio)
audio_rms, audio_peak, clipping_percentage = (
calculate_audio_statistics(pcm16_audio)
)
print("Сохранение WAV...")
wavfile.write(
WAV_FILE_PATH,
AUDIO_SAMPLE_RATE_HZ,
pcm16_audio,
)
if not WAV_FILE_PATH.is_file():
raise RuntimeError("Выходной WAV-файл не создан.")
print("Сохранение графиков...")
create_rf_spectrum_graph(samples)
create_audio_waveform_graph(pcm16_audio)
create_audio_spectrum_graph(pcm16_audio)
print("Создание текстового отчёта...")
save_report(
iq_sample_count=iq_sample_count,
iq_duration_seconds=iq_duration_seconds,
pcm16_audio=pcm16_audio,
audio_rms=audio_rms,
audio_peak=audio_peak,
clipping_percentage=clipping_percentage,
)
print()
print("Созданы файлы:")
print(f" WAV: {WAV_FILE_PATH}")
print(f" Радиоспектр: {RF_SPECTRUM_FILE_PATH}")
print(f" Звуковая волна: {AUDIO_WAVEFORM_FILE_PATH}")
print(f" Спектр звука: {AUDIO_SPECTRUM_FILE_PATH}")
print(f" Отчёт: {REPORT_FILE_PATH}")
print()
print(f"RMS аудио: {audio_rms:.6f}")
print(f"Пиковая амплитуда: {audio_peak:.6f}")
print(
f"Отсчёты на границе PCM: "
f"{clipping_percentage:.6f} %"
)
print()
print("Lab025 выполнена успешно.")
print()
print("Для прослушивания откройте:")
print(WAV_FILE_PATH)
finally:
if sdr is not None:
destroy_buffer = getattr(
sdr,
"rx_destroy_buffer",
None,
)
if callable(destroy_buffer):
try:
destroy_buffer()
except Exception as cleanup_error:
print(
"Предупреждение: не удалось освободить "
f"RX-буфер: {cleanup_error}"
)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,925 @@
"""
Lab027. Динамическое превью временного обновления BASE и ROI.
Скрипт не повторяет расчёт benchmark Lab027 и не изменяет его
результаты. Он создаёт одно игнорируемое Git preview-видео с четырьмя
фиксированными профилями. Для каждого профиля независимо удерживаются
последние декодированные JPEG-состояния BASE и ROI.
Отдельные JPEG-файлы не сохраняются: кодирование и декодирование
выполняются только в памяти средствами OpenCV.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import cv2
import numpy as np
SOURCE_VIDEO_PATH = Path("data/raw/lab026_rover_source.mp4")
PREVIEW_DIRECTORY = Path("data/raw/lab027_previews")
MP4_PREVIEW_PATH = (
PREVIEW_DIRECTORY / "lab027_roi_temporal_preview.mp4"
)
AVI_PREVIEW_PATH = (
PREVIEW_DIRECTORY / "lab027_roi_temporal_preview.avi"
)
OUTPUT_FPS = 30.0
PANEL_WIDTH = 640
PANEL_HEIGHT = 360
OUTPUT_WIDTH = PANEL_WIDTH * 2
OUTPUT_HEIGHT = PANEL_HEIGHT * 2
ROI_X_MIN = 0.20
ROI_X_MAX = 0.80
ROI_Y_MIN = 0.42
ROI_Y_MAX = 1.00
FRAME_TIME_EPSILON_SECONDS = 1e-9
NEW_UPDATE_LABEL_SECONDS = 0.15
@dataclass(frozen=True)
class PreviewProfile:
"""
Описывает один из четырёх фиксированных preview-профилей.
"""
name: str
base_width: int
base_height: int
base_fps: float
base_quality: int
roi_enabled: bool
roi_width: int
roi_height: int
roi_fps: float
roi_quality: int
measured_payload_kbps: float
@dataclass
class ProfileState:
"""
Хранит независимое временное состояние BASE и ROI профиля.
"""
latest_base: np.ndarray | None = None
latest_roi: np.ndarray | None = None
next_base_time: float = 0.0
next_roi_time: float = 0.0
last_base_update_time: float = 0.0
last_roi_update_time: float = 0.0
base_update_count: int = 0
roi_update_count: int = 0
def read_video_metadata(
source_path: Path,
) -> tuple[int, int, float, int, float]:
"""
Читает и проверяет параметры исходного видео.
"""
if not source_path.exists():
raise RuntimeError(
f"Исходный видеофайл отсутствует: {source_path}"
)
capture = cv2.VideoCapture(str(source_path))
if not capture.isOpened():
raise RuntimeError(
f"OpenCV не смог открыть видео: {source_path}"
)
try:
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = float(capture.get(cv2.CAP_PROP_FPS))
frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
finally:
capture.release()
if width <= 0 or height <= 0:
raise RuntimeError(
"OpenCV вернул некорректное разрешение видео."
)
if fps <= 0.0:
raise RuntimeError("FPS исходного видео равен нулю.")
if frame_count <= 0:
raise RuntimeError("Число кадров исходного видео равно нулю.")
duration_seconds = frame_count / fps
return width, height, fps, frame_count, duration_seconds
def build_profiles() -> list[PreviewProfile]:
"""
Создаёт ровно четыре согласованных preview-профиля.
"""
profiles = [
PreviewProfile(
name="baseline",
base_width=320,
base_height=180,
base_fps=2.0,
base_quality=40,
roi_enabled=False,
roi_width=0,
roi_height=0,
roi_fps=0.0,
roi_quality=0,
measured_payload_kbps=80.27,
),
PreviewProfile(
name="ROI normal",
base_width=240,
base_height=135,
base_fps=1.0,
base_quality=25,
roi_enabled=True,
roi_width=320,
roi_height=180,
roi_fps=2.0,
roi_quality=35,
measured_payload_kbps=93.28,
),
PreviewProfile(
name="ROI economy",
base_width=160,
base_height=90,
base_fps=1.0,
base_quality=25,
roi_enabled=True,
roi_width=320,
roi_height=180,
roi_fps=2.0,
roi_quality=35,
measured_payload_kbps=85.19,
),
PreviewProfile(
name="ROI degraded channel",
base_width=240,
base_height=135,
base_fps=1.0,
base_quality=20,
roi_enabled=True,
roi_width=320,
roi_height=180,
roi_fps=1.0,
roi_quality=30,
measured_payload_kbps=50.49,
),
]
if len(profiles) != 4:
raise RuntimeError(
"Preview Lab027 должно содержать ровно четыре профиля."
)
return profiles
def normalized_roi_to_pixels(
width: int,
height: int,
) -> tuple[int, int, int, int]:
"""
Переводит фиксированные нормализованные координаты ROI в пиксели.
Результат задаёт полуоткрытый прямоугольник:
x_min, y_min, x_max, y_max.
"""
if width <= 0 or height <= 0:
raise ValueError("Размер кадра должен быть положительным.")
x_min = int(round(width * ROI_X_MIN))
x_max = int(round(width * ROI_X_MAX))
y_min = int(round(height * ROI_Y_MIN))
y_max = int(round(height * ROI_Y_MAX))
x_min = min(max(x_min, 0), width - 1)
x_max = min(max(x_max, x_min + 1), width)
y_min = min(max(y_min, 0), height - 1)
y_max = min(max(y_max, y_min + 1), height)
return x_min, y_min, x_max, y_max
def should_update(
current_time: float,
next_update_time: float,
epsilon_seconds: float = FRAME_TIME_EPSILON_SECONDS,
) -> bool:
"""
Проверяет наступление времени очередного обновления потока.
"""
return (
current_time + epsilon_seconds
>= next_update_time
)
def encode_decode_grayscale_jpeg(
gray_frame: np.ndarray,
jpeg_quality: int,
) -> np.ndarray:
"""
Кодирует grayscale JPEG в памяти и декодирует его обратно.
"""
if gray_frame.ndim != 2:
raise RuntimeError(
"JPEG preview должен получать grayscale-кадр."
)
encoding_ok, encoded = cv2.imencode(
".jpg",
gray_frame,
[cv2.IMWRITE_JPEG_QUALITY, jpeg_quality],
)
if not encoding_ok or encoded is None or encoded.size == 0:
raise RuntimeError("OpenCV не смог закодировать JPEG.")
decoded = cv2.imdecode(
encoded,
cv2.IMREAD_GRAYSCALE,
)
if decoded is None or decoded.shape != gray_frame.shape:
raise RuntimeError(
"Декодированный JPEG имеет некорректный размер."
)
return decoded
def encode_decode_base(
source_frame: np.ndarray,
profile: PreviewProfile,
) -> np.ndarray:
"""
Формирует очередное декодированное состояние BASE.
"""
source_gray = cv2.cvtColor(
source_frame,
cv2.COLOR_BGR2GRAY,
)
resized_base = cv2.resize(
source_gray,
(profile.base_width, profile.base_height),
interpolation=cv2.INTER_AREA,
)
return encode_decode_grayscale_jpeg(
resized_base,
profile.base_quality,
)
def encode_decode_roi(
source_frame: np.ndarray,
source_roi: tuple[int, int, int, int],
profile: PreviewProfile,
) -> np.ndarray:
"""
Вырезает ROI исходного BGR-кадра и формирует JPEG-состояние.
"""
if not profile.roi_enabled:
raise RuntimeError(
"Нельзя кодировать ROI для отключённого ROI-профиля."
)
x_min, y_min, x_max, y_max = source_roi
roi_bgr = source_frame[y_min:y_max, x_min:x_max]
if roi_bgr.size == 0:
raise RuntimeError("Вырезана пустая область ROI.")
roi_gray = cv2.cvtColor(
roi_bgr,
cv2.COLOR_BGR2GRAY,
)
resized_roi = cv2.resize(
roi_gray,
(profile.roi_width, profile.roi_height),
interpolation=cv2.INTER_AREA,
)
return encode_decode_grayscale_jpeg(
resized_roi,
profile.roi_quality,
)
def reconstruct_panel(
profile: PreviewProfile,
state: ProfileState,
panel_roi: tuple[int, int, int, int],
) -> np.ndarray:
"""
Восстанавливает одну grayscale-панель размером 640x360.
"""
if state.latest_base is None:
raise RuntimeError("BASE-состояние ещё не создано.")
reconstructed = cv2.resize(
state.latest_base,
(PANEL_WIDTH, PANEL_HEIGHT),
interpolation=cv2.INTER_LINEAR,
)
if profile.roi_enabled:
if state.latest_roi is None:
raise RuntimeError("ROI-состояние ещё не создано.")
x_min, y_min, x_max, y_max = panel_roi
resized_roi = cv2.resize(
state.latest_roi,
(x_max - x_min, y_max - y_min),
interpolation=cv2.INTER_LINEAR,
)
reconstructed[y_min:y_max, x_min:x_max] = resized_roi
return reconstructed
def draw_text_line(
image: np.ndarray,
text: str,
y_position: int,
text_color: tuple[int, int, int] = (255, 255, 255),
) -> None:
"""
Рисует одну ASCII-строку служебной информации OpenCV.
"""
cv2.putText(
image,
text,
(10, y_position),
cv2.FONT_HERSHEY_SIMPLEX,
0.46,
text_color,
1,
cv2.LINE_AA,
)
def draw_panel_information(
reconstructed_gray: np.ndarray,
profile: PreviewProfile,
state: ProfileState,
current_time: float,
panel_roi: tuple[int, int, int, int],
) -> np.ndarray:
"""
Добавляет рамку ROI, параметры и индикаторы обновления панели.
"""
panel = cv2.cvtColor(
reconstructed_gray,
cv2.COLOR_GRAY2BGR,
)
if profile.roi_enabled:
cv2.rectangle(
panel,
(panel_roi[0], panel_roi[1]),
(panel_roi[2] - 1, panel_roi[3] - 1),
(0, 255, 255),
2,
)
overlay = panel.copy()
cv2.rectangle(
overlay,
(0, 0),
(PANEL_WIDTH - 1, 142),
(0, 0, 0),
thickness=-1,
)
cv2.addWeighted(
overlay,
0.72,
panel,
0.28,
0.0,
panel,
)
base_age = max(
0.0,
current_time - state.last_base_update_time,
)
roi_age = max(
0.0,
current_time - state.last_roi_update_time,
)
base_is_new = (
base_age <= NEW_UPDATE_LABEL_SECONDS
)
roi_is_new = (
profile.roi_enabled
and roi_age <= NEW_UPDATE_LABEL_SECONDS
)
draw_text_line(panel, profile.name, 20)
draw_text_line(
panel,
(
f"BASE: {profile.base_width}x{profile.base_height}, "
f"{profile.base_fps:g} fps, Q{profile.base_quality}"
),
41,
)
if profile.roi_enabled:
roi_text = (
f"ROI: {profile.roi_width}x{profile.roi_height}, "
f"{profile.roi_fps:g} fps, Q{profile.roi_quality}"
)
roi_age_text = f"{roi_age:.3f} s"
else:
roi_text = "ROI: disabled"
roi_age_text = "disabled"
draw_text_line(panel, roi_text, 62)
draw_text_line(
panel,
(
f"payload={profile.measured_payload_kbps:.2f} kbps, "
f"source time={current_time:.3f} s"
),
83,
)
draw_text_line(
panel,
(
f"since BASE={base_age:.3f} s, "
f"since ROI={roi_age_text}"
),
104,
)
update_labels: list[str] = []
if base_is_new:
update_labels.append("NEW BASE")
if roi_is_new:
update_labels.append("NEW ROI")
if update_labels:
draw_text_line(
panel,
" | ".join(update_labels),
130,
text_color=(0, 255, 0),
)
return panel
def compose_grid(panels: list[np.ndarray]) -> np.ndarray:
"""
Объединяет четыре панели в сетку 2x2 размером 1280x720.
"""
if len(panels) != 4:
raise RuntimeError(
"Для сетки preview требуется ровно четыре панели."
)
for panel in panels:
if panel.shape != (PANEL_HEIGHT, PANEL_WIDTH, 3):
raise RuntimeError(
f"Некорректный размер панели: {panel.shape}"
)
top_row = np.hstack((panels[0], panels[1]))
bottom_row = np.hstack((panels[2], panels[3]))
grid = np.vstack((top_row, bottom_row))
if grid.shape != (OUTPUT_HEIGHT, OUTPUT_WIDTH, 3):
raise RuntimeError(
f"Некорректный размер сетки: {grid.shape}"
)
return grid
def open_preview_writer() -> tuple[cv2.VideoWriter, Path, bool]:
"""
Открывает MP4 writer либо разрешённый fallback AVI/MJPG.
"""
mp4_writer = cv2.VideoWriter(
str(MP4_PREVIEW_PATH),
cv2.VideoWriter_fourcc(*"mp4v"),
OUTPUT_FPS,
(OUTPUT_WIDTH, OUTPUT_HEIGHT),
True,
)
if mp4_writer.isOpened():
return mp4_writer, MP4_PREVIEW_PATH, False
mp4_writer.release()
if MP4_PREVIEW_PATH.exists():
MP4_PREVIEW_PATH.unlink()
avi_writer = cv2.VideoWriter(
str(AVI_PREVIEW_PATH),
cv2.VideoWriter_fourcc(*"MJPG"),
OUTPUT_FPS,
(OUTPUT_WIDTH, OUTPUT_HEIGHT),
True,
)
if not avi_writer.isOpened():
avi_writer.release()
if AVI_PREVIEW_PATH.exists():
AVI_PREVIEW_PATH.unlink()
raise RuntimeError(
"OpenCV не смог открыть ни MP4, ни AVI writer."
)
return avi_writer, AVI_PREVIEW_PATH, True
def create_preview(
source_path: Path,
profiles: list[PreviewProfile],
source_width: int,
source_height: int,
source_fps: float,
expected_frame_count: int,
) -> tuple[Path, bool, int, list[ProfileState]]:
"""
Создаёт preview-видео с одним выходным кадром на исходный кадр.
"""
if len(profiles) != 4:
raise RuntimeError(
"Ожидалось ровно четыре preview-профиля."
)
PREVIEW_DIRECTORY.mkdir(
parents=True,
exist_ok=True,
)
capture = cv2.VideoCapture(str(source_path))
if not capture.isOpened():
raise RuntimeError(
f"OpenCV не смог открыть видео: {source_path}"
)
writer, preview_path, fallback_used = open_preview_writer()
source_roi = normalized_roi_to_pixels(
source_width,
source_height,
)
panel_roi = normalized_roi_to_pixels(
PANEL_WIDTH,
PANEL_HEIGHT,
)
states = [
ProfileState()
for _ in profiles
]
frame_index = 0
try:
while True:
frame_read, source_frame = capture.read()
if not frame_read:
break
if source_frame is None:
raise RuntimeError(
f"Получен пустой кадр {frame_index}."
)
if (
source_frame.shape[1] != source_width
or source_frame.shape[0] != source_height
):
raise RuntimeError(
"Размер кадра отличается от метаданных."
)
current_time = frame_index / source_fps
panels: list[np.ndarray] = []
for profile, state in zip(profiles, states):
if should_update(
current_time,
state.next_base_time,
):
state.latest_base = encode_decode_base(
source_frame,
profile,
)
state.last_base_update_time = current_time
state.next_base_time += 1.0 / profile.base_fps
state.base_update_count += 1
if (
profile.roi_enabled
and should_update(
current_time,
state.next_roi_time,
)
):
state.latest_roi = encode_decode_roi(
source_frame,
source_roi,
profile,
)
state.last_roi_update_time = current_time
state.next_roi_time += 1.0 / profile.roi_fps
state.roi_update_count += 1
reconstructed = reconstruct_panel(
profile,
state,
panel_roi,
)
panels.append(
draw_panel_information(
reconstructed,
profile,
state,
current_time,
panel_roi,
)
)
writer.write(compose_grid(panels))
frame_index += 1
if (
frame_index % 100 == 0
or frame_index == expected_frame_count
):
print(
f" Written frames: "
f"{frame_index}/{expected_frame_count}"
)
except Exception:
capture.release()
writer.release()
if preview_path.exists():
preview_path.unlink()
raise
finally:
capture.release()
writer.release()
if frame_index != expected_frame_count:
if preview_path.exists():
preview_path.unlink()
raise RuntimeError(
"Число записанных кадров не совпало с исходным: "
f"{frame_index} != {expected_frame_count}."
)
return (
preview_path,
fallback_used,
frame_index,
states,
)
def read_frame_at(
capture: cv2.VideoCapture,
frame_index: int,
) -> tuple[bool, tuple[int, ...] | None]:
"""
Читает один кадр preview по индексу для итоговой проверки.
"""
capture.set(
cv2.CAP_PROP_POS_FRAMES,
frame_index,
)
frame_read, frame = capture.read()
if not frame_read or frame is None:
return False, None
return True, frame.shape
def verify_preview(
preview_path: Path,
source_frame_count: int,
source_duration_seconds: float,
) -> tuple[
int,
int,
float,
int,
float,
tuple[bool, tuple[int, ...] | None],
tuple[bool, tuple[int, ...] | None],
tuple[bool, tuple[int, ...] | None],
]:
"""
Проверяет контейнер, геометрию, FPS, длительность и три кадра.
"""
if not preview_path.exists():
raise RuntimeError(
f"Preview отсутствует: {preview_path}"
)
if preview_path.stat().st_size <= 0:
raise RuntimeError("Preview имеет нулевой размер.")
capture = cv2.VideoCapture(str(preview_path))
if not capture.isOpened():
raise RuntimeError(
f"OpenCV не смог открыть preview: {preview_path}"
)
try:
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = float(capture.get(cv2.CAP_PROP_FPS))
frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
if fps <= 0.0:
raise RuntimeError("FPS preview равен нулю.")
duration_seconds = frame_count / fps
first_frame = read_frame_at(capture, 0)
middle_frame = read_frame_at(
capture,
frame_count // 2,
)
last_frame = read_frame_at(
capture,
frame_count - 1,
)
finally:
capture.release()
if width != OUTPUT_WIDTH or height != OUTPUT_HEIGHT:
raise RuntimeError(
f"Некорректное разрешение preview: {width}x{height}."
)
if frame_count != source_frame_count:
raise RuntimeError(
"Число кадров preview не совпало с исходным."
)
allowed_duration_difference = (
1.0 / fps + FRAME_TIME_EPSILON_SECONDS
)
if (
abs(duration_seconds - source_duration_seconds)
> allowed_duration_difference
):
raise RuntimeError(
"Длительность preview отличается более чем на один кадр."
)
for label, frame_result in [
("первый", first_frame),
("средний", middle_frame),
("последний", last_frame),
]:
if not frame_result[0]:
raise RuntimeError(
f"Не удалось прочитать {label} кадр preview."
)
return (
width,
height,
fps,
frame_count,
duration_seconds,
first_frame,
middle_frame,
last_frame,
)
def main() -> None:
"""
Создаёт и проверяет динамическое preview Lab027.
"""
print("Reading source video metadata...")
(
source_width,
source_height,
source_fps,
source_frame_count,
source_duration_seconds,
) = read_video_metadata(SOURCE_VIDEO_PATH)
print(f" Source: {SOURCE_VIDEO_PATH}")
print(f" Resolution: {source_width}x{source_height}")
print(f" FPS: {source_fps:.6f}")
print(f" Frames: {source_frame_count}")
print(f" Duration: {source_duration_seconds:.6f} s")
profiles = build_profiles()
print("Creating dynamic preview...")
(
preview_path,
fallback_used,
written_frame_count,
states,
) = create_preview(
source_path=SOURCE_VIDEO_PATH,
profiles=profiles,
source_width=source_width,
source_height=source_height,
source_fps=source_fps,
expected_frame_count=source_frame_count,
)
print("Verifying preview...")
(
preview_width,
preview_height,
preview_fps,
preview_frame_count,
preview_duration_seconds,
first_frame,
middle_frame,
last_frame,
) = verify_preview(
preview_path,
source_frame_count,
source_duration_seconds,
)
print("")
print(f"Preview path: {preview_path}")
print(f"Fallback used: {fallback_used}")
print(f"File size: {preview_path.stat().st_size} bytes")
print(
f"Preview resolution: "
f"{preview_width}x{preview_height}"
)
print(f"Preview FPS: {preview_fps:.6f}")
print(f"Written frames: {written_frame_count}")
print(f"Verified frames: {preview_frame_count}")
print(
f"Preview duration: "
f"{preview_duration_seconds:.6f} s"
)
print(f"First frame: {first_frame}")
print(f"Middle frame: {middle_frame}")
print(f"Last frame: {last_frame}")
print("")
print("Profile update counts:")
for profile, state in zip(profiles, states):
print(
f" {profile.name}: "
f"BASE={state.base_update_count}, "
f"ROI={state.roi_update_count}"
)
print("")
print("Lab027 temporal preview completed successfully.")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,869 @@
"""
Lab028. Packetization of synchronous BASE + ROI JPEG video objects.
The laboratory forms the selected Lab027E profile directly from the source
video, keeps every JPEG and packet in memory, verifies the transport layer,
and writes only aggregate CSV/report/plot artifacts.
"""
from __future__ import annotations
import csv
from dataclasses import dataclass
from pathlib import Path
import random
from typing import Callable
import cv2
import matplotlib
import numpy as np
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from protocol.video_packet import (
CompositeFrame,
CompositeReassembler,
HEADER_FORMAT,
HEADER_SIZE,
ObjectCRCError,
ObjectType,
PacketCRCError,
VideoPacket,
decode_packet,
encode_packet,
packetize_jpeg,
)
SOURCE_VIDEO_PATH = Path("data/raw/lab026_rover_source.mp4")
OUTPUT_DIRECTORY = Path("data/processed/lab028")
CSV_PATH = OUTPUT_DIRECTORY / "lab028_packet_payload_results.csv"
REPORT_PATH = OUTPUT_DIRECTORY / "lab028_report.txt"
OVERHEAD_PLOT_PATH = (
OUTPUT_DIRECTORY / "lab028_overhead_efficiency.png"
)
TRAFFIC_PLOT_PATH = (
OUTPUT_DIRECTORY / "lab028_packets_wire_bitrate.png"
)
COMPOSITE_FPS = 3.0
BASE_WIDTH = 240
BASE_HEIGHT = 135
BASE_QUALITY = 23
ROI_WIDTH = 320
ROI_HEIGHT = 180
ROI_QUALITY = 33
ROI_X_MIN = 0.20
ROI_X_MAX = 0.80
ROI_Y_MIN = 0.42
ROI_Y_MAX = 1.00
PAYLOAD_LENGTHS = (64, 128, 256, 512, 1024)
FRAME_TIME_EPSILON_SECONDS = 1e-9
CSV_FIELDS = [
"max_payload_bytes",
"composite_frames",
"mean_base_packets_per_frame",
"mean_roi_packets_per_frame",
"mean_total_packets_per_frame",
"max_total_packets_per_frame",
"packets_per_second",
"jpeg_payload_bytes",
"jpeg_payload_bitrate_kbps",
"header_bytes_per_second",
"header_bitrate_kbps",
"wire_bytes",
"wire_bitrate_kbps",
"service_data_percent",
"efficiency_percent",
"mean_wire_packet_bytes",
"p95_wire_packet_bytes",
"max_wire_packet_bytes",
]
@dataclass(frozen=True)
class VideoMetadata:
width: int
height: int
fps: float
frame_count: int
duration_seconds: float
@dataclass(frozen=True)
class EncodedComposite:
composite_frame_id: int
source_frame_index: int
base_jpeg: bytes
roi_jpeg: bytes
@dataclass(frozen=True)
class PayloadMetrics:
max_payload_bytes: int
composite_frames: int
mean_base_packets_per_frame: float
mean_roi_packets_per_frame: float
mean_total_packets_per_frame: float
max_total_packets_per_frame: int
packets_per_second: float
jpeg_payload_bytes: int
jpeg_payload_bitrate_kbps: float
header_bytes_per_second: float
header_bitrate_kbps: float
wire_bytes: int
wire_bitrate_kbps: float
service_data_percent: float
efficiency_percent: float
mean_wire_packet_bytes: float
p95_wire_packet_bytes: float
max_wire_packet_bytes: int
@dataclass(frozen=True)
class TestResult:
name: str
passed: bool
detail: str
def normalized_roi_to_pixels(
width: int,
height: int,
) -> tuple[int, int, int, int]:
"""Use the normalized ROI coordinates from Lab027 through Lab027E."""
coordinates = (
int(round(width * ROI_X_MIN)),
int(round(height * ROI_Y_MIN)),
int(round(width * ROI_X_MAX)),
int(round(height * ROI_Y_MAX)),
)
x_min, y_min, x_max, y_max = coordinates
if not (
0 <= x_min < x_max <= width
and 0 <= y_min < y_max <= height
):
raise RuntimeError("calculated ROI is outside the source frame")
return coordinates
def encode_grayscale_jpeg(image: np.ndarray, quality: int) -> bytes:
"""Encode one grayscale image to an in-memory JPEG."""
encoded, buffer = cv2.imencode(
".jpg",
image,
[int(cv2.IMWRITE_JPEG_QUALITY), int(quality)],
)
if not encoded:
raise RuntimeError("OpenCV could not encode JPEG")
jpeg = buffer.tobytes()
if not jpeg:
raise RuntimeError("OpenCV produced an empty JPEG")
return jpeg
def encode_composite(
source_frame: np.ndarray,
source_roi: tuple[int, int, int, int],
composite_frame_id: int,
source_frame_index: int,
) -> EncodedComposite:
"""Form synchronous BASE and ROI JPEGs from exactly one source frame."""
grayscale = cv2.cvtColor(source_frame, cv2.COLOR_BGR2GRAY)
base = cv2.resize(
grayscale,
(BASE_WIDTH, BASE_HEIGHT),
interpolation=cv2.INTER_AREA,
)
x_min, y_min, x_max, y_max = source_roi
roi = grayscale[y_min:y_max, x_min:x_max]
if roi.size == 0:
raise RuntimeError("source ROI is empty")
roi = cv2.resize(
roi,
(ROI_WIDTH, ROI_HEIGHT),
interpolation=cv2.INTER_AREA,
)
return EncodedComposite(
composite_frame_id=composite_frame_id,
source_frame_index=source_frame_index,
base_jpeg=encode_grayscale_jpeg(base, BASE_QUALITY),
roi_jpeg=encode_grayscale_jpeg(roi, ROI_QUALITY),
)
def load_video_profile(
source_path: Path,
) -> tuple[VideoMetadata, list[EncodedComposite]]:
"""Read the video sequentially and select synchronous updates at 3 fps."""
if not source_path.exists():
raise FileNotFoundError(f"source video is missing: {source_path}")
capture = cv2.VideoCapture(str(source_path))
if not capture.isOpened():
raise RuntimeError(f"OpenCV could not open {source_path}")
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = float(capture.get(cv2.CAP_PROP_FPS))
declared_frame_count = int(
capture.get(cv2.CAP_PROP_FRAME_COUNT)
)
if width <= 0 or height <= 0 or fps <= 0.0:
capture.release()
raise RuntimeError("invalid source video metadata")
source_roi = normalized_roi_to_pixels(width, height)
selected: list[EncodedComposite] = []
source_frame_index = 0
next_composite_time = 0.0
try:
while True:
frame_read, source_frame = capture.read()
if not frame_read or source_frame is None:
break
source_time = source_frame_index / fps
if (
source_time + FRAME_TIME_EPSILON_SECONDS
>= next_composite_time
):
selected.append(
encode_composite(
source_frame,
source_roi,
len(selected),
source_frame_index,
)
)
next_composite_time += 1.0 / COMPOSITE_FPS
source_frame_index += 1
finally:
capture.release()
if source_frame_index <= 0 or not selected:
raise RuntimeError("source video did not yield frames")
if (
declared_frame_count > 0
and source_frame_index != declared_frame_count
):
raise RuntimeError(
"decoded frame count differs from video metadata: "
f"{source_frame_index} != {declared_frame_count}"
)
metadata = VideoMetadata(
width=width,
height=height,
fps=fps,
frame_count=source_frame_index,
duration_seconds=source_frame_index / fps,
)
return metadata, selected
def packets_for_composite(
composite: EncodedComposite,
max_payload_bytes: int,
) -> tuple[list[bytes], list[bytes]]:
"""Packetize BASE and ROI separately with one composite frame ID."""
base_packets = packetize_jpeg(
composite.base_jpeg,
composite.composite_frame_id,
ObjectType.BASE,
max_payload_bytes,
)
roi_packets = packetize_jpeg(
composite.roi_jpeg,
composite.composite_frame_id,
ObjectType.ROI,
max_payload_bytes,
)
return base_packets, roi_packets
def calculate_payload_metrics(
composites: list[EncodedComposite],
duration_seconds: float,
max_payload_bytes: int,
) -> PayloadMetrics:
"""Calculate actual packet and bitrate statistics for one payload limit."""
base_counts: list[int] = []
roi_counts: list[int] = []
total_counts: list[int] = []
wire_packet_sizes: list[int] = []
for composite in composites:
base_packets, roi_packets = packets_for_composite(
composite, max_payload_bytes
)
base_counts.append(len(base_packets))
roi_counts.append(len(roi_packets))
total_counts.append(len(base_packets) + len(roi_packets))
wire_packet_sizes.extend(
len(packet) for packet in base_packets + roi_packets
)
jpeg_payload_bytes = sum(
len(composite.base_jpeg) + len(composite.roi_jpeg)
for composite in composites
)
packet_count = len(wire_packet_sizes)
header_bytes = packet_count * HEADER_SIZE
wire_bytes = jpeg_payload_bytes + header_bytes
return PayloadMetrics(
max_payload_bytes=max_payload_bytes,
composite_frames=len(composites),
mean_base_packets_per_frame=float(np.mean(base_counts)),
mean_roi_packets_per_frame=float(np.mean(roi_counts)),
mean_total_packets_per_frame=float(np.mean(total_counts)),
max_total_packets_per_frame=max(total_counts),
packets_per_second=packet_count / duration_seconds,
jpeg_payload_bytes=jpeg_payload_bytes,
jpeg_payload_bitrate_kbps=(
jpeg_payload_bytes * 8.0 / duration_seconds / 1000.0
),
header_bytes_per_second=header_bytes / duration_seconds,
header_bitrate_kbps=(
header_bytes * 8.0 / duration_seconds / 1000.0
),
wire_bytes=wire_bytes,
wire_bitrate_kbps=(
wire_bytes * 8.0 / duration_seconds / 1000.0
),
service_data_percent=header_bytes / wire_bytes * 100.0,
efficiency_percent=jpeg_payload_bytes / wire_bytes * 100.0,
mean_wire_packet_bytes=float(np.mean(wire_packet_sizes)),
p95_wire_packet_bytes=float(
np.percentile(wire_packet_sizes, 95)
),
max_wire_packet_bytes=max(wire_packet_sizes),
)
def feed_packets(
packets: list[bytes],
reassembler: CompositeReassembler | None = None,
) -> tuple[list[CompositeFrame], CompositeReassembler]:
"""Feed packets and collect every atomically published frame."""
receiver = reassembler or CompositeReassembler()
completed = []
for packet in packets:
frame = receiver.ingest(packet)
if frame is not None:
completed.append(frame)
return completed, receiver
def assert_frame_matches(
frame: CompositeFrame,
expected: EncodedComposite,
) -> None:
if frame.composite_frame_id != expected.composite_frame_id:
raise AssertionError("composite frame ID differs")
if frame.base_jpeg != expected.base_jpeg:
raise AssertionError("BASE JPEG differs byte-for-byte")
if frame.roi_jpeg != expected.roi_jpeg:
raise AssertionError("ROI JPEG differs byte-for-byte")
def run_functional_tests(
composites: list[EncodedComposite],
) -> list[TestResult]:
"""Run header and all mandatory Lab028 transport checks."""
if len(composites) < 2:
raise RuntimeError("functional checks need two video frames")
first = composites[0]
second = composites[1]
base_packets, roi_packets = packets_for_composite(first, 256)
all_packets = base_packets + roi_packets
tests: list[tuple[str, Callable[[], str]]] = []
def header_round_trip() -> str:
parsed = decode_packet(all_packets[0])
rebuilt = encode_packet(
VideoPacket(
composite_frame_id=parsed.composite_frame_id,
object_type=parsed.object_type,
fragment_index=parsed.fragment_index,
fragment_count=parsed.fragment_count,
jpeg_size=parsed.jpeg_size,
object_crc32=parsed.object_crc32,
payload=parsed.payload,
)
)
if rebuilt != all_packets[0]:
raise AssertionError("serialized bytes changed after round trip")
return f"fixed {HEADER_SIZE}-byte header round trip is exact"
def ordered_lossless() -> str:
frames, _ = feed_packets(all_packets)
if len(frames) != 1:
raise AssertionError("ordered transfer did not emit one frame")
assert_frame_matches(frames[0], first)
return "BASE and ROI match original JPEG bytes"
def shuffled_packets() -> str:
shuffled = list(all_packets)
random.Random(28001).shuffle(shuffled)
frames, _ = feed_packets(shuffled)
if len(frames) != 1:
raise AssertionError("shuffled transfer did not emit one frame")
assert_frame_matches(frames[0], first)
return "arbitrary packet order reconstructed correctly"
def duplicate_packets() -> str:
duplicated = list(all_packets)
duplicated.extend(
[all_packets[0], all_packets[len(base_packets)]]
)
random.Random(28002).shuffle(duplicated)
frames, receiver = feed_packets(duplicated)
if len(frames) != 1:
raise AssertionError("duplicates changed publication count")
assert_frame_matches(frames[0], first)
if receiver.duplicate_packets < 2:
raise AssertionError("exact duplicates were not counted")
return f"{receiver.duplicate_packets} exact duplicates ignored"
def packet_crc_corruption() -> str:
corrupted = bytearray(all_packets[0])
corrupted[-1] ^= 0x01
receiver = CompositeReassembler()
try:
receiver.ingest(bytes(corrupted))
except PacketCRCError:
pass
else:
raise AssertionError("corrupted packet passed packet CRC")
frames, _ = feed_packets(all_packets[1:], receiver)
if frames:
raise AssertionError("frame emitted despite rejected packet")
return "payload bit flip rejected; composite not emitted"
def missing_fragment() -> str:
missing_packet = roi_packets[len(roi_packets) // 2]
missing_index = decode_packet(missing_packet).fragment_index
remaining = [
packet
for packet in all_packets
if packet is not missing_packet
]
frames, receiver = feed_packets(remaining)
if frames:
raise AssertionError("frame emitted with a missing fragment")
missing = receiver.missing_fragments(
first.composite_frame_id, ObjectType.ROI
)
if missing is None or missing_index not in missing:
raise AssertionError("missing fragment was not reported")
return f"ROI fragment {missing_index} reported missing"
def object_crc_corruption() -> str:
target_position = len(base_packets)
parsed = decode_packet(all_packets[target_position])
changed_payload = bytearray(parsed.payload)
changed_payload[0] ^= 0x01
altered = encode_packet(
VideoPacket(
composite_frame_id=parsed.composite_frame_id,
object_type=parsed.object_type,
fragment_index=parsed.fragment_index,
fragment_count=parsed.fragment_count,
jpeg_size=parsed.jpeg_size,
object_crc32=parsed.object_crc32,
payload=bytes(changed_payload),
)
)
formally_valid = list(all_packets)
formally_valid[target_position] = altered
receiver = CompositeReassembler()
emitted = []
object_error_seen = False
for packet in formally_valid:
try:
frame = receiver.ingest(packet)
except ObjectCRCError:
object_error_seen = True
continue
if frame is not None:
emitted.append(frame)
if not object_error_seen:
raise AssertionError("object CRC did not detect changed JPEG")
if emitted:
raise AssertionError("frame emitted after object CRC failure")
return "valid packet CRCs still failed full-object CRC"
def adjacent_frames_do_not_mix() -> str:
first_packets = sum(packets_for_composite(first, 256), [])
second_packets = sum(packets_for_composite(second, 256), [])
interleaved = first_packets + second_packets
random.Random(28003).shuffle(interleaved)
frames, _ = feed_packets(interleaved)
by_id = {frame.composite_frame_id: frame for frame in frames}
if set(by_id) != {
first.composite_frame_id,
second.composite_frame_id,
}:
raise AssertionError("adjacent frames were lost or mixed")
assert_frame_matches(by_id[first.composite_frame_id], first)
assert_frame_matches(by_id[second.composite_frame_id], second)
return "two interleaved frame IDs remained independent"
def base_only_is_not_atomic() -> str:
frames, receiver = feed_packets(base_packets)
if frames:
raise AssertionError("BASE-only input emitted a composite")
if not receiver.object_is_complete(
first.composite_frame_id, ObjectType.BASE
):
raise AssertionError("complete BASE object was not retained")
return "complete BASE retained while composite stayed unpublished"
tests.extend(
[
("header_serialization_round_trip", header_round_trip),
("ordered_lossless_transfer", ordered_lossless),
("random_packet_order", shuffled_packets),
("exact_duplicate_packets", duplicate_packets),
("packet_crc_corruption", packet_crc_corruption),
("missing_fragment", missing_fragment),
("object_crc_corruption", object_crc_corruption),
("adjacent_frame_isolation", adjacent_frames_do_not_mix),
("base_without_roi_atomicity", base_only_is_not_atomic),
]
)
results = []
for name, test in tests:
try:
detail = test()
except Exception as error:
results.append(TestResult(name, False, str(error)))
else:
results.append(TestResult(name, True, detail))
failed = [result for result in results if not result.passed]
if failed:
details = "; ".join(
f"{result.name}: {result.detail}" for result in failed
)
raise RuntimeError(f"functional transport tests failed: {details}")
return results
def save_csv(metrics: list[PayloadMetrics]) -> None:
OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True)
with CSV_PATH.open("w", encoding="utf-8", newline="") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=CSV_FIELDS)
writer.writeheader()
for item in metrics:
row = {}
for field_name in CSV_FIELDS:
value = getattr(item, field_name)
row[field_name] = (
f"{value:.6f}"
if isinstance(value, float)
else value
)
writer.writerow(row)
def save_plots(metrics: list[PayloadMetrics]) -> None:
payloads = [item.max_payload_bytes for item in metrics]
figure, axis = plt.subplots(figsize=(9, 5.5))
axis.plot(
payloads,
[item.service_data_percent for item in metrics],
marker="o",
linewidth=2,
label="Service data (header / wire)",
)
axis.plot(
payloads,
[item.efficiency_percent for item in metrics],
marker="s",
linewidth=2,
label="Efficiency (JPEG / wire)",
)
axis.set_xscale("log", base=2)
axis.set_xticks(payloads)
axis.set_xticklabels([str(value) for value in payloads])
axis.set_xlabel("Maximum packet payload, bytes")
axis.set_ylabel("Share, %")
axis.set_title("Lab028 packet overhead and efficiency")
axis.grid(True, alpha=0.3)
axis.legend()
figure.tight_layout()
figure.savefig(OVERHEAD_PLOT_PATH, dpi=160)
plt.close(figure)
figure, packet_axis = plt.subplots(figsize=(9, 5.5))
bitrate_axis = packet_axis.twinx()
packet_line = packet_axis.plot(
payloads,
[item.packets_per_second for item in metrics],
color="tab:blue",
marker="o",
linewidth=2,
label="Packets/s",
)
bitrate_lines = bitrate_axis.plot(
payloads,
[item.wire_bitrate_kbps for item in metrics],
color="tab:red",
marker="s",
linewidth=2,
label="Wire bitrate",
)
bitrate_axis.plot(
payloads,
[item.jpeg_payload_bitrate_kbps for item in metrics],
color="tab:green",
linestyle="--",
linewidth=2,
label="JPEG payload bitrate",
)
packet_axis.set_xscale("log", base=2)
packet_axis.set_xticks(payloads)
packet_axis.set_xticklabels([str(value) for value in payloads])
packet_axis.set_xlabel("Maximum packet payload, bytes")
packet_axis.set_ylabel("Packets per second", color="tab:blue")
bitrate_axis.set_ylabel("Bitrate, kbit/s", color="tab:red")
packet_axis.set_title("Lab028 packet rate and wire bitrate")
packet_axis.grid(True, alpha=0.3)
lines = packet_line + bitrate_lines + bitrate_axis.lines[1:]
packet_axis.legend(
lines,
[line.get_label() for line in lines],
loc="best",
)
figure.tight_layout()
figure.savefig(TRAFFIC_PLOT_PATH, dpi=160)
plt.close(figure)
def metrics_table(metrics: list[PayloadMetrics]) -> list[str]:
lines = [
(
"payload | BASE pkt/frame | ROI pkt/frame | all pkt/frame | "
"max pkt/frame | pkt/s | JPEG kbit/s | headers B/s | "
"wire kbit/s | service % | efficiency % | packet mean/p95/max"
),
(
"-------:|---------------:|--------------:|--------------:|"
"--------------:|------:|------------:|------------:|"
"------------:|----------:|-------------:|--------------------:"
),
]
for item in metrics:
lines.append(
f"{item.max_payload_bytes} | "
f"{item.mean_base_packets_per_frame:.3f} | "
f"{item.mean_roi_packets_per_frame:.3f} | "
f"{item.mean_total_packets_per_frame:.3f} | "
f"{item.max_total_packets_per_frame} | "
f"{item.packets_per_second:.3f} | "
f"{item.jpeg_payload_bitrate_kbps:.3f} | "
f"{item.header_bytes_per_second:.3f} | "
f"{item.wire_bitrate_kbps:.3f} | "
f"{item.service_data_percent:.3f} | "
f"{item.efficiency_percent:.3f} | "
f"{item.mean_wire_packet_bytes:.1f}/"
f"{item.p95_wire_packet_bytes:.1f}/"
f"{item.max_wire_packet_bytes}"
)
return lines
def write_report(
metadata: VideoMetadata,
composites: list[EncodedComposite],
metrics: list[PayloadMetrics],
test_results: list[TestResult],
) -> None:
source_roi = normalized_roi_to_pixels(
metadata.width, metadata.height
)
base_sizes = [len(item.base_jpeg) for item in composites]
roi_sizes = [len(item.roi_jpeg) for item in composites]
lines = [
"Lab028. Пакетирование синхронного BASE + ROI",
"",
"Профиль и исходные данные",
f"- Видео: {SOURCE_VIDEO_PATH}",
(
f"- Источник: {metadata.width}x{metadata.height}, "
f"{metadata.fps:.6f} fps, {metadata.frame_count} кадров, "
f"{metadata.duration_seconds:.6f} с."
),
(
f"- ROI: x={ROI_X_MIN:.2f}...{ROI_X_MAX:.2f}, "
f"y={ROI_Y_MIN:.2f}...{ROI_Y_MAX:.2f}; "
f"пиксели x={source_roi[0]}...{source_roi[2]}, "
f"y={source_roi[1]}...{source_roi[3]}."
),
(
f"- Синхронные составные кадры: {len(composites)} "
f"при {COMPOSITE_FPS:.3f} fps."
),
(
f"- BASE: {BASE_WIDTH}x{BASE_HEIGHT}, grayscale JPEG Q"
f"{BASE_QUALITY}; средний размер {np.mean(base_sizes):.3f} B, "
f"min/max {min(base_sizes)}/{max(base_sizes)} B."
),
(
f"- ROI: {ROI_WIDTH}x{ROI_HEIGHT}, grayscale JPEG Q"
f"{ROI_QUALITY}; средний размер {np.mean(roi_sizes):.3f} B, "
f"min/max {min(roi_sizes)}/{max(roi_sizes)} B."
),
(
"- BASE и ROI формируются из одного source frame и имеют "
"общий composite_frame_id."
),
"",
"Бинарный заголовок",
f"- struct format: {HEADER_FORMAT}",
f"- Размер: {HEADER_SIZE} байта; network byte order; padding нет.",
(
"- Layout: magic[4] @0, version:u8 @4, object_type:u8 @5, "
"header_size:u16 @6, composite_frame_id:u32 @8, "
"fragment_index:u16 @12, fragment_count:u16 @14, "
"payload_length:u16 @16, flags:u16 @18, jpeg_size:u32 @20, "
"object_crc32:u32 @24, packet_crc32:u32 @28."
),
"- object_type: 1=BASE, 2=ROI; flags зарезервирован и равен 0.",
"",
"CRC32",
(
"- object_crc32 = zlib.crc32(полный JPEG) & 0xFFFFFFFF; "
"значение помещается во все фрагменты объекта и проверяется "
"после полной сборки."
),
(
"- packet_crc32: сначала поле packet_crc32 заголовка "
"обнуляется, затем CRC считается как "
"zlib.crc32(header_with_zero_crc + payload) & 0xFFFFFFFF."
),
"- Таким образом packet CRC защищает все поля заголовка и payload.",
"",
"Функциональные проверки",
]
lines.extend(
f"- {'PASS' if item.passed else 'FAIL'} {item.name}: {item.detail}"
for item in test_results
)
lines.extend(
[
"",
"Результаты размеров packet payload",
*metrics_table(metrics),
"",
(
"JPEG payload bitrate одинаков для всех строк, потому что "
"исходные JPEG не меняются при выборе размера фрагмента."
),
(
"Wire bitrate включает только JPEG payload и 32-байтные "
"заголовки каждого пакета."
),
(
"Wire bitrate пока НЕ включает FEC, преамбулу, "
"синхронизацию, модуляцию, интервалы, повторные передачи, "
"команды управления и телеметрию."
),
(
"Лучший размер packet payload автоматически не выбирается: "
"таблица показывает только транспортный компромисс."
),
"",
"Артефакты",
f"- CSV: {CSV_PATH}",
f"- Overhead/efficiency: {OVERHEAD_PLOT_PATH}",
f"- Packet count/wire bitrate: {TRAFFIC_PLOT_PATH}",
(
"- Промежуточные JPEG и пакеты сохранялись только в памяти; "
"бинарные дампы не создавались."
),
"",
]
)
REPORT_PATH.write_text("\n".join(lines), encoding="utf-8")
def validate_outputs(metrics: list[PayloadMetrics]) -> None:
if len(metrics) != len(PAYLOAD_LENGTHS):
raise RuntimeError("not all payload sizes were measured")
if [item.max_payload_bytes for item in metrics] != list(
PAYLOAD_LENGTHS
):
raise RuntimeError("payload result order differs")
if any(
abs(
item.service_data_percent
+ item.efficiency_percent
- 100.0
) > 1e-9
for item in metrics
):
raise RuntimeError("overhead and efficiency do not sum to 100%")
for path in (
CSV_PATH,
REPORT_PATH,
OVERHEAD_PLOT_PATH,
TRAFFIC_PLOT_PATH,
):
if not path.exists() or path.stat().st_size <= 0:
raise RuntimeError(f"missing or empty output: {path}")
def main() -> None:
print("Lab028: loading and JPEG-encoding the Lab027E profile...")
metadata, composites = load_video_profile(SOURCE_VIDEO_PATH)
print(
f" source={metadata.width}x{metadata.height}, "
f"{metadata.fps:.3f} fps, frames={metadata.frame_count}"
)
print(f" synchronous composite frames={len(composites)}")
print("Running functional transport checks...")
test_results = run_functional_tests(composites)
for result in test_results:
print(f" PASS {result.name}: {result.detail}")
print("Measuring packet payload sizes...")
metrics = [
calculate_payload_metrics(
composites,
metadata.duration_seconds,
payload_length,
)
for payload_length in PAYLOAD_LENGTHS
]
for item in metrics:
print(
f" payload={item.max_payload_bytes:4d} B: "
f"{item.packets_per_second:.3f} packet/s, "
f"wire={item.wire_bitrate_kbps:.3f} kbit/s, "
f"service={item.service_data_percent:.3f}%"
)
OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True)
save_csv(metrics)
save_plots(metrics)
write_report(metadata, composites, metrics, test_results)
validate_outputs(metrics)
print(f"CSV: {CSV_PATH}")
print(f"Report: {REPORT_PATH}")
print(f"Plots: {OVERHEAD_PLOT_PATH}, {TRAFFIC_PLOT_PATH}")
print("Lab028 completed successfully.")
if __name__ == "__main__":
main()