@@ -6,6 +6,25 @@ pub struct YoloConverter {
66 darknet : bool ,
77}
88
9+ struct SplitPresence {
10+ train : bool ,
11+ val : bool ,
12+ test : bool ,
13+ }
14+
15+ impl SplitPresence {
16+ fn from_data ( data : & NDJSONData ) -> Self {
17+ Self {
18+ train : data. images . iter ( ) . any ( |img| img. split == "train" ) ,
19+ val : data
20+ . images
21+ . iter ( )
22+ . any ( |img| img. split == "valid" || img. split == "val" ) ,
23+ test : data. images . iter ( ) . any ( |img| img. split == "test" ) ,
24+ }
25+ }
26+ }
27+
928fn quote_yaml_scalar ( value : & str ) -> String {
1029 serde_json:: to_string ( value) . unwrap_or_else ( |_| "\" \" " . to_string ( ) )
1130}
@@ -40,15 +59,26 @@ impl YoloConverter {
4059 fn create_data_yaml ( & self , data : & NDJSONData ) -> String {
4160 let class_names = get_class_list ( data) ;
4261 let task = & data. metadata . task ;
62+ // Split presence reflects data.images after download filtering when images are requested.
63+ let splits = SplitPresence :: from_data ( data) ;
4364
4465 let mut yaml = String :: new ( ) ;
4566 yaml. push_str ( "# Generated by YOLO NDJSON Converter\n " ) ;
4667 yaml. push_str ( & format ! ( "# Version: {}\n " , env!( "CARGO_PKG_VERSION" ) ) ) ;
4768 yaml. push_str ( "# https://github.com/amanharshx/YOLO-Ndjson-Zip\n \n " ) ;
4869 yaml. push_str ( "path: .\n " ) ;
49- yaml. push_str ( "train: train/images\n " ) ;
50- yaml. push_str ( "val: valid/images\n " ) ;
51- yaml. push_str ( "test: test/images\n " ) ;
70+ if splits. train {
71+ yaml. push_str ( "train: train/images\n " ) ;
72+ }
73+ if splits. val {
74+ yaml. push_str ( "val: valid/images\n " ) ;
75+ }
76+ if splits. test {
77+ yaml. push_str ( "test: test/images\n " ) ;
78+ }
79+ if !splits. train || !splits. val {
80+ yaml. push_str ( "# WARNING: Ultralytics training requires both train and val splits.\n " ) ;
81+ }
5282 yaml. push_str ( & format ! ( "nc: {}\n " , class_names. len( ) ) ) ;
5383 yaml. push_str ( "names:\n " ) ;
5484
@@ -155,10 +185,12 @@ impl Converter for YoloConverter {
155185 ) ;
156186 } else {
157187 // Standard YOLO mode
158- files. insert (
159- "data.yaml" . to_string ( ) ,
160- self . create_data_yaml ( data) . into_bytes ( ) ,
161- ) ;
188+ if task != "classify" {
189+ files. insert (
190+ "data.yaml" . to_string ( ) ,
191+ self . create_data_yaml ( data) . into_bytes ( ) ,
192+ ) ;
193+ }
162194 let class_list = get_class_list ( data) ;
163195 files. insert (
164196 "classes.txt" . to_string ( ) ,
@@ -283,6 +315,92 @@ mod tests {
283315 }
284316 }
285317
318+ fn image_with_split ( split : & str ) -> ImageEntry {
319+ ImageEntry {
320+ r#type : "image" . to_string ( ) ,
321+ file : format ! ( "{}.jpg" , split) ,
322+ output_file : None ,
323+ url : String :: new ( ) ,
324+ width : 640 ,
325+ height : 480 ,
326+ split : split. to_string ( ) ,
327+ annotations : Some ( json ! ( {
328+ "bboxes" : [ [ 0 , 0.5 , 0.5 , 0.2 , 0.2 ] ]
329+ } ) ) ,
330+ }
331+ }
332+
333+ #[ test]
334+ fn data_yaml_emits_only_present_splits ( ) {
335+ let cases = [
336+ ( & [ "train" ] [ ..] , [ true , false , false ] , true ) ,
337+ ( & [ "train" , "val" ] [ ..] , [ true , true , false ] , false ) ,
338+ ( & [ "train" , "valid" , "test" ] [ ..] , [ true , true , true ] , false ) ,
339+ ( & [ "train" , "test" ] [ ..] , [ true , false , true ] , true ) ,
340+ ( & [ "val" , "test" ] [ ..] , [ false , true , true ] , true ) ,
341+ ] ;
342+
343+ for ( source_splits, expected, warns) in cases {
344+ let images = source_splits
345+ . iter ( )
346+ . map ( |split| image_with_split ( split) )
347+ . collect ( ) ;
348+ let data = make_data ( "detect" , HashMap :: new ( ) , None , images) ;
349+ let files = YoloConverter :: new ( ) . convert ( & data, & HashMap :: new ( ) ) ;
350+ let yaml = std:: str:: from_utf8 ( files. get ( "data.yaml" ) . unwrap ( ) ) . unwrap ( ) ;
351+
352+ let has_line = |line| yaml. lines ( ) . any ( |candidate| candidate == line) ;
353+ assert_eq ! (
354+ [
355+ has_line( "train: train/images" ) ,
356+ has_line( "val: valid/images" ) ,
357+ has_line( "test: test/images" ) ,
358+ ] ,
359+ expected,
360+ "unexpected splits in:\n {yaml}"
361+ ) ;
362+ assert_eq ! ( yaml. contains( "# WARNING:" ) , warns) ;
363+ }
364+ }
365+
366+ #[ test]
367+ fn data_yaml_omits_split_whose_images_all_failed ( ) {
368+ let mut data = make_data (
369+ "detect" ,
370+ HashMap :: new ( ) ,
371+ None ,
372+ vec ! [ image_with_split( "train" ) , image_with_split( "val" ) ] ,
373+ ) ;
374+ data. images . retain ( |img| img. split == "train" ) ;
375+
376+ let files = YoloConverter :: new ( ) . convert ( & data, & HashMap :: new ( ) ) ;
377+ let yaml = std:: str:: from_utf8 ( files. get ( "data.yaml" ) . unwrap ( ) ) . unwrap ( ) ;
378+
379+ assert ! ( yaml. contains( "train: train/images" ) ) ;
380+ assert ! ( !yaml. contains( "val: valid/images" ) ) ;
381+ assert ! ( !files. contains_key( "valid/labels/val.txt" ) ) ;
382+ }
383+
384+ #[ test]
385+ fn classification_conversion_omits_yaml_metadata ( ) {
386+ let data = make_data (
387+ "classify" ,
388+ HashMap :: from ( [ ( "0" . to_string ( ) , "animal" . to_string ( ) ) ] ) ,
389+ None ,
390+ vec ! [ ImageEntry {
391+ annotations: Some ( json!( { "classification" : [ 0 ] } ) ) ,
392+ ..image_with_split( "train" )
393+ } ] ,
394+ ) ;
395+ let downloaded_images =
396+ HashMap :: from ( [ ( image_download_key ( "train" , "train.jpg" ) , vec ! [ 1 , 2 , 3 ] ) ] ) ;
397+
398+ let files = YoloConverter :: new ( ) . convert ( & data, & downloaded_images) ;
399+
400+ assert ! ( files. contains_key( "train/animal/train.jpg" ) ) ;
401+ assert ! ( files. keys( ) . all( |path| !path. ends_with( ".yaml" ) ) ) ;
402+ }
403+
286404 #[ test]
287405 fn create_data_yaml_quotes_class_names ( ) {
288406 let mut class_names = HashMap :: new ( ) ;
0 commit comments