Skip to content

Commit

Permalink
Merge pull request #136 from rochefort-lab/rel_0.7.1
Browse files Browse the repository at this point in the history
REL: Release version 0.7.1
  • Loading branch information
scottclowe authored May 22, 2020
2 parents 37f6133 + 073fa5a commit 6efdd57
Show file tree
Hide file tree
Showing 40 changed files with 282 additions and 84 deletions.
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ env:
# Values are "true", "false" or "".
- TEST_NOTEBOOKS="true"
- USE_SIMA="true"
- USE_SUITE2P="true"
- USE_SUITE2P="false"
# Set flag for whether to use a conda environment
# values can be:
# "" or "false": conda not used
Expand Down Expand Up @@ -505,8 +505,8 @@ after_success:
# coverage report wasn't published.
- |
if [[ "$TRAVIS" = "true" && "$SHIPPABLE" != "true" ]]; then
$PYTHONCMD -m pip install codecov;
travis_retry codecov || echo "Codecov push failed";
curl -s -S -L --connect-timeout 5 --retry 6 https://codecov.io/bash -o codecov-upload.sh || echo "Codecov script failed to download";
travis_retry bash codecov-upload.sh || echo "Codecov push failed";
$PYTHONCMD -m pip install coveralls;
travis_retry coveralls || echo "Coveralls push failed";
fi;
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@ Categories for changes are: Added, Changed, Deprecated, Removed, Fixed,
Security.


Version `0.7.1 <https://github.com/rochefort-lab/fissa/tree/0.7.1>`__
---------------------------------------------------------------------

Release date: 2020-05-22.
`Full commit changelog <https://github.com/rochefort-lab/fissa/compare/0.7.0...0.7.1>`__.

.. _v0.7.1 Fixed:

Fixed
~~~~~

- Loading oval, ellipse, brush/freehand, freeline, and polyline ImageJ ROIs on Python 3.
(`#135 <https://github.com/rochefort-lab/fissa/pull/135>`__)

.. _v0.7.1 Added:

Added
~~~~~

- Support for rotated rectangle and multipoint ROIs on Python 3.
(`#135 <https://github.com/rochefort-lab/fissa/pull/135>`__)


