Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 79 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# bee_track
Tracking software to run on pi
# Bumblebee Tracking System
This is a web-based bee tracking system that runs on a Raspberry Pi that may be accessed via a mobile phone. It controls a collection of cameras and captures data.

# Getting the pi set up
This code repository contains two systems:

- a front-end user interface implemented in HTML and Javascript;
- a back-end API implemented using the Flask web framework.

# Installation

## Getting the RPi set up

To make it possible to ssh into, use:

Expand All @@ -23,13 +30,13 @@ Edit `/etc/wpa_supplicant/wpa_supplicant.conf`, enter:
psk="PASSWORD"
}

# Installation
# Dependencies

Download the aravis library:
Download the Aravis library:

git clone https://github.com/AravisProject/aravis.git

Or donwload earlier version from
Or download earlier version from
http://ftp.gnome.org/pub/GNOME/sources/aravis/0.6/

then if you need the viewer (although I did find I had to split these installs).
Expand Down Expand Up @@ -105,18 +112,78 @@ frontend <--"HTTP 5000"--> backend

## API architecture

The Flask application has four components which run in separate threads:
The Flask application has four main components, which are workers that run in separate threads:

- Cameras
- Triggers
- Rotation
- Tracking
- Trigger: handles triggering the camera and flashes using GPIO pins.
- Cameras: reads in image data from a camera.
- Rotation: sends a rotation signal to a stepper motor via GPIO pins.
- Tracking: takes images from the greyscale camera's image queue (`cam.photo_queue`) and looks for the tag

Each thread has a worker process with a configuration message queue.

## Camera capture process

### Camera exposure trigger

The `Trigger` class waits for the instruction to activate the exposure pin.

Photos are captured in "sets" which are synchronised with the flash sequence.

1. Click "Start" on the GUI.
2. API activates `Trigger.run` event
3. (Optional) Delay start: wait
4. Activate flashes (via their GPIO pins)
5. Wait for preparation time (20 microseconds)
6. Record a photo capture in the exposure log.
7. Increment the trigger index (photo exposure counter).
8. Trigger camera exposure (activate GPIO pin)
9. The camera captured data based on the GPIO pin activation.
10. Wait for the exposure/trigger time (30 microseconds)
11. Deactivate the trigger pins for the camera and flashes.

### Camera data retrieval

The `Camera` class gets photos from the camera's data buffer.

## User interface

The front-end is implemented using jQuery, a popular JavaScript library for manipulating web pages. In simple terms, each HTML form widget in [webinterface/index.html](./webinterface/index.html) has an associated behaviour determined by the JavaScript code in [webinterface/track.js](./webinterface/track.js), which listens for certain events such as button clicks and makes a call to the back-end API using AJAX (Asynchronous JavaScript and XML).

| Button | Button ID | Endpoint | Behaviour |
| -------------- | --------- | -------- | --------------------------- |
| Capture: Start | `#start` | `/start` | Starts camera data capture. |
| Capture: Stop | `#stop` | `/stop` | Stops camera data capture. |
| | | | |

There is a JavaScript function called `msg()` that prints message to the "console" which is a HTML text area.

# Usage

TODO
The user interface is designed to control the bee tracker system.

1. Access the user interface
2. Enter a label for your files
3. Under capture, click "Start"
4. Under capture, click "Stop"

## API usage

The API returns HTTP responses.

```bash
$ curl http://192.168.50.58:5000/getimagecount
42
```

# Configuration

There are endpoints to set and get configuration options for each of the components.

The configuration options are stored in this file, which is loaded when the app starts up.

```
/home/pi/bee_track/webinterface/configvals.pkl
```

# Development

Expand Down
4 changes: 4 additions & 0 deletions bee_track/battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def scale(v):


def read_batteries():
"""
Get battery level information.
"""

scaleval = 0.0164
# Open SPI bus
spi = spidev.SpiDev()
Expand Down
83 changes: 50 additions & 33 deletions bee_track/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
import multiprocessing
import pickle
import threading
import logging

# TODO what's this?
# https://github.com/SheffieldMLtracking/QueueBuffer
from QueueBuffer import QueueBuffer
import numpy as np

from configurable import Configurable

logger = logging.getLogger(__name__)


class CameraException(Exception):
pass
Expand Down Expand Up @@ -45,6 +48,9 @@ def downscalecolour(img, blocksize):


class Camera(Configurable):
"""
A worker to reads in image data from a camera.
"""

