ppbbww

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

commit 7460a01764dd54f790d6f99b1c56a92666503cbe
parent 139db1832366a3cf82459f2ff5f7cd5649affdf1
Author: david cochran <about.trout@gmail.com>
Date:   Mon, 26 Feb 2024 19:47:45 -0800

fixes #4 - add PPBW command that posts to Slack

Diffstat:
Appboatwatch/ppbw.py | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpyproject.toml | 1+
2 files changed, 109 insertions(+), 0 deletions(-)

diff --git a/ppboatwatch/ppbw.py b/ppboatwatch/ppbw.py @@ -0,0 +1,108 @@ +import argparse +import asyncio +import logging +import random +import tempfile +import os + +from slack_sdk.web.async_client import AsyncWebClient +from PIL import Image, ImageDraw, ImageFont + +from .stream_sampler import StreamSampler +from .object_detector import ObjectDetector + + +async def sample_stream(frames_q, tmp_dir): + ss = StreamSampler("mavericksov", tmp_dir) + while True: + try: + frames = await ss.get_recent_frames() + logging.warning(f"[sample_stream]: Extracted {len(frames)} recent frames") + assert len(frames) > 0 + await frames_q.put(frames[0].path) + for frame in frames[1:]: # we only keep 1 frame! + os.remove(frame) + except Exception as ex: + logging.error(f"[sample_stream]: Failed to get_recent_frames: {ex}") + + delay = random.randint(3, 10) # seconds! + logging.warning(f"[sample_stream]: Sleeping {delay} seconds") + await asyncio.sleep(delay) + + +async def find_matches(frames_q, matches_q): + detector = ObjectDetector(thresh=0.80) + while True: + # Get filenames from the queue and run object detector. + frame_file = await frames_q.get() + image = Image.open(frame_file) + matches = list(filter_matches(detector.find(image))) + if len(matches) == 0: + os.remove(frame_file) + logging.warning(f"[find_matches]: No matches; cleaned up") + continue + # Draw bounding boxes and labels for all matches. + ImageFont.load_default(size=13) + draw = ImageDraw.Draw(image) + for label, score, box in matches: + #draw.text((box[0], box[1]-20), f"{label} {score}", fill="#ffffff", font_size=13) + draw.rectangle(box, outline="#ffffff", width=2) + # Save file and put in queue; post_matches job will handle cleanup. + out_file = frame_file.removesuffix(".jpg") + "_matches.jpg" + image.save(out_file) + await matches_q.put(out_file) + + +def filter_matches(matches): + for label, score, box in matches: + box = list(box) + # Skip matches at the bottom of the image area (rocks). + if box[1] > 640: + continue + # Skip common but uninteresting cases based on size and label. + box_size = (box[2] - box[0]) * (box[3] - box[1]) + if box_size < 500: + logging.debug(f"Skipping small box (box_size={box_size})") + continue + if label == "boat" and box_size < 1500: + logging.debug(f"Skipping small boat (box_size={box_size})") + continue + if label == "bird" and box_size < 1000: + logging.debug(f"Skipping small bird (box_size={box_size})") + continue + # Return everything else. + yield (label, score, box) + + +async def post_matches(matches_q, client): + while True: + matches_path = await matches_q.get() + await post_match(client, matches_path) + + +async def post_match(client, frame_file): + with open(frame_file, "rb") as f: + res = await client.files_upload_v2(file=f, channel="C06LMBRNV8U") + if not res["ok"]: + logging.error(f"Failed to post_match: {res}") + + +async def main_task(): + client = AsyncWebClient(token=os.environ["SLACK_API_TOKEN"]) + frames_q, matches_q = asyncio.Queue(), asyncio.Queue() + tmp_dir = tempfile.TemporaryDirectory(dir=".") + async with asyncio.TaskGroup() as tg: + tg.create_task(sample_stream(frames_q, tmp_dir.name)) + tg.create_task(find_matches(frames_q, matches_q)) + tg.create_task(post_matches(matches_q, client)) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-v", "--verbose", action="store_true") + args = parser.parse_args() + + level = logging.DEBUG if args.verbose else logging.WARN + logging.basicConfig(level=level, format="%(asctime)s %(message)s") + + asyncio.run(main_task()) diff --git a/pyproject.toml b/pyproject.toml @@ -54,6 +54,7 @@ dependencies = [ [project.scripts] find-objects = "ppboatwatch.find_objects:main" sample-streams = "ppboatwatch.sample_streams:main" +ppbw = "ppboatwatch.ppbw:main" [tool.setuptools] packages = ["ppboatwatch"]