Skip to content

Commit a265e04

Browse files
committed
Coco format generation completed, accurate bounding boxes
1 parent d54654e commit a265e04

File tree

6 files changed

+103
-55
lines changed

6 files changed

+103
-55
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ imageproc = "0.25.0"
1818
serde = {version = "1.0.210", features = ["derive"]}
1919
serde_json = "1.0.68"
2020
strum = {version = "0.26.3", features = ["derive"]}
21+
chrono = "0.4.38"
2122

2223
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
2324
[profile.release]

backgrounds/archive/output.png

115 KB
Loading

src/backgrounds/mod.rs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use std::fs;
2+
use std::fs::DirEntry;
23
use std::path::Path;
34
use std::sync::Mutex;
45
use std::time::Instant;
56
use anyhow::{anyhow, Result};
7+
use chrono::{DateTime, Local};
68
use image::{DynamicImage, RgbaImage};
79
use log::{debug, trace, warn};
810
use rand::seq::SliceRandom;
@@ -11,7 +13,7 @@ use rayon::iter::ParallelBridge;
1113
use rayon::iter::ParallelIterator;
1214

1315
pub struct BackgroundLoader {
14-
pub backgrounds: Vec<RgbaImage>,
16+
pub backgrounds: Vec<BackgroundImage>,
1517
}
1618

