Skip to content

Commit

Permalink
Cropping (#33)
Browse files Browse the repository at this point in the history
* Motion detection threshold accepts floating point input.

* First crack at client-side cropping.

* Fixing a couple bugs.  Client-side crop is working now.

* Automatically reformatting code with black and isort

---------

Co-authored-by: Auto-format Bot <[email protected]>
  • Loading branch information
robotrapta and Auto-format Bot authored Mar 14, 2023
1 parent 0850213 commit 05664b1
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 0 deletions.
47 changes: 47 additions & 0 deletions src/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
-v, --verbose enable debug logs
-w, --width=WIDTH resize images to w pixels wide (and scale height proportionately if not set explicitly)
-y, --height=HEIGHT resize images to y pixels high (and scale width proportionately if not set explicitly)
-c, --crop=[x,y,w,h] crop image to box before resizing. x,y,w,h are fractions from 0-1. [Default:"0,0,1,1"]
-m, --motion enable motion detection with pixel change threshold percentage (disabled by default)
-r, --threshold=THRESHOLD detection threshold for motion detection - percent of changed pixels [default: 1]
-p, --postmotion=POSTMOTION minimum number of seconds to capture for every motion detection [default: 1]
Expand All @@ -28,6 +29,7 @@
from operator import truediv
from queue import Empty, Queue
from threading import Thread
from typing import Tuple
from xmlrpc.client import Boolean

import cv2
Expand Down Expand Up @@ -101,6 +103,42 @@ def resize_if_needed(frame, width: int, height: int):
frame = cv2.resize(frame, (target_width, target_height))


def crop_frame(frame, crop_region: Tuple[float, float, float, float]):
"""Returns a cropped version of the frame."""
(img_height, img_width, _) = frame.shape
x1 = int(img_width * crop_region[0])
y1 = int(img_height * crop_region[1])
x2 = x1 + int(img_width * crop_region[2])
y2 = y1 + int(img_height * crop_region[3])

out = frame[y1:y2, x1:x2, :]
return out


def parse_crop_string(crop_string: str) -> Tuple[float, float, float, float]:
"""Parses a string like "0.25,0.25,0.5,0.5" to a tuple like (0.25,0.25,0.5,0.5)
Also validates that numbers are between 0-1, and that it doesn't go off the edge.
"""
parts = crop_string.split(",")
if len(parts) != 4:
raise ValueError("Expected crop to be list of four floating point numbers.")
numbers = tuple([float(n) for n in parts])

for n in numbers:
if (n < 0) or (n > 1):
raise ValueError("All numbers must be between 0 and 1, showing relative position in image")

if numbers[0] + numbers[2] > 1.0:
raise ValueError("Invalid crop: x+w is greater than 1.")
if numbers[1] + numbers[3] > 1.0:
raise ValueError("Invalid crop: y+h is greater than 1.")

if numbers[2] * numbers[3] == 0:
raise ValueError("Width and Height must both be >0")

return numbers


def main():
args = docopt.docopt(__doc__)
if args.get("--verbose"):
Expand All @@ -121,6 +159,11 @@ def main():
except ValueError as e:
raise ValueError(f"invalid height parameter: {args['--height']}")

if args.get("--crop"):
crop_region = parse_crop_string(args["--crop"])
else:
crop_region = None

ENDPOINT = args["--endpoint"]
TOKEN = args["--token"]
DETECTOR = args["--detector"]
Expand Down Expand Up @@ -203,6 +246,10 @@ def main():
now = time.time()
logger.debug(f"captured a new frame after {now-start:.3f}s of size {frame.shape=} ")

if crop_region:
frame = crop_frame(frame, crop_region)
logger.debug(f"Cropped to {frame.shape=}")

if motion_detect:
if m.motion_detected(frame):
logger.info(f"Motion detected")
Expand Down
40 changes: 40 additions & 0 deletions src/test_crop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest

from stream import parse_crop_string


def test_good_crop():
assert parse_crop_string("0,0,1,1") == (0, 0, 1, 1)
assert parse_crop_string("0.5,0,0.5,1") == (0.5, 0, 0.5, 1)

with pytest.raises(ValueError):
# too short
parse_crop_string("0.5,0,0.5")

with pytest.raises(ValueError):
# not numbers
parse_crop_string("a,b,c,d")

with pytest.raises(ValueError):
# too big
parse_crop_string("0,0,256,250")

with pytest.raises(ValueError):
# negative
parse_crop_string("-1,0,1,1")

with pytest.raises(ValueError):
# Off both edges
parse_crop_string("0.5,0.5,0.6,0.6")

with pytest.raises(ValueError):
# Off right
parse_crop_string("0.3,0,0.8,1")

with pytest.raises(ValueError):
# Off bottom
parse_crop_string("0,0.2,0.5,0.9")

with pytest.raises(ValueError):
# zero size
parse_crop_string("0,0,1,0")

0 comments on commit 05664b1

Please sign in to comment.