commit 7ffa5e60929321d4c2d9f991c5501483ca336152
Author: david cochran <about.trout@gmail.com>
Date: Sat, 10 Feb 2024 13:56:10 -0800
initial commit
Diffstat:
| A | README.md | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ |
| A | detect.py | | | 49 | +++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | requirements.txt | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ |
| A | scrape.py | | | 68 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 203 insertions(+), 0 deletions(-)
diff --git a/README.md b/README.md
@@ -0,0 +1,43 @@
+# `pp-boatwatch`
+
+Locate large boats (e.g. container ships) as they pass by Pillar Point.
+
+---
+
+Install the requirements.
+
+```
+$ python3 -m venv .venv
+$ source .venv/bin/activate
+$ pip install -r requirements.txt
+```
+
+Accumulate data by sampling the Mavericks cams on Surfline.
+
+```
+$ ./scrape.py
+2024-02-10 10:19:32,612 cam_name=mavericksov Extracted keyframes from chunk
+2024-02-10 10:19:32,612 cam_name=mavericksov delay=52 Sleeping
+2024-02-10 10:19:33,157 cam_name=mavericks Extracted keyframes from chunk
+2024-02-10 10:19:33,157 cam_name=mavericks delay=77 Sleeping
+...
+```
+
+Find boats in images with [`BoatFinder`](./detect.py#L12).
+
+```
+$ ./detect.py $(find ./data/mavericks/02102024/10/*.jpg | head -n10)
+2024-02-10 10:40:43,803 BoatFinder initializing ...
+2024-02-10 10:40:44,473 BoatFinder initialized! took 0.6702592820074642 seconds
+2024-02-10 10:40:44,477 Searching file ./data/mavericks/02102024/10/1707589172-thumb-0001.jpg...
+2024-02-10 10:40:46,057 Searching file ./data/mavericks/02102024/10/1707589172-thumb-0002.jpg...
+2024-02-10 10:40:47,474 Searching file ./data/mavericks/02102024/10/1707589172-thumb-0003.jpg...
+2024-02-10 10:40:48,865 Searching file ./data/mavericks/02102024/10/1707589172-thumb-0004.jpg...
+2024-02-10 10:40:50,264 Searching file ./data/mavericks/02102024/10/1707589172-thumb-0005.jpg...
+2024-02-10 10:40:51,669 Searching file ./data/mavericks/02102024/10/1707589250-thumb-0001.jpg...
+2024-02-10 10:40:53,074 >> label=boat score=0.677 box=[431.265, 673.42, 694.994, 719.792]
+2024-02-10 10:40:53,076 Searching file ./data/mavericks/02102024/10/1707589250-thumb-0002.jpg...
+2024-02-10 10:40:54,760 Searching file ./data/mavericks/02102024/10/1707589250-thumb-0003.jpg...
+2024-02-10 10:40:56,160 Searching file ./data/mavericks/02102024/10/1707589250-thumb-0004.jpg...
+2024-02-10 10:40:57,594 Searching file ./data/mavericks/02102024/10/1707589250-thumb-0005.jpg...
+```
diff --git a/detect.py b/detect.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+import logging
+import sys
+import torch
+
+from PIL import Image
+from time import perf_counter
+from transformers import DetrImageProcessor, DetrForObjectDetection
+
+
+class BoatFinder:
+ def __init__(self):
+ logging.info(f"BoatFinder initializing ...")
+ t0 = perf_counter()
+ # https://huggingface.co/facebook/detr-resnet-50
+ self.processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50", revision="no_timm")
+ self.model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50", revision="no_timm")
+ logging.info(f"BoatFinder initialized! took {perf_counter() - t0} seconds")
+
+ def find(self, img):
+ res = self.__match_results(img)
+ for score, label, box in zip(res["scores"], res["labels"], res["boxes"]):
+ label = self.model.config.id2label[label.item()]
+ if label == "boat":
+ score = round(score.item(), 3)
+ box = [round(i, 3) for i in box.tolist()]
+ yield (score, label, box)
+
+ def __match_results(self, image):
+ inputs = self.processor(images=image, return_tensors="pt")
+ outputs = self.model(**inputs)
+ target_sizes = torch.tensor([image.size[::-1]])
+ return self.processor.post_process_object_detection(outputs, target_sizes=target_sizes, threshold=0.5)[0]
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print(f"USAGE: {sys.argv[0]} /path/to/file_that_might_have_boats.jpg")
+ sys.exit(1)
+
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
+
+ bf = BoatFinder()
+ for img_file in sys.argv[1:]:
+ image = Image.open(img_file)
+ logging.info(f"Searching file {img_file}...")
+ for score, label, box in bf.find(image):
+ logging.info(f">> label={label}\t score={score}\t box={box}\t")
diff --git a/requirements.txt b/requirements.txt
@@ -0,0 +1,43 @@
+aiohttp==3.9.3
+aiosignal==1.3.1
+attrs==23.2.0
+certifi==2024.2.2
+charset-normalizer==3.3.2
+filelock==3.13.1
+frozenlist==1.4.1
+fsspec==2024.2.0
+huggingface-hub==0.20.3
+idna==3.6
+Jinja2==3.1.3
+MarkupSafe==2.1.5
+mpmath==1.3.0
+multidict==6.0.5
+networkx==3.2.1
+numpy==1.26.4
+nvidia-cublas-cu12==12.1.3.1
+nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cudnn-cu12==8.9.2.26
+nvidia-cufft-cu12==11.0.2.54
+nvidia-curand-cu12==10.3.2.106
+nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusparse-cu12==12.1.0.106
+nvidia-nccl-cu12==2.19.3
+nvidia-nvjitlink-cu12==12.3.101
+nvidia-nvtx-cu12==12.1.105
+packaging==23.2
+pillow==10.2.0
+PyYAML==6.0.1
+regex==2023.12.25
+requests==2.31.0
+safetensors==0.4.2
+sympy==1.12
+tokenizers==0.15.1
+torch==2.2.0
+tqdm==4.66.1
+transformers==4.37.2
+triton==2.2.0
+typing_extensions==4.9.0
+urllib3==2.2.0
+yarl==1.9.4
diff --git a/scrape.py b/scrape.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+
+import aiohttp
+import asyncio
+import datetime
+import logging
+import os
+import random
+import subprocess
+import tempfile
+import time
+
+
+async def get_recent_chunk_url(session, cam_name):
+ base_url = f"https://cams.cdn-surfline.com/cdn-wc/wc-{cam_name}"
+ async with session.get(f"{base_url}/chunklist.m3u8") as res:
+ chunklist = await res.text()
+ chunk = next(filter(lambda x: not x.startswith('#'), chunklist.splitlines()))
+ return f"{base_url}/{chunk}"
+
+
+def split_keyframes(chunk_url, data_dir, cam_name):
+ dt = datetime.datetime.now()
+ day, hour, ts = dt.strftime("%m%d%Y"), dt.strftime("%H"), dt.strftime("%s")
+
+ keyframes_dir = f"{data_dir}/{cam_name}/{day}/{hour}"
+ if not os.path.exists(keyframes_dir):
+ os.makedirs(keyframes_dir)
+
+ subprocess.run(["ffmpeg",
+ "-hide_banner", "-loglevel", "0",
+ "-i", chunk_url,
+ "-vf", "select=eq(pict_type\,I)",
+ "-vsync", "vfr",
+ f"{keyframes_dir}/{ts}-thumb-%04d.jpg"])
+
+
+async def save_keyframes_from_chunk(cam_name, data_dir):
+ async with aiohttp.ClientSession(raise_for_status=True) as session:
+ chunk_url = await get_recent_chunk_url(session, cam_name)
+ split_keyframes(chunk_url, data_dir, cam_name)
+ logging.info(f"cam_name={cam_name} Extracted keyframes from chunk")
+
+
+async def sample_stream(cam_name, data_dir):
+ while True:
+ try:
+ await save_keyframes_from_chunk(cam_name, data_dir)
+ except Exception as ex:
+ logging.error(f"cam_name={cam_name} Failed to extract keyframes from chunk: {ex}")
+
+ delay = random.randint(30, 90) # seconds.
+ logging.info(f"cam_name={cam_name} delay={delay} Sleeping")
+ await asyncio.sleep(delay)
+
+
+async def main():
+ data_dir = os.environ.get("DATA_DIR", "./data")
+ if not os.path.exists(data_dir):
+ os.makedirs(data_dir)
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(sample_stream("mavericks", data_dir))
+ tg.create_task(sample_stream("mavericksov", data_dir))
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
+ asyncio.run(main())