use crate::{
cfg::{ExportPath, ExportPathConnection},
file_util::path_to_str,
menu::{
params_menu::{
ExistingParamMenuAction, add_buffer_sorted, add_parameter_menu, existing_params_menu,
no_more_cols,
},
ui_util::{process_number, removable_rows},
},
parameters::ParamVal,
result::trace_ok_err,
tools::{BBOX_NAME, BRUSH_NAME, get_visible_inactive_names},
tools_data::{
AnnotationsMap, AttributesToolData, BrushToolData, CoreOptions, ImportExportTrigger,
InstanceAnnotate, LabelInfo, OUTLINE_THICKNESS_CONVERSION, ToolSpecifics, ToolsData,
VisibleInactiveToolsState,
annotations::SplitMode,
bbox_data::BboxToolData,
brush_data::{MAX_INTENSITY, MAX_THICKNESS, MIN_INTENSITY, MIN_THICKNESS},
predictive_labeling::PredictiveLabelingData,
},
};
use egui::Ui;
use rvimage_domain::TPtF;
use rvimage_domain::{RvResult, to_rv};
use std::{mem, path::PathBuf, str::FromStr};
use tracing;
use tracing::{info, warn};
use super::ui_util::{slider, text_edit_singleline};
enum LabelEditMode {
Add,
Rename,
}
fn new_label_text(
ui: &mut Ui,
new_label: &mut String,
are_tools_active: &mut bool,
) -> Option<(String, LabelEditMode)> {
text_edit_singleline(ui, new_label, are_tools_active);
ui.horizontal(|ui| {
if ui.button("add").clicked() {
Some((new_label.clone(), LabelEditMode::Add))
} else if ui.button("rename").clicked() {
Some((new_label.clone(), LabelEditMode::Rename))
} else {
None
}
})
.inner
}
fn show_inactive_tool_menu(
ui: &mut Ui,
tool_name: &'static str,
visible: &mut VisibleInactiveToolsState,
) -> bool {
ui.label("Show inactive tool");
let mut changed = false;
let inactives = get_visible_inactive_names(tool_name);
for (name, show) in inactives.iter().zip(visible.iter_mut()) {
changed |= ui.checkbox(show, *name).changed();
}
changed
}
#[derive(Default)]
pub struct LabelMenuResult {
pub label_change: bool,
pub show_only_change: bool,
}
pub fn label_menu<'a, T>(
ui: &mut Ui,
label_info: &mut LabelInfo,
annotations_map: &mut AnnotationsMap<T>,
are_tools_active: &mut bool,
) -> LabelMenuResult
where
T: InstanceAnnotate + 'a,
{
let mut new_idx = label_info.cat_idx_current;
let mut label_change = false;
let mut show_only_change = false;
let new_label = new_label_text(ui, &mut label_info.new_label, are_tools_active);
let default_label = label_info.find_default();
if let (Some(default_label), Some((new_label, _))) = (default_label, new_label.as_ref()) {
info!("replaced default '{default_label}' label by '{new_label}'");
default_label.clone_from(new_label);
label_change = true;
} else if let Some((new_label, edit_mode)) = new_label {
match edit_mode {
LabelEditMode::Add => {
if let Err(e) = label_info.push(new_label, None, None) {
warn!("{e:?}");
return LabelMenuResult::default();
}
label_change = true;
new_idx = label_info.len() - 1;
}
LabelEditMode::Rename => {
if let Err(e) = label_info.rename_label(label_info.cat_idx_current, new_label) {
warn!("{e:?}");
return LabelMenuResult::default();
}
label_change = true;
}
}
}
let mut show_only_current = label_info.show_only_current;
let mut to_be_removed = None;
let n_rows = label_info.labels().len();
egui::Grid::new("label_grid").num_columns(3).show(ui, |ui| {
to_be_removed = removable_rows(ui, n_rows, |ui, label_idx| {
let label = label_info.labels().get(label_idx);
if let Some(label) = label {
let checked = label_idx == label_info.cat_idx_current;
let label = if show_only_current && checked {
egui::RichText::new(label).monospace().strong().italics()
} else {
egui::RichText::new(label).monospace()
};
if ui.selectable_label(checked, label).clicked() {
if checked {
show_only_current = !label_info.show_only_current;
show_only_change = true;
}
new_idx = label_idx;
}
}
let rgb = label_info.colors().get(label_idx);
if let Some(rgb) = rgb {
ui.label(
egui::RichText::new("â– ")
.heading()
.strong()
.color(egui::Color32::from_rgb(rgb[0], rgb[1], rgb[2])),
);
}
ui.end_row();
});
});
label_info.show_only_current = show_only_current;
if new_idx != label_info.cat_idx_current {
for (annos, _) in annotations_map.values_mut() {
annos.label_selected(new_idx);
}
label_change = true;
label_info.cat_idx_current = new_idx;
}
if let Some(tbr) = to_be_removed {
label_change = true;
label_info.remove_catidx(tbr, annotations_map)
}
if label_change {
label_info.show_only_current = false;
}
LabelMenuResult {
label_change,
show_only_change,
}
}
fn hide_menu(ui: &mut Ui, mut core_options: CoreOptions) -> CoreOptions {
let mut hide = !core_options.visible;
if ui.checkbox(&mut hide, "hide").changed() {
core_options.is_redraw_annos_triggered = true;
core_options.visible = !hide;
}
core_options
}
fn export_file_menu(
ui: &mut Ui,
label: &str,
export_path: &mut ExportPath,
are_tools_active: &mut bool,
import_export_trigger: &mut ImportExportTrigger,
double_check_shape: Option<&mut bool>,
skip_import_mode: bool,
) -> RvResult<()> {
let mut file_txt = path_to_str(&export_path.path)?.to_string();
ui.horizontal(|ui| {
ui.label(label);
ui.radio_value(&mut export_path.conn, ExportPathConnection::Local, "local");
ui.radio_value(&mut export_path.conn, ExportPathConnection::Ssh, "ssh");
});
text_edit_singleline(ui, &mut file_txt, are_tools_active)
.on_hover_text(path_to_str(&export_path.path)?);
if path_to_str(&export_path.path)? != file_txt {
export_path.path = PathBuf::from_str(&file_txt).map_err(to_rv)?;
}
ui.horizontal(|ui| {
if ui.button("export").clicked() {
tracing::info!("clicked on export trigger");
import_export_trigger.trigger_export();
}
if ui.button("import").clicked() {
tracing::info!("clicked on import trigger");
import_export_trigger.trigger_import();
}
if let Some(double_check_shape) = double_check_shape {
ui.checkbox(double_check_shape, "double check shape")
.on_hover_text(
"For shape correction the image needs to be loaded which slows down the export.",
);
}
if skip_import_mode {
let mut checked = import_export_trigger.merge_mode();
ui.checkbox(&mut checked, "merge import");
if checked {
import_export_trigger.use_merge_import();
} else {
import_export_trigger.use_replace_import();
}
}
});
Ok(())
}
fn toggle_erase(ui: &mut Ui, mut options: CoreOptions) -> CoreOptions {
if ui.checkbox(&mut options.erase, "erase").clicked() {
if options.erase {
info!("start erasing");
} else {
info!("stop erasing");
}
}
options
}
fn transparency_slider(
ui: &mut Ui,
are_tools_active: &mut bool,
alpha: &mut u8,
name: &str,
) -> bool {
let mut transparency: f32 = *alpha as f32 / 255.0 * 100.0;
let is_redraw_triggered =
slider(ui, are_tools_active, &mut transparency, 0.0..=100.0, name).changed();
*alpha = (transparency / 100.0 * 255.0).round() as u8;
is_redraw_triggered
}
pub fn bbox_menu(
ui: &mut Ui,
mut window_open: bool,
mut data: BboxToolData,
are_tools_active: &mut bool,
mut visible_inactive_tools: VisibleInactiveToolsState,
) -> RvResult<ToolsData> {
let LabelMenuResult {
label_change,
show_only_change,
} = label_menu(
ui,
&mut data.label_info,
&mut data.annotations_map,
are_tools_active,
);
if label_change {
data.options.core = data.options.core.trigger_redraw_and_hist();
}
if show_only_change {
data.options.core.is_redraw_annos_triggered = true;
}
ui.separator();
data.options.core = toggle_erase(ui, data.options.core);
data.options.core = hide_menu(ui, data.options.core);
ui.checkbox(&mut data.options.core.auto_paste, "auto paste");
let mut export_file_menu_result = Ok(());
egui::CollapsingHeader::new("advanced").show(ui, |ui| {
ui.checkbox(&mut data.options.core.track_changes, "track changes");
ui.horizontal(|ui| {
ui.separator();
ui.label("split mode");
ui.radio_value(&mut data.options.split_mode, SplitMode::None, "none");
ui.radio_value(
&mut data.options.split_mode,
SplitMode::Horizontal,
"horizontal",
);
ui.radio_value(
&mut data.options.split_mode,
SplitMode::Vertical,
"vertical",
);
});
egui::CollapsingHeader::new("View").show(ui, |ui| {
if transparency_slider(
ui,
are_tools_active,
&mut data.options.fill_alpha,
"fill transparency",
) {
data.options.core.is_redraw_annos_triggered = true;
}
if transparency_slider(
ui,
are_tools_active,
&mut data.options.outline_alpha,
"outline transparency",
) {
data.options.core.is_redraw_annos_triggered = true;
}
let mut outline_thickness_f =
data.options.outline_thickness as TPtF / OUTLINE_THICKNESS_CONVERSION;
if slider(
ui,
are_tools_active,
&mut outline_thickness_f,
0.0..=10.0,
"outline thickness",
)
.changed()
{
data.options.core.is_redraw_annos_triggered = true;
}
data.options.outline_thickness =
(outline_thickness_f * OUTLINE_THICKNESS_CONVERSION).round() as u16;
if slider(
ui,
are_tools_active,
&mut data.options.drawing_distance,
1..=50,
"drawing distance parameter",
)
.changed()
{
data.options.core.is_redraw_annos_triggered = true;
}
ui.separator();
if ui.button("new random colors").clicked() {
data.options.core.is_colorchange_triggered = true;
}
});
egui::CollapsingHeader::new("Coco Import/Export").show(ui, |ui| {
let skip_import_mode = false;
export_file_menu_result = export_file_menu(
ui,
"coco file",
&mut data.coco_file,
are_tools_active,
&mut data.options.core.import_export_trigger,
Some(&mut data.options.core.doublecheck_cocoexport_shape),
skip_import_mode,
);
});
egui::CollapsingHeader::new("Predictive Labeling").show(ui, |ui| {
let mut pd = mem::take(&mut data.predictive_labeling_data);
trace_ok_err(predictive_labeling_menu(ui, &mut pd, are_tools_active));
data.predictive_labeling_data = pd;
});
});
export_file_menu_result?;
ui.separator();
if show_inactive_tool_menu(ui, BBOX_NAME, &mut visible_inactive_tools) {
data.options.core.is_redraw_annos_triggered = true;
}
ui.separator();
ui.horizontal(|ui| {
if ui.button("close").clicked() {
window_open = false;
}
});
Ok(ToolsData {
specifics: ToolSpecifics::Bbox(data),
menu_active: window_open,
visible_inactive_tools,
})
}
pub fn brush_menu(
ui: &mut Ui,
mut window_open: bool,
mut data: BrushToolData,
are_tools_active: &mut bool,
mut visible_inactive_tools: VisibleInactiveToolsState,
) -> RvResult<ToolsData> {
let LabelMenuResult {
label_change,
show_only_change,
} = label_menu(
ui,
&mut data.label_info,
&mut data.annotations_map,
are_tools_active,
);
if label_change {
data.options.core = data.options.core.trigger_redraw_and_hist();
}
if show_only_change {
data.options.core.is_redraw_annos_triggered = true;
}
ui.separator();
data.options.core = toggle_erase(ui, data.options.core);
data.options.core = hide_menu(ui, data.options.core);
ui.checkbox(&mut data.options.core.auto_paste, "auto paste");
egui::CollapsingHeader::new("advanced").show(ui, |ui| {
ui.checkbox(&mut data.options.core.track_changes, "track changes");
ui.separator();
ui.label("properties");
if slider(
ui,
are_tools_active,
&mut data.options.thickness,
MIN_THICKNESS..=MAX_THICKNESS,
"thickness",
)
.changed()
{
data.options.is_selection_change_needed = true;
}
if slider(
ui,
are_tools_active,
&mut data.options.intensity,
MIN_INTENSITY..=MAX_INTENSITY,
"intensity",
)
.changed()
{
data.options.is_selection_change_needed = true;
}
ui.separator();
ui.label("visualization");
if transparency_slider(
ui,
are_tools_active,
&mut data.options.fill_alpha,
"transparency",
) {
data.options.core.is_redraw_annos_triggered = true;
}
if ui.button("new random colors").clicked() {
data.options.core.is_colorchange_triggered = true;
}
ui.separator();
ui.checkbox(
&mut data.options.per_file_crowd,
"export merged annotations per file",
);
egui::CollapsingHeader::new("Coco Import/Export").show(ui, |ui| {
let skip_import_mode = false;
trace_ok_err(export_file_menu(
ui,
"coco file",
&mut data.coco_file,
are_tools_active,
&mut data.options.core.import_export_trigger,
Some(&mut data.options.core.doublecheck_cocoexport_shape),
skip_import_mode,
));
});
egui::CollapsingHeader::new("Predictive Labeling").show(ui, |ui| {
let mut pd = mem::take(&mut data.predictive_labeling_data);
trace_ok_err(predictive_labeling_menu(ui, &mut pd, are_tools_active));
data.predictive_labeling_data = pd;
});
});
ui.separator();
if show_inactive_tool_menu(ui, BRUSH_NAME, &mut visible_inactive_tools) {
data.options.core.is_redraw_annos_triggered = true;
}
ui.separator();
if ui.button("close").clicked() {
window_open = false;
}
Ok(ToolsData {
specifics: ToolSpecifics::Brush(data),
menu_active: window_open,
visible_inactive_tools,
})
}
pub fn attributes_menu(
ui: &mut Ui,
mut window_open: bool,
mut data: AttributesToolData,
are_tools_active: &mut bool,
) -> RvResult<ToolsData> {
let add_new;
(data.new_attr_name, data.new_attr_val, add_new) = add_parameter_menu(
ui,
mem::take(&mut data.new_attr_name),
mem::take(&mut data.new_attr_val),
data.attr_names().iter(),
are_tools_active,
);
if add_new {
data.options.is_addition_triggered = true;
data.options.is_update_triggered = true;
}
let mut to_propagate = mem::take(&mut data.to_propagate_attr_val);
let more_cols = |ui: &mut Ui, input_changed: bool, idx_row: usize, attr_val: ParamVal| {
let mut is_update_triggered = false;
if input_changed {
to_propagate.retain(|(idx_attr, _)| *idx_attr != idx_row);
is_update_triggered = true;
}
let checked = to_propagate
.iter()
.any(|(idx_attr, _)| *idx_attr == idx_row);
if ui
.selectable_label(checked, "propagate")
.on_hover_text("propagate attribute value to next opened image")
.clicked()
{
if checked {
to_propagate.retain(|(idx_attr, _)| *idx_attr != idx_row);
} else {
to_propagate.push((idx_row, attr_val));
}
}
is_update_triggered
};
let param_buffers = mem::take(data.attr_value_buffers_mut());
if let Some(attr_map) = &mut data.current_attr_map {
let existing_res = existing_params_menu(
ui,
mem::take(attr_map),
are_tools_active,
more_cols,
param_buffers,
);
data.current_attr_map = Some(existing_res.param_map);
*data.attr_value_buffers_mut() = existing_res.buffers;
data.to_propagate_attr_val = to_propagate;
match existing_res.action {
ExistingParamMenuAction::Rename(idx) => {
data.options.rename_src_idx = Some(idx);
}
ExistingParamMenuAction::Remove(idx) => {
data.options.removal_idx = Some(idx);
}
ExistingParamMenuAction::None => (),
}
if existing_res.has_value_changed {
data.options.is_update_triggered = true;
}
}
ui.separator();
let skip_merge_menu = true;
ui.checkbox(
&mut data.options.export_only_opened_folder,
"export only opened folder",
);
export_file_menu(
ui,
"export attributes as json",
&mut data.export_path,
are_tools_active,
&mut data.options.import_export_trigger,
None,
skip_merge_menu,
)?;
ui.separator();
if ui.button("Close").clicked() {
window_open = false;
}
Ok(ToolsData {
specifics: ToolSpecifics::Attributes(data),
menu_active: window_open,
visible_inactive_tools: VisibleInactiveToolsState::default(),
})
}
pub fn predictive_labeling_menu(
ui: &mut Ui,
data: &mut PredictiveLabelingData,
are_tools_active: &mut bool,
) -> RvResult<()> {
ui.label("Parameters");
let add_param;
(
data.new_param_name_buffer,
data.new_param_val_buffer,
add_param,
) = add_parameter_menu(
ui,
mem::take(&mut data.new_param_name_buffer),
mem::take(&mut data.new_param_val_buffer),
data.parameters.keys(),
are_tools_active,
);
if add_param {
add_buffer_sorted(
&data.parameters,
&data.new_param_name_buffer,
"".to_string(),
&mut data.param_buffers,
);
data.parameters.insert(
mem::take(&mut data.new_param_name_buffer),
mem::take(&mut data.new_param_val_buffer),
);
}
let res = existing_params_menu(
ui,
mem::take(&mut data.parameters),
are_tools_active,
no_more_cols,
mem::take(&mut data.param_buffers),
);
res.apply(
&mut data.parameters,
&mut data.param_buffers,
&data.new_param_name_buffer,
);
text_edit_singleline(ui, &mut data.url, are_tools_active).on_hover_text("url");
if data.timeout_buffer.is_empty() {
data.timeout_buffer = data.timeout_ms.to_string();
}
let (_, val) = process_number::<usize>(
ui,
are_tools_active,
"timeout [ms]",
&mut data.timeout_buffer,
);
if let Some(val) = val {
data.timeout_ms = val;
}
if ui.button("Predict").clicked() {
tracing::info!("Predictive labeling triggered");
data.trigger_prediction();
}
Ok(())
}