1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
use std::cmp::{Ordering, PartialOrd}; use std::fs::File; use std::path::PathBuf; use image::{ColorType, save_buffer}; use tiff::encoder::*; use ndarray::*; use crate::palettes; /// All supported thermogram formats implement this trait. /// /// ```rust /// pub trait ThermogramTrait { /// fn thermal(&self) -> &Array<f32, Ix2>; // Extract the thermal data /// fn optical(&self) -> &Array<u8, Ix3>>; // Extract embedded photos, if present /// fn identifier(&self) -> &str; // A uniquely identifying string for this thermogram /// fn render(&self min_temp: f32, max_temp: f32, palette: [[f32; 3]; 256]) -> Array<u8, Ix3>; // Thermal data render using the given palette /// fn render_defaults(&self) -> Array<u8, Ix3>; // Thermal data rendered using the minimum and maximum thermal value and the `palette::TURBO` palette. /// fn thermal_shape(&self) -> [usize; 2]; // The [height, width] of the thermal data /// fn normalized_minmax(&self) -> Array<f32, Ix2>; // Thermal data normalized to lie in the range 0.0..=1.0 /// } /// ``` pub trait ThermogramTrait { /// Returns a reference to the 2D array of thermal data in celsius. fn thermal(&self) -> &Array<f32, Ix2>; /// Returns reference to the raw RGB values of the thermogram's corresponding optical photo, if /// present. Otherwise `None`. fn optical(&self) -> Option<&Array<u8, Ix3>>; /// Provide the identifier for this thermogram, which is typically the file path. It can also be /// a randomly generated uuid or similar, however, if there is no path associated with the data. fn identifier(&self) -> &str; // Returns the file path, or `None` if not a file. fn path(&self) -> Option<&str>; /// Render the thermogram with the given color palette and using the given minimum and maximum /// temperature bounds. /// /// All values are clipped to be between the minimum and maximum value, then put in one of 256 /// bins. Each bin is mapped to one of the colors in the palette to render an RGB color value. /// /// # Arguments /// * `min_temp` - The temperature value, and all values below it, that needs to be mapped to /// the first color in the palette. /// * `max_temp` - The temperature value, and all values above it, that needs to be mapped to /// the last color in the palette. /// * `palette` - A collection of 256 colors to which the 256 bins will be mapped. /// /// # Returns /// A three-dimensional RGB array of u8 values between 0 and 255. fn render(&self, min_temp: f32, max_temp: f32, palette: [[f32; 3]; 256]) -> Array<u8, Ix3> { let num_bands = 3; let map_color = |v: &f32| { let idx = match (min_temp.partial_cmp(v), max_temp.partial_cmp(v)) { (Some(Ordering::Greater), _) => 0, (_, Some(Ordering::Less)) => 255, (_, _) => ((v - min_temp) / (max_temp - min_temp) * 255f32) as usize, }; let to_u8 = |f| (f * 255.0) as u8; let color = [ // Create color array sized [u8; num_bands] to_u8(palette[idx][0]), to_u8(palette[idx][1]), to_u8(palette[idx][2]), ]; // Create iterator out of the array so we can use this in flat_map (0..num_bands).map(move |i| color[i]) }; // Convert thermal array into a color array by iterating over all values, // converting thermal values to RGB arrays, flattening the result into a // single vector of u8s. Lastly we recreate an ndarray with the shape // (height, width, num_bands) from this vector. let colored_array: Vec<u8> = self.thermal().iter().flat_map(map_color).collect(); let width = self.thermal().ncols(); let height = self.thermal().nrows(); Array::from_shape_vec((height, width, num_bands), colored_array).unwrap() } /// Render the thermogram using the minimum and maximum thermal value and the // `palette::TURBO` palette. fn render_defaults(&self) -> Array<u8, Ix3> { self.render(self.min_temp(), self.max_temp(), palettes::TURBO) } /// Export thermal data to a tiff file. /// /// # Arguments /// `path` - Where to save the thermogram export to. Regardless of the file extension, a tiff /// file is created. /// /// # Returns /// `Some<()>` in case of success, otherwise `None`. fn export_thermal(&self, path: &PathBuf) -> Option<()> { // TODO Return LibblackbodyErrorEnum with finegrained failure info instead of Option let thermal = self.thermal().iter().map(|v| *v).collect::<Vec<f32>>(); let width = self.thermal_shape()[1] as u32; let height = self.thermal_shape()[0] as u32; match File::create(path) { // TODO Return error codes and handle in Blackbody Ok(mut file) => match TiffEncoder::new(&mut file) { Ok(mut tiff) => tiff.write_image::<colortype::Gray32Float>(width, height, &thermal).ok(), _ => None, }, _ => None, } } /// Save render to file. /// /// # Arguments /// `path` - Where to save the render to. The image type is extrapolated from the extension. /// `min_temp` - The minimum temperature for the render, see `render(..)`. /// `max_temp` - The maximum temperature for the render, see `render(..)`. /// `palette` - The color palette to render the thermogram with, see `render(..)`. /// /// # Returns /// `Some<()>` in case of success, otherwise `None`. fn save_render( &self, path: PathBuf, min_temp: f32, max_temp: f32, palette: [[f32; 3]; 256] ) -> Option<()> { let render = self.render(min_temp, max_temp, palette); let width = render.shape()[1] as u32; let height = render.shape()[0] as u32; let render = render.iter().map(|v| *v).collect::<Vec<u8>>(); // TODO Return LibblackbodyErrorEnum with finegrained failure info instead of Option save_buffer(path, &render.as_slice(), width, height, ColorType::Rgb8).ok() } /// Gives the shape of the thermal data, in the order of [height, width]. fn thermal_shape(&self) -> [usize; 2] { let thermal = self.thermal(); [thermal.nrows(), thermal.ncols()] } fn has_optical(&self) -> bool { self.optical() == None // TODO } /// Returns the lowest temperature in the thermogram, or `f32::MAX` if there is no such value. fn min_temp(&self) -> f32 { self.thermal().fold(f32::MAX, |acc, elem| acc.min(*elem)) } /// Returns the highest temperature in the thermogram, or `f32::MIN` if there is no such value. fn max_temp(&self) -> f32 { self.thermal().fold(f32::MIN, |acc, elem| acc.max(*elem)) } /// Normalized the thermal array to lie in the 0.0..=1.0 in such a way to prevent division by 0 /// errors. fn normalized_minmax(&self) -> Array<f32, Ix2> { let thermal = self.thermal(); let max_temp = self.max_temp(); let divider = match max_temp == 0.0 { true => self.min_temp() + 0.0000000001, false => max_temp, }; (thermal - self.min_temp()) / divider } }