From 9a238b2f90b8abd4b21587b9c60af71cabe8ff40 Mon Sep 17 00:00:00 2001 From: Mateo Date: Sat, 15 Jul 2023 11:39:30 +0200 Subject: [PATCH 01/20] add bbox --- pyroengine/engine.py | 28 ++++++++++++++++++-------- pyroengine/utils.py | 48 ++++++++++++++++++++++++++++++++++++++++++-- pyroengine/vision.py | 16 ++++++++------- 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/pyroengine/engine.py b/pyroengine/engine.py index b5a7126e..3b983089 100644 --- a/pyroengine/engine.py +++ b/pyroengine/engine.py @@ -248,20 +248,28 @@ def predict(self, frame: Image.Image, cam_id: Optional[str] = None) -> float: if is_day_time(self._cache, frame, self.day_time_strategy): # Inference with ONNX - pred = float(self.model(frame.convert("RGB"))) + preds = self.model(frame.convert("RGB")) + if len(preds)==0: + conf=0 + localization = "" + else: + conf = float(np.max(preds[:,-1])) + localization = str(json.dumps(preds.tolist())) + # Log analysis result device_str = f"Camera '{cam_id}' - " if isinstance(cam_id, str) else "" - pred_str = "Wildfire detected" if pred >= self.conf_thresh else "No wildfire" - logging.info(f"{device_str}{pred_str} (confidence: {pred:.2%})") + pred_str = "Wildfire detected" if conf >= self.conf_thresh else "No wildfire" + logging.info(f"{device_str}{pred_str} (confidence: {conf:.2%})") + # Alert - to_be_staged = self._update_states(pred, cam_key) + to_be_staged = self._update_states(conf, cam_key) if to_be_staged and len(self.api_client) > 0 and isinstance(cam_id, str): # Save the alert in cache to avoid connection issues - self._stage_alert(frame_resize, cam_id) + self._stage_alert(frame_resize, cam_id, localization) else: - pred = 0 # return default value + conf = 0 # return default value # Uploading pending alerts if len(self._alerts) > 0: @@ -289,7 +297,7 @@ def predict(self, frame: Image.Image, cam_id: Optional[str] = None) -> float: except ConnectionError: stream.seek(0) # "Rewind" the stream to the beginning so we can read its content - return pred + return conf def _upload_frame(self, cam_id: str, media_data: bytes) -> Response: """Save frame""" @@ -303,7 +311,7 @@ def _upload_frame(self, cam_id: str, media_data: bytes) -> Response: return response - def _stage_alert(self, frame: Image.Image, cam_id: str) -> None: + def _stage_alert(self, frame: Image.Image, cam_id: str, localization: str) -> None: # Store information in the queue self._alerts.append( { @@ -312,6 +320,7 @@ def _stage_alert(self, frame: Image.Image, cam_id: str) -> None: "ts": datetime.utcnow().isoformat(), "media_id": None, "alert_id": None, + "localization": localization, } ) @@ -322,6 +331,8 @@ def _process_alerts(self) -> None: cam_id = frame_info["cam_id"] logging.info(f"Camera '{cam_id}' - Sending alert from {frame_info['ts']}...") + print(self._alerts[0]) + # Save alert on device self._local_backup(frame_info["frame"], cam_id, is_alert=True) @@ -338,6 +349,7 @@ def _process_alerts(self) -> None: self.latitude, self.longitude, self._alerts[0]["media_id"], + self._alerts[0]["localization"], ) .json()["id"] ) diff --git a/pyroengine/utils.py b/pyroengine/utils.py index 730ba0fc..4ef555c7 100644 --- a/pyroengine/utils.py +++ b/pyroengine/utils.py @@ -6,11 +6,20 @@ import cv2 import numpy as np +import torch -__all__ = ["letterbox"] +__all__ = ["letterbox", "NMS", "xywh2xyxy"] +def xywh2xyxy(x): + y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x) + y[..., 0] = x[..., 0] - x[..., 2] / 2 # top left x + y[..., 1] = x[..., 1] - x[..., 3] / 2 # top left y + y[..., 2] = x[..., 0] + x[..., 2] / 2 # bottom right x + y[..., 3] = x[..., 1] + x[..., 3] / 2 # bottom right y + return y -def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, stride=32): + +def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=False, stride=32): """Letterbox image transform for yolo models Args: im (np.array): Input image @@ -51,3 +60,38 @@ def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, stride im_b[top : top + h, left : left + w, :] = im return im_b.astype("uint8") + + +def NMS(boxes, overlapThresh = 0): + # Return an empty list, if no boxes given + if len(boxes) == 0: + return [] + x1 = boxes[:, 0] # x coordinate of the top-left corner + y1 = boxes[:, 1] # y coordinate of the top-left corner + x2 = boxes[:, 2] # x coordinate of the bottom-right corner + y2 = boxes[:, 3] # y coordinate of the bottom-right corner + # Compute the area of the bounding boxes and sort the bounding + # Boxes by the bottom-right y-coordinate of the bounding box + areas = (x2 - x1 + 1) * (y2 - y1 + 1) # We add 1, because the pixel at the start as well as at the end counts + # The indices of all boxes at start. We will redundant indices one by one. + indices = np.arange(len(x1)) + for i,box in enumerate(boxes): + # Create temporary indices + temp_indices = indices[indices!=i] + # Find out the coordinates of the intersection box + xx1 = np.maximum(box[0], boxes[temp_indices,0]) + yy1 = np.maximum(box[1], boxes[temp_indices,1]) + xx2 = np.minimum(box[2], boxes[temp_indices,2]) + yy2 = np.minimum(box[3], boxes[temp_indices,3]) + # Find out the width and the height of the intersection box + w = np.maximum(0, xx2 - xx1 + 1) + h = np.maximum(0, yy2 - yy1 + 1) + # compute the ratio of overlap + overlap = (w * h) / areas[temp_indices] + # if the actual boungding box has an overlap bigger than treshold with any other box, remove it's index + if np.any(overlap) > overlapThresh: + indices = indices[indices != i] + #return only the boxes at the remaining indices + boxes = boxes[indices] + boxes[:,:4] = boxes[:,:4].astype(int) + return boxes \ No newline at end of file diff --git a/pyroengine/vision.py b/pyroengine/vision.py index d9673b64..ae12d24c 100644 --- a/pyroengine/vision.py +++ b/pyroengine/vision.py @@ -11,7 +11,7 @@ import onnxruntime from PIL import Image -from .utils import letterbox +from .utils import letterbox, xywh2xyxy, NMS __all__ = ["Classifier"] @@ -38,7 +38,7 @@ def __init__(self, model_path: Optional[str] = "data/model.onnx") -> None: self.ort_session = onnxruntime.InferenceSession(model_path) - def preprocess_image(self, pil_img: Image.Image, img_size=(640, 384)) -> np.ndarray: + def preprocess_image(self, pil_img: Image.Image, img_size=(384, 640)) -> np.ndarray: """Preprocess an image for inference Args: @@ -49,7 +49,7 @@ def preprocess_image(self, pil_img: Image.Image, img_size=(640, 384)) -> np.ndar the resized and normalized image of shape (1, C, H, W) """ - np_img = letterbox(np.array(pil_img)) # letterbox + np_img = letterbox(np.array(pil_img), img_size) # letterbox np_img = np.expand_dims(np_img.astype("float"), axis=0) np_img = np.ascontiguousarray(np_img.transpose((0, 3, 1, 2))) # BHWC to BCHW np_img = np_img.astype("float32") / 255 @@ -60,8 +60,10 @@ def __call__(self, pil_img: Image.Image) -> np.ndarray: np_img = self.preprocess_image(pil_img) # ONNX inference - y = self.ort_session.run(["output0"], {"images": np_img})[0] - # Non maximum suppression need to be added here when we will use the location information - # let's avoid useless compute for now + y = self.ort_session.run(["output0"], {"images": np_img})[0][0] + y = y[:,y[-1,:]>0.1] + y = np.transpose(y) + y = xywh2xyxy(y) + y = y[y[:, 4].argsort()] - return np.max(y[0, :, 4]) + return NMS(y) \ No newline at end of file From 5c03674264599750e10738752531df7e2b6b334a Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 18 Jul 2023 07:56:52 +0200 Subject: [PATCH 02/20] fix inference --- pyroengine/utils.py | 72 +++++++++++++++++++++++++------------------- pyroengine/vision.py | 12 +++++--- 2 files changed, 49 insertions(+), 35 deletions(-) diff --git a/pyroengine/utils.py b/pyroengine/utils.py index 4ef555c7..1fc6baa6 100644 --- a/pyroengine/utils.py +++ b/pyroengine/utils.py @@ -6,12 +6,11 @@ import cv2 import numpy as np -import torch __all__ = ["letterbox", "NMS", "xywh2xyxy"] def xywh2xyxy(x): - y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x) + y = np.copy(x) y[..., 0] = x[..., 0] - x[..., 2] / 2 # top left x y[..., 1] = x[..., 1] - x[..., 3] / 2 # top left y y[..., 2] = x[..., 0] + x[..., 2] / 2 # bottom right x @@ -61,37 +60,48 @@ def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=False, strid return im_b.astype("uint8") +def box_iou(box1, box2, eps=1e-7): + """ + Calculate intersection-over-union (IoU) of boxes. + Both sets of boxes are expected to be in (x1, y1, x2, y2) format. + Based on https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py + + Args: + box1 (np.array): A numpy array of shape (N, 4) representing N bounding boxes. + box2 (np.array): A numpy array of shape (M, 4) representing M bounding boxes. + eps (float, optional): A small value to avoid division by zero. Defaults to 1e-7. + + Returns: + (np.array): An NxM numpy array containing the pairwise IoU values for every element in box1 and box2. + """ + + (a1, a2), (b1, b2) = np.split(box1, 2, 1), np.split(box2, 2, 1) + inter = (np.minimum(a2,b2[:,None,:])- np.maximum(a1,b1[:,None,:])).clip(0).prod(2) + + # IoU = inter / (area1 + area2 - inter) + return inter / ((a2 - a1).prod(1) + (b2 - b1).prod(1)[:,None] - inter + eps) -def NMS(boxes, overlapThresh = 0): + +def NMS(boxes, overlapThresh=0): + """Non maximum suppression + + Args: + boxes (np.array): A numpy array of shape (N, 4) representing N bounding boxes in (x1, y1, x2, y2, conf) format + overlapThresh (int, optional): iou threshold. Defaults to 0. + + Returns: + boxes: Boxes after NMS + """ # Return an empty list, if no boxes given + boxes = boxes[boxes[:, -1].argsort()] if len(boxes) == 0: return [] - x1 = boxes[:, 0] # x coordinate of the top-left corner - y1 = boxes[:, 1] # y coordinate of the top-left corner - x2 = boxes[:, 2] # x coordinate of the bottom-right corner - y2 = boxes[:, 3] # y coordinate of the bottom-right corner - # Compute the area of the bounding boxes and sort the bounding - # Boxes by the bottom-right y-coordinate of the bounding box - areas = (x2 - x1 + 1) * (y2 - y1 + 1) # We add 1, because the pixel at the start as well as at the end counts - # The indices of all boxes at start. We will redundant indices one by one. - indices = np.arange(len(x1)) - for i,box in enumerate(boxes): - # Create temporary indices - temp_indices = indices[indices!=i] - # Find out the coordinates of the intersection box - xx1 = np.maximum(box[0], boxes[temp_indices,0]) - yy1 = np.maximum(box[1], boxes[temp_indices,1]) - xx2 = np.minimum(box[2], boxes[temp_indices,2]) - yy2 = np.minimum(box[3], boxes[temp_indices,3]) - # Find out the width and the height of the intersection box - w = np.maximum(0, xx2 - xx1 + 1) - h = np.maximum(0, yy2 - yy1 + 1) - # compute the ratio of overlap - overlap = (w * h) / areas[temp_indices] - # if the actual boungding box has an overlap bigger than treshold with any other box, remove it's index - if np.any(overlap) > overlapThresh: + + indices = np.arange(len(boxes)) + rr = box_iou(boxes[:,:4], boxes[:,:4]) + for i, box in enumerate(boxes): + temp_indices = indices[indices != i] + if np.any(rr[i,temp_indices]>overlapThresh): indices = indices[indices != i] - #return only the boxes at the remaining indices - boxes = boxes[indices] - boxes[:,:4] = boxes[:,:4].astype(int) - return boxes \ No newline at end of file + + return boxes[indices] \ No newline at end of file diff --git a/pyroengine/vision.py b/pyroengine/vision.py index ae12d24c..665257ed 100644 --- a/pyroengine/vision.py +++ b/pyroengine/vision.py @@ -29,7 +29,7 @@ class Classifier: model_path: model path """ - def __init__(self, model_path: Optional[str] = "data/model.onnx") -> None: + def __init__(self, model_path: Optional[str] = "data/model.onnx", img_size=(384, 640)) -> None: # Download model if not available if not os.path.isfile(model_path): os.makedirs(os.path.split(model_path)[0], exist_ok=True) @@ -37,8 +37,9 @@ def __init__(self, model_path: Optional[str] = "data/model.onnx") -> None: urllib.request.urlretrieve(MODEL_URL, model_path) self.ort_session = onnxruntime.InferenceSession(model_path) + self.img_size=img_size - def preprocess_image(self, pil_img: Image.Image, img_size=(384, 640)) -> np.ndarray: + def preprocess_image(self, pil_img: Image.Image) -> np.ndarray: """Preprocess an image for inference Args: @@ -49,7 +50,7 @@ def preprocess_image(self, pil_img: Image.Image, img_size=(384, 640)) -> np.ndar the resized and normalized image of shape (1, C, H, W) """ - np_img = letterbox(np.array(pil_img), img_size) # letterbox + np_img = letterbox(np.array(pil_img), self.img_size) # letterbox np_img = np.expand_dims(np_img.astype("float"), axis=0) np_img = np.ascontiguousarray(np_img.transpose((0, 3, 1, 2))) # BHWC to BCHW np_img = np_img.astype("float32") / 255 @@ -65,5 +66,8 @@ def __call__(self, pil_img: Image.Image) -> np.ndarray: y = np.transpose(y) y = xywh2xyxy(y) y = y[y[:, 4].argsort()] + y = NMS(y) + y[:,::2]/=self.img_size[1] + y[:,1::2]/=self.img_size[0] - return NMS(y) \ No newline at end of file + return y \ No newline at end of file From 3a43cec5d43ebbe7054ccbbc0871b627ad3e8971 Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 18 Jul 2023 07:59:23 +0200 Subject: [PATCH 03/20] style --- pyroengine/utils.py | 14 ++++++++------ pyroengine/vision.py | 12 ++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pyroengine/utils.py b/pyroengine/utils.py index 1fc6baa6..8322de11 100644 --- a/pyroengine/utils.py +++ b/pyroengine/utils.py @@ -9,6 +9,7 @@ __all__ = ["letterbox", "NMS", "xywh2xyxy"] + def xywh2xyxy(x): y = np.copy(x) y[..., 0] = x[..., 0] - x[..., 2] / 2 # top left x @@ -60,6 +61,7 @@ def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=False, strid return im_b.astype("uint8") + def box_iou(box1, box2, eps=1e-7): """ Calculate intersection-over-union (IoU) of boxes. @@ -76,10 +78,10 @@ def box_iou(box1, box2, eps=1e-7): """ (a1, a2), (b1, b2) = np.split(box1, 2, 1), np.split(box2, 2, 1) - inter = (np.minimum(a2,b2[:,None,:])- np.maximum(a1,b1[:,None,:])).clip(0).prod(2) + inter = (np.minimum(a2, b2[:, None, :]) - np.maximum(a1, b1[:, None, :])).clip(0).prod(2) # IoU = inter / (area1 + area2 - inter) - return inter / ((a2 - a1).prod(1) + (b2 - b1).prod(1)[:,None] - inter + eps) + return inter / ((a2 - a1).prod(1) + (b2 - b1).prod(1)[:, None] - inter + eps) def NMS(boxes, overlapThresh=0): @@ -98,10 +100,10 @@ def NMS(boxes, overlapThresh=0): return [] indices = np.arange(len(boxes)) - rr = box_iou(boxes[:,:4], boxes[:,:4]) + rr = box_iou(boxes[:, :4], boxes[:, :4]) for i, box in enumerate(boxes): temp_indices = indices[indices != i] - if np.any(rr[i,temp_indices]>overlapThresh): + if np.any(rr[i, temp_indices] > overlapThresh): indices = indices[indices != i] - - return boxes[indices] \ No newline at end of file + + return boxes[indices] diff --git a/pyroengine/vision.py b/pyroengine/vision.py index 665257ed..4397707d 100644 --- a/pyroengine/vision.py +++ b/pyroengine/vision.py @@ -11,7 +11,7 @@ import onnxruntime from PIL import Image -from .utils import letterbox, xywh2xyxy, NMS +from .utils import NMS, letterbox, xywh2xyxy __all__ = ["Classifier"] @@ -37,7 +37,7 @@ def __init__(self, model_path: Optional[str] = "data/model.onnx", img_size=(384, urllib.request.urlretrieve(MODEL_URL, model_path) self.ort_session = onnxruntime.InferenceSession(model_path) - self.img_size=img_size + self.img_size = img_size def preprocess_image(self, pil_img: Image.Image) -> np.ndarray: """Preprocess an image for inference @@ -62,12 +62,12 @@ def __call__(self, pil_img: Image.Image) -> np.ndarray: # ONNX inference y = self.ort_session.run(["output0"], {"images": np_img})[0][0] - y = y[:,y[-1,:]>0.1] + y = y[:, y[-1, :] > 0.1] y = np.transpose(y) y = xywh2xyxy(y) y = y[y[:, 4].argsort()] y = NMS(y) - y[:,::2]/=self.img_size[1] - y[:,1::2]/=self.img_size[0] + y[:, ::2] /= self.img_size[1] + y[:, 1::2] /= self.img_size[0] - return y \ No newline at end of file + return y From 7f94b7e29cfe605ef61514d2229717ee8afef819 Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 18 Jul 2023 08:34:00 +0200 Subject: [PATCH 04/20] fix vision --- pyroengine/vision.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyroengine/vision.py b/pyroengine/vision.py index 4397707d..9b22a9da 100644 --- a/pyroengine/vision.py +++ b/pyroengine/vision.py @@ -67,7 +67,8 @@ def __call__(self, pil_img: Image.Image) -> np.ndarray: y = xywh2xyxy(y) y = y[y[:, 4].argsort()] y = NMS(y) - y[:, ::2] /= self.img_size[1] - y[:, 1::2] /= self.img_size[0] + if len(y)>0: + y[:, :4:2] /= self.img_size[1] + y[:, 1:4:2] /= self.img_size[0] return y From 01cb1b6f8794d0eb383dccb7b25c9bb8cd220689 Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 18 Jul 2023 08:34:15 +0200 Subject: [PATCH 05/20] style vision --- pyroengine/vision.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyroengine/vision.py b/pyroengine/vision.py index 9b22a9da..29084cb6 100644 --- a/pyroengine/vision.py +++ b/pyroengine/vision.py @@ -67,7 +67,7 @@ def __call__(self, pil_img: Image.Image) -> np.ndarray: y = xywh2xyxy(y) y = y[y[:, 4].argsort()] y = NMS(y) - if len(y)>0: + if len(y) > 0: y[:, :4:2] /= self.img_size[1] y[:, 1:4:2] /= self.img_size[0] From bd85bd0c2d260efc62b1d229544cdaf721517ac3 Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 18 Jul 2023 08:34:33 +0200 Subject: [PATCH 06/20] fix engine --- pyroengine/engine.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pyroengine/engine.py b/pyroengine/engine.py index 3b983089..2755c322 100644 --- a/pyroengine/engine.py +++ b/pyroengine/engine.py @@ -249,11 +249,11 @@ def predict(self, frame: Image.Image, cam_id: Optional[str] = None) -> float: if is_day_time(self._cache, frame, self.day_time_strategy): # Inference with ONNX preds = self.model(frame.convert("RGB")) - if len(preds)==0: - conf=0 + if len(preds) == 0: + conf = 0 localization = "" else: - conf = float(np.max(preds[:,-1])) + conf = float(np.max(preds[:, -1])) localization = str(json.dumps(preds.tolist())) # Log analysis result @@ -261,7 +261,6 @@ def predict(self, frame: Image.Image, cam_id: Optional[str] = None) -> float: pred_str = "Wildfire detected" if conf >= self.conf_thresh else "No wildfire" logging.info(f"{device_str}{pred_str} (confidence: {conf:.2%})") - # Alert to_be_staged = self._update_states(conf, cam_key) @@ -331,8 +330,6 @@ def _process_alerts(self) -> None: cam_id = frame_info["cam_id"] logging.info(f"Camera '{cam_id}' - Sending alert from {frame_info['ts']}...") - print(self._alerts[0]) - # Save alert on device self._local_backup(frame_info["frame"], cam_id, is_alert=True) @@ -346,10 +343,10 @@ def _process_alerts(self) -> None: self._alerts[0]["alert_id"] = ( self.api_client[cam_id] .send_alert_from_device( - self.latitude, - self.longitude, - self._alerts[0]["media_id"], - self._alerts[0]["localization"], + lat=self.latitude, + lon=self.longitude, + media_id=self._alerts[0]["media_id"], + localization=self._alerts[0]["localization"], ) .json()["id"] ) From 68ccd14dd7f03e54a967ab17f2cd0fa39ce2402b Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 18 Jul 2023 08:44:43 +0200 Subject: [PATCH 07/20] keep all preds --- pyroengine/vision.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyroengine/vision.py b/pyroengine/vision.py index 29084cb6..e9965eac 100644 --- a/pyroengine/vision.py +++ b/pyroengine/vision.py @@ -62,11 +62,13 @@ def __call__(self, pil_img: Image.Image) -> np.ndarray: # ONNX inference y = self.ort_session.run(["output0"], {"images": np_img})[0][0] - y = y[:, y[-1, :] > 0.1] + # Post processing y = np.transpose(y) y = xywh2xyxy(y) + # Sort by confidence y = y[y[:, 4].argsort()] y = NMS(y) + # Normalize preds if len(y) > 0: y[:, :4:2] /= self.img_size[1] y[:, 1:4:2] /= self.img_size[0] From c8e41c66eab31fb86db473f45178761f6ef5a7f3 Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 18 Jul 2023 09:48:50 +0200 Subject: [PATCH 08/20] speed up --- pyroengine/vision.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyroengine/vision.py b/pyroengine/vision.py index e9965eac..98fd76c8 100644 --- a/pyroengine/vision.py +++ b/pyroengine/vision.py @@ -62,6 +62,8 @@ def __call__(self, pil_img: Image.Image) -> np.ndarray: # ONNX inference y = self.ort_session.run(["output0"], {"images": np_img})[0][0] + # Drop low conf for speed-up + y = y[:, y[-1, :] > 0.05] # Post processing y = np.transpose(y) y = xywh2xyxy(y) From 6b0d98282183c4f10391ea9288f7f57f22e5cb6b Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 18 Jul 2023 16:08:13 +0200 Subject: [PATCH 09/20] pass dummy loc --- tests/test_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index 1b8f648b..d2a1cfef 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -16,7 +16,7 @@ def test_engine_offline(tmpdir_factory, mock_wildfire_image, mock_forest_image): # Cache saving _ts = datetime.utcnow().isoformat() - engine._stage_alert(mock_wildfire_image, 0) + engine._stage_alert(mock_wildfire_image, 0, localization="dummy") assert len(engine._alerts) == 1 assert engine._alerts[0]["ts"] < datetime.utcnow().isoformat() and _ts < engine._alerts[0]["ts"] assert engine._alerts[0]["media_id"] is None From faf9c8108d3076d7c7ce36403dbb865977c68922 Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 18 Jul 2023 16:09:07 +0200 Subject: [PATCH 10/20] switch to v8 --- pyroengine/vision.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyroengine/vision.py b/pyroengine/vision.py index 98fd76c8..b72677d4 100644 --- a/pyroengine/vision.py +++ b/pyroengine/vision.py @@ -15,7 +15,7 @@ __all__ = ["Classifier"] -MODEL_URL = "https://github.com/pyronear/pyro-vision/releases/download/v0.2.0/yolov5s_v002.onnx" +MODEL_URL = "https://github.com/pyronear/pyro-vision/releases/download/v0.2.0/yolov8s_v001.onnx" class Classifier: From 83c52851569df0789be122dc488bcabd8c8a0d9d Mon Sep 17 00:00:00 2001 From: Mateo Date: Wed, 19 Jul 2023 09:16:29 +0200 Subject: [PATCH 11/20] fix tests --- pyroengine/engine.py | 2 +- tests/test_engine.py | 6 ++++-- tests/test_vision.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyroengine/engine.py b/pyroengine/engine.py index 2755c322..72ee262c 100644 --- a/pyroengine/engine.py +++ b/pyroengine/engine.py @@ -296,7 +296,7 @@ def predict(self, frame: Image.Image, cam_id: Optional[str] = None) -> float: except ConnectionError: stream.seek(0) # "Rewind" the stream to the beginning so we can read its content - return conf + return float(conf) def _upload_frame(self, cam_id: str, media_data: bytes) -> Response: """Save frame""" diff --git a/tests/test_engine.py b/tests/test_engine.py index d2a1cfef..fefff362 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -3,6 +3,7 @@ from datetime import datetime from pathlib import Path +import numpy as np from dotenv import load_dotenv from pyroengine.engine import Engine @@ -37,16 +38,17 @@ def test_engine_offline(tmpdir_factory, mock_wildfire_image, mock_forest_image): engine._dump_cache() # Cache dump loading - engine = Engine(cache_folder=folder + "model.onnx") + engine = Engine(cache_folder=folder) assert len(engine._alerts) == 1 engine.clear_cache() # inference - engine = Engine(alert_relaxation=3, cache_folder=folder + "model.onnx") + engine = Engine(alert_relaxation=3, cache_folder=folder) out = engine.predict(mock_forest_image) assert isinstance(out, float) and 0 <= out <= 1 assert engine._states["-1"]["consec"] == 0 out = engine.predict(mock_wildfire_image) + assert isinstance(out, float) and 0 <= out <= 1 assert engine._states["-1"]["consec"] == 1 # Alert relaxation diff --git a/tests/test_vision.py b/tests/test_vision.py index 70b76f6a..a0733f49 100644 --- a/tests/test_vision.py +++ b/tests/test_vision.py @@ -12,4 +12,6 @@ def test_classifier(mock_wildfire_image): assert out.shape == (1, 3, 384, 640) # Check inference out = model(mock_wildfire_image) - assert out >= 0 and out <= 1 + assert out.shape == (1, 5) + conf = np.max(out[:, 4]) + assert conf >= 0 and conf <= 1 From f45fcda4742f27d13309e52f5c0d8888d3f4e2a2 Mon Sep 17 00:00:00 2001 From: Mateo Date: Wed, 19 Jul 2023 09:36:21 +0200 Subject: [PATCH 12/20] test --- tests/test_engine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_engine.py b/tests/test_engine.py index fefff362..387263fb 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -68,6 +68,7 @@ def test_engine_online(tmpdir_factory, mock_wildfire_stream, mock_wildfire_image api_url = os.environ.get("API_URL") lat = os.environ.get("LAT") lon = os.environ.get("LON") + print(api_url, os.environ.get("API_LOGIN"), os.environ.get("API_PWD")[-3:]) cam_creds = {"dummy_cam": {"login": os.environ.get("API_LOGIN"), "password": os.environ.get("API_PWD")}} # Skip the API-related tests if the URL is not specified if isinstance(api_url, str): From 155b43a64bee5fec65f14c5931f36825814615ca Mon Sep 17 00:00:00 2001 From: Mateo Date: Wed, 19 Jul 2023 09:56:39 +0200 Subject: [PATCH 13/20] drop dummy test --- tests/test_engine.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index 387263fb..fefff362 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -68,7 +68,6 @@ def test_engine_online(tmpdir_factory, mock_wildfire_stream, mock_wildfire_image api_url = os.environ.get("API_URL") lat = os.environ.get("LAT") lon = os.environ.get("LON") - print(api_url, os.environ.get("API_LOGIN"), os.environ.get("API_PWD")[-3:]) cam_creds = {"dummy_cam": {"login": os.environ.get("API_LOGIN"), "password": os.environ.get("API_PWD")}} # Skip the API-related tests if the URL is not specified if isinstance(api_url, str): From c66d3526c698fca95db685dfa141da81af7162a1 Mon Sep 17 00:00:00 2001 From: Mateo Date: Wed, 19 Jul 2023 09:57:28 +0200 Subject: [PATCH 14/20] new api version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d598903b..70f20e30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "Pillow>=8.4.0", "onnxruntime>=1.10.0,<2.0.0", "numpy>=1.19.5,<2.0.0", - "pyroclient>=0.1.2", + "pyroclient @ git+https://github.com/pyronear/pyro-api.git#egg=pkg&subdirectory=client", "requests>=2.20.0,<3.0.0", "opencv-python==4.5.5.64", ] From 5624c462e61e8ad2da0103fbc75b6c284357df67 Mon Sep 17 00:00:00 2001 From: Mateo Date: Wed, 19 Jul 2023 10:00:26 +0200 Subject: [PATCH 15/20] unsed import --- tests/test_engine.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index fefff362..c1e3d17d 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -3,7 +3,6 @@ from datetime import datetime from pathlib import Path -import numpy as np from dotenv import load_dotenv from pyroengine.engine import Engine From 531ab66b9bb4d010fcbe527b131acc6fca189af6 Mon Sep 17 00:00:00 2001 From: Mateo Date: Wed, 19 Jul 2023 10:03:54 +0200 Subject: [PATCH 16/20] install git --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 1788fe34..95a33c47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,9 @@ COPY ./pyproject.toml /tmp/pyproject.toml COPY ./README.md /tmp/README.md COPY ./setup.py /tmp/setup.py +# install git +RUN apt update && apt install git -y + COPY ./src/requirements.txt /tmp/requirements.txt RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y\ && pip install --upgrade pip setuptools wheel \ From 5c1dba1628d693283e11fc64c19a62dfcefefe7b Mon Sep 17 00:00:00 2001 From: Mateo Date: Wed, 19 Jul 2023 10:40:32 +0200 Subject: [PATCH 17/20] code quality --- pyroengine/utils.py | 12 +++++++----- pyroengine/vision.py | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pyroengine/utils.py b/pyroengine/utils.py index 8322de11..f1d3dc51 100644 --- a/pyroengine/utils.py +++ b/pyroengine/utils.py @@ -7,10 +7,10 @@ import cv2 import numpy as np -__all__ = ["letterbox", "NMS", "xywh2xyxy"] +__all__ = ["letterbox", "nms", "xywh2xyxy"] -def xywh2xyxy(x): +def xywh2xyxy(x: np.array): y = np.copy(x) y[..., 0] = x[..., 0] - x[..., 2] / 2 # top left x y[..., 1] = x[..., 1] - x[..., 3] / 2 # top left y @@ -19,7 +19,9 @@ def xywh2xyxy(x): return y -def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=False, stride=32): +def letterbox( + im: np.array, new_shape: tuple = (640, 640), color: tuple = (114, 114, 114), auto: bool = False, stride: int = 32 +): """Letterbox image transform for yolo models Args: im (np.array): Input image @@ -62,7 +64,7 @@ def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=False, strid return im_b.astype("uint8") -def box_iou(box1, box2, eps=1e-7): +def box_iou(box1: np.array, box2: np.array, eps: float = 1e-7): """ Calculate intersection-over-union (IoU) of boxes. Both sets of boxes are expected to be in (x1, y1, x2, y2) format. @@ -84,7 +86,7 @@ def box_iou(box1, box2, eps=1e-7): return inter / ((a2 - a1).prod(1) + (b2 - b1).prod(1)[:, None] - inter + eps) -def NMS(boxes, overlapThresh=0): +def nms(boxes: np.array, overlapThresh: int = 0): """Non maximum suppression Args: diff --git a/pyroengine/vision.py b/pyroengine/vision.py index b72677d4..53e2d2ec 100644 --- a/pyroengine/vision.py +++ b/pyroengine/vision.py @@ -11,7 +11,7 @@ import onnxruntime from PIL import Image -from .utils import NMS, letterbox, xywh2xyxy +from .utils import letterbox, nms, xywh2xyxy __all__ = ["Classifier"] @@ -29,7 +29,7 @@ class Classifier: model_path: model path """ - def __init__(self, model_path: Optional[str] = "data/model.onnx", img_size=(384, 640)) -> None: + def __init__(self, model_path: Optional[str] = "data/model.onnx", img_size: tuple = (384, 640)) -> None: # Download model if not available if not os.path.isfile(model_path): os.makedirs(os.path.split(model_path)[0], exist_ok=True) @@ -69,7 +69,7 @@ def __call__(self, pil_img: Image.Image) -> np.ndarray: y = xywh2xyxy(y) # Sort by confidence y = y[y[:, 4].argsort()] - y = NMS(y) + y = nms(y) # Normalize preds if len(y) > 0: y[:, :4:2] /= self.img_size[1] From 8be77e8c7f9df35797ee669cac23dc72305f8b30 Mon Sep 17 00:00:00 2001 From: Mateo Date: Wed, 19 Jul 2023 10:44:15 +0200 Subject: [PATCH 18/20] use bbox branch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 70f20e30..ae6b2e3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "Pillow>=8.4.0", "onnxruntime>=1.10.0,<2.0.0", "numpy>=1.19.5,<2.0.0", - "pyroclient @ git+https://github.com/pyronear/pyro-api.git#egg=pkg&subdirectory=client", + "pyroclient @ git+https://github.com/pyronear/pyro-api.git@bbox#egg=pkg&subdirectory=client", "requests>=2.20.0,<3.0.0", "opencv-python==4.5.5.64", ] From 3f6fe0efff74854cfdb16bfa26953fa1305cba65 Mon Sep 17 00:00:00 2001 From: Mateo Date: Thu, 20 Jul 2023 09:24:39 +0200 Subject: [PATCH 19/20] use apt get --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 95a33c47..c90e7222 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ COPY ./README.md /tmp/README.md COPY ./setup.py /tmp/setup.py # install git -RUN apt update && apt install git -y +RUN apt-get update && apt-get install git -y COPY ./src/requirements.txt /tmp/requirements.txt RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y\ From ea63761e4da3a7e819f562aa8bd2f1d53c40ee01 Mon Sep 17 00:00:00 2001 From: Mateo Date: Thu, 20 Jul 2023 13:42:16 +0200 Subject: [PATCH 20/20] alert relaxation 3 --- src/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/run.py b/src/run.py index 2356f2a8..9bdf5fb9 100644 --- a/src/run.py +++ b/src/run.py @@ -96,7 +96,7 @@ def main(args): parser.add_argument( "--alert_relaxation", type=int, - default=2, + default=3, help="Number of consecutive positive detections required to send the first alert", ) parser.add_argument(