From f3285a2b61f2626c8886bd2c666b75ad0dd8c49e Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 9 Jun 2023 15:30:00 +0000 Subject: [PATCH 001/102] add new metric to compute mAP at different distances from patch --- armory/metrics/task.py | 63 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index 64be1a0f2..b9ced0a19 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -640,6 +640,69 @@ def video_tracking_mean_success_rate(y, y_pred): return mean_success_rates +@populationwise +def object_detection_AP_per_class_by_distance_from_patch( + y_list, + y_pred_list, + y_patch_metadata_list, + iou_threshold=0.5, + class_list=None, + mean=True, + increment=50, +): + + y_distances_list = [] + y_pred_distances_list = [] + result = {} + + for y, y_pred, metadata in zip(y_list, y_pred_list, y_patch_metadata_list): + # For each image, find centroid of patch and compute distance to centroid of each box + patch_centroid = metadata["gs_coords"].mean(axis=0) + y_centroids = (y["boxes"][:, :2] + y["boxes"][:, 2:]) / 2 + y_distances = np.linalg.norm(y_centroids - patch_centroid, axis=1) + pred_centroids = (y_pred["boxes"][:, :2] + y_pred["boxes"][:, 2:]) / 2 + pred_distances = np.linalg.norm(pred_centroids - patch_centroid, axis=1) + y_distances_list.append(y_distances) + y_pred_distances_list.append(pred_distances) + + max_distance = max( + max([max(d) for d in y_distances_list]), + max([max(d) for d in y_pred_distances_list]), + ) + threshold = increment + final = False + while not final: + # Compute mAP restricted to boxes whose distance to patch is less than 'threshold' + + if threshold > max_distance: + final = True + + # Build new y_lists with only sufficiently close boxes + y_list_ = [ + { + "boxes": y["boxes"][y_d < threshold], + "labels": y["labels"][y_d < threshold], + } + for y, y_d in zip(y_list, y_distances_list) + ] + y_pred_list_ = [ + { + "boxes": y["boxes"][y_d < threshold], + "labels": y["labels"][y_d < threshold], + "scores": y["scores"][y_d < threshold], + } + for y, y_d in zip(y_pred_list, y_pred_distances_list) + ] + + # Add mAP to result dict + result[threshold] = object_detection_AP_per_class( + y_list_, y_pred_list_, iou_threshold, class_list, mean + ) + threshold += increment + + return result + + @populationwise def object_detection_AP_per_class( y_list, From 97593ea514969839980b8705ffff9a894113c759 Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 9 Jun 2023 15:30:47 +0000 Subject: [PATCH 002/102] load new metric in carla od scenario --- armory/scenarios/carla_object_detection.py | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/armory/scenarios/carla_object_detection.py b/armory/scenarios/carla_object_detection.py index 4b4eea1b6..2a26d8651 100644 --- a/armory/scenarios/carla_object_detection.py +++ b/armory/scenarios/carla_object_detection.py @@ -7,6 +7,8 @@ from armory.instrument.export import ObjectDetectionExporter from armory.logs import log from armory.scenarios.object_detection import ObjectDetectionTask +from armory.instrument import GlobalMeter, ResultsLogWriter +from armory import metrics class CarlaObjectDetectionTask(ObjectDetectionTask): @@ -23,6 +25,31 @@ def load_dataset(self): raise ValueError("batch_size must be 1 for evaluation.") super().load_dataset(eval_split_default="dev") + def load_metrics(self): + super().load_metrics() + + # These metrics are loaded here manually because y_patch_metadata cannot be passed through the default MetricsLogger loading code. + # I will attempt to update that in the near future. + meters = [ + GlobalMeter( + "benign_AP_per_class_by_distance_from_patch", + metrics.get("object_detection_AP_per_class_by_distance_from_patch"), + "scenario.y", + "scenario.y_pred", + "scenario.y_patch_metadata", + ), + GlobalMeter( + "adversarial_AP_per_class_by_distance_from_patch", + metrics.get("object_detection_AP_per_class_by_distance_from_patch"), + "scenario.y", + "scenario.y_pred_adv", + "scenario.y_patch_metadata", + ), + ] + for meter in meters: + self.hub.connect_meter(meter) + self.hub.connect_writer(ResultsLogWriter(), meters=meters, default=False) + def next(self): super().next() # The CARLA dev and test sets (as opposed to train/val) contain green-screens From cd65243233e079e2a966065439fd3826ad565250 Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 9 Jun 2023 15:34:05 +0000 Subject: [PATCH 003/102] add missing class to __init__.py --- armory/instrument/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/armory/instrument/__init__.py b/armory/instrument/__init__.py index 1351a985e..9fceab94c 100644 --- a/armory/instrument/__init__.py +++ b/armory/instrument/__init__.py @@ -10,6 +10,7 @@ PrintWriter, Probe, ResultsWriter, + ResultsLogWriter, Writer, del_globals, get_hub, From c77d7963ffb9f89f15738f669d68178259e3b1dd Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 9 Jun 2023 16:10:18 +0000 Subject: [PATCH 004/102] check that scenario has y_patch_metadata --- armory/scenarios/carla_object_detection.py | 43 +++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/armory/scenarios/carla_object_detection.py b/armory/scenarios/carla_object_detection.py index 2a26d8651..504d56e96 100644 --- a/armory/scenarios/carla_object_detection.py +++ b/armory/scenarios/carla_object_detection.py @@ -28,27 +28,28 @@ def load_dataset(self): def load_metrics(self): super().load_metrics() - # These metrics are loaded here manually because y_patch_metadata cannot be passed through the default MetricsLogger loading code. - # I will attempt to update that in the near future. - meters = [ - GlobalMeter( - "benign_AP_per_class_by_distance_from_patch", - metrics.get("object_detection_AP_per_class_by_distance_from_patch"), - "scenario.y", - "scenario.y_pred", - "scenario.y_patch_metadata", - ), - GlobalMeter( - "adversarial_AP_per_class_by_distance_from_patch", - metrics.get("object_detection_AP_per_class_by_distance_from_patch"), - "scenario.y", - "scenario.y_pred_adv", - "scenario.y_patch_metadata", - ), - ] - for meter in meters: - self.hub.connect_meter(meter) - self.hub.connect_writer(ResultsLogWriter(), meters=meters, default=False) + if hasattr(self, "y_patch_metadata"): + # These metrics are loaded here manually because y_patch_metadata cannot be passed through the default MetricsLogger loading code. + # I will attempt to update that in the near future. + meters = [ + GlobalMeter( + "benign_AP_per_class_by_distance_from_patch", + metrics.get("object_detection_AP_per_class_by_distance_from_patch"), + "scenario.y", + "scenario.y_pred", + "scenario.y_patch_metadata", + ), + GlobalMeter( + "adversarial_AP_per_class_by_distance_from_patch", + metrics.get("object_detection_AP_per_class_by_distance_from_patch"), + "scenario.y", + "scenario.y_pred_adv", + "scenario.y_patch_metadata", + ), + ] + for meter in meters: + self.hub.connect_meter(meter) + self.hub.connect_writer(ResultsLogWriter(), meters=meters, default=False) def next(self): super().next() From 3b56ce3e3ae83ba597d26a098d5f1cb3b411004c Mon Sep 17 00:00:00 2001 From: Sterling Date: Mon, 12 Jun 2023 13:58:55 +0000 Subject: [PATCH 005/102] split iou function into modular components for reusability --- armory/metrics/task.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index b9ced0a19..823ef0877 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -534,6 +534,25 @@ def _check_video_tracking_input(y, y_pred): assert y_box_array_shape == y_pred_box_array_shape +def _intersection(box_1, box_2): + """ Return the area of the intersection of two boxes + """ + x_left = max(box_1[1], box_2[1]) + x_right = min(box_1[3], box_2[3]) + y_top = max(box_1[0], box_2[0]) + y_bottom = min(box_1[2], box_2[2]) + + return max(0, x_right - x_left) * max(0, y_bottom - y_top) + +def _union(box_1, box_2): + """ Return the area of the union of two boxes + """ + box_1_area = (box_1[3] - box_1[1]) * (box_1[2] - box_1[0]) + box_2_area = (box_2[3] - box_2[1]) * (box_2[2] - box_2[0]) + intersect_area = _intersection(box_1, box_2) + return box_1_area + box_2_area - intersect_area + + def _intersection_over_union(box_1, box_2): """ Assumes each input has shape (4,) and format [y1, x1, y2, x2] or [x1, y1, x2, y2] @@ -548,20 +567,13 @@ def _intersection_over_union(box_1, box_2): ): log.warning("One set of boxes appears to be normalized while the other is not") - # Determine coordinates of intersection box - x_left = max(box_1[1], box_2[1]) - x_right = min(box_1[3], box_2[3]) - y_top = max(box_1[0], box_2[0]) - y_bottom = min(box_1[2], box_2[2]) - - intersect_area = max(0, x_right - x_left) * max(0, y_bottom - y_top) + intersect_area = _intersection(box_1, box_2) if intersect_area == 0: return 0 - box_1_area = (box_1[3] - box_1[1]) * (box_1[2] - box_1[0]) - box_2_area = (box_2[3] - box_2[1]) * (box_2[2] - box_2[0]) + union_area = _union(box_1, box_2) - iou = intersect_area / (box_1_area + box_2_area - intersect_area) + iou = intersect_area / union_area assert iou >= 0 assert iou <= 1 return iou From 2a403ca7a3a88fc5a8e65263a5c248a61f850d6f Mon Sep 17 00:00:00 2001 From: Sterling Date: Mon, 12 Jun 2023 14:00:51 +0000 Subject: [PATCH 006/102] add generalized iou function --- armory/metrics/task.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index 823ef0877..28c24a108 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -535,8 +535,7 @@ def _check_video_tracking_input(y, y_pred): def _intersection(box_1, box_2): - """ Return the area of the intersection of two boxes - """ + """Return the area of the intersection of two boxes""" x_left = max(box_1[1], box_2[1]) x_right = min(box_1[3], box_2[3]) y_top = max(box_1[0], box_2[0]) @@ -544,14 +543,14 @@ def _intersection(box_1, box_2): return max(0, x_right - x_left) * max(0, y_bottom - y_top) + def _union(box_1, box_2): - """ Return the area of the union of two boxes - """ + """Return the area of the union of two boxes""" box_1_area = (box_1[3] - box_1[1]) * (box_1[2] - box_1[0]) box_2_area = (box_2[3] - box_2[1]) * (box_2[2] - box_2[0]) intersect_area = _intersection(box_1, box_2) return box_1_area + box_2_area - intersect_area - + def _intersection_over_union(box_1, box_2): """ @@ -579,6 +578,29 @@ def _intersection_over_union(box_1, box_2): return iou +def _generalized_intersection_over_union(box_1, box_2): + """https://giou.stanford.edu/ + Note that the call to _intersection_over_union will check the format of the input boxes. + """ + + # Find c: the area of smallest box enclosing both boxes + top = min(box_1[0], box_2[0]) + left = min(box_1[1], box_2[1]) + bottom = max(box_1[2], box_2[2]) + right = max(box_1[3], box_2[3]) + c = max(bottom - top, 0) * max(right - left, 0) + + # GIoU = IoU - ((C - (A U B)) | / C) + u = _union(box_1, box_2) + c_term = (c - u) / c if c > 0 else 0 + iou = _intersection_over_union(box_1, box_2) + giou = iou - c_term + + assert giou >= -1 + assert giou <= 1 + return giou + + @batchwise def video_tracking_mean_iou(y, y_pred): """ From dc757913197e0af004b681a0d00d7ef5429ef689 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 14 Jun 2023 15:16:14 +0000 Subject: [PATCH 007/102] use giou for distance based carla metric --- armory/metrics/task.py | 52 ++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index 28c24a108..504b1b042 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -675,14 +675,14 @@ def video_tracking_mean_success_rate(y, y_pred): @populationwise -def object_detection_AP_per_class_by_distance_from_patch( +def object_detection_AP_per_class_by_min_giou_from_patch( y_list, y_pred_list, y_patch_metadata_list, iou_threshold=0.5, class_list=None, mean=True, - increment=50, + increment=0.1, ): y_distances_list = [] @@ -690,40 +690,39 @@ def object_detection_AP_per_class_by_distance_from_patch( result = {} for y, y_pred, metadata in zip(y_list, y_pred_list, y_patch_metadata_list): - # For each image, find centroid of patch and compute distance to centroid of each box - patch_centroid = metadata["gs_coords"].mean(axis=0) - y_centroids = (y["boxes"][:, :2] + y["boxes"][:, 2:]) / 2 - y_distances = np.linalg.norm(y_centroids - patch_centroid, axis=1) - pred_centroids = (y_pred["boxes"][:, :2] + y_pred["boxes"][:, 2:]) / 2 - pred_distances = np.linalg.norm(pred_centroids - patch_centroid, axis=1) + # For each image, use GIoU as proxy for distance between boxes and patch. + # GIoU is positive if there is overlap. + # Note: patch is quadrilateral but not necessarily square. We use the smallest enclosing box instead. + patch = metadata["gs_coords"] + smallest_box_enclosing_patch = np.array( + [min(patch[:, 0]), min(patch[:, 1]), max(patch[:, 0]), max(patch[:, 1])] + ) + y_distances = [ + _generalized_intersection_over_union(box, smallest_box_enclosing_patch) + for box in y["boxes"] + ] + pred_distances = [ + _generalized_intersection_over_union(box, smallest_box_enclosing_patch) + for box in y_pred["boxes"] + ] y_distances_list.append(y_distances) y_pred_distances_list.append(pred_distances) - max_distance = max( - max([max(d) for d in y_distances_list]), - max([max(d) for d in y_pred_distances_list]), - ) - threshold = increment - final = False - while not final: - # Compute mAP restricted to boxes whose distance to patch is less than 'threshold' - - if threshold > max_distance: - final = True - - # Build new y_lists with only sufficiently close boxes + # Compute mAP for boxes restricted to a minimum GIoU + for threshold in np.arange(0, -1 - increment, -increment): + # Build new y_lists containing only boxes with large enough GIoU y_list_ = [ { - "boxes": y["boxes"][y_d < threshold], - "labels": y["labels"][y_d < threshold], + "boxes": y["boxes"][y_d > threshold], + "labels": y["labels"][y_d > threshold], } for y, y_d in zip(y_list, y_distances_list) ] y_pred_list_ = [ { - "boxes": y["boxes"][y_d < threshold], - "labels": y["labels"][y_d < threshold], - "scores": y["scores"][y_d < threshold], + "boxes": y["boxes"][y_d > threshold], + "labels": y["labels"][y_d > threshold], + "scores": y["scores"][y_d > threshold], } for y, y_d in zip(y_pred_list, y_pred_distances_list) ] @@ -732,7 +731,6 @@ def object_detection_AP_per_class_by_distance_from_patch( result[threshold] = object_detection_AP_per_class( y_list_, y_pred_list_, iou_threshold, class_list, mean ) - threshold += increment return result From e6401ed6cfb6fcc121d9269cbdd4bfa1eb014f5e Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 14 Jun 2023 15:26:50 +0000 Subject: [PATCH 008/102] remove untimely variable existence check. and update metric name --- armory/scenarios/carla_object_detection.py | 43 +++++++++++----------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/armory/scenarios/carla_object_detection.py b/armory/scenarios/carla_object_detection.py index 504d56e96..c7609341a 100644 --- a/armory/scenarios/carla_object_detection.py +++ b/armory/scenarios/carla_object_detection.py @@ -28,28 +28,27 @@ def load_dataset(self): def load_metrics(self): super().load_metrics() - if hasattr(self, "y_patch_metadata"): - # These metrics are loaded here manually because y_patch_metadata cannot be passed through the default MetricsLogger loading code. - # I will attempt to update that in the near future. - meters = [ - GlobalMeter( - "benign_AP_per_class_by_distance_from_patch", - metrics.get("object_detection_AP_per_class_by_distance_from_patch"), - "scenario.y", - "scenario.y_pred", - "scenario.y_patch_metadata", - ), - GlobalMeter( - "adversarial_AP_per_class_by_distance_from_patch", - metrics.get("object_detection_AP_per_class_by_distance_from_patch"), - "scenario.y", - "scenario.y_pred_adv", - "scenario.y_patch_metadata", - ), - ] - for meter in meters: - self.hub.connect_meter(meter) - self.hub.connect_writer(ResultsLogWriter(), meters=meters, default=False) + # These metrics are loaded here manually because y_patch_metadata cannot be passed through the default MetricsLogger loading code. + # I will attempt to update that in the near future. + meters = [ + GlobalMeter( + "benign_AP_per_class_by_min_giou_from_patch", + metrics.get("object_detection_AP_per_class_by_min_giou_from_patch"), + "scenario.y", + "scenario.y_pred", + "scenario.y_patch_metadata", + ), + GlobalMeter( + "adversarial_AP_per_class_by_min_giou_from_patch", + metrics.get("object_detection_AP_per_class_by_min_giou_from_patch"), + "scenario.y", + "scenario.y_pred_adv", + "scenario.y_patch_metadata", + ), + ] + for meter in meters: + self.hub.connect_meter(meter) + self.hub.connect_writer(ResultsLogWriter(), meters=meters, default=False) def next(self): super().next() From c473e4d01bac50209a7e9eab38afcd928da8f269 Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 16 Jun 2023 19:59:49 +0000 Subject: [PATCH 009/102] adding helper functions for more accurate GIoU --- armory/metrics/task.py | 113 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index 504b1b042..b3a92bdf4 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -578,6 +578,119 @@ def _intersection_over_union(box_1, box_2): return iou +def _area_of_polygon(points): + # Shoelace formula, implementation due to https://stackoverflow.com/a/30408825 + # points is shape (N, 2) and points must be in a consistent clockwise or counterclockwise order + + x = points[:, 0] + y = points[:, 1] + return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) + + +def _area_of_convex_hull(points): + # Return area of convex hull of set of 2D points. + # points is shape (N, 2). + + from scipy.spatial import ConvexHull + + return ConvexHull(points).volume + + +def _orient_box_counterclockwise(box): + # Reverse box vertices if orientation is clockwise. + # https://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon + # Assumes box is convex, so arbitrary vertices can be used. + + a, b, c = box[0], box[1], box[2] + det = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]) + if det < 0: + # negative determinant => clockwise orientation + box = box[::-1] + return box + + +def _intersection_nonsquare(box_1, box_2): + """ + Find the area of the intersection of two convex polygons. + box_1 and box_2 are lists or arrays of 2D points, not necessarily the same length. + It is required, but not verified, that box_1 and box_2 are oriented counter-clockwise. + + Adapted with deepest gratitude from https://rosettacode.org/wiki/Sutherland-Hodgman_polygon_clipping#Python + This version returns the area, not just the points defining the polygon. + In computing the area, we assume that the intersection is a convex set, hence the input boxes must be convex. + This assumption could be relaxed with some additional verification of the algorithm's output. + """ + + def inside(p): + inside = (cp2[0] - cp1[0]) * (p[1] - cp1[1]) > (cp2[1] - cp1[1]) * ( + p[0] - cp1[0] + ) + return inside + + def computeIntersection(): + # Find the intersection of two line segments cp1-cp2 and s-e defined outside the function + # See https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line + # Note: this is called only if the line segments are known to intersect. Thus there is no + # risk of divisiding by 0 when computing n3 below + dc = [cp1[0] - cp2[0], cp1[1] - cp2[1]] + dp = [s[0] - e[0], s[1] - e[1]] + n1 = cp1[0] * cp2[1] - cp1[1] * cp2[0] + n2 = s[0] * e[1] - s[1] * e[0] + n3 = 1.0 / (dc[0] * dp[1] - dc[1] * dp[0]) + return [(n1 * dp[0] - n2 * dc[0]) * n3, (n1 * dp[1] - n2 * dc[1]) * n3] + + box_1 = _orient_box_counterclockwise(box_1) + box_2 = _orient_box_counterclockwise(box_2) + outputList = box_1 + cp1 = box_2[-1] + + for point in box_2: + if len(outputList) == 0: + # All points of box1 are on the far side of the plane cp1-cp2, hence intersection is empty + return 0 + + cp2 = point + # cp1 and cp2 define an edge of box2 + inputList = outputList + outputList = [] + s = inputList[-1] + + for subjectVertex in inputList: + e = subjectVertex + # s and e define edge of box1 + if inside(e): + if not inside(s): + outputList.append(computeIntersection()) + outputList.append(e) + elif inside(s): + outputList.append(computeIntersection()) + s = e + cp1 = cp2 + + if len(outputList) == 0: + # The intersection is empty + return 0 + + # Compute area using convex_hull, in case points are not ordered. + # (Assumption: input boxes are convex) + area = _area_of_convex_hull(outputList) + + return area + + +def _validate_input_box_for_giou(box): + if len(box.shape) == 1: + assert box[2] >= box[0] + assert box[3] >= box[1] + box = np.array( + [[box[0], box[1]], [box[0], box[3]], [box[2], box[3]], [box[2], box[1]]] + ) + for pt in box: + assert len(pt) == 2 + + return box + + def _generalized_intersection_over_union(box_1, box_2): """https://giou.stanford.edu/ Note that the call to _intersection_over_union will check the format of the input boxes. From 02f39d8b370325c31a1f7e7cb21642a774eb253c Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 16 Jun 2023 20:01:04 +0000 Subject: [PATCH 010/102] update GIoU function with more accurate components --- armory/metrics/task.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index b3a92bdf4..235985259 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -692,22 +692,31 @@ def _validate_input_box_for_giou(box): def _generalized_intersection_over_union(box_1, box_2): - """https://giou.stanford.edu/ - Note that the call to _intersection_over_union will check the format of the input boxes. """ + https://giou.stanford.edu/ - # Find c: the area of smallest box enclosing both boxes - top = min(box_1[0], box_2[0]) - left = min(box_1[1], box_2[1]) - bottom = max(box_1[2], box_2[2]) - right = max(box_1[3], box_2[3]) - c = max(bottom - top, 0) * max(right - left, 0) + Return the GIoU between two convex polygons. + (Convexity is currently required by _orient_box_counterclockwise and _intersection_nonsquare. + This requirement can be relaxed in the future if desired.) + + box_1 and box_2 are either shape (4,) with format [x1, y1, x2, y2] (defining a rectangle), + or shape (N,2) where each element is of the form [x, y] (defining an arbitrary polygon) + + """ + + box_1 = _validate_input_box_for_giou(box_1) + box_2 = _validate_input_box_for_giou(box_2) + + # Find C: the area of convex hull enclosing both boxes + C = _area_of_convex_hull(np.vstack((box_1, box_2))) # GIoU = IoU - ((C - (A U B)) | / C) - u = _union(box_1, box_2) - c_term = (c - u) / c if c > 0 else 0 - iou = _intersection_over_union(box_1, box_2) - giou = iou - c_term + intersection = _intersection_nonsquare(box_1, box_2) + union = _area_of_polygon(box_1) + _area_of_polygon(box_2) - intersection + IoU = (intersection / union) if union > 0 else 0 + + c_term = (C - union) / C if C > 0 else 0 + giou = IoU - c_term assert giou >= -1 assert giou <= 1 From de3ef44d3b7849959f9a3216c14c27217e4a4008 Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 16 Jun 2023 20:03:45 +0000 Subject: [PATCH 011/102] update giou-based mAP function --- armory/metrics/task.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index 235985259..163b5e2f4 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -701,9 +701,7 @@ def _generalized_intersection_over_union(box_1, box_2): box_1 and box_2 are either shape (4,) with format [x1, y1, x2, y2] (defining a rectangle), or shape (N,2) where each element is of the form [x, y] (defining an arbitrary polygon) - """ - box_1 = _validate_input_box_for_giou(box_1) box_2 = _validate_input_box_for_giou(box_2) @@ -814,25 +812,19 @@ def object_detection_AP_per_class_by_min_giou_from_patch( for y, y_pred, metadata in zip(y_list, y_pred_list, y_patch_metadata_list): # For each image, use GIoU as proxy for distance between boxes and patch. # GIoU is positive if there is overlap. - # Note: patch is quadrilateral but not necessarily square. We use the smallest enclosing box instead. patch = metadata["gs_coords"] - smallest_box_enclosing_patch = np.array( - [min(patch[:, 0]), min(patch[:, 1]), max(patch[:, 0]), max(patch[:, 1])] - ) y_distances = [ - _generalized_intersection_over_union(box, smallest_box_enclosing_patch) - for box in y["boxes"] + _generalized_intersection_over_union(box, patch) for box in y["boxes"] ] pred_distances = [ - _generalized_intersection_over_union(box, smallest_box_enclosing_patch) - for box in y_pred["boxes"] + _generalized_intersection_over_union(box, patch) for box in y_pred["boxes"] ] y_distances_list.append(y_distances) y_pred_distances_list.append(pred_distances) # Compute mAP for boxes restricted to a minimum GIoU for threshold in np.arange(0, -1 - increment, -increment): - # Build new y_lists containing only boxes with large enough GIoU + # Start with 0 -- all boxes that overlap the patch y_list_ = [ { "boxes": y["boxes"][y_d > threshold], From 48caefe594ad4c8189667dd426985eb3517c039d Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 21 Jun 2023 17:39:34 +0000 Subject: [PATCH 012/102] code and comment improvements --- armory/metrics/task.py | 196 +++++++++++++++++++++++++++++++---------- 1 file changed, 149 insertions(+), 47 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index 163b5e2f4..9d1815ac5 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -579,8 +579,16 @@ def _intersection_over_union(box_1, box_2): def _area_of_polygon(points): - # Shoelace formula, implementation due to https://stackoverflow.com/a/30408825 - # points is shape (N, 2) and points must be in a consistent clockwise or counterclockwise order + """ + Shoelace formula for area of a polygon. + https://en.wikipedia.org/wiki/Shoelace_formula + + points is an array or list of shape (N, 2) and points may be in clockwise or counterclockwise order + """ + + if type(points) == list: + points = np.array(points) + assert points.shape[1] == 2 x = points[:, 0] y = points[:, 1] @@ -588,40 +596,95 @@ def _area_of_polygon(points): def _area_of_convex_hull(points): - # Return area of convex hull of set of 2D points. - # points is shape (N, 2). + """ + Return area of convex hull of set of 2D points. + points is an array of shape (N, 2). + """ from scipy.spatial import ConvexHull + assert points.shape[1] == 2 return ConvexHull(points).volume -def _orient_box_counterclockwise(box): - # Reverse box vertices if orientation is clockwise. - # https://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon - # Assumes box is convex, so arbitrary vertices can be used. +def _is_convex(poly): + """ + Return True if poly is a convex polygon, else False. + poly is an array of shape (N,2) + """ + + assert poly.shape[1] == 2 + + for i in range(len(poly)): + if (poly[i] == poly[i - 1]).all(): + raise ValueError(f"Adjacent points should not be identical: {poly}") + + # A polygon is convex if the cross product has the same sign at all vertices. + signs = [ + np.sign(np.cross(poly[i - 1] - poly[i], poly[i - 2] - poly[i - 1])) + for i in range(len(poly)) + ] + + if (np.array(signs) == 0).all(): + raise ValueError("Input polygon is a straight line.") + + return not (1 in signs and -1 in signs) + - a, b, c = box[0], box[1], box[2] - det = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]) - if det < 0: - # negative determinant => clockwise orientation - box = box[::-1] - return box +def _orient_polygon_counterclockwise(poly): + """ + Reverse polygon vertices if orientation is clockwise. + https://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon + Assumes polygon is convex, so arbitrary vertices can be used. + This can be relaxed in the future if needed, see link. + poly is array of shape (N,2) -def _intersection_nonsquare(box_1, box_2): + Returns (N,2) array of indices oriented counterclockwise + """ + + assert poly.shape[1] == 2 + + if len(poly) < 3: + raise ValueError("Input polygon must have at least 3 vertices.") + + if not _is_convex(poly): + raise ValueError( + "Input polygon should be convex, or this function can be extended for the non-convex case." + ) + + cross = np.cross(poly[1] - poly[0], poly[2] - poly[1]) + + if cross == 0: + log.warning("Input polygon has vertex along straight line segment.") + if len(poly) > 3: + # remove redundant vertex + return _orient_polygon_counterclockwise(np.vstack((poly[0], poly[2:]))) + else: + raise ValueError("Input polygon is a straight line and has no orientation.") + + if cross < 0: + # negative cross product => clockwise orientation + poly = poly[::-1] + return poly + + +def _polygon_intersection(poly_1, poly_2): """ Find the area of the intersection of two convex polygons. - box_1 and box_2 are lists or arrays of 2D points, not necessarily the same length. - It is required, but not verified, that box_1 and box_2 are oriented counter-clockwise. + + poly_1 and poly_2 are arrays of 2D points, not necessarily the same length. + They may be oriented either clockwise or counter-clockwise. Adapted with deepest gratitude from https://rosettacode.org/wiki/Sutherland-Hodgman_polygon_clipping#Python This version returns the area, not just the points defining the polygon. - In computing the area, we assume that the intersection is a convex set, hence the input boxes must be convex. - This assumption could be relaxed with some additional verification of the algorithm's output. + + Behavior of this algorithm is not verified for non-convex polygons, the intersection of which may be disconnected. """ def inside(p): + # Given a hyperplane defined by cp1 and cp2 (outside the function), determine which side + # of the hyperplane p lies on inside = (cp2[0] - cp1[0]) * (p[1] - cp1[1]) > (cp2[1] - cp1[1]) * ( p[0] - cp1[0] ) @@ -639,25 +702,30 @@ def computeIntersection(): n3 = 1.0 / (dc[0] * dp[1] - dc[1] * dp[0]) return [(n1 * dp[0] - n2 * dc[0]) * n3, (n1 * dp[1] - n2 * dc[1]) * n3] - box_1 = _orient_box_counterclockwise(box_1) - box_2 = _orient_box_counterclockwise(box_2) - outputList = box_1 - cp1 = box_2[-1] + assert poly_1.shape[1] == 2 + assert poly_2.shape[1] == 2 - for point in box_2: + poly_1 = _orient_polygon_counterclockwise(poly_1) + poly_2 = _orient_polygon_counterclockwise(poly_2) + outputList = poly_1 + cp1 = poly_2[-1] + + # Loop over sides of poly_2 + for point in poly_2: if len(outputList) == 0: # All points of box1 are on the far side of the plane cp1-cp2, hence intersection is empty return 0 cp2 = point - # cp1 and cp2 define an edge of box2 + # cp1 and cp2 define an edge of poly_2 inputList = outputList outputList = [] s = inputList[-1] + # Loop over sides of poly_1 for subjectVertex in inputList: e = subjectVertex - # s and e define edge of box1 + # s and e define edge of poly_1 if inside(e): if not inside(s): outputList.append(computeIntersection()) @@ -671,46 +739,53 @@ def computeIntersection(): # The intersection is empty return 0 - # Compute area using convex_hull, in case points are not ordered. - # (Assumption: input boxes are convex) - area = _area_of_convex_hull(outputList) + area = _area_of_polygon(outputList) return area -def _validate_input_box_for_giou(box): - if len(box.shape) == 1: - assert box[2] >= box[0] - assert box[3] >= box[1] - box = np.array( - [[box[0], box[1]], [box[0], box[3]], [box[2], box[3]], [box[2], box[1]]] +def _validate_input_polygon_for_giou(poly): + """ + Standardize polygon format for GIoU input. + Converts two-corner rect format to four-corner rect and validates shape + """ + + if len(poly.shape) == 1: + assert poly[2] >= poly[0] + assert poly[3] >= poly[1] + poly = np.array( + [ + [poly[0], poly[1]], + [poly[0], poly[3]], + [poly[2], poly[3]], + [poly[2], poly[1]], + ] ) - for pt in box: + for pt in poly: assert len(pt) == 2 - return box + return poly -def _generalized_intersection_over_union(box_1, box_2): +def _generalized_intersection_over_union(poly_1, poly_2): """ https://giou.stanford.edu/ Return the GIoU between two convex polygons. - (Convexity is currently required by _orient_box_counterclockwise and _intersection_nonsquare. - This requirement can be relaxed in the future if desired.) + (Convexity is required by _polygon_intersection.) - box_1 and box_2 are either shape (4,) with format [x1, y1, x2, y2] (defining a rectangle), + poly_1 and poly_2 are either shape (4,) with format [x1, y1, x2, y2] (defining a rectangle), or shape (N,2) where each element is of the form [x, y] (defining an arbitrary polygon) """ - box_1 = _validate_input_box_for_giou(box_1) - box_2 = _validate_input_box_for_giou(box_2) + poly_1 = _validate_input_polygon_for_giou(poly_1) + poly_2 = _validate_input_polygon_for_giou(poly_2) - # Find C: the area of convex hull enclosing both boxes - C = _area_of_convex_hull(np.vstack((box_1, box_2))) + # Find C: the area of convex hull enclosing both polygons + C = _area_of_convex_hull(np.vstack((poly_1, poly_2))) # GIoU = IoU - ((C - (A U B)) | / C) - intersection = _intersection_nonsquare(box_1, box_2) - union = _area_of_polygon(box_1) + _area_of_polygon(box_2) - intersection + intersection = _polygon_intersection(poly_1, poly_2) + union = _area_of_polygon(poly_1) + _area_of_polygon(poly_2) - intersection IoU = (intersection / union) if union > 0 else 0 c_term = (C - union) / C if C > 0 else 0 @@ -804,6 +879,33 @@ def object_detection_AP_per_class_by_min_giou_from_patch( mean=True, increment=0.1, ): + """ + Mean average precision for adversarial object detection, organizing results according to + how far boxes are from the patch. The motivation is that a patch is most effective against + nearby objects, and reporting mAP over a range of distances from the patch will give additional + insight into the attack's behavior. + + This function uses GIoU as a proxy for distance to patch. GIoU ranges from -1 to 1, with positive + values indicating overlap and smaller negative values indicating a greater distance. + + This function returns a dictionary, where the keys are the GIoU thresholds, and the values are mAP + for all objects within that range of the patch. + + y_list (list): of length equal to the number of input examples. Each element in the list + should be a dict with "labels" and "boxes" keys mapping to a numpy array of + shape (N,) and (N, 4) respectively where N = number of boxes. + y_pred_list (list): of length equal to the number of input examples. Each element in the + list should be a dict with "labels", "boxes", and "scores" keys mapping to a numpy + array of shape (N,), (N, 4), and (N,) respectively where N = number of boxes. + y_patch_metadata_list (list): of length equal to the number of input examples. Each element + is a dict with a "gs_coords" key mapping to a numpy array of shape (4,2). + class_list (list, optional): a list of classes, such that all predictions and ground-truths + with labels NOT in class_list are to be ignored. + mean: if False, returns a dict mapping each class to its average precision (AP) + if True, calls `mean_ap` on the AP dict and returns a encapsulating dict: + {'class': {: , ...}, 'mean': } + increment: the amount to increment the distance threshold for each reported mAP value + """ y_distances_list = [] y_pred_distances_list = [] From 24d3129584526cc986773c59300690c17c767aa2 Mon Sep 17 00:00:00 2001 From: Yusong Date: Thu, 22 Jun 2023 02:25:47 +0000 Subject: [PATCH 013/102] updated CARLA object detection attacks so that region perturbed in depth is the same as the region perturbed in RGB --- .../attacks/carla_obj_det_adversarial_patch.py | 3 +++ .../attacks/carla_obj_det_patch.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/armory/art_experimental/attacks/carla_obj_det_adversarial_patch.py b/armory/art_experimental/attacks/carla_obj_det_adversarial_patch.py index 3ea3068df..b571cf876 100644 --- a/armory/art_experimental/attacks/carla_obj_det_adversarial_patch.py +++ b/armory/art_experimental/attacks/carla_obj_det_adversarial_patch.py @@ -348,6 +348,9 @@ def _random_overlay( ).to(self.estimator.device) foreground_mask = torch.permute(foreground_mask, (2, 0, 1)) foreground_mask = torch.unsqueeze(foreground_mask, dim=0) + foreground_mask = ~( + ~foreground_mask * image_mask.bool() + ) # ensure area perturbed in depth is consistent with area perturbed in RGB # Adjust green screen brightness v_avg = ( diff --git a/armory/art_experimental/attacks/carla_obj_det_patch.py b/armory/art_experimental/attacks/carla_obj_det_patch.py index 5928cd292..b4eb6884d 100644 --- a/armory/art_experimental/attacks/carla_obj_det_patch.py +++ b/armory/art_experimental/attacks/carla_obj_det_patch.py @@ -770,6 +770,23 @@ def generate(self, x, y=None, y_patch_metadata=None): ) # (1,H,W,3) self.foreground = np.all(self.binarized_patch_mask == 255, axis=-1) self.foreground = np.expand_dims(self.foreground, (-1, 0)) # (1,H,W,1) + # ensure area perturbed in depth is consistent with area perturbed in RGB + h, _ = cv2.findHomography( + np.array( + [ + [0, 0], + [patch_width - 1, 0], + [patch_width - 1, patch_height - 1], + [0, patch_height - 1], + ] + ), + gs_coords, + ) + rgb_mask = np.ones((patch_height, patch_width, 3), dtype=np.float32) + rgb_mask = cv2.warpPerspective( + rgb_mask, h, (x.shape[2], x.shape[1]), cv2.INTER_CUBIC + ) + self.foreground = self.foreground * rgb_mask[:, :, 0:1] if y is None: patch = self.inner_generate( From e6c12c2124f74c8bae6638d2812baa2442d4fa42 Mon Sep 17 00:00:00 2001 From: Sterling Date: Thu, 22 Jun 2023 15:27:07 +0000 Subject: [PATCH 014/102] expand metric to report map by max giou as well as min --- armory/metrics/task.py | 62 ++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index 9d1815ac5..a3bd849d1 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -870,7 +870,7 @@ def video_tracking_mean_success_rate(y, y_pred): @populationwise -def object_detection_AP_per_class_by_min_giou_from_patch( +def object_detection_AP_per_class_by_giou_from_patch( y_list, y_pred_list, y_patch_metadata_list, @@ -888,8 +888,9 @@ def object_detection_AP_per_class_by_min_giou_from_patch( This function uses GIoU as a proxy for distance to patch. GIoU ranges from -1 to 1, with positive values indicating overlap and smaller negative values indicating a greater distance. - This function returns a dictionary, where the keys are the GIoU thresholds, and the values are mAP - for all objects within that range of the patch. + This function returns a dictionary with two subdictionaries, "cumulative_by_min_giou" and + "cumulative_by_max_giou". In each case, the keys are the GIoU thresholds, and the values the are + mAP for all objects with whose GIoU is greater (resp. lower) than the threshold. y_list (list): of length equal to the number of input examples. Each element in the list should be a dict with "labels" and "boxes" keys mapping to a numpy array of @@ -909,12 +910,16 @@ def object_detection_AP_per_class_by_min_giou_from_patch( y_distances_list = [] y_pred_distances_list = [] - result = {} + result = { + "cumulative_by_min_giou":{}, + "cumulative_by_max_giou":{}, + } for y, y_pred, metadata in zip(y_list, y_pred_list, y_patch_metadata_list): # For each image, use GIoU as proxy for distance between boxes and patch. # GIoU is positive if there is overlap. patch = metadata["gs_coords"] + assert patch.shape == (4,2) y_distances = [ _generalized_intersection_over_union(box, patch) for box in y["boxes"] ] @@ -924,29 +929,56 @@ def object_detection_AP_per_class_by_min_giou_from_patch( y_distances_list.append(y_distances) y_pred_distances_list.append(pred_distances) - # Compute mAP for boxes restricted to a minimum GIoU + # Compute mAP for boxes restricted to GIoU range for threshold in np.arange(0, -1 - increment, -increment): # Start with 0 -- all boxes that overlap the patch - y_list_ = [ + y_list_min = [ { - "boxes": y["boxes"][y_d > threshold], - "labels": y["labels"][y_d > threshold], + "boxes": y["boxes"][y_d >= threshold], + "labels": y["labels"][y_d >= threshold], } for y, y_d in zip(y_list, y_distances_list) ] - y_pred_list_ = [ + y_pred_list_min = [ { - "boxes": y["boxes"][y_d > threshold], - "labels": y["labels"][y_d > threshold], - "scores": y["scores"][y_d > threshold], + "boxes": y["boxes"][y_d >= threshold], + "labels": y["labels"][y_d >= threshold], + "scores": y["scores"][y_d >= threshold], } for y, y_d in zip(y_pred_list, y_pred_distances_list) ] + ap = object_detection_AP_per_class( + y_list_min, y_pred_list_min, iou_threshold, class_list, mean + ) + if not np.isnan(ap["mean"]): + # Possibly nan if there are no boxes on or near the patch + result["cumulative_by_min_giou"][threshold] = ap - # Add mAP to result dict - result[threshold] = object_detection_AP_per_class( - y_list_, y_pred_list_, iou_threshold, class_list, mean + + y_list_max = [ + { + "boxes": y["boxes"][y_d < threshold], + "labels": y["labels"][y_d < threshold], + } + for y, y_d in zip(y_list, y_distances_list) + ] + y_pred_list_max = [ + { + "boxes": y["boxes"][y_d < threshold], + "labels": y["labels"][y_d < threshold], + "scores": y["scores"][y_d < threshold], + } + for y, y_d in zip(y_pred_list, y_pred_distances_list) + ] + + ap = object_detection_AP_per_class( + y_list_max, y_pred_list_max, iou_threshold, class_list, mean ) + if not np.isnan(ap["mean"]): + # May be nan for smallest giou thresholds where there are no boxes + result["cumulative_by_max_giou"][threshold] = ap + + return result From 002388fabfcd5b9b7d9ffdf0f6e630800a6c80a3 Mon Sep 17 00:00:00 2001 From: Sterling Date: Thu, 22 Jun 2023 15:27:24 +0000 Subject: [PATCH 015/102] update scenario with new metric name --- armory/scenarios/carla_object_detection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/armory/scenarios/carla_object_detection.py b/armory/scenarios/carla_object_detection.py index c7609341a..cd20bb8e7 100644 --- a/armory/scenarios/carla_object_detection.py +++ b/armory/scenarios/carla_object_detection.py @@ -32,15 +32,15 @@ def load_metrics(self): # I will attempt to update that in the near future. meters = [ GlobalMeter( - "benign_AP_per_class_by_min_giou_from_patch", - metrics.get("object_detection_AP_per_class_by_min_giou_from_patch"), + "benign_AP_per_class_by_giou_from_patch", + metrics.get("object_detection_AP_per_class_by_giou_from_patch"), "scenario.y", "scenario.y_pred", "scenario.y_patch_metadata", ), GlobalMeter( - "adversarial_AP_per_class_by_min_giou_from_patch", - metrics.get("object_detection_AP_per_class_by_min_giou_from_patch"), + "adversarial_AP_per_class_by_giou_from_patch", + metrics.get("object_detection_AP_per_class_by_giou_from_patch"), "scenario.y", "scenario.y_pred_adv", "scenario.y_patch_metadata", From b902437925d372ba5be3455d0e63d16eaedec74a Mon Sep 17 00:00:00 2001 From: Sterling Date: Thu, 22 Jun 2023 15:27:55 +0000 Subject: [PATCH 016/102] formatting --- armory/metrics/task.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index a3bd849d1..f3f498910 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -889,7 +889,7 @@ def object_detection_AP_per_class_by_giou_from_patch( values indicating overlap and smaller negative values indicating a greater distance. This function returns a dictionary with two subdictionaries, "cumulative_by_min_giou" and - "cumulative_by_max_giou". In each case, the keys are the GIoU thresholds, and the values the are + "cumulative_by_max_giou". In each case, the keys are the GIoU thresholds, and the values the are mAP for all objects with whose GIoU is greater (resp. lower) than the threshold. y_list (list): of length equal to the number of input examples. Each element in the list @@ -911,15 +911,15 @@ def object_detection_AP_per_class_by_giou_from_patch( y_distances_list = [] y_pred_distances_list = [] result = { - "cumulative_by_min_giou":{}, - "cumulative_by_max_giou":{}, + "cumulative_by_min_giou": {}, + "cumulative_by_max_giou": {}, } for y, y_pred, metadata in zip(y_list, y_pred_list, y_patch_metadata_list): # For each image, use GIoU as proxy for distance between boxes and patch. # GIoU is positive if there is overlap. patch = metadata["gs_coords"] - assert patch.shape == (4,2) + assert patch.shape == (4, 2) y_distances = [ _generalized_intersection_over_union(box, patch) for box in y["boxes"] ] @@ -954,7 +954,6 @@ def object_detection_AP_per_class_by_giou_from_patch( # Possibly nan if there are no boxes on or near the patch result["cumulative_by_min_giou"][threshold] = ap - y_list_max = [ { "boxes": y["boxes"][y_d < threshold], @@ -978,8 +977,6 @@ def object_detection_AP_per_class_by_giou_from_patch( # May be nan for smallest giou thresholds where there are no boxes result["cumulative_by_max_giou"][threshold] = ap - - return result From d81d0357a9a7e6d298179ac87781cd447eee0694 Mon Sep 17 00:00:00 2001 From: Yusong Date: Sat, 24 Jun 2023 02:04:48 +0000 Subject: [PATCH 017/102] adding option to perform random targeted attacks for CARLA object detection --- .../attacks/carla_obj_det_patch.py | 9 +- armory/scenarios/carla_object_detection.py | 2 +- armory/utils/labels.py | 100 ++++++++++++++++++ ..._adversarialpatch_targeted_undefended.json | 71 +++++++++++++ ..._adversarialpatch_targeted_undefended.json | 71 +++++++++++++ 5 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json create mode 100644 scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json diff --git a/armory/art_experimental/attacks/carla_obj_det_patch.py b/armory/art_experimental/attacks/carla_obj_det_patch.py index b4eb6884d..a158e3ffe 100644 --- a/armory/art_experimental/attacks/carla_obj_det_patch.py +++ b/armory/art_experimental/attacks/carla_obj_det_patch.py @@ -450,7 +450,9 @@ def inner_generate( # type: ignore if self.depth_type == "log": depth_log = ( self.depth_perturbation - + np.sign(depth_gradients) * self.learning_rate_depth + + np.sign(depth_gradients) + * (1 - 2 * int(self.targeted)) + * self.learning_rate_depth ) perturbed_images = np.clip( images_depth + depth_log, self.min_depth, self.max_depth @@ -468,7 +470,10 @@ def inner_generate( # type: ignore self.depth_perturbation[:, :, :, 2], ).astype("float32") depth_linear = ( - depth_linear + np.sign(grads_linear) * self.learning_rate_depth + depth_linear + + np.sign(grads_linear) + * (1 - 2 * int(self.targeted)) + * self.learning_rate_depth ) images_depth_linear = rgb_depth_to_linear( diff --git a/armory/scenarios/carla_object_detection.py b/armory/scenarios/carla_object_detection.py index 4b4eea1b6..a6f9b0dd1 100644 --- a/armory/scenarios/carla_object_detection.py +++ b/armory/scenarios/carla_object_detection.py @@ -45,7 +45,7 @@ def run_attack(self): if self.use_label: y_target = y elif self.targeted: - y_target = self.label_targeter.generate(y) + y_target = self.label_targeter.generate(y, self.y_patch_metadata) else: y_target = None diff --git a/armory/utils/labels.py b/armory/utils/labels.py index 522a08d31..b62b49727 100644 --- a/armory/utils/labels.py +++ b/armory/utils/labels.py @@ -125,6 +125,106 @@ def generate(self, y): return targeted_y +class CARLAOverObjectDetectionRandomTargeter: + """ + Generate random annotations of CARLA objects, specifically pedestrians and vehicles, + using known statistics from the CARLA overhead dataset + """ + + def __init__(self, *, hallucination_per_label=[100, 100]): + # Object statistics from the CARLA Eval 6 overhead training data + self.hallucination_labels = [1, 2] # [pedestrian, vehicle] + self.hallucination_slopes = [0.27, 0.43] + self.hallucination_intercepts = [18.0, 40.0] + self.hallucination_min_widths = [6.0, 6.0] + self.hallucination_max_widths = [81.0, 277.0] + self.hallucination_width_means = [25.5, 56.3] + self.hallucination_width_stds = [13.0, 32.9] + self.hallucination_per_label = hallucination_per_label + self.X_MAX = 1280 # input resolution + self.Y_MAX = 960 + + if isinstance(hallucination_per_label, list): + for idx in range(len(hallucination_per_label)): + if ( + not isinstance(hallucination_per_label[idx], int) + or hallucination_per_label[idx] < 0 + ): + raise ValueError( + f"hallucination_per_label {hallucination_per_label[idx]} must be a nonnegative int" + ) + self.hallucination_per_label = hallucination_per_label + elif isinstance(hallucination_per_label, int): + if hallucination_per_label[idx] < 0: + raise ValueError( + f"hallucination_per_label {hallucination_per_label} must be a nonnegative int" + ) + self.hallucination_per_label = [hallucination_per_label] * len( + self.hallucination_labels + ) + else: + raise ValueError( + f"hallucination_per_label {hallucination_per_label} must be a nonnegative int or a list of nonnegative int" + ) + + def generate(self, y, y_patch_metadata): + from collections import defaultdict + + labels = self.hallucination_labels + slopes = self.hallucination_slopes + intercepts = self.hallucination_intercepts + min_widths = self.hallucination_min_widths + max_widths = self.hallucination_max_widths + width_means = self.hallucination_width_means + width_stds = self.hallucination_width_stds + num_hallucinations_per_class = self.hallucination_per_label + + y_out = [] + for i in range(len(y)): + gs_coords = y_patch_metadata[i]["gs_coords"] + x_min = min(gs_coords[:, 0]) + x_max = max(gs_coords[:, 0]) + y_min = min(gs_coords[:, 1]) + y_max = max(gs_coords[:, 1]) + + targeted_y = defaultdict(list) + for idx in range(len(labels)): + targeted_y["labels"].extend( + num_hallucinations_per_class[idx] * [labels[idx]] + ) + targeted_y["scores"].extend(num_hallucinations_per_class[idx] * [1.0]) + ws = np.minimum( + max_widths[idx], + np.maximum( + min_widths[idx], + width_means[idx] + + width_stds[idx] + * np.random.randn(num_hallucinations_per_class[idx]), + ), + ) + hs = intercepts[idx] + slopes[idx] * ws + lefts = np.random.uniform( + x_min, x_max, num_hallucinations_per_class[idx] + ) + tops = np.random.uniform( + y_min, y_max, num_hallucinations_per_class[idx] + ) + + for lt, tp, w, h in zip(lefts, tops, ws, hs): + x0 = int(lt) + y0 = int(tp) + x1 = int(min(self.X_MAX - 1, lt + w)) + y1 = int(min(self.Y_MAX - 1, tp + h)) + targeted_y["boxes"].extend([[x0, y0, x1, y1]]) + + targeted_y["labels"] = np.array(targeted_y["labels"]) + targeted_y["scores"] = np.array(targeted_y["scores"]) + targeted_y["boxes"] = np.array(targeted_y["boxes"]) + y_out.append(targeted_y) + + return y_out + + class MatchedTranscriptLengthTargeter: """ Targets labels of a length close to the true label diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json new file mode 100644 index 000000000..a6c67752d --- /dev/null +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json @@ -0,0 +1,71 @@ +{ + "_description": "CARLA single modality object detection, contributed by MITRE Corporation", + "adhoc": null, + "attack": { + "knowledge": "white", + "kwargs": { + "batch_size": 1, + "learning_rate": 0.003, + "max_iter": 1000, + "optimizer": "pgd", + "targeted": true, + "verbose": true + }, + "module": "armory.art_experimental.attacks.carla_obj_det_adversarial_patch", + "name": "CARLAAdversarialPatchPyTorch", + "targeted_labels": { + "kwargs": { + "hallucination_per_label": [300, 300] + }, + "module": "armory.utils.labels", + "name": "CARLAOverObjectDetectionRandomTargeter" + }, + "use_label": false + }, + "dataset": { + "batch_size": 1, + "eval_split": "dev", + "framework": "numpy", + "modality": "rgb", + "module": "armory.data.adversarial_datasets", + "name": "carla_over_obj_det_dev" + }, + "defense": null, + "metric": { + "means": true, + "perturbation": "l0", + "record_metric_per_sample": false, + "task": [ + "carla_od_AP_per_class", + "carla_od_disappearance_rate", + "carla_od_hallucinations_per_image", + "carla_od_misclassification_rate", + "carla_od_true_positive_rate", + "object_detection_mAP_tide" + ] + }, + "model": { + "fit": false, + "fit_kwargs": {}, + "model_kwargs": { + "num_classes": 3 + }, + "module": "armory.baseline_models.pytorch.carla_single_modality_object_detection_frcnn", + "name": "get_art_model", + "weights_file": "carla_rgb_weights_eval7and8.pt", + "wrapper_kwargs": {} + }, + "scenario": { + "kwargs": {}, + "module": "armory.scenarios.carla_object_detection", + "name": "CarlaObjectDetectionTask" + }, + "sysconfig": { + "docker_image": "twosixarmory/armory", + "external_github_repo": null, + "gpus": "all", + "output_dir": null, + "output_filename": null, + "use_gpu": false + } +} diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json new file mode 100644 index 000000000..50336eda8 --- /dev/null +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json @@ -0,0 +1,71 @@ +{ + "_description": "CARLA multimodality object detection, contributed by MITRE Corporation", + "adhoc": null, + "attack": { + "knowledge": "white", + "kwargs": { + "batch_size": 1, + "depth_delta_meters": 3, + "learning_rate": 0.003, + "learning_rate_depth": 0.005, + "max_iter": 1000, + "optimizer": "pgd", + "targeted": true, + "verbose": true + }, + "module": "armory.art_experimental.attacks.carla_obj_det_adversarial_patch", + "name": "CARLAAdversarialPatchPyTorch", + "targeted_labels": { + "kwargs": { + "hallucination_per_label": [300, 300] + }, + "module": "armory.utils.labels", + "name": "CARLAOverObjectDetectionRandomTargeter" + }, + "use_label": false + }, + "dataset": { + "batch_size": 1, + "eval_split": "dev", + "framework": "numpy", + "modality": "both", + "module": "armory.data.adversarial_datasets", + "name": "carla_over_obj_det_dev" + }, + "defense": null, + "metric": { + "means": true, + "perturbation": "l0", + "record_metric_per_sample": false, + "task": [ + "carla_od_AP_per_class", + "carla_od_disappearance_rate", + "carla_od_hallucinations_per_image", + "carla_od_misclassification_rate", + "carla_od_true_positive_rate", + "object_detection_mAP_tide" + ] + }, + "model": { + "fit": false, + "fit_kwargs": {}, + "model_kwargs": {}, + "module": "armory.baseline_models.pytorch.carla_multimodality_object_detection_frcnn", + "name": "get_art_model_mm", + "weights_file": "carla_multimodal_naive_weights_eval7and8.pt", + "wrapper_kwargs": {} + }, + "scenario": { + "kwargs": {}, + "module": "armory.scenarios.carla_object_detection", + "name": "CarlaObjectDetectionTask" + }, + "sysconfig": { + "docker_image": "twosixarmory/armory", + "external_github_repo": null, + "gpus": "all", + "output_dir": null, + "output_filename": null, + "use_gpu": false + } +} From bfd58b3bc31aebe21661c911043f645697ba9782 Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 30 Jun 2023 14:57:50 +0000 Subject: [PATCH 018/102] histogram version (not working) --- armory/metrics/task.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index f3f498910..b94f48fe6 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -977,6 +977,30 @@ def object_detection_AP_per_class_by_giou_from_patch( # May be nan for smallest giou thresholds where there are no boxes result["cumulative_by_max_giou"][threshold] = ap + histogram_bin_top = threshold + increment + if histogram_bin_top > 0: + histogram_bin_top = 1.0 + y_list_range = [ + { + "boxes": y["boxes"][(y_d >= threshold) & (y_d < histogram_bin_top)], + "labels": y["labels"][(y_d >= threshold) & (y_d < histogram_bin_top)], + } + for y, y_d in zip(y_list, y_distances_list) + ] + y_pred_list_range = [ + { + "boxes": y["boxes"][(y_d >= threshold) & (y_d < histogram_bin_top)], + "labels": y["labels"][(y_d >= threshold) & (y_d < histogram_bin_top)], + "scores": y["scores"][(y_d >= threshold) & (y_d < histogram_bin_top)], + } + for y, y_d in zip(y_pred_list, y_pred_distances_list) + ] + ap = object_detection_AP_per_class( + y_list_range, y_pred_list_range, iou_threshold, class_list, mean + ) + if not np.isnan(ap["mean"]): + result["histogram_"][threshold] = ap + return result From e5e93a864132d0fd048ee380331e6819552c713c Mon Sep 17 00:00:00 2001 From: Sterling Date: Mon, 3 Jul 2023 16:02:36 +0000 Subject: [PATCH 019/102] fix histogram version --- armory/metrics/task.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index b94f48fe6..c5fcda511 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -913,6 +913,7 @@ def object_detection_AP_per_class_by_giou_from_patch( result = { "cumulative_by_min_giou": {}, "cumulative_by_max_giou": {}, + "histogram_left": {}, } for y, y_pred, metadata in zip(y_list, y_pred_list, y_patch_metadata_list): @@ -920,12 +921,12 @@ def object_detection_AP_per_class_by_giou_from_patch( # GIoU is positive if there is overlap. patch = metadata["gs_coords"] assert patch.shape == (4, 2) - y_distances = [ + y_distances = np.array([ _generalized_intersection_over_union(box, patch) for box in y["boxes"] - ] - pred_distances = [ + ]) + pred_distances = np.array([ _generalized_intersection_over_union(box, patch) for box in y_pred["boxes"] - ] + ]) y_distances_list.append(y_distances) y_pred_distances_list.append(pred_distances) @@ -979,7 +980,7 @@ def object_detection_AP_per_class_by_giou_from_patch( histogram_bin_top = threshold + increment if histogram_bin_top > 0: - histogram_bin_top = 1.0 + histogram_bin_top = 1 y_list_range = [ { "boxes": y["boxes"][(y_d >= threshold) & (y_d < histogram_bin_top)], @@ -999,7 +1000,7 @@ def object_detection_AP_per_class_by_giou_from_patch( y_list_range, y_pred_list_range, iou_threshold, class_list, mean ) if not np.isnan(ap["mean"]): - result["histogram_"][threshold] = ap + result["histogram_left"][threshold] = ap return result From 0d8e9f8a77329031b7aa291ca98ac8a71d6a915e Mon Sep 17 00:00:00 2001 From: Yusong Date: Mon, 3 Jul 2023 19:05:25 +0000 Subject: [PATCH 020/102] json formatting --- ...carla_obj_det_adversarialpatch_targeted_undefended.json | 7 +++++-- ...et_multimodal_adversarialpatch_targeted_undefended.json | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json index a6c67752d..3fb73ae67 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json @@ -7,7 +7,7 @@ "batch_size": 1, "learning_rate": 0.003, "max_iter": 1000, - "optimizer": "pgd", + "optimizer": "pgd", "targeted": true, "verbose": true }, @@ -15,7 +15,10 @@ "name": "CARLAAdversarialPatchPyTorch", "targeted_labels": { "kwargs": { - "hallucination_per_label": [300, 300] + "hallucination_per_label": [ + 300, + 300 + ] }, "module": "armory.utils.labels", "name": "CARLAOverObjectDetectionRandomTargeter" diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json index 50336eda8..6141c5454 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json @@ -17,7 +17,10 @@ "name": "CARLAAdversarialPatchPyTorch", "targeted_labels": { "kwargs": { - "hallucination_per_label": [300, 300] + "hallucination_per_label": [ + 300, + 300 + ] }, "module": "armory.utils.labels", "name": "CARLAOverObjectDetectionRandomTargeter" From 638b35dbf4a5bde9a433ac931b1a5310a5830cf1 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 5 Jul 2023 13:09:24 +0000 Subject: [PATCH 021/102] minor correction --- armory/utils/labels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armory/utils/labels.py b/armory/utils/labels.py index b62b49727..fe01ba709 100644 --- a/armory/utils/labels.py +++ b/armory/utils/labels.py @@ -155,7 +155,7 @@ def __init__(self, *, hallucination_per_label=[100, 100]): ) self.hallucination_per_label = hallucination_per_label elif isinstance(hallucination_per_label, int): - if hallucination_per_label[idx] < 0: + if hallucination_per_label < 0: raise ValueError( f"hallucination_per_label {hallucination_per_label} must be a nonnegative int" ) From 68b11a106efa90904e84cba443199d6d2834dbde Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 5 Jul 2023 13:10:02 +0000 Subject: [PATCH 022/102] additional validation on input list --- armory/utils/labels.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/armory/utils/labels.py b/armory/utils/labels.py index fe01ba709..26e3ca64d 100644 --- a/armory/utils/labels.py +++ b/armory/utils/labels.py @@ -145,6 +145,10 @@ def __init__(self, *, hallucination_per_label=[100, 100]): self.Y_MAX = 960 if isinstance(hallucination_per_label, list): + if len(hallucination_per_label) != len(self.hallucination_labels): + raise ValueError( + f"hallucination_per_label list must have length {len(self.hallucination_labels)}" + ) for idx in range(len(hallucination_per_label)): if ( not isinstance(hallucination_per_label[idx], int) From de93cd509ebea67bea10c92b9461c042d5273329 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 5 Jul 2023 13:38:20 +0000 Subject: [PATCH 023/102] adding baseline results for carla mot test set --- docs/baseline_results/carla_mot_results.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/baseline_results/carla_mot_results.md b/docs/baseline_results/carla_mot_results.md index 8cac1dc1c..0577c7c11 100644 --- a/docs/baseline_results/carla_mot_results.md +++ b/docs/baseline_results/carla_mot_results.md @@ -3,6 +3,7 @@ This is the baseline evaluation for the multi-object tracking scenario. For single-object tracking, see [carla_video_tracking_results.md](../baseline_results/carla_video_tracking_results.md). For [dev data](https://github.com/twosixlabs/armory/blob/master/armory/data/adversarial/carla_mot_dev.py), results obtained using Armory v0.16.1. +[Test](https://github.com/twosixlabs/armory/blob/master/armory/data/adversarial/carla_mot_test.py) results obtained using Armory v0.17.2. | Data | Defended | Attack | Attack Parameters | Benign DetA / AssA / HOTA | Adversarial DetA / AssA / HOTA | Test Size | @@ -10,8 +11,11 @@ For [dev data](https://github.com/twosixlabs/armory/blob/master/armory/data/adve | Dev | no | Adversarial Patch | step_size=0.02, max_iter=100 | 0.49 / 0.62 / 0.55 | 0.18 / 0.57 / 0.32 | 20 | | Dev | no | Robust DPatch | step_size=0.002, max_iter=1000 | 0.49 / 0.62 / 0.55 | 0.39 / 0.59 / 0.48 | 20 | | Dev | yes | Robust DPatch | step_size=0.002, max_iter=1000 | 0.34 / 0.53 / 0.42 | 0.24 / 0.51 / 0.34 | 20 | +| Test | no | Adversarial Patch | step_size=0.02, max_iter=100 | 0.43 / 0.51 / 0.46 | 0.19 / 0.45 / 0.29 | 10 | +| Test | no | Robust DPatch | step_size=0.002, max_iter=1000 | 0.43 / 0.51 / 0.46 | 0.31 / 0.46 / 0.37 | 10 | +| Test | yes | Robust DPatch | step_size=0.002, max_iter=1000 | 0.32 / 0.45 / 0.38 | 0.22 / 0.41 / 0.30 | 10 | Defended results not available for Adversarial Patch attack because JPEG Compression defense is not implemented in PyTorch and so is not fully differentiable. Note that Robust DPatch is considerably slower than Adversarial Patch. -Find reference baseline configurations [here](https://github.com/twosixlabs/armory/tree/master/scenario_configs/eval6/carla_mot) \ No newline at end of file +Find reference baseline configurations [here](https://github.com/twosixlabs/armory/tree/master/scenario_configs/eval7/carla_mot) \ No newline at end of file From d9ce743c8b8b7fe59dcb0f5ce3131d1743f00098 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 5 Jul 2023 13:53:57 +0000 Subject: [PATCH 024/102] update art --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 36d5249a2..b7d6c5d5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ developer =[ ] engine = [ - "adversarial-robustness-toolbox == 1.14.1", + "adversarial-robustness-toolbox == 1.15.0", "Pillow", # Data dependencies "boto3", # Needed for armory.data.utils "botocore" , # Needed for armory.data.utils From 2b2453f17fc720e93d0ce832e109524eff3ddd85 Mon Sep 17 00:00:00 2001 From: Sterling Date: Mon, 10 Jul 2023 18:19:29 +0000 Subject: [PATCH 025/102] save copy of original config for results json --- armory/scenarios/scenario.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/armory/scenarios/scenario.py b/armory/scenarios/scenario.py index fc6a62781..c27e8ce1c 100644 --- a/armory/scenarios/scenario.py +++ b/armory/scenarios/scenario.py @@ -67,6 +67,9 @@ def __init__( self._check_config_and_cli_args( config, num_eval_batches, skip_benign, skip_attack, skip_misclassified ) + self.original_config = copy.deepcopy( + config + ) # Save original for output json, since some scenarios/models modify it self.config = config self.num_eval_batches = num_eval_batches self.skip_benign = bool(skip_benign) @@ -466,7 +469,7 @@ def prepare_results(self) -> dict: output = { "armory_version": armory.__version__, - "config": self.config, + "config": self.original_config, "results": self.results, "timestamp": int(self.time_stamp), } From ad4a7378a458b645423f61c8c38e31005d7fd95c Mon Sep 17 00:00:00 2001 From: Sterling Date: Thu, 13 Jul 2023 16:26:59 +0000 Subject: [PATCH 026/102] mute giou output from log --- armory/scenarios/carla_object_detection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/armory/scenarios/carla_object_detection.py b/armory/scenarios/carla_object_detection.py index cd20bb8e7..00210cd11 100644 --- a/armory/scenarios/carla_object_detection.py +++ b/armory/scenarios/carla_object_detection.py @@ -48,7 +48,6 @@ def load_metrics(self): ] for meter in meters: self.hub.connect_meter(meter) - self.hub.connect_writer(ResultsLogWriter(), meters=meters, default=False) def next(self): super().next() From ff69028115e41ea721184e07f824e8aa4e2393bf Mon Sep 17 00:00:00 2001 From: Sterling Date: Thu, 13 Jul 2023 16:34:23 +0000 Subject: [PATCH 027/102] remove unused import --- armory/metrics/task.py | 17 ++++++++++------- armory/scenarios/carla_object_detection.py | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index c5fcda511..a55e8a2b3 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -921,12 +921,15 @@ def object_detection_AP_per_class_by_giou_from_patch( # GIoU is positive if there is overlap. patch = metadata["gs_coords"] assert patch.shape == (4, 2) - y_distances = np.array([ - _generalized_intersection_over_union(box, patch) for box in y["boxes"] - ]) - pred_distances = np.array([ - _generalized_intersection_over_union(box, patch) for box in y_pred["boxes"] - ]) + y_distances = np.array( + [_generalized_intersection_over_union(box, patch) for box in y["boxes"]] + ) + pred_distances = np.array( + [ + _generalized_intersection_over_union(box, patch) + for box in y_pred["boxes"] + ] + ) y_distances_list.append(y_distances) y_pred_distances_list.append(pred_distances) @@ -982,7 +985,7 @@ def object_detection_AP_per_class_by_giou_from_patch( if histogram_bin_top > 0: histogram_bin_top = 1 y_list_range = [ - { + { "boxes": y["boxes"][(y_d >= threshold) & (y_d < histogram_bin_top)], "labels": y["labels"][(y_d >= threshold) & (y_d < histogram_bin_top)], } diff --git a/armory/scenarios/carla_object_detection.py b/armory/scenarios/carla_object_detection.py index bdbe20ef4..c1e45d9c1 100644 --- a/armory/scenarios/carla_object_detection.py +++ b/armory/scenarios/carla_object_detection.py @@ -7,7 +7,7 @@ from armory.instrument.export import ObjectDetectionExporter from armory.logs import log from armory.scenarios.object_detection import ObjectDetectionTask -from armory.instrument import GlobalMeter, ResultsLogWriter +from armory.instrument import GlobalMeter from armory import metrics From c5989f9c1258102355899d54b9f62f73b779476e Mon Sep 17 00:00:00 2001 From: Farhan Ahmed Date: Thu, 13 Jul 2023 13:01:39 -0700 Subject: [PATCH 028/102] add support for pre-computed fairness majority masks Signed-off-by: Farhan Ahmed --- armory/scenarios/poison.py | 103 ++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/armory/scenarios/poison.py b/armory/scenarios/poison.py index 593e509fe..614796096 100644 --- a/armory/scenarios/poison.py +++ b/armory/scenarios/poison.py @@ -389,14 +389,20 @@ def load_dataset(self, eval_split_default="test"): self.init_explanatory() def load_fairness_metrics(self): - explanatory_config = self.config["adhoc"].get("explanatory_model") - if explanatory_config: - self.explanatory_model = ExplanatoryModel.from_config(explanatory_config) + majority_masks_config = self.config["adhoc"].get("majority_masks") + if majority_masks_config: + # Attempt to load majority masks first, if provided + self.majority_masks = np.load(majority_masks_config) else: - # compute_fairness_metrics was true, but there is no explanatory config - raise ValueError( - "If computing fairness metrics, must specify 'explanatory_model' under 'adhoc'" - ) + # Load explanatory model otherwise + explanatory_config = self.config["adhoc"].get("explanatory_model") + if explanatory_config: + self.explanatory_model = ExplanatoryModel.from_config(explanatory_config) + else: + # compute_fairness_metrics was true, but there is no explanatory config + raise ValueError( + "If computing fairness metrics, must specify 'explanatory_model' under 'adhoc'" + ) if not self.check_run and self.use_filtering_defense: self.hub.connect_meter( @@ -574,48 +580,63 @@ def get_train_majority_mask_and_ceilings(self): """ get majority ceilings on unpoisoned part of train set """ - if self.explanatory_model is None: - raise ValueError("No explanatory model") - if self.fit_generator: - batch_size = self.fit_batch_size + if self.majority_masks is not None: + # Use pre-computed majority masks if provided + self.majority_mask_train_unpoisoned = self.majority_masks["train"] + self.majority_ceilings = None else: - batch_size = None - class_majority_mask = metrics.get("class_majority_mask") - activations = self.explanatory_model.get_activations( - self.x_poison[~self.poisoned], - batch_size=batch_size, - ) - ( - self.majority_mask_train_unpoisoned, - self.majority_ceilings, - ) = class_majority_mask( - activations, - self.y_poison[~self.poisoned], - ) + # Calculate majority masks from explanatory model otherwise + if self.explanatory_model is None: + raise ValueError("No explanatory model") + + if self.fit_generator: + batch_size = self.fit_batch_size + else: + batch_size = None + class_majority_mask = metrics.get("class_majority_mask") + activations = self.explanatory_model.get_activations( + self.x_poison[~self.poisoned], + batch_size=batch_size, + ) + ( + self.majority_mask_train_unpoisoned, + self.majority_ceilings, + ) = class_majority_mask( + activations, + self.y_poison[~self.poisoned], + ) + return self.majority_mask_train_unpoisoned, self.majority_ceilings def get_test_majority_mask(self): """ get majority ceilings on unpoisoned part of test set """ - if self.explanatory_model is None: - raise ValueError("No explanatory model") - if not hasattr(self, "majority_ceilings"): - raise ValueError("Must first call 'get_train_majority_mask_and_ceilings'") - class_majority_mask = metrics.get("class_majority_mask") - if self.fit_generator: - batch_size = self.fit_batch_size + if self.majority_masks is not None: + # Use pre-computed majority masks if provided + self.majority_mask_train_unpoisoned = self.majority_masks["train"] + self.majority_ceilings = None else: - batch_size = None - activations = self.explanatory_model.get_activations( - self.test_x, batch_size=batch_size - ) - # use copy of majority ceilings computed from train set - self.majority_mask_test_set, _ = class_majority_mask( - activations, - self.test_y, - majority_ceilings=copy.copy(self.majority_ceilings), - ) + # Calculate majority masks from explanatory model otherwise + if self.explanatory_model is None: + raise ValueError("No explanatory model") + if not hasattr(self, "majority_ceilings"): + raise ValueError("Must first call 'get_train_majority_mask_and_ceilings'") + + class_majority_mask = metrics.get("class_majority_mask") + if self.fit_generator: + batch_size = self.fit_batch_size + else: + batch_size = None + activations = self.explanatory_model.get_activations( + self.test_x, batch_size=batch_size + ) + # use copy of majority ceilings computed from train set + self.majority_mask_test_set, _ = class_majority_mask( + activations, + self.test_y, + majority_ceilings=copy.copy(self.majority_ceilings), + ) return self.majority_mask_test_set From c9e5be514d2b284c7ecf53996afcaa0339b3f288 Mon Sep 17 00:00:00 2001 From: Farhan Ahmed Date: Thu, 13 Jul 2023 14:37:04 -0700 Subject: [PATCH 029/102] fix docker bugs Signed-off-by: Farhan Ahmed --- armory/scenarios/poison.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/armory/scenarios/poison.py b/armory/scenarios/poison.py index 614796096..625d11caa 100644 --- a/armory/scenarios/poison.py +++ b/armory/scenarios/poison.py @@ -109,6 +109,7 @@ def __init__( raise NotImplementedError("triggered=False attacks are not implemented") self.triggered = triggered self.explanatory_model = None + self.majority_masks = None self.fit_generator = fit_generator def set_random_seed(self): @@ -385,19 +386,23 @@ def load_dataset(self, eval_split_default="test"): **self.dataset_kwargs, ) self.i = -1 - if self.explanatory_model is not None: + if self.explanatory_model is not None or self.majority_masks is not None: self.init_explanatory() def load_fairness_metrics(self): majority_masks_config = self.config["adhoc"].get("majority_masks") if majority_masks_config: # Attempt to load majority masks first, if provided + log.info("Using pre-computed majority masks...") self.majority_masks = np.load(majority_masks_config) else: # Load explanatory model otherwise + log.info("Using explanatory model...") explanatory_config = self.config["adhoc"].get("explanatory_model") if explanatory_config: - self.explanatory_model = ExplanatoryModel.from_config(explanatory_config) + self.explanatory_model = ExplanatoryModel.from_config( + explanatory_config + ) else: # compute_fairness_metrics was true, but there is no explanatory config raise ValueError( @@ -528,7 +533,7 @@ def run_benign(self): self.y_pred = y_pred self.source = source - if self.explanatory_model is not None: + if self.explanatory_model is not None or self.majority_masks is not None: self.run_explanatory() def run_attack(self): @@ -582,7 +587,9 @@ def get_train_majority_mask_and_ceilings(self): """ if self.majority_masks is not None: # Use pre-computed majority masks if provided - self.majority_mask_train_unpoisoned = self.majority_masks["train"] + self.majority_mask_train_unpoisoned = self.majority_masks["train"][ + ~self.poisoned + ] self.majority_ceilings = None else: # Calculate majority masks from explanatory model otherwise @@ -614,14 +621,15 @@ def get_test_majority_mask(self): """ if self.majority_masks is not None: # Use pre-computed majority masks if provided - self.majority_mask_train_unpoisoned = self.majority_masks["train"] - self.majority_ceilings = None + self.majority_mask_test_set = self.majority_masks["test"] else: # Calculate majority masks from explanatory model otherwise if self.explanatory_model is None: raise ValueError("No explanatory model") if not hasattr(self, "majority_ceilings"): - raise ValueError("Must first call 'get_train_majority_mask_and_ceilings'") + raise ValueError( + "Must first call 'get_train_majority_mask_and_ceilings'" + ) class_majority_mask = metrics.get("class_majority_mask") if self.fit_generator: @@ -684,7 +692,9 @@ def compute_explanatory(self): ) def finalize_results(self): - if getattr(self, "explanatory_model") and not self.check_run: + if ( + getattr(self, "explanatory_model") or getattr(self, "majority_masks") + ) and not self.check_run: self.finalize_explanatory() self.compute_explanatory() self.hub.close() From d8ab040480054679acd658932febd0607dfbd40f Mon Sep 17 00:00:00 2001 From: Yusong Date: Thu, 13 Jul 2023 23:45:03 +0000 Subject: [PATCH 030/102] minor update to datasets --- armory/data/adversarial/carla_mot_dev.py | 7 ++++--- armory/data/adversarial/carla_mot_test.py | 7 ++++--- armory/data/adversarial/carla_over_obj_det_dev.py | 5 +++-- armory/data/adversarial_datasets.py | 6 +++--- armory/data/url_checksums/carla_mot_dev.txt | 2 +- armory/data/url_checksums/carla_mot_test.txt | 2 +- armory/data/url_checksums/carla_over_obj_det_dev.txt | 2 +- 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/armory/data/adversarial/carla_mot_dev.py b/armory/data/adversarial/carla_mot_dev.py index aef392b2b..08ac59da5 100644 --- a/armory/data/adversarial/carla_mot_dev.py +++ b/armory/data/adversarial/carla_mot_dev.py @@ -22,15 +22,16 @@ } """ -_URLS = "https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_mot_dev_1.0.0.tar.gz" +_URLS = "carla_mot_dev_1.0.1.tar.gz" class CarlaMOTDev(tfds.core.GeneratorBasedBuilder): """DatasetBuilder for carla_mot_dev dataset.""" - VERSION = tfds.core.Version("1.0.0") + VERSION = tfds.core.Version("1.0.1") RELEASE_NOTES = { "1.0.0": "Initial release.", + "1.0.1": "Updated green screen coordinates so a patch appears static in each video.", } def _info(self) -> tfds.core.DatasetInfo: @@ -135,4 +136,4 @@ def create_gs_coords(gs_coords_files): for f in gs_coords_files: gs_coords.append(np.load(f)) - return np.array(gs_coords, dtype=np.int) + return np.array(gs_coords, dtype=int) diff --git a/armory/data/adversarial/carla_mot_test.py b/armory/data/adversarial/carla_mot_test.py index 7f3867b14..394162395 100644 --- a/armory/data/adversarial/carla_mot_test.py +++ b/armory/data/adversarial/carla_mot_test.py @@ -22,15 +22,16 @@ } """ -_URLS = "https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_mot_test_1.0.0.tar.gz" +_URLS = "carla_mot_test_1.0.1.tar.gz" class CarlaMOTTest(tfds.core.GeneratorBasedBuilder): """DatasetBuilder for carla_mot_test dataset.""" - VERSION = tfds.core.Version("1.0.0") + VERSION = tfds.core.Version("1.0.1") RELEASE_NOTES = { "1.0.0": "Initial release.", + "1.0.1": "Updated green screen coordinates so a patch appears static in each video.", } def _info(self) -> tfds.core.DatasetInfo: @@ -135,4 +136,4 @@ def create_gs_coords(gs_coords_files): for f in gs_coords_files: gs_coords.append(np.load(f)) - return np.array(gs_coords, dtype=np.int) + return np.array(gs_coords, dtype=int) diff --git a/armory/data/adversarial/carla_over_obj_det_dev.py b/armory/data/adversarial/carla_over_obj_det_dev.py index 54c857131..327bb8ea9 100644 --- a/armory/data/adversarial/carla_over_obj_det_dev.py +++ b/armory/data/adversarial/carla_over_obj_det_dev.py @@ -24,17 +24,18 @@ """ # fmt: off -_URLS = "https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_over_od_dev_2.0.0.tar.gz" +_URLS = "carla_over_od_dev_2.0.1.tar.gz" # fmt: on class CarlaOverObjDetDev(tfds.core.GeneratorBasedBuilder): """DatasetBuilder for carla_obj_det_dev dataset.""" - VERSION = tfds.core.Version("2.0.0") + VERSION = tfds.core.Version("2.0.1") RELEASE_NOTES = { "1.0.0": "Eval6 update from CarlaObjDetDev with images collected from overhead perspectives", "2.0.0": "Eval7 images collected from overhead perspectives, where patches on the sidewalk/street are constrained to +-0.03m depth perturbation and patches located elsewhere are constrained to +-3m", + "2.0.1": "Updated green screen coordinates in select images to fix imperfect patch coverage", } def _info(self) -> tfds.core.DatasetInfo: diff --git a/armory/data/adversarial_datasets.py b/armory/data/adversarial_datasets.py index ebf5f8d0f..98a87ef0a 100644 --- a/armory/data/adversarial_datasets.py +++ b/armory/data/adversarial_datasets.py @@ -721,7 +721,7 @@ def both_fn(batch): ) return datasets._generator_from_tfds( - "carla_over_obj_det_dev:2.0.0", + "carla_over_obj_det_dev:2.0.1", split=split, batch_size=batch_size, epochs=epochs, @@ -1192,7 +1192,7 @@ def carla_multi_object_tracking_dev( ) return datasets._generator_from_tfds( - "carla_mot_dev:1.0.0", + "carla_mot_dev:1.0.1", split=split, epochs=epochs, batch_size=batch_size, @@ -1253,7 +1253,7 @@ def carla_multi_object_tracking_test( ) return datasets._generator_from_tfds( - "carla_mot_test:1.0.0", + "carla_mot_test:1.0.1", split=split, epochs=epochs, batch_size=batch_size, diff --git a/armory/data/url_checksums/carla_mot_dev.txt b/armory/data/url_checksums/carla_mot_dev.txt index f8e5988e9..270b2b618 100644 --- a/armory/data/url_checksums/carla_mot_dev.txt +++ b/armory/data/url_checksums/carla_mot_dev.txt @@ -1 +1 @@ -https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_mot_dev_1.0.0.tar.gz 704303119 cdd4be9cd3bcb5c2f94a6628350f106deec6fdc7b6a9c05711309b2bcc814f3d \ No newline at end of file +carla_mot_dev_1.0.1.tar.gz 704304674 5748a67b6f2010ec42e22a4120161f9e8fb09a40df0ea8711afbbc60cc38fe96 \ No newline at end of file diff --git a/armory/data/url_checksums/carla_mot_test.txt b/armory/data/url_checksums/carla_mot_test.txt index 7f021e32c..859f886c1 100644 --- a/armory/data/url_checksums/carla_mot_test.txt +++ b/armory/data/url_checksums/carla_mot_test.txt @@ -1 +1 @@ -https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_mot_test_1.0.0.tar.gz 342710393 a4adbfae4c3d0c887ffa7b9605901deb142b7001bef7eb13e8ba21504fbf2453 +carla_mot_test_1.0.1.tar.gz 342711005 a75045f7363df45990ea6f2363b68cec3ca9610f5632833dc9217e6e01f5a3bc \ No newline at end of file diff --git a/armory/data/url_checksums/carla_over_obj_det_dev.txt b/armory/data/url_checksums/carla_over_obj_det_dev.txt index db10ac71e..cd915a11e 100644 --- a/armory/data/url_checksums/carla_over_obj_det_dev.txt +++ b/armory/data/url_checksums/carla_over_obj_det_dev.txt @@ -1 +1 @@ -https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_over_od_dev_2.0.0.tar.gz 64846940 248a04c61c8dc96f8bfd62e4a0977973b77962fdd64685ee7e4327e82c80140f \ No newline at end of file +carla_over_od_dev_2.0.1.tar.gz 64846910 499cd578a8c9ebb4dc2b8578e9ae3ac89c60bd57d55250327f683e164944a749 \ No newline at end of file From 487736c8e3995576c037bd007ea66a1c98ae5eb7 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Tue, 18 Jul 2023 13:24:21 -0500 Subject: [PATCH 031/102] add support for `.readthedocs.yaml` --- .readthedocs.yaml | 13 +++++++++++++ mkdocs.yml | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..83e68723e --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/builds.html for details + +# Required +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.8" + +mkdocs: + configuration: mkdocs.yml diff --git a/mkdocs.yml b/mkdocs.yml index 37f1e22c2..d187773f9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,8 @@ --- + site_name: Armory + +theme: readthedocs nav: - Home: index.md - Getting Started: getting_started.md @@ -29,4 +32,3 @@ nav: - Contributing: contributing.md - Style: style.md - Testing: developers/testing.md -theme: readthedocs From 6445f616eb6a7ab46d38793a0fbd6795cfe7b3c2 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Tue, 18 Jul 2023 14:35:47 -0500 Subject: [PATCH 032/102] update ` PyYAML` requirements --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7d6c5d5a..c7cc942c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "jsonschema", "loguru", "pytest", - "PyYAML", + "PyYAML; python_version >= '3.10'", "requests", "toml", ] From 426b9e53ccf55da723b4b6aae7ee5128e3fbe08c Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Tue, 18 Jul 2023 14:48:30 -0500 Subject: [PATCH 033/102] only install `pyyaml` when using python version > 3.10 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c7cc942c0..1720d037a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "jsonschema", "loguru", "pytest", - "PyYAML; python_version >= '3.10'", + "PyYAML; python_version < '3.10'", "requests", "toml", ] From 0e7dea94f6f6180f343e161732be50b438bdb129 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 11:46:12 +0000 Subject: [PATCH 034/102] allow more generic list of inputs to task_meter functions --- armory/instrument/config.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/armory/instrument/config.py b/armory/instrument/config.py index 5586a870e..adb560690 100644 --- a/armory/instrument/config.py +++ b/armory/instrument/config.py @@ -220,8 +220,7 @@ def task_meter( name, prefix, metric_kwargs, - y, - y_pred, + inputs, use_mean=True, record_final_only=True, suffix="", @@ -246,8 +245,7 @@ def task_meter( return GlobalMeter( f"{prefix}{name}{suffix}", metric, - y, - y_pred, + *inputs, final_kwargs=metric_kwargs, final_result_formatter=result_formatter, ) @@ -268,8 +266,7 @@ def task_meter( return Meter( f"{prefix}{name}{suffix}", metric, - y, - y_pred, + *inputs, metric_kwargs=metric_kwargs, result_formatter=result_formatter, final=final, @@ -304,8 +301,8 @@ def task_meters( name, prefix, metric_kwargs, - y, - y_pred, + [y, + y_pred], use_mean=use_mean, record_final_only=record_final_only, suffix=suffix, From 51432cb477a98e21cbbd436aa27f4a0d85154037 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 11:46:47 +0000 Subject: [PATCH 035/102] metrics logger add_custom_task function for metrics with unique inputs --- armory/instrument/config.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/armory/instrument/config.py b/armory/instrument/config.py index adb560690..fab2beac1 100644 --- a/armory/instrument/config.py +++ b/armory/instrument/config.py @@ -144,6 +144,23 @@ def add_targeted_tasks(self): ) self.connect(meters, writer) + def add_custom_task(self, task, inputs, prefix, metric_kwargs=None, use_mean=True, record_final_only=True, suffix="", load_writer=True): + meter = task_meter( + task, + prefix, + metric_kwargs, + inputs, + use_mean=use_mean, + record_final_only=record_final_only, + suffix=suffix + ) + if load_writer: + writer = ResultsLogWriter( + format_string="{name}: {result}" + ) + else: writer = None + self.connect([meter], writer) + def add_tasks_wrt_benign_predictions(self): """ Measure adversarial predictions w.r.t. benign predictions From c2274cc95d346f92bdb73dca7af60ccf29289613 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 11:48:18 +0000 Subject: [PATCH 036/102] formatting --- armory/instrument/config.py | 24 ++++++++++----- armory/scenarios/carla_object_detection.py | 34 ++++++++-------------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/armory/instrument/config.py b/armory/instrument/config.py index fab2beac1..47a175b56 100644 --- a/armory/instrument/config.py +++ b/armory/instrument/config.py @@ -144,7 +144,17 @@ def add_targeted_tasks(self): ) self.connect(meters, writer) - def add_custom_task(self, task, inputs, prefix, metric_kwargs=None, use_mean=True, record_final_only=True, suffix="", load_writer=True): + def add_custom_task( + self, + task, + inputs, + prefix, + metric_kwargs=None, + use_mean=True, + record_final_only=True, + suffix="", + load_writer=True, + ): meter = task_meter( task, prefix, @@ -152,13 +162,12 @@ def add_custom_task(self, task, inputs, prefix, metric_kwargs=None, use_mean=Tru inputs, use_mean=use_mean, record_final_only=record_final_only, - suffix=suffix + suffix=suffix, ) if load_writer: - writer = ResultsLogWriter( - format_string="{name}: {result}" - ) - else: writer = None + writer = ResultsLogWriter(format_string="{name}: {result}") + else: + writer = None self.connect([meter], writer) def add_tasks_wrt_benign_predictions(self): @@ -318,8 +327,7 @@ def task_meters( name, prefix, metric_kwargs, - [y, - y_pred], + [y, y_pred], use_mean=use_mean, record_final_only=record_final_only, suffix=suffix, diff --git a/armory/scenarios/carla_object_detection.py b/armory/scenarios/carla_object_detection.py index c1e45d9c1..5eaf580ff 100644 --- a/armory/scenarios/carla_object_detection.py +++ b/armory/scenarios/carla_object_detection.py @@ -7,8 +7,6 @@ from armory.instrument.export import ObjectDetectionExporter from armory.logs import log from armory.scenarios.object_detection import ObjectDetectionTask -from armory.instrument import GlobalMeter -from armory import metrics class CarlaObjectDetectionTask(ObjectDetectionTask): @@ -28,26 +26,18 @@ def load_dataset(self): def load_metrics(self): super().load_metrics() - # These metrics are loaded here manually because y_patch_metadata cannot be passed through the default MetricsLogger loading code. - # I will attempt to update that in the near future. - meters = [ - GlobalMeter( - "benign_AP_per_class_by_giou_from_patch", - metrics.get("object_detection_AP_per_class_by_giou_from_patch"), - "scenario.y", - "scenario.y_pred", - "scenario.y_patch_metadata", - ), - GlobalMeter( - "adversarial_AP_per_class_by_giou_from_patch", - metrics.get("object_detection_AP_per_class_by_giou_from_patch"), - "scenario.y", - "scenario.y_pred_adv", - "scenario.y_patch_metadata", - ), - ] - for meter in meters: - self.hub.connect_meter(meter) + self.metrics_logger.add_custom_task( + "object_detection_AP_per_class_by_giou_from_patch", + ["scenario.y", "scenario.y_pred", "scenario.y_patch_metadata"], + "benign_", + load_writer=False, + ) + self.metrics_logger.add_custom_task( + "object_detection_AP_per_class_by_giou_from_patch", + ["scenario.y", "scenario.y_pred_adv", "scenario.y_patch_metadata"], + "adversarial_", + load_writer=False, + ) def next(self): super().next() From 6d45b137f2a9aef9615d5832b17c447f0ad487df Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 11:49:15 +0000 Subject: [PATCH 037/102] stub for plotting utility --- armory/postprocessing/plot_distance_aware_carla_metric.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 armory/postprocessing/plot_distance_aware_carla_metric.py diff --git a/armory/postprocessing/plot_distance_aware_carla_metric.py b/armory/postprocessing/plot_distance_aware_carla_metric.py new file mode 100644 index 000000000..3d8aeb65e --- /dev/null +++ b/armory/postprocessing/plot_distance_aware_carla_metric.py @@ -0,0 +1,8 @@ +from matplotlib import pyplot as plt +import json +import numpy as np + + +def plot_mAP_by_giou_from_patch(json_filepath, show=True, output_filepath=None): + + pass From cae1fd9a5f609835fbaa0d20a9a469056c6e2d1c Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 12:32:20 +0000 Subject: [PATCH 038/102] restructure models dir and add init --- .../baseline_models/model_configs/__init__.py | 23 +++++++++++++++++++ .../{pytorch => model_configs}/yolov3.cfg | 0 2 files changed, 23 insertions(+) create mode 100644 armory/baseline_models/model_configs/__init__.py rename armory/baseline_models/{pytorch => model_configs}/yolov3.cfg (100%) diff --git a/armory/baseline_models/model_configs/__init__.py b/armory/baseline_models/model_configs/__init__.py new file mode 100644 index 000000000..911e6d526 --- /dev/null +++ b/armory/baseline_models/model_configs/__init__.py @@ -0,0 +1,23 @@ +from pathlib import Path + +from armory.data.utils import maybe_download_weights_from_s3 +from armory.logs import log + +CONFIGS_DIR = Path(__file__).parent + + +def get_path(filename) -> str: + """ + Get the absolute path of the provided config. Ordering priority is: + 1) Check directly for provided filepath + 2) Load from `model_configs` directory + 3) Attempt to download from s3 as a weights file + """ + filename = Path(filename) + if filename.is_file(): + return str(filename) + cfgs_path = CONFIGS_DIR / filename + if cfgs_path.is_file(): + return str(cfgs_path) + + return maybe_download_weights_from_s3(filename) diff --git a/armory/baseline_models/pytorch/yolov3.cfg b/armory/baseline_models/model_configs/yolov3.cfg similarity index 100% rename from armory/baseline_models/pytorch/yolov3.cfg rename to armory/baseline_models/model_configs/yolov3.cfg From 3cddebf14df5aadadd7f814ae7ecb5472bd5c51a Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 12:32:57 +0000 Subject: [PATCH 039/102] find correct cfg path --- armory/baseline_models/pytorch/yolov3.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/armory/baseline_models/pytorch/yolov3.py b/armory/baseline_models/pytorch/yolov3.py index d8edf4ad4..2b0400f2b 100644 --- a/armory/baseline_models/pytorch/yolov3.py +++ b/armory/baseline_models/pytorch/yolov3.py @@ -5,6 +5,7 @@ from art.estimators.object_detection import PyTorchYolo from pytorchyolo.utils.loss import compute_loss from pytorchyolo.models import load_model +from armory.baseline_models import model_configs DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") @@ -30,6 +31,7 @@ def get_art_model( model_kwargs: dict, wrapper_kwargs: dict, weights_path: Optional[str] = None ) -> PyTorchYolo: + model_kwargs["model_path"] = model_configs.get_path(model_kwargs["model_path"]) model = load_model(weights_path=weights_path, **model_kwargs) model_wrapper = Yolo(model) From f1b611f6560239dc112dd75ac2ad6de25efd02e3 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 12:33:18 +0000 Subject: [PATCH 040/102] update json configs --- scenario_configs/eval7/poisoning/obj_det_dlbd_GMA.json | 2 +- scenario_configs/eval7/poisoning/obj_det_dlbd_ODA.json | 2 +- scenario_configs/eval7/poisoning/obj_det_dlbd_OGA.json | 2 +- scenario_configs/eval7/poisoning/obj_det_dlbd_RMA.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scenario_configs/eval7/poisoning/obj_det_dlbd_GMA.json b/scenario_configs/eval7/poisoning/obj_det_dlbd_GMA.json index 357f257f2..b2aef9d8c 100644 --- a/scenario_configs/eval7/poisoning/obj_det_dlbd_GMA.json +++ b/scenario_configs/eval7/poisoning/obj_det_dlbd_GMA.json @@ -50,7 +50,7 @@ "fit": true, "fit_kwargs": {}, "model_kwargs": { - "model_path": "armory/baseline_models/pytorch/yolov3.cfg" + "model_path": "yolov3.cfg" }, "module": "armory.baseline_models.pytorch.yolov3", "name": "get_art_model", diff --git a/scenario_configs/eval7/poisoning/obj_det_dlbd_ODA.json b/scenario_configs/eval7/poisoning/obj_det_dlbd_ODA.json index 213647f35..1513f9c30 100644 --- a/scenario_configs/eval7/poisoning/obj_det_dlbd_ODA.json +++ b/scenario_configs/eval7/poisoning/obj_det_dlbd_ODA.json @@ -50,7 +50,7 @@ "fit": true, "fit_kwargs": {}, "model_kwargs": { - "model_path": "armory/baseline_models/pytorch/yolov3.cfg" + "model_path": "yolov3.cfg" }, "module": "armory.baseline_models.pytorch.yolov3", "name": "get_art_model", diff --git a/scenario_configs/eval7/poisoning/obj_det_dlbd_OGA.json b/scenario_configs/eval7/poisoning/obj_det_dlbd_OGA.json index 8057fe130..70208b527 100644 --- a/scenario_configs/eval7/poisoning/obj_det_dlbd_OGA.json +++ b/scenario_configs/eval7/poisoning/obj_det_dlbd_OGA.json @@ -52,7 +52,7 @@ "fit": true, "fit_kwargs": {}, "model_kwargs": { - "model_path": "armory/baseline_models/pytorch/yolov3.cfg" + "model_path": "yolov3.cfg" }, "module": "armory.baseline_models.pytorch.yolov3", "name": "get_art_model", diff --git a/scenario_configs/eval7/poisoning/obj_det_dlbd_RMA.json b/scenario_configs/eval7/poisoning/obj_det_dlbd_RMA.json index 3d62d5283..cc71d791c 100644 --- a/scenario_configs/eval7/poisoning/obj_det_dlbd_RMA.json +++ b/scenario_configs/eval7/poisoning/obj_det_dlbd_RMA.json @@ -51,7 +51,7 @@ "fit": true, "fit_kwargs": {}, "model_kwargs": { - "model_path": "armory/baseline_models/pytorch/yolov3.cfg" + "model_path": "yolov3.cfg" }, "module": "armory.baseline_models.pytorch.yolov3", "name": "get_art_model", From 469e88995bd176be1aeb19adc621136c02b1f882 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Wed, 19 Jul 2023 12:41:29 -0500 Subject: [PATCH 041/102] remove unused dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1720d037a..1327cebcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ dependencies = [ "jsonschema", "loguru", "pytest", - "PyYAML; python_version < '3.10'", "requests", "toml", ] From d4ca3442f34a31209ea8fcdad61cdff747b40229 Mon Sep 17 00:00:00 2001 From: Yusong Date: Wed, 19 Jul 2023 18:07:47 +0000 Subject: [PATCH 042/102] added CARLA overhead object detection test data --- .../adversarial/carla_over_obj_det_test.py | 26 +++++++++++++++---- armory/data/adversarial_datasets.py | 2 +- .../url_checksums/carla_over_obj_det_test.txt | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/armory/data/adversarial/carla_over_obj_det_test.py b/armory/data/adversarial/carla_over_obj_det_test.py index 54a1655ff..b082c9b72 100644 --- a/armory/data/adversarial/carla_over_obj_det_test.py +++ b/armory/data/adversarial/carla_over_obj_det_test.py @@ -24,16 +24,17 @@ """ # fmt: off -_URLS = "https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_over_od_test_1.0.0.tar.gz" +_URLS = "carla_over_od_test_2.0.0.tar.gz" # fmt: on class CarlaOverObjDetTest(tfds.core.GeneratorBasedBuilder): """DatasetBuilder for carla_obj_det_test dataset.""" - VERSION = tfds.core.Version("1.0.0") + VERSION = tfds.core.Version("2.0.0") RELEASE_NOTES = { "1.0.0": "Eval6 update from CarlaObjDetTest with images collected from overhead perspectives", + "2.0.0": "Eval7 images collected from overhead perspectives, where patches on the sidewalk/street are constrained to +-0.03m depth perturbation and patches located elsewhere are constrained to +-3m", } def _info(self) -> tfds.core.DatasetInfo: @@ -86,6 +87,9 @@ def _info(self) -> tfds.core.DatasetInfo: # mask[x,y] == 1 indicates patch pixel; 0 otherwise "mask": tfds.features.Image(shape=(960, 1280, 3)), "avg_patch_depth": tfds.features.Tensor(shape=(), dtype=tf.float64), + "max_depth_perturb_meters": tfds.features.Tensor( + shape=(), dtype=tf.float64 + ), } ), } @@ -102,9 +106,13 @@ def _split_generators(self, dl_manager: tfds.download.DownloadManager): path = dl_manager.download_and_extract(_URLS) return [ tfds.core.SplitGenerator( - name="test", - gen_kwargs={"path": os.path.join(path, "test")}, - ) + name="test_hallucination", + gen_kwargs={"path": os.path.join(path, "test/hallucination")}, + ), + tfds.core.SplitGenerator( + name="test_disappearance", + gen_kwargs={"path": os.path.join(path, "test/disappearance")}, + ), ] def _generate_examples(self, path): @@ -143,6 +151,13 @@ def _generate_examples(self, path): image_depth = deepcopy(image_rgb) image_depth["file_name"] = fpath_depth + # Set depth perturbation bound based on split + if "hallucination" in path: + max_depth_perturb_meters = 3.0 + + else: + max_depth_perturb_meters = 0.03 + # get object annotations for each image annotations = cocoanno.get_annotations(image_rgb["id"]) @@ -188,6 +203,7 @@ def build_bbox(x, y, width, height): ) ), "mask": os.path.join(path, foreground_mask_folder, fname), + "max_depth_perturb_meters": max_depth_perturb_meters, }, } diff --git a/armory/data/adversarial_datasets.py b/armory/data/adversarial_datasets.py index ebf5f8d0f..a4b1d2c68 100644 --- a/armory/data/adversarial_datasets.py +++ b/armory/data/adversarial_datasets.py @@ -789,7 +789,7 @@ def both_fn(batch): ) return datasets._generator_from_tfds( - "carla_over_obj_det_test:1.0.0", + "carla_over_obj_det_test:2.0.0", split=split, batch_size=batch_size, epochs=epochs, diff --git a/armory/data/url_checksums/carla_over_obj_det_test.txt b/armory/data/url_checksums/carla_over_obj_det_test.txt index a7bd81b02..6bc12a691 100644 --- a/armory/data/url_checksums/carla_over_obj_det_test.txt +++ b/armory/data/url_checksums/carla_over_obj_det_test.txt @@ -1 +1 @@ -https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_over_od_test_1.0.0.tar.gz 54806190 fbf713db40b29fa23cbc9ebe11c0e5269c283251d63bbe05b635326a53b6f342 \ No newline at end of file +carla_over_od_test_2.0.0.tar.gz 153756875 96f5e4e0846c8e4b970691ff750c0ec4631dd92ac527ddd633eb9b0bbae648a4 \ No newline at end of file From 7e2634c29caa3bf4c0f4d0d8a3cf3b015b47f7aa Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Wed, 19 Jul 2023 13:12:44 -0500 Subject: [PATCH 043/102] remove `readthedocs` --- .readthedocs.yaml | 13 ------------- mkdocs.yml | 34 ---------------------------------- pyproject.toml | 2 -- 3 files changed, 49 deletions(-) delete mode 100644 .readthedocs.yaml delete mode 100644 mkdocs.yml diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index 83e68723e..000000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/builds.html for details - -# Required -version: 2 - -build: - os: ubuntu-22.04 - tools: - python: "3.8" - -mkdocs: - configuration: mkdocs.yml diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index d187773f9..000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- - -site_name: Armory - -theme: readthedocs -nav: - - Home: index.md - - Getting Started: getting_started.md - - FAQs: faqs.md - - Environment: - - Command Line: command_line.md - - Docker: docker.md - - No-Docker: no_docker_mode.md - - Experiments: - - Configuration Files: configuration_files.md - - Baseline Models: baseline_models.md - - External Repos: external_repos.md - - Sweep Attacks: sweep_attacks.md - - Evaluation Scenarios: - - Evasion: scenarios.md - - Poisoning: poisoning.md - - Data: - - Datasets: datasets.md - - Integrating TFDS Datasets: integrate_tensorflow_datasets.md - - Adversarial Datasets: adversarial_datasets.md - - Dataset Licensing: dataset_licensing.md - - Measurment and Instrumentation: - - Exporting Data: exporting_data.md - - Logging: logging.md - - Metrics: metrics.md - - Developers: - - Contributing: contributing.md - - Style: style.md - - Testing: developers/testing.md diff --git a/pyproject.toml b/pyproject.toml index 1327cebcc..ceb0aa232 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,12 +56,10 @@ armory = "armory.__main__:main" developer =[ "hatch", # build tool "wheel", # build tool - "mkdocs", "black[jupyter]==22.*", "isort", "flake8", "bandit[toml]", # code scanning - "hydra-core", # configuration ] engine = [ From 14943dd52ce9641ffc415856b92edc5305b60530 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Wed, 19 Jul 2023 13:57:59 -0500 Subject: [PATCH 044/102] remove `bandit` --- .github/workflows/1-scan-lint-build.yml | 11 ----------- pyproject.toml | 22 +--------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/.github/workflows/1-scan-lint-build.yml b/.github/workflows/1-scan-lint-build.yml index 268a77eb8..524b232df 100755 --- a/.github/workflows/1-scan-lint-build.yml +++ b/.github/workflows/1-scan-lint-build.yml @@ -80,17 +80,6 @@ jobs: fi - - name: 🦹‍♂️ Scanning with Bandit - run: | - bandit \ - -v \ - -f txt \ - -r ./armory \ - -c "pyproject.toml" \ - --output /tmp/artifacts/bandit_scan.txt \ - || $( exit 0 ); echo $? - - - name: 🖋️ mypy Type Checking run: | python3 -m pip install mypy diff --git a/pyproject.toml b/pyproject.toml index ceb0aa232..bce50c4ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ build-backend = "hatchling.build" requires = [ "hatchling>=1.10.0", "hatch-vcs", + "cython<3.0.0", ] @@ -39,7 +40,6 @@ dependencies = [ "loguru", "pytest", "requests", - "toml", ] [project.urls] @@ -59,7 +59,6 @@ developer =[ "black[jupyter]==22.*", "isort", "flake8", - "bandit[toml]", # code scanning ] engine = [ @@ -183,16 +182,6 @@ version_scheme = "post-release" [tool.setuptools_scm] # the presence of this empty block stops setuptools_scm from complaining -[tool.bandit] -recursive = true -targets = [ "armory" ] -skips = [ - "B101", # Ignore defensive `assert`s (especially useful for mypy) - "B404", # Ignore warnings about importing subprocess - "B603", # Ignore warnings about calling subprocess.Popen without shell=True - "B607", # Ignore warnings about calling subprocess.Popen without a full path to executable -] - [tool.pytest.ini_options] addopts = "-ra -q" log_level = "ERROR" @@ -219,15 +208,6 @@ markers = [ # ignore::Warning:art* -# ------------ pre-commit hooks ------------ -[tool.vulture] -paths = ["src"] -ignore_decorators = ["#~vulture.ignore~#"] -min_confidence = 80 -make_whitelist = true -sort_by_size = true -verbose = true - [tool.isort] profile = "black" force_sort_within_sections = true From b4d629eba40db41a15f9f79f4babfd91e8c81a37 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 19:54:21 +0000 Subject: [PATCH 045/102] update default split --- armory/data/adversarial_datasets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armory/data/adversarial_datasets.py b/armory/data/adversarial_datasets.py index a4b1d2c68..400ef3c93 100644 --- a/armory/data/adversarial_datasets.py +++ b/armory/data/adversarial_datasets.py @@ -739,7 +739,7 @@ def both_fn(batch): def carla_over_obj_det_test( - split: str = "test", + split: str = "test_hallucination", epochs: int = 1, batch_size: int = 1, dataset_dir: str = None, From 2a8ef759c161389c1a291d0d551d32c087329749 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 19:54:59 +0000 Subject: [PATCH 046/102] update url checksum file with url --- armory/data/url_checksums/carla_over_obj_det_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armory/data/url_checksums/carla_over_obj_det_test.txt b/armory/data/url_checksums/carla_over_obj_det_test.txt index 6bc12a691..d85d9e188 100644 --- a/armory/data/url_checksums/carla_over_obj_det_test.txt +++ b/armory/data/url_checksums/carla_over_obj_det_test.txt @@ -1 +1 @@ -carla_over_od_test_2.0.0.tar.gz 153756875 96f5e4e0846c8e4b970691ff750c0ec4631dd92ac527ddd633eb9b0bbae648a4 \ No newline at end of file +https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_over_od_test_2.0.0.tar.gz 153756875 96f5e4e0846c8e4b970691ff750c0ec4631dd92ac527ddd633eb9b0bbae648a4 \ No newline at end of file From 135a6f8c9c63bc38b860b94fd12b90988327367e Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 19:55:48 +0000 Subject: [PATCH 047/102] update url --- armory/data/adversarial/carla_over_obj_det_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armory/data/adversarial/carla_over_obj_det_test.py b/armory/data/adversarial/carla_over_obj_det_test.py index b082c9b72..2824b520b 100644 --- a/armory/data/adversarial/carla_over_obj_det_test.py +++ b/armory/data/adversarial/carla_over_obj_det_test.py @@ -24,7 +24,7 @@ """ # fmt: off -_URLS = "carla_over_od_test_2.0.0.tar.gz" +_URLS = "https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_over_od_test_2.0.0.tar.gz" # fmt: on From 1cb1e6b9ceeae04b49874d06a8faf669328a3128 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 19:56:08 +0000 Subject: [PATCH 048/102] update cached checksum --- armory/data/cached_s3_checksums/carla_over_obj_det_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armory/data/cached_s3_checksums/carla_over_obj_det_test.txt b/armory/data/cached_s3_checksums/carla_over_obj_det_test.txt index 21a17be38..92439076b 100644 --- a/armory/data/cached_s3_checksums/carla_over_obj_det_test.txt +++ b/armory/data/cached_s3_checksums/carla_over_obj_det_test.txt @@ -1 +1 @@ -armory-public-data carla/carla_over_od_test_cached_1.0.0.tar.gz 53217575 2023fcb2274623ef3a351a2035a5412a0a6b5a8b72899b915948dc8358dfea10 \ No newline at end of file +armory-public-data carla/carla_over_od_test_cached_2.0.0.tar.gz 148538130 fd498cb8c8c005dd5c70a0f9914d92b59aee273eee1a9005f4fcdf44173cd0a9 \ No newline at end of file From 192d61e85c5d2ec4039ed332de016914aa39f11c Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 19 Jul 2023 19:56:28 +0000 Subject: [PATCH 049/102] update docs --- docs/adversarial_datasets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adversarial_datasets.md b/docs/adversarial_datasets.md index 88a4d848c..8ae86abb6 100644 --- a/docs/adversarial_datasets.md +++ b/docs/adversarial_datasets.md @@ -31,7 +31,7 @@ from Two Six's public S3 dataset repository. | "carla_obj_det_dev" | ["dev"] | [CARLA Simulator Object Detection](https://carla.org) | dev | (nb=1, 960, 1280, 3 or 6) | uint8 | 2-tuple | 31 images | | "carla_obj_det_test" | ["test"] | [CARLA Simulator Object Detection](https://carla.org) | test | (nb=1, 960, 1280, 3 or 6) | uint8 | 2-tuple | 30 images | | "carla_over_obj_det_dev" | ["dev"] | [CARLA Simulator Object Detection](https://carla.org) | dev | (nb=1, 960, 1280, 3 or 6) | uint8 | 2-tuple | 20 images | -| "carla_over_obj_det_test" | ["test"] | [CARLA Simulator Object Detection](https://carla.org) | test | (nb=1, 960, 1280, 3 or 6) |uint8 | 2-tuple | 15 images | +| "carla_over_obj_det_test" | ["test_hallucination", "test_disappearance"] | [CARLA Simulator Object Detection](https://carla.org) | test | (nb=1, 960, 1280, 3 or 6) |uint8 | 2-tuple | 50 images (25 per split) | | "carla_video_tracking_dev" | ["dev"] | [CARLA Simulator Video Tracking](https://carla.org) | dev | (nb=1, num_frames, 960, 1280, 3) | uint8 | 2-tuple | 20 videos | | "carla_video_tracking_test" | ["test"] | [CARLA Simulator Video Tracking](https://carla.org) | test | (nb=1, num_frames, 960, 1280, 3) | uint8 | 2-tuple | 20 videos | | "carla_multi_object_tracking_dev" | ["dev"] | [CARLA Simulator Multi-object Video Tracking](https://carla.org) | dev | (nb=1, num_frames, 960, 1280, 3) | uint8 | 2-tuple | 20 videos | From bf7b758257176f0c922562f468a9cbf3d7a1e8c6 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Wed, 19 Jul 2023 19:10:16 -0500 Subject: [PATCH 050/102] update pinned dependency --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index bce50c4ca..fa3c95179 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,8 @@ dependencies = [ "loguru", "pytest", "requests", + # See https://github.com/yaml/pyyaml/issues/724 for issues with pyyaml 5.4.1 + "PyYAML>=5.1,<7.0", ] [project.urls] From 04903a26d12e28874b1761529cf2ca82edca4c76 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Wed, 19 Jul 2023 19:23:10 -0500 Subject: [PATCH 051/102] patch `pyyaml` issues in CI until fixed in upstream ` --- .github/workflows/2-test-stand-alone.yml | 4 ++++ pyproject.toml | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/2-test-stand-alone.yml b/.github/workflows/2-test-stand-alone.yml index dcef6ab64..fb2a6ad29 100755 --- a/.github/workflows/2-test-stand-alone.yml +++ b/.github/workflows/2-test-stand-alone.yml @@ -44,6 +44,10 @@ jobs: - name: ⚙️ Installing Armory shell: bash run: | + # See https://github.com/yaml/pyyaml/issues/724 for issues with pyyaml 5.4.1 + pip install wheel cython + pip install "pyyaml>=5.1,<7.0", + pip install --no-compile --editable '.[developer,engine,pytorch]' armory configure --use-defaults diff --git a/pyproject.toml b/pyproject.toml index fa3c95179..5b61da23a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,6 @@ build-backend = "hatchling.build" requires = [ "hatchling>=1.10.0", "hatch-vcs", - "cython<3.0.0", ] @@ -40,8 +39,6 @@ dependencies = [ "loguru", "pytest", "requests", - # See https://github.com/yaml/pyyaml/issues/724 for issues with pyyaml 5.4.1 - "PyYAML>=5.1,<7.0", ] [project.urls] From 54f6fdb5e751c8182923cf1ecd8d15d76bfc0aab Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Wed, 19 Jul 2023 19:25:20 -0500 Subject: [PATCH 052/102] fix formatting in CI yaml --- .github/workflows/2-test-stand-alone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/2-test-stand-alone.yml b/.github/workflows/2-test-stand-alone.yml index fb2a6ad29..31ecf8b50 100755 --- a/.github/workflows/2-test-stand-alone.yml +++ b/.github/workflows/2-test-stand-alone.yml @@ -46,7 +46,7 @@ jobs: run: | # See https://github.com/yaml/pyyaml/issues/724 for issues with pyyaml 5.4.1 pip install wheel cython - pip install "pyyaml>=5.1,<7.0", + pip install "pyyaml>=5.1,<7.0" pip install --no-compile --editable '.[developer,engine,pytorch]' armory configure --use-defaults From 1cbe91e85c5c609fa3654513d3f45abeb3930906 Mon Sep 17 00:00:00 2001 From: Yusong Date: Thu, 20 Jul 2023 02:16:21 +0000 Subject: [PATCH 053/102] updated CARLA MOT datasets with more accurate green screen coordinates generated using tool from the Georgia Tech team --- armory/data/url_checksums/carla_mot_dev.txt | 2 +- armory/data/url_checksums/carla_mot_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/armory/data/url_checksums/carla_mot_dev.txt b/armory/data/url_checksums/carla_mot_dev.txt index 270b2b618..f7bf6bc32 100644 --- a/armory/data/url_checksums/carla_mot_dev.txt +++ b/armory/data/url_checksums/carla_mot_dev.txt @@ -1 +1 @@ -carla_mot_dev_1.0.1.tar.gz 704304674 5748a67b6f2010ec42e22a4120161f9e8fb09a40df0ea8711afbbc60cc38fe96 \ No newline at end of file +carla_mot_dev_1.0.1.tar.gz 704302804 439c93a85f65acccd1368e2bc6a9d9c30e6253642bdc8bb676da9d2667564acd \ No newline at end of file diff --git a/armory/data/url_checksums/carla_mot_test.txt b/armory/data/url_checksums/carla_mot_test.txt index 859f886c1..1786a10aa 100644 --- a/armory/data/url_checksums/carla_mot_test.txt +++ b/armory/data/url_checksums/carla_mot_test.txt @@ -1 +1 @@ -carla_mot_test_1.0.1.tar.gz 342711005 a75045f7363df45990ea6f2363b68cec3ca9610f5632833dc9217e6e01f5a3bc \ No newline at end of file +carla_mot_test_1.0.1.tar.gz 342710111 a3857b0d684be3be14fc365f7cfc59fb14a8a9167c259b6395fc12d578861970 \ No newline at end of file From 917fbcb9659db8eb0a795d71c69b675fbaafe324 Mon Sep 17 00:00:00 2001 From: Sterling Date: Thu, 20 Jul 2023 13:51:54 +0000 Subject: [PATCH 054/102] adding directory for majority mask data and helper function for loading it --- armory/data/majority_masks/__init__.py | 23 ++++++++++++++++++ .../speech_commands_majority_masks.npz | Bin 0 -> 90905 bytes armory/scenarios/poison.py | 13 ++++++---- 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 armory/data/majority_masks/__init__.py create mode 100644 armory/data/majority_masks/speech_commands_majority_masks.npz diff --git a/armory/data/majority_masks/__init__.py b/armory/data/majority_masks/__init__.py new file mode 100644 index 000000000..689e651ef --- /dev/null +++ b/armory/data/majority_masks/__init__.py @@ -0,0 +1,23 @@ +from pathlib import Path + +from armory.data.utils import maybe_download_weights_from_s3 +from armory.logs import log + +PARENT_DIR = Path(__file__).parent + + +def get_path(filename) -> str: + """ + Get the absolute path of the provided file name. Ordering priority is: + 1) Check directly for provided filepath + 2) Load from parent directory + 3) Attempt to download from s3 as a weights file + """ + filename = Path(filename) + if filename.is_file(): + return str(filename) + filepath = PARENT_DIR / filename + if filepath.is_file(): + return str(filepath) + + return maybe_download_weights_from_s3(filename) diff --git a/armory/data/majority_masks/speech_commands_majority_masks.npz b/armory/data/majority_masks/speech_commands_majority_masks.npz new file mode 100644 index 0000000000000000000000000000000000000000..f123976ea274568e123184766b6716c9daa5bd4d GIT binary patch literal 90905 zcmbWbO{%tQmKOLOQ>C;e7y}4fM7ox~(MQrMD6d(IE^LM`e#sB>KfB9ej z_=j)!`5WJU_U+Gq`>TKZn}6_|Kl=|qeEVAjAq0uffBoZs{?C5-FMjy;@4x-WAAkMR z@BYF58OpTGITw;%ldp`UMq0p-nlI_aPd!-{Sou;5f7FR=qc zVP4{C=VKTlal^UFuVpyiYPRI&%DRt$;r8Lm4@T{0P-}?MVIZqYe1)nZL*+Y}XaVY< z(9ez%Q9QLg(PKx58KQ{gwBuY4clluGm}6ik9KE$oTos41rE3n}^vXq(ad6zUbGWrE zBB+pavl>oS!pLxYAe`uxEQz?g|oWToqGvS7m1brBjF|(5UYYTvsddVKOp*A_Zacr`Xq?)EpT#iZAMpR3xMg|5`Na>)$M#{BnO- zY1eTW$q09px@leTQ35I_d7x~TDvt+HKQ&5#^C7f>;334}LY0qeUE#7{dWj#YQ;a3; zxxpP?p*Zy=cQu545YHN&wJ&a|iGdgz-E8>E+3x`i9e)VRG3AMq9gO7$=%XtK@37FL z4#>nbBGTzMneLielf*GU6{;|TkTMYA-QYyV`?-+nIWJfj6JoZALJ%B6k~MKmG^E=v zjS4z^XfN+*(Z>>0CdHw=a(I^h2rLw*mo4ZfL?qrh0JCE4sc-h2B#Ql%6GEVu+BreW<gFk474&hJ9cDX~bT<_9T_1^D%oV6%`H`gE|zu^N=#0ohX@)0Ayh zxK)P97<9iKcJU8qsv&}L{KSzSH*F*mQ1-~XTxK|a^UuBj_4NP;HIKyuzAI0b*f~*0 zx`hUS&bGHcFlmUuTa3tKD?t#4exTanP~lfr5!uENPN&<-x$t#o2|+OrWz|uuTw5zj z-OZ&if!DLLt>UtpW?=Q9@V6CBa>O7mogHm8MStcD^vi=lC}(0Wtj|wFJYqLn&k0e) zF2}%ZcN{47aiD*r(PxKiB^KA*cX`9RJ1g#yW$2LZeasLQ)`mjb?o*MHo->>+{4M5v zg#);?9s(hHelnux)VCwXC@KhBV`LC5Fp-r`VRMviQOwU6c^bEqWT1Ovl6qa7EmLG2 zc?y-QV2`#xQ}D}pp!*T!p6jyyA_A&?C?Y?Vb?+rD$DczbV{%ZamlUBhCy;aW5YlIe z|E>=hCTjHvU}YN9^tpg*ZJ}lcu#mmt7B>Z;2zXpsfUAf?)soi?OYHT+{)=1j*<`_{ z`|pgYGcb#+aw}-iIZ$Pcy0i1#LqkY=)}5PuLfix+84ChM(52T3fMGtSf6Vqw2J}=b zrz*7k1Yt1c8RrpDaSU!YI2J?69>;tf