1719
impl BackgroundLoader {
@@ -27,8 +29,8 @@ impl BackgroundLoader {
2729

2830
debug!("Loading backgrounds from: {:?}", dir);
2931

30-
// parallelize here because loading images can be slow
31-
fs::read_dir(dir)?.for_each(|entry| {
32+
// TODO: parallelize here because loading images can be slow
33+
fs::read_dir(dir)?.for_each(|entry | {
3234
let entry = entry.unwrap();
3335
let path = entry.path();
3436

@@ -39,32 +41,42 @@ impl BackgroundLoader {
3941
let path_name = path.display().to_string();
4042

4143
if let Ok(img) = image::open(path) {
42-
v.push(img);
44+
let datetime: DateTime<Local> = entry.metadata().unwrap().created().unwrap().into();
45+
46+
let back = BackgroundImage {
47+
image: img.to_rgba8(),
48+
filename: path_name.clone(),
49+
date_captured: datetime.to_string(),
50+
id: v.len() as u32,
51+
};
52+
53+
v.push(back);
4354
} else {
4455
warn!("Failed to load image: {}", path_name);
4556
}
4657

4758
debug!("Loaded image in {}ms: {}", start.elapsed().as_millis(), path_name);
4859
});
49-
50-
let mut r = vec![];
51-
52-
for img in v.iter() {
53-
r.push(img.to_rgba8());
54-
}
5560

5661
let x = Ok(Self { // don't ask why we need to make a variable here, it just works
57-
backgrounds: r
62+
backgrounds: v
5863
});
5964

6065
x
6166
}
6267

63-
pub fn random(&self) -> Option<&RgbaImage> {
68+
pub fn random(&self) -> Option<&BackgroundImage> {
6469
self.backgrounds.choose(&mut thread_rng())
6570
}
6671
}
6772

73+
pub struct BackgroundImage {
74+
pub image: RgbaImage,
75+
pub filename: String,
76+
pub date_captured: String,
77+
pub id: u32,
78+
}
79+
6880
#[test]
6981
#[ignore]
7082
fn test_bg_loader() {

src/generator/coco/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// https://docs.aws.amazon.com/rekognition/latest/customlabels-dg/md-coco-overview.html
22

3+
use std::fs::OpenOptions;
34
use std::path::{Path, PathBuf};
45
use serde::{Deserialize, Serialize};
56

@@ -36,8 +37,9 @@ impl CocoGenerator {
3637
}
3738

3839
pub fn save(&self) {
39-
let json = serde_json::to_string_pretty(&self.file).unwrap();
40-
std::fs::write(&self.file_path, json).unwrap();
40+
let file = OpenOptions::new().write(true).create(true).open(&self.file_path).unwrap();
41+
42+
serde_json::to_writer_pretty(file, &self.file).unwrap();
4143
}
4244

4345
/// Add a background image in, then return the image id
@@ -143,5 +145,5 @@ impl CocoCategory {
143145
}
144146

145147
pub trait CocoCategoryInfo {
146-
fn new(&self) -> Vec<CocoCategory>;
148+
fn categories() -> Vec<CocoCategory>;
147149
}

src/generator/mod.rs

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ use imageproc::drawing::draw_text_mut;
88
use log::{debug, trace};
99
use rand::{Rng, thread_rng};
1010
use simple_logger::SimpleLogger;
11+
use strum::Display;
12+
use thiserror::Error;
1113
use crate::backgrounds::BackgroundLoader;
12-
use crate::generator::coco::CocoGenerator;
13-
use crate::objects::ObjectManager;
14+
use crate::generator::coco::{CocoCategoryInfo, CocoGenerator};
15+
use crate::objects::{ObjectClass, ObjectManager};
1416

1517
pub mod coco;
1618

@@ -33,31 +35,48 @@ impl TargetGenerator {
3335
backgrounds_path: background_path.as_ref().to_path_buf(),
3436
object_manager,
3537
background_loader: BackgroundLoader::new(background_path)?,
36-
coco_generator: CocoGenerator::new(annotations_path)
38+
coco_generator: CocoGenerator::new(annotations_path, ObjectClass::categories())
3739
})
3840
}
3941

40-
pub fn generate_target(&self, altitude: f32, fov: f32, iteration: u32, pixels_per_meter: f32) -> Result<RgbaImage> {
42+
pub fn generate_target(&mut self, pixels_per_meter: f32) -> Result<RgbaImage> {
4143
trace!("Beginning to generate a target...");
4244

43-
let mut background = self.background_loader.random().unwrap().clone();
44-
let (w, h) = (background.width(), background.height());
45+
let background = self.background_loader.random().unwrap();
46+
let mut image = background.image.clone();
47+
let (w, h) = (image.width(), image.height());
4548
let set = self.object_manager.generate_set(1)?;
4649

50+
// add background image to coco here
51+
self.coco_generator.add_image(w, h, background.filename.clone(), background.date_captured.clone());
52+
4753
for obj in set {
4854
let clone = &obj.dynamic_image.clone();
4955
let (obj_w, obj_h) = (obj.dynamic_image.width(), obj.dynamic_image.height());
5056
let (x, y) = (thread_rng().gen_range(0..w - obj_w), thread_rng().gen_range(0..h - obj_h));
5157
trace!("Placing object at {}, {}", x, y);
5258

53-
let (obj_w, obj_h) = new_sizes(obj_w, obj_h, pixels_per_meter, obj.object_width_meters);
59+
let (obj_w, obj_h) = new_sizes(obj_w, obj_h, pixels_per_meter, obj.object_width_meters)?;
5460
debug!("Resizing object to {}x{}", obj_w, obj_h);
5561

5662
// overlay respects transparent pixels unlike copy_from
57-
image::imageops::overlay(&mut background, &clone.resize(obj_w, obj_h, FilterType::Gaussian), x as i64, y as i64);
63+
image::imageops::overlay(&mut image, &clone.resize(obj_w, obj_h, FilterType::Gaussian), x as i64, y as i64);
64+
65+
// TODO: remove, both are top left
66+
//imageproc::drawing::draw_filled_circle_mut(&mut image, (x as i32, y as i32), 4, Rgba([255, 0, 0, 255]));
67+
//imageproc::drawing::draw_filled_circle_mut(&mut image, (0i32, 0i32), 8, Rgba([255, 0, 255, 255]));
68+
imageproc::drawing::draw_hollow_rect_mut(&mut image, imageproc::rect::Rect::at(x as i32, y as i32).of_size(obj_w, obj_h), Rgba([0, 255, 0, 255]));
69+
70+
// add annotation to coco here
71+
self.coco_generator.add_annotation(0, obj.object_class as u32, 0, vec![], (obj_w * obj_h) as f64, coco::BoundingBox {
72+
x,
73+
y,
74+
width: obj_w,
75+
height: obj_h,
76+
});
5877
}
5978

60-
Ok(background)
79+
Ok(image)
6180
}
6281

6382
pub fn generate_targets<A: AsRef<Path>>(&self, amount: u32, range_to: RangeTo<u32>, path: A) -> Result<()> {
@@ -69,6 +88,10 @@ impl TargetGenerator {
6988

7089
Ok(())
7190
}
91+
92+
pub fn close(&self) {
93+
self.coco_generator.save();
94+
}
7295
}
7396

7497
const STANDARD_PPM: f32 = 35.0;
@@ -84,47 +107,39 @@ fn resize_ratio(object_real_size: f32, pixels_per_meter: f32) -> f32 {
84107
/// 1. Calculate the aspect ratio
85108
/// 2. Calculate the width of the object in pixels that we expect based on the real width and the Pixels Per Meter value
86109
/// 3. Calculate the height from this new width using the previously calculated aspect ratio
87-
fn new_sizes(object_width: u32, object_height: u32, pixels_per_meter: f32, real_width: f32) -> (u32, u32) {
110+
fn new_sizes(object_width: u32, object_height: u32, pixels_per_meter: f32, real_width: f32) -> Result<(u32, u32), GenerationError> {
88111
let (w, h) = (object_width as f32, object_height as f32);
89112
let aspect_ratio = w / h;
90113
let new_width = resize_ratio(real_width, pixels_per_meter) as u32;
91114
let new_height = (new_width as f32 / aspect_ratio) as u32;
92115

93-
(new_width, new_height)
94-
}
95-
96-
fn degrees_to_radians(degrees: f32) -> f32 {
97-
degrees * std::f32::consts::PI / 180.0
98-
}
99-
100-
// Calculate the fov in radians based on the image width, height and focal length
101-
fn calculate_fov(image_width: u32, image_height: u32, focal_length: f32) -> f32 {
102-
2.0 * (0.5 * image_width as f32 / focal_length).atan()
103-
}
104-
105-
/// Calculate the width of the ground in meters based on camera position and field of view
106-
fn calculate_ground_width(altitude: f32, fov: f32) -> f32 {
107-
2.0 * altitude * (fov.to_radians() / 2.0).tan()
108-
}
109-
110-
fn meters_per_pixel(image_width: u32, ground_width: f32) -> f32 {
111-
ground_width / image_width as f32
116+
if new_height == 0 || new_width == 0 {
117+
return Err(GenerationError::SizeError);
118+
}
119+
120+
Ok((new_width, new_height))
112121
}
113122

114-
/// This gives you the expected size of an object in pixels, use this to calculate a ratio between
115-
/// the expected and actual values. Then use that to scale up/down the object to the expected size
116-
fn expected_object_size_pixels(real_size: f32, meters_per_pixel: f32) -> f32 {
117-
real_size / meters_per_pixel
123+
#[derive(Debug, Error)]
124+
pub enum GenerationError {
125+
#[error("Serde decoding or encoding error")]
126+
SerdeError(#[from] serde_json::Error),
127+
#[error("IO error occurred while generating target")]
128+
IOError(#[from] std::io::Error),
129+
#[error("Calculated new sizes provided an invalid size")]
130+
SizeError
118131
}
119132

120133
#[test]
121134
#[ignore]
122135
pub fn test_generate_target() {
123136
SimpleLogger::new().init().unwrap();
124137

125-
let tg = TargetGenerator::new("output", "backgrounds", "objects", "output").unwrap();
126-
let b = tg.generate_target(22.8, degrees_to_radians(35.0), 1, STANDARD_PPM).unwrap();
138+
let mut tg = TargetGenerator::new("output", "backgrounds", "objects", "output/annotations.json").unwrap();
139+
let b = tg.generate_target(STANDARD_PPM).unwrap();
127140

128141
b.save("output_1.png".to_string()).unwrap();
129142
debug!("Saved generated target to output_1.png");
143+
144+
tg.close();
130145
}

src/objects/mod.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ impl ObjectManager {
9090

9191
#[derive(Debug, Clone)]
9292
pub struct Object {
93-
object_class: ObjectClass,
93+
pub(crate) object_class: ObjectClass,
9494
id: u16,
9595
pub(crate) dynamic_image: DynamicImage,
9696
pub(crate) object_width_meters: f32,
@@ -138,16 +138,23 @@ impl ObjectClass {
138138
ObjectClass::TIRE => "tire",
139139
}
140140
}
141+
142+
/// Gets the numerical id of the object class for COCO format category id.
143+
/// NOTE: object ids should be sequential and non-duplicative
144+
pub fn id(&self) -> u32 {
145+
match self {
146+
ObjectClass::BICYCLE => 0,
147+
ObjectClass::TIRE => 1,
148+
}
149+
}
141150
}
142151

143152
impl CocoCategoryInfo for ObjectClass {
144-
fn new(&self) -> Vec<CocoCategory> {
153+
fn categories() -> Vec<CocoCategory> {
145154
let mut v = vec![];
146-
let mut id = 0;
147155

148156
for object_class in ObjectClass::iter() {
149-
v.push(CocoCategory::new(id, object_class.prefix().to_string()));
150-
id += 1;
157+
v.push(CocoCategory::new(object_class.id(), object_class.prefix().to_string()));
151158
}
152159

153160
v
@@ -175,6 +182,7 @@ pub struct PlacedObject {
175182
bounding_box: BoundingBox
176183
}
177184

185+
// Used to generate the starting object mapping file
178186
#[ignore]
179187
#[test]
180188
fn generate_objects_json_file() {
@@ -199,4 +207,14 @@ fn generate_objects_json_file() {
199207
let json = serde_json::to_string_pretty(&object_details_file).unwrap();
200208

201209
std::fs::write("objects/objects.json", json).unwrap();
210+
}
211+
212+
#[test]
213+
fn ensure_sequential_no_duplicate_ids() {
214+
let mut id = 0;
215+
216+
for object_class in ObjectClass::iter() {
217+
assert_eq!(object_class.id(), id, "Object class id is not sequential for {:?}", object_class);
218+
id += 1;
219+
}
202220
}

0 commit comments

Comments
 (0)