Skip to content

Commit c0d4eb3

Browse files
authored
Upgrade to python 3.9 and TF 2.9 (autorope#1119)
* Updating TF to 2.9 and python to 3.9 * On Jetson 5.0.X use system python 3.8 with Nvidia's TF 2.9 * Tensorrt models cannot be build on the PC any longer, but can be created at runtime on the Jetson during first inference. This might be slow. * Update gh actions accordingly * Small updates in the UI * On RPi use 64-bit Bullseye using Picamera2 * Switch to using savedmodel format by default instead of legacy h5 * Factor out predict method into interpreter base class, as now TF interfaces between different interpreters are aligned and can all be called by input dictionary. * Complying with pep-440 for version numbering of dev versions
1 parent eedd7ed commit c0d4eb3

38 files changed

+616
-379
lines changed

.github/workflows/python-package-conda.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ jobs:
2222
steps:
2323
- name: Checkout code
2424
uses: actions/checkout@v3
25-
- name: Create python 3.7 conda env
25+
- name: Create python 3.9 conda env
2626
uses: conda-incubator/setup-miniconda@v2
2727
with:
28-
python-version: 3.7
29-
mamba-version: "*"
28+
python-version: 3.9
29+
mamba-version: 1.3 # "*"
3030
activate-environment: donkey
3131
environment-file: ${{matrix.ENV_FILE}}
3232
auto-activate-base: false

donkeycar/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
print(f.renderText('Donkey Car'))
1515
print(f'using donkey v{__version__} ...')
1616

17-
if sys.version_info.major < 3 or sys.version_info.minor < 6:
18-
msg = f'Donkey Requires Python 3.6 or greater. You are using {sys.version}'
17+
if sys.version_info.major < 3 or sys.version_info.minor < 8:
18+
msg = f'Donkey Requires Python 3.8 or greater. You are using {sys.version}'
1919
raise ValueError(msg)
2020

2121
# The default recursion limits in CPython are too small.

donkeycar/management/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,8 @@ def plot_predictions(self, cfg, tub_paths, model_path, limit, model_type,
480480
pilot_angles.append(pilot_angle)
481481
pilot_throttles.append(pilot_throttle)
482482
bar.next()
483-
print() # to break the line after progress bar finishes.
484483

484+
bar.finish()
485485
angles_df = pd.DataFrame({'user_angle': user_angles,
486486
'pilot_angle': pilot_angles})
487487
throttles_df = pd.DataFrame({'user_throttle': user_throttles,

donkeycar/management/kivy_ui.py

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ def on_model_type(self, obj, model_type):
716716
if 'tflite' in self.model_type:
717717
self.filters = ['*.tflite']
718718
elif 'tensorrt' in self.model_type:
719-
self.filters = ['*.trt']
719+
self.filters = ['*.trt', '*.savedmodel']
720720
else:
721721
self.filters = ['*.h5', '*.savedmodel']
722722

@@ -971,7 +971,7 @@ class DataFrameLabel(Label):
971971

972972
class TransferSelector(BoxLayout, FileChooserBase):
973973
""" Class to select transfer model"""
974-
filters = ['*.h5']
974+
filters = ['*.h5', '*.savedmodel']
975975

976976

977977
class TrainScreen(Screen):
@@ -980,34 +980,57 @@ class TrainScreen(Screen):
980980
database = ObjectProperty()
981981
pilot_df = ObjectProperty(force_dispatch=True)
982982
tub_df = ObjectProperty(force_dispatch=True)
983+
train_checker = False
983984

984-
def train_call(self, model_type, *args):
985-
# remove car directory from path
985+
def train_call(self, *args):
986986
tub_path = tub_screen().ids.tub_loader.tub.base_path
987987
transfer = self.ids.transfer_spinner.text
988+
model_type = self.ids.train_spinner.text
988989
if transfer != 'Choose transfer model':
989-
transfer = os.path.join(self.config.MODELS_PATH, transfer + '.h5')
990+
h5 = os.path.join(self.config.MODELS_PATH, transfer + '.h5')
991+
sm = os.path.join(self.config.MODELS_PATH, transfer + '.savedmodel')
992+
if os.path.exists(sm):
993+
transfer = sm
994+
elif os.path.exists(h5):
995+
transfer = h5
996+
else:
997+
transfer = None
998+
self.ids.status.text = \
999+
f'Could find neither {sm} nor {trans_h5} - training ' \
1000+
f'without transfer'
9901001
else:
9911002
transfer = None
9921003
try:
9931004
history = train(self.config, tub_paths=tub_path,
9941005
model_type=model_type,
9951006
transfer=transfer,
9961007
comment=self.ids.comment.text)
997-
self.ids.status.text = f'Training completed.'
998-
self.ids.comment.text = 'Comment'
999-
self.ids.transfer_spinner.text = 'Choose transfer model'
1000-
self.reload_database()
10011008
except Exception as e:
10021009
Logger.error(e)
10031010
self.ids.status.text = f'Train failed see console'
1004-
finally:
1005-
self.ids.train_button.state = 'normal'
10061011

1007-
def train(self, model_type):
1012+
def train(self):
10081013
self.config.SHOW_PLOT = False
1009-
Thread(target=self.train_call, args=(model_type,)).start()
1010-
self.ids.status.text = f'Training started.'
1014+
t = Thread(target=self.train_call)
1015+
self.ids.status.text = 'Training started.'
1016+
1017+
def func(dt):
1018+
t.start()
1019+
1020+
def check_training_done(dt):
1021+
if not t.is_alive():
1022+
self.train_checker.cancel()
1023+
self.ids.comment.text = 'Comment'
1024+
self.ids.transfer_spinner.text = 'Choose transfer model'
1025+
self.ids.train_button.state = 'normal'
1026+
self.ids.status.text = 'Training completed.'
1027+
self.ids.train_button.disabled = False
1028+
self.reload_database()
1029+
1030+
# schedules the call after the current frame
1031+
Clock.schedule_once(func, 0)
1032+
# checks if training finished and updates the window if
1033+
self.train_checker = Clock.schedule_interval(check_training_done, 0.5)
10111034

10121035
def set_config_attribute(self, input):
10131036
try:
@@ -1197,19 +1220,20 @@ def connected(self, event):
11971220
self.is_connected = False
11981221
if return_val is None:
11991222
# command still running, do nothing and check next time again
1200-
status = 'Awaiting connection...'
1223+
status = 'Awaiting connection to...'
12011224
self.ids.connected.color = 0.8, 0.8, 0.0, 1
12021225
else:
12031226
# command finished, check if successful and reset connection
12041227
if return_val == 0:
1205-
status = 'Connected'
1228+
status = 'Connected to'
12061229
self.ids.connected.color = 0, 0.9, 0, 1
12071230
self.is_connected = True
12081231
else:
1209-
status = 'Disconnected'
1232+
status = 'Disconnected from'
12101233
self.ids.connected.color = 0.9, 0, 0, 1
12111234
self.connection = None
1212-
self.ids.connected.text = status
1235+
self.ids.connected.text \
1236+
= f'{status} {getattr(self.config, "PI_HOSTNAME")}'
12131237

12141238
def drive(self):
12151239
model_args = ''

donkeycar/management/makemovie.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ def draw_user_input(self, record, img, img_drawon):
104104
user_angle = float(record["user/angle"])
105105
user_throttle = float(record["user/throttle"])
106106
green = (0, 255, 0)
107-
self.draw_line_into_image(user_angle, user_throttle, False, img_drawon, green)
107+
self.draw_line_into_image(user_angle, user_throttle, False,
108+
img_drawon, green)
108109

109110
def draw_model_prediction(self, img, img_drawon):
110111
"""
@@ -114,7 +115,7 @@ def draw_model_prediction(self, img, img_drawon):
114115
if self.keras_part is None:
115116
return
116117

117-
expected = tuple(self.keras_part.get_input_shapes()[0][1:])
118+
expected = tuple(self.keras_part.get_input_shape('img_in')[1:])
118119
actual = img.shape
119120

120121
# if model expects grey-scale but got rgb, covert

donkeycar/management/ui.kv

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -588,8 +588,9 @@
588588
ToggleButton:
589589
id: train_button
590590
text: 'Training running...' if self.state == 'down' else 'Train'
591-
on_press: root.train(train_spinner.text)
592-
591+
on_press:
592+
root.train()
593+
self.disabled = True
593594
ScrollableLabel:
594595
DataFrameLabel:
595596
id: scroll_pilots
@@ -604,7 +605,7 @@
604605
spacing: layout_pad_x
605606
MySpinner:
606607
id: delete_spinner
607-
text: 'Pilot'
608+
text_autoupdate: True
608609
Button:
609610
id: delete_btn
610611
on_press:

donkeycar/parts/camera.py

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,37 @@
99
logger = logging.getLogger(__name__)
1010
logging.basicConfig(level=logging.INFO)
1111

12+
1213
class CameraError(Exception):
1314
pass
1415

16+
1517
class BaseCamera:
1618

1719
def run_threaded(self):
1820
return self.frame
1921

20-
class PiCamera(BaseCamera):
21-
def __init__(self, image_w=160, image_h=120, image_d=3, framerate=20, vflip=False, hflip=False):
22-
from picamera.array import PiRGBArray
23-
from picamera import PiCamera
2422

25-
resolution = (image_w, image_h)
26-
# initialize the camera and stream
27-
self.camera = PiCamera() #PiCamera gets resolution (height, width)
28-
self.camera.resolution = resolution
29-
self.camera.framerate = framerate
30-
self.camera.vflip = vflip
31-
self.camera.hflip = hflip
32-
self.rawCapture = PiRGBArray(self.camera, size=resolution)
33-
self.stream = self.camera.capture_continuous(self.rawCapture,
34-
format="rgb", use_video_port=True)
23+
class PiCamera(BaseCamera):
24+
"""
25+
RPi Camera class based on Bullseye's python class Picamera2.
26+
"""
27+
def __init__(self, image_w=160, image_h=120, image_d=3,
28+
vflip=False, hflip=False):
29+
from picamera2 import Picamera2
30+
from libcamera import Transform
31+
32+
# it's weird but BGR returns RGB images
33+
config_dict = {"size": (image_w, image_h), "format": "BGR888"}
34+
transform = Transform(hflip=hflip, vflip=vflip)
35+
self.camera = Picamera2()
36+
config = self.camera.create_preview_configuration(
37+
config_dict, transform=transform)
38+
self.camera.align_configuration(config)
39+
self.camera.configure(config)
40+
# try min / max frame rate as 0.1 / 1 ms (it will be slower though)
41+
self.camera.set_controls({"FrameDurationLimits": (100, 1000)})
42+
self.camera.start()
3543

3644
# initialize the frame and the variable used to indicate
3745
# if the thread should be stopped
@@ -40,31 +48,23 @@ def __init__(self, image_w=160, image_h=120, image_d=3, framerate=20, vflip=Fals
4048
self.image_d = image_d
4149

4250
# get the first frame or timeout
43-
logger.info('PiCamera loaded...')
44-
if self.stream is not None:
45-
logger.info('PiCamera opened...')
46-
warming_time = time.time() + 5 # quick after 5 seconds
47-
while self.frame is None and time.time() < warming_time:
48-
logger.info("...warming camera")
49-
self.run()
50-
time.sleep(0.2)
51+
logger.info('PiCamera opened...')
52+
warming_time = time.time() + 5 # quick after 5 seconds
53+
while self.frame is None and time.time() < warming_time:
54+
logger.info("...warming camera")
55+
self.run()
56+
time.sleep(0.2)
57+
58+
if self.frame is None:
59+
raise CameraError("Unable to start PiCamera.")
5160

52-
if self.frame is None:
53-
raise CameraError("Unable to start PiCamera.")
54-
else:
55-
raise CameraError("Unable to open PiCamera.")
5661
logger.info("PiCamera ready.")
5762

5863
def run(self):
59-
# grab the frame from the stream and clear the stream in
60-
# preparation for the next frame
61-
if self.stream is not None:
62-
f = next(self.stream)
63-
if f is not None:
64-
self.frame = f.array
65-
self.rawCapture.truncate(0)
66-
if self.image_d == 1:
67-
self.frame = rgb2gray(self.frame)
64+
# grab the next frame from the camera buffer
65+
self.frame = self.camera.capture_array("main")
66+
if self.image_d == 1:
67+
self.frame = rgb2gray(self.frame)
6868

6969
return self.frame
7070

@@ -78,16 +78,13 @@ def shutdown(self):
7878
self.on = False
7979
logger.info('Stopping PiCamera')
8080
time.sleep(.5)
81-
self.stream.close()
82-
self.rawCapture.close()
8381
self.camera.close()
84-
self.stream = None
85-
self.rawCapture = None
8682
self.camera = None
8783

8884

8985
class Webcam(BaseCamera):
90-
def __init__(self, image_w=160, image_h=120, image_d=3, framerate = 20, camera_index = 0):
86+
def __init__(self, image_w=160, image_h=120, image_d=3,
87+
framerate=20, camera_index=0):
9188
#
9289
# pygame is not installed by default.
9390
# Installation on RaspberryPi (with env activated):
@@ -270,18 +267,20 @@ def shutdown(self):
270267
self.running = False
271268
logger.info('Stopping CSICamera')
272269
time.sleep(.5)
273-
del(self.camera)
270+
del self.camera
274271

275272

276273
class V4LCamera(BaseCamera):
277274
'''
278-
uses the v4l2capture library from this fork for python3 support: https://github.com/atareao/python3-v4l2capture
275+
uses the v4l2capture library from this fork for python3 support:
276+
https://github.com/atareao/python3-v4l2capture
279277
sudo apt-get install libv4l-dev
280278
cd python3-v4l2capture
281279
python setup.py build
282280
pip install -e .
283281
'''
284-
def __init__(self, image_w=160, image_h=120, image_d=3, framerate=20, dev_fn="/dev/video0", fourcc='MJPG'):
282+
def __init__(self, image_w=160, image_h=120, image_d=3, framerate=20,
283+
dev_fn="/dev/video0", fourcc='MJPG'):
285284

286285
self.running = True
287286
self.frame = None

donkeycar/parts/datastore.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def put_record(self, data):
219219
elif typ in ['str', 'float', 'int', 'boolean', 'vector']:
220220
json_data[key] = val
221221

222-
elif typ is 'image':
222+
elif typ == 'image':
223223
path = self.make_file_path(key)
224224
val.save(path)
225225
json_data[key]=path

0 commit comments

Comments
 (0)