convert_onnx_to_rknn.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. #!/usr/bin/env python3
  2. """Convert the exported YOLOv5 ONNX model to an INT8 RK3588 RKNN model.
  3. Run this script on an x86_64 Ubuntu 20.04 / Python 3.8 machine with
  4. RKNN Toolkit2 1.5.2 installed. It is deliberately not run on the robot;
  5. the robot only needs RKNN Toolkit Lite2 to execute the resulting file.
  6. """
  7. from __future__ import annotations
  8. import argparse
  9. import sys
  10. from pathlib import Path
  11. from rknn.api import RKNN
  12. def parse_args() -> argparse.Namespace:
  13. parser = argparse.ArgumentParser(description=__doc__)
  14. parser.add_argument("--onnx", required=True, type=Path, help="Path to best.onnx")
  15. parser.add_argument(
  16. "--dataset",
  17. required=True,
  18. type=Path,
  19. help="Text file containing absolute paths to representative PNG/JPG images",
  20. )
  21. parser.add_argument(
  22. "--output",
  23. type=Path,
  24. default=Path("traffic_sign_direction.rknn"),
  25. help="Output RKNN model path",
  26. )
  27. parser.add_argument(
  28. "--no-quantize",
  29. action="store_true",
  30. help="Build an FP model for conversion diagnosis (larger and slower than INT8)",
  31. )
  32. return parser.parse_args()
  33. def check_inputs(onnx_path: Path, dataset_path: Path, needs_calibration: bool) -> None:
  34. if not onnx_path.is_file():
  35. raise FileNotFoundError("ONNX model not found: %s" % onnx_path)
  36. if not needs_calibration:
  37. return
  38. if not dataset_path.is_file():
  39. raise FileNotFoundError("Calibration dataset list not found: %s" % dataset_path)
  40. images = [Path(line.strip()) for line in dataset_path.read_text(encoding="utf-8").splitlines() if line.strip()]
  41. if len(images) < 20:
  42. raise ValueError("Use at least 20 representative images for INT8 calibration; got %d" % len(images))
  43. missing = [str(image) for image in images if not image.is_file()]
  44. if missing:
  45. raise FileNotFoundError("Missing image paths in %s, first: %s" % (dataset_path, missing[0]))
  46. print("INT8 calibration images: %d" % len(images))
  47. def main() -> int:
  48. args = parse_args()
  49. onnx_path = args.onnx.expanduser().resolve()
  50. dataset_path = args.dataset.expanduser().resolve()
  51. output_path = args.output.expanduser().resolve()
  52. quantize = not args.no_quantize
  53. check_inputs(onnx_path, dataset_path, needs_calibration=quantize)
  54. output_path.parent.mkdir(parents=True, exist_ok=True)
  55. rknn = RKNN(verbose=True)
  56. try:
  57. # The ROS node supplies a 640x640 RGB uint8 image. RKNN performs the
  58. # equivalent of YOLOv5's /255 input normalization inside the graph.
  59. result = rknn.config(
  60. target_platform="rk3588",
  61. mean_values=[[0, 0, 0]],
  62. std_values=[[255, 255, 255]],
  63. optimization_level=3,
  64. )
  65. if result != 0:
  66. raise RuntimeError("rknn.config failed: %s" % result)
  67. result = rknn.load_onnx(model=str(onnx_path))
  68. if result != 0:
  69. raise RuntimeError("rknn.load_onnx failed: %s" % result)
  70. build_options = {"do_quantization": quantize}
  71. if quantize:
  72. build_options["dataset"] = str(dataset_path)
  73. result = rknn.build(**build_options)
  74. if result != 0:
  75. raise RuntimeError("rknn.build failed: %s" % result)
  76. result = rknn.export_rknn(str(output_path))
  77. if result != 0:
  78. raise RuntimeError("rknn.export_rknn failed: %s" % result)
  79. finally:
  80. rknn.release()
  81. print("RKNN conversion complete (%s): %s" % ("INT8" if quantize else "FP", output_path))
  82. return 0
  83. if __name__ == "__main__":
  84. try:
  85. raise SystemExit(main())
  86. except (FileNotFoundError, RuntimeError, ValueError) as error:
  87. print("ERROR: %s" % error, file=sys.stderr)
  88. raise SystemExit(1)