def __init__(self, message_queue, record, cam_trigger, cam_id=None):
"""
Expand All @@ -55,6 +61,7 @@ def __init__(self, message_queue, record, cam_trigger, cam_id=None):
self.record = record
self.label = multiprocessing.Array('c', 100)
self.index = multiprocessing.Value('i', 0)
'Incrementing identifier number per capture (i.e. image count)'
self.savephotos = multiprocessing.Value('b', True)
self.fastqueue = multiprocessing.Value('b', False) ###THIS WILL STOP PROCESSING
self.test = multiprocessing.Value('b', False)
Expand Down Expand Up @@ -86,66 +93,74 @@ def camera_trigger(self):
"""
raise NotImplementedError

# def trigger(self):
# print("Triggering Camera")
# self.cam_trigger.set()
def get_photo(self, get_raw: bool = False) -> np.ndarray:
"""
Retrieve image data from the camera.

This is a synchronous (blocking) function.

def get_photo(self, getraw: bool = False):
"""Blocking, returns a photo numpy array"""
:returns: Photo data (numpy array)
"""
raise NotImplementedError

def worker(self):
print("Camera worker started")
"""
Get image data from the camera.
"""
logger.info("Camera worker started")
self.setup_camera()
t = threading.Thread(target=self.camera_trigger)
t.start()
t = threading.Thread(target=self.camera_config_worker)
t.start()
print("Camera setup complete")

# Start threads to listen for camera trigger and configuration messages
threading.Thread(target=self.camera_trigger).start()
threading.Thread(target=self.camera_config_worker).start()

logger.info("Camera setup complete")

# ???
last_photo_object = None
while True:
# print("waiting for photo")

if self.debug: print(
'Blocking started for getting photo at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
photo = self.get_photo(getraw=self.fastqueue.value)
print(".", end="", flush=True)
if self.debug: print('Got photo at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
# Indefinite loop: transfer image data from the camera
while True:
logger.debug('Blocking started for getting photo at %s' % (
datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
photo = self.get_photo(get_raw=self.fastqueue.value)
logger.debug('Got photo at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))

if photo is None:
print("Photo failed")
logger.info("Photo failed")

rec = None
for r in self.record:
if r['index'] == self.index.value:
rec = r
break
if rec is None:
print("WARNING: Failed to find associated photo record")
logger.info("WARNING: Failed to find associated photo record")

photo_object = {'index': self.index.value, 'record': rec}

if bool(self.return_full_colour.value):
if self.debug: print(
logger.debug(
'generating greyscale copy at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
colorphoto = photo
if photo is not None:
# ???
if self.fastqueue.value:
# photo = downscale(np.mean(photo,2),10)
photo = np.mean(photo[::10, ::10, :], 2)
else:
photo = np.mean(photo, 2)

if self.debug: print(
logger.debug(
'averaging completed at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
photo = photo.astype(np.ubyte)
if self.debug: print(
logger.debug(
'type conversion completed at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
if not self.fastqueue.value: colorphoto = colorphoto.astype(np.ubyte)
if self.debug: print(
logger.debug(
'colour type conv completed at%s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
photo_object['colorimg'] = colorphoto
if self.debug: print(
logger.debug(
'greyscale copy completed at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
else:
if photo is not None:
Expand Down Expand Up @@ -191,12 +206,14 @@ def worker(self):
camidstr, triggertime_string, self.label.value.decode('utf-8'), self.index.value)
photo_object['filename'] = filename
self.message_queue.put("Saved Photo: %s" % filename)
if self.debug: print(
'starting save at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
pickle.dump(photo_object, open(filename, 'wb'))
if self.debug: print(
'finished save at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
if self.info: print("Saved photo as %s" % filename)
if self.debug:
print('starting save at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
with open(filename, 'wb') as file:
pickle.dump(photo_object, file)
if self.debug:
print('finished save at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")))
if self.info:
print("Saved photo as %s" % filename)
else:
filename = 'photo_object_%s_%s.np' % (
camidstr, datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))
Expand All @@ -206,7 +223,7 @@ def worker(self):
self.message_queue.put("Saved Photo: %s" % filename)
# np.save(open('photo_%04i.np' % self.index.value,'wb'),photo.astype(np.ubyte))
print("Incrementing camera index, from %d" % self.index.value)
self.index.value = self.index.value + 1
self.index.value += 1

def close(self):
"""
Expand Down
Loading