Skip to content

Commit 680f2d4

Browse files
authored
fix: 🐛 Prioritize model metadata for image size when not provided (#37)
Signed-off-by: Onuralp SEZER <onuralp@ultralytics.com>
1 parent b20473f commit 680f2d4

4 files changed

Lines changed: 90 additions & 23 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ cargo run --release -- predict --model <model.onnx> --source <source>
103103
| `--source` | `-s` | Input source (image, video, webcam index, or URL) | `Task dependent Ultralytics URL assets` |
104104
| `--conf` | | Confidence threshold | `0.25` |
105105
| `--iou` | | IoU threshold for NMS | `0.45` |
106-
| `--imgsz` | | Inference image size | `640` |
106+
| `--imgsz` | | Inference image size | `Model metadata` |
107107
| `--half` | | Use FP16 half-precision inference | `false` |
108108
| `--save` | | Save annotated images to runs/<task>/predict | `false` |
109109
| `--show` | | Display results in a window | `false` |

src/main.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ fn run_prediction(args: &[String]) {
7575
let mut source_path: Option<&String> = None;
7676
let mut conf_threshold = 0.25_f32;
7777
let mut iou_threshold = 0.45_f32;
78-
let mut imgsz: usize = 640;
78+
let mut imgsz: Option<usize> = None;
7979
let mut save = false;
8080
let mut half = false;
8181
let mut verbose = true;
@@ -144,8 +144,16 @@ fn run_prediction(args: &[String]) {
144144
}
145145
"--imgsz" => {
146146
if i + 1 < args.len() {
147-
imgsz = args[i + 1].parse().unwrap_or(640);
148-
i += 2;
147+
if let Ok(val) = args[i + 1].parse::<usize>() {
148+
imgsz = Some(val);
149+
i += 2;
150+
} else {
151+
eprintln!(
152+
"Error: --imgsz requires a positive integer, found '{}'",
153+
args[i + 1]
154+
);
155+
process::exit(1);
156+
}
149157
} else {
150158
eprintln!("Error: --imgsz requires a value");
151159
process::exit(1);
@@ -201,8 +209,10 @@ fn run_prediction(args: &[String]) {
201209
.with_iou(iou_threshold)
202210
.with_half(half);
203211

204-
// Apply imgsz (default 640)
205-
config = config.with_imgsz(imgsz, imgsz);
212+
// Apply imgsz if specified
213+
if let Some(sz) = imgsz {
214+
config = config.with_imgsz(sz, sz);
215+
}
206216

207217
let mut model = match YOLOModel::load_with_config(&model_path, config) {
208218
Ok(m) => m,

src/metadata.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pub struct ModelMetadata {
3535
/// Batch size the model was exported with.
3636
pub batch: usize,
3737
/// Input image size as (height, width).
38-
pub imgsz: (usize, usize),
38+
pub imgsz: Option<(usize, usize)>,
3939
/// Number of input channels (typically 3 for RGB).
4040
pub channels: usize,
4141
/// Whether the model uses FP16 (half precision).
@@ -171,7 +171,7 @@ impl ModelMetadata {
171171

172172
/// Parse the imgsz field which can be a YAML list.
173173
#[allow(clippy::unnecessary_wraps)]
174-
fn parse_imgsz(yaml_str: &str, imgsz_line: &str) -> Result<(usize, usize)> {
174+
fn parse_imgsz(yaml_str: &str, imgsz_line: &str) -> Result<Option<(usize, usize)>> {
175175
// Check if imgsz is on a single line like "imgsz: [640, 640]"
176176
if let Some(bracket_start) = imgsz_line.find('[')
177177
&& let Some(bracket_end) = imgsz_line.find(']')
@@ -181,7 +181,7 @@ impl ModelMetadata {
181181
.filter_map(|s| s.trim().parse().ok())
182182
.collect();
183183
if values.len() >= 2 {
184-
return Ok((values[0], values[1]));
184+
return Ok(Some((values[0], values[1])));
185185
}
186186
}
187187

@@ -210,10 +210,9 @@ impl ModelMetadata {
210210
}
211211

212212
if imgsz_values.len() >= 2 {
213-
Ok((imgsz_values[0], imgsz_values[1]))
213+
Ok(Some((imgsz_values[0], imgsz_values[1])))
214214
} else {
215-
// Default to 640x640
216-
Ok((640, 640))
215+
Ok(None)
217216
}
218217
}
219218

@@ -334,7 +333,7 @@ impl Default for ModelMetadata {
334333
task: Task::Detect,
335334
stride: 32,
336335
batch: 1,
337-
imgsz: (640, 640),
336+
imgsz: None,
338337
channels: 3,
339338
half: false,
340339
names: HashMap::new(),
@@ -374,7 +373,7 @@ channels: 3
374373
assert_eq!(metadata.task, Task::Detect);
375374
assert_eq!(metadata.stride, 32);
376375
assert_eq!(metadata.batch, 1);
377-
assert_eq!(metadata.imgsz, (640, 640));
376+
assert_eq!(metadata.imgsz, Some((640, 640)));
378377
assert_eq!(metadata.channels, 3);
379378
assert_eq!(metadata.num_classes(), 4);
380379
assert_eq!(metadata.class_name(0), Some("person"));
@@ -387,14 +386,14 @@ channels: 3
387386
fn test_parse_inline_imgsz() {
388387
let yaml = "task: detect\nimgsz: [640, 640]\nstride: 32";
389388
let metadata = ModelMetadata::from_yaml_str(yaml).unwrap();
390-
assert_eq!(metadata.imgsz, (640, 640));
389+
assert_eq!(metadata.imgsz, Some((640, 640)));
391390
}
392391

393392
#[test]
394393
fn test_default_metadata() {
395394
let metadata = ModelMetadata::default();
396395
assert_eq!(metadata.task, Task::Detect);
397396
assert_eq!(metadata.stride, 32);
398-
assert_eq!(metadata.imgsz, (640, 640));
397+
assert_eq!(metadata.imgsz, None);
399398
}
400399
}

src/model.rs

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,17 @@ impl YOLOModel {
208208
)
209209
});
210210

211+
// Check for dynamic input dimensions
212+
// Dimensions are typically [-1, 3, -1, -1] for dynamic batch/height/width
213+
// ort 2.0 returns Option<i64> (None means dynamic/unknown)
214+
let is_dynamic = input_info.is_some_and(|i| {
215+
if let ValueType::Tensor { shape, .. } = i.dtype() {
216+
shape.iter().any(|d| *d == -1 || *d == 0)
217+
} else {
218+
false
219+
}
220+
});
221+
211222
// Use FP16 input if model tensor type requires it
212223
let fp16_input = model_input_fp16;
213224

@@ -217,14 +228,53 @@ impl YOLOModel {
217228
.map(|o| o.name().to_string())
218229
.collect();
219230

220-
// Update config with model metadata if not overridden
221-
// Sync half flag with model's half metadata (for display purposes)
231+
// Resolve image size
232+
// Priority:
233+
// 1. User config
234+
// 2. Model metadata
235+
// 3. Dynamic default (1024 for OBB, 640 for others)
236+
// 4. Static input shape
237+
// 5. Hard default (640)
238+
let resolved_imgsz = if let Some(sz) = config.imgsz {
239+
sz
240+
} else if let Some(sz) = metadata.imgsz {
241+
sz
242+
} else if is_dynamic {
243+
// Dynamic input without metadata -> apply robust defaults
244+
match metadata.task {
245+
Task::Obb => (1024, 1024),
246+
_ => (640, 640),
247+
}
248+
} else {
249+
// Static input without metadata -> try to read from tensor shape
250+
// Typically [1, 3, H, W]
251+
input_info
252+
.and_then(|i| {
253+
if let ValueType::Tensor { shape, .. } = i.dtype() {
254+
if shape.len() == 4 && shape[2] > 0 && shape[3] > 0 {
255+
#[allow(clippy::cast_sign_loss)]
256+
Some((shape[2] as usize, shape[3] as usize))
257+
} else {
258+
None
259+
}
260+
} else {
261+
None
262+
}
263+
})
264+
.unwrap_or((640, 640))
265+
};
266+
267+
// Update config with resolved values
222268
let config = InferenceConfig {
223-
imgsz: config.imgsz.or(Some(metadata.imgsz)),
269+
imgsz: Some(resolved_imgsz),
224270
half: config.half || metadata.half, // Use half if user requested OR model was exported with half
225271
..config
226272
};
227273

274+
// Ensure metadata reflects the resolved size
275+
let mut metadata = metadata;
276+
metadata.imgsz = Some(resolved_imgsz);
277+
228278
let mut model = Self {
229279
session,
230280
metadata,
@@ -253,7 +303,11 @@ impl YOLOModel {
253303
return Ok(());
254304
}
255305

256-
let target_size = self.config.imgsz.unwrap_or(self.metadata.imgsz);
306+
let target_size = self
307+
.config
308+
.imgsz
309+
.or(self.metadata.imgsz)
310+
.unwrap_or((640, 640));
257311

258312
// Sanity check to prevent huge allocations from invalid imgsz
259313
if target_size.0 > Self::MAX_IMGSZ || target_size.1 > Self::MAX_IMGSZ {
@@ -379,7 +433,11 @@ impl YOLOModel {
379433
/// Vector of Results.
380434
pub fn predict_image(&mut self, image: &DynamicImage, path: String) -> Result<Vec<Results>> {
381435
// Get target size from config or metadata
382-
let target_size = self.config.imgsz.unwrap_or(self.metadata.imgsz);
436+
let target_size = self
437+
.config
438+
.imgsz
439+
.or(self.metadata.imgsz)
440+
.unwrap_or((640, 640));
383441

384442
// Preprocess - generate FP16 tensor if model expects FP16 input
385443
let start_preprocess = Instant::now();
@@ -595,8 +653,8 @@ impl YOLOModel {
595653

596654
/// Get the model's input size.
597655
#[must_use]
598-
pub const fn imgsz(&self) -> (usize, usize) {
599-
self.metadata.imgsz
656+
pub fn imgsz(&self) -> (usize, usize) {
657+
self.metadata.imgsz.unwrap_or((640, 640))
600658
}
601659

602660
/// Get the model's stride.

0 commit comments

Comments
 (0)