From 8b50c126b97f8e9aec302af6f881518850728904 Mon Sep 17 00:00:00 2001 From: xin Date: Thu, 25 Jun 2026 16:30:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=88=AA=E7=BA=BF=20Y=20=E5=81=8F?= =?UTF-8?q?=E7=A7=BB=201/2=20=E5=B9=85=E5=AE=BD=E3=80=81=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E5=B8=A7=E7=8E=87/=E6=9B=9D=E5=85=89=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E3=80=81=E9=80=9F=E5=BA=A6=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=80=BC=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 第一条航线 Y 坐标偏移 1/2 幅宽,不再紧贴绘制区域边缘 - 生成航线时自动计算推荐帧率和曝光时间(Pika L 900像元 / Pika NIR 320像元) - 推荐帧率×曝光时间=999,Pika L 最大帧率限制 250fps - 生成航线后自动填入子任务帧率和曝光时间 - Y 轴定位速度默认 10cm/s,X 扫描 3cm/s,X 回起点 10cm/s --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/path_commands.rs | 13 +++++ src-tauri/src/lib.rs | 1 + src-tauri/src/models/path_plan.rs | 6 +-- src-tauri/src/services/camera_recommender.rs | 54 ++++++++++++++++++++ src-tauri/src/services/mod.rs | 1 + src-tauri/src/services/path_generator.rs | 48 +++++++++++++---- src-tauri/tauri.conf.json | 2 +- src/components/planner/ScanParamPanel.vue | 11 +++- src/types/mission.ts | 2 +- src/views/PathPlannerView.vue | 40 +++++++++++++-- update.md | 8 +++ 14 files changed, 167 insertions(+), 25 deletions(-) create mode 100644 src-tauri/src/services/camera_recommender.rs diff --git a/package.json b/package.json index 8650eb2..98b20b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "happa-mission-plan", "private": true, - "version": "0.0.9", + "version": "0.0.10", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 257f912..26460d5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3180,7 +3180,7 @@ dependencies = [ [[package]] name = "spectral-insight-mission-plan" -version = "0.0.8" +version = "0.0.9" dependencies = [ "byteorder", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d4087a3..a6ef4d0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spectral-insight-mission-plan" -version = "0.0.9" +version = "0.0.10" description = "Spectral Insight Mission Plan" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/commands/path_commands.rs b/src-tauri/src/commands/path_commands.rs index 28e6322..3fe0b5a 100644 --- a/src-tauri/src/commands/path_commands.rs +++ b/src-tauri/src/commands/path_commands.rs @@ -4,6 +4,7 @@ use crate::error::AppError; use crate::io; use crate::models::{PathLineFile, ScanParams, ScanRegion, CameraParams, ScanMode}; use crate::services; +use crate::services::camera_recommender::RecommendedCameraParams; #[command] pub fn load_path_line(path: String) -> Result { @@ -63,3 +64,15 @@ pub fn estimate_scan_time_multi_minutes( speed_y_cm_s, speed_x_start_cm_s, speed_x_scan_cm_s, &mode, ) } + +#[command] +pub fn recommend_hyperspectral_params( + device_type: String, + height_cm: f64, + fov_degrees: f64, + scan_speed_cm_s: f64, +) -> RecommendedCameraParams { + services::camera_recommender::recommend_hyperspectral_params( + &device_type, height_cm, fov_degrees, scan_speed_cm_s, + ) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 95c617a..18e6552 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -40,6 +40,7 @@ pub fn run() { commands::path_commands::estimate_scan_time_minutes, commands::path_commands::generate_scan_paths_multi, commands::path_commands::estimate_scan_time_multi_minutes, + commands::path_commands::recommend_hyperspectral_params, commands::devtools_commands::toggle_devtools, commands::defaults_commands::save_default_scan_config, commands::defaults_commands::load_default_scan_config, diff --git a/src-tauri/src/models/path_plan.rs b/src-tauri/src/models/path_plan.rs index 053ff55..260652e 100644 --- a/src-tauri/src/models/path_plan.rs +++ b/src-tauri/src/models/path_plan.rs @@ -68,9 +68,9 @@ impl Default for PlannerDefaults { fov_degrees: 30.0, height_cm: 50.0, coverage_rate: 30.0, - speed_y_cm_s: 5.0, - speed_x_start_cm_s: 20.0, - speed_x_scan_cm_s: 10.0, + speed_y_cm_s: 10.0, + speed_x_start_cm_s: 10.0, + speed_x_scan_cm_s: 3.0, mode: "OneWay".to_string(), } } diff --git a/src-tauri/src/services/camera_recommender.rs b/src-tauri/src/services/camera_recommender.rs new file mode 100644 index 0000000..dbf6c7e --- /dev/null +++ b/src-tauri/src/services/camera_recommender.rs @@ -0,0 +1,54 @@ +use serde::Serialize; + +use super::path_generator::calc_footprint; + +#[derive(Debug, Clone, Serialize)] +pub struct RecommendedCameraParams { + #[serde(rename = "recommendedFrameRate")] + pub recommended_frame_rate: u32, + #[serde(rename = "recommendedExposureTime")] + pub recommended_exposure_time: u32, +} + +/// Calculate recommended frame rate and maximum exposure time for hyperspectral cameras. +/// +/// Formula: +/// across-track pixel size = footprint / num_pixels +/// along-track pixel size = scan_speed / frame_rate +/// Square pixels: footprint / num_pixels = scan_speed / frame_rate +/// => frame_rate = scan_speed * num_pixels / footprint +/// => exposure_time = 999 / frame_rate +/// +/// Device limits: +/// Pika L max frame rate = 250 fps +pub fn recommend_hyperspectral_params( + device_type: &str, + height_cm: f64, + fov_degrees: f64, + scan_speed_cm_s: f64, +) -> RecommendedCameraParams { + let (num_pixels, max_fps) = match device_type { + "HyperSpectual400_1000nm" => (900.0, 250), // Pika L + "HyperSpectual1000_1700nm" => (320.0, u32::MAX), // Pika NIR + _ => return RecommendedCameraParams { + recommended_frame_rate: 0, + recommended_exposure_time: 0, + }, + }; + + if scan_speed_cm_s <= 0.0 || height_cm <= 0.0 || fov_degrees <= 0.0 { + return RecommendedCameraParams { + recommended_frame_rate: 0, + recommended_exposure_time: 0, + }; + } + + let footprint = calc_footprint(height_cm, fov_degrees); + let frame_rate = ((scan_speed_cm_s * num_pixels / footprint).round() as u32).max(1).min(max_fps); + let exposure_time = 999 / frame_rate; + + RecommendedCameraParams { + recommended_frame_rate: frame_rate, + recommended_exposure_time: exposure_time, + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 1452acc..9599aab 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1,3 +1,4 @@ pub mod scheduler; pub mod validator; +pub mod camera_recommender; pub mod path_generator; diff --git a/src-tauri/src/services/path_generator.rs b/src-tauri/src/services/path_generator.rs index 9b66831..4018e81 100644 --- a/src-tauri/src/services/path_generator.rs +++ b/src-tauri/src/services/path_generator.rs @@ -18,10 +18,15 @@ pub fn generate_path(params: &ScanParams) -> Vec { let y_min = params.region.y_min; let y_max = params.region.y_max; - let line_count = if step <= 0.0 { + let half_fp = footprint / 2.0; + let y_start = y_min + half_fp; + let y_end = y_max - half_fp; + + let single_line = step <= 0.0 || y_end <= y_start; + let line_count = if single_line { 1 } else { - ((y_max - y_min) / step).ceil() as u64 + 1 + ((y_end - y_start) / step).ceil() as u64 + 1 }; let mut records = Vec::with_capacity(line_count as usize); @@ -29,8 +34,12 @@ pub fn generate_path(params: &ScanParams) -> Vec { match params.mode { ScanMode::Zigzag => { for i in 0..line_count { - let y = y_min + (i as f64) * step; - if y > y_max { + let y = if single_line { + (y_min + y_max) / 2.0 + } else { + y_start + (i as f64) * step + }; + if !single_line && y > y_end { break; } @@ -53,8 +62,12 @@ pub fn generate_path(params: &ScanParams) -> Vec { } ScanMode::OneWay => { for i in 0..line_count { - let y = y_min + (i as f64) * step; - if y > y_max { + let y = if single_line { + (y_min + y_max) / 2.0 + } else { + y_start + (i as f64) * step + }; + if !single_line && y > y_end { break; } @@ -117,13 +130,26 @@ pub fn generate_paths_multi( let mut all_records = Vec::new(); for (i, region) in regions.iter().enumerate() { let is_circle = shapes.get(i).map(|s| s == "Circle").unwrap_or(false); - let step = calc_footprint(camera.height_cm, camera.fov_degrees) - * (1.0 - coverage_rate / 100.0); - let line_count = if step <= 0.0 { 1 } else { ((region.y_max - region.y_min) / step).ceil() as u64 + 1 }; + let footprint = calc_footprint(camera.height_cm, camera.fov_degrees); + let step = footprint * (1.0 - coverage_rate / 100.0); + let half_fp = footprint / 2.0; + let y_start = region.y_min + half_fp; + let y_end = region.y_max - half_fp; + + let single_line = step <= 0.0 || y_end <= y_start; + let line_count = if single_line { + 1 + } else { + ((y_end - y_start) / step).ceil() as u64 + 1 + }; for li in 0..line_count { - let y = region.y_min + (li as f64) * step; - if y > region.y_max { break; } + let y = if single_line { + (region.y_min + region.y_max) / 2.0 + } else { + y_start + (li as f64) * step + }; + if !single_line && y > y_end { break; } let (x_start, x_end) = if is_circle { let cx = (region.x_min + region.x_max) / 2.0; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4068c8e..86b6780 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Spectral Insight Mission Plan", - "version": "0.0.9", + "version": "0.0.10", "identifier": "com.spectral-insight.mission-plan", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/planner/ScanParamPanel.vue b/src/components/planner/ScanParamPanel.vue index d11cf1f..c1c2338 100644 --- a/src/components/planner/ScanParamPanel.vue +++ b/src/components/planner/ScanParamPanel.vue @@ -49,6 +49,8 @@ {{ lineCount }} {{ estTimeFormatted }} + {{ recommendedFrameRate }} fps + {{ recommendedExposureTime }} ms @@ -61,7 +63,7 @@ import type { CameraParams, ScanMode } from '../../types/path-plan'; import { formatDuration } from '../../utils/constants'; import HelpIcon from '../common/HelpIcon.vue'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ camera: CameraParams; coverageRate: number; speedY: number; @@ -70,7 +72,12 @@ const props = defineProps<{ mode: ScanMode; lineCount: number; estTime: number; -}>(); + recommendedFrameRate?: number; + recommendedExposureTime?: number; +}>(), { + recommendedFrameRate: undefined, + recommendedExposureTime: undefined, +}); const emit = defineEmits<{ update: [params: any]; diff --git a/src/types/mission.ts b/src/types/mission.ts index e3063b4..01a1560 100644 --- a/src/types/mission.ts +++ b/src/types/mission.ts @@ -33,7 +33,7 @@ export function createDefaultPlannerParams(): PlannerDefaults { return { fovDegrees: 30, heightCm: 50, coverageRate: 30, - speedYCmS: 5, speedXStartCmS: 20, speedXScanCmS: 10, + speedYCmS: 10, speedXStartCmS: 10, speedXScanCmS: 3, mode: 'Zigzag', }; } diff --git a/src/views/PathPlannerView.vue b/src/views/PathPlannerView.vue index 277bc80..5dd5466 100644 --- a/src/views/PathPlannerView.vue +++ b/src/views/PathPlannerView.vue @@ -60,6 +60,8 @@ :mode="scanMode" :line-count="generatedRecords.length" :est-time="estimatedTime" + :recommended-frame-rate="recommendedParams?.recommendedFrameRate ?? undefined" + :recommended-exposure-time="recommendedParams?.recommendedExposureTime ?? undefined" @update="onParamChange" /> @@ -103,14 +105,15 @@ function getDeviceFov(): number { const regions = ref([]); const camera = ref({ fovDegrees: getDeviceFov(), heightCm: 50 }); const coverageRate = ref(30); -const speedY = ref(5); -const speedXScan = ref(10); -const speedXStart = ref(20); +const speedY = ref(10); +const speedXScan = ref(3); +const speedXStart = ref(10); const scanMode = ref('OneWay'); const generatedRecords = ref([]); const previewRecords = computed(() => generatedRecords.value); const estimatedTime = ref(0); +const recommendedParams = ref<{recommendedFrameRate: number, recommendedExposureTime: number} | null>(null); invoke('load_planner_defaults').then(p => { coverageRate.value = p.coverageRate; @@ -175,6 +178,27 @@ async function generatePath() { speedYCmS: speedY.value, speedXStartCmS: speedXStart.value, speedXScanCmS: speedXScan.value, mode: scanMode.value, }); estimatedTime.value = estTime; + + // 获取推荐相机参数 + if (props.taskId !== null && props.subTaskId !== null) { + const task = missionStore.mission.tasks.find(t => t.id === props.taskId); + const sub = task?.subTasks.find(s => s.id === props.subTaskId); + if (sub) { + recommendedParams.value = await invoke<{recommendedFrameRate: number, recommendedExposureTime: number}>('recommend_hyperspectral_params', { + deviceType: sub.type, + heightCm: camera.value.heightCm, + fovDegrees: camera.value.fovDegrees, + scanSpeedCmS: speedXScan.value, + }); + // 将推荐参数立即写入子任务 + if (recommendedParams.value && DEVICE_FOV[sub.type]) { + sub.frameRate = recommendedParams.value.recommendedFrameRate; + sub.exposureTime = recommendedParams.value.recommendedExposureTime; + await missionStore.updateSubTask(props.taskId, { ...sub }); + } + } + } + message.success(`航线生成完成: ${file.count} 条记录`); } catch (e) { message.error('航线生成失败: ' + e); } } @@ -191,7 +215,15 @@ async function savePath() { if (props.taskId !== null && props.subTaskId !== null) { const task = missionStore.mission.tasks.find(t => t.id === props.taskId); const sub = task?.subTasks.find(s => s.id === props.subTaskId); - if (sub) { sub.pathLineFilePath = finalPath; await missionStore.updateSubTask(props.taskId, { ...sub }); message.success('已自动填入子任务航线路径'); } + if (sub) { + sub.pathLineFilePath = finalPath; + if (recommendedParams.value) { + sub.frameRate = recommendedParams.value.recommendedFrameRate; + sub.exposureTime = recommendedParams.value.recommendedExposureTime; + } + await missionStore.updateSubTask(props.taskId, { ...sub }); + message.success('已自动填入子任务航线路径及推荐相机参数'); + } } } } catch (e) { message.error('保存失败: ' + e); } diff --git a/update.md b/update.md index aaf1f37..b18e151 100644 --- a/update.md +++ b/update.md @@ -1,5 +1,13 @@ # Spectral Insight Mission Plan - 更新日志 +## v0.0.10 (2026-06-25) + +- 第一条航线 Y 坐标偏移 1/2 幅宽,不再紧贴绘制区域边缘 +- 生成航线时自动计算推荐帧率和曝光时间(Pika L 900像元 / Pika NIR 320 像元,帧率×曝光时间=999) +- 生成航线后自动填入子任务帧率和曝光时间 +- Pika L 最大帧率限制 250fps(唐超) +- Y 轴定位速度默认值修改为 10cm/s,X 扫描速度 3cm/s,X 回起点速度 10cm/s + ## v0.0.9 (2026-06-25) - 航线生成画布支持 scanConfig 坐标范围(修改 X/Y 范围后画布随之更新)