SHA256
778 lines
26 KiB
Rust
778 lines
26 KiB
Rust
//! Entry point: parse CLI, run the requested stage(s), stream progress.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::ExitCode;
|
|
|
|
use clap::{Args, Parser, Subcommand};
|
|
|
|
use pipeline_360::error::{CancelFlag, PipelineError};
|
|
use pipeline_360::gpu::GpuContext;
|
|
use pipeline_360::logging::{self, Logger};
|
|
use pipeline_360::project::ProjectLayout;
|
|
use pipeline_360::stages::cubemap;
|
|
use pipeline_360::stages::extract::{self, BadFrameAction, ExtractConfig, MotionMode, Selection};
|
|
use pipeline_360::stages::masks::{self, MasksConfig, Projection};
|
|
use pipeline_360::stages::rig::{self, Layout, LinkMode};
|
|
|
|
/// CLI parsed into a typed struct. `gui_log_output` is read from either the
|
|
/// parent or subcommand position.
|
|
#[derive(Debug, Parser)]
|
|
#[command(
|
|
name = "pipeline_360",
|
|
version,
|
|
about = "360° -> COLMAP cubemap rig pipeline"
|
|
)]
|
|
struct Cli {
|
|
/// Whether to emit `[progress] ` lines for the GUI (accepted in any position).
|
|
#[arg(long, hide = true)]
|
|
gui_log_output: bool,
|
|
|
|
/// Launch the native GUI (requires the `gui` feature). Also launched when
|
|
/// the binary is run with no subcommand.
|
|
#[arg(long, hide = true)]
|
|
gui: bool,
|
|
|
|
#[command(subcommand)]
|
|
command: Option<Command>,
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
#[allow(clippy::large_enum_variant)]
|
|
enum Command {
|
|
/// Run the full pipeline: extract -> masks -> cubemap -> rig.
|
|
All(AllArgs),
|
|
/// Stage 1 only: extract sharpest/best frames from video.
|
|
Extract(ExtractArgs),
|
|
/// Stage 2 only: generate YOLO SfM masks (planned).
|
|
Masks(MasksArgs),
|
|
/// Stage 3 only: project equirect frames to cube faces.
|
|
Cubemap(CubemapArgs),
|
|
/// Stage 4 only: generate COLMAP rig config + nested tree (or --self-test).
|
|
Rig(RigArgs),
|
|
}
|
|
|
|
#[derive(Debug, Args, Clone)]
|
|
struct ExtractOpts {
|
|
#[arg(
|
|
long,
|
|
default_value_t = 30,
|
|
help = "Select one frame per this many frames (chunk mode)"
|
|
)]
|
|
chunk_size: u32,
|
|
#[arg(
|
|
long,
|
|
default_value_t = 1920,
|
|
help = "Resize width for analysis; 0 = native"
|
|
)]
|
|
scale_width: u32,
|
|
#[arg(
|
|
long,
|
|
default_value_t = 4,
|
|
help = "Worker count (parity; GPU is the parallelism)"
|
|
)]
|
|
workers: u32,
|
|
#[arg(
|
|
long,
|
|
default_value = "output_frame_%05d.png",
|
|
help = "printf-style output pattern"
|
|
)]
|
|
output_pattern: String,
|
|
#[arg(long, default_value = "png", help = "png | jpg")]
|
|
output_format: String,
|
|
#[arg(long, default_value_t = 95)]
|
|
jpeg_quality: u32,
|
|
#[arg(
|
|
long = "extract-regenerate-metadata",
|
|
help = "Ignore existing metadata CSV and regenerate"
|
|
)]
|
|
reuse_metadata_inv: bool,
|
|
|
|
// --- Quality thresholds ("throw away bad frames"). ---
|
|
#[arg(
|
|
long,
|
|
default_value_t = 0.0,
|
|
help = "Drop frames whose variance-of-Laplacian is below this"
|
|
)]
|
|
min_sharpness: f64,
|
|
#[arg(
|
|
long,
|
|
default_value_t = 0.0,
|
|
help = "Drop frames whose mean luma is below this (0..255)"
|
|
)]
|
|
min_exposure: f64,
|
|
#[arg(
|
|
long,
|
|
default_value_t = 255.0,
|
|
help = "Drop frames whose mean luma is above this (0..255)"
|
|
)]
|
|
max_exposure: f64,
|
|
#[arg(
|
|
long,
|
|
default_value_t = 0.0,
|
|
help = "Drop frames whose luma std is below this"
|
|
)]
|
|
min_contrast: f64,
|
|
#[arg(
|
|
long,
|
|
default_value_t = 0.0,
|
|
help = "Min inter-frame motion (0..255) — parallax baseline"
|
|
)]
|
|
min_motion: f64,
|
|
#[arg(
|
|
long,
|
|
default_value_t = 1.0e12,
|
|
help = "Max inter-frame motion (0..255) — reject jitter/blur"
|
|
)]
|
|
max_motion: f64,
|
|
|
|
// --- Selection ---
|
|
#[arg(long, default_value = "chunk", help = "chunk | spread")]
|
|
selection: String,
|
|
#[arg(
|
|
long,
|
|
default_value_t = 0,
|
|
help = "Target frame count (spread mode); 0 = all survivors / chunk-based"
|
|
)]
|
|
target_count: u32,
|
|
#[arg(
|
|
long,
|
|
default_value_t = 0,
|
|
help = "Min frame-index spacing between selected frames"
|
|
)]
|
|
min_gap: u32,
|
|
#[arg(
|
|
long,
|
|
default_value = "keep",
|
|
help = "skip | keep | warn — chunk with no surviving frame"
|
|
)]
|
|
bad_frame_action: String,
|
|
#[arg(
|
|
long,
|
|
default_value = "frame_diff",
|
|
help = "off | frame_diff — inter-frame motion proxy"
|
|
)]
|
|
motion_mode: String,
|
|
#[arg(long, default_value_t = 8, help = "Frames per GPU batch (throughput)")]
|
|
batch: u32,
|
|
}
|
|
|
|
impl ExtractOpts {
|
|
fn to_config(&self) -> Result<ExtractConfig, PipelineError> {
|
|
Ok(ExtractConfig {
|
|
chunk_size: self.chunk_size,
|
|
scale_width: self.scale_width,
|
|
workers: self.workers,
|
|
output_pattern: self.output_pattern.clone(),
|
|
output_format: self.output_format.clone(),
|
|
jpeg_quality: self.jpeg_quality,
|
|
reuse_metadata: !self.reuse_metadata_inv,
|
|
min_sharpness: self.min_sharpness,
|
|
min_exposure: self.min_exposure,
|
|
max_exposure: self.max_exposure,
|
|
min_contrast: self.min_contrast,
|
|
min_motion: self.min_motion,
|
|
max_motion: self.max_motion,
|
|
selection: Selection::parse(&self.selection)
|
|
.map_err(|e| PipelineError(e.to_string()))?,
|
|
target_count: self.target_count,
|
|
min_gap: self.min_gap,
|
|
bad_frame_action: BadFrameAction::parse(&self.bad_frame_action)
|
|
.map_err(|e| PipelineError(e.to_string()))?,
|
|
motion_mode: MotionMode::parse(&self.motion_mode)
|
|
.map_err(|e| PipelineError(e.to_string()))?,
|
|
batch: self.batch,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Shared extract flags for the `all` subcommand, with `--extract-*` prefixes
|
|
/// (mirrors the Python `all` CLI the GUI drives).
|
|
#[derive(Debug, Args, Clone)]
|
|
struct ExtractOptsAll {
|
|
#[arg(long = "extract-chunk-size", default_value_t = 30)]
|
|
chunk_size: u32,
|
|
#[arg(long = "extract-scale-width", default_value_t = 1920)]
|
|
scale_width: u32,
|
|
#[arg(long = "extract-workers", default_value_t = 4)]
|
|
workers: u32,
|
|
#[arg(
|
|
long = "extract-output-pattern",
|
|
default_value = "output_frame_%05d.png"
|
|
)]
|
|
output_pattern: String,
|
|
#[arg(long = "extract-output-format", default_value = "png")]
|
|
output_format: String,
|
|
#[arg(long = "extract-jpeg-quality", default_value_t = 95)]
|
|
jpeg_quality: u32,
|
|
#[arg(long = "extract-regenerate-metadata")]
|
|
reuse_metadata_inv: bool,
|
|
#[arg(long = "extract-min-sharpness", default_value_t = 0.0)]
|
|
min_sharpness: f64,
|
|
#[arg(long = "extract-min-exposure", default_value_t = 0.0)]
|
|
min_exposure: f64,
|
|
#[arg(long = "extract-max-exposure", default_value_t = 255.0)]
|
|
max_exposure: f64,
|
|
#[arg(long = "extract-min-contrast", default_value_t = 0.0)]
|
|
min_contrast: f64,
|
|
#[arg(long = "extract-min-motion", default_value_t = 0.0)]
|
|
min_motion: f64,
|
|
#[arg(long = "extract-max-motion", default_value_t = 1.0e12)]
|
|
max_motion: f64,
|
|
#[arg(long = "extract-selection", default_value = "chunk")]
|
|
selection: String,
|
|
#[arg(long = "extract-target-count", default_value_t = 0)]
|
|
target_count: u32,
|
|
#[arg(long = "extract-min-gap", default_value_t = 0)]
|
|
min_gap: u32,
|
|
#[arg(long = "extract-bad-frame-action", default_value = "keep")]
|
|
bad_frame_action: String,
|
|
#[arg(long = "extract-motion-mode", default_value = "frame_diff")]
|
|
motion_mode: String,
|
|
#[arg(long = "extract-batch", default_value_t = 8)]
|
|
batch: u32,
|
|
}
|
|
|
|
impl ExtractOptsAll {
|
|
fn to_config(&self) -> Result<ExtractConfig, PipelineError> {
|
|
Ok(ExtractConfig {
|
|
chunk_size: self.chunk_size,
|
|
scale_width: self.scale_width,
|
|
workers: self.workers,
|
|
output_pattern: self.output_pattern.clone(),
|
|
output_format: self.output_format.clone(),
|
|
jpeg_quality: self.jpeg_quality,
|
|
reuse_metadata: !self.reuse_metadata_inv,
|
|
min_sharpness: self.min_sharpness,
|
|
min_exposure: self.min_exposure,
|
|
max_exposure: self.max_exposure,
|
|
min_contrast: self.min_contrast,
|
|
min_motion: self.min_motion,
|
|
max_motion: self.max_motion,
|
|
selection: Selection::parse(&self.selection)
|
|
.map_err(|e| PipelineError(e.to_string()))?,
|
|
target_count: self.target_count,
|
|
min_gap: self.min_gap,
|
|
bad_frame_action: BadFrameAction::parse(&self.bad_frame_action)
|
|
.map_err(|e| PipelineError(e.to_string()))?,
|
|
motion_mode: MotionMode::parse(&self.motion_mode)
|
|
.map_err(|e| PipelineError(e.to_string()))?,
|
|
batch: self.batch,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Args)]
|
|
struct AllArgs {
|
|
#[arg(long)]
|
|
project_dir: PathBuf,
|
|
#[arg(long, num_args = 1..)]
|
|
video: Vec<PathBuf>,
|
|
#[arg(long, default_value = "extract")]
|
|
start_stage: String,
|
|
#[arg(long)]
|
|
skip_masks: bool,
|
|
#[arg(long, hide = true)]
|
|
gui_log_output: bool,
|
|
#[command(flatten)]
|
|
extract: ExtractOptsAll,
|
|
#[command(flatten)]
|
|
mask: MasksOptsAll,
|
|
#[arg(long = "cubemap-face-size", default_value_t = 1600)]
|
|
cubemap_face_size: u32,
|
|
#[arg(long = "cubemap-skip-faces", default_value = "")]
|
|
cubemap_skip_faces: String,
|
|
#[arg(long = "cubemap-jpeg-quality", default_value_t = 95)]
|
|
cubemap_jpeg_quality: u32,
|
|
#[arg(long = "cubemap-batch", default_value_t = 4)]
|
|
cubemap_batch: u32,
|
|
#[arg(long = "rig-ref-face", default_value = "F")]
|
|
rig_ref_face: String,
|
|
#[arg(long = "rig-intrinsics-convention", default_value = "linspace")]
|
|
rig_intrinsics_convention: String,
|
|
#[arg(long = "rig-link-mode", default_value = "symlink")]
|
|
rig_link_mode: String,
|
|
}
|
|
|
|
#[derive(Debug, Args)]
|
|
struct ExtractArgs {
|
|
#[arg(long, num_args = 1..)]
|
|
video: Vec<PathBuf>,
|
|
#[arg(long)]
|
|
output_dir: PathBuf,
|
|
#[arg(long, hide = true)]
|
|
gui_log_output: bool,
|
|
#[command(flatten)]
|
|
extract: ExtractOpts,
|
|
}
|
|
|
|
// Shared masks-stage flags for the `masks` subcommand (unprefixed).
|
|
#[derive(Debug, Args, Clone)]
|
|
struct MasksOpts {
|
|
#[arg(long, default_value = "person,car,bus,truck,bicycle,motorcycle")]
|
|
classes: String,
|
|
#[arg(
|
|
long,
|
|
default_value = "yolo11x-seg.onnx",
|
|
help = "ONNX segmentation model path"
|
|
)]
|
|
model: String,
|
|
#[arg(
|
|
long,
|
|
default_value = "cpu",
|
|
help = "ort execution device (cpu|cuda|rocm)"
|
|
)]
|
|
device: String,
|
|
#[arg(long, default_value_t = 0.35)]
|
|
conf: f32,
|
|
#[arg(long, default_value_t = 1280)]
|
|
imgsz: u32,
|
|
#[arg(long, default_value_t = 6)]
|
|
dilate: u32,
|
|
#[arg(long, default_value = "cubemap", help = "cubemap | equirect")]
|
|
projection: String,
|
|
#[arg(long, default_value_t = 1024)]
|
|
face_size: u32,
|
|
#[arg(long, default_value = "")]
|
|
skip_faces: String,
|
|
#[arg(long, default_value_t = 8)]
|
|
mask_batch: u32,
|
|
#[arg(long, default_value_t = 8)]
|
|
tile_batch: u32,
|
|
#[arg(long = "mask-empty-if-none")]
|
|
empty_if_none: bool,
|
|
#[arg(long, default_value_t = 0.45, help = "NMS IoU threshold")]
|
|
iou: f32,
|
|
#[arg(
|
|
long,
|
|
help = "Directory for the MIGraphX compiled-model cache (GPU path). First \
|
|
run compiles+saves; later runs load. Pair with --device rocm. \
|
|
Defaults to ~/.cache/pipeline_360/migraphx."
|
|
)]
|
|
ep_cache: Option<PathBuf>,
|
|
}
|
|
|
|
impl MasksOpts {
|
|
fn to_config(&self) -> Result<MasksConfig, PipelineError> {
|
|
let skip = cubemap::parse_skip_faces(&self.skip_faces)
|
|
.map_err(|e| PipelineError(e.to_string()))?;
|
|
Ok(MasksConfig {
|
|
classes: masks::resolve_classes(&self.classes)
|
|
.map_err(|e| PipelineError(e.to_string()))?,
|
|
model: PathBuf::from(&self.model),
|
|
device: self.device.clone(),
|
|
conf: self.conf,
|
|
imgsz: self.imgsz,
|
|
dilate: self.dilate,
|
|
projection: Projection::parse(&self.projection)
|
|
.map_err(|e| PipelineError(e.to_string()))?,
|
|
face_size: self.face_size,
|
|
skip_faces: skip,
|
|
batch: self.mask_batch,
|
|
tile_batch: self.tile_batch,
|
|
empty_if_none: self.empty_if_none,
|
|
iou: self.iou,
|
|
ep_cache: self.ep_cache.clone(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Shared masks-stage flags for `all` (with `--mask-*` prefixes, matching Python).
|
|
#[derive(Debug, Args, Clone)]
|
|
struct MasksOptsAll {
|
|
#[arg(
|
|
long = "mask-classes",
|
|
default_value = "person,car,bus,truck,bicycle,motorcycle"
|
|
)]
|
|
classes: String,
|
|
#[arg(long = "mask-model", default_value = "yolo11x-seg.onnx")]
|
|
model: String,
|
|
#[arg(long = "mask-device", default_value = "cpu")]
|
|
device: String,
|
|
#[arg(long = "mask-conf", default_value_t = 0.35)]
|
|
conf: f32,
|
|
#[arg(long = "mask-imgsz", default_value_t = 1280)]
|
|
imgsz: u32,
|
|
#[arg(long = "mask-dilate", default_value_t = 6)]
|
|
dilate: u32,
|
|
#[arg(long = "mask-projection", default_value = "cubemap")]
|
|
projection: String,
|
|
#[arg(long = "mask-face-size", default_value_t = 1024)]
|
|
face_size: u32,
|
|
#[arg(long = "mask-skip-faces", default_value = "")]
|
|
skip_faces: String,
|
|
#[arg(long = "mask-batch", default_value_t = 8)]
|
|
mask_batch: u32,
|
|
#[arg(long = "mask-tile-batch", default_value_t = 8)]
|
|
mask_tile_batch: u32,
|
|
#[arg(long = "mask-empty-if-none")]
|
|
empty_if_none: bool,
|
|
#[arg(long = "mask-iou", default_value_t = 0.45)]
|
|
iou: f32,
|
|
#[arg(
|
|
long = "mask-ep-cache",
|
|
help = "Directory for the MIGraphX compiled-model cache (GPU path). First \
|
|
run compiles+saves; later runs load. Pair with --mask-device rocm. \
|
|
Defaults to ~/.cache/pipeline_360/migraphx."
|
|
)]
|
|
ep_cache: Option<PathBuf>,
|
|
}
|
|
|
|
impl MasksOptsAll {
|
|
fn to_config(&self) -> Result<MasksConfig, PipelineError> {
|
|
let skip = cubemap::parse_skip_faces(&self.skip_faces)
|
|
.map_err(|e| PipelineError(e.to_string()))?;
|
|
Ok(MasksConfig {
|
|
classes: masks::resolve_classes(&self.classes)
|
|
.map_err(|e| PipelineError(e.to_string()))?,
|
|
model: PathBuf::from(&self.model),
|
|
device: self.device.clone(),
|
|
conf: self.conf,
|
|
imgsz: self.imgsz,
|
|
dilate: self.dilate,
|
|
projection: Projection::parse(&self.projection)
|
|
.map_err(|e| PipelineError(e.to_string()))?,
|
|
face_size: self.face_size,
|
|
skip_faces: skip,
|
|
batch: self.mask_batch,
|
|
tile_batch: self.mask_tile_batch,
|
|
empty_if_none: self.empty_if_none,
|
|
iou: self.iou,
|
|
ep_cache: self.ep_cache.clone(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Args)]
|
|
struct MasksArgs {
|
|
#[arg(long)]
|
|
input_dir: PathBuf,
|
|
#[arg(long)]
|
|
output_dir: PathBuf,
|
|
#[arg(long, hide = true)]
|
|
gui_log_output: bool,
|
|
#[command(flatten)]
|
|
opts: MasksOpts,
|
|
}
|
|
|
|
#[derive(Debug, Args)]
|
|
struct CubemapArgs {
|
|
#[arg(long)]
|
|
frames_dir: PathBuf,
|
|
#[arg(long)]
|
|
masks_dir: Option<PathBuf>,
|
|
#[arg(long)]
|
|
output_frames_dir: PathBuf,
|
|
#[arg(long)]
|
|
output_masks_dir: Option<PathBuf>,
|
|
#[arg(long, default_value_t = 1600)]
|
|
face_size: u32,
|
|
#[arg(long, default_value = "")]
|
|
skip_faces: String,
|
|
#[arg(long, default_value_t = 95)]
|
|
jpeg_quality: u32,
|
|
#[arg(long)]
|
|
resume: bool,
|
|
#[arg(long, default_value_t = 4, help = "Frames per GPU batch (throughput)")]
|
|
cubemap_batch: u32,
|
|
#[arg(long, hide = true)]
|
|
gui_log_output: bool,
|
|
}
|
|
|
|
#[derive(Debug, Args)]
|
|
struct RigArgs {
|
|
#[arg(long)]
|
|
images_dir: Option<PathBuf>,
|
|
#[arg(long)]
|
|
masks_dir: Option<PathBuf>,
|
|
#[arg(long, default_value = "rig_config.json")]
|
|
output: PathBuf,
|
|
#[arg(long)]
|
|
face_size: Option<u32>,
|
|
#[arg(long, default_value = "auto")]
|
|
layout: String,
|
|
#[arg(long)]
|
|
link_tree: Option<PathBuf>,
|
|
#[arg(long)]
|
|
masks_link_tree: Option<PathBuf>,
|
|
#[arg(long, default_value = "symlink")]
|
|
link_mode: String,
|
|
#[arg(long, default_value = "F")]
|
|
ref_face: String,
|
|
#[arg(long, default_value = "linspace")]
|
|
intrinsics_convention: String,
|
|
#[arg(long)]
|
|
self_test: bool,
|
|
#[arg(long, hide = true)]
|
|
gui_log_output: bool,
|
|
}
|
|
|
|
fn run() -> Result<(), PipelineError> {
|
|
let cli = Cli::parse();
|
|
let sub_gui = match &cli.command {
|
|
Some(Command::All(a)) => a.gui_log_output,
|
|
Some(Command::Extract(a)) => a.gui_log_output,
|
|
Some(Command::Masks(a)) => a.gui_log_output,
|
|
Some(Command::Cubemap(a)) => a.gui_log_output,
|
|
Some(Command::Rig(a)) => a.gui_log_output,
|
|
None => false,
|
|
};
|
|
let gui_log = cli.gui_log_output || sub_gui;
|
|
let logger: Logger = if gui_log {
|
|
Some(&|m| println!("{m}"))
|
|
} else {
|
|
None
|
|
};
|
|
let cancel = CancelFlag::new();
|
|
|
|
// No subcommand (or explicit --gui): launch the native GUI if available.
|
|
if cli.command.is_none() || cli.gui {
|
|
#[cfg(feature = "gui")]
|
|
{
|
|
pipeline_360::gui::launch().map_err(|e| PipelineError(format!("GUI: {e}")))?;
|
|
return Ok(());
|
|
}
|
|
#[cfg(not(feature = "gui"))]
|
|
{
|
|
use clap::CommandFactory;
|
|
Cli::command()
|
|
.print_help()
|
|
.map_err(|e| PipelineError(format!("print_help: {e}")))?;
|
|
println!("\n\n(no subcommand given; rebuild with `--features gui` to launch the GUI)");
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
match cli.command {
|
|
Some(Command::Rig(RigArgs {
|
|
self_test: true, ..
|
|
})) => {
|
|
let ok = pipeline_360::selftest::run_selftest().map_err(PipelineError)?;
|
|
logging::emit(&format!("self-test: {ok} check groups passed."), logger);
|
|
}
|
|
Some(Command::Rig(a)) => {
|
|
let images_dir = a.images_dir.as_deref().ok_or_else(|| {
|
|
PipelineError("--images-dir is required (or use --self-test)".into())
|
|
})?;
|
|
if a.masks_link_tree.is_some() && a.masks_dir.is_none() {
|
|
return Err(PipelineError(
|
|
"--masks-link-tree requires --masks-dir".into(),
|
|
));
|
|
}
|
|
run_rig(images_dir, &a, logger)?;
|
|
}
|
|
Some(Command::All(a)) => run_all(&a, logger, &cancel)?,
|
|
Some(Command::Extract(a)) => {
|
|
let cfg = a.extract.to_config()?;
|
|
let gpu = pollster::block_on(GpuContext::new("extract"))
|
|
.map_err(|e| PipelineError(format!("GPU init: {e}")))?;
|
|
extract::run_extract_stage(&a.video, &a.output_dir, &cfg, &gpu, &cancel, logger)
|
|
.map_err(|e| PipelineError(e.to_string()))?;
|
|
}
|
|
Some(Command::Masks(a)) => {
|
|
let cfg = a.opts.to_config()?;
|
|
let gpu = pollster::block_on(GpuContext::new("masks"))
|
|
.map_err(|e| PipelineError(format!("GPU init: {e}")))?;
|
|
masks::run_masks_stage(&a.input_dir, &a.output_dir, &cfg, &gpu, &cancel, logger)
|
|
.map_err(|e| PipelineError(e.to_string()))?;
|
|
}
|
|
Some(Command::Cubemap(a)) => run_cubemap(a, &cancel, logger)?,
|
|
None => unreachable!(),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn run_cubemap(a: CubemapArgs, cancel: &CancelFlag, logger: Logger) -> Result<(), PipelineError> {
|
|
if a.masks_dir.is_some() && a.output_masks_dir.is_none() {
|
|
return Err(PipelineError(
|
|
"--masks-dir given but --output-masks-dir missing".into(),
|
|
));
|
|
}
|
|
let gpu = pollster::block_on(GpuContext::new("cubemap"))
|
|
.map_err(|e| PipelineError(format!("GPU init: {e}")))?;
|
|
let skip =
|
|
cubemap::parse_skip_faces(&a.skip_faces).map_err(|e| PipelineError(e.to_string()))?;
|
|
let (fout, mout) = cubemap::run_cubemap_stage(
|
|
&a.frames_dir,
|
|
&a.output_frames_dir,
|
|
a.masks_dir.as_deref(),
|
|
a.output_masks_dir.as_deref(),
|
|
a.face_size,
|
|
&skip,
|
|
a.jpeg_quality,
|
|
a.resume,
|
|
a.cubemap_batch,
|
|
&gpu,
|
|
cancel,
|
|
logger,
|
|
)
|
|
.map_err(|e| PipelineError(e.to_string()))?;
|
|
logging::emit(&format!("Cubemap output: {}", fout.display()), logger);
|
|
if let Some(m) = mout {
|
|
logging::emit(&format!("Cubemap masks: {}", m.display()), logger);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn run_rig(images_dir: &std::path::Path, a: &RigArgs, logger: Logger) -> Result<(), PipelineError> {
|
|
let layout = match a.layout.as_str() {
|
|
"flat" => Layout::Flat,
|
|
"nested" => Layout::Nested,
|
|
_ => Layout::Auto,
|
|
};
|
|
let mode = LinkMode::parse(&a.link_mode)?;
|
|
let out = rig::run_rig_stage(
|
|
images_dir,
|
|
&a.output,
|
|
a.masks_dir.as_deref(),
|
|
a.face_size,
|
|
layout,
|
|
&a.ref_face,
|
|
&a.intrinsics_convention,
|
|
a.link_tree.as_deref(),
|
|
a.masks_link_tree.as_deref(),
|
|
mode,
|
|
)?;
|
|
for msg in &out.messages {
|
|
logging::emit(msg, logger);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn run_all(a: &AllArgs, logger: Logger, cancel: &CancelFlag) -> Result<(), PipelineError> {
|
|
let project = ProjectLayout::new(&a.project_dir);
|
|
project
|
|
.create_all()
|
|
.map_err(|e| PipelineError(format!("mkdir: {e}")))?;
|
|
|
|
let stage_idx = |s: &str| match s {
|
|
"extract" => 0,
|
|
"cubemap" => 1,
|
|
"masks" => 2,
|
|
"rig" => 3,
|
|
_ => 0,
|
|
};
|
|
let from = stage_idx(&a.start_stage);
|
|
// NOTE: order is extract -> cubemap -> masks -> rig. Masks run on the
|
|
// cube-face images the cubemap stage produces, so cubemap must come first
|
|
// (masking equirect frames is useless, and round-tripping e2c->YOLO->c2e
|
|
// then e2c again was wasteful). `02_masks` is no longer written by `all`.
|
|
let stages = ["extract", "cubemap", "masks", "rig"];
|
|
|
|
if a.video.is_empty() && from == 0 {
|
|
return Err(PipelineError(
|
|
"--video is required when starting from the extract stage.".into(),
|
|
));
|
|
}
|
|
|
|
// One GPU device shared by every GPU stage in the run.
|
|
let gpu = pollster::block_on(GpuContext::new("pipeline"))
|
|
.map_err(|e| PipelineError(format!("GPU init: {e}")))?;
|
|
|
|
for (i, stage) in stages.iter().enumerate() {
|
|
if i < from {
|
|
continue;
|
|
}
|
|
cancel
|
|
.check()
|
|
.map_err(|_| PipelineError("Cancelled.".into()))?;
|
|
logging::stage_header(i + 1, 4, stage_title(stage), logger);
|
|
match *stage {
|
|
"extract" => {
|
|
let cfg = a.extract.to_config()?;
|
|
extract::run_extract_stage(&a.video, &project.frames, &cfg, &gpu, cancel, logger)
|
|
.map_err(|e| PipelineError(e.to_string()))?;
|
|
}
|
|
"masks" => {
|
|
if a.skip_masks {
|
|
logging::emit("Skipping masks stage (--skip-masks).", logger);
|
|
} else {
|
|
let cfg = a.mask.to_config()?;
|
|
// Masks run on the cube-face images the cubemap stage
|
|
// produced, and write cube-face masks next to them.
|
|
masks::run_masks_stage(
|
|
&project.cubemap_images,
|
|
&project.cubemap_masks,
|
|
&cfg,
|
|
&gpu,
|
|
cancel,
|
|
logger,
|
|
)
|
|
.map_err(|e| PipelineError(e.to_string()))?;
|
|
}
|
|
}
|
|
"cubemap" => {
|
|
let skip = cubemap::parse_skip_faces(&a.cubemap_skip_faces)
|
|
.map_err(|e| PipelineError(e.to_string()))?;
|
|
// In the `all` flow the cubemap stage only projects images; the
|
|
// masks stage runs afterwards on those cube faces (no equirect
|
|
// mask round-trip, no double e2c projection).
|
|
cubemap::run_cubemap_stage(
|
|
&project.frames,
|
|
&project.cubemap_images,
|
|
None,
|
|
None,
|
|
a.cubemap_face_size,
|
|
&skip,
|
|
a.cubemap_jpeg_quality,
|
|
false,
|
|
a.cubemap_batch,
|
|
&gpu,
|
|
cancel,
|
|
logger,
|
|
)
|
|
.map_err(|e| PipelineError(e.to_string()))?;
|
|
}
|
|
"rig" => {
|
|
let mode = LinkMode::parse(&a.rig_link_mode)?;
|
|
let out = rig::run_rig_stage(
|
|
&project.cubemap_images,
|
|
&project.rig_config,
|
|
if a.skip_masks {
|
|
None
|
|
} else {
|
|
Some(project.cubemap_masks.as_path())
|
|
},
|
|
None,
|
|
Layout::Auto,
|
|
&a.rig_ref_face,
|
|
&a.rig_intrinsics_convention,
|
|
Some(&project.colmap_images),
|
|
if a.skip_masks {
|
|
None
|
|
} else {
|
|
Some(&project.colmap_masks)
|
|
},
|
|
mode,
|
|
)
|
|
.map_err(|e| PipelineError(e.to_string()))?;
|
|
for msg in &out.messages {
|
|
logging::emit(msg, logger);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
logging::emit("Pipeline complete.", logger);
|
|
Ok(())
|
|
}
|
|
|
|
fn stage_title(stage: &str) -> &'static str {
|
|
match stage {
|
|
"extract" => "Extract Sharpest Frames",
|
|
"masks" => "Generate SfM Masks",
|
|
"cubemap" => "Project to Cubemap",
|
|
"rig" => "Generate Rig Config",
|
|
_ => "Unknown",
|
|
}
|
|
}
|
|
|
|
fn main() -> ExitCode {
|
|
match run() {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(e) => {
|
|
eprintln!("Error: {e}");
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|