Skip to content

Commit ff50d1c

Browse files
committed
code and tests
1 parent 7e8c5cb commit ff50d1c

File tree

10 files changed

+252
-40
lines changed

10 files changed

+252
-40
lines changed

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
> python module for testing neuron finding algorithms.
44
5-
This repository contains a module and a CLI for working with neuron finding algorithms. It is used by the [neurofinder](https://github.com/neurofinder) benchmarking challenge to compare ground truth results to results from submitted algorithms.
5+
This repository contains a module and a CLI for working with neuron finding algorithm results. It is used by the [neurofinder](https://github.com/neurofinder) benchmarking challenge to compare ground truth results to results from submitted algorithms.
6+
7+
Assumes a standard format for spatial regions, in either `JSON` or `MAT`.
8+
9+
The `JSON` format is:
10+
11+
And the `MAT` format is:
612

713
## install
814

@@ -26,12 +32,20 @@ You can also pass `MAT` files as one or both arguments
2632
neurofinder evaluate neurons1.mat neurons2.mat
2733
```
2834

29-
## use as a python module
35+
## methods
3036

31-
Import the module and pass it two dictionaries
37+
#### `neurofinder.load(file)`
3238

33-
```
34-
import neurofinder
39+
Load regions from either a `JSON` or `MAT` file.
40+
41+
#### `neurofinder.match(a, b, unique=True, min_distance=inf)`
42+
43+
Match regions from `a` to `b` based on distances between their centers. Returns a list of indicies specifying, for each region in `a`, what the index of the matching region in `b` is. If `unique` is true, will ensure uniqueness of matches. If `min_distance` is less than `inf`, will not allow matches that exceed this distance.
44+
45+
#### `neurofinder.centers(a, b, threshold=5)`
46+
47+
Compare centers between two sets of regions `a` and `b`. Returns two metrics, the `recall` and `precision`, which are defined as the total number of matching regions, according to the given distance `threshold`, dividing by the number of regions in `a`, or `b`, respectively.
48+
49+
#### `neurofinder.shapes(a, b, min_distance=inf)`
3550

36-
results = neurofinder.evaluate(neurons1, neurons2)
37-
```
51+
Compare shapes between regions in `a` and `b` after first finding matches. For each pair of matched regions, the `overlap` and `exactness` are computed as the number of intersecting pixels divided by the number of pixels in the first, or second, region, respectively.

a.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
{"coordinates": [[0, 0], [0, 1], [1, 0], [1, 1]]},
3+
{"coordinates": [[10, 10], [10, 11], [11, 10], [11, 11]]},
4+
{"coordinates": [[20, 20], [20, 21], [21, 20], [21, 21]]},
5+
{"coordinates": [[30, 30], [30, 31], [31, 30], [31, 31]]}
6+
]

b.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[
2+
{"coordinates": [[0, 0], [0, 1], [1, 2], [1, 0], [1, 1]]},
3+
{"coordinates": [[10, 10], [11, 10], [11, 11]]},
4+
{"coordinates": [[30, 30], [30, 31], [32, 30], [31, 31]]}
5+
]

example.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import json
2+
from neurofinder import load, match, similarity, overlap
3+
4+
a = load('a.json')
5+
b = load('b.json')
6+
7+
#print(match(a, b, min_distance=5))
8+
print(similarity(a, b))
9+
10+
# recall, precision = similarity(a, b, metric='distance', minDistance=threshold)
11+
# stats = overlap(a, b, method='rates', minDistance=threshold)
12+
13+
# score = 2 * (recall * precision) / (recall + precision)
14+
15+
# if sum(~isnan(stats)) > 0:
16+
# overlap, exactness = tuple(nanmean(stats, axis=0))
17+
# else:
18+
# overlap, exactness = 0.0, 1.0

neurofinder/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
from main import load, match, centers, shapes
2+
13
__version__='1.0.0'

neurofinder/cli.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
@click.group(options_metavar='', subcommand_metavar='<command>', context_settings=settings)
77
def cli():
88
"""
9-
Hi! This is a command line tool for comparing neuron finding algorithms.
9+
Hi! This is a tool for working with neuron finding algorithm results.
10+
11+
Check out the list of commands to see what you can do.
1012
"""
11-
print 'hi'
1213

1314
cli.add_command(evaluate)

neurofinder/commands/evaluate.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import os
22
import click
3+
from .. import load, centers, shapes
34

5+
@click.argument('file1', nargs=1, metavar='<file1>', required=True)
6+
@click.argument('file2', nargs=1, metavar='<file2>', required=True)
47
@click.command('evaluate', short_help='compare results of two algorithms', options_metavar='<options>')
5-
def evaluate():
6-
print('evaluating algorithms')
8+
def evaluate(file1, file2):
9+
a = load(file1)
10+
b = load(file2)
11+
precision, recall = centers(a, b)
12+
overlap, exactness = shapes(a, b)
13+
average = 2 * (recall * precision) / (recall + precision)
14+
15+
result = {'average': average, 'overlap': overlap, 'precision': precision, 'recall': recall, 'exactness': exactness}
16+
print(result)

neurofinder/main.py

Lines changed: 124 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,133 @@
1-
import os
1+
import json
2+
from numpy import inf, NaN, newaxis, argmin, delete, asarray, isnan, sum, nanmean
3+
from scipy.spatial.distance import cdist
4+
from regional import one, many
25

3-
def init(force):
6+
def load(file):
47
"""
5-
This command initializes a folder with the typical contents of a Python package.
6-
After running this and writing your code, you should be ready to publish your package.
8+
Load neuronal regions from a file.
79
"""
8-
echo('\nThis utility will help you set up a new python module for publishing on PyPi!\n')
9-
echo('After answering a few questions, it will create a few files.')
10-
echo('\nPress ^C at any time to bail!\n')
10+
with open(file, 'r') as f:
11+
values = json.load(f)
12+
return many([v['coordinates'] for v in values])
13+
14+
def match(a, b, unique=True, min_distance=inf):
15+
"""
16+
Find matches between two sets of regions.
17+
18+
Can select nearest matches with or without enforcing uniqueness;
19+
if unique is False, will return the closest source in other for
20+
each source in self, possibly repeating sources multiple times
21+
if unique is True, will only allow each source in other to be matched
22+
with a single source in self, as determined by a greedy selection procedure.
23+
The min_distance parameter can be used to prevent far-away sources from being
24+
chosen during greedy selection.
25+
26+
Params
27+
------
28+
a, b : regions
29+
The regions to match.
30+
31+
unique : boolean, optional, deafult = True
32+
Whether to only return unique matches.
33+
34+
min_distance : scalar, optiona, default = inf
35+
Minimum distance to use when selecting matches.
36+
"""
37+
targets = b.center
38+
target_inds = range(0, len(targets))
39+
matches = []
40+
for s in a:
41+
update = 1
1142

12-
remap = {
13-
'entry': 'entry point',
14-
'package': 'package name'
15-
}
16-
d = _defaults()
17-
for k, v in d.items():
18-
d[k] = prompt.query('%s:' % remap.get(k, k), default=v)
43+
# skip if no targets left, otherwise update
44+
if len(targets) == 0:
45+
update = 0
46+
else:
47+
dists = cdist(targets, s.center[newaxis])
48+
if dists.min() < min_distance:
49+
ind = argmin(dists)
50+
else:
51+
update = 0
52+
53+
# apply updates, otherwise add a nan
54+
if update == 1:
55+
matches.append(target_inds[ind])
56+
if unique is True:
57+
targets = delete(targets, ind, axis=0)
58+
target_inds = delete(target_inds, ind)
59+
else:
60+
matches.append(NaN)
61+
62+
return matches
63+
64+
def shapes(a, b, min_distance=inf):
65+
"""
66+
Compare shapes between two sets of regions.
67+
68+
Parameters
69+
----------
70+
a, b : regions
71+
The regions for which to estimate overlap.
72+
73+
min_distance : scalar, optional, default = inf
74+
Minimum distance to use when matching indices.
75+
"""
76+
inds = match(a, b, unique=True, min_distance=min_distance)
77+
d = []
78+
for jj, ii in enumerate(inds):
79+
if ii is not NaN:
80+
d.append(a[jj].overlap(b[ii], method='rates'))
81+
else:
82+
d.append((NaN, NaN))
83+
84+
result = asarray(d)
85+
86+
if sum(~isnan(result)) > 0:
87+
overlap, exactness = tuple(nanmean(result, axis=0))
88+
else:
89+
overlap, exactness = 0.0, 1.0
90+
91+
return overlap, exactness
92+
93+
def centers(a, b, threshold=5):
94+
"""
95+
Compare centers between two sets of regions.
96+
97+
The recall rate is the number of matches divided by the number in self,
98+
and the precision rate is the number of matches divided by the number in other.
99+
Typically a is ground truth and b is an estimate.
100+
The F score is defined as 2 * (recall * precision) / (recall + precision)
101+
102+
Before computing metrics, all sources in self are matched to other,
103+
and a minimum distance can be set to control matching.
104+
105+
Parameters
106+
----------
107+
a, b : regions
108+
The regions for which to estimate overlap.
109+
110+
threshold : scalar, optional, default = 5
111+
The distance below which a source is considered found.
112+
113+
min_distance : scalar, optional, default = inf
114+
Minimum distance to use when matching indices.
115+
"""
116+
inds = match(a, b, unique=True, min_distance=threshold)
19117

20-
echo('\nReady to create the following files:')
118+
d = []
119+
for jj, ii in enumerate(inds):
120+
if ii is not NaN:
121+
d.append(a[jj].distance(b[ii]))
122+
else:
123+
d.append(NaN)
21124

22-
with indent(4, quote=' -'):
23-
puts('setup.py')
24-
puts('setup.cfg')
25-
puts('MANIFEST.in')
26-
puts(d['package'] + '/' + '__init__.py')
27-
puts(d['package'] + '/' + d['entry'])
28-
puts('requirements.txt')
125+
result = asarray(d)
29126

30-
finalize = prompt.yn('\nSound like a plan?', default='y')
127+
result[isnan(result)] = inf
128+
compare = lambda x: x < threshold
31129

32-
if finalize:
33-
echo('')
34-
_make_package(d, force)
35-
echo('')
130+
recall = sum(map(compare, result)) / float(a.count)
131+
precision = sum(map(compare, result)) / float(b.count)
36132

37-
success('Your package is initialized!')
133+
return recall, precision

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
click
1+
click
2+
regional

tests/test_main.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from numpy import allclose, nan
2+
from regional import many
3+
from neurofinder import match, shapes, centers
4+
5+
6+
def test_match():
7+
a = many([[[0, 0], [0, 1], [1, 0], [1, 1]], [[10, 10], [10, 11], [11, 10], [11, 11]]])
8+
b = many([[[0, 0], [0, 1], [1, 0], [1, 1]], [[30, 30], [31, 30], [31, 31]]])
9+
assert match(a, b) == [0, 1]
10+
assert match(a, b, min_distance=5) == [0, nan]
11+
12+
13+
def test_match_flipped():
14+
a = many([[[0, 0], [0, 1], [1, 0], [1, 1]], [[10, 10], [10, 11], [11, 10], [11, 11]]])
15+
b = many([[[30, 30], [31, 30], [31, 31]], [[0, 0], [0, 1], [1, 0], [1, 1]]])
16+
assert match(a, b) == [1, 0]
17+
assert match(a, b, min_distance=5) == [1, nan]
18+
19+
20+
def test_similarity():
21+
a = many([[[0, 0], [0, 1], [1, 0], [1, 1]], [[10, 10], [10, 11], [11, 10], [11, 11]]])
22+
b = many([[[0, 0], [0, 1], [1, 0], [1, 1]], [[30, 30], [31, 30], [31, 31]]])
23+
assert centers(a, b) == (0.5, 0.5)
24+
25+
26+
def test_similarity_perfect():
27+
a = many([[[0, 0], [0, 1], [1, 0], [1, 1]], [[10, 10], [10, 11], [11, 10], [11, 11]]])
28+
b = many([[[0, 0], [0, 1], [1, 0], [1, 1]], [[10, 10], [10, 11], [11, 10], [11, 11]]])
29+
assert centers(a, b) == (1.0, 1.0)
30+
31+
32+
def test_similarity_perfect_flipped():
33+
a = many([[[0, 0], [0, 1], [1, 0], [1, 1]], [[10, 10], [10, 11], [11, 10], [11, 11]]])
34+
b = many([[[10, 10], [10, 11], [11, 10], [11, 11]], [[0, 0], [0, 1], [1, 0], [1, 1]]])
35+
assert centers(a, b) == (1.0, 1.0)
36+
37+
38+
def test_overlap_too_few():
39+
a = many([[[0, 0], [0, 1], [1, 0], [1, 1]], [[10, 10], [10, 11], [11, 10], [11, 11]]])
40+
b = many([[[0, 0], [0, 1], [1, 0], [1, 1]], [[10, 10], [11, 11]]])
41+
assert shapes(a, b) == (0.75, 1.0)
42+
43+
44+
def test_overlap_too_many():
45+
a = many([[[0, 0], [0, 1]], [[10, 10], [10, 11]]])
46+
b = many([[[0, 0], [0, 1]], [[10, 10], [10, 11], [11, 10], [11, 12]]])
47+
assert shapes(a, b) == (1.0, 0.75)
48+
49+
50+
def test_overlap_perfect():
51+
a = many([[[0, 0], [0, 1]], [[10, 10], [10, 11]]])
52+
b = many([[[0, 0], [0, 1]], [[10, 10], [10, 11]]])
53+
assert shapes(a, b) == (1.0, 1.0)
54+
55+
56+
def test_overlap_perfect_flipped():
57+
a = many([[[0, 0], [0, 1]], [[10, 10], [10, 11]]])
58+
b = many([[[10, 10], [10, 11]], [[0, 0], [0, 1]]])
59+
assert shapes(a, b) == (1.0, 1.0)

0 commit comments

Comments
 (0)