Lab027: optimize synchronized ROI video stream
This commit is contained in:
BIN
data/processed/lab027/lab027_bitrate_comparison.png
Normal file
BIN
data/processed/lab027/lab027_bitrate_comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
BIN
data/processed/lab027/lab027_candidate_profiles.png
Normal file
BIN
data/processed/lab027/lab027_candidate_profiles.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
10
data/processed/lab027/lab027_profiles.csv
Normal file
10
data/processed/lab027/lab027_profiles.csv
Normal 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
|
||||
|
BIN
data/processed/lab027/lab027_quality_comparison.png
Normal file
BIN
data/processed/lab027/lab027_quality_comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
74
data/processed/lab027/lab027_report.txt
Normal file
74
data/processed/lab027/lab027_report.txt
Normal file
@@ -0,0 +1,74 @@
|
||||
Lab027. Многомасштабная передача изображения: общий кадр и область интереса
|
||||
|
||||
Цель: проверить совместную передачу общего кадра и фиксированной ROI при payload около 80–100 кбит/с.
|
||||
Исходное видео: 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
|
||||
Следующий шаг: ручная оценка пользователем.
|
||||
5
data/processed/lab027c/lab027c_preview_profiles.csv
Normal file
5
data/processed/lab027c/lab027c_preview_profiles.csv
Normal 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
|
||||
|
44
data/processed/lab027c/lab027c_preview_report.txt
Normal file
44
data/processed/lab027c/lab027c_preview_report.txt
Normal 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-видео.
|
||||
5
data/processed/lab027d/lab027d_profiles.csv
Normal file
5
data/processed/lab027d/lab027d_profiles.csv
Normal 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
|
||||
|
37
data/processed/lab027d/lab027d_report.txt
Normal file
37
data/processed/lab027d/lab027d_report.txt
Normal 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-видео.
|
||||
2
data/processed/lab027e/lab027e_profile.csv
Normal file
2
data/processed/lab027e/lab027e_profile.csv
Normal 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
|
||||
|
50
data/processed/lab027e/lab027e_report.txt
Normal file
50
data/processed/lab027e/lab027e_report.txt
Normal 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.
|
||||
Следующий шаг: ручная оценка удобства управления пользователем.
|
||||
405
docs/video_compression_research_lab026_lab027.md
Normal file
405
docs/video_compression_research_lab026_lab027.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# Исследование сжатия видеопотока в Lab026–Lab027
|
||||
|
||||
## 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. Итоговый рабочий видеорежим после Lab027C–Lab027E
|
||||
|
||||
Динамические сравнения 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 и не является
|
||||
частью передаваемого радиоканального формата. Реальная скорость
|
||||
автомобиля в исходном видео неизвестна.
|
||||
1710
tests/lab027_roi_multiresolution_benchmark.py
Normal file
1710
tests/lab027_roi_multiresolution_benchmark.py
Normal file
File diff suppressed because it is too large
Load Diff
925
tests/lab027_roi_temporal_preview.py
Normal file
925
tests/lab027_roi_temporal_preview.py
Normal 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()
|
||||
1941
tests/lab027c_synchronous_roi_preview.py
Normal file
1941
tests/lab027c_synchronous_roi_preview.py
Normal file
File diff suppressed because it is too large
Load Diff
1517
tests/lab027d_fps_quality_preview.py
Normal file
1517
tests/lab027d_fps_quality_preview.py
Normal file
File diff suppressed because it is too large
Load Diff
1575
tests/lab027e_operator_view_preview.py
Normal file
1575
tests/lab027e_operator_view_preview.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user