2#HR_&aX+kG<@m3;tlAj9>ZFipYF(DFS z&&93M*0_s~yX#PEt5WTcD^V~(fMb3ROh7Ldg84KrA%b<<>5Nej5Ndr96b(a-FCJC$ z6KK#lMf4JY4Fc#<=PSR!kMl}kq}LbH`DK9;EPphG!|0H(+0jbPjN$a*deGCzI1{-< zphLw@VofbxT^B22b8Kim$>T>U@${+f<|QYoz)u+RM9k@mQ+R-}4W$0z;xHOY%f*Ob z91GmR#icFrfuo(DqAB*zNGy7AxTsO%VVfat-JVXhYlyd(L0eRieMk~*<)VYZmP|l^ zx$(`;p>2Umt4KrbLR9-n6mBV!HAK1u@po1q;HmbbORZ#nN%4S??iB1zv4>ef>T>Au>d31}>4|@Oa@;vHx7s z4<-#Sah4pcBaG$<~}5 z=KVecKBm^_T_B$fy`6ef%*`@4!)y?iM?^;{3@SXqo`aHyMpCNtBA^z03ujY#U6E>i zjflvbA#gtPgd$kO$ItEzTmhFM^dzCtxCc(6f?@NI(n4c`B*0&ZkY(wFgvB1}dE(IT zjuq?Lq?7N~^Z?EqNs19`#gd~K1{AWn4943=ik3M0>%Eo#ss}PZe0Px@)hQ+9na8l9 z%o)5CSjXr$freH8K$aJwl^h%I-1j%B0Zz2$olk>gFTr>dykv*|yFpiqTFaw|+={fh zIp~O{T*@A4l3Gwj)niosorJzdMkV2l-J#7p(mu^#pFGCixzn+=&h&9%`bdBj-Beb^ zrJ2@34oLG?P-J!M z#r@J|dQ5(3DB$3yS?SCuW`rsr+Yjp7n+YuO7Yk`=XzplsT#BAZ&Py1GsBg~mUjcl8 z!JiM6#�|MIm?dR43*`f>ClPX-t20J!YJ4xD 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 literal 0 HcmV?d00001 diff --git a/armory/scenarios/poison.py b/armory/scenarios/poison.py index 625d11caa..863425ae9 100644 --- a/armory/scenarios/poison.py +++ b/armory/scenarios/poison.py @@ -19,6 +19,7 @@ from armory.scenarios.scenario import Scenario from armory.scenarios.utils import to_categorical from armory.utils import config_loading +from armory.data import majority_masks as majority_mask_dir class DatasetPoisoner: @@ -390,11 +391,13 @@ def load_dataset(self, eval_split_default="test"): self.init_explanatory() def load_fairness_metrics(self): - majority_masks_config = self.config["adhoc"].get("majority_masks") - if majority_masks_config: + majority_mask_file = self.config["adhoc"].get("majority_masks") + if majority_mask_file: # Attempt to load majority masks first, if provided log.info("Using pre-computed majority masks...") - self.majority_masks = np.load(majority_masks_config) + self.majority_masks = np.load( + majority_mask_dir.get_path(majority_mask_file) + ) else: # Load explanatory model otherwise log.info("Using explanatory model...") @@ -404,9 +407,9 @@ def load_fairness_metrics(self): explanatory_config ) else: - # compute_fairness_metrics was true, but there is no explanatory config + # compute_fairness_metrics was true, but there is no explanatory config or masks raise ValueError( - "If computing fairness metrics, must specify 'explanatory_model' under 'adhoc'" + "If computing fairness metrics, must specify 'explanatory_model' or 'majority_masks' under 'adhoc'" ) if not self.check_run and self.use_filtering_defense: From dbb4c4d7868e354adc5bf10902cd2b8dc11e59ed Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 21 Jul 2023 13:46:31 +0000 Subject: [PATCH 055/102] url checksums --- armory/data/url_checksums/carla_mot_dev.txt | 2 +- armory/data/url_checksums/carla_mot_test.txt | 2 +- armory/data/url_checksums/carla_over_obj_det_dev.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/armory/data/url_checksums/carla_mot_dev.txt b/armory/data/url_checksums/carla_mot_dev.txt index f7bf6bc32..cab68b0eb 100644 --- a/armory/data/url_checksums/carla_mot_dev.txt +++ b/armory/data/url_checksums/carla_mot_dev.txt @@ -1 +1 @@ -carla_mot_dev_1.0.1.tar.gz 704302804 439c93a85f65acccd1368e2bc6a9d9c30e6253642bdc8bb676da9d2667564acd \ No newline at end of file +https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_mot_dev_1.0.1.tar.gz 704302804 439c93a85f65acccd1368e2bc6a9d9c30e6253642bdc8bb676da9d2667564acd \ No newline at end of file diff --git a/armory/data/url_checksums/carla_mot_test.txt b/armory/data/url_checksums/carla_mot_test.txt index 1786a10aa..7ce7405d3 100644 --- a/armory/data/url_checksums/carla_mot_test.txt +++ b/armory/data/url_checksums/carla_mot_test.txt @@ -1 +1 @@ -carla_mot_test_1.0.1.tar.gz 342710111 a3857b0d684be3be14fc365f7cfc59fb14a8a9167c259b6395fc12d578861970 \ No newline at end of file +https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_mot_test_1.0.1.tar.gz 342710111 a3857b0d684be3be14fc365f7cfc59fb14a8a9167c259b6395fc12d578861970 \ No newline at end of file diff --git a/armory/data/url_checksums/carla_over_obj_det_dev.txt b/armory/data/url_checksums/carla_over_obj_det_dev.txt index cd915a11e..d0d86b763 100644 --- a/armory/data/url_checksums/carla_over_obj_det_dev.txt +++ b/armory/data/url_checksums/carla_over_obj_det_dev.txt @@ -1 +1 @@ -carla_over_od_dev_2.0.1.tar.gz 64846910 499cd578a8c9ebb4dc2b8578e9ae3ac89c60bd57d55250327f683e164944a749 \ No newline at end of file +https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_over_od_dev_2.0.1.tar.gz 64846910 499cd578a8c9ebb4dc2b8578e9ae3ac89c60bd57d55250327f683e164944a749 \ No newline at end of file From 08e7c26263f606068587b1a27b869c46500035ff Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 21 Jul 2023 13:47:00 +0000 Subject: [PATCH 056/102] dataset urls --- armory/data/adversarial/carla_mot_dev.py | 2 +- armory/data/adversarial/carla_mot_test.py | 2 +- armory/data/adversarial/carla_over_obj_det_dev.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/armory/data/adversarial/carla_mot_dev.py b/armory/data/adversarial/carla_mot_dev.py index 08ac59da5..b1b118990 100644 --- a/armory/data/adversarial/carla_mot_dev.py +++ b/armory/data/adversarial/carla_mot_dev.py @@ -22,7 +22,7 @@ } """ -_URLS = "carla_mot_dev_1.0.1.tar.gz" +_URLS = "https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_mot_dev_1.0.1.tar.gz" class CarlaMOTDev(tfds.core.GeneratorBasedBuilder): diff --git a/armory/data/adversarial/carla_mot_test.py b/armory/data/adversarial/carla_mot_test.py index 394162395..781397e6f 100644 --- a/armory/data/adversarial/carla_mot_test.py +++ b/armory/data/adversarial/carla_mot_test.py @@ -22,7 +22,7 @@ } """ -_URLS = "carla_mot_test_1.0.1.tar.gz" +_URLS = "https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_mot_test_1.0.1.tar.gz" class CarlaMOTTest(tfds.core.GeneratorBasedBuilder): diff --git a/armory/data/adversarial/carla_over_obj_det_dev.py b/armory/data/adversarial/carla_over_obj_det_dev.py index 327bb8ea9..9aa0bdc05 100644 --- a/armory/data/adversarial/carla_over_obj_det_dev.py +++ b/armory/data/adversarial/carla_over_obj_det_dev.py @@ -24,7 +24,7 @@ """ # fmt: off -_URLS = "carla_over_od_dev_2.0.1.tar.gz" +_URLS = "https://armory-public-data.s3.us-east-2.amazonaws.com/carla/carla_over_od_dev_2.0.1.tar.gz" # fmt: on From ec5db25b1b19ab6fe001869d11cfb50bea48a593 Mon Sep 17 00:00:00 2001 From: Sterling Date: Fri, 21 Jul 2023 13:47:14 +0000 Subject: [PATCH 057/102] cached checksums --- armory/data/cached_s3_checksums/carla_mot_dev.txt | 2 +- armory/data/cached_s3_checksums/carla_mot_test.txt | 2 +- armory/data/cached_s3_checksums/carla_over_obj_det_dev.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/armory/data/cached_s3_checksums/carla_mot_dev.txt b/armory/data/cached_s3_checksums/carla_mot_dev.txt index a211e9d55..c4b48c85e 100644 --- a/armory/data/cached_s3_checksums/carla_mot_dev.txt +++ b/armory/data/cached_s3_checksums/carla_mot_dev.txt @@ -1 +1 @@ -armory-public-data carla/carla_mot_dev_cached_1.0.0.tar.gz 678719928 23d80f92c458c7fea20c1dfc209421c58ee006d2c47a6202969957fda496ce83 \ No newline at end of file +armory-public-data carla/carla_mot_dev_cached_1.0.1.tar.gz 678720178 3c800b0289a84fea3f11ffa9b5f4cee105cae644223a23532e1b7351560869ed \ No newline at end of file diff --git a/armory/data/cached_s3_checksums/carla_mot_test.txt b/armory/data/cached_s3_checksums/carla_mot_test.txt index 12b8f6547..828d0e034 100644 --- a/armory/data/cached_s3_checksums/carla_mot_test.txt +++ b/armory/data/cached_s3_checksums/carla_mot_test.txt @@ -1 +1 @@ -armory-public-data carla/carla_mot_test_cached_1.0.0.tar.gz 327822268 1ba37678d8d9e11d97dfbdfccfa5bb833457b137fdb96eff6ed804238fec8494 +armory-public-data carla/carla_mot_test_cached_1.0.1.tar.gz 327821370 0fc9d1afdd1da171fedbdb8727165d75062909a890c7d89995e0218df0f8ccc8 diff --git a/armory/data/cached_s3_checksums/carla_over_obj_det_dev.txt b/armory/data/cached_s3_checksums/carla_over_obj_det_dev.txt index 89d50199c..1d01c447f 100644 --- a/armory/data/cached_s3_checksums/carla_over_obj_det_dev.txt +++ b/armory/data/cached_s3_checksums/carla_over_obj_det_dev.txt @@ -1 +1 @@ -armory-public-data carla/carla_over_od_dev_cached_2.0.0.tar.gz 62773923 fa991a146afd771f806e050fdcfdf6953667a86acbbc05a3cd9c7d7a51ba92a6 \ No newline at end of file +armory-public-data carla/carla_over_od_dev_cached_2.0.1.tar.gz 62773711 2c0ef1c43fbb6235ed10a41ff1c70dfa940ff40d91a94591c716d86b458bcedb \ No newline at end of file From 41299c8eedef78b9c08a0652375d20d673576c7d Mon Sep 17 00:00:00 2001 From: Yusong Date: Sat, 22 Jul 2023 00:13:07 +0000 Subject: [PATCH 058/102] updated CARLA MOT test dataset to fix RGB/instance segmentation misalignment --- armory/data/adversarial/carla_mot_test.py | 2 +- armory/data/url_checksums/carla_mot_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/armory/data/adversarial/carla_mot_test.py b/armory/data/adversarial/carla_mot_test.py index 394162395..8aefb2469 100644 --- a/armory/data/adversarial/carla_mot_test.py +++ b/armory/data/adversarial/carla_mot_test.py @@ -31,7 +31,7 @@ class CarlaMOTTest(tfds.core.GeneratorBasedBuilder): VERSION = tfds.core.Version("1.0.1") RELEASE_NOTES = { "1.0.0": "Initial release.", - "1.0.1": "Updated green screen coordinates so a patch appears static in each video.", + "1.0.1": "Updated green screen coordinates and RGB/instance segmentation misalignment to eliminate patch spatial movement or flickering", } def _info(self) -> tfds.core.DatasetInfo: diff --git a/armory/data/url_checksums/carla_mot_test.txt b/armory/data/url_checksums/carla_mot_test.txt index 1786a10aa..50a4ad8d8 100644 --- a/armory/data/url_checksums/carla_mot_test.txt +++ b/armory/data/url_checksums/carla_mot_test.txt @@ -1 +1 @@ -carla_mot_test_1.0.1.tar.gz 342710111 a3857b0d684be3be14fc365f7cfc59fb14a8a9167c259b6395fc12d578861970 \ No newline at end of file +carla_mot_test_1.0.1.tar.gz 342551271 4ea53820553f17d90bfb97c92631e71f6155915d5c3d9319f83ad66251ce54d3 \ No newline at end of file From 18f2f660250652ccf0186a4d1bc5af2a2c5d7823 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Wed, 26 Jul 2023 07:02:14 -0500 Subject: [PATCH 059/102] update CI --- .github/workflows/2-test-stand-alone.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/2-test-stand-alone.yml b/.github/workflows/2-test-stand-alone.yml index 31ecf8b50..dcef6ab64 100755 --- a/.github/workflows/2-test-stand-alone.yml +++ b/.github/workflows/2-test-stand-alone.yml @@ -44,10 +44,6 @@ jobs: - name: ⚙️ Installing Armory shell: bash run: | - # See https://github.com/yaml/pyyaml/issues/724 for issues with pyyaml 5.4.1 - pip install wheel cython - pip install "pyyaml>=5.1,<7.0" - pip install --no-compile --editable '.[developer,engine,pytorch]' armory configure --use-defaults From 6c30bcd1ceebfbf6feb1f93f3cddbd65fa235e48 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Wed, 26 Jul 2023 07:24:32 -0500 Subject: [PATCH 060/102] update references to `readthedocs.com` --- README.md | 4 ++-- docs/assets/docs-badge.svg | 1 + docs/integrate_tensorflow_datasets.md | 6 +++--- pyproject.toml | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 docs/assets/docs-badge.svg diff --git a/README.md b/README.md index 31c883f74..1bea470f9 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Agency (DARPA). [python-url]: https://pypi.org/project/armory-testbed [license-badge]: https://img.shields.io/badge/License-MIT-yellow.svg [license-url]: https://opensource.org/licenses/MIT -[docs-badge]: https://readthedocs.org/projects/armory/badge/ -[docs-url]: https://readthedocs.org/projects/armory/ +[docs-badge]: https://github.com/twosixlabs/armory/docs/assets/docs-badge.svg +[docs-url]: https://github.com/twosixlabs/armory/docs [style-badge]: https://img.shields.io/badge/code%20style-black-000000.svg [style-url]: https://github.com/ambv/black diff --git a/docs/assets/docs-badge.svg b/docs/assets/docs-badge.svg new file mode 100644 index 000000000..c387fcc68 --- /dev/null +++ b/docs/assets/docs-badge.svg @@ -0,0 +1 @@ +docsdocspassingpassing \ No newline at end of file diff --git a/docs/integrate_tensorflow_datasets.md b/docs/integrate_tensorflow_datasets.md index 1d692f040..0b15e4803 100644 --- a/docs/integrate_tensorflow_datasets.md +++ b/docs/integrate_tensorflow_datasets.md @@ -1,7 +1,7 @@ # Instructions to Integrate TFDS Datasets 1. Get the name, version number of the Tensorflow Dataset, and optionally the config: "name[/config]:version_number", where the brackets denote optional text. -2. Set the environmental variables ARMORY_PRIVATE_S3_ID and ARMORY_PRIVATE_S3_KEY to the appropriate keys with write access to the Armory S3 bucket. +2. Set the environmental variables ARMORY_PRIVATE_S3_ID and ARMORY_PRIVATE_S3_KEY to the appropriate keys with write access to the Armory S3 bucket. 3. From a locally cloned version of armory, on a new branch, run: ``` python -m armory exec pytorch -- python -m armory.data.integrate_tfds name[/config]:version @@ -11,8 +11,8 @@ where the brackets denote optional text. The script will download and process the TFDS dataset, generate TF Records files, create a tarball, and upload the tarball to S3. It also will create a S3 checksum file in ```armory/data/cached_s3_checksums/{name}.txt``` 4. Run ```git status``` to confirm the S3 checksum file was generated and to see the path of the template file. -5. Manually put the template code from ```TEMPLATE_{name}.txt``` in ```armory/data/datasets.py```. Create a context object that contains metadata and a preprocessing function that does appropriate integrity checks/input normalizing. See for example the [canonical fixed-size image preprocessing function](https://github.com/twosixlabs/armory/blob/deb7a469bf4a7497d14fdd87eba6417b5e44589f/armory/data/datasets.py#L617-L631) which checks the shapes of an image, and renormalizes it to be in the appropriate range defined by the context object (typically 0.0-1.0) with a standard type. See the documentation on dataset [preprocessing](https://armory.readthedocs.io/en/latest/datasets/#preprocessing) for more details. -6. [Optional] Add the dataset to the [SUPPORTED_DATASETS](https://github.com/twosixlabs/armory/blob/deb7a469bf4a7497d14fdd87eba6417b5e44589f/armory/data/datasets.py#L1498-L1511) dictionary by adding a key with the dataset's name and value of the dataset function from the template code. +5. Manually put the template code from ```TEMPLATE_{name}.txt``` in ```armory/data/datasets.py```. Create a context object that contains metadata and a preprocessing function that does appropriate integrity checks/input normalizing. See for example the [canonical fixed-size image preprocessing function](https://github.com/twosixlabs/armory/blob/deb7a469bf4a7497d14fdd87eba6417b5e44589f/armory/data/datasets.py#L617-L631) which checks the shapes of an image, and renormalizes it to be in the appropriate range defined by the context object (typically 0.0-1.0) with a standard type. See the documentation on dataset [preprocessing](https://github.com/twosixlabs/armory/docs/datasets.md) for more details. +6. [Optional] Add the dataset to the [SUPPORTED_DATASETS](https://github.com/twosixlabs/armory/blob/deb7a469bf4a7497d14fdd87eba6417b5e44589f/armory/data/datasets.py#L1498-L1511) dictionary by adding a key with the dataset's name and value of the dataset function from the template code. 7. [Optional] Create a continuous integration test for the dataset in ```tests/test_docker/test_dataset.py```, possibly using ```pytest.skip```. 8. Commit the changes to the branch on your fork of the Armory repo. 9. Open a PR to integrate the dataset. diff --git a/pyproject.toml b/pyproject.toml index 5b61da23a..888b2e816 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ [project.urls] Source = "https://github.com/twosixlabs/armory" -Documentation = "https://armory.readthedocs.io/en/latest/" +Documentation = "https://github.com/twosixlabs/armory" [project.scripts] armory = "armory.__main__:main" From fd7088942111bda241687d259e6d1be7de3e9277 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 26 Jul 2023 15:00:47 +0000 Subject: [PATCH 061/102] standardize threshold keys --- armory/metrics/task.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index a55e8a2b3..0a2b8e018 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -956,7 +956,7 @@ def object_detection_AP_per_class_by_giou_from_patch( ) if not np.isnan(ap["mean"]): # Possibly nan if there are no boxes on or near the patch - result["cumulative_by_min_giou"][threshold] = ap + result["cumulative_by_min_giou"][f"{threshold:.1f}"] = ap y_list_max = [ { @@ -979,7 +979,7 @@ def object_detection_AP_per_class_by_giou_from_patch( ) if not np.isnan(ap["mean"]): # May be nan for smallest giou thresholds where there are no boxes - result["cumulative_by_max_giou"][threshold] = ap + result["cumulative_by_max_giou"][f"{threshold:.1f}"] = ap histogram_bin_top = threshold + increment if histogram_bin_top > 0: @@ -1003,7 +1003,7 @@ def object_detection_AP_per_class_by_giou_from_patch( y_list_range, y_pred_list_range, iou_threshold, class_list, mean ) if not np.isnan(ap["mean"]): - result["histogram_left"][threshold] = ap + result["histogram_left"][f"{threshold:.1f}"] = ap return result From 7787237eade7ee2caa96703dfd70daa99d5d859d Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 26 Jul 2023 15:02:24 +0000 Subject: [PATCH 062/102] plot functions for visualizing giou output --- .../plot_distance_aware_carla_metric.py | 8 - .../plot_patch_aware_carla_metric.py | 217 ++++++++++++++++++ 2 files changed, 217 insertions(+), 8 deletions(-) delete mode 100644 armory/postprocessing/plot_distance_aware_carla_metric.py create mode 100644 armory/postprocessing/plot_patch_aware_carla_metric.py diff --git a/armory/postprocessing/plot_distance_aware_carla_metric.py b/armory/postprocessing/plot_distance_aware_carla_metric.py deleted file mode 100644 index 3d8aeb65e..000000000 --- a/armory/postprocessing/plot_distance_aware_carla_metric.py +++ /dev/null @@ -1,8 +0,0 @@ -from matplotlib import pyplot as plt -import json -import numpy as np - - -def plot_mAP_by_giou_from_patch(json_filepath, show=True, output_filepath=None): - - pass diff --git a/armory/postprocessing/plot_patch_aware_carla_metric.py b/armory/postprocessing/plot_patch_aware_carla_metric.py new file mode 100644 index 000000000..4633e9c87 --- /dev/null +++ b/armory/postprocessing/plot_patch_aware_carla_metric.py @@ -0,0 +1,217 @@ +""" +Utility functions for visualizing the output of the metric "object_detection_AP_per_class_by_giou_from_patch" + + +Example usage: +>>> from armory.postprocessing.plot_patch_aware_carla_metric import plot_mAP_by_giou_with_patch +>>> plot_mAP_by_giou_with_patch("path/to/results.json", flavors=["cumulative_by_min_giou"]) +""" +import json + +from matplotlib import pyplot as plt +import numpy as np + + +ben_color = "tab:blue" +adv_color = "tab:red" + +flavor_titles = { + "cumulative_by_max_giou": "Cumulative by Upper Bound\n(Exclusive)", + "cumulative_by_min_giou": "Cumulative by Lower Bound\n(Inclusive)", + "histogram_left": "Histogram\n(Left Oriented)", +} + +fontsize = 8 + + +def _init_plots(json_filepath, include_classes, n_flavors=1): + # Read json from json_filepath and initialize fig and ax objects for plotting. + + # Returns giou dicts from json, the fig and axes, as well as number of classes. + + with open(json_filepath) as f: + blob = json.load(f) + results = blob["results"] + + adv_giou = results["adversarial_object_detection_AP_per_class_by_giou_from_patch"][ + 0 + ] + ben_giou = results["benign_object_detection_AP_per_class_by_giou_from_patch"][0] + adv_ap = results["adversarial_carla_od_AP_per_class"][0]["mean"] + ben_ap = results["benign_carla_od_AP_per_class"][0]["mean"] + + if include_classes: + n_class = len(ben_giou["cumulative_by_max_giou"]["0.0"]["class"]) + else: + n_class = 0 + + fig, axes = plt.subplots(n_class + 1, n_flavors, sharex=True, sharey=True) + # Ensure axes are in an array for consistent reference + if type(axes) != np.array or len(axes.shape) == 1: + axes = np.array(axes).reshape(n_class + 1, n_flavors) + + axes[0, 0].set_ylabel("Mean AP") + + for ax in axes.flatten(): + # Add dotted lines for total mAP + ax.set_ylim(0, 1.05) + ax.axhline( + y=ben_ap, + linestyle="dotted", + color=ben_color, + linewidth=1.5, + label="Ben. mAP: {0:.2f}".format(ben_ap), + ) + ax.axhline( + y=adv_ap, + linestyle="dotted", + color=adv_color, + linewidth=1.5, + label="Adv. mAP: {0:.2f}".format(adv_ap), + ) + + return ben_giou, adv_giou, n_class, (fig, axes) + + +def plot_mAP_by_giou_with_patch( + json_filepath, flavors=None, show=True, output_filepath=None, include_classes=True +): + """Plots mAP of boxes according to their giou with the patch. + + json_filepath: the path to the output json file. + flavors: a list of data accumulation variants. + Subset of ["cumulative_by_max_giou", "cumulative_by_min_giou", "histogram_left"] + None defaults to all flavors. + show: whether to show the plot. + output_filepath: if provided, figure is saved. + include_classes: include subplots for each class. + """ + + if flavors is None: + flavors, titles = zip(*flavor_titles.items()) + else: + for flavor in flavors: + if flavor not in flavor_titles: + raise ValueError( + f"Invalid flavor {flavor}; should be one of {list(flavor_titles.keys())}" + ) + titles = [flavor_titles[flavor] for flavor in flavors] + + ben_giou, adv_giou, n_class, fig_ax = _init_plots( + json_filepath, include_classes, n_flavors=len(flavors) + ) + fig, axes = fig_ax + + fig.suptitle("AP by GIoU with patch") + for i in range(len(titles)): + axes[0, i].set_title(titles[i]) + axes[-1, i].set_xlabel("GIoU") + + def add_bars( + ax, + x, + y, + offsets=[0, 0.04], + colors=[ben_color, adv_color], + labels=["Benign", "Adversarial"], + ): + # Helper function to plot bars. x and y each contain benign and adversarial data for looping over + width = 0.04 + for x_, y_, o, c, l in zip(x, y, offsets, colors, labels): + rects = ax.bar(x_ + o, y_, width, color=c, label=l) + if len(flavors) == 1: + # If figure is only 1 subplot wide, annotate bars with their values. + # Too messy for figures with more subplots. + ax.bar_label(rects, fmt="%.2f", label_type="center", fontsize=fontsize) + + for i, flavor in enumerate(flavors): + # Get data and add plots for each flavor. + + ben = ben_giou[flavor] + adv = adv_giou[flavor] + m_b = np.array([[float(k), ben[k]["mean"]] for k in ben]) + m_a = np.array([[float(k), adv[k]["mean"]] for k in adv]) + + # Plot mean data + add_bars(axes[0, i], (m_b[:, 0], m_a[:, 0]), (m_b[:, 1], m_a[:, 1])) + + for j in range(1, n_class + 1): + # Plot per-class data + m_b = np.array([[float(k), ben[k]["class"][str(j)]] for k in ben]) + m_a = np.array([[float(k), adv[k]["class"][str(j)]] for k in adv]) + add_bars(axes[j, i], (m_b[:, 0], m_a[:, 0]), (m_b[:, 1], m_a[:, 1])) + + axes[j, 0].set_ylabel(f"Class {j} AP") + + # Set up legend + handles, labels = axes[0, 0].get_legend_handles_labels() + fig.legend(handles, labels, loc=8, ncol=4) + fig.tight_layout() + fig.subplots_adjust(bottom=0.15) + + if output_filepath is not None: + plt.savefig(output_filepath) + if show: + plt.show() + + +def plot_single_giou_threshold( + json_filepath, threshold=0.0, show=True, output_filepath=None, include_classes=True +): + """Plots mAP of boxes over and below a given GIoU threshold. + + json_filepath: the path to the output json file. + threshold: the threshold of interest. Must be a valid key in the json results. + show: whether to show the plot. + output_filepath: if provided, figure is saved. + include_classes: include subplots for each class. + """ + + ben_giou, adv_giou, n_class, fig_ax = _init_plots(json_filepath, include_classes) + fig, axes = fig_ax + + ap_dicts = [ + ben_giou["cumulative_by_max_giou"][str(threshold)], + ben_giou["cumulative_by_min_giou"][str(threshold)], + adv_giou["cumulative_by_max_giou"][str(threshold)], + adv_giou["cumulative_by_min_giou"][str(threshold)], + ] + + fig.suptitle(f"AP by GIoU with patch\nrelative to threshold of {threshold}") + + def add_bars( + ax, d_list, colors=[ben_color, adv_color], labels=["Benign", "Adversarial"] + ): + # Helper function to plot bars. d_list contains benign and adversarial data for looping over + x = np.arange(2) + width = 0.25 + multiplier = -0.5 + + for d, c, l in zip(d_list, colors, labels): + offset = width * multiplier + width + rects = ax.bar(x + offset, d, width, label=l, color=c) + ax.bar_label(rects, fmt="%.2f", label_type="center", fontsize=fontsize) + multiplier *= -1 + + ax.set_xticks(x + width, ["Below threshold", "Above threshold"]) + + # Plot mean data + add_bars(axes[0, 0], np.array([d["mean"] for d in ap_dicts]).reshape(2, 2)) + + for j in range(1, n_class + 1): + # Plot per-class data + add_bars( + axes[j, 0], np.array([d["class"][str(j)] for d in ap_dicts]).reshape(2, 2) + ) + axes[j, 0].set_ylabel(f"Class {j} AP") + + # Set up legend + handles, labels = axes[0, 0].get_legend_handles_labels() + fig.legend(handles, labels, loc=8, ncol=4) + fig.tight_layout() + fig.subplots_adjust(bottom=0.15) + + if output_filepath is not None: + plt.savefig(output_filepath) + if show: + plt.show() From 1574986711c924ca963661e8adc3b53ee326fdf8 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Wed, 26 Jul 2023 11:26:00 -0500 Subject: [PATCH 063/102] add tensorflow specific requirements to conda --- environment.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 893972208..44c9ec1b6 100644 --- a/environment.yml +++ b/environment.yml @@ -16,7 +16,10 @@ dependencies: - matplotlib - conda-forge::ffmpeg # conda-forge ffmpeg comes with libx264 encoder, which the pytorch channel version does not include. This encoder is required for video compression defenses (ART) and video exporting. Future work could migrate this to libopenh264 encoder, which is available in both channels. - librosa - - cudnn # cudnn required for tensorflow - pandas - protobuf + - nvidia # Required for tensorflow. See: https://github.com/tensorflow/tensorflow/issues/58681#issuecomment-1406967453 + - cuda-nvcc # Required for tensorflow. See: https://github.com/tensorflow/tensorflow/issues/58681#issuecomment-1406967453 + - cudnn # cudnn required for tensorflow + prefix: /opt/conda From 2ecfe23afe6b6d990bdfcc532211d78691983f90 Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 26 Jul 2023 16:29:38 +0000 Subject: [PATCH 064/102] update cached checksum --- armory/data/cached_s3_checksums/carla_mot_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armory/data/cached_s3_checksums/carla_mot_test.txt b/armory/data/cached_s3_checksums/carla_mot_test.txt index 828d0e034..836cd10c5 100644 --- a/armory/data/cached_s3_checksums/carla_mot_test.txt +++ b/armory/data/cached_s3_checksums/carla_mot_test.txt @@ -1 +1 @@ -armory-public-data carla/carla_mot_test_cached_1.0.1.tar.gz 327821370 0fc9d1afdd1da171fedbdb8727165d75062909a890c7d89995e0218df0f8ccc8 +armory-public-data carla/carla_mot_test_cached_1.0.1.tar.gz 327687101 7d9e3f3cfc59c138727a976fbaeb6209909ba764ef82c5093d1bfbe360529ea0 From a9d8713c428b0ca96fbc00e48e8847a57cb4f7cb Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 26 Jul 2023 17:11:09 +0000 Subject: [PATCH 065/102] descriptive docstring --- .../plot_patch_aware_carla_metric.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/armory/postprocessing/plot_patch_aware_carla_metric.py b/armory/postprocessing/plot_patch_aware_carla_metric.py index 4633e9c87..d596fc242 100644 --- a/armory/postprocessing/plot_patch_aware_carla_metric.py +++ b/armory/postprocessing/plot_patch_aware_carla_metric.py @@ -1,10 +1,25 @@ """ -Utility functions for visualizing the output of the metric "object_detection_AP_per_class_by_giou_from_patch" +Utility functions for visualizing the output of the metric "object_detection_AP_per_class_by_giou_from_patch." +This metric captures how adversarial AP varies as objects get farther from the patch. +There are two functions which produce slightly different plots: +plot_mAP_by_giou_with_patch, and plot_single_giou_threshold. -Example usage: +plot_mAP_by_giou_with_patch() shows how the mAP changes over a range of GIoU values. It +can display this in three "flavors": cumulative by max GIoU, cumulative by min GIoU, +or as a histogram. The first flavor represents all objects outside each given range of GIoU +(i.e. further from the patch, or more negative GIoU). The second flavor is all objects within +each range (i.e. closer to the patch, or more positive GIoU). The histogram version reports AP +for disjoint intervals of GIoU value. + +plot_single_giou_threshold() only considers one user-specified GIoU threshold, and shows +the adversarial and benign AP both above and below that threshold. For example, using a threshold +of 0.0 would correspond to all objects touching and not touching the patch. + +Intended for stand-alone usage: >>> from armory.postprocessing.plot_patch_aware_carla_metric import plot_mAP_by_giou_with_patch >>> plot_mAP_by_giou_with_patch("path/to/results.json", flavors=["cumulative_by_min_giou"]) + """ import json From 6a92644ba3e1fef87c8ee5729c95c424ffd81d0a Mon Sep 17 00:00:00 2001 From: Sterling Date: Wed, 26 Jul 2023 17:12:16 +0000 Subject: [PATCH 066/102] formatting --- armory/postprocessing/plot_patch_aware_carla_metric.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/armory/postprocessing/plot_patch_aware_carla_metric.py b/armory/postprocessing/plot_patch_aware_carla_metric.py index d596fc242..1552b9112 100644 --- a/armory/postprocessing/plot_patch_aware_carla_metric.py +++ b/armory/postprocessing/plot_patch_aware_carla_metric.py @@ -2,14 +2,14 @@ Utility functions for visualizing the output of the metric "object_detection_AP_per_class_by_giou_from_patch." This metric captures how adversarial AP varies as objects get farther from the patch. -There are two functions which produce slightly different plots: +There are two functions which produce slightly different plots: plot_mAP_by_giou_with_patch, and plot_single_giou_threshold. -plot_mAP_by_giou_with_patch() shows how the mAP changes over a range of GIoU values. It +plot_mAP_by_giou_with_patch() shows how the mAP changes over a range of GIoU values. It can display this in three "flavors": cumulative by max GIoU, cumulative by min GIoU, or as a histogram. The first flavor represents all objects outside each given range of GIoU -(i.e. further from the patch, or more negative GIoU). The second flavor is all objects within -each range (i.e. closer to the patch, or more positive GIoU). The histogram version reports AP +(i.e. further from the patch, or more negative GIoU). The second flavor is all objects within +each range (i.e. closer to the patch, or more positive GIoU). The histogram version reports AP for disjoint intervals of GIoU value. plot_single_giou_threshold() only considers one user-specified GIoU threshold, and shows From 11ee9bb96e62ede96bae7a74ca5db299a891b063 Mon Sep 17 00:00:00 2001 From: Yusong Date: Fri, 28 Jul 2023 19:16:22 +0000 Subject: [PATCH 067/102] updating CARLA attacks with the option to use Adam optimizer --- .../attacks/carla_mot_adversarial_patch.py | 43 ++++++++++++- .../carla_obj_det_adversarial_patch.py | 62 +++++++++++++++++-- ...carla_mot_adversarialpatch_undefended.json | 4 +- ...a_obj_det_adversarialpatch_undefended.json | 6 +- ..._multimodal_adversarialpatch_defended.json | 6 +- ...ultimodal_adversarialpatch_undefended.json | 6 +- 6 files changed, 109 insertions(+), 18 deletions(-) diff --git a/armory/art_experimental/attacks/carla_mot_adversarial_patch.py b/armory/art_experimental/attacks/carla_mot_adversarial_patch.py index 869cfa163..113e5c310 100644 --- a/armory/art_experimental/attacks/carla_mot_adversarial_patch.py +++ b/armory/art_experimental/attacks/carla_mot_adversarial_patch.py @@ -23,6 +23,45 @@ def __init__(self, estimator, coco_format=False, **kwargs): super().__init__(estimator=estimator, **kwargs) + # Set`loss.backward(retain_graph=False)` and zero out Adam optimizer gradients before each attack iteration + def _train_step( + self, + images: "torch.Tensor", + target: "torch.Tensor", + mask: Optional["torch.Tensor"] = None, + ) -> "torch.Tensor": + + self.estimator.model.zero_grad() + # only zero gradients when there is a non-pgd optimizer; pgd optimizer appears to perform better when gradients accumulate + if self._optimizer_string == "Adam": + self._optimizer.zero_grad(set_to_none=True) + loss = self._loss(images, target, mask) + loss.backward(retain_graph=False) + + if self._optimizer_string == "pgd": + if self._patch.grad is not None: + gradients = self._patch.grad.sign() * self.learning_rate + else: + raise ValueError("Gradient term in PyTorch model is `None`.") + + with torch.no_grad(): + self._patch[:] = torch.clamp( + self._patch + gradients, + min=self.estimator.clip_values[0], + max=self.estimator.clip_values[1], + ) + else: + self._optimizer.step() + + with torch.no_grad(): + self._patch[:] = torch.clamp( + self._patch, + min=self.estimator.clip_values[0], + max=self.estimator.clip_values[1], + ) + + return loss + def create_initial_image(self, size): """ Create initial patch based on a user-defined image @@ -400,7 +439,7 @@ def generate(self, x, y, y_patch_metadata): # Use this mask to embed patch into the background in the event of occlusion self.patch_masks_video = y_patch_metadata[i]["masks"] - # self._patch needs to be re-initialized with the correct shape + # self._patch and optimizer need to be re-initialized if self.patch_base_image is not None: self.patch_base = self.create_initial_image( (patch_width, patch_height), @@ -412,6 +451,8 @@ def generate(self, x, y, y_patch_metadata): self._patch = torch.tensor( patch_init, requires_grad=True, device=self.estimator.device ) + if self._optimizer_string == "Adam": + self._optimizer = torch.optim.Adam([self._patch], lr=self.learning_rate) # Perform batch attack by attacking multiple frames from the same video for batch_i in range(0, x[i].shape[0], self.batch_frame_size): diff --git a/armory/art_experimental/attacks/carla_obj_det_adversarial_patch.py b/armory/art_experimental/attacks/carla_obj_det_adversarial_patch.py index b571cf876..254499189 100644 --- a/armory/art_experimental/attacks/carla_obj_det_adversarial_patch.py +++ b/armory/art_experimental/attacks/carla_obj_det_adversarial_patch.py @@ -115,8 +115,13 @@ def _train_step( import torch # lgtm [py/repeated-import] self.estimator.model.zero_grad() + # only zero gradients when there is a non-pgd optimizer; pgd optimizer appears to perform better when gradients accumulate + if self._optimizer_string == "Adam": + self._optimizer_rgb.zero_grad(set_to_none=True) + if images.shape[-1] == 6: + self._optimizer_depth.zero_grad(set_to_none=True) loss = self._loss(images, target, mask) - loss.backward(retain_graph=True) + loss.backward(retain_graph=False) if self._optimizer_string == "pgd": patch_grads = self._patch.grad @@ -159,7 +164,6 @@ def _train_step( min=self.min_depth, max=self.max_depth, ) - self.depth_perturbation[:] = perturbed_images - images_depth else: images_depth_linear = rgb_depth_to_linear( images_depth[:, 0, :, :], @@ -175,12 +179,49 @@ def _train_step( perturbed_images = torch.stack( [depth_r, depth_g, depth_b], dim=1 ) - self.depth_perturbation[:] = perturbed_images - images_depth + self.depth_perturbation[:] = perturbed_images - images_depth else: - raise ValueError( - "Adam optimizer for CARLA Adversarial Patch not supported." - ) + self._optimizer_rgb.step() + if images.shape[-1] == 6: + self._optimizer_depth.step() + + with torch.no_grad(): + self._patch[:] = torch.clamp( + self._patch, + min=self.estimator.clip_values[0], + max=self.estimator.clip_values[1], + ) + + if images.shape[-1] == 6: + images_depth = torch.permute(images[:, :, :, 3:], (0, 3, 1, 2)) + if self.depth_type == "log": + perturbed_images = torch.clamp( + images_depth + self.depth_perturbation, + min=self.min_depth, + max=self.max_depth, + ) + else: + images_depth_linear = rgb_depth_to_linear( + images_depth[:, 0, :, :], + images_depth[:, 1, :, :], + images_depth[:, 2, :, :], + ) + depth_linear = rgb_depth_to_linear( + self.depth_perturbation[:, 0, :, :], + self.depth_perturbation[:, 1, :, :], + self.depth_perturbation[:, 2, :, :], + ) + depth_linear = torch.clamp( + images_depth_linear + depth_linear, + min=self.min_depth, + max=self.max_depth, + ) + depth_r, depth_g, depth_b = linear_depth_to_rgb(depth_linear) + perturbed_images = torch.stack( + [depth_r, depth_g, depth_b], dim=1 + ) + self.depth_perturbation[:] = perturbed_images - images_depth return loss @@ -504,6 +545,15 @@ def generate(self, x, y=None, y_patch_metadata=None): device=self.estimator.device, ) + if self._optimizer_string == "Adam": + self._optimizer_rgb = torch.optim.Adam( + [self._patch], lr=self.learning_rate + ) + if x.shape[-1] == 6: + self._optimizer_depth = torch.optim.Adam( + [self.depth_perturbation], lr=self.learning_rate_depth + ) + patch, _ = super().generate(np.expand_dims(x[i], axis=0), y=[y_gt]) # Patch image diff --git a/scenario_configs/eval7/carla_mot/carla_mot_adversarialpatch_undefended.json b/scenario_configs/eval7/carla_mot/carla_mot_adversarialpatch_undefended.json index 4e6bb0993..c3fb56714 100644 --- a/scenario_configs/eval7/carla_mot/carla_mot_adversarialpatch_undefended.json +++ b/scenario_configs/eval7/carla_mot/carla_mot_adversarialpatch_undefended.json @@ -6,9 +6,9 @@ "kwargs": { "batch_frame_size": 2, "coco_format": true, - "learning_rate": 0.02, + "learning_rate": 0.2, "max_iter": 100, - "optimizer": "pgd", + "optimizer": "Adam", "targeted": false, "verbose": true }, diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_undefended.json index 77a91860c..cb9144613 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_undefended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_undefended.json @@ -5,9 +5,9 @@ "knowledge": "white", "kwargs": { "batch_size": 1, - "learning_rate": 0.003, - "max_iter": 1000, - "optimizer": "pgd", + "learning_rate": 0.05, + "max_iter": 500, + "optimizer": "Adam", "targeted": false, "verbose": true }, diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_defended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_defended.json index 8ad114788..d325f830d 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_defended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_defended.json @@ -6,10 +6,10 @@ "kwargs": { "batch_size": 1, "depth_delta_meters": 3, - "learning_rate": 0.003, - "learning_rate_depth": 0.005, + "learning_rate": 0.02, + "learning_rate_depth": 0.0001, "max_iter": 1000, - "optimizer": "pgd", + "optimizer": "Adam", "targeted": false, "verbose": true }, diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_undefended.json index 6153fd746..1822329df 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_undefended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_undefended.json @@ -6,10 +6,10 @@ "kwargs": { "batch_size": 1, "depth_delta_meters": 3, - "learning_rate": 0.003, - "learning_rate_depth": 0.005, + "learning_rate": 0.02, + "learning_rate_depth": 0.0001, "max_iter": 1000, - "optimizer": "pgd", + "optimizer": "Adam", "targeted": false, "verbose": true }, From e4e0fb633a473e1227517f4a1840e7fadbe23087 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 31 Jul 2023 12:49:19 -0500 Subject: [PATCH 068/102] resort imports --- .../art_experimental/attacks/carla_obj_det_patch.py | 2 +- armory/baseline_models/pytorch/yolov3.py | 6 ++---- armory/data/adversarial_datasets.py | 2 +- armory/data/datasets.py | 2 +- armory/metrics/poisoning.py | 2 +- armory/scenarios/poison.py | 2 +- armory/scenarios/poisoning_obj_det.py | 11 ++++------- 7 files changed, 11 insertions(+), 16 deletions(-) diff --git a/armory/art_experimental/attacks/carla_obj_det_patch.py b/armory/art_experimental/attacks/carla_obj_det_patch.py index a158e3ffe..caf3689e6 100644 --- a/armory/art_experimental/attacks/carla_obj_det_patch.py +++ b/armory/art_experimental/attacks/carla_obj_det_patch.py @@ -10,9 +10,9 @@ from armory.art_experimental.attacks.carla_obj_det_utils import ( linear_depth_to_rgb, - rgb_depth_to_linear, linear_to_log, log_to_linear, + rgb_depth_to_linear, ) from armory.logs import log from armory.utils.external_repo import ExternalRepoImport diff --git a/armory/baseline_models/pytorch/yolov3.py b/armory/baseline_models/pytorch/yolov3.py index d8edf4ad4..c51f751ab 100644 --- a/armory/baseline_models/pytorch/yolov3.py +++ b/armory/baseline_models/pytorch/yolov3.py @@ -1,11 +1,9 @@ from typing import Optional -import torch - from art.estimators.object_detection import PyTorchYolo -from pytorchyolo.utils.loss import compute_loss from pytorchyolo.models import load_model - +from pytorchyolo.utils.loss import compute_loss +import torch DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") diff --git a/armory/data/adversarial_datasets.py b/armory/data/adversarial_datasets.py index 400ef3c93..15b2ebd15 100644 --- a/armory/data/adversarial_datasets.py +++ b/armory/data/adversarial_datasets.py @@ -17,8 +17,8 @@ resisc45_densenet121_univpatch_and_univperturbation_adversarial_224x224, ucf101_mars_perturbation_and_patch_adversarial_112x112, ) -from armory.data.adversarial import carla_mot_dev as cmotd # noqa: F401 from armory.data.adversarial import apricot_dev, apricot_test # noqa: F401 +from armory.data.adversarial import carla_mot_dev as cmotd # noqa: F401 from armory.data.adversarial import carla_mot_test as cmott # noqa: F401 from armory.data.adversarial import carla_obj_det_dev as codd # noqa: F401 from armory.data.adversarial import carla_obj_det_test as codt # noqa: F401 diff --git a/armory/data/datasets.py b/armory/data/datasets.py index c5000fecf..7bda8d666 100644 --- a/armory/data/datasets.py +++ b/armory/data/datasets.py @@ -28,10 +28,10 @@ from armory.data.german_traffic_sign import german_traffic_sign as gtsrb # noqa: F401 from armory.data.librispeech import librispeech_dev_clean_split # noqa: F401 from armory.data.librispeech import librispeech_full as lf # noqa: F401 +from armory.data.minicoco import minicoco as mc # noqa: F401 from armory.data.resisc10 import resisc10_poison # noqa: F401 from armory.data.resisc45 import resisc45_split # noqa: F401 from armory.data.ucf101 import ucf101_clean as uc # noqa: F401 -from armory.data.minicoco import minicoco as mc # noqa: F401 from armory.data.utils import ( _read_validate_scenario_config, add_checksums_dir, diff --git a/armory/metrics/poisoning.py b/armory/metrics/poisoning.py index 3eeb43df1..d8133fd2f 100644 --- a/armory/metrics/poisoning.py +++ b/armory/metrics/poisoning.py @@ -3,8 +3,8 @@ from PIL import Image import numpy as np -import torch import tensorflow as tf +import torch from armory.data.utils import maybe_download_weights_from_s3 diff --git a/armory/scenarios/poison.py b/armory/scenarios/poison.py index 863425ae9..ab248ee23 100644 --- a/armory/scenarios/poison.py +++ b/armory/scenarios/poison.py @@ -11,6 +11,7 @@ from tensorflow.random import set_seed as tf_set_seed from armory import metrics +from armory.data import majority_masks as majority_mask_dir from armory.data.datasets import NumpyDataGenerator from armory.instrument import GlobalMeter, LogWriter, Meter, ResultsWriter from armory.instrument.export import ImageClassificationExporter @@ -19,7 +20,6 @@ from armory.scenarios.scenario import Scenario from armory.scenarios.utils import to_categorical from armory.utils import config_loading -from armory.data import majority_masks as majority_mask_dir class DatasetPoisoner: diff --git a/armory/scenarios/poisoning_obj_det.py b/armory/scenarios/poisoning_obj_det.py index 8f6a79d87..a45738e69 100644 --- a/armory/scenarios/poisoning_obj_det.py +++ b/armory/scenarios/poisoning_obj_det.py @@ -1,21 +1,18 @@ import copy -import imgaug.augmenters as iaa from imgaug.augmentables.bbs import BoundingBox, BoundingBoxesOnImage +import imgaug.augmenters as iaa import numpy as np import torch from torchvision.ops import nms from tqdm import tqdm -from armory.logs import log from armory import metrics -from armory.instrument import LogWriter, Meter, ResultsWriter, GlobalMeter +from armory.instrument import GlobalMeter, LogWriter, Meter, ResultsWriter +from armory.instrument.export import ExportMeter, ObjectDetectionExporter +from armory.logs import log from armory.scenarios.poison import Poison from armory.utils import config_loading -from armory.instrument.export import ( - ExportMeter, - ObjectDetectionExporter, -) """ Paper link: https://arxiv.org/pdf/2205.14497.pdf From 88ef7b10f725a1928557c809e3b4176603ede6cd Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 31 Jul 2023 12:56:11 -0500 Subject: [PATCH 069/102] fix E721 --- armory/data/datasets.py | 4 ++-- armory/utils/labels.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/armory/data/datasets.py b/armory/data/datasets.py index 7bda8d666..ebef422b6 100644 --- a/armory/data/datasets.py +++ b/armory/data/datasets.py @@ -644,9 +644,9 @@ def check_shapes(actual, target): actual and target should be tuples actual should not have None values """ - if type(actual) != tuple: + if type(actual) != tuple: # noqa raise ValueError(f"actual shape {actual} is not a tuple") - if type(target) != tuple: + if type(target) != tuple: # noqa raise ValueError(f"target shape {target} is not a tuple") if None in actual: raise ValueError(f"None should not be in actual shape {actual}") diff --git a/armory/utils/labels.py b/armory/utils/labels.py index 26e3ca64d..a8d59a5a9 100644 --- a/armory/utils/labels.py +++ b/armory/utils/labels.py @@ -266,6 +266,6 @@ def _generate(self, y): def generate(self, y): y_target = [self._generate(y_i) for y_i in y] - if type(y) != list: + if type(y) != list: # noqa y_target = np.array(y_target) return y_target From 01d437efc39f97d0acd01ab48af10eae1b48f67b Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 31 Jul 2023 12:58:40 -0500 Subject: [PATCH 070/102] reformat with black --- armory/data/datasets.py | 4 ++-- armory/utils/labels.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/armory/data/datasets.py b/armory/data/datasets.py index ebef422b6..bdd1a5432 100644 --- a/armory/data/datasets.py +++ b/armory/data/datasets.py @@ -644,9 +644,9 @@ def check_shapes(actual, target): actual and target should be tuples actual should not have None values """ - if type(actual) != tuple: # noqa + if type(actual) != tuple: # noqa raise ValueError(f"actual shape {actual} is not a tuple") - if type(target) != tuple: # noqa + if type(target) != tuple: # noqa raise ValueError(f"target shape {target} is not a tuple") if None in actual: raise ValueError(f"None should not be in actual shape {actual}") diff --git a/armory/utils/labels.py b/armory/utils/labels.py index a8d59a5a9..83b0c1876 100644 --- a/armory/utils/labels.py +++ b/armory/utils/labels.py @@ -266,6 +266,6 @@ def _generate(self, y): def generate(self, y): y_target = [self._generate(y_i) for y_i in y] - if type(y) != list: # noqa + if type(y) != list: # noqa y_target = np.array(y_target) return y_target From 7e4cd48d571af5ed7a71a7905dc891c6744a94b7 Mon Sep 17 00:00:00 2001 From: Sterling Suggs Date: Wed, 2 Aug 2023 14:55:54 -0400 Subject: [PATCH 071/102] od poisoning test triggers --- armory/utils/triggers/skull.png | Bin 0 -> 59055 bytes armory/utils/triggers/student-driver.png | Bin 0 -> 151270 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 armory/utils/triggers/skull.png create mode 100644 armory/utils/triggers/student-driver.png diff --git a/armory/utils/triggers/skull.png b/armory/utils/triggers/skull.png new file mode 100644 index 0000000000000000000000000000000000000000..522a60496d04f9c423ed5c4326ec7d346a1697a6 GIT binary patch literal 59055 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�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%0A3xqlJg6^m8$sa4@>p&h)IQ>0{ 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 literal 0 HcmV?d00001 diff --git a/armory/utils/triggers/student-driver.png b/armory/utils/triggers/student-driver.png new file mode 100644 index 0000000000000000000000000000000000000000..9a04fc2026f3cf62d8a8db5980eef0ae8af8f049 GIT binary patch literal 151270 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 z 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 WmJBz*OQ4y;S~#6?btpe|HSvEf#0571 literal 0 HcmV?d00001 From 10377fdf628db725cbaa9a09ee00d1c2402795b4 Mon Sep 17 00:00:00 2001 From: Sterling Suggs Date: Wed, 2 Aug 2023 14:58:18 -0400 Subject: [PATCH 072/102] add min/max size kwargs to targeted configs --- ...carla_obj_det_adversarialpatch_targeted_undefended.json | 2 ++ ...et_multimodal_adversarialpatch_targeted_undefended.json | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json index 3fb73ae67..b43431d0b 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json @@ -51,6 +51,8 @@ "fit": false, "fit_kwargs": {}, "model_kwargs": { + "max_size": 1280, + "min_size": 960, "num_classes": 3 }, "module": "armory.baseline_models.pytorch.carla_single_modality_object_detection_frcnn", diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json index 6141c5454..686e6992e 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json @@ -1,4 +1,4 @@ -{ +i{ "_description": "CARLA multimodality object detection, contributed by MITRE Corporation", "adhoc": null, "attack": { @@ -52,7 +52,10 @@ "model": { "fit": false, "fit_kwargs": {}, - "model_kwargs": {}, + "model_kwargs": { + "max_size": 1280, + "min_size": 960 + }, "module": "armory.baseline_models.pytorch.carla_multimodality_object_detection_frcnn", "name": "get_art_model_mm", "weights_file": "carla_multimodal_naive_weights_eval7and8.pt", From c9ccb094662a203685a27b6386925240a3258fc8 Mon Sep 17 00:00:00 2001 From: Sterling Suggs Date: Thu, 3 Aug 2023 07:34:06 -0400 Subject: [PATCH 073/102] update targeted configs for adam optimizer --- .../carla_obj_det_adversarialpatch_targeted_undefended.json | 6 +++--- ...det_multimodal_adversarialpatch_targeted_undefended.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json index b43431d0b..2ce8ea9a8 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json @@ -5,9 +5,9 @@ "knowledge": "white", "kwargs": { "batch_size": 1, - "learning_rate": 0.003, - "max_iter": 1000, - "optimizer": "pgd", + "learning_rate": 0.05, + "max_iter": 500, + "optimizer": "Adam", "targeted": true, "verbose": true }, diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json index 686e6992e..674063748 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json @@ -6,10 +6,10 @@ i{ "kwargs": { "batch_size": 1, "depth_delta_meters": 3, - "learning_rate": 0.003, - "learning_rate_depth": 0.005, + "learning_rate": 0.02, + "learning_rate_depth": 0.0001, "max_iter": 1000, - "optimizer": "pgd", + "optimizer": "Adam", "targeted": true, "verbose": true }, From 399ac98f4229ebb94dd9fd6942ffdfc57262ccff Mon Sep 17 00:00:00 2001 From: Sterling Suggs Date: Thu, 3 Aug 2023 07:37:06 -0400 Subject: [PATCH 074/102] fix typo --- ...obj_det_multimodal_adversarialpatch_targeted_undefended.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json index 674063748..60e7c14dc 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_multimodal_adversarialpatch_targeted_undefended.json @@ -1,4 +1,4 @@ -i{ +{ "_description": "CARLA multimodality object detection, contributed by MITRE Corporation", "adhoc": null, "attack": { From d6ace3a8d8c541252606dc72d31903ba729e2b11 Mon Sep 17 00:00:00 2001 From: Sterling Suggs Date: Thu, 3 Aug 2023 08:10:59 -0400 Subject: [PATCH 075/102] format --- .../carla_obj_det_adversarialpatch_targeted_undefended.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json index 2ce8ea9a8..fd104ccd4 100644 --- a/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json +++ b/scenario_configs/eval7/carla_overhead_object_detection/carla_obj_det_adversarialpatch_targeted_undefended.json @@ -51,7 +51,7 @@ "fit": false, "fit_kwargs": {}, "model_kwargs": { - "max_size": 1280, + "max_size": 1280, "min_size": 960, "num_classes": 3 }, From 08552ab96362d881d158700cfa7a6a62b4902eb2 Mon Sep 17 00:00:00 2001 From: Sterling Date: Mon, 7 Aug 2023 12:47:59 +0000 Subject: [PATCH 076/102] remove np.object references --- armory/art_experimental/attacks/sweep.py | 2 +- armory/data/datasets.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/armory/art_experimental/attacks/sweep.py b/armory/art_experimental/attacks/sweep.py index 0db31d895..85a6e27a6 100644 --- a/armory/art_experimental/attacks/sweep.py +++ b/armory/art_experimental/attacks/sweep.py @@ -142,7 +142,7 @@ def _is_robust(self, y, y_pred): return metric_result > self.metric_threshold def _get_metric_result(self, y, y_pred): - if isinstance(y, np.ndarray) and y.dtype == np.object: + if isinstance(y, np.ndarray) and y.dtype == object: # convert np object array to list of dicts metric_result = self.metric_fn([y[0]], y_pred) else: diff --git a/armory/data/datasets.py b/armory/data/datasets.py index c5000fecf..20d3718b4 100644 --- a/armory/data/datasets.py +++ b/armory/data/datasets.py @@ -698,14 +698,14 @@ def canonical_variable_image_preprocess(context, batch): """ Preprocessing when images are of variable size """ - if batch.dtype == np.object: + if batch.dtype == object: for x in batch: check_shapes(x.shape, context.x_shape) assert x.dtype == context.input_type assert x.min() >= context.input_min assert x.max() <= context.input_max - quantized_batch = np.zeros_like(batch, dtype=np.object) + quantized_batch = np.zeros_like(batch, dtype=object) for i in range(len(batch)): quantized_batch[i] = ( batch[i].astype(context.output_type) / context.quantization From fc6a6bfb82485b2358d917fd9c570768f45610ca Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 08:16:12 -0500 Subject: [PATCH 077/102] update `yolo` Dockerfile --- docker/Dockerfile-yolo | 14 ++++++++++++++ environment.yml | 7 ++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile-yolo b/docker/Dockerfile-yolo index 434fab4b2..c2d320579 100644 --- a/docker/Dockerfile-yolo +++ b/docker/Dockerfile-yolo @@ -22,4 +22,18 @@ RUN echo "Building Armory from local source" && \ echo "Cleaning up..." && \ rm -rf /armory-repo/.git + +## Tensorflow Patches +# see: https://github.com/tensorflow/tensorflow/issues/56927 +# see: https://github.com/keras-team/keras/issues/17422#issuecomment-1539388368 +ENV CONDA_PREFIX=/opt/conda +ENV XLA_FLAGS=--xla_gpu_cuda_data_dir=$CONDA_PREFIX/lib/ + +RUN echo "Applying TensorFlow patches" && \ + mamba install -c nvidia cuda-nvcc=11.3.58 && \ + mkdir -p $CONDA_PREFIX/etc/conda/activate.d && \ + mkdir -p $CONDA_PREFIX/lib/nvvm/libdevice && \ + cp $CONDA_PREFIX/lib/libdevice.10.bc $CONDA_PREFIX/lib/nvvm/libdevice/ + + WORKDIR /workspace diff --git a/environment.yml b/environment.yml index 44c9ec1b6..2a4c5b9f6 100644 --- a/environment.yml +++ b/environment.yml @@ -16,10 +16,7 @@ dependencies: - matplotlib - conda-forge::ffmpeg # conda-forge ffmpeg comes with libx264 encoder, which the pytorch channel version does not include. This encoder is required for video compression defenses (ART) and video exporting. Future work could migrate this to libopenh264 encoder, which is available in both channels. - librosa + - cudnn # cudnn required for tensorflow - pandas - protobuf - - nvidia # Required for tensorflow. See: https://github.com/tensorflow/tensorflow/issues/58681#issuecomment-1406967453 - - cuda-nvcc # Required for tensorflow. See: https://github.com/tensorflow/tensorflow/issues/58681#issuecomment-1406967453 - - cudnn # cudnn required for tensorflow - -prefix: /opt/conda +prefix: /opt/conda \ No newline at end of file From feb658f9e29b3fb451b9a0292bece2f03680e211 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 08:23:17 -0500 Subject: [PATCH 078/102] migrate patch to deepspeech --- docker/Dockerfile-pytorch-deepspeech | 14 ++++++++++++++ docker/Dockerfile-yolo | 14 -------------- environment.yml | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docker/Dockerfile-pytorch-deepspeech b/docker/Dockerfile-pytorch-deepspeech index 7df8c8c39..70addfbf9 100644 --- a/docker/Dockerfile-pytorch-deepspeech +++ b/docker/Dockerfile-pytorch-deepspeech @@ -20,6 +20,20 @@ RUN echo "Building Armory from local source" echo "Cleaning up..." && \ rm -rf /armory-repo/.git + +## Tensorflow Patches +# see: https://github.com/tensorflow/tensorflow/issues/56927 +# see: https://github.com/keras-team/keras/issues/17422#issuecomment-1539388368 +ENV CONDA_PREFIX=/opt/conda +ENV XLA_FLAGS=--xla_gpu_cuda_data_dir=$CONDA_PREFIX/lib/ + +RUN echo "Applying TensorFlow patches" && \ + mamba install -c nvidia cuda-nvcc=11.3.58 && \ + mkdir -p $CONDA_PREFIX/etc/conda/activate.d && \ + mkdir -p $CONDA_PREFIX/lib/nvvm/libdevice && \ + cp $CONDA_PREFIX/lib/libdevice.10.bc $CONDA_PREFIX/lib/nvvm/libdevice/ + + WORKDIR /workspace diff --git a/docker/Dockerfile-yolo b/docker/Dockerfile-yolo index c2d320579..434fab4b2 100644 --- a/docker/Dockerfile-yolo +++ b/docker/Dockerfile-yolo @@ -22,18 +22,4 @@ RUN echo "Building Armory from local source" && \ echo "Cleaning up..." && \ rm -rf /armory-repo/.git - -## Tensorflow Patches -# see: https://github.com/tensorflow/tensorflow/issues/56927 -# see: https://github.com/keras-team/keras/issues/17422#issuecomment-1539388368 -ENV CONDA_PREFIX=/opt/conda -ENV XLA_FLAGS=--xla_gpu_cuda_data_dir=$CONDA_PREFIX/lib/ - -RUN echo "Applying TensorFlow patches" && \ - mamba install -c nvidia cuda-nvcc=11.3.58 && \ - mkdir -p $CONDA_PREFIX/etc/conda/activate.d && \ - mkdir -p $CONDA_PREFIX/lib/nvvm/libdevice && \ - cp $CONDA_PREFIX/lib/libdevice.10.bc $CONDA_PREFIX/lib/nvvm/libdevice/ - - WORKDIR /workspace diff --git a/environment.yml b/environment.yml index 2a4c5b9f6..893972208 100644 --- a/environment.yml +++ b/environment.yml @@ -19,4 +19,4 @@ dependencies: - cudnn # cudnn required for tensorflow - pandas - protobuf -prefix: /opt/conda \ No newline at end of file +prefix: /opt/conda From ce5943d3b9db5bbd4e14075573247ca85a677b76 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 08:35:28 -0500 Subject: [PATCH 079/102] update hydra install --- docker/Dockerfile-pytorch-deepspeech | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile-pytorch-deepspeech b/docker/Dockerfile-pytorch-deepspeech index 70addfbf9..3fbe51b28 100644 --- a/docker/Dockerfile-pytorch-deepspeech +++ b/docker/Dockerfile-pytorch-deepspeech @@ -8,7 +8,9 @@ WORKDIR /armory-repo # in the root of the repo. COPY ./ /armory-repo -RUN pip install git+https://github.com/romesco/hydra-lightning/\#subdirectory=hydra-configs-pytorch-lightning +RUN echo "Installing Hydra" && \ + pip install hydra-core && \ + pip install git+https://github.com/romesco/hydra-lightning/\#subdirectory=hydra-configs-pytorch-lightning RUN echo "Building Armory from local source" && \ echo "Updating Base Image..." && \ @@ -20,7 +22,6 @@ RUN echo "Building Armory from local source" echo "Cleaning up..." && \ rm -rf /armory-repo/.git - ## Tensorflow Patches # see: https://github.com/tensorflow/tensorflow/issues/56927 # see: https://github.com/keras-team/keras/issues/17422#issuecomment-1539388368 @@ -40,9 +41,6 @@ WORKDIR /workspace # ------------------------------------------------------------------ # DEVELOPER NOTES: # ------------------------------------------------------------------ -# TODO: determine if this environment setup is needed -# $ ENV LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:/usr/local/cuda/lib64" - # NOTE: # - pytorch-lightning >= 1.5.0 will break Deep Speech 2 # - torchmetrics >= 0.8.0 will break pytorch-lightning 1.4 From af368750cab72053a02625ec1bf6e791cfb3997a Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 10:04:44 -0500 Subject: [PATCH 080/102] pin tensorflow version in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 888b2e816..a34acf2dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ pytorch = [ tensorflow = [ "armory-testbed[engine,datasets,math]", "tf-models-official", - "tensorflow >= 2.10.0", + "tensorflow == 2.10.0", ] deepspeech = [ From 0d779286aad3d91fa03baf6931aadbd5a3bbccd8 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 10:36:33 -0500 Subject: [PATCH 081/102] pin tensorflow to version 2.10.0 --- docker/Dockerfile-armory | 4 ++++ docker/Dockerfile-pytorch-deepspeech | 13 ------------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/docker/Dockerfile-armory b/docker/Dockerfile-armory index 6ede39168..bbab66d80 100644 --- a/docker/Dockerfile-armory +++ b/docker/Dockerfile-armory @@ -30,4 +30,8 @@ RUN pip install \ # Requires cython for install, so will fail if run in the same pip install as cython pip install cython-bbox +## Pin Tensorflow version +# see: https://github.com/twosixlabs/armory/pull/1968 +RUN pip install --upgrade tensorflow==2.10.0 + WORKDIR /workspace diff --git a/docker/Dockerfile-pytorch-deepspeech b/docker/Dockerfile-pytorch-deepspeech index 3fbe51b28..4cd83654a 100644 --- a/docker/Dockerfile-pytorch-deepspeech +++ b/docker/Dockerfile-pytorch-deepspeech @@ -22,19 +22,6 @@ RUN echo "Building Armory from local source" echo "Cleaning up..." && \ rm -rf /armory-repo/.git -## Tensorflow Patches -# see: https://github.com/tensorflow/tensorflow/issues/56927 -# see: https://github.com/keras-team/keras/issues/17422#issuecomment-1539388368 -ENV CONDA_PREFIX=/opt/conda -ENV XLA_FLAGS=--xla_gpu_cuda_data_dir=$CONDA_PREFIX/lib/ - -RUN echo "Applying TensorFlow patches" && \ - mamba install -c nvidia cuda-nvcc=11.3.58 && \ - mkdir -p $CONDA_PREFIX/etc/conda/activate.d && \ - mkdir -p $CONDA_PREFIX/lib/nvvm/libdevice && \ - cp $CONDA_PREFIX/lib/libdevice.10.bc $CONDA_PREFIX/lib/nvvm/libdevice/ - - WORKDIR /workspace From 266dd0364e441257334e76d08beaed5a06ef8aff Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 10:57:14 -0500 Subject: [PATCH 082/102] remove `--upgrade` tag --- docker/Dockerfile-armory | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile-armory b/docker/Dockerfile-armory index bbab66d80..2cfd1c548 100644 --- a/docker/Dockerfile-armory +++ b/docker/Dockerfile-armory @@ -32,6 +32,6 @@ RUN pip install \ ## Pin Tensorflow version # see: https://github.com/twosixlabs/armory/pull/1968 -RUN pip install --upgrade tensorflow==2.10.0 +RUN pip install tensorflow==2.10.0 WORKDIR /workspace From f34e4a87868b2d4520f81ad07484f9787e510b38 Mon Sep 17 00:00:00 2001 From: Sterling Date: Mon, 7 Aug 2023 18:44:00 +0000 Subject: [PATCH 083/102] update dataset test for new dataset splits --- tests/end_to_end/test_e2e_datasets.py | 53 ++++++++++++++------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/tests/end_to_end/test_e2e_datasets.py b/tests/end_to_end/test_e2e_datasets.py index c168b8387..9e1955867 100644 --- a/tests/end_to_end/test_e2e_datasets.py +++ b/tests/end_to_end/test_e2e_datasets.py @@ -706,33 +706,36 @@ def test_carla_overhead_obj_det_dev(): def test_carla_overhead_obj_det_test(): - ds_rgb = adversarial_datasets.carla_over_obj_det_test(split="test", modality="rgb") - ds_depth = adversarial_datasets.carla_over_obj_det_test( - split="test", modality="depth" - ) - ds_multimodal = adversarial_datasets.carla_over_obj_det_test( - split="test", modality="both" - ) + for split in ["test_hallucination", "test_disappearance"]: + ds_rgb = adversarial_datasets.carla_over_obj_det_test( + split=split, modality="rgb" + ) + ds_depth = adversarial_datasets.carla_over_obj_det_test( + split=split, modality="depth" + ) + ds_multimodal = adversarial_datasets.carla_over_obj_det_test( + split=split, modality="both" + ) - for i, ds in enumerate([ds_multimodal, ds_rgb, ds_depth]): - assert ds.size == 15 - for x, y in ds: - if i == 0: - assert x.shape == (1, 960, 1280, 6) - else: - assert x.shape == (1, 960, 1280, 3) + for i, ds in enumerate([ds_multimodal, ds_rgb, ds_depth]): + assert ds.size == 25 + for x, y in ds: + if i == 0: + assert x.shape == (1, 960, 1280, 6) + else: + assert x.shape == (1, 960, 1280, 3) - y_object, y_patch_metadata = y - assert isinstance(y_object, dict) - for obj_key in ["labels", "boxes", "area"]: - assert obj_key in y_object - assert isinstance(y_patch_metadata, dict) - for patch_key in [ - "avg_patch_depth", - "gs_coords", - "mask", - ]: - assert patch_key in y_patch_metadata + y_object, y_patch_metadata = y + assert isinstance(y_object, dict) + for obj_key in ["labels", "boxes", "area"]: + assert obj_key in y_object + assert isinstance(y_patch_metadata, dict) + for patch_key in [ + "avg_patch_depth", + "gs_coords", + "mask", + ]: + assert patch_key in y_patch_metadata def test_carla_video_tracking_dev(): From 8402c9c1f69a67c999dc216023c02506ad290a62 Mon Sep 17 00:00:00 2001 From: Jonathan Prokos Date: Mon, 7 Aug 2023 18:58:47 +0000 Subject: [PATCH 084/102] Adding plot util function to . --- armory/__main__.py | 10 +++++++++- armory/cli/tools.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/armory/__main__.py b/armory/__main__.py index 5daa504ca..e77f5f53b 100644 --- a/armory/__main__.py +++ b/armory/__main__.py @@ -19,7 +19,11 @@ import armory from armory import arguments, paths -from armory.cli.tools import log_current_branch, rgb_depth_convert +from armory.cli.tools import ( + log_current_branch, + plot_mAP_by_giou_with_patch_cli, + rgb_depth_convert, +) from armory.configuration import load_global_config, save_config from armory.eval import Evaluator import armory.logs @@ -722,6 +726,10 @@ def exec(command_args, prog, description): UTILS_COMMANDS = { "get-branch": (log_current_branch, "log the current git branch of armory"), "rgb-convert": (rgb_depth_convert, "converts rgb depth images to another format"), + "plot-mAP-by-giou": ( + plot_mAP_by_giou_with_patch_cli, + "Visualize the output of the metric 'object_detection_AP_per_class_by_giou_from_patch.'", + ), } diff --git a/armory/cli/tools.py b/armory/cli/tools.py index a5ec0e832..de9d10687 100644 --- a/armory/cli/tools.py +++ b/armory/cli/tools.py @@ -295,3 +295,48 @@ def apply_action(but, dtype): # Show the plot plt.show() + + +def plot_mAP_by_giou_with_patch_cli(command_args, prog, description): + from armory.postprocessing.plot_patch_aware_carla_metric import ( + plot_mAP_by_giou_with_patch, + ) + + parser = argparse.ArgumentParser( + prog=prog, + description=description, + formatter_class=argparse.RawTextHelpFormatter, + ) + + parser.add_argument( + "input", + type=Path, + help="Path to json. Must have 'results.adversarial_object_detection_AP_per_class_by_giou_from_patch' key.", + ) + parser.add_argument( + "--flavors", + type=str, + nargs="+", + default=None, + choices=["cumulative_by_max_giou", "cumulative_by_min_giou", "histogram_left"], + help="Flavors of mAP by giou to plot. Subset of ['cumulative_by_max_giou', 'cumulative_by_min_giou', 'histogram_left'] or None to plot all.", + ) + parser.add_argument("--headless", action="store_true", help="Don't show the plot") + parser.add_argument( + "--output", type=Path, default=None, help="Path to save the plot" + ) + parser.add_argument( + "--exclude-classes", + action="store_true", + help="Don't include subplot for each class.", + ) + _debug(parser) + + args = parser.parse_args(command_args) + plot_mAP_by_giou_with_patch( + args.input, + flavors=args.flavors, + show=not args.headless, + output_filepath=args.output, + include_classes=not args.exclude_classes, + ) From 60c4985e5c9faa24790a5380863532d0d9a91dea Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 16:18:55 -0500 Subject: [PATCH 085/102] update environment markers --- docker/Dockerfile-armory | 3 --- docker/Dockerfile-base | 5 +++-- pyproject.toml | 3 ++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docker/Dockerfile-armory b/docker/Dockerfile-armory index 2cfd1c548..b3c6c436e 100644 --- a/docker/Dockerfile-armory +++ b/docker/Dockerfile-armory @@ -30,8 +30,5 @@ RUN pip install \ # Requires cython for install, so will fail if run in the same pip install as cython pip install cython-bbox -## Pin Tensorflow version -# see: https://github.com/twosixlabs/armory/pull/1968 -RUN pip install tensorflow==2.10.0 WORKDIR /workspace diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 7cadb36d5..ac535bb62 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -60,8 +60,8 @@ RUN mamba env update -f environment.yml -n base --prune \ # NOTE: Armory requirements and ART requirements are installed here to make patch updates fast and small RUN echo "Installing TensorFlow and ART/Armory requirements via pip" RUN /opt/conda/bin/pip install --no-cache-dir \ - tensorflow-datasets==4.6 \ - tensorflow==2.10 \ + # tensorflow-datasets==4.6 \ + # tensorflow==2.10 \ tensorboardx \ boto3 \ opencv-python \ @@ -79,4 +79,5 @@ RUN /opt/conda/bin/pip install --no-cache-dir \ # transformers is used for the Entailment metric only # pydub required for ART mp3 defense + WORKDIR /workspace diff --git a/pyproject.toml b/pyproject.toml index a34acf2dd..ed8adde39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,8 @@ pytorch = [ tensorflow = [ "armory-testbed[engine,datasets,math]", "tf-models-official", - "tensorflow == 2.10.0", + "tensorflow == 2.10.0; python_version <= 3.10", + "tensorflow == 2.13.0; python_version >= 3.10", ] deepspeech = [ From da30341582e534fa36c34d19dd47a27c109353fa Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 16:26:23 -0500 Subject: [PATCH 086/102] format environment markers --- docker/Dockerfile-base | 35 ++++++++++++----------------------- pyproject.toml | 4 ++-- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index ac535bb62..6d0547e32 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -10,6 +10,16 @@ FROM nvidia/cuda:11.6.2-cudnn8-runtime-ubuntu20.04 +ENV PATH=/opt/conda/bin:$PATH +# TensorFlow requirement +ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/conda/lib/ + +WORKDIR /armory-repo/ + +# NOTE: This COPY command is filtered using the `.dockerignore` file +# in the root of the repo. +COPY ./ /armory-repo + # Temporary fix for broken nvidia package checksum # RUN rm -f /etc/apt/sources.list.d/nvidia-ml.list @@ -33,23 +43,11 @@ RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86 rm ~/miniconda.sh && \ /opt/conda/bin/conda clean -tipsy && \ ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh -# ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \ -# echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc && \ -# echo "conda activate base" >> ~/.bashrc && \ -# echo 'alias ll="ls -al"' >> ~/.bashrc - -ENV PATH=/opt/conda/bin:$PATH - -# TensorFlow requirement -ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/conda/lib/ # NOTE: using mamba because conda fails when trying to solve for environment RUN conda install -c conda-forge -n base mamba \ && conda clean --all -WORKDIR /armory-repo/ -COPY environment.yml /armory-repo/ - RUN mamba env update -f environment.yml -n base --prune \ && mamba clean --all @@ -60,17 +58,6 @@ RUN mamba env update -f environment.yml -n base --prune \ # NOTE: Armory requirements and ART requirements are installed here to make patch updates fast and small RUN echo "Installing TensorFlow and ART/Armory requirements via pip" RUN /opt/conda/bin/pip install --no-cache-dir \ - # tensorflow-datasets==4.6 \ - # tensorflow==2.10 \ - tensorboardx \ - boto3 \ - opencv-python \ - ffmpeg-python \ - pytest \ - loguru \ - docker \ - jsonschema \ - requests \ pydub \ transformers \ six \ @@ -79,5 +66,7 @@ RUN /opt/conda/bin/pip install --no-cache-dir \ # transformers is used for the Entailment metric only # pydub required for ART mp3 defense +# Install base requirements +RUN pip install --editable '.[engine]' WORKDIR /workspace diff --git a/pyproject.toml b/pyproject.toml index ed8adde39..cbf61fb37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,8 +85,8 @@ pytorch = [ tensorflow = [ "armory-testbed[engine,datasets,math]", "tf-models-official", - "tensorflow == 2.10.0; python_version <= 3.10", - "tensorflow == 2.13.0; python_version >= 3.10", + "tensorflow == 2.10.0; python_version <= '3.10'", + "tensorflow == 2.13.0; python_version >= '3.10'", ] deepspeech = [ From 40c729bcead6f3789148f6b6883fb9c8c42340cd Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 16:31:57 -0500 Subject: [PATCH 087/102] loosen version ranges --- docker/Dockerfile-armory | 1 - pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docker/Dockerfile-armory b/docker/Dockerfile-armory index b3c6c436e..6ede39168 100644 --- a/docker/Dockerfile-armory +++ b/docker/Dockerfile-armory @@ -30,5 +30,4 @@ RUN pip install \ # Requires cython for install, so will fail if run in the same pip install as cython pip install cython-bbox - WORKDIR /workspace diff --git a/pyproject.toml b/pyproject.toml index cbf61fb37..4713ffe2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ tensorflow = [ "armory-testbed[engine,datasets,math]", "tf-models-official", "tensorflow == 2.10.0; python_version <= '3.10'", - "tensorflow == 2.13.0; python_version >= '3.10'", + "tensorflow >= 2.12.0; python_version >= '3.10'", ] deepspeech = [ From fff13230902cc0a18106b1151f9016b2250d4276 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 16:35:38 -0500 Subject: [PATCH 088/102] expand versions once more --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4713ffe2f..e2f25b4e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ tensorflow = [ "armory-testbed[engine,datasets,math]", "tf-models-official", "tensorflow == 2.10.0; python_version <= '3.10'", - "tensorflow >= 2.12.0; python_version >= '3.10'", + "tensorflow >= 2.10.0; python_version >= '3.10'", ] deepspeech = [ From 819500f75bb06580a97c099b803e1fc17c35dd24 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 16:44:43 -0500 Subject: [PATCH 089/102] update `python_version` --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e2f25b4e7..48785cf61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,8 +85,8 @@ pytorch = [ tensorflow = [ "armory-testbed[engine,datasets,math]", "tf-models-official", - "tensorflow == 2.10.0; python_version <= '3.10'", - "tensorflow >= 2.10.0; python_version >= '3.10'", + "tensorflow == 2.10.0; python_version < '3.10'", + "tensorflow >= 2.5.0; python_version >= '3.10'", ] deepspeech = [ From e82aeb3d4cf6027e0ddf9d95d91d37fa433e15ad Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 17:20:39 -0500 Subject: [PATCH 090/102] bump `scikit-learn` version --- docker/Dockerfile-base | 2 +- environment.yml | 5 +---- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 6d0547e32..e5138e8be 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -67,6 +67,6 @@ RUN /opt/conda/bin/pip install --no-cache-dir \ # pydub required for ART mp3 defense # Install base requirements -RUN pip install --editable '.[engine]' +RUN pip install --editable ".[engine,pytorch,tensorflow,jupyter]" WORKDIR /workspace diff --git a/environment.yml b/environment.yml index 893972208..b1852339f 100644 --- a/environment.yml +++ b/environment.yml @@ -5,13 +5,10 @@ channels: - conda-forge - defaults dependencies: - - pytorch=1.12 - torchvision - torchaudio - cudatoolkit=11.6 - - scikit-learn=1.0 # ART requires scikit-learn >=0.22.2,<1.1.0 - - jupyterlab - - jupyterlab_widgets + - scikit-learn=1.0 # ART requires scikit-learn >=0.22.2,<1.2.0 - ipywidgets - matplotlib - conda-forge::ffmpeg # conda-forge ffmpeg comes with libx264 encoder, which the pytorch channel version does not include. This encoder is required for video compression defenses (ART) and video exporting. Future work could migrate this to libopenh264 encoder, which is available in both channels. diff --git a/pyproject.toml b/pyproject.toml index 48785cf61..997096227 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ math = [ "numpy", "pandas", "scipy >= 1.4.1", - "scikit-learn < 1.1.0", # ART requires scikit-learn >=0.22.2,<1.1.0 + "scikit-learn>=0.22.2,<1.2.0", # ART requires scikit-learn >=0.22.2,<1.2.0 "matplotlib", ] From cb8749b4d151034d652d9ffde378497626fc5829 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Mon, 7 Aug 2023 17:36:14 -0500 Subject: [PATCH 091/102] update base requirements in `environment.yaml` --- docker/Dockerfile-base | 18 ++++++++++++------ docker/Dockerfile-pytorch-deepspeech | 1 - environment.yml | 6 +++++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index e5138e8be..3ef8af93e 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -16,9 +16,7 @@ ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/conda/lib/ WORKDIR /armory-repo/ -# NOTE: This COPY command is filtered using the `.dockerignore` file -# in the root of the repo. -COPY ./ /armory-repo +COPY environment.yml /armory-repo/ # Temporary fix for broken nvidia package checksum # RUN rm -f /etc/apt/sources.list.d/nvidia-ml.list @@ -58,6 +56,17 @@ RUN mamba env update -f environment.yml -n base --prune \ # NOTE: Armory requirements and ART requirements are installed here to make patch updates fast and small RUN echo "Installing TensorFlow and ART/Armory requirements via pip" RUN /opt/conda/bin/pip install --no-cache-dir \ + tensorflow-datasets \ + tensorflow \ + tensorboardx \ + boto3 \ + opencv-python \ + ffmpeg-python \ + pytest \ + loguru \ + docker \ + jsonschema \ + requests \ pydub \ transformers \ six \ @@ -66,7 +75,4 @@ RUN /opt/conda/bin/pip install --no-cache-dir \ # transformers is used for the Entailment metric only # pydub required for ART mp3 defense -# Install base requirements -RUN pip install --editable ".[engine,pytorch,tensorflow,jupyter]" - WORKDIR /workspace diff --git a/docker/Dockerfile-pytorch-deepspeech b/docker/Dockerfile-pytorch-deepspeech index 4cd83654a..7b70a4c6b 100644 --- a/docker/Dockerfile-pytorch-deepspeech +++ b/docker/Dockerfile-pytorch-deepspeech @@ -28,7 +28,6 @@ WORKDIR /workspace # ------------------------------------------------------------------ # DEVELOPER NOTES: # ------------------------------------------------------------------ -# NOTE: # - pytorch-lightning >= 1.5.0 will break Deep Speech 2 # - torchmetrics >= 0.8.0 will break pytorch-lightning 1.4 # - hydra-lightning installs omegaconf diff --git a/environment.yml b/environment.yml index b1852339f..673d5e76b 100644 --- a/environment.yml +++ b/environment.yml @@ -5,10 +5,14 @@ channels: - conda-forge - defaults dependencies: + - tensorflow + - pytorch - torchvision - torchaudio - cudatoolkit=11.6 - - scikit-learn=1.0 # ART requires scikit-learn >=0.22.2,<1.2.0 + - scikit-learn=1.0 # ART requires scikit-learn >=0.22.2,<1.1.0 + - jupyterlab + - jupyterlab_widgets - ipywidgets - matplotlib - conda-forge::ffmpeg # conda-forge ffmpeg comes with libx264 encoder, which the pytorch channel version does not include. This encoder is required for video compression defenses (ART) and video exporting. Future work could migrate this to libopenh264 encoder, which is available in both channels. From 6a5a5f8f33193562d79d72770b14252e753a28f1 Mon Sep 17 00:00:00 2001 From: swsuggs <15131284+swsuggs@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:12:47 -0400 Subject: [PATCH 092/102] fix type checking Co-authored-by: Jonathan Prokos <107635150+jprokos26@users.noreply.github.com> --- armory/metrics/task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armory/metrics/task.py b/armory/metrics/task.py index 0a2b8e018..66aeead43 100644 --- a/armory/metrics/task.py +++ b/armory/metrics/task.py @@ -586,7 +586,7 @@ def _area_of_polygon(points): points is an array or list of shape (N, 2) and points may be in clockwise or counterclockwise order """ - if type(points) == list: + if isinstance(points, list): points = np.array(points) assert points.shape[1] == 2 From 90ae5b3111890e578564d920bd608f1b4e00b720 Mon Sep 17 00:00:00 2001 From: swsuggs <15131284+swsuggs@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:15:44 -0400 Subject: [PATCH 093/102] fix type checking Co-authored-by: Jonathan Prokos <107635150+jprokos26@users.noreply.github.com> --- armory/postprocessing/plot_patch_aware_carla_metric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armory/postprocessing/plot_patch_aware_carla_metric.py b/armory/postprocessing/plot_patch_aware_carla_metric.py index 1552b9112..3cd2e3221 100644 --- a/armory/postprocessing/plot_patch_aware_carla_metric.py +++ b/armory/postprocessing/plot_patch_aware_carla_metric.py @@ -62,7 +62,7 @@ def _init_plots(json_filepath, include_classes, n_flavors=1): fig, axes = plt.subplots(n_class + 1, n_flavors, sharex=True, sharey=True) # Ensure axes are in an array for consistent reference - if type(axes) != np.array or len(axes.shape) == 1: + if not isinstance(axes, np.ndarray) or len(axes.shape) == 1: axes = np.array(axes).reshape(n_class + 1, n_flavors) axes[0, 0].set_ylabel("Mean AP") From cab8d3abb470276530e753f2927d36fd30887663 Mon Sep 17 00:00:00 2001 From: swsuggs <15131284+swsuggs@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:17:04 -0400 Subject: [PATCH 094/102] Add error message for plot utility Co-authored-by: Jonathan Prokos <107635150+jprokos26@users.noreply.github.com> --- armory/postprocessing/plot_patch_aware_carla_metric.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/armory/postprocessing/plot_patch_aware_carla_metric.py b/armory/postprocessing/plot_patch_aware_carla_metric.py index 3cd2e3221..32852d0da 100644 --- a/armory/postprocessing/plot_patch_aware_carla_metric.py +++ b/armory/postprocessing/plot_patch_aware_carla_metric.py @@ -48,6 +48,8 @@ def _init_plots(json_filepath, include_classes, n_flavors=1): blob = json.load(f) results = blob["results"] + if "adversarial_object_detection_AP_per_class_by_giou_from_patch" not in results: + raise ValueError("Provided json does not have results.adversarial_object_detection_AP_per_class_by_giou_from_patch.") adv_giou = results["adversarial_object_detection_AP_per_class_by_giou_from_patch"][ 0 ] From 7d5b0163c77fa3a2ea06635fd3c13a9a8751f00f Mon Sep 17 00:00:00 2001 From: Sterling Date: Tue, 8 Aug 2023 13:23:22 +0000 Subject: [PATCH 095/102] formatting --- armory/baseline_models/pytorch/yolov3.py | 6 ++---- armory/instrument/__init__.py | 2 +- armory/postprocessing/plot_patch_aware_carla_metric.py | 5 +++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/armory/baseline_models/pytorch/yolov3.py b/armory/baseline_models/pytorch/yolov3.py index d8edf4ad4..c51f751ab 100644 --- a/armory/baseline_models/pytorch/yolov3.py +++ b/armory/baseline_models/pytorch/yolov3.py @@ -1,11 +1,9 @@ from typing import Optional -import torch - from art.estimators.object_detection import PyTorchYolo -from pytorchyolo.utils.loss import compute_loss from pytorchyolo.models import load_model - +from pytorchyolo.utils.loss import compute_loss +import torch DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") diff --git a/armory/instrument/__init__.py b/armory/instrument/__init__.py index 9fceab94c..668b3359d 100644 --- a/armory/instrument/__init__.py +++ b/armory/instrument/__init__.py @@ -9,8 +9,8 @@ NullWriter, PrintWriter, Probe, - ResultsWriter, ResultsLogWriter, + ResultsWriter, Writer, del_globals, get_hub, diff --git a/armory/postprocessing/plot_patch_aware_carla_metric.py b/armory/postprocessing/plot_patch_aware_carla_metric.py index 32852d0da..cb0d86f0c 100644 --- a/armory/postprocessing/plot_patch_aware_carla_metric.py +++ b/armory/postprocessing/plot_patch_aware_carla_metric.py @@ -26,7 +26,6 @@ from matplotlib import pyplot as plt import numpy as np - ben_color = "tab:blue" adv_color = "tab:red" @@ -49,7 +48,9 @@ def _init_plots(json_filepath, include_classes, n_flavors=1): results = blob["results"] if "adversarial_object_detection_AP_per_class_by_giou_from_patch" not in results: - raise ValueError("Provided json does not have results.adversarial_object_detection_AP_per_class_by_giou_from_patch.") + raise ValueError( + "Provided json does not have results.adversarial_object_detection_AP_per_class_by_giou_from_patch." + ) adv_giou = results["adversarial_object_detection_AP_per_class_by_giou_from_patch"][ 0 ] From 3d82490316fdb333f73625aac8afde43a7b0fbb9 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Tue, 8 Aug 2023 08:25:54 -0500 Subject: [PATCH 096/102] pin pytorch to version 1.12 in `pyproject` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 997096227..41887dc17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ engine = [ pytorch = [ "armory-testbed[engine,datasets,math]", - "torch", + "torch == 1.12", # Pytorch 2 will break parts of armory "torchaudio", "torchvision", ] From cf2943a6a1b249b11be7a0e7ed030bbf1b3a436a Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Tue, 8 Aug 2023 08:26:05 -0500 Subject: [PATCH 097/102] update deepspeech requirements --- docker/Dockerfile-pytorch-deepspeech | 4 +--- pyproject.toml | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile-pytorch-deepspeech b/docker/Dockerfile-pytorch-deepspeech index 7b70a4c6b..9469cd344 100644 --- a/docker/Dockerfile-pytorch-deepspeech +++ b/docker/Dockerfile-pytorch-deepspeech @@ -8,9 +8,7 @@ WORKDIR /armory-repo # in the root of the repo. COPY ./ /armory-repo -RUN echo "Installing Hydra" && \ - pip install hydra-core && \ - pip install git+https://github.com/romesco/hydra-lightning/\#subdirectory=hydra-configs-pytorch-lightning +RUN pip install git+https://github.com/romesco/hydra-lightning/\#subdirectory=hydra-configs-pytorch-lightning RUN echo "Building Armory from local source" && \ echo "Updating Base Image..." && \ diff --git a/pyproject.toml b/pyproject.toml index 41887dc17..3ae241bdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ engine = [ pytorch = [ "armory-testbed[engine,datasets,math]", - "torch == 1.12", # Pytorch 2 will break parts of armory + "torch >=1.12.0,<=1.13.1", # Pytorch 2 will break parts of armory "torchaudio", "torchvision", ] @@ -98,6 +98,7 @@ deepspeech = [ "google-cloud-storage", "transformers", "pytorch-lightning < 1.5.0", + "hydra-core", ] math = [ From b3da63688056ec1c4524ab5dc2da993db22eb944 Mon Sep 17 00:00:00 2001 From: Sterling Date: Tue, 8 Aug 2023 13:28:26 +0000 Subject: [PATCH 098/102] isort --- armory/baseline_models/pytorch/yolov3.py | 1 - 1 file changed, 1 deletion(-) diff --git a/armory/baseline_models/pytorch/yolov3.py b/armory/baseline_models/pytorch/yolov3.py index 83c29bfe7..b8884d7aa 100644 --- a/armory/baseline_models/pytorch/yolov3.py +++ b/armory/baseline_models/pytorch/yolov3.py @@ -7,7 +7,6 @@ from armory.baseline_models import model_configs - DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") From 2c100e173b9a66158d7b61e3b34764c3b0dc0dc0 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Tue, 8 Aug 2023 11:48:06 -0500 Subject: [PATCH 099/102] update base image --- docker/Dockerfile-base | 71 ++++++++-------------------------- docker/build-base.sh | 88 +++++++++++++++++++++--------------------- environment.yml | 38 +++++++++++++----- pyproject.toml | 2 +- 4 files changed, 89 insertions(+), 110 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 7cadb36d5..406b71ff9 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -10,8 +10,12 @@ FROM nvidia/cuda:11.6.2-cudnn8-runtime-ubuntu20.04 -# Temporary fix for broken nvidia package checksum -# RUN rm -f /etc/apt/sources.list.d/nvidia-ml.list +ENV PATH=/opt/mamba/bin:$PATH +ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/mamba/bin + +WORKDIR /armory-repo/ + +COPY environment.yml /armory-repo/ # Basic Apt-get Bits RUN apt-get -y -qq update && \ @@ -23,60 +27,17 @@ RUN apt-get -y -qq update && \ git \ curl \ libgl1-mesa-glx \ -# libglib2.0-0 \ + libglib2.0-0 \ + libarchive13 \ && rm -rf /var/lib/apt/lists/* # libgl1-mesa-glx is needed for cv2 (opencv-python) - -# Install Conda -RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \ - /bin/bash ~/miniconda.sh -b -p /opt/conda && \ - rm ~/miniconda.sh && \ - /opt/conda/bin/conda clean -tipsy && \ - ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh -# ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \ -# echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc && \ -# echo "conda activate base" >> ~/.bashrc && \ -# echo 'alias ll="ls -al"' >> ~/.bashrc - -ENV PATH=/opt/conda/bin:$PATH - -# TensorFlow requirement -ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/conda/lib/ - -# NOTE: using mamba because conda fails when trying to solve for environment -RUN conda install -c conda-forge -n base mamba \ - && conda clean --all - -WORKDIR /armory-repo/ -COPY environment.yml /armory-repo/ - -RUN mamba env update -f environment.yml -n base --prune \ - && mamba clean --all - -#RUN /opt/conda/bin/conda env update -f environment.yml --prune \ -# && /opt/conda/bin/conda clean --all -# NOTE: with conda version 5, will need to set channel priority to flexible (as strict will become default) - -# NOTE: Armory requirements and ART requirements are installed here to make patch updates fast and small -RUN echo "Installing TensorFlow and ART/Armory requirements via pip" -RUN /opt/conda/bin/pip install --no-cache-dir \ - tensorflow-datasets==4.6 \ - tensorflow==2.10 \ - tensorboardx \ - boto3 \ - opencv-python \ - ffmpeg-python \ - pytest \ - loguru \ - docker \ - jsonschema \ - requests \ - pydub \ - transformers \ - six \ - setuptools \ - tqdm -# transformers is used for the Entailment metric only -# pydub required for ART mp3 defense +# libarchive13 is needed for mamba + +RUN curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" && \ + /bin/bash Mambaforge-$(uname)-$(uname -m).sh -b -p /opt/mamba && \ + rm Mambaforge-$(uname)-$(uname -m).sh && \ + mamba update mamba && \ + mamba update -n base -c defaults conda && \ + mamba env update -n base -f environment.yml WORKDIR /workspace diff --git a/docker/build-base.sh b/docker/build-base.sh index 5133fd146..6c9f911ad 100755 --- a/docker/build-base.sh +++ b/docker/build-base.sh @@ -1,44 +1,44 @@ -#!/usr/bin/env bash -set -e - -usage() { echo "usage: $0 [--dry-run] [--push]" 1>&2; exit 1; } - -dryrun= -push= - -while [ "${1:-}" != "" ]; do - case "$1" in - -n|--dry-run) - echo "dry-run requested. not building or pushing to docker hub" - dryrun="echo" ;; - --push) - push=true ;; - *) - usage ;; - esac - shift -done - -echo "Building the base image locally" -$dryrun docker build --force-rm --file ./docker/Dockerfile-base -t twosixarmory/base:latest --progress=auto . - -if [[ -z "$push" ]]; then - echo "" - echo "If building the framework images locally, use the '--no-pull' argument. E.g.:" - echo " python docker/build.py all --no-pull" - exit 0 -fi - -tag=$(python -m armory --version) -echo tagging twosixarmory/base:latest as $tag for dockerhub tracking -$dryrun docker tag twosixarmory/base:latest twosixarmory/base:$tag - -echo "" -echo "If you have not run 'docker login', with the proper credentials, these pushes will fail" -echo "see docs/docker.md for instructions" -echo "" - -# the second push should result in no new upload, it just tag the new image as -# latest -$dryrun docker push twosixarmory/base:$tag -$dryrun docker push twosixarmory/base:latest +#!/usr/bin/env bash +set -e + +usage() { echo "usage: $0 [--dry-run] [--push]" 1>&2; exit 1; } + +dryrun= +push= + +while [ "${1:-}" != "" ]; do + case "$1" in + -n|--dry-run) + echo "dry-run requested. not building or pushing to docker hub" + dryrun="echo" ;; + --push) + push=true ;; + *) + usage ;; + esac + shift +done + +echo "Building the base image locally" +$dryrun docker build --force-rm --file ./docker/Dockerfile-base -t twosixarmory/base:latest --progress=auto . + +if [[ -z "$push" ]]; then + echo "" + echo "If building the framework images locally, use the '--no-pull' argument. E.g.:" + echo " python docker/build.py all --no-pull" + exit 0 +fi + +tag=$(python -m armory --version) +echo tagging twosixarmory/base:latest as $tag for dockerhub tracking +$dryrun docker tag twosixarmory/base:latest twosixarmory/base:$tag + +echo "" +echo "If you have not run 'docker login', with the proper credentials, these pushes will fail" +echo "see docs/docker.md for instructions" +echo "" + +# the second push should result in no new upload, it just tag the new image as +# latest +$dryrun docker push twosixarmory/base:$tag +$dryrun docker push twosixarmory/base:latest diff --git a/environment.yml b/environment.yml index 893972208..eda5a29d1 100644 --- a/environment.yml +++ b/environment.yml @@ -5,18 +5,36 @@ channels: - conda-forge - defaults dependencies: - - pytorch=1.12 - - torchvision - - torchaudio - - cudatoolkit=11.6 - - scikit-learn=1.0 # ART requires scikit-learn >=0.22.2,<1.1.0 + - conda-forge::pip + - conda-forge::cudatoolkit = 11.6 + - conda-forge::cudnn # cudnn required for tensorflow + - conda-forge::tensorflow = 2.10.0 # If using python version <= 3.9 + - pytorch::pytorch < 1.13.0 + - pytorch::torchvision + - pytorch::torchaudio + - scikit-learn < 1.2.0 # ART requires scikit-learn >=0.22.2,<1.1.0 - jupyterlab - - jupyterlab_widgets - - ipywidgets - matplotlib - - conda-forge::ffmpeg # conda-forge ffmpeg comes with libx264 encoder, which the pytorch channel version does not include. This encoder is required for video compression defenses (ART) and video exporting. Future work could migrate this to libopenh264 encoder, which is available in both channels. - librosa - - cudnn # cudnn required for tensorflow - pandas - protobuf -prefix: /opt/conda + - conda-forge::ffmpeg # conda-forge ffmpeg comes with libx264 encoder, which the pytorch channel version does not include. This encoder is required for video compression defenses (ART) and video exporting. Future work could migrate this to libopenh264 encoder, which is available in both channels. + - pip: + - setuptools_scm + - boto3 + - opencv-python + - ffmpeg-python + - pytest + - loguru + - docker + - jsonschema + - requests + - pydub # pydub required for ART mp3 defense + - transformers # transformers is used for the Entailment metric only + - six + - setuptools + - tqdm + - wheel + - tensorflow-datasets + - tensorboardx +prefix: /opt/mamba diff --git a/pyproject.toml b/pyproject.toml index 888b2e816..928ef3c63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ math = [ "numpy", "pandas", "scipy >= 1.4.1", - "scikit-learn < 1.1.0", # ART requires scikit-learn >=0.22.2,<1.1.0 + "scikit-learn < 1.2.0", # ART requires scikit-learn >=0.22.2,<1.2.0 "matplotlib", ] From a450f30325194b1fc9bdf14de47344c06eac6cd8 Mon Sep 17 00:00:00 2001 From: Sterling Date: Tue, 8 Aug 2023 16:56:21 +0000 Subject: [PATCH 100/102] minor update for older pyplot version --- armory/postprocessing/plot_patch_aware_carla_metric.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/armory/postprocessing/plot_patch_aware_carla_metric.py b/armory/postprocessing/plot_patch_aware_carla_metric.py index cb0d86f0c..e1bf6274c 100644 --- a/armory/postprocessing/plot_patch_aware_carla_metric.py +++ b/armory/postprocessing/plot_patch_aware_carla_metric.py @@ -211,7 +211,8 @@ def add_bars( ax.bar_label(rects, fmt="%.2f", label_type="center", fontsize=fontsize) multiplier *= -1 - ax.set_xticks(x + width, ["Below threshold", "Above threshold"]) + ax.set_xticks(x + width) + ax.set_xticklabels(["Below threshold", "Above threshold"]) # Plot mean data add_bars(axes[0, 0], np.array([d["mean"] for d in ap_dicts]).reshape(2, 2)) From 4fa3d0471c1c3ca2daa3a8dc9e1300cf097a8637 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Tue, 8 Aug 2023 11:59:13 -0500 Subject: [PATCH 101/102] update workflow --- .github/workflows/release.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83a334995..8d61f7552 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,6 +64,35 @@ jobs: dist/*.whl + release-base-image: + name: Build and Release Base Image + needs: [release-wheel] + runs-on: ubuntu-latest + steps: + - name: 🐍 Setup Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: 3.9 + + - name: 📩 Checkout Armory w/ full depth(for tags and SCM) + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: ☁️ Login to DockerHub + if: ${{ env.RELEASE_DRY_RUN == 'false' }} + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: 🔨 Build and Push Base Image + run: | + echo "Building Base Image" + sed -i 's/\r$//' docker/build-base.sh + bash docker/build-base.sh + + release-docker: name: Build and Release Docker Images needs: [release-wheel] From 11675f2bccb82ca24dc6572ad17a895cd8427d51 Mon Sep 17 00:00:00 2001 From: Christopher Woodall Date: Tue, 8 Aug 2023 12:07:13 -0500 Subject: [PATCH 102/102] add installation to release used for versioning --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d61f7552..00e4e1e15 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,6 +79,12 @@ jobs: with: fetch-depth: 0 + - name: 🌎 Setup Build Environment + run: | + pip install pip>=22.2.2 + pip install . + armory configure --use-defaults + - name: ☁️ Login to DockerHub if: ${{ env.RELEASE_DRY_RUN == 'false' }} uses: docker/login-action@v1