Skip to content

Commit cfe0714

Browse files
committed
Ability to export camera data as .usd or .jsx
1 parent b0e1857 commit cfe0714

File tree

5 files changed

+177
-38
lines changed

5 files changed

+177
-38
lines changed

src/controller.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,9 +1917,9 @@ impl Controller {
19171917
}
19181918
fn export_gyro_data(&self, url: QUrl, fields: QJsonObject) {
19191919
let url = util::qurl_to_encoded(url);
1920-
let is_csv = filesystem::get_filename(&url).ends_with(".csv");
1920+
let filename = filesystem::get_filename(&url).to_ascii_lowercase();
19211921

1922-
let contents = gyroflow_core::gyro_export::export_gyro_data(is_csv, fields.to_json().to_str().unwrap(), &self.stabilizer);
1922+
let contents = gyroflow_core::gyro_export::export_gyro_data(&filename, fields.to_json().to_str().unwrap(), &self.stabilizer);
19231923
if let Err(e) = filesystem::write(&url, contents.as_bytes()) {
19241924
self.error(QString::from("An error occured: %1"), QString::from(e.to_string()), QString::default());
19251925
}
@@ -2270,7 +2270,7 @@ impl Filesystem {
22702270
let folder = filesystem::get_folder(&current_url);
22712271
let filename = filesystem::get_filename(&current_url);
22722272

2273-
let extensions = [ "mp4", "mov", "mxf", "mkv", "webm", "insv", "gyroflow", "braw", "r3d" ];
2273+
let extensions = [ "mp4", "mov", "mxf", "mkv", "webm", "insv", "braw", "r3d" ];
22742274

22752275
let list: Vec<(String, String)> = filesystem::list_folder(&folder)
22762276
.into_iter()

src/core/gyro_export.rs

Lines changed: 168 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,31 @@ pub fn export_full_metadata(gyro_url: &str, _stab: &Arc<crate::StabilizationMana
3535
Ok(serde_json::to_string_pretty(&output)?)
3636
}
3737

38-
pub fn export_gyro_data(is_csv: bool, fields_json: &str, stab: &Arc<crate::StabilizationManager>) -> String {
38+
pub fn export_gyro_data(filename: &str, fields_json: &str, stab: &Arc<crate::StabilizationManager>) -> String {
3939
use std::fmt::Write;
4040
use crate::util::MapClosest;
4141
const RAD2DEG: f64 = 180.0 / std::f64::consts::PI;
4242
enum TimestampType {
4343
Milliseconds(f64),
4444
Microseconds(i64),
4545
}
46+
#[derive(PartialEq, Eq)]
47+
enum Format {
48+
Csv, Json, Usd, Jsx
49+
}
4650
fn get(f: Option<[f64; 3]>, i: usize) -> f64 { f.map(|x| x[i]).unwrap_or_default() }
4751

52+
let format = match filename.split('.').last().unwrap_or_default() {
53+
"csv" => Format::Csv,
54+
"json" => Format::Json,
55+
"usd" => Format::Usd,
56+
"jsx" => Format::Jsx,
57+
_ => Format::Csv,
58+
};
59+
4860
let fields: serde_json::Value = serde_json::from_str(fields_json).unwrap();
4961

50-
let all_samples = fields.get("export_all_samples").and_then(|x| x.as_bool()).unwrap_or_default();
62+
let all_samples = fields.get("export_all_samples").and_then(|x| x.as_bool()).unwrap_or_default() && format != Format::Usd && format != Format::Jsx;
5163

5264
let original = fields.get("original") .and_then(|x| x.as_object()).unwrap();
5365
let stabilized = fields.get("stabilized").and_then(|x| x.as_object()).unwrap();
@@ -65,19 +77,20 @@ pub fn export_gyro_data(is_csv: bool, fields_json: &str, stab: &Arc<crate::Stabi
6577
let fovs = zooming.get("fovs") .and_then(|x| x.as_bool()).unwrap_or_default();
6678
let minimal_fovs = zooming.get("minimal_fovs").and_then(|x| x.as_bool()).unwrap_or_default();
6779

68-
let mut csv = String::from("frame,timestamp_ms");
80+
let mut output = String::new();
6981
let mut json = Vec::<std::collections::HashMap<&str, serde_json::Value>>::new();
70-
if is_csv {
71-
if oaccl { let _ = write!(csv, ",org_acc_x,org_acc_y,org_acc_z"); }
72-
if oeulr { let _ = write!(csv, ",org_pitch,org_yaw,org_roll"); }
73-
if ogyro { let _ = write!(csv, ",org_gyro_x,org_gyro_y,org_gyro_z"); }
74-
if oquat { let _ = write!(csv, ",org_quat_w,org_quat_x,org_quat_y,org_quat_z"); }
75-
if seulr { let _ = write!(csv, ",stab_pitch,stab_yaw,stab_roll"); }
76-
if squat { let _ = write!(csv, ",stab_quat_w,stab_quat_x,stab_quat_y,stab_quat_z"); }
77-
if focal_length { let _ = write!(csv, ",focal_length"); }
78-
if fovs { let _ = write!(csv, ",fov_scale"); }
79-
if minimal_fovs { let _ = write!(csv, ",minimal_fov_scale"); }
80-
let _ = write!(csv, "\n");
82+
if format == Format::Csv {
83+
let _ = write!(output, "frame,timestamp_ms");
84+
if oaccl { let _ = write!(output, ",org_acc_x,org_acc_y,org_acc_z"); }
85+
if oeulr { let _ = write!(output, ",org_pitch,org_yaw,org_roll"); }
86+
if ogyro { let _ = write!(output, ",org_gyro_x,org_gyro_y,org_gyro_z"); }
87+
if oquat { let _ = write!(output, ",org_quat_w,org_quat_x,org_quat_y,org_quat_z"); }
88+
if seulr { let _ = write!(output, ",stab_pitch,stab_yaw,stab_roll"); }
89+
if squat { let _ = write!(output, ",stab_quat_w,stab_quat_x,stab_quat_y,stab_quat_z"); }
90+
if focal_length { let _ = write!(output, ",focal_length"); }
91+
if fovs { let _ = write!(output, ",fov_scale"); }
92+
if minimal_fovs { let _ = write!(output, ",minimal_fov_scale"); }
93+
let _ = write!(output, "\n");
8194
}
8295

8396
let params = stab.params.read();
@@ -110,6 +123,44 @@ pub fn export_gyro_data(is_csv: bool, fields_json: &str, stab: &Arc<crate::Stabi
110123
(None, frame, TimestampType::Milliseconds(middle_timestamp), timestamp_ms)
111124
}).collect()
112125
};
126+
let num_frames = params.frame_count;
127+
let fps = params.get_scaled_fps();
128+
let frame_times = serde_json::to_string(&(0..num_frames).map(|i| i as f64 / fps).collect::<Vec<_>>()).unwrap();
129+
130+
if format == Format::Usd {
131+
output.push_str(&format!(r#"#usda 1.0
132+
(
133+
defaultPrim = "root"
134+
doc = "Gyroflow"
135+
endTimeCode = {num_frames}
136+
metersPerUnit = 1
137+
startTimeCode = 1
138+
timeCodesPerSecond = {fps:.6}
139+
upAxis = "Z"
140+
)
141+
def Xform "root" (
142+
customData = {{
143+
dictionary Blender = {{
144+
bool generated = 1
145+
}}
146+
}}
147+
)
148+
{{
149+
def Xform "GyroflowCamera"
150+
{{
151+
matrix4d xformOp:transform.timeSamples = {{
152+
"#
153+
));
154+
}
155+
156+
if format == Format::Jsx {
157+
let duration_s = params.duration_ms / 1000.0;
158+
output.push_str(&format!(r#"var comp = app.project.activeItem;
159+
var GyroflowCamera = comp.layers.addCamera("GyroflowCamera",[0,0]);
160+
GyroflowCamera.inPoint = 0.0;
161+
GyroflowCamera.outPoint = {duration_s};
162+
GyroflowCamera.property("orientation").setValuesAtTimes({frame_times}, ["#));
163+
}
113164

114165
let raw_imu = gyro.raw_imu(&file_metadata);
115166

@@ -123,12 +174,38 @@ pub fn export_gyro_data(is_csv: bool, fields_json: &str, stab: &Arc<crate::Stabi
123174
let val_ogyro = [get(raw_imu.gyro, 0), get(raw_imu.gyro, 1), get(raw_imu.gyro, 2)];
124175
let val_oquat = [quatv[3], quatv[0], quatv[1], quatv[2]];
125176

177+
if format == Format::Jsx && !(seulr && !oeulr) {
178+
output.push_str(&format!("[{},{},{}],\n", val_oeulr[0], -val_oeulr[2], val_oeulr[1]));
179+
}
180+
if format == Format::Usd && !(seulr && !oeulr) {
181+
let matrix = nalgebra::Matrix4::from(quat);
182+
output.push_str(&format!(" {}: ( ({},{},{}, 0), ({}, {}, {}, 0), ({}, {}, {}, 0), (7.0, -7.0, 1.5, 1) ),\n",
183+
frame + 1,
184+
matrix[(0, 0)], matrix[(1, 0)], matrix[(2, 0)],
185+
matrix[(0, 1)], matrix[(1, 1)], matrix[(2, 1)],
186+
matrix[(0, 2)], matrix[(1, 2)], matrix[(2, 2)]
187+
));
188+
}
189+
126190
let quat = match ts { TimestampType::Microseconds(ts) => *gyro.smoothed_quaternions.get(&ts).unwrap(), TimestampType::Milliseconds(ts) => gyro.smoothed_quat_at_timestamp(ts) };
127191
let quate = quat.euler_angles();
128192
let quatv = quat.as_vector();
129193
let val_seulr = [quate.0 * RAD2DEG, quate.1 * RAD2DEG, quate.2 * RAD2DEG];
130194
let val_squat = [quatv[3], quatv[0], quatv[1], quatv[2]];
131195

196+
if format == Format::Jsx && (seulr && !oeulr) {
197+
output.push_str(&format!("[{},{},{}],\n", val_seulr[0], -val_seulr[2], val_seulr[1]));
198+
}
199+
if format == Format::Usd && (seulr && !oeulr) {
200+
let matrix = nalgebra::Matrix4::from(quat);
201+
output.push_str(&format!(" {}: ( ({},{},{}, 0), ({}, {}, {}, 0), ({}, {}, {}, 0), (7.0, -7.0, 1.5, 1) ),\n",
202+
frame + 1,
203+
matrix[(0, 0)], matrix[(1, 0)], matrix[(2, 0)],
204+
matrix[(0, 1)], matrix[(1, 1)], matrix[(2, 1)],
205+
matrix[(0, 2)], matrix[(1, 2)], matrix[(2, 2)]
206+
));
207+
}
208+
132209
if let Some(val) = file_metadata.lens_params.get_closest(&((timestamp_ms * 1000.0).round() as i64), 100000) { // closest within 100ms
133210
if let Some(fl) = val.focal_length {
134211
focal_length_value = Some(fl as f64);
@@ -138,19 +215,19 @@ pub fn export_gyro_data(is_csv: bool, fields_json: &str, stab: &Arc<crate::Stabi
138215
let val_fov = *params.fovs.get(frame).unwrap_or(&0.0);
139216
let val_minimal_fov = *params.minimal_fovs.get(frame).unwrap_or(&0.0);
140217

141-
if is_csv {
142-
let _ = write!(csv, "{frame},{timestamp_ms:.3}");
143-
if oaccl { let _ = write!(csv, ",{:.6},{:.6},{:.6}", val_oaccl[0], val_oaccl[1], val_oaccl[2]); }
144-
if oeulr { let _ = write!(csv, ",{:.3},{:.3},{:.3}", val_oeulr[0], val_oeulr[1], val_oeulr[2]); }
145-
if ogyro { let _ = write!(csv, ",{:.6},{:.6},{:.6}", val_ogyro[0], val_ogyro[1], val_ogyro[2]); }
146-
if oquat { let _ = write!(csv, ",{:.6},{:.6},{:.6},{:.6}", val_oquat[0], val_oquat[1], val_oquat[2], val_oquat[3]); }
147-
if seulr { let _ = write!(csv, ",{:.3},{:.3},{:.3}", val_seulr[0], val_seulr[1], val_seulr[2]); }
148-
if squat { let _ = write!(csv, ",{:.6},{:.6},{:.6},{:.6}", val_squat[0], val_squat[1], val_squat[2], val_squat[3]); }
149-
if focal_length { let _ = write!(csv, ",{val_fl:.3}"); }
150-
if fovs { let _ = write!(csv, ",{val_fov:.3}"); }
151-
if minimal_fovs { let _ = write!(csv, ",{val_minimal_fov:.3}"); }
152-
let _ = write!(csv, "\n");
153-
} else {
218+
if format == Format::Csv {
219+
let _ = write!(output, "{frame},{timestamp_ms:.3}");
220+
if oaccl { let _ = write!(output, ",{:.6},{:.6},{:.6}", val_oaccl[0], val_oaccl[1], val_oaccl[2]); }
221+
if oeulr { let _ = write!(output, ",{:.3},{:.3},{:.3}", val_oeulr[0], val_oeulr[1], val_oeulr[2]); }
222+
if ogyro { let _ = write!(output, ",{:.6},{:.6},{:.6}", val_ogyro[0], val_ogyro[1], val_ogyro[2]); }
223+
if oquat { let _ = write!(output, ",{:.6},{:.6},{:.6},{:.6}", val_oquat[0], val_oquat[1], val_oquat[2], val_oquat[3]); }
224+
if seulr { let _ = write!(output, ",{:.3},{:.3},{:.3}", val_seulr[0], val_seulr[1], val_seulr[2]); }
225+
if squat { let _ = write!(output, ",{:.6},{:.6},{:.6},{:.6}", val_squat[0], val_squat[1], val_squat[2], val_squat[3]); }
226+
if focal_length { let _ = write!(output, ",{val_fl:.3}"); }
227+
if fovs { let _ = write!(output, ",{val_fov:.3}"); }
228+
if minimal_fovs { let _ = write!(output, ",{val_minimal_fov:.3}"); }
229+
let _ = write!(output, "\n");
230+
} else if format == Format::Json {
154231
let mut obj = std::collections::HashMap::new();
155232
obj.insert("frame", frame.into());
156233
obj.insert("timestamp_ms", timestamp_ms.into());
@@ -166,9 +243,71 @@ pub fn export_gyro_data(is_csv: bool, fields_json: &str, stab: &Arc<crate::Stabi
166243
json.push(obj);
167244
}
168245
}
246+
let mut comp_params = crate::stabilization::ComputeParams::from_manager(stab);
247+
248+
if format == Format::Jsx {
249+
output = output.trim_end_matches(",\n").to_string();
250+
output.push_str("]);\n");
251+
252+
let (camera_matrix, _, _, _, _, _) = crate::stabilization::FrameTransform::get_lens_data_at_timestamp(&comp_params, 0.0);
253+
output.push_str(&format!("GyroflowCamera.property(\"zoom\").setValue({});\n", camera_matrix[(0, 0)]));
254+
if comp_params.gyro.read().file_metadata.read().lens_params.len() > 1 {
255+
output.push_str(&format!("GyroflowCamera.property(\"zoom\").setValuesAtTimes({frame_times}, ["));
256+
for f in 0..num_frames as i32 {
257+
let timestamp = crate::timestamp_at_frame(f, fps);
258+
let (camera_matrix, _, _, _, _, _) = crate::stabilization::FrameTransform::get_lens_data_at_timestamp(&comp_params, timestamp);
259+
output.push_str(&format!("{},\n", camera_matrix[(0, 0)]));
260+
}
261+
output = output.trim_end_matches(",\n").to_string();
262+
output.push_str("]);\n");
263+
}
264+
}
265+
266+
if format == Format::Csv || format == Format::Jsx {
267+
output
268+
} else if format == Format::Usd {
269+
let aspect = params.video_size.0 as f64 / params.video_size.1 as f64;
270+
let width_mm = 35.0;
271+
let height_mm = width_mm / aspect;
272+
273+
comp_params.calculate_camera_fovs();
274+
275+
output.push_str("\n}");
276+
let fov = comp_params.camera_diagonal_fovs.first().unwrap();
277+
let diagonal_mm = (width_mm.powi(2) + height_mm.powi(2)).sqrt();
278+
let focal_length_mm = diagonal_mm / (2.0 * (fov.to_radians() / 2.0).tan()) / 100.0;
279+
280+
let focal_lengths = {
281+
let mut fls = String::new();
282+
if comp_params.camera_diagonal_fovs.len() > 1 {
283+
fls.push_str("float focalLength.timeSamples = {\n");
284+
for (frame, fov) in comp_params.camera_diagonal_fovs.iter().enumerate() {
285+
let focal_length_mm = diagonal_mm / (2.0 * (fov.to_radians() / 2.0).tan()) / 100.0;
286+
fls.push_str(&format!(" {}: {focal_length_mm},\n", frame + 1));
287+
}
288+
fls.push_str("}");
289+
}
290+
fls
291+
};
292+
293+
output.push_str(&format!(r#"
294+
uniform token[] xformOpOrder = ["xformOp:transform"]
295+
296+
def Camera "GyroflowCamera"
297+
{{
298+
float2 clippingRange = (0.1, 100)
299+
float focalLength = {focal_length_mm}
300+
{focal_lengths}
301+
float horizontalAperture = {}
302+
float horizontalApertureOffset = 0
303+
token projection = "perspective"
304+
float verticalAperture = {}
305+
float verticalApertureOffset = 0
306+
}}
307+
}}
308+
}}"#, width_mm / 100.0, height_mm / 100.0));
169309

170-
if is_csv {
171-
csv
310+
output
172311
} else {
173312
serde_json::to_string(&json).unwrap()
174313
}

src/core/gyro_source/sony.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ pub fn stab_collect(is: &mut ISTemp, tag_map: &GroupedTagMap, _info: &telemetry_
300300
Some(())
301301
}
302302

303-
pub fn stab_calc_splines(md: &FileMetadata, is_temp: &ISTemp, sample_rate: f64, _frame_rate: f64, size: (usize, usize)) -> Option<Vec<CameraStabData>> {
303+
pub fn stab_calc_splines(md: &FileMetadata, is_temp: &ISTemp, sample_rate: f64, _frame_rate: f64, _size: (usize, usize)) -> Option<Vec<CameraStabData>> {
304304
let num_frames = is_temp.per_frame_exposure.len();
305305

306306
let readout_time = md.frame_readout_time.unwrap_or_default() / is_temp.original_sample_rate * sample_rate * 1000.0;

src/core/stabilization/frame_transform.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ impl FrameTransform {
211211
} else {
212212
(1.0, 1.0)
213213
};
214-
let height_scale = params.video_height as f64 / params.height.max(1) as f64;
214+
// let height_scale = params.video_height as f64 / params.height.max(1) as f64;
215215

216216
let image_rotation = Matrix3::new_rotation(video_rotation * (std::f64::consts::PI / 180.0));
217217

src/ui/menu/MotionData.qml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ MenuItem {
575575
}
576576
}
577577
Action {
578-
text: qsTr("Export to CSV or JSON");
578+
text: qsTr("Export camera data (CSV/JSON/USD/AE)");
579579
onTriggered: {
580580
const el = Qt.createComponent("../SettingsSelector.qml").createObject(window, {
581581
desc: [
@@ -612,7 +612,7 @@ MenuItem {
612612
el.opened = true;
613613
el.onApply.connect((obj) => {
614614
settings.setValue("CSVExportSelection", JSON.stringify(obj));
615-
exportFileDialog.nameFilters = ["*.csv", "*.json"];
615+
exportFileDialog.nameFilters = ["CSV (*.csv)", "JSON (*.json)", "Universal Scene Description (*.usd)", "After Effects Script (*.jsx)"];
616616
exportFileDialog.exportData = obj;
617617
exportFileDialog.open2();
618618
});
@@ -621,15 +621,15 @@ MenuItem {
621621
Action {
622622
text: qsTr("Export full metadata");
623623
onTriggered: {
624-
exportFileDialog.nameFilters = ["*.json"];
624+
exportFileDialog.nameFilters = ["JSON (*.json)"];
625625
exportFileDialog.exportData = "full";
626626
exportFileDialog.open2();
627627
}
628628
}
629629
Action {
630630
text: qsTr("Export parsed metadata");
631631
onTriggered: {
632-
exportFileDialog.nameFilters = ["*.json"];
632+
exportFileDialog.nameFilters = ["JSON (*.json)"];
633633
exportFileDialog.exportData = "parsed";
634634
exportFileDialog.open2();
635635
}

0 commit comments

Comments
 (0)