Repository: deepchecks/deepchecks

Issue:
[BUG] jaccard_iou takes a large chunk of the runtime
**Describe the bug**
892.728 <module>  ../../tmp/ipykernel_34/1659815497.py:7
└─ 892.726 run  deepchecks/vision/base_checks.py:45
      [2669 frames hidden]  deepchecks, ignite, <built-in>, torch...
         723.237 jaccard_iou  deepchecks/vision/metrics_utils/iou_utils.py:18

jaccard_iou gets called many times during a run, so even though it's a simple calculation, we want to improve it's run time by using numpy

**Expected behavior**
reduced runtime.

**Environment (please complete the following information):**
 - OS: Kaggle
 - Python Version: 3.8
 - Deepchecks Version: 6.1




Discussion:
Numpy vectorization should be done at:
1. Confusion matrix - list_of_ious
2. iou_utils - compute_pairwise_ious

Do timing before the modification and after. If this won't improve enough, can consider numba. 

Resolving patch:
diff --git a/deepchecks/vision/checks/performance/confusion_matrix.py b/deepchecks/vision/checks/performance/confusion_matrix.py
--- a/deepchecks/vision/checks/performance/confusion_matrix.py
+++ b/deepchecks/vision/checks/performance/confusion_matrix.py
@@ -181,7 +181,7 @@ def update_object_detection(self, predictions, labels):
                 continue
 
             list_of_ious = (
-                (label_index, detected_index, jaccard_iou(detected, label))
+                (label_index, detected_index, jaccard_iou(detected.cpu().numpy(), label.cpu().numpy()))
                 for label_index, label in enumerate(image_labels)
                 for detected_index, detected in enumerate(detections_passed_threshold)
             )
diff --git a/deepchecks/vision/metrics_utils/iou_utils.py b/deepchecks/vision/metrics_utils/iou_utils.py
--- a/deepchecks/vision/metrics_utils/iou_utils.py
+++ b/deepchecks/vision/metrics_utils/iou_utils.py
@@ -15,10 +15,17 @@
 import torch
 
 
-def jaccard_iou(dt, gt):
+def jaccard_iou(dt: np.array, gt: np.array):
     """Calculate the jaccard IoU.
 
     See https://en.wikipedia.org/wiki/Jaccard_index
+
+    Parameters
+    ----------
+    dt: np.array
+        Single Detection in the shape of [x, y, width, height, confidence, class]
+    gt: np.array
+        Single Ground Truth in the shape of [class, x, y, width, height]
     """
     x_dt, y_dt, w_dt, h_dt = dt[:4]
     x_gt, y_gt, w_gt, h_gt = gt[1:]
@@ -27,18 +34,20 @@ def jaccard_iou(dt, gt):
     x2_gt, y2_gt = x_gt + w_gt, y_gt + h_gt
 
     # innermost left x
-    xi = max(x_dt, x_gt)
+    xi = x_dt if x_dt > x_gt else x_gt
     # innermost right x
-    x2i = min(x2_dt, x2_gt)
+    x2i = x2_dt if x2_dt < x2_gt else x2_gt
     # same for y
-    yi = max(y_dt, y_gt)
-    y2i = min(y2_dt, y2_gt)
+    yi = y_dt if y_dt > y_gt else y_gt
+    y2i = y2_dt if y2_dt < y2_gt else y2_gt
 
     # calculate areas
-    dt_area = float(w_dt * h_dt)
-    gt_area = float(w_gt * h_gt)
-    intersection = float(max(x2i - xi, 0)) * float(max(y2i - yi, 0))
-    return float(intersection / (dt_area + gt_area - intersection))
+    dt_area = w_dt * h_dt
+    gt_area = w_gt * h_gt
+    iwidth = x2i - xi if x2i > xi else 0
+    ihight = y2i - yi if y2i > yi else 0
+    intersection = iwidth * ihight
+    return intersection / (dt_area + gt_area - intersection)
 
 
 def compute_pairwise_ious(detected, ground_truth, iou_func):
@@ -50,22 +59,23 @@ def compute_pairwise_ious(detected, ground_truth, iou_func):
     return ious
 
 
