ppbbww

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

commit 64f208a8683d5c659039dbe3ba98b0e5be88c342
parent 6352b48db4f62f22b16a22482e7e2912bb2b45c0
Author: david cochran <about.trout@gmail.com>
Date:   Sat, 17 Feb 2024 12:00:25 -0800

fixes #5 - add pyproject config and reorganize source files

Diffstat:
MREADME.md | 20++++++++++----------
Dboatfinder.py | 66------------------------------------------------------------------
Appboatwatch/__init__.py | 4++++
Appboatwatch/boat_finder.py | 40++++++++++++++++++++++++++++++++++++++++
Appboatwatch/find_boats.py | 26++++++++++++++++++++++++++
Appboatwatch/sample_streams.py | 32++++++++++++++++++++++++++++++++
Appboatwatch/stream_sampler.py | 42++++++++++++++++++++++++++++++++++++++++++
Apyproject.toml | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Drequirements.txt | 43-------------------------------------------
Dstreamsampler.py | 70----------------------------------------------------------------------
10 files changed, 213 insertions(+), 189 deletions(-)

diff --git a/README.md b/README.md @@ -4,20 +4,20 @@ Locate large boats as they pass by Pillar Point. ![](https://i.imgur.com/uSNPctj.jpeg) -Install the requirements. +#### Install the requirements. ``` -$ sudo apt install ffmpeg # for extracing keyframes -$ sudo apt install nvidia-cuda-toolkit # for GPU support -$ python3 -m venv .venv -$ source .venv/bin/activate -$ pip install -r requirements.txt +$ apt install ffmpeg # for frame extraction +$ apt install nvidia-cuda-toolkit # for GPU support +$ python3 -m venv venv +$ source venv/bin/activate +$ pip install -e . ``` -Accumulate data by sampling the Mavericks cams on Surfline. +#### Accumulate data by sampling streams. ``` -$ ./streamsampler.py +$ sample-streams 2024-02-11 20:36:15,997 cam_name=mavericksov num_frames=4 Extracted frames 2024-02-11 20:36:15,998 cam_name=mavericksov delay=12 Sleeping 2024-02-11 20:36:16,583 cam_name=mavericks num_frames=5 Extracted frames @@ -29,10 +29,10 @@ $ ./streamsampler.py ... ``` -Find boats in images with [`BoatFinder`](./boatfinder.py#L12). +#### Find boats in images with [DETR](https://huggingface.co/facebook/detr-resnet-50). ``` -$ ./boatfinder.py ./data/mavericksov/02142024/11/1707939852-thumb-0003.jpg +$ find-boats 2024-02-14 13:56:06,022 BoatFinder initializing ... 2024-02-14 13:56:08,380 BoatFinder initialized (for cuda:0); took 2.3576930790004553 seconds 2024-02-14 13:56:08,384 Searching file ./data/mavericksov/02142024/11/1707939852-thumb-0003.jpg... diff --git a/boatfinder.py b/boatfinder.py @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import sys -import torch - -from PIL import Image, ImageDraw -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.device = "cuda:0" if torch.cuda.is_available() else "cpu" - self.processor = DetrImageProcessor.from_pretrained( - "facebook/detr-resnet-50", revision="no_timm" - ) - self.model = DetrForObjectDetection.from_pretrained( - "facebook/detr-resnet-50", revision="no_timm" - ).to(self.device) - logging.info(f"BoatFinder initialized (for {self.device}); took {perf_counter() - t0} seconds") - - def find(self, img): - t0 = perf_counter() - 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 = [int(i) for i in box.tolist()] - yield (score, label, box) - logging.info(f"Finished search in {perf_counter() - t0} seconds") - - def __match_results(self, image): - inputs = self.processor(images=image, return_tensors="pt").to(self.device) - outputs = self.model(**inputs) - target_sizes = torch.tensor([image.size[::-1]]).to(self.device) - 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}...") - res = list(bf.find(image)) - if len(res) > 0: - draw = ImageDraw.Draw(image) - for score, label, box in res: - logging.info(f">> label={label}\t score={score}\t box={box}\t") - draw.rectangle(box, outline="#a6e22e") - out_file = img_file.removesuffix(".jpg") + "_boats.jpg" - image.save(out_file) - logging.info(f"Outlined matches and saved to file {out_file}") - diff --git a/ppboatwatch/__init__.py b/ppboatwatch/__init__.py @@ -0,0 +1,4 @@ +from .stream_sampler import StreamSampler +from .boat_finder import BoatFinder + +__all__ = ["StreamSampler", "BoatFinder"] diff --git a/ppboatwatch/boat_finder.py b/ppboatwatch/boat_finder.py @@ -0,0 +1,40 @@ +import logging +import sys +import torch + +from PIL import Image, ImageDraw +from time import perf_counter +from transformers import DetrImageProcessor, DetrForObjectDetection + + +class BoatFinder: + def __init__(self): + logging.info(f"BoatFinder initializing ...") + t0 = perf_counter() + self.device = "cuda:0" if torch.cuda.is_available() else "cpu" + # 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").to(self.device) + logging.info(f"BoatFinder initialized (for {self.device}); took {perf_counter() - t0} seconds") + + def find(self, img): + t0 = perf_counter() + 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 = [int(i) for i in box.tolist()] + yield (score, label, box) + logging.info(f"Finished search in {perf_counter() - t0} seconds") + + def __match_results(self, image): + inputs = self.processor(images=image, return_tensors="pt").to(self.device) + outputs = self.model(**inputs) + target_sizes = torch.tensor([image.size[::-1]]).to(self.device) + return self.processor.post_process_object_detection( + outputs, target_sizes=target_sizes, threshold=0.5 + )[0] + diff --git a/ppboatwatch/find_boats.py b/ppboatwatch/find_boats.py @@ -0,0 +1,26 @@ +import logging +import os + +from PIL import Image, ImageDraw +from .boat_finder import BoatFinder + + +def main(): + logging.basicConfig(level=logging.WARN, format="%(asctime)s %(message)s") + bf = BoatFinder() + find_dir = "./data" # TODO: argparse. + for base, _, fs in os.walk(find_dir): + for f in fs: + # TODO: Ignore _boats.jpg files! + img_file = f"{base}/{f}" + image = Image.open(img_file) + res = list(bf.find(image)) + if len(res) > 0: + draw = ImageDraw.Draw(image) + for score, label, box in res: + draw.rectangle(box, outline="#ff0000") + out_file = img_file.removesuffix(".jpg") + "_boats.jpg" + image.save(out_file) + logging.warning(f"Found {len(res)} matches in {img_file}") + else: + logging.warning(f"No matches in {img_file}") diff --git a/ppboatwatch/sample_streams.py b/ppboatwatch/sample_streams.py @@ -0,0 +1,32 @@ +import asyncio +import logging +import random + +from .stream_sampler import StreamSampler + + +async def sample_stream(cam_name, data_dir): + ss = StreamSampler(cam_name, data_dir) + while True: + try: + frames = await ss.get_recent_frames() + logging.info(f"cam_name={cam_name} num_frames={len(frames)} Extracted frames") + except Exception as ex: + logging.error(f"cam_name={cam_name} Failed to get_recent_frames: {ex}") + # TODO: Decrease delay but only keep 1 frame? + delay = random.randint(5, 20) # seconds! + logging.info(f"cam_name={cam_name} delay={delay} Sleeping") + await asyncio.sleep(delay) + + +async def main_task(cams, data_dir): + async with asyncio.TaskGroup() as tg: + for cam in cams: + tg.create_task(sample_stream(cam, data_dir)) + + +def main(): + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") + cams = ["mavericks", "mavericksov"] # TODO: argparse. + data_dir = "./data" # TODO: argparse + asyncio.run(main_task(cams, data_dir)) diff --git a/ppboatwatch/stream_sampler.py b/ppboatwatch/stream_sampler.py @@ -0,0 +1,42 @@ +import aiohttp +import asyncio +import datetime +import logging +import os +import random +import subprocess +import tempfile +import time + + +class StreamSampler: + def __init__(self, cam_name, data_dir="./data"): + self.base_url = f"https://cams.cdn-surfline.com/cdn-wc/wc-{cam_name}" + self.cam_name = cam_name + self.data_dir = data_dir + + async def get_recent_frames(self): + async with aiohttp.ClientSession(raise_for_status=True) as session: + chunk_url = await self.__get_recent_chunk_url(session) + return self.__extract_frames(chunk_url) + + async def __get_recent_chunk_url(self, session): + async with session.get(f"{self.base_url}/chunklist.m3u8") as res: + chunklist = await res.text() + chunk = next(l for l in chunklist.splitlines() if not l.startswith("#")) + return f"{self.base_url}/{chunk}" + + def __extract_frames(self, chunk_url): + dt = datetime.datetime.now() + day, hour, ts = dt.strftime("%m%d%Y"), dt.strftime("%H"), dt.strftime("%s") + frames_dir = f"{self.data_dir}/{self.cam_name}/{day}/{hour}" + if not os.path.exists(frames_dir): + os.makedirs(frames_dir) + subprocess.run(["ffmpeg", + "-hide_banner", "-loglevel", "0", + "-i", chunk_url, + "-vf", "select=eq(pict_type\,I)", + "-vsync", "vfr", + f"{frames_dir}/{ts}-thumb-%04d.jpg"]) + # Return filenames of (ffmpeg generated) frames to caller. + return [f for f in os.scandir(frames_dir) if f.name.startswith(f"{ts}-thumb")] diff --git a/pyproject.toml b/pyproject.toml @@ -0,0 +1,59 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "pp-boatwatch" +version = "0.0.1" +dependencies = [ + "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", +] + +[project.scripts] +find-boats = "ppboatwatch.find_boats:main" +sample-streams = "ppboatwatch.sample_streams:main" + +[tool.setuptools] +packages = ["ppboatwatch"] diff --git a/requirements.txt b/requirements.txt @@ -1,43 +0,0 @@ -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/streamsampler.py b/streamsampler.py @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 - -import aiohttp -import asyncio -import datetime -import logging -import os -import random -import subprocess -import tempfile -import time - - -class StreamSampler: - def __init__(self, cam_name, data_dir="./data"): - self.base_url = f"https://cams.cdn-surfline.com/cdn-wc/wc-{cam_name}" - self.cam_name = cam_name - self.data_dir = data_dir - - async def get_recent_frames(self): - async with aiohttp.ClientSession(raise_for_status=True) as session: - chunk_url = await self.__get_recent_chunk_url(session) - return self.__extract_frames(chunk_url) - - async def __get_recent_chunk_url(self, session): - async with session.get(f"{self.base_url}/chunklist.m3u8") as res: - chunklist = await res.text() - chunk = next(l for l in chunklist.splitlines() if not l.startswith("#")) - return f"{self.base_url}/{chunk}" - - def __extract_frames(self, chunk_url): - dt = datetime.datetime.now() - day, hour, ts = dt.strftime("%m%d%Y"), dt.strftime("%H"), dt.strftime("%s") - frames_dir = f"{self.data_dir}/{self.cam_name}/{day}/{hour}" - if not os.path.exists(frames_dir): - os.makedirs(frames_dir) - subprocess.run(["ffmpeg", - "-hide_banner", "-loglevel", "0", - "-i", chunk_url, - "-vf", "select=eq(pict_type\,I)", - "-vsync", "vfr", - f"{frames_dir}/{ts}-thumb-%04d.jpg"]) - - return [f for f in os.scandir(frames_dir) if f.name.startswith(f"{ts}")] - - -async def sample_stream(cam_name): - ss = StreamSampler(cam_name) - while True: - try: - frames = await ss.get_recent_frames() - logging.info( - f"cam_name={cam_name} num_frames={len(frames)} Extracted frames" - ) - except Exception as ex: - logging.error(f"cam_name={cam_name} Failed to get_recent_frames: {ex}") - delay = random.randint(5, 20) # seconds. - logging.info(f"cam_name={cam_name} delay={delay} Sleeping") - await asyncio.sleep(delay) - - -async def main(): - async with asyncio.TaskGroup() as tg: - tg.create_task(sample_stream("mavericks")) - tg.create_task(sample_stream("mavericksov")) - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") - asyncio.run(main())