Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added CFAR and CCL feeders #494

Merged
merged 9 commits into from
May 6, 2022
Merged
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
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
python -m venv venv
. venv/bin/activate
pip install --upgrade pip
pip install -e .[dev]
pip install -e .[dev] opencv-python-headless
- save_cache:
paths:
- ./venv
Expand Down Expand Up @@ -71,7 +71,7 @@ jobs:
python -m venv venv
. venv/bin/activate
pip install --upgrade pip
pip install -e .[dev]
pip install -e .[dev] opencv-python-headless
- save_cache:
paths:
- ./venv
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
'member-order': 'bysource',
}
autodoc_mock_imports = [
'ffmpeg', 'moviepy', 'tensorflow', 'object_detection', 'tensornets']
'ffmpeg', 'moviepy', 'tensorflow', 'object_detection', 'tensornets', 'cv2']

autosectionlabel_prefix_document = True

Expand Down
6 changes: 6 additions & 0 deletions docs/source/stonesoup.feeder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ Time Based
.. automodule:: stonesoup.feeder.time
:show-inheritance:

Image
-----

.. automodule:: stonesoup.feeder.image
:show-inheritance:

2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
'pytest-flake8', 'pytest-cov', 'pytest-remotedata',
'Sphinx', 'sphinx_rtd_theme', 'sphinx-gallery>=0.8', 'pillow', 'folium',
],
'video': ['ffmpeg-python', 'moviepy'],
'video': ['ffmpeg-python', 'moviepy', 'opencv-python'],
'tensorflow': ['tensorflow>=2.2.0'],
'tensornets': ['tensorflow>=2.2.0', 'tensornets'],
},
Expand Down
126 changes: 126 additions & 0 deletions stonesoup/feeder/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import numpy as np

try:
import cv2
except ImportError as error:
raise ImportError('Use of the image feeder classes requires that opencv-python is installed.')\
from error

from .base import Feeder
from ..base import Property
from ..buffered_generator import BufferedGenerator
from ..types.sensordata import ImageFrame


class CFAR(Feeder):
"""Cell-averaging (CA) Constant False Alarm Rate (CFAR) image data feeder

The CFAR feeder reads grayscale frames from an appropriate :class:`~.FrameReader`or
:class:`~.Feeder` and outputs binary frames whose pixel values are either 0 or 255,
indicating the lack or presence of a detection, respectively.

See `here <https://en.wikipedia.org/wiki/Constant_false_alarm_rate#Cell-averaging_CFAR>`__ for
more information on CA-CFAR.

.. note::
The frames forwarded by the :attr:`~.CFAR.reader` must be grayscale :class:`~.ImageFrame`
objects. As such :attr:`~.ImageFrame.pixels` for all frames must be 2-D arrays, containing
grayscale intensity values.
"""
train_size: int = Property(doc="The number of train pixels", default=10)
guard_size: int = Property(doc="The number of guard pixels", default=4)
alpha: float = Property(doc="The threshold value", default=1.)
squared: bool = Property(doc="If set to True, the threshold will be computed as a function of "
"the sum of squares. The default is False, in which case a "
"simple sum will be evaluated.", default=False)

@BufferedGenerator.generator_method
def data_gen(self):
for timestamp, frame in self.reader:
img = frame.pixels.copy()
output_img = self.cfar(img, self.train_size, self.guard_size, self.alpha, self.squared)
new_frame = ImageFrame(output_img, frame.timestamp)
yield timestamp, new_frame