-def group_class_detection_label(detected, ground_truth, detection_classes, ground_truth_classes):
+def group_class_detection_label(detected, ground_truth):
     """Group bounding detection and labels by class."""
     class_bounding_boxes = defaultdict(lambda: {"detected": [], "ground_truth": []})
 
-    for single_detection, class_id in zip(detected, detection_classes):
-        class_bounding_boxes[class_id]["detected"].append(single_detection)
-    for single_ground_truth, class_id in zip(ground_truth, ground_truth_classes):
-        class_bounding_boxes[class_id]["ground_truth"].append(single_ground_truth)
+    for single_detection in detected:
+        class_id = untorchify(single_detection[5])
+        class_bounding_boxes[class_id]["detected"].append(single_detection.cpu().numpy())
+    for single_ground_truth in ground_truth:
+        class_id = untorchify(single_ground_truth[0])
+        class_bounding_boxes[class_id]["ground_truth"].append(single_ground_truth.cpu().numpy())
+
     return class_bounding_boxes
 
 
 def compute_bounding_box_class_ious(detected, ground_truth):
     """Compute ious between bounding boxes of the same class."""
-    detection_classes = [untorchify(d[5]) for d in detected]
-    ground_truth_classes = [untorchify(g[0]) for g in ground_truth]
-    bb_info = group_class_detection_label(detected, ground_truth, detection_classes, ground_truth_classes)
+    bb_info = group_class_detection_label(detected, ground_truth)
 
     # Calculating pairwise IoUs per class
     return {class_id: compute_pairwise_ious(info["detected"], info["ground_truth"], jaccard_iou)
diff --git a/deepchecks/vision/metrics_utils/object_detection_precision_recall.py b/deepchecks/vision/metrics_utils/object_detection_precision_recall.py
--- a/deepchecks/vision/metrics_utils/object_detection_precision_recall.py
+++ b/deepchecks/vision/metrics_utils/object_detection_precision_recall.py
@@ -8,13 +8,12 @@
 # ----------------------------------------------------------------------------
 #
 """Module for average precision for object detection."""
-from collections import defaultdict
 from typing import List
 
 import numpy as np
 
 from deepchecks.vision.metrics_utils.detection_precision_recall import AveragePrecision
-from deepchecks.vision.metrics_utils.iou_utils import compute_pairwise_ious, jaccard_iou, untorchify
+from deepchecks.vision.metrics_utils.iou_utils import compute_pairwise_ious, jaccard_iou, group_class_detection_label
 
 
 class ObjectDetectionAveragePrecision(AveragePrecision):
@@ -26,16 +25,7 @@ def get_labels_areas(self, labels) -> List[int]:
 
     def group_class_detection_label(self, detections, labels) -> dict:
         """Group detection and labels in dict of format {class_id: {'detected' [...], 'ground_truth': [...] }}."""
-        class_bounding_boxes = defaultdict(lambda: {"detected": [], "ground_truth": []})
-
-        for single_detection in detections:
-            class_id = untorchify(single_detection[5])
-            class_bounding_boxes[class_id]["detected"].append(single_detection)
-        for single_ground_truth in labels:
-            class_id = untorchify(single_ground_truth[0])
-            class_bounding_boxes[class_id]["ground_truth"].append(single_ground_truth)
-
-        return class_bounding_boxes
+        return group_class_detection_label(detections, labels)
 
     def get_confidences(self, detections) -> List[float]:
         """Get detections object of single image and should return confidence for each detection."""


Test patch:
diff --git a/tests/vision/checks/performance/vision_model_error_analysis_test.py b/tests/vision/checks/performance/vision_model_error_analysis_test.py
--- a/tests/vision/checks/performance/vision_model_error_analysis_test.py
+++ b/tests/vision/checks/performance/vision_model_error_analysis_test.py
@@ -39,7 +39,7 @@ def test_detection(coco_train_visiondata, coco_test_visiondata, mock_trained_yol
                        mock_trained_yolov5_object_detection,
                        device=device)
     # Assert
-    assert_that(len(result.value['feature_segments']), equal_to(3))
+    assert_that(len(result.value['feature_segments']), equal_to(4))
     assert_that(result.value['feature_segments']['Mean Green Relative Intensity']['segment1']['n_samples'],
                 equal_to(21))