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:
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.

-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())