Skip to content

Commit

Permalink
simplify metadata and add slice filter for DICOM
Browse files Browse the repository at this point in the history
  • Loading branch information
woelper committed Jan 18, 2025
1 parent add5327 commit 0b3bbb1
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 137 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ Uninstalling Oculante is a quick process, just delete the executable and delete
- webp (via `libwebp-sys` - `image` had _very_ limited format support)
- farbfeld
- DDS (DXT1-5, via `dds-rs`)
- DICOM (via dicom-rs) - Some metadata supported, too.
- psd (via `psd`)
- svg (via `resvg`)
- exr (via `exr-rs`), tonemapped
Expand Down
10 changes: 7 additions & 3 deletions src/appstate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub struct OculanteState {
pub current_path: Option<PathBuf>,
pub current_image: Option<DynamicImage>,
pub settings_enabled: bool,
pub image_info: Option<ExtendedImageInfo>,
pub image_metadata: Option<ExtendedImageInfo>,
pub tiling: usize,
pub mouse_grab: bool,
pub key_grab: bool,
Expand Down Expand Up @@ -133,7 +133,11 @@ impl<'b> Default for OculanteState {
cursor: Default::default(),
cursor_relative: Default::default(),
sampled_color: [0., 0., 0., 0.],
player: Player::new(tx_channel.0.clone(), 20, meta_channel.0.clone(), msg_channel.0.clone()),
player: Player::new(
tx_channel.0.clone(),
20,
msg_channel.0.clone(),
),
texture_channel: tx_channel,
message_channel: msg_channel,
load_channel: mpsc::channel(),
Expand All @@ -144,7 +148,7 @@ impl<'b> Default for OculanteState {
current_image: Default::default(),
current_path: Default::default(),
settings_enabled: Default::default(),
image_info: Default::default(),
image_metadata: Default::default(),
tiling: 1,
mouse_grab: Default::default(),
key_grab: Default::default(),
Expand Down
39 changes: 35 additions & 4 deletions src/image_editing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ impl fmt::Display for ImgOpItem {
pub enum ImageOperation {
ColorConverter(ColorTypeExt),
Brightness(i32),
/// discard pixels around a threshold: position and range, bool for mode.
Slice(u8, u8, bool),
Expression(String),
Desaturate(u8),
Posterize(u8),
Expand Down Expand Up @@ -241,6 +243,7 @@ impl fmt::Display for ImageOperation {
match *self {
Self::ColorConverter(_) => write!(f, "Color Type"),
Self::Brightness(_) => write!(f, "Brightness"),
Self::Slice(..) => write!(f, "Slice"),
Self::Noise { .. } => write!(f, "Noise"),
Self::Desaturate(_) => write!(f, "Desaturate"),
Self::Posterize(_) => write!(f, "Posterize"),
Expand Down Expand Up @@ -317,6 +320,21 @@ impl ImageOperation {
x
}
Self::Brightness(val) => ui.styled_slider(val, -255..=255),
Self::Slice(position, range, hard) => {
let mut x = ui.allocate_response(vec2(0.0, 0.0), Sense::click_and_drag());
ui.label("Position");
if ui.styled_slider(position, 0..=255).changed() {
x.mark_changed();
}
ui.label("Range");
if ui.styled_slider(range, 0..=255).changed() {
x.mark_changed();
}
if ui.styled_checkbox(hard, "Smooth").changed() {
x.mark_changed();
}
x
}
Self::Exposure(val) => ui.styled_slider(val, -100..=100),
Self::ChromaticAberration(val) => ui.styled_slider(val, 0..=255),
Self::Filter3x3(val) => {
Expand Down Expand Up @@ -1465,9 +1483,6 @@ impl ImageOperation {
}
Self::Exposure(amt) => {
let amt = (*amt as f32 / 100.) * 4.;

// *p = *p * Vector4::new(2., 2., 2., 2.).;

p[0] = p[0] * (2_f32).powf(amt);
p[1] = p[1] * (2_f32).powf(amt);
p[2] = p[2] * (2_f32).powf(amt);
Expand All @@ -1481,7 +1496,23 @@ impl ImageOperation {
p[1] = egui::lerp(bounds.0..=bounds.1, p[1]);
p[2] = egui::lerp(bounds.0..=bounds.1, p[2]);
}

Self::Slice(position, range, smooth) => {
let normalized_pos = (*position as f32) / 255.;
let normalized_range = (*range as f32) / 255.;
for i in 0..=2 {
if *smooth {
let distance = 1.0 - (normalized_pos - p[i]).abs();
p[i] *= distance + normalized_range;
} else {
if p[i] > normalized_pos + normalized_range {
p[i] = 0.;
}
if p[i] < (normalized_pos - normalized_range) {
p[i] = 0.;
}
}
}
}
Self::GradientMap(col) => {
let brightness = 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
// let res = interpolate_spline(col, brightness);
Expand Down
30 changes: 1 addition & 29 deletions src/image_loader.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::ktx2_loader::CompressedImageFormats;
use crate::utils::{fit, ExtendedImageInfo, Frame};
use crate::utils::{fit, Frame};
use crate::{appstate::Message, ktx2_loader, FONT};
use log::{debug, error, info};
use psd::Psd;
Expand Down Expand Up @@ -29,7 +29,6 @@ use zune_png::zune_core::result::DecodingResult;
pub fn open_image(
img_location: &Path,
message_sender: Option<Sender<Message>>,
metadata_sender: Option<Sender<ExtendedImageInfo>>,
) -> Result<Receiver<Frame>> {
let (sender, receiver): (Sender<Frame>, Receiver<Frame>) = channel();
let img_location = (*img_location).to_owned();
Expand Down Expand Up @@ -96,33 +95,6 @@ pub fn open_image(
"dcm" | "ima" => {
use dicom_pixeldata::PixelDecoder;
let obj = dicom_object::open_file(img_location)?;


// WIP: Find out interesting items to display
for name in &[
"StudyDate",
"ModalitiesInStudy",
"Modality",
"SourceType",
"ImageType",
"Manufacturer",
"InstitutionName",
"PrivateDataElement",
"PrivateDataElementName",
"OperatorsName",
"ManufacturerModelName",
"PatientName",
"PatientBirthDate",
"PatientAge",
"PixelSpacing",
] {
if let Ok(e) = obj.element_by_name(name) {
if let Ok(s) = e.to_str() {
info!("{name}: {s}");
}
}
}

let image = obj.decode_pixel_data()?;
let dynamic_image = image.to_dynamic_image(0)?;
_ = sender.send(Frame::new_still(dynamic_image));
Expand Down
42 changes: 13 additions & 29 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,7 @@ fn init(_app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins) -> OculanteSt
state.player = Player::new(
state.texture_channel.0.clone(),
state.persistent_settings.max_cache,
state.extended_info_channel.0.clone(),
state.message_channel.0.clone()

state.message_channel.0.clone(),
);

debug!("matches {:?}", matches);
Expand All @@ -224,16 +222,12 @@ fn init(_app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins) -> OculanteSt
if let Ok(first_img_location) = find_first_image_in_directory(location) {
state.is_loaded = false;
state.current_path = Some(first_img_location.clone());
state
.player
.load(&first_img_location);
state.player.load(&first_img_location);
}
} else {
state.is_loaded = false;
state.current_path = Some(location.clone().clone());
state
.player
.load(&location);
state.player.load(&location);
};
}

Expand All @@ -257,7 +251,6 @@ fn init(_app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins) -> OculanteSt
state.player.load_advanced(
&location,
Some(Frame::ImageCollectionMember(Default::default())),

);
};
state.scrubber.entries = paths_to_open.clone();
Expand Down Expand Up @@ -560,11 +553,6 @@ fn process_events(app: &mut App, state: &mut OculanteState, evt: Event) {
}
if key_pressed(app, state, InfoMode) {
state.persistent_settings.info_enabled = !state.persistent_settings.info_enabled;
send_extended_info(
&state.current_image,
&state.current_path,
&state.extended_info_channel,
);
}
if key_pressed(app, state, EditMode) {
state.persistent_settings.edit_enabled = !state.persistent_settings.edit_enabled;
Expand Down Expand Up @@ -727,9 +715,7 @@ fn update(app: &mut App, state: &mut OculanteState) {
let t = app.timer.elapsed_f32() % 0.8;
if t <= 0.05 {
trace!("chk mod {}", t);
state
.player
.check_modified(p);
state.player.check_modified(p);
}
}

Expand Down Expand Up @@ -777,14 +763,14 @@ fn update(app: &mut App, state: &mut OculanteState) {
}

// redraw if extended info is missing so we make sure it's promply displayed
if state.persistent_settings.info_enabled && state.image_info.is_none() {
if state.persistent_settings.info_enabled && state.image_metadata.is_none() {
app.window().request_frame();
}

// check extended info has been sent
if let Ok(info) = state.extended_info_channel.1.try_recv() {
debug!("Received extended image info for {}", info.name);
state.image_info = Some(info);
state.image_metadata = Some(info);
app.window().request_frame();
}

Expand Down Expand Up @@ -959,7 +945,7 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O
}
}
state.redraw = false;
state.image_info = None;
// state.image_info = None;
}
Frame::EditResult(_) => {
state.redraw = false;
Expand Down Expand Up @@ -1046,14 +1032,12 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O
// In those cases, we want the image to stay as it is.
// TODO: PERF: This copies the image buffer. This should also maybe not run for animation frames
// although it looks cool.
if state.persistent_settings.info_enabled {
debug!("Sending extended info");
send_extended_info(
&state.current_image,
&state.current_path,
&state.extended_info_channel,
);
}
state.image_metadata = None;
send_extended_info(
&state.current_image,
&state.current_path,
&state.extended_info_channel,
);
}

if state.redraw {
Expand Down
2 changes: 1 addition & 1 deletion src/thumbnails.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub fn generate<P: AsRef<Path>>(source_path: P) -> Result<()> {
source_path.as_ref().display(),
dest_path.display()
);
let f = open_image(source_path.as_ref(), None, None)?;
let f = open_image(source_path.as_ref(), None)?;
let i = f.recv()?.get_image().context("Can't get buffer")?;

debug!("\tOpened {}", source_path.as_ref().display());
Expand Down
16 changes: 5 additions & 11 deletions src/ui/edit_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu
ImgOpItem::new(ImageOperation::Add([0, 0, 0])),
ImgOpItem::new(ImageOperation::Mult([255, 255, 255])),
ImgOpItem::new(ImageOperation::Fill([255, 255, 255, 255])),
ImgOpItem::new(ImageOperation::Slice(128, 20, false)),
// Colour Mapping and Conversion
ImgOpItem::new(ImageOperation::LUT("Lomography Redscale 100".into())),
ImgOpItem::new(ImageOperation::GradientMap(vec![
Expand Down Expand Up @@ -458,7 +459,7 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu
key_slice.as_slice(),
&mut state.volatile_settings,
|p| {
_ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_info, &encoders);
_ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_metadata, &encoders);
},
ctx,
);
Expand All @@ -470,14 +471,14 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu
if let Some(p) = &state.current_path {
let text = if p.exists() { "Overwrite" } else { "Save"};
let modal = show_modal(ui.ctx(), "Overwrite?", |_|{
_ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_info, &state.volatile_settings.encoding_options).map(|_| state.send_message_info("Saved")).map_err(|e| state.send_message_err(&format!("Error: {e}")));
_ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_metadata, &state.volatile_settings.encoding_options).map(|_| state.send_message_info("Saved")).map_err(|e| state.send_message_err(&format!("Error: {e}")));
}, "overwrite");

if ui.button(text).on_hover_text("Saves the image. This will create a new file or overwrite an existing one.").clicked() {
if p.exists() {
modal.open();
} else {
_ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_info, &state.volatile_settings.encoding_options).map(|_| state.send_message_info("Saved")).map_err(|e| state.send_message_err(&format!("Error: {e}")));
_ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_metadata, &state.volatile_settings.encoding_options).map(|_| state.send_message_info("Saved")).map_err(|e| state.send_message_err(&format!("Error: {e}")));
}
}

Expand Down Expand Up @@ -629,14 +630,7 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu

state.image_geometry.dimensions = state.edit_state.result_pixel_op.dimensions();

if pixels_changed && state.persistent_settings.info_enabled {
state.image_info = None;
send_extended_info(
&Some(state.edit_state.result_pixel_op.clone()),
&state.current_path,
&state.extended_info_channel,
);
}

});
}

Expand Down
27 changes: 25 additions & 2 deletions src/ui/info_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, _gfx: &mut Graphics) ->
ui.ctx().request_repaint();
ui.ctx().request_repaint_after(Duration::from_millis(500));
state.current_path = Some(path);
state.image_info = None;
}
});
});
Expand Down Expand Up @@ -269,6 +268,7 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, _gfx: &mut Graphics) ->
ui.styled_slider(&mut state.tiling, 1..=10);
});
}

