Skip to content

Commit 29d11a7

Browse files
feat: AI Detector and Send Images using FileIO URL (#1)
* chore: Add unwanted files * docs: Update dependencies * feat: Add tinyml model files * feat: Add ai detector strategy * feat: Add lock mechanism for camera unuse * refactor: Remove debugging print statements * feat: add file sending option * fix: Add error text check for request limits * fix: return image with boxes * fix: create image directory if not already * fix: return image result from detector * feat: Use new functionality
1 parent 8ac04dc commit 29d11a7

13 files changed

Lines changed: 281 additions & 30 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
__pycache__/
22
.pytest_cache/
33
.tox/
4-
.venv/
4+
.*venv/
55
.vscode/
66
.vscode-test/
77
.vagra
88
DS_Store
9-
hss_venv/
9+
*.log

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This project is a home security system that uses a Raspberry Pi and a camera, wh
1010
### Installation
1111

1212
```bash
13+
$ sudo apt install -y python3-picamera2
1314
$ virtualenv venv
1415
$ source venv/bin/activate
1516
$ pip install -r requirements.txt

core/observers/observer/hss_observer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from core.observers.subject.wifi_subject import WiFiSubject
88
from core.observers.subject.eye_subject import EyeSubject
99
from core.utils.datatypes import EyeStates, WiFiStates
10+
from core.utils.fileio_adaptor import upload_to_fileio, read_latest_file
1011
from core.strategies.notifier.base_notifier_strategy import BaseNotifierStrategy
1112

1213
# Add logging support.
@@ -34,7 +35,10 @@ def update(self, subject: BaseSubject) -> None:
3435

3536
if self.wifi_state == WiFiStates.DISCONNECTED and self.eye_state == EyeStates.DETECTED:
3637
logger.info("There is an intruder!")
37-
self._notifier.notify_all("There is an intruder!")
38+
fileio_link = upload_to_fileio(
39+
read_latest_file("~/.home-security-system/images")
40+
)
41+
self._notifier.notify_all(f"There is an intruder! Here is the image: {fileio_link}.")
3842

3943
def set_notifier(self, notifier: BaseNotifierStrategy) -> None:
4044
"""This method is called when the observer is updated."""

core/observers/subject/eye_subject.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
This class inherits from IBaseSubject.
33
Concretes a subject for Eye/Camera features.
44
"""
5+
import os
56
import logging
67
from datetime import datetime
78
from time import sleep
8-
from threading import Thread
9+
from threading import Thread, Lock
10+
from typing import Optional
911

1012
import cv2
1113

1214
from core.utils.datatypes import EyeStates, EyeStrategyResult
15+
from core.utils.fileio_adaptor import upload_to_fileio
1316
from core.observers.subject.base_subject import BaseSubject
1417
from core.strategies.eye.base_eye_strategy import BaseEyeStrategy
1518

@@ -22,37 +25,61 @@ class EyeSubject(BaseSubject):
2225
This class inherits from IBaseSubject.
2326
Concretes a subject for Eye/Camera features.
2427
"""
28+
DEFAULT_IMAGE_LOCATIONS: str = "~/.home-security-system/images"
2529
DEFAULT_SLEEP_INTERVAL = 10
2630
SLEEP_INTERVAL_DETECTED = 5
2731

28-
def __init__(self, image_path: str):
32+
def __init__(self, image_path: str = DEFAULT_IMAGE_LOCATIONS):
2933
super().__init__()
30-
self._image_path = image_path
34+
self._image_path = (
35+
image_path
36+
if '~' not in image_path
37+
else os.path.expanduser(image_path)
38+
)
39+
40+
# Create the default image directory if not exists.
41+
os.makedirs(self._image_path, exist_ok=True)
3142

3243
@staticmethod
3344
def get_default_state() -> EyeStates:
3445
"""This method is called when the observer is updated."""
3546
return EyeStates.UNREACHABLE
3647

37-
def run(self, eye_strategy: BaseEyeStrategy) -> None:
48+
def run(self,
49+
eye_strategy: BaseEyeStrategy,
50+
wifi_lock: Optional[Lock] = None
51+
) -> None:
3852
"""This method is called when the observer is updated."""
39-
thread = Thread(target=self._run_in_loop, args=(self, eye_strategy,))
53+
thread = Thread(target=self._run_in_loop, args=(self, eye_strategy, wifi_lock))
4054
thread.start()
4155
logger.debug("EyeSubject is running...")
4256

4357
@staticmethod
44-
def _run_in_loop(self, eye_strategy: BaseEyeStrategy) -> None:
58+
def _run_in_loop(self,
59+
eye_strategy: BaseEyeStrategy,
60+
wifi_lock: Optional[Lock] = None
61+
) -> None:
4562
"""This method is called when the observer is updated."""
4663
sleep_interval = EyeSubject.DEFAULT_SLEEP_INTERVAL
4764

65+
# Create a dummy lock instance if not given.
66+
if wifi_lock is None:
67+
wifi_lock = Lock()
68+
4869
while True:
49-
result = eye_strategy.check_if_detected()
50-
logger.debug("EyeStrategyResult: " + str(result.result))
70+
# If WiFi subject would give rights to use camera,
71+
# Check if any intruders detected.
72+
if not wifi_lock.locked():
73+
result = eye_strategy.check_if_detected()
74+
logger.debug("EyeStrategyResult: " + str(result.result))
5175

52-
if result.result:
53-
self.set_state(EyeStates.DETECTED)
54-
self._save_image(result)
55-
sleep_interval = EyeSubject.SLEEP_INTERVAL_DETECTED
76+
if result.result:
77+
self.set_state(EyeStates.DETECTED)
78+
self._save_image(result)
79+
sleep_interval = EyeSubject.SLEEP_INTERVAL_DETECTED
80+
81+
# If the WiFi subject does not give rights,
82+
# aka: "There is protectors around the house."
5683
else:
5784
self.set_state(EyeStates.NOT_DETECTED)
5885
sleep_interval = EyeSubject.DEFAULT_SLEEP_INTERVAL

core/observers/subject/wifi_subject.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
Concretes a subject WiFi features.
44
"""
55
import logging
6-
from threading import Thread
6+
from threading import Thread, Lock
77
from time import sleep
8+
from typing import Optional
89

910
from core.utils.datatypes import WiFiStates
1011
from core.observers.subject.base_subject import BaseSubject
@@ -19,6 +20,8 @@ class WiFiSubject(BaseSubject):
1920
This class inherits from IBaseSubject.
2021
Concretes a subject for WiFiS features.
2122
"""
23+
SINGLETON_LOCK: Optional[Lock] = None
24+
2225
@staticmethod
2326
def get_default_state() -> WiFiStates:
2427
"""This method is called when the observer is updated."""
@@ -29,16 +32,32 @@ def run(self, wifi_strategy: BaseWiFiStrategy) -> None:
2932
thread = Thread(target=self._run_in_loop, args=(self, wifi_strategy,))
3033
thread.start()
3134
logger.debug("WiFiSubject is running...")
35+
36+
@classmethod
37+
def get_protector_lock(cls) -> Lock:
38+
"""This method returns a Lock object where it can be
39+
used to block camera when there is a WiFi connection
40+
from protectors.
41+
"""
42+
if cls.SINGLETON_LOCK is None:
43+
cls.SINGLETON_LOCK = Lock()
44+
return cls.SINGLETON_LOCK
3245

3346
@staticmethod
3447
def _run_in_loop(self, wifi_strategy: BaseWiFiStrategy) -> None:
3548
"""This method is called when the observer is updated."""
49+
protector_lock: Lock = self.get_protector_lock()
50+
3651
while True:
3752
protectors = wifi_strategy.check_protectors()
3853
logger.debug("Protectors: " + str(protectors.result) + " " + str(protectors.protector))
3954

4055
if protectors.result:
4156
self.set_state(WiFiStates.CONNECTED)
57+
if not protector_lock.locked():
58+
protector_lock.acquire()
4259
else:
4360
self.set_state(WiFiStates.DISCONNECTED)
61+
if protector_lock.locked():
62+
protector_lock.release()
4463
sleep(5)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
An TinyML detection technique using Efficientdet model.
3+
"""
4+
import time
5+
from typing import Any
6+
import cv2
7+
import numpy
8+
from tflite_runtime.interpreter import Interpreter
9+
from .base_detector_strategy import BaseDetectorStrategy, DetectorResult
10+
11+
12+
class EfficientdetStrategy(BaseDetectorStrategy):
13+
"""
14+
The Efficientdet strategy for detection of objects.
15+
"""
16+
MODEL_PATH: str = "models/efficientdet_1.tflite"
17+
LABEL_PATH: str = "models/efficientdet_1_labelmap.txt"
18+
DETECTION_THRES: float = 0.35
19+
20+
@classmethod
21+
def detect_humans(cls, frame: numpy.ndarray) -> DetectorResult:
22+
"""This method detects if there are any humans in the frame."""
23+
# Create an model interpreter.
24+
interpreter: Interpreter = Interpreter(model_path=cls.MODEL_PATH)
25+
interpreter.allocate_tensors()
26+
27+
# Get model input and output details.
28+
input_details: list[dict[str, Any]] = interpreter.get_input_details()
29+
output_details: list[dict[str, Any]] = interpreter.get_output_details()
30+
_, input_height, input_width, _ = input_details[0]['shape']
31+
32+
# Prepare image for input-tensor.
33+
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
34+
image = cv2.resize(image, (input_width, input_height), interpolation=cv2.INTER_AREA)
35+
image_height, image_width = image.shape[:2]
36+
37+
# Apply the frame into first tensor of the model.
38+
input_data = numpy.expand_dims(image, axis=0)
39+
interpreter.set_tensor(input_details[0]['index'], input_data)
40+
41+
# Calculate the output tensor.
42+
interpreter.invoke()
43+
44+
# Recieve the output.
45+
boxes = interpreter.get_tensor(output_details[0]['index'])[0]
46+
classes = interpreter.get_tensor(output_details[1]['index'])[0]
47+
scores = interpreter.get_tensor(output_details[2]['index'])[0]
48+
49+
# Read label-map.
50+
with open(cls.LABEL_PATH, 'r', encoding="utf-8") as labelmap:
51+
labels = [line.strip() for line in labelmap.readlines()]
52+
53+
# Create color legend for each class type.
54+
colors = numpy.random.randint(0, 255, size=(len(labels), 3), dtype='uint8')
55+
56+
# Convert RGB to BGR again.
57+
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
58+
59+
# Travers through detections.
60+
detection_regions: list[tuple[int, int, int, int]] = []
61+
for score, box, pred_class in zip(scores, boxes, classes):
62+
if score < cls.DETECTION_THRES:
63+
continue
64+
65+
class_name = labels[int(pred_class)]
66+
if class_name == "person":
67+
min_y = round(box[0] * image_height)
68+
min_x = round(box[1] * image_width)
69+
max_y = round(box[2] * image_height)
70+
max_x = round(box[3] * image_width)
71+
detection_regions.append((min_x, max_x, min_y, max_y))
72+
73+
cv2.rectangle(image, (min_x, min_y), (max_x, max_y), (0, 255, 0), 2)
74+
75+
result = DetectorResult(
76+
image=image,
77+
human_found=len(detection_regions) > 0,
78+
regions=detection_regions,
79+
num_detections=len(detection_regions),
80+
)
81+
return result

core/strategies/eye/base_eye_strategy.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,4 @@ def check_if_detected(self) -> EyeStrategyResult:
3434
frame = self.get_frame()
3535
# Detect humans in the frame.
3636
result = self._detect_humans(frame)
37-
38-
if result.human_found:
39-
return EyeStrategyResult(image=frame, result=True)
40-
return EyeStrategyResult(image=frame, result=False)
41-
37+
return EyeStrategyResult(image=result.image, result=result.human_found)

core/strategies/notifier/whatsapp_strategy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def _send_message(self, reciever: WhatsappReciever, message: str) -> bool:
3232
response = requests.get(request_url, timeout=10)
3333

3434
# Check if the request was unsuccessful.
35-
if response.status_code != 200:
35+
if response.status_code != 200 or "ERROR" in response.text:
3636
logger.error("Failed to send WhatsApp message to %s", reciever.telephone_number)
3737
logger.error("Status code: %s", response.status_code)
3838
return False

core/utils/fileio_adaptor.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from typing import Any
2+
import requests
3+
from requests.auth import HTTPBasicAuth
4+
import os
5+
import glob
6+
7+
def read_latest_file(dir_path: str) -> str:
8+
"""This method reads the latest file from the given directory."""
9+
# Check if ~ is used.
10+
if dir_path.startswith("~"):
11+
dir_path = os.path.expanduser(dir_path)
12+
13+
# Check if the directory exists.
14+
if not os.path.exists(dir_path):
15+
raise FileNotFoundError(f"The given directory path does not exist: {dir_path}")
16+
17+
# Get the latest file.
18+
list_of_files = glob.glob(dir_path + '/*')
19+
return max(list_of_files, key=os.path.getctime)
20+
21+
def upload_to_fileio(file_path: str) -> str:
22+
"""Uploads a image file to File.io server."""
23+
response = requests.post(
24+
'https://file.io/',
25+
files={"file": open(file_path, 'rb')},
26+
auth=HTTPBasicAuth("API_KEY_HERE", '')
27+
)
28+
res: dict[str, Any] = response.json()
29+
if res['success'] == True:
30+
return res['link']
31+
else:
32+
return "File upload failed!"

main.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
from core.observers.subject.wifi_subject import WiFiSubject
77
from core.observers.observer.hss_observer import HomeSecuritySystemObserver
88
from core.strategies.wifi.ipaddress_strategy import IpAddressStrategy
9-
from core.strategies.eye.usbcamera_strategy import UsbCameraStrategy
109
from core.strategies.eye.picamera_strategy import PiCameraStrategy
1110
from core.strategies.notifier.whatsapp_strategy import WhatsappStrategy
12-
from core.strategies.detectors.hog_descriptor_strategy import HogDescriptorStrategy
11+
from core.strategies.detectors.efficientdet_strategy import EfficientdetStrategy
1312
from core.utils.datatypes import WhatsappReciever, Protector
1413

1514

@@ -22,18 +21,17 @@
2221
filemode='a',
2322
)
2423

25-
2624
def main():
2725
"""
2826
This method is the entry point of the application.
2927
"""
3028
# Create a WhatsApp notifier.
3129
whatsapp_notifier = WhatsappStrategy()
32-
whatsapp_notifier.add_reciever(WhatsappReciever("Gokhan", "tel_no", "api_key"))
30+
whatsapp_notifier.add_reciever(WhatsappReciever("RECIEVER_NAME", "TEL_NO", "API_KEY"))
3331

3432
# Create a Protector within IpAddressStrategy.
3533
ip_address_strategy = IpAddressStrategy()
36-
ip_address_strategy.add_protector(Protector("Gokhan_iPhone", "tel_ip"))
34+
ip_address_strategy.add_protector(Protector("PROTOTECTOR_NAME", "IP_ADDR"))
3735

3836
# Create observer.
3937
hss_observer = HomeSecuritySystemObserver()
@@ -42,14 +40,16 @@ def main():
4240
# Create subjects to observe.
4341
wifi_subject = WiFiSubject()
4442
wifi_subject.attach(hss_observer)
45-
eye_subject = EyeSubject("images/")
43+
eye_subject = EyeSubject()
4644
eye_subject.attach(hss_observer)
4745

4846
# Run subjects.
4947
wifi_subject.run(ip_address_strategy)
48+
49+
# Set-up the camera to detect humans.
5050
camera = PiCameraStrategy()
51-
camera.set_detector(HogDescriptorStrategy())
52-
eye_subject.run(camera)
51+
camera.set_detector(EfficientdetStrategy())
52+
eye_subject.run(camera, wifi_subject.get_protector_lock())
5353

5454

5555
if __name__ == "__main__":

0 commit comments

Comments
 (0)