@staticmethod
def cfar(input_img, train_size=10, guard_size=4, alpha=1., squared=False):
""" Perform Constant False Alarm Rate (CFAR) detection on an input image

Parameters
----------
input_img: numpy.ndarray
The input grayscale image.
train_size: int
The number of train pixels.
guard_size: int
The number of guard pixels.
alpha: float
The threshold value.
squared: bool
If set to True, the threshold will be computed as a function of the sum of squares.
The default is False, in which case a simple sum will be evaluated.
Returns
-------
numpy.ndarray
Output image containing 255 for pixels where a target is detected and 0 otherwise.
"""
# Get width and height of image
width, height = input_img.shape
# Compute the CFAR window size
window_size = 1 + 2*guard_size + 2*train_size
# Initialise empty output image
output_img = np.zeros(input_img.shape, np.uint8)
# Iterate through all pixels
for i in range(height-window_size):
for j in range(width-window_size):
# Compute coordinates of test pixel
c_i = i + guard_size + train_size
c_j = j + guard_size + train_size
# Select the pixels inside the window
v = input_img[i:i + window_size, j:j + window_size].copy()
# Exclude pixels inside guard zone
v[train_size:train_size + 2 * guard_size + 1,
train_size:train_size + 2 * guard_size + 1] = 0
# # The above should be equivalent to the code below
# v = np.zeros((window_size, window_size))
# for k in range(window_size):
# for l in range(window_size):
# if (k >= train_size) and (k < (window_size - train_size)) \
# and (l >= train_size) and (l < (window_size - train_size)):
# continue
# v[k, l] += input_img[i+k,j+l]
# Compute the threshold
if squared:
v = v**2
threshold = np.sum(v) / (window_size**2 - (2*guard_size + 1)**2)
# Populate the output image
input_value = input_img[c_i, c_j]
if squared:
input_value = input_value**2
if input_value/threshold > alpha:
output_img[c_i, c_j] = 255
return output_img


class CCL(Feeder):
"""Connected Component Labelling (CCL) image data feeder

The CCL feeder reads binary frames from an appropriate :class:`~.FrameReader`or
:class:`~.Feeder` and outputs labelled frames whose pixel values contain the label of the
connected component to which each pixel is has been assigned.

See `here <https://en.wikipedia.org/wiki/Connected-component_labeling#Graphical_example_of_
two-pass_algorithm>`__ for more information on and example applications of CCL.

.. note::
The frames forwarded by the :attr:`~.CCL.reader` must be binary :class:`~.ImageFrame`
objects. As such :attr:`~.ImageFrame.pixels` for all frames must be 2-D arrays, where each
element can take only 1 of 2 possible values (e.g. [0 or 1], [0 or 255], etc.).
"""
@BufferedGenerator.generator_method
def data_gen(self):
for timestamp, frame in self.reader:
img = frame.pixels.copy()
_, labels_img = cv2.connectedComponents(img)
new_frame = ImageFrame(labels_img, frame.timestamp)
yield timestamp, new_frame
19 changes: 18 additions & 1 deletion stonesoup/feeder/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import os
import datetime

from distutils import dir_util
import pytest

from ...buffered_generator import BufferedGenerator
Expand Down Expand Up @@ -159,3 +160,19 @@ def groundtruth_paths_gen(self):
yield time, {redpath, yellowpath}

return GroundTruth()


@pytest.fixture
def datadir(tmpdir, request):
'''
Fixture responsible for searching a folder with the same name of test
module and, if available, moving all contents to a temporary directory so
tests can use them freely.
'''
filename = request.module.__file__
test_dir, _ = os.path.splitext(filename)

if os.path.isdir(test_dir):
dir_util.copy_tree(test_dir, str(tmpdir))

return tmpdir
39 changes: 39 additions & 0 deletions stonesoup/feeder/tests/test_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pytest
import numpy as np
from pathlib import Path
import matplotlib.image as mpimg

from stonesoup.reader.image import SingleImageFileReader

try:
from stonesoup.feeder.image import CFAR, CCL
except ImportError:
# Catch optional dependencies import error
pytest.skip(
"Skipping due to missing optional dependencies. Use of the image feeder classes"
" requires that opencv-python is installed.",
allow_module_level=True
)


def test_cfar_detections(datadir):
input_filename = datadir.join('test_img.png')
reader = SingleImageFileReader(input_filename)
feeder = CFAR(reader, train_size=10, guard_size=4, alpha=4, squared=True)
for _, frame in feeder:
th1 = feeder.current[1].pixels
expected_result_filename = Path(datadir.join('expected_result_cfar.png'))
img = mpimg.imread(expected_result_filename)*255
assert np.array_equal(th1, img)


def test_ccl_detections(datadir):
input_filename = datadir.join('test_img.png')
reader = SingleImageFileReader(input_filename)
cfar = CFAR(reader, train_size=10, guard_size=4, alpha=4, squared=True)
feeder = CCL(cfar)
for _, frame in feeder:
labels_img = frame.pixels
expected_result_filename = Path(datadir.join('expected_result_ccl.png'))
img = mpimg.imread(expected_result_filename) * 255
assert np.array_equal(labels_img, img)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added stonesoup/feeder/tests/test_image/test_img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.