Image
Now that we have our data in a CSV file, we are operating on it. The first thing that we should do is make an image.
Artist Impression
Often artists are commissioned to create a stunning visualization of new findings. This is also the case with the Trappist-1 news. Above you find an artist impression of Trappist-1.
The downside of this is that we could loose track of the actual data that is used. In order to get a sense of awe for the search of exo-planets, we are creating our own impression.
Creating an image
So go ahead and start a new Rust file named image.rs
in the src/bin
directory of your project.
Reading Data
We will be reading our data from CSV. We will use the crate simple_csv
for
that. In order to use it include the following lines in image.rs
.
# #![allow(unused_variables)] #fn main() { extern crate simple_csv; use simple_csv::SimpleCsvReader; #}
The SimpleCsvReader
expects some sort of BufReader
, a buffered reader. We
can create one from a File
. So include the following modules.
# #![allow(unused_variables)] #fn main() { use std::fs::File; use std::io::BufReader; #}
And in the main function add.
# #![allow(unused_variables)] #fn main() { let f = File::open("../long-cadence.csv").expect("input CSV to exist."); let buf = BufReader::new(f); #}
Notice that we are not handling errors in a graceful way. We are just going to arrange everything correctly and hope for the best.
With the buf
we can create a CSV reader and read the first row of our data.
# #![allow(unused_variables)] #fn main() { let mut reader = SimpleCsvReader::new(buf); let row = reader.next_row().unwrap().unwrap(); #}
The unsightly double unwrap
at the end comes from the interplay of the
Iterator
trait that has a next
function that returns an Option
, and the
way simple_csv
parses lines from CSV files into a Result
. So the first
unwrap
unwraps the Option
, the second unwrap
unwraps the Result
.
We should make a mental note when working with the simple_csv
crate, we should
mind our unwrap
s.
Processing Data
Our CSV file contains rows of floating point numbers. But the simple_csv
crate
returns a slice of Strings. We will need to turn those Strings into floating
point numbers before we can properly process them.
We do this by iterating over the row
. Remember how the first column
contained the time? We don't need it now so we will drop it for the moment.
# #![allow(unused_variables)] #fn main() { let mut current_row = row.iter(); current_row.next(); // dropping time #}
Next we can transform all the measurements in floating point numbers. We can do
that by using the FromStr
trait. Import it with use std::str::FromStr
. It
provides a method from_str
that transforms &str
into an other type.
# #![allow(unused_variables)] #fn main() { let raw: Vec<f64> = current_row .map(|s| f64::from_str(s).unwrap()) .collect(); #}
Note we need to include a use std::str::FromStr;
line at the top of our file.
What we are going to do is map these measurements onto a gray scale that we can save as an image. We do this by determining the maximum measurement, determining the relative measurement compared to the maximum, and scaling it the an integer range from 0 to 255.
The following lines achieve this.
# #![allow(unused_variables)] #fn main() { let maximum = raw .iter() .fold(std::f64::MIN, |acc, v| acc.max(*v)); let data: Vec<u8> = raw .iter() .map(|s| s/maximum) .map(|s| 255.0 * s) .map(|s| s.floor() as u8) .collect(); #}
It uses a method fold
with the following signature
# #![allow(unused_variables)] #fn main() { fn fold<B, F>(self, init: B, f: F) -> B where F: FnMut(B, Self::Item) -> B #}
It takes something that implements the Iterator
trait, a initial value called
init
and repeatedly calls f
. The function f
accepts two arguments. At
first it accepts the initial init
value and the first element the Iterator
produces. After that it accepts the previous call to f
return value with the
next value of the iterator. A fold returns the final return value of the
function f
.
Writing data
Now that we have the gray-scale data, it is time to write it as an image. For
this we will use the png
crate. Before we can use it add
# #![allow(unused_variables)] #fn main() { extern crate png; #}
To the top of the source file. We also need to include an import statement that makes our live working with PNGs easier.
# #![allow(unused_variables)] #fn main() { use png::HasParameters; #}
We are going to save the PNG into our working directory. Because the png
crate
expects a BufWriter
we will have to include the following modules.
# #![allow(unused_variables)] #fn main() { use std::env; use std::io::{BufWriter, BufReader}; #}
Notice that we already had imported the BufReader
module. With these imports
we can create a BufWriter
in one fell swoop.
# #![allow(unused_variables)] #fn main() { let mut path = env::current_dir().unwrap(); path.push(format!("trappist-1.{}.png", 0)); let file = File::create(path).unwrap(); let ref mut w = BufWriter::new(file); #}
Now we can hand over this BufWriter
to a PNG Encoder
, configure it to our
liking, create a PNG Writer
and write the data.
# #![allow(unused_variables)] #fn main() { let mut encoder = png::Encoder::new(w, 11, 11); encoder.set(png::ColorType::Grayscale).set(png::BitDepth::Eight); let mut writer = encoder.write_header().unwrap(); writer.write_image_data(data.as_slice()).unwrap(); #}
Our Image
It is finally time to make our own impression of Trappist-1. Use cargo
to
build and run your code.
> cargo build
> cargo run --bin image
Which creates
Appreciate the Image
At first glance the image can be a little underwhelming. But it is precisely this image that blew my mind! Being accustomed to the marvelous artist impression, when I learned about the actual data was 11x11 pixels I was hooked. How could anyone extract so much information from so little data?
I had to know and I hope you want to know too!
Further Considerations
- Make a bigger image with larger "pixels".
- Make an entire series of images, one for each row.
- Make a GIF or movie of the images.