advanced_ui(ui, state);

});
Expand All @@ -277,7 +277,7 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, _gfx: &mut Graphics) ->
}

fn advanced_ui(ui: &mut Ui, state: &mut OculanteState) {
if let Some(info) = &state.image_info {
if let Some(info) = &state.image_metadata {
egui::Grid::new("extended").num_columns(2).show(ui, |ui| {
ui.label("Number of colors");
ui.label_right(format!("{}", info.num_colors));
Expand Down Expand Up @@ -317,6 +317,29 @@ fn advanced_ui(ui: &mut Ui, state: &mut OculanteState) {
});
}

if let Some(dicom) = &info.dicom {
ui.styled_collapsing("DICOM", |ui| {
dark_panel(ui, |ui| {
for (key, val) in &dicom.dicom_data {
ui.scope(|ui| {
ui.style_mut().override_font_id =
Some(FontId::new(14., FontFamily::Name("bold".into())));
ui.colored_label(
if ui.style().visuals.dark_mode {
Color32::from_gray(200)
} else {
Color32::from_gray(20)
},
key,
);
});
ui.label(val);
ui.separator();
}
});
});
}

let red_vals = Line::new(
info.red_histogram
.iter()
Expand Down
5 changes: 0 additions & 5 deletions src/ui/top_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,6 @@ pub fn main_menu(ui: &mut Ui, state: &mut OculanteState, app: &mut App, gfx: &mu
.clicked()
{
state.persistent_settings.info_enabled = !state.persistent_settings.info_enabled;
send_extended_info(
&state.current_image,
&state.current_path,
&state.extended_info_channel,
);
}
if window_x > ui.cursor().left() + 80. {
if tooltip(
Expand Down
Loading

0 comments on commit 0b3bbb1

Please sign in to comment.