Version `0.7.0 <https://github.com/rochefort-lab/fissa/tree/0.7.0>`__
---------------------------------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion fissa/__meta__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = 'fissa'
path = name
version = '0.7.0'
version = '0.7.1'
author = "Sander Keemink & Scott Lowe"
author_email = "[email protected]"
description = "A Python Library estimating somatic signals in 2-photon data"
Expand Down
153 changes: 113 additions & 40 deletions fissa/readimagejrois.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ def _parse_roi_file_py2(roi_obj):
Parameters
----------
roi_obj : file object
File object containing a single ImageJ ROI
roi_obj : file object or str
File object containing a single ImageJ ROI, or a path to a single roi
file.
Returns
-------
Expand All @@ -54,6 +55,11 @@ def _parse_roi_file_py2(roi_obj):
If unable to parse ROI
"""
# If this is a string, try opening the path as a file and running on its
# contents
if isinstance(roi_obj, basestring):
with open(roi_obj) as f:
return _parse_roi_file_py2(f)

# Note:
# _getX() calls with no assignment are present to move our pointer
Expand Down Expand Up @@ -155,6 +161,11 @@ def _getcoords(z=0):
_get32() # stroke color
_get32() # fill color
subtype = _get16()
if subtype == 5:
raise ValueError(
'read_imagej_roi: ROI subtype {} (rotated rectangle) not supported'
.format(subtype)
)
if subtype != 0 and subtype != 3:
raise ValueError('read_imagej_roi: \
ROI subtype {} not supported (!= 0)'.format(subtype))
Expand Down Expand Up @@ -197,23 +208,47 @@ def _getcoords(z=0):
return {'mask': mask}
elif roi_type == 7:
if subtype == 3:
# ellipse
mask = np.zeros((1, right+10, bottom+10), dtype=bool)
r_radius = np.sqrt((x2-x1)**2+(y2-y1)**2)/2.0
c_radius = r_radius*aspect_ratio
r = (x1+x2)/2-0.5
c = (y1+y2)/2-0.5
shpe = mask.shape
orientation = np.arctan2(y2-y1, x2-x1)
X, Y = ellipse(r, c, r_radius, c_radius, shpe[1:], orientation)
mask[0, X, Y] = True
# Ellipse
# Radius of major and minor axes
r_radius = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / 2.
c_radius = r_radius * aspect_ratio
# Centre coordinates
# We subtract 0.5 because ImageJ's co-ordinate system has indices at
# the pixel boundaries, and we are using indices at the pixel centers.
x_mid = (x1 + x2) / 2. - 0.5
y_mid = (y1 + y2) / 2. - 0.5
orientation = np.arctan2(y2 - y1, x2 - x1)

# We need to make a mask which is a bit bigger than this, because
# we don't know how big the ellipse will end up being. In the most
# extreme case, it is a circle aligned along a cartesian axis.
right = 1 + int(np.ceil(max(x1, x2) + r_radius))
bottom = 1 + int(np.ceil(max(y1, y2) + r_radius))
mask = np.zeros((z + 1, right, bottom), dtype=bool)

# Generate the ellipse
xx, yy = ellipse(
x_mid,
y_mid,
r_radius,
c_radius,
mask.shape[1:],
rotation=orientation,
)
# Trim mask down to only the points needed
mask = mask[:, :max(xx) + 1, :max(yy) + 1]
# Convert sparse ellipse representation to mask
mask[z, xx, yy] = True
return {'mask': mask}
else:
# Freehand
coords = _getcoords(z)
coords = coords.astype('float')
return {'polygons': coords}

elif roi_type == 10:
raise ValueError("read_imagej_roi: point/mulipoint types are not supported")

else:
try:
coords = _getcoords(z)
Expand Down Expand Up @@ -258,51 +293,89 @@ def _parse_roi_file_py3(roi_source):
roi = roi[keys[0]]

# Convert the roi dictionary into either polygon or a mask
if roi['type'] in ('polygon', 'freehand'):
# Polygon
if 'x' in roi and 'y' in roi and 'n' in roi:
# ROI types "freehand", "freeline", "multipoint", "point", "polygon",
# "polyline", and "trace" are loaded and returned as a set of polygon
# co-ordinates.
coords = np.empty((roi['n'], 3), dtype=np.float)
coords[:, 0] = roi['x']
coords[:, 1] = roi['y']
coords[:, 2] = 0
coords[:, 2] = roi.get('z', 0)
return {'polygons': coords}

width = roi['width']
height = roi['height']
left = roi['left']
top = roi['top']
right = left + width
bottom = top - height
z = 0
if 'width' in roi and 'height' in roi and 'left' in roi and 'top' in roi:
width = roi['width']
height = roi['height']
left = roi['left']
top = roi['top']
right = left + width
bottom = top + height
z = roi.get('z', 0)

if roi['type'] == 'rectangle':
# Rectangle
# Rectangle is converted into polygon co-ordinates
coords = [[left, top, z], [right, top, z], [right, bottom, z],
[left, bottom, z]]
coords = np.array(coords).astype('float')
return {'polygons': coords}

elif roi['type'] == 'oval':
# Oval
# 0.5 moves the mid point to the center of the pixel
x_mid = (right + left) / 2.0 - 0.5
y_mid = (top + bottom) / 2.0 - 0.5
mask = np.zeros((z + 1, right, bottom), dtype=bool)
for y, x in product(np.arange(top, bottom), np.arange(left, right)):
mask[z, x, y] = ((x - x_mid) ** 2 / (width / 2.0) ** 2 +
(y - y_mid) ** 2 / (height / 2.0) ** 2 <= 1)

# We subtract 0.5 because ImageJ's co-ordinate system has indices at
# the pixel boundaries, and we are using indices at the pixel centers.
x_mid = left + width / 2. - 0.5
y_mid = top + height / 2. - 0.5

# Work out whether each pixel is inside the oval. We only need to check
# pixels within the extent of the oval.
xx = np.arange(left, right)
yy = np.arange(top, bottom)
xx = ((xx - x_mid) / (width / 2.)) ** 2
yy = ((yy - y_mid) / (height / 2.)) ** 2
dd = np.expand_dims(xx, 1) + np.expand_dims(yy, 0)
mask[z, left:, top:] = dd <= 1
return {'mask': mask}

elif roi['type'] == 'ellipse':
# ellipse
mask = np.zeros((1, right+10, bottom+10), dtype=bool)
r_radius = np.sqrt((x2-x1)**2+(y2-y1)**2)/2.0
c_radius = r_radius*aspect_ratio
r = (x1+x2)/2-0.5
c = (y1+y2)/2-0.5
shpe = mask.shape
orientation = np.arctan2(y2-y1, x2-x1)
X, Y = ellipse(r, c, r_radius, c_radius, shpe[1:], orientation)
mask[0, X, Y] = True
elif roi['type'] == 'freehand' and 'aspect_ratio' in roi and 'ex1' in roi:
# Ellipse
# Co-ordinates of points at either end of major axis
x1 = roi['ex1']
y1 = roi['ey1']
x2 = roi['ex2']
y2 = roi['ey2']

# Radius of major and minor axes
r_radius = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / 2.
c_radius = r_radius * roi['aspect_ratio']
# Centre coordinates
# We subtract 0.5 because ImageJ's co-ordinate system has indices at
# the pixel boundaries, and we are using indices at the pixel centers.
x_mid = (x1 + x2) / 2. - 0.5
y_mid = (y1 + y2) / 2. - 0.5
orientation = np.arctan2(y2 - y1, x2 - x1)

# We need to make a mask which is a bit bigger than this, because
# we don't know how big the ellipse will end up being. In the most
# extreme case, it is a circle aligned along a cartesian axis.
right = 1 + int(np.ceil(max(x1, x2) + r_radius))
bottom = 1 + int(np.ceil(max(y1, y2) + r_radius))
mask = np.zeros((z + 1, right, bottom), dtype=bool)

# Generate the ellipse
xx, yy = ellipse(
x_mid,
y_mid,
r_radius,
c_radius,
mask.shape[1:],
rotation=orientation,
)
# Trim mask down to only the points needed
mask = mask[:, :max(xx) + 1, :max(yy) + 1]
# Convert sparse ellipse representation to mask
mask[z, xx, yy] = True
return {'mask': mask}

else:
Expand Down
8 changes: 8 additions & 0 deletions fissa/tests/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def assert_array_equal(self, *args, **kwargs):
return assert_array_equal(*args, **kwargs)

def assert_allclose(self, *args, **kwargs):
# Handle msg argument, which is passed from assertEqual, established
# with addTypeEqualityFunc in __init__
msg = kwargs.pop('msg', None)
return assert_allclose(*args, **kwargs)

def assert_equal(self, *args, **kwargs):
Expand All @@ -64,3 +67,8 @@ def assert_equal_list_of_array_perm_inv(self, desired, actual):
if np.equal(actual_j, desired_i).all():
n_matches += 1
self.assertTrue(n_matches >= 0)

def assert_equal_dict_of_array(self, desired, actual):
self.assertEqual(desired.keys(), actual.keys())
for k in desired.keys():
self.assertEqual(desired[k], actual[k])
Binary file added fissa/tests/resources/rois/all.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/all.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/brush.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/brush.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/composite-rectangle.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/composite-rectangle.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/ellipse.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/ellipse.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/freehand.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/freehand.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/freeline.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/freeline.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/multipoint.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/multipoint.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/oval-full.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/oval-full.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/oval.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/oval.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/polygon-spline.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/polygon-spline.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/polygon.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/polygon.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/polyline.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/polyline.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/rectangle-rotated.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/rectangle-rotated.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/rectangle-rounded.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/rectangle-rounded.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/rectangle.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/rectangle.roi
Binary file not shown.
Binary file added fissa/tests/resources/rois/wand.npy
Binary file not shown.
Binary file added fissa/tests/resources/rois/wand.roi
Binary file not shown.
76 changes: 37 additions & 39 deletions fissa/tests/test_neuropil.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,54 +18,52 @@ def setup_class(self):
self.l = 100
# setup basic x values for fake data
self.x = np.linspace(0, np.pi * 2, self.l)
# Generate simple source data
self.data = np.array([np.sin(self.x), np.cos(3 * self.x)]) + 1
# Mix to make test data
self.W = np.array([[0.2, 0.8], [0.8, 0.2]])
self.S = np.dot(self.W, self.data)

def test_separate(self):
"""Tests the separate function data format."""
# TODO: Successfullness of methods are hard to test with unittests.
# Need to make a test case where answer is known, and test
# against that.

# desired shapes
shape_desired = (2, self.l)
con_desired_ica = {'converged': False,
'iterations': 1,
'max_iterations': 1,
'random_state': 892}
con_desired_nmf = {'converged': True,
'iterations': 0,
'max_iterations': 1,
'random_state': 892}

# setup fake data
data = np.array([np.sin(self.x), np.cos(3 * self.x)]) + 1

# mix fake data
W = np.array([[0.2, 0.8], [0.8, 0.2]])
S = np.dot(W, data)

# function for testing a method
def run_method(method):
"""Test a single method.
Parameters
----------
method : str
What method to test: 'nmf', 'ica', or 'nmf_sklearn')
"""
# unmix data with ica
def run_method(method, expected_converged=None, **kwargs):
"""Test a single method, with specific parameters."""
# Run the separation routine
S_sep, S_matched, A_sep, convergence = npil.separate(
S, sep_method=method, n=2, maxiter=1, maxtries=1)

# assert if formats are good
self.S, sep_method=method, **kwargs
)
# Ensure output shapes are as expected
self.assert_equal(S_sep.shape, shape_desired)
self.assert_equal(S_matched.shape, shape_desired)
self.assert_equal(S_sep.shape, shape_desired)
if method == 'ica':
self.assert_equal(convergence, con_desired_ica)
elif method == 'nmf':
self.assert_equal(convergence, con_desired_nmf)
# If specified, assert that the result is as expected
if expected_converged is not None:
self.assert_equal(convergence['converged'], expected_converged)

# Run tests
i_subtest = 0
for method in ['nmf', 'ica']:
with self.subTest(i_subtest):
run_method(method, expected_converged=True, maxtries=1, n=2)
i_subtest += 1
with self.subTest(i_subtest):
run_method(method, expected_converged=True, maxtries=1)
i_subtest += 1
with self.subTest(i_subtest):
run_method(method, expected_converged=True, maxtries=1, random_state=0)
i_subtest += 1
with self.subTest(i_subtest):
run_method(method, maxiter=1, maxtries=3)
i_subtest += 1

with self.subTest(i_subtest):
run_method('nmf', expected_converged=True, alpha=.2)
i_subtest += 1

# test all two methods
run_method('nmf')
run_method('ica')
def test_badmethod(self):
with self.assertRaises(ValueError):
npil.separate(self.S, sep_method='bogus_method')
Loading

0 comments on commit 6efdd57

Please sign in to comment.