Repository: ultralytics/ultralytics

Issue:
Change onnx2tf cli command call to python sdk
### Search before asking

- [X] I have searched the YOLOv8 [issues](https://github.com/ultralytics/ultralytics/issues) and found no similar bug report.


### YOLOv8 Component

Export

### Bug

During the Yolov8 export to tflite, the `onnx2tf` cli command is called using this command:
``` python
cmd = f'onnx2tf -i "{f_onnx}" -o "{f}" -nuo {verbosity} {int8}'.strip()
subprocess.run(cmd, shell=True)
```

This can cause issues with things like IDE debuggers (I'm using VSCode), because the python environment that is used when the script is run does not apply to the command line, so that line fails.

There is an sdk alternative, like so

``` python
import onnx2tf

onnx2tf.convert(
    input_onnx_file_path=f_onnx,
    output_folder_path=f,
    not_use_onnxsim=True, # -nuo
    verbosity="info", # or "error"
    output_integer_quantized_tflite=True, # int8
)
```

This would simplify the script, not having to call it from the shell.
I am willing to work on a PR if that would help.

### Environment

On an M1 macbook pro, I tried to run the YOLOv8 exporter to tflite in the VSCode debugger tool, and it failed saying it couldn't find the command "onnx2tf", because the debugger environment doesn't extend to the shell

### Minimal Reproducible Example

Run the following from VSCode using the python debugger

``` python
from ultralytics import YOLO

model = YOLO("yolov8n.pt")
model.export(format="tflite")
```

It should show this in the terminal: `/bin/sh: onnx2tf: command not found`
And it will fail later in the export when it tries to load the saved_model, which was never made

`OSError: SavedModel file does not exist at: yolov8n_saved_model/{saved_model.pbtxt|saved_model.pb}`

### Additional

_No response_

### Are you willing to submit a PR?

- [X] Yes I'd like to help by submitting a PR!


Discussion:
@ALMerrill hey thanks for the suggestion! This seems like a great idea. Can you please submit a PR for this change to the Exporter class? The relevant file to make the changes to is:
https://github.com/ultralytics/ultralytics/blob/main/ultralytics/engine/exporter.py
Definitely, I should have time before too long to get into this.
@ALMerrill that sounds great! We appreciate your willingness to contribute. If you have any questions or need assistance as you work on it, don't hesitate to reach out. Happy coding! 😊👍

Resolving patch:
diff --git a/ultralytics/engine/exporter.py b/ultralytics/engine/exporter.py
--- a/ultralytics/engine/exporter.py
+++ b/ultralytics/engine/exporter.py
@@ -41,6 +41,7 @@
                          yolov8n.tflite             # TensorFlow Lite
                          yolov8n_edgetpu.tflite     # TensorFlow Edge TPU
                          yolov8n_paddle_model       # PaddlePaddle
+                         yolov8n_ncnn_model         # NCNN
 
 TensorFlow.js:
     $ cd .. && git clone https://github.com/zldrobit/tfjs-yolov5-example.git && cd tfjs-yolov5-example
@@ -743,10 +744,10 @@ def export_saved_model(self, prefix=colorstr("TensorFlow SavedModel:")):
             verbose=True,
             msg="https://github.com/ultralytics/ultralytics/issues/5161",
         )
+        import onnx2tf
+
         f = Path(str(self.file).replace(self.file.suffix, "_saved_model"))
         if f.is_dir():
-            import shutil
-
             shutil.rmtree(f)  # delete output folder
 
         # Pre-download calibration file to fix https://github.com/PINTO0309/onnx2tf/issues/545
@@ -760,8 +761,9 @@ def export_saved_model(self, prefix=colorstr("TensorFlow SavedModel:")):
 
         # Export to TF
         tmp_file = f / "tmp_tflite_int8_calibration_images.npy"  # int8 calibration images file
+        np_data = None
         if self.args.int8:
-            verbosity = "--verbosity info"
+            verbosity = "info"
             if self.args.data:
                 # Generate calibration data for integer quantization
                 LOGGER.info(f"{prefix} collecting INT8 calibration images from 'data={self.args.data}'")
@@ -778,16 +780,20 @@ def export_saved_model(self, prefix=colorstr("TensorFlow SavedModel:")):
                 # mean = images.view(-1, 3).mean(0)  # imagenet mean [123.675, 116.28, 103.53]
                 # std = images.view(-1, 3).std(0)  # imagenet std [58.395, 57.12, 57.375]
                 np.save(str(tmp_file), images.numpy())  # BHWC
-                int8 = f'-oiqt -qt per-tensor -cind images "{tmp_file}" "[[[[0, 0, 0]]]]" "[[[[255, 255, 255]]]]"'
-            else:
-                int8 = "-oiqt -qt per-tensor"
+                np_data = [["images", tmp_file, [[[[0, 0, 0]]]], [[[[255, 255, 255]]]]]]
         else:
-            verbosity = "--non_verbose"
-            int8 = ""
-
-        cmd = f'onnx2tf -i "{f_onnx}" -o "{f}" -nuo {verbosity} {int8}'.strip()
-        LOGGER.info(f"{prefix} running '{cmd}'")
-        subprocess.run(cmd, shell=True)
+            verbosity = "error"
+
+        LOGGER.info(f"{prefix} starting TFLite export with onnx2tf {onnx2tf.__version__}...")
+        onnx2tf.convert(
+            input_onnx_file_path=f_onnx,
+            output_folder_path=str(f),
+            not_use_onnxsim=True,
+            verbosity=verbosity,
+            output_integer_quantized_tflite=self.args.int8,
+            quant_type="per-tensor",  # "per-tensor" (faster) or "per-channel" (slower but more accurate)
+            custom_input_op_name_np_data_path=np_data,
+        )
         yaml_save(f / "metadata.yaml", self.metadata)  # add metadata.yaml
 
         # Remove/rename TFLite models
@@ -884,7 +890,10 @@ def export_tfjs(self, prefix=colorstr("TensorFlow.js:")):
 
         quantization = "--quantize_float16" if self.args.half else "--quantize_uint8" if self.args.int8 else ""
         with spaces_in_path(f_pb) as fpb_, spaces_in_path(f) as f_:  # exporter can not handle spaces in path
-            cmd = f'tensorflowjs_converter --input_format=tf_frozen_model {quantization} --output_node_names={outputs} "{fpb_}" "{f_}"'
+            cmd = (
+                "tensorflowjs_converter "
+                f'--input_format=tf_frozen_model {quantization} --output_node_names={outputs} "{fpb_}" "{f_}"'
+            )
             LOGGER.info(f"{prefix} running '{cmd}'")
             subprocess.run(cmd, shell=True)
 


Test patch: