1use clap::{Arg, ArgAction, ArgMatches, Command, Parser};
7use lazy_static::lazy_static;
8use std::path::PathBuf;
9use tracing::{error, info};
10
11use crate::{
12 algorithms::ThresholdAlgorithm,
13 core::{
14 masker::MaskerBuilder,
15 postprocessor::{CompositePostProcessor, MetashapeConverter, PixelBufferProcessor},
16 sensor::SensorRegistry,
17 },
18 error::{GlintError, Result},
19 loaders::{BigTiffLoader, ConfigurableMultiFileLoader, SingleFileLoader},
20};
21
22lazy_static! {
23 static ref SENSOR_REGISTRY: SensorRegistry = SensorRegistry::from_user_config();
24}
25
26#[derive(Parser)]
28#[command(name = "glint-mask")]
29#[command(about = "Generate glint masks for specular reflection in UAV and aerial imagery")]
30#[command(version)]
31pub struct Cli {
32 #[arg(short, long, global = true)]
34 pub verbose: bool,
35}
36
37#[derive(Debug)]
39pub struct SensorArgs {
40 pub input: PathBuf,
42
43 pub output: PathBuf,
45
46 pub thresholds: Option<Vec<f64>>,
48
49 pub pixel_buffer: usize,
51}
52
53impl From<&ArgMatches> for SensorArgs {
54 fn from(matches: &ArgMatches) -> Self {
55 let input = matches
56 .get_one::<String>("input")
57 .map(PathBuf::from)
58 .unwrap();
59 let output = matches
60 .get_one::<String>("output")
61 .map(PathBuf::from)
62 .unwrap();
63 let thresholds = matches
64 .get_many::<f64>("thresholds")
65 .map(|v| v.copied().collect());
66 let pixel_buffer = matches.get_one::<usize>("pixel_buffer").copied().unwrap();
67
68 Self {
69 input,
70 output,
71 thresholds,
72 pixel_buffer,
73 }
74 }
75}
76
77pub fn run() -> Result<()> {
79 let mut cmd = Command::new("glint-mask")
80 .about("Generate glint masks for specular reflection in UAV and aerial imagery")
81 .version(env!("CARGO_PKG_VERSION"))
82 .arg(
83 Arg::new("verbose")
84 .short('v')
85 .long("verbose")
86 .action(ArgAction::SetTrue)
87 .help("Enable verbose logging")
88 .global(true),
89 )
90 .subcommand_required(true);
91
92 for sensor in SENSOR_REGISTRY.sensors() {
93 let mut subcommand = Command::new(sensor.id.as_str()).about(sensor.name.as_str());
94 subcommand = subcommand
95 .arg(
96 Arg::new("input")
97 .help("Path to input directory or single image file")
98 .required(true),
99 )
100 .arg(
101 Arg::new("output")
102 .help("Path to output directory for mask files")
103 .required(true),
104 )
105 .arg(
106 Arg::new("thresholds")
107 .short('t')
108 .long("thresholds")
109 .value_delimiter(',')
110 .value_parser(clap::value_parser!(f64))
111 .help("Comma-separated threshold values for each band (e.g., 0.8,0.9,0.7)"),
112 )
113 .arg(
114 Arg::new("pixel_buffer")
115 .short('b')
116 .long("pixel_buffer")
117 .default_value("0")
118 .value_parser(clap::value_parser!(usize))
119 .help("Pixel buffer radius to expand masks"),
120 );
121 cmd = cmd.subcommand(subcommand);
122 }
123
124 cmd = cmd.subcommand(Command::new("list-sensors").about("List available sensors"));
125 cmd = cmd.subcommand(
126 Command::new("sensor-info")
127 .about("Show sensor information")
128 .arg(
129 Arg::new("sensor_id")
130 .help("Sensor ID to show information for")
131 .required(true),
132 ),
133 );
134
135 let matches = cmd.get_matches();
136
137 let verbose = matches.get_flag("verbose");
139 let subscriber = tracing_subscriber::fmt()
140 .with_max_level(if verbose {
141 tracing::Level::DEBUG
142 } else {
143 tracing::Level::INFO
144 })
145 .finish();
146
147 tracing::subscriber::set_global_default(subscriber)
148 .map_err(|e| GlintError::processing(format!("Failed to initialize logging: {}", e)))?;
149
150 match matches.subcommand() {
151 Some(("list-sensors", _)) => list_sensors(),
152 Some(("sensor-info", sub_matches)) => {
153 let sensor_id = sub_matches.get_one::<String>("sensor_id").unwrap();
154 show_sensor_info(sensor_id)
155 }
156 Some((sensor_id, sub_matches)) => {
157 let args = SensorArgs::from(sub_matches);
158 process_sensor(sensor_id, args)
159 }
160 _ => unreachable!("Subcommand is required"),
161 }
162}
163
164fn process_sensor(sensor_id: &str, args: SensorArgs) -> Result<()> {
166 info!("Processing {} images", sensor_id.to_uppercase());
167 info!("Input: {}", args.input.display());
168 info!("Output: {}", args.output.display());
169
170 if !args.input.exists() {
172 return Err(GlintError::Io(std::io::Error::new(
173 std::io::ErrorKind::NotFound,
174 format!("Input path does not exist: {}", args.input.display()),
175 )));
176 }
177
178 crate::utils::ensure_directory_exists(&args.output)?;
179
180 let sensor = SENSOR_REGISTRY
182 .get_sensor(sensor_id)
183 .ok_or_else(|| GlintError::sensor(format!("Unknown sensor: {}", sensor_id)))?
184 .clone();
185
186 let thresholds = if let Some(user_thresholds) = args.thresholds {
188 if user_thresholds.len() != sensor.band_count() {
189 return Err(GlintError::BandCountMismatch {
190 expected: sensor.band_count(),
191 actual: user_thresholds.len(),
192 });
193 }
194 user_thresholds
195 } else {
196 sensor.default_thresholds()
197 };
198
199 info!("Using thresholds: {:?}", thresholds);
200 info!("Pixel buffer: {} pixels", args.pixel_buffer);
201
202 let algorithm = ThresholdAlgorithm::new(thresholds)?;
204
205 let mut postprocessor = CompositePostProcessor::new();
207
208 if args.pixel_buffer > 0 {
209 postprocessor =
210 postprocessor.add_processor(Box::new(PixelBufferProcessor::new(args.pixel_buffer)));
211 }
212
213 postprocessor = postprocessor.add_processor(Box::new(MetashapeConverter::new()));
215
216 let loader = create_loader(&sensor)?;
218
219 let masker = MaskerBuilder::new()
221 .with_sensor(sensor.clone())
222 .with_algorithm(Box::new(algorithm))
223 .with_postprocessor(Box::new(postprocessor))
224 .with_loader(loader)
225 .build()?;
226
227 let stats = masker.process_directory(&args.input, &args.output, None)?;
229
230 info!("Processing complete!");
232 info!("Total captures: {}", stats.total_captures);
233 info!("Successful: {}", stats.successful_captures);
234 info!("Failed: {}", stats.failed_captures);
235 info!("Success rate: {:.1}%", stats.success_rate());
236
237 if !stats.errors.is_empty() {
238 error!("Errors encountered:");
239 for error in &stats.errors {
240 error!(" {}", error);
241 }
242 }
243
244 if !stats.all_successful() {
245 std::process::exit(1);
246 }
247
248 Ok(())
249}
250
251fn create_loader(
253 sensor: &crate::core::sensor::Sensor,
254) -> Result<Box<dyn crate::core::ImageLoader>> {
255 match sensor.loader_type.as_str() {
256 "single_file" => {
257 if sensor.band_count() == 1 {
258 Ok(Box::new(SingleFileLoader::grayscale()?))
259 } else if sensor.band_count() == 3 {
260 Ok(Box::new(SingleFileLoader::rgb()?))
261 } else if sensor.band_count() == 4 {
262 if sensor.bit_depth == 8 {
264 let extensions = vec![
266 "tif".to_string(),
267 "tiff".to_string(),
268 "jpg".to_string(),
269 "jpeg".to_string(),
270 "png".to_string(),
271 ];
272 Ok(Box::new(SingleFileLoader::new(extensions, 4, 8)?))
273 } else {
274 Ok(Box::new(SingleFileLoader::tiff_16bit(4)?))
276 }
277 } else {
278 Err(GlintError::validation(format!(
279 "Unsupported band count for single file loader: {}",
280 sensor.band_count()
281 )))
282 }
283 }
284 "multifile" => {
285 Ok(Box::new(ConfigurableMultiFileLoader::from_config(
287 &sensor.loader_config,
288 )?))
289 }
290 "big_tiff" => {
291 let chunk_size = sensor
293 .loader_config
294 .get("chunk_size")
295 .and_then(|s| s.parse::<usize>().ok());
296
297 let extensions = sensor
299 .loader_config
300 .get("extensions")
301 .map(|s| s.split(',').map(|ext| ext.trim().to_string()).collect())
302 .unwrap_or_else(|| vec!["tif".to_string(), "tiff".to_string()]);
303
304 Ok(Box::new(BigTiffLoader::new(
305 extensions,
306 sensor.band_count(),
307 sensor.bit_depth,
308 chunk_size,
309 )?))
310 }
311 _ => Err(GlintError::validation(format!(
312 "Unknown loader type: {}",
313 sensor.loader_type
314 ))),
315 }
316}
317
318fn list_sensors() -> Result<()> {
320 println!("Available sensors:");
321 println!();
322
323 for sensor in SENSOR_REGISTRY.sensors() {
324 println!(" {} - {}", sensor.id, sensor.name);
325 println!(
326 " Bands: {} ({}-bit)",
327 sensor.band_count(),
328 sensor.bit_depth
329 );
330 println!(" Band names: {}", sensor.band_names().join(", "));
331 if let Some(desc) = &sensor.description {
332 println!(" Description: {}", desc);
333 }
334 println!();
335 }
336
337 Ok(())
338}
339
340fn show_sensor_info(sensor_id: &str) -> Result<()> {
342 let sensor = SENSOR_REGISTRY
343 .get_sensor(sensor_id)
344 .ok_or_else(|| GlintError::sensor(format!("Unknown sensor: {}", sensor_id)))?;
345
346 println!("Sensor: {} ({})", sensor.name, sensor.id);
347 println!("Bit depth: {}", sensor.bit_depth);
348 println!("Loader type: {}", sensor.loader_type);
349
350 if let Some(desc) = &sensor.description {
351 println!("Description: {}", desc);
352 }
353
354 println!();
355 println!("Bands ({}):", sensor.band_count());
356 for (i, band) in sensor.bands.iter().enumerate() {
357 print!(
358 " {}: {} (threshold: {:.3}",
359 i + 1,
360 band.name,
361 band.default_threshold
362 );
363 if let Some(wavelength) = band.wavelength {
364 print!(", wavelength: {} nm", wavelength);
365 }
366 println!(")");
367 if let Some(desc) = &band.description {
368 println!(" {}", desc);
369 }
370 }
371
372 println!();
373 println!("Default thresholds: {:?}", sensor.default_thresholds());
374
375 if !sensor.loader_config.is_empty() {
376 println!();
377 println!("Loader configuration:");
378 for (key, value) in &sensor.loader_config {
379 println!(" {}: {}", key, value);
380 }
381 }
382
383 Ok(())
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn test_cli_parse() {
392 let mut cmd = Command::new("glint-mask");
393 for sensor in SENSOR_REGISTRY.sensors() {
394 cmd = cmd.subcommand(Command::new(sensor.id.as_str()));
395 }
396 let matches = cmd.try_get_matches_from(vec!["glint-mask", "rgb"]);
397 assert!(matches.is_ok());
398 let matches = matches.unwrap();
399 assert_eq!(matches.subcommand_name(), Some("rgb"));
400 }
401
402 #[test]
403 fn test_sensor_args_validation() {
404 let mut cmd = Command::new("test");
406 let mut subcommand = Command::new("rgb");
407 subcommand = subcommand
408 .arg(Arg::new("input").required(true))
409 .arg(Arg::new("output").required(true))
410 .arg(
411 Arg::new("thresholds")
412 .short('t')
413 .long("thresholds")
414 .value_delimiter(',')
415 .value_parser(clap::value_parser!(f64)),
416 )
417 .arg(
418 Arg::new("pixel_buffer")
419 .short('b')
420 .long("pixel_buffer")
421 .default_value("0")
422 .value_parser(clap::value_parser!(usize)),
423 );
424 cmd = cmd.subcommand(subcommand);
425 let matches = cmd.get_matches_from(vec![
426 "test",
427 "rgb",
428 "test_input",
429 "test_output",
430 "-t",
431 "0.8,0.9,0.7",
432 "-b",
433 "5",
434 ]);
435
436 let sub_matches = matches.subcommand_matches("rgb").unwrap();
437 let args = SensorArgs::from(sub_matches);
438
439 assert_eq!(args.pixel_buffer, 5);
440 assert_eq!(args.thresholds.as_ref().unwrap().len(), 3);
441 }
442
443 #[test]
444 fn test_create_loader() {
445 let rgb_sensor = SENSOR_REGISTRY.get_sensor("rgb").unwrap();
446 let loader = create_loader(rgb_sensor);
447 assert!(loader.is_ok());
448
449 let p4ms_sensor = SENSOR_REGISTRY.get_sensor("p4ms").unwrap();
450 let loader = create_loader(p4ms_sensor);
451 assert!(loader.is_ok());
452
453 let msre_sensor = SENSOR_REGISTRY.get_sensor("msre").unwrap();
454 let loader = create_loader(msre_sensor);
455 assert!(loader.is_ok());
456 }
457}