ppbbww

Pillar Point boats, birds, and waves watcher
git clone git@abtrout.com:ppbbww.git
Log | Files | Refs | README | LICENSE

commit 7ffa5e60929321d4c2d9f991c5501483ca336152
Author: david cochran <about.trout@gmail.com>
Date:   Sat, 10 Feb 2024 13:56:10 -0800

initial commit

Diffstat:
AREADME.md | 43+++++++++++++++++++++++++++++++++++++++++++
Adetect.py | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Arequirements.txt | 43+++++++++++++++++++++++++++++++++++++++++++
Ascrape.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())