def dataframe_from_tubs(tubs): dfs = [] for tub in tubs: df = pd.DataFrame(tub) name = Path(tub.base_path).name pref = os.path.join(tub.base_path, Tub.images()) + "/" df["cam/image_array"] = pref + df["cam/image_array"] dfs.append(df) #print( f"Tub {name}: {df['user/throttle'].min()} - {df['user/throttle'].max()}" ) return pd.concat(dfs)
def clips_of_tub(self, tub_path): tub = Tub(tub_path) clips = [] for record in tub: index = record['_index'] images_relative_path = os.path.join(Tub.images(), record['cam/image_array']) record['cam/image_array'] = images_relative_path clips.append(record) return [clips]
def plot_predictions(self, cfg, tub_paths, model_path, limit, model_type): ''' Plot model predictions for angle and throttle against data from tubs. ''' import matplotlib.pyplot as plt import pandas as pd model_path = os.path.expanduser(model_path) model = dk.utils.get_model_by_type(model_type, cfg) # This just gets us the text for the plot title: if model_type is None: model_type = cfg.DEFAULT_MODEL_TYPE model.load(model_path) user_angles = [] user_throttles = [] pilot_angles = [] pilot_throttles = [] from donkeycar.parts.tub_v2 import Tub from pathlib import Path base_path = Path(os.path.expanduser(tub_paths)).absolute().as_posix() tub = Tub(base_path) records = list(tub) records = records[:limit] bar = IncrementalBar('Inferencing', max=len(records)) for record in records: img_filename = os.path.join(base_path, Tub.images(), record['cam/image_array']) img = load_image(img_filename, cfg) user_angle = float(record["user/angle"]) user_throttle = float(record["user/throttle"]) pilot_angle, pilot_throttle = model.run(img) user_angles.append(user_angle) user_throttles.append(user_throttle) pilot_angles.append(pilot_angle) pilot_throttles.append(pilot_throttle) bar.next() angles_df = pd.DataFrame({ 'user_angle': user_angles, 'pilot_angle': pilot_angles }) throttles_df = pd.DataFrame({ 'user_throttle': user_throttles, 'pilot_throttle': pilot_throttles }) fig = plt.figure() title = "Model Predictions\nTubs: " + tub_paths + "\nModel: " + model_path + "\nType: " + model_type fig.suptitle(title) ax1 = fig.add_subplot(211) ax2 = fig.add_subplot(212) angles_df.plot(ax=ax1) throttles_df.plot(ax=ax2) ax1.legend(loc=4) ax2.legend(loc=4) plt.savefig(model_path + '_pred.png') plt.show()
class Tubv2Format(DriveFormat): """ A class to represent a DonkeyCar Tub v2 on disc. Current assumptions: Tub records are 1 indexed and sequential with no gaps. We only care about editing steering and throttle. Steering and throttle should be clipped to -1/1. """ def __init__(self, path): DriveFormat.__init__(self) if not os.path.exists(path): raise IOError( "Tubv2Format directory does not exist: {}".format(path)) if not os.path.isdir(path): raise IOError( "Tubv2Format path is not a directory: {}".format(path)) self.path = path self.tub = Tub(path, read_only=False) self.meta = self.tub.manifest.metadata # Bug. tub.metadata doesn't get updated with info from disc self.deleted_indexes = self.tub.manifest.deleted_indexes print(f"Deleted: {self.deleted_indexes}") self.edit_list = set() self.shape = None def _load(self, path, image_norm=True, progress=None): records = {} indexes = [] images = [ ] # Store images separately so we can easily write changed records back to the tub total = len(self.tub) for idx, rec in enumerate(self.tub): img_path = os.path.join(self.path, self.tub.images(), rec['cam/image_array']) try: img = Image.open(img_path) img_arr = np.asarray(img) if self.shape is None: self.shape = img_arr.shape except Exception as ex: print(f"Failed to load image: {img_path}") print(f" Exception: {ex}") records[idx] = rec indexes.append(idx) images.append(img_arr) progress(idx, total) self.records = records self.indexes = indexes self.images = images def load(self, progress=None): self._load(self.path, progress=progress) self.setClean() def update_line(self, line_num, new_rec): contents = json.dumps(new_rec, allow_nan=False, sort_keys=True) if contents[-1] == NEWLINE: line = contents else: line = f'{contents}{NEWLINE}' self.tub.manifest.current_catalog.seekable.update_line( line_num + 1, line) def save(self): if self.isClean(): return self.tub.manifest.deleted_indexes = self.deleted_indexes for ix in self.edit_list: rec = self.records[ix] self.update_line(ix, rec) self.tub.manifest._update_catalog_metadata(update=True) self.edit_list.clear() self.setClean() def count(self): return len(self.records) def imageForIndex(self, index): idx = self.indexes[index] img = self.images[idx] if self.isIndexDeleted(index): # This grayed out image ends up looking ugly, can't figure out why tmp = img.mean(axis=-1, dtype=img.dtype, keepdims=False) tmp = np.repeat(tmp[:, :, np.newaxis], 3, axis=2) return tmp return img def get_angle_throttle(self, json_data): angle = float(json_data['user/angle']) throttle = float(json_data["user/throttle"]) # If non-valid user entries and we have pilot data (e.g. AI), use that instead. if (0.0 == angle) and (0.0 == throttle): if "pilot/angle" in json_data: pa = json_data['pilot/angle'] if pa is not None: angle = float(pa) if "pilot/throttle" in json_data: pt = json_data['pilot/throttle'] if pt is not None: throttle = float(pt) return angle, throttle def actionForIndex(self, index): idx = self.indexes[index] rec = self.records[idx] angle, throttle = self.get_angle_throttle(rec) return [angle, throttle] def setActionForIndex(self, new_action, index): idx = self.indexes[index] rec = self.records[idx] angle, throttle = self.get_angle_throttle(rec) old_action = [angle, throttle] if not np.array_equal(old_action, new_action): if (rec["user/angle"] != new_action[0]) or (rec["user/throttle"] != new_action[1]): # Save the original values if not already done if "orig/angle" not in rec: rec["orig/angle"] = rec["user/angle"] if "orig/throttle" not in rec: rec["orig/throttle"] = rec["user/throttle"] rec["user/angle"] = new_action[0] rec["user/throttle"] = new_action[1] self.edit_list.add(idx) self.setDirty() def actionForKey(self, keybind, oldAction=None): oldAction = copy.copy(oldAction) if keybind == 'w': oldAction[1] += 0.1 elif keybind == 'x': oldAction[1] -= 0.1 elif keybind == 'a': oldAction[0] -= 0.1 elif keybind == 'd': oldAction[0] += 0.1 elif keybind == 's': oldAction[0] = 0.0 oldAction[1] = 0.0 else: return None return np.clip(oldAction, -1.0, 1.0) def deleteIndex(self, index): if index >= 0 and index < self.count(): index += 1 if index in self.deleted_indexes: self.deleted_indexes.remove(index) else: self.deleted_indexes.add(index) self.setDirty() def isIndexDeleted(self, index): if index >= 0 and index < self.count(): index += 1 return index in self.deleted_indexes return False def metaString(self): #{"inputs": ["cam/image_array", "user/angle", "user/throttle", "user/mode"], "start": 1550950724.8622544, "types": ["image_array", "float", "float", "str"]} ret = "" for k, v in self.meta.items(): ret += "{}: {}\n".format(k, v) return ret def actionStats(self): stats = defaultdict(int) if self.count() > 0: actions = [] for i in range(self.count()): act = self.actionForIndex(i) actions.append(act) stats["Min"] = np.min(actions) stats["Max"] = np.max(actions) stats["Mean"] = np.mean(actions) stats["StdDev"] = np.std(actions) return stats def supportsAuxData(self): return False def getAuxMeta(self): return None def addAuxData(self, meta): return None def auxDataAtIndex(self, auxName, index): return None def setAuxDataAtIndex(self, auxName, auxData, index): return False @classmethod def canOpenFile(cls, path): if not os.path.exists(path): return False if not os.path.isdir(path): return False meta_file = os.path.join(path, "manifest.json") if not os.path.exists(meta_file): return False return True @staticmethod def defaultInputTypes(): return [{ "name": "Images", "type": "numpy image", "shape": (120, 160, 3) }] def inputTypes(self): res = Tubv2Format.defaultInputTypes() if self.shape is not None: res[0]["shape"] = self.shape return res @staticmethod def defaultOutputTypes(): return [{ "name": "Actions", "type": "continuous", "range": (-1.0, 1.0) }] def outputTypes(self): res = [] for act in ["user/angle", "user/throttle"]: display_name = act.split("/")[1] res.append({ "name": display_name, "type": "continuous", "range": (-1.0, 1.0) }) return res