|
73 | 73 | """ |
74 | 74 |
|
75 | 75 | import uuid |
76 | | -from typing import Optional, cast, get_args |
| 76 | +from typing import Optional, Tuple, Union, cast, get_args |
77 | 77 |
|
78 | 78 | import typer |
79 | 79 | from typing_extensions import Annotated |
|
100 | 100 |
|
101 | 101 | logger = get_logger("CLI") |
102 | 102 |
|
| 103 | + |
| 104 | +def parse_im_size(value: Union[str, int]) -> Union[int, Tuple[int, int]]: |
| 105 | + """Parse image size from string or int. |
| 106 | +
|
| 107 | + Supports formats: |
| 108 | + - int: 640 (square image) |
| 109 | + - str: "640" (square image) |
| 110 | + - str: "640,480" or "640x480" (non-square image as height,width) |
| 111 | +
|
| 112 | + Args: |
| 113 | + value: Image size as int or string |
| 114 | +
|
| 115 | + Returns: |
| 116 | + int for square images, tuple (height, width) for non-square |
| 117 | + """ |
| 118 | + if isinstance(value, int): |
| 119 | + return value |
| 120 | + if isinstance(value, str): |
| 121 | + # Try comma or x separator |
| 122 | + if "," in value: |
| 123 | + parts = value.split(",") |
| 124 | + elif "x" in value or "X" in value: |
| 125 | + parts = value.replace("X", "x").split("x") |
| 126 | + else: |
| 127 | + # Single number - square image |
| 128 | + return int(value) |
| 129 | + |
| 130 | + if len(parts) == 2: |
| 131 | + return (int(parts[0].strip()), int(parts[1].strip())) |
| 132 | + else: |
| 133 | + raise ValueError(f"Invalid image size format: {value}. Use '640', '640,480', or '640x480'") |
| 134 | + return value |
| 135 | + |
| 136 | + |
103 | 137 | app = typer.Typer( |
104 | 138 | name="focoos", |
105 | 139 | help=__doc__, |
@@ -251,7 +285,9 @@ def train( |
251 | 285 | Optional[str], typer.Option(help="Datasets directory (default: ~/FocoosAI/datasets/)") |
252 | 286 | ] = None, |
253 | 287 | dataset_layout: Annotated[DatasetLayout, typer.Option(help="Dataset layout")] = DatasetLayout.ROBOFLOW_COCO, |
254 | | - im_size: Annotated[int, typer.Option(help="Image size")] = 640, |
| 288 | + im_size: Annotated[ |
| 289 | + str, typer.Option(help="Image size (int for square, or 'height,width' or 'heightxwidth' for non-square)") |
| 290 | + ] = "640", |
255 | 291 | output_dir: Annotated[Optional[str], typer.Option(help="Output directory")] = None, |
256 | 292 | ckpt_dir: Annotated[Optional[str], typer.Option(help="Checkpoint directory")] = None, |
257 | 293 | init_checkpoint: Annotated[Optional[str], typer.Option(help="Initial checkpoint path")] = None, |
@@ -316,7 +352,9 @@ def train( |
316 | 352 | generates a unique name using model name and UUID. |
317 | 353 | datasets_dir (Optional[str]): Custom directory for datasets. |
318 | 354 | dataset_layout (DatasetLayout): Layout format of the dataset. Defaults to ROBOFLOW_COCO. |
319 | | - im_size (int): Input image size for training. Defaults to 640. |
| 355 | + im_size (str): Input image size for training. Can be int (e.g., "640") for square images, |
| 356 | + or "height,width" or "heightxwidth" (e.g., "640,480" or "640x480") for non-square images. |
| 357 | + Defaults to "640". |
320 | 358 | output_dir (Optional[str]): Directory to save training outputs and logs. |
321 | 359 | ckpt_dir (Optional[str]): Directory to save model checkpoints. |
322 | 360 | init_checkpoint (Optional[str]): Path to initial checkpoint for transfer learning. |
@@ -435,11 +473,12 @@ def train( |
435 | 473 | validated_optimizer = cast(OptimizerType, optimizer.upper()) |
436 | 474 | assert optimizer in get_args(OptimizerType) |
437 | 475 |
|
| 476 | + parsed_im_size = parse_im_size(im_size) |
438 | 477 | train_command( |
439 | 478 | model_name=model, |
440 | 479 | dataset_name=dataset, |
441 | 480 | dataset_layout=dataset_layout, |
442 | | - im_size=im_size, |
| 481 | + im_size=parsed_im_size, |
443 | 482 | run_name=run_name or f"{model}-{uuid.uuid4()}", |
444 | 483 | output_dir=output_dir, |
445 | 484 | ckpt_dir=ckpt_dir, |
@@ -494,7 +533,9 @@ def val( |
494 | 533 | ] = None, |
495 | 534 | run_name: Annotated[Optional[str], typer.Option(help="Run name")] = None, |
496 | 535 | dataset_layout: Annotated[DatasetLayout, typer.Option(help="Dataset layout")] = DatasetLayout.ROBOFLOW_COCO, |
497 | | - im_size: Annotated[int, typer.Option(help="Image size")] = 640, |
| 536 | + im_size: Annotated[ |
| 537 | + str, typer.Option(help="Image size (int for square, or 'height,width' or 'heightxwidth' for non-square)") |
| 538 | + ] = "640", |
498 | 539 | output_dir: Annotated[Optional[str], typer.Option(help="Output directory")] = None, |
499 | 540 | ckpt_dir: Annotated[Optional[str], typer.Option(help="Checkpoint directory")] = None, |
500 | 541 | init_checkpoint: Annotated[Optional[str], typer.Option(help="Initial checkpoint")] = None, |
@@ -677,11 +718,12 @@ def val( |
677 | 718 | validated_optimizer = cast(OptimizerType, optimizer.upper()) |
678 | 719 | assert optimizer in get_args(OptimizerType) |
679 | 720 |
|
| 721 | + parsed_im_size = parse_im_size(im_size) |
680 | 722 | val_command( |
681 | 723 | model_name=model, |
682 | 724 | dataset_name=dataset, |
683 | 725 | dataset_layout=dataset_layout, |
684 | | - im_size=im_size, |
| 726 | + im_size=parsed_im_size, |
685 | 727 | run_name=run_name or f"{model}-{uuid.uuid4()}", |
686 | 728 | output_dir=output_dir, |
687 | 729 | ckpt_dir=ckpt_dir, |
@@ -881,7 +923,10 @@ def export( |
881 | 923 | output_dir: Annotated[Optional[str], typer.Option(help="Output directory")] = None, |
882 | 924 | device: Annotated[Optional[str], typer.Option(help="Device (cuda or cpu)")] = "cuda", |
883 | 925 | onnx_opset: Annotated[Optional[int], typer.Option(help="ONNX opset version")] = 17, |
884 | | - im_size: Annotated[Optional[int], typer.Option(help="Image size for export")] = 640, |
| 926 | + im_size: Annotated[ |
| 927 | + Optional[str], |
| 928 | + typer.Option(help="Image size for export (int for square, or 'height,width' or 'heightxwidth' for non-square)"), |
| 929 | + ] = "640", |
885 | 930 | overwrite: Annotated[Optional[bool], typer.Option(help="Overwrite existing files")] = False, |
886 | 931 | ): |
887 | 932 | """Export a trained model to various deployment formats. |
@@ -985,13 +1030,14 @@ def export( |
985 | 1030 | try: |
986 | 1031 | validated_device = cast(DeviceType, device) |
987 | 1032 | assert device in get_args(DeviceType) |
| 1033 | + parsed_im_size = parse_im_size(im_size) if im_size is not None else None |
988 | 1034 | export_command( |
989 | 1035 | model_name=model, |
990 | 1036 | format=format, |
991 | 1037 | output_dir=output_dir, |
992 | 1038 | device=validated_device, |
993 | 1039 | onnx_opset=onnx_opset, |
994 | | - im_size=im_size, |
| 1040 | + im_size=parsed_im_size, |
995 | 1041 | overwrite=overwrite, |
996 | 1042 | ) |
997 | 1043 | except Exception as e: |
|
0 commit comments