mirror of
https://github.com/wassname/scikit-image.git
synced 2026-07-20 12:40:31 +08:00
Merge pull request #1 from stefanv/find-contours
Convert contour finding code to Cython and add an example.
This commit is contained in:
+1
-1
@@ -28,7 +28,7 @@
|
||||
Code to generate skimage logo.
|
||||
|
||||
- Zachary Pincus
|
||||
Tracing of low cost paths, FreeImage I/O plugin
|
||||
Tracing of low cost paths, FreeImage I/O plugin, iso-contours
|
||||
|
||||
- Almar Klein
|
||||
Binary heap class for graph algorithms
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
===============
|
||||
Contour finding
|
||||
===============
|
||||
|
||||
``skimage.measure.find_contours`` uses a marching squares method to find
|
||||
constant valued contours in an image. Array values are linearly interpolated
|
||||
to provide better precision of the output contours. Contours which intersect
|
||||
the image edge are open; all others are closed.
|
||||
|
||||
The `marching squares algorithm
|
||||
<http://www.essi.fr/~lingrand/MarchingCubes/algo.html>`__ is a special case of
|
||||
the marching cubes algorithm (Lorensen, William and Harvey E. Cline. Marching
|
||||
Cubes: A High Resolution 3D Surface Construction Algorithm. Computer Graphics
|
||||
(SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170).
|
||||
|
||||
"""
|
||||
|
||||
from skimage import data
|
||||
from skimage import measure
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Construct some test data
|
||||
x, y = np.ogrid[-np.pi:np.pi:100j, -np.pi:np.pi:100j]
|
||||
r = np.sin(np.exp((np.sin(x)**3 + np.cos(y)**2)))
|
||||
|
||||
# Find contours at a constant value of 0.8
|
||||
contours = measure.find_contours(r, 0.8)
|
||||
|
||||
# Display the image and plot all contours found
|
||||
plt.imshow(r, interpolation='nearest')
|
||||
|
||||
for n, contour in enumerate(contours):
|
||||
plt.plot(contour[:, 1], contour[:, 0], linewidth=2)
|
||||
|
||||
plt.axis('image')
|
||||
plt.xticks([])
|
||||
plt.yticks([])
|
||||
plt.show()
|
||||
|
||||
@@ -8,7 +8,7 @@ from glob import glob
|
||||
import os.path
|
||||
|
||||
import numpy as np
|
||||
from io import imread
|
||||
from ._io import imread
|
||||
|
||||
|
||||
class MultiImage(object):
|
||||
|
||||
@@ -1 +1 @@
|
||||
from find_contours import find_contours
|
||||
from find_contours import find_contours
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
#include <Python.h>
|
||||
#include "numpy/arrayobject.h"
|
||||
|
||||
static char _find_contours_doc[] =
|
||||
"This module defines C helper functions for find_contours";
|
||||
|
||||
|
||||
static char iterate_and_store_doc[] =
|
||||
"iterate_and_store(array, level, vertex_connect_high)\n\
|
||||
\n\
|
||||
Iterate across the given array in a marching-squares fashion, looking for\n\
|
||||
segments that cross 'level'. If such a segment is found, its coordinates are\n\
|
||||
added to a growing list of segments, which is returned by the function.\n\
|
||||
if vertex_connect_high is nonzero, high-values pixels are considered to be\n\
|
||||
face+vertex connected into objects; otherwise low-valued pixels are.";
|
||||
|
||||
|
||||
// Nasty macros to inline the inner loop of interpolating the position of
|
||||
// the contour and add an appropriate tuple to the output list.
|
||||
// These macros define blocks of code that can only be used within the inner
|
||||
// loop of 'iterate_and_store' because they use variables therefrom.
|
||||
|
||||
#define GET_FRACTION(from_value, to_value) \
|
||||
((level - from_value) / (to_value - from_value))
|
||||
|
||||
#define TOP { \
|
||||
output0 = coords[0]; \
|
||||
output1 = coords[1] + GET_FRACTION(*ul_ptr, *ur_ptr); \
|
||||
}
|
||||
|
||||
#define BOTTOM { \
|
||||
output0 = coords[0] + 1; \
|
||||
output1 = coords[1] + GET_FRACTION(*ll_ptr, *lr_ptr); \
|
||||
}
|
||||
|
||||
#define LEFT { \
|
||||
output0 = coords[0] + GET_FRACTION(*ul_ptr, *ll_ptr); \
|
||||
output1 = coords[1]; \
|
||||
}
|
||||
|
||||
#define RIGHT { \
|
||||
output0 = coords[0] + GET_FRACTION(*ur_ptr, *lr_ptr); \
|
||||
output1 = coords[1] + 1; \
|
||||
}
|
||||
|
||||
#define ADD_TUPLE { \
|
||||
PyObject* tuple = Py_BuildValue("(dd)", output0, output1); \
|
||||
if (!tuple) { \
|
||||
Py_DECREF(double_array); \
|
||||
Py_DECREF(arc_list); \
|
||||
return NULL; \
|
||||
} \
|
||||
char res = PyList_Append(arc_list, tuple); \
|
||||
Py_DECREF(tuple); \
|
||||
if (res < 0) { \
|
||||
Py_DECREF(double_array); \
|
||||
Py_DECREF(arc_list); \
|
||||
return NULL; \
|
||||
} \
|
||||
}
|
||||
|
||||
#define ADD_SEGMENT(START, END) { \
|
||||
double output0, output1; \
|
||||
START \
|
||||
ADD_TUPLE \
|
||||
END \
|
||||
ADD_TUPLE \
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
iterate_and_store(PyObject *self, PyObject *args)
|
||||
{
|
||||
PyObject* array;
|
||||
double level;
|
||||
int vertex_connect_high;
|
||||
if (!PyArg_ParseTuple(args, "Odi:iterate_and_store", &array, &level,
|
||||
&vertex_connect_high)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* double_array = PyArray_FromAny(array,
|
||||
PyArray_DescrFromType(NPY_DOUBLE), 2, 2, NPY_CONTIGUOUS | NPY_ALIGNED,
|
||||
NULL);
|
||||
if (!double_array) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
npy_intp *dims = PyArray_DIMS(double_array);
|
||||
if (dims[0] < 2 || dims[1] < 2) {
|
||||
Py_DECREF(double_array);
|
||||
PyErr_SetString(PyExc_ValueError, "Input array must be at least 2x2.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// The plan is to iterate a 2x2 square across the input array. This means
|
||||
// that the upper-left corner of the square needs to iterate across a
|
||||
// sub-array that's one-less-large in each direction (so that the square
|
||||
// never steps out of bounds). The square is represented by four pointers:
|
||||
// ul, ur, ll, and lr (for 'upper left', etc.). We also maintain the current
|
||||
// 2D coordinates for the position of the upper-left pointer. Note that we
|
||||
// ensured that the array is of type 'double' and is C-contiguous (last
|
||||
// index varies the fastest).
|
||||
|
||||
// Current coords start at 0,0.
|
||||
npy_intp coords[2] = {0,0};
|
||||
// Precompute the size of the array minus 2 in each direction, so we'll know
|
||||
// when to update the coordinates and double-increment the square pointers
|
||||
// so that the upper-left pointer never visits the last column.
|
||||
npy_intp dims_m2[2];
|
||||
dims_m2[0] = dims[0] - 2;
|
||||
dims_m2[1] = dims[1] - 2;
|
||||
// Calculate the number of iterations we'll need
|
||||
npy_intp num_square_steps = (dims[0] - 1) * (dims[1] - 1);
|
||||
// and set up the square pointers.
|
||||
double* ul_ptr = PyArray_DATA(double_array);
|
||||
double* ur_ptr = ul_ptr + 1;
|
||||
double* ll_ptr = ul_ptr + dims[1];
|
||||
double* lr_ptr = ll_ptr + 1;
|
||||
|
||||
// make a list to hold the returned coordinates
|
||||
PyObject* arc_list = PyList_New(0);
|
||||
if (!arc_list) {
|
||||
Py_DECREF(double_array);
|
||||
return NULL;
|
||||
}
|
||||
while(num_square_steps--) {
|
||||
// There are sixteen different possible square types, diagramed below.
|
||||
// A + indicates that the vertex is above the contour value, and a -
|
||||
// indicates that the vertex is below or equal to the contour value.
|
||||
// The vertices of each square are:
|
||||
// ul ur
|
||||
// ll lr
|
||||
// and can be treated as a binary value with the bits in that order. Thus
|
||||
// each square case can be numbered:
|
||||
// 0-- 1+- 2-+ 3++ 4-- 5+- 6-+ 7++
|
||||
// -- -- -- -- +- +- +- +-
|
||||
//
|
||||
// 8-- 9+- 10-+ 11++ 12-- 13+- 14-+ 15++
|
||||
// -+ -+ -+ -+ ++ ++ ++ ++
|
||||
//
|
||||
// The position of the line segment that cuts through (or doesn't, in case
|
||||
// 0 and 15) each square is clear, except in cases 6 and 9. In this case,
|
||||
// where the segments are placed is determined by vertex_connect_high.
|
||||
// If vertex_connect_high is false, then lines like \\ are drawn
|
||||
// through square 6, and lines like // are drawn through square 9.
|
||||
// Otherwise, the situation is reversed.
|
||||
// Finally, recall that we draw the lines so that (moving from tail to
|
||||
// head) the lower-valued pixels are on the left of the line. So, for
|
||||
// example, case 1 entails a line slanting from the middle of the top of
|
||||
// the square to the middle of the left side of the square.
|
||||
|
||||
unsigned char square_case = 0;
|
||||
if ((*ul_ptr) > level) square_case += 1;
|
||||
if ((*ur_ptr) > level) square_case += 2;
|
||||
if ((*ll_ptr) > level) square_case += 4;
|
||||
if ((*lr_ptr) > level) square_case += 8;
|
||||
|
||||
switch(square_case)
|
||||
{
|
||||
case 0: // no line
|
||||
break;
|
||||
case 1: // top to left
|
||||
ADD_SEGMENT(TOP, LEFT);
|
||||
break;
|
||||
case 2: // right to top
|
||||
ADD_SEGMENT(RIGHT, TOP);
|
||||
break;
|
||||
case 3: // right to left
|
||||
ADD_SEGMENT(RIGHT, LEFT);
|
||||
break;
|
||||
case 4: // left to bottom
|
||||
ADD_SEGMENT(LEFT, BOTTOM);
|
||||
break;
|
||||
case 5: // top to bottom
|
||||
ADD_SEGMENT(TOP, BOTTOM);
|
||||
break;
|
||||
case 6:
|
||||
if (vertex_connect_high)
|
||||
{
|
||||
// left to top
|
||||
ADD_SEGMENT(LEFT, TOP);
|
||||
// right to bottom
|
||||
ADD_SEGMENT(RIGHT, BOTTOM);
|
||||
}
|
||||
else
|
||||
{
|
||||
// right to top
|
||||
ADD_SEGMENT(RIGHT, TOP);
|
||||
// left to bottom
|
||||
ADD_SEGMENT(LEFT, BOTTOM);
|
||||
}
|
||||
break;
|
||||
case 7: // right to bottom
|
||||
ADD_SEGMENT(RIGHT, BOTTOM);
|
||||
break;
|
||||
case 8: // bottom to right
|
||||
ADD_SEGMENT(BOTTOM, RIGHT);
|
||||
break;
|
||||
case 9:
|
||||
if (vertex_connect_high)
|
||||
{
|
||||
// top to right
|
||||
ADD_SEGMENT(TOP, RIGHT);
|
||||
// bottom to left
|
||||
ADD_SEGMENT(BOTTOM, LEFT);
|
||||
}
|
||||
else
|
||||
{
|
||||
// top to left
|
||||
ADD_SEGMENT(TOP, LEFT);
|
||||
// bottom to right
|
||||
ADD_SEGMENT(BOTTOM, RIGHT);
|
||||
}
|
||||
break;
|
||||
case 10: // bottom to top
|
||||
ADD_SEGMENT(BOTTOM, TOP);
|
||||
break;
|
||||
case 11: // bottom to left
|
||||
ADD_SEGMENT(BOTTOM, LEFT);
|
||||
break;
|
||||
case 12: // left to right
|
||||
ADD_SEGMENT(LEFT, RIGHT);
|
||||
break;
|
||||
case 13: // top to right
|
||||
ADD_SEGMENT(TOP, RIGHT);
|
||||
break;
|
||||
case 14: // left to top
|
||||
ADD_SEGMENT(LEFT, TOP);
|
||||
break;
|
||||
case 15: // no line
|
||||
break;
|
||||
} // switch square_case
|
||||
|
||||
if (coords[1] < dims_m2[1]) {
|
||||
coords[1]++;
|
||||
} else {
|
||||
coords[1] = 0;
|
||||
coords[0]++;
|
||||
// Double-increment pointers to advance them to the next row, since
|
||||
// we're skipping the last column, as far as ul_ptr is concerned.
|
||||
ul_ptr++; ur_ptr++; ll_ptr++; lr_ptr++;
|
||||
}
|
||||
ul_ptr++; ur_ptr++; ll_ptr++; lr_ptr++;
|
||||
} // iteration
|
||||
|
||||
// get rid of the double array reference that we own
|
||||
Py_DECREF(double_array);
|
||||
return arc_list;
|
||||
}
|
||||
|
||||
|
||||
static PyMethodDef _find_contours_methods[] = {
|
||||
{"iterate_and_store", iterate_and_store, METH_VARARGS, iterate_and_store_doc},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC
|
||||
init_find_contours(void)
|
||||
{
|
||||
Py_InitModule3("_find_contours", _find_contours_methods, _find_contours_doc);
|
||||
import_array();
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
# -*- python -*-
|
||||
# cython: cdivision=True
|
||||
|
||||
import numpy as np
|
||||
cimport numpy as np
|
||||
|
||||
np.import_array()
|
||||
|
||||
cdef double _get_fraction(double from_value, double to_value, double level):
|
||||
if (to_value == from_value):
|
||||
return 0
|
||||
return ((level - from_value) / (to_value - from_value))
|
||||
|
||||
|
||||
def iterate_and_store(np.ndarray[double, ndim=2, mode='c'] array,
|
||||
double level, int vertex_connect_high):
|
||||
"""Iterate across the given array in a marching-squares fashion,
|
||||
looking for segments that cross 'level'. If such a segment is
|
||||
found, its coordinates are added to a growing list of segments,
|
||||
which is returned by the function. if vertex_connect_high is
|
||||
nonzero, high-values pixels are considered to be face+vertex
|
||||
connected into objects; otherwise low-valued pixels are.
|
||||
|
||||
"""
|
||||
if array.shape[0] < 2 or array.shape[1] < 2:
|
||||
raise ValueError("Input array must be at least 2x2.")
|
||||
|
||||
cdef list arc_list = []
|
||||
cdef int n
|
||||
|
||||
# The plan is to iterate a 2x2 square across the input array. This means
|
||||
# that the upper-left corner of the square needs to iterate across a
|
||||
# sub-array that's one-less-large in each direction (so that the square
|
||||
# never steps out of bounds). The square is represented by four pointers:
|
||||
# ul, ur, ll, and lr (for 'upper left', etc.). We also maintain the current
|
||||
# 2D coordinates for the position of the upper-left pointer. Note that we
|
||||
# ensured that the array is of type 'double' and is C-contiguous (last
|
||||
# index varies the fastest).
|
||||
|
||||
# Current coords start at 0,0.
|
||||
cdef int[2] coords
|
||||
coords[0] = 0
|
||||
coords[1] = 0
|
||||
|
||||
# Calculate the number of iterations we'll need
|
||||
cdef int num_square_steps = (array.shape[0] - 1) * (array.shape[1] - 1)
|
||||
|
||||
cdef unsigned char square_case = 0
|
||||
cdef tuple top, bottom, left, right
|
||||
cdef double ul, ur, ll, lr
|
||||
cdef int r0, r1, c0, c1
|
||||
|
||||
for n in range(num_square_steps):
|
||||
# There are sixteen different possible square types, diagramed below.
|
||||
# A + indicates that the vertex is above the contour value, and a -
|
||||
# indicates that the vertex is below or equal to the contour value.
|
||||
# The vertices of each square are:
|
||||
# ul ur
|
||||
# ll lr
|
||||
# and can be treated as a binary value with the bits in that order. Thus
|
||||
# each square case can be numbered:
|
||||
# 0-- 1+- 2-+ 3++ 4-- 5+- 6-+ 7++
|
||||
# -- -- -- -- +- +- +- +-
|
||||
#
|
||||
# 8-- 9+- 10-+ 11++ 12-- 13+- 14-+ 15++
|
||||
# -+ -+ -+ -+ ++ ++ ++ ++
|
||||
#
|
||||
# The position of the line segment that cuts through (or
|
||||
# doesn't, in case 0 and 15) each square is clear, except in
|
||||
# cases 6 and 9. In this case, where the segments are placed
|
||||
# is determined by vertex_connect_high. If
|
||||
# vertex_connect_high is false, then lines like \\ are drawn
|
||||
# through square 6, and lines like # are drawn through square
|
||||
# 9. Otherwise, the situation is reversed.
|
||||
# Finally, recall that we draw the lines so that (moving from tail to
|
||||
# head) the lower-valued pixels are on the left of the line. So, for
|
||||
# example, case 1 entails a line slanting from the middle of the top of
|
||||
# the square to the middle of the left side of the square.
|
||||
|
||||
r0, c0 = coords[0], coords[1]
|
||||
r1, c1 = r0 + 1, c0 + 1
|
||||
|
||||
ul = array[r0, c0]
|
||||
ur = array[r0, c0 + 1]
|
||||
ll = array[r0 + 1, c0]
|
||||
lr = array[r0 + 1, c0 + 1]
|
||||
|
||||
square_case = 0
|
||||
if (ul > level): square_case += 1
|
||||
if (ur > level): square_case += 2
|
||||
if (ll > level): square_case += 4
|
||||
if (lr > level): square_case += 8
|
||||
|
||||
top = coords[0], coords[1] + _get_fraction(ul, ur, level)
|
||||
bottom = coords[0] + 1, coords[1] + _get_fraction(ll, lr, level)
|
||||
left = coords[0] + _get_fraction(ul, ll, level), coords[1]
|
||||
right = coords[0] + _get_fraction(ur, lr, level), coords[1] + 1
|
||||
|
||||
if (square_case == 0):
|
||||
# no line
|
||||
pass
|
||||
elif (square_case == 1):
|
||||
# top to left
|
||||
arc_list.append(top)
|
||||
arc_list.append(left)
|
||||
elif (square_case == 2):
|
||||
# right to top
|
||||
arc_list.append(right)
|
||||
arc_list.append(top)
|
||||
elif (square_case == 3):
|
||||
# right to left
|
||||
arc_list.append(right)
|
||||
arc_list.append(left)
|
||||
elif (square_case == 4):
|
||||
# left to bottom
|
||||
arc_list.append(left)
|
||||
arc_list.append(bottom)
|
||||
elif (square_case == 5):
|
||||
# top to bottom
|
||||
arc_list.append(top)
|
||||
arc_list.append(bottom)
|
||||
elif (square_case == 6):
|
||||
if vertex_connect_high:
|
||||
arc_list.append(left)
|
||||
arc_list.append(top)
|
||||
|
||||
arc_list.append(right)
|
||||
arc_list.append(bottom)
|
||||
else:
|
||||
arc_list.append(right)
|
||||
arc_list.append(top)
|
||||
arc_list.append(left)
|
||||
arc_list.append(bottom)
|
||||
elif (square_case == 7):
|
||||
# right to bottom
|
||||
arc_list.append(right)
|
||||
arc_list.append(bottom)
|
||||
elif (square_case == 8):
|
||||
# bottom to right
|
||||
arc_list.append(bottom)
|
||||
arc_list.append(right)
|
||||
elif (square_case == 9):
|
||||
if vertex_connect_high:
|
||||
arc_list.append(top)
|
||||
arc_list.append(right)
|
||||
|
||||
arc_list.append(bottom)
|
||||
arc_list.append(left)
|
||||
else:
|
||||
arc_list.append(top)
|
||||
arc_list.append(left)
|
||||
|
||||
arc_list.append(bottom)
|
||||
arc_list.append(right)
|
||||
elif (square_case == 10):
|
||||
# bottom to top
|
||||
arc_list.append(bottom)
|
||||
arc_list.append(top)
|
||||
elif (square_case == 11):
|
||||
# bottom to left
|
||||
arc_list.append(bottom)
|
||||
arc_list.append(left)
|
||||
elif (square_case == 12):
|
||||
# lef to right
|
||||
arc_list.append(left)
|
||||
arc_list.append(right)
|
||||
elif (square_case == 13):
|
||||
# top to right
|
||||
arc_list.append(top)
|
||||
arc_list.append(right)
|
||||
elif (square_case == 14):
|
||||
# left to top
|
||||
arc_list.append(left)
|
||||
arc_list.append(top)
|
||||
elif (square_case == 15):
|
||||
# no line
|
||||
pass
|
||||
|
||||
if coords[1] < array.shape[1] - 2:
|
||||
coords[1] += 1
|
||||
else:
|
||||
coords[0] += 1
|
||||
coords[1] = 0
|
||||
|
||||
return arc_list
|
||||
@@ -5,44 +5,46 @@ from collections import deque
|
||||
|
||||
_param_options = ('high', 'low')
|
||||
|
||||
def find_contours(array, level, fully_connected='low', positive_orientation='low'):
|
||||
'''Find iso-valued contours in a 2D array for a given level value.
|
||||
|
||||
def find_contours(array, level,
|
||||
fully_connected='low', positive_orientation='low'):
|
||||
"""Find iso-valued contours in a 2D array for a given level value.
|
||||
|
||||
Uses the "marching squares" method to compute a the iso-valued contours of
|
||||
the input 2D array for a particular level value. Array values are linearly
|
||||
interpolated to provide better precision for the output contours.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
array : convertible to a 2D ndarray object
|
||||
Input data in which to find isocontours.
|
||||
level : float
|
||||
----------
|
||||
array : 2D ndarray of double
|
||||
Input data in which to find contours.
|
||||
level : float
|
||||
Value along which to find contours in the array.
|
||||
fully_connected : either 'low' or 'high'
|
||||
Indicates whether array elements below the given level value are to
|
||||
be considered fully- connected (and hence elements above the value
|
||||
will only be face connected), or vice-versa. (See below for details.)
|
||||
fully_connected : str, {'low', 'high'}
|
||||
Indicates whether array elements below the given level value are to be
|
||||
considered fully-connected (and hence elements above the value will
|
||||
only be face connected), or vice-versa. (See notes below for details.)
|
||||
positive_orientation : either 'low' or 'high'
|
||||
Indicates whether the output contours will produce
|
||||
positively-oriented polygons around islands of low- or high-valued
|
||||
elements. If 'low' then contours will wind counter- clockwise around
|
||||
elements below the iso-value. Alternately, this means that low-valued
|
||||
elements are always on the left of the contour. (See below for
|
||||
details.)
|
||||
|
||||
Indicates whether the output contours will produce positively-oriented
|
||||
polygons around islands of low- or high-valued elements. If 'low' then
|
||||
contours will wind counter- clockwise around elements below the
|
||||
iso-value. Alternately, this means that low-valued elements are always
|
||||
on the left of the contour. (See below for details.)
|
||||
|
||||
Returns
|
||||
-------
|
||||
A list of contours, where each contour is an ndarray of shape (n, 2)
|
||||
consisting of n (x,y) coordinates along the contour.
|
||||
|
||||
-------
|
||||
contours : list of (n,2)-ndarrays
|
||||
Each contour is an ndarray of shape ``(n, 2)``,
|
||||
consisting of n ``(x, y)`` coordinates along the contour.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The marching squares algorithm is a special case of the marching cubes
|
||||
algorithm (Lorensen, William and Harvey E. Cline. Marching Cubes: A High
|
||||
Resolution 3D Surface Construction Algorithm. Computer Graphics (SIGGRAPH
|
||||
87 Proceedings) 21(4) July 1987, p. 163-170). A simple explanation is
|
||||
available here: http://www.essi.fr/~lingrand/MarchingCubes/algo.html
|
||||
|
||||
algorithm [1]_. A simple explanation is available here::
|
||||
|
||||
http://www.essi.fr/~lingrand/MarchingCubes/algo.html
|
||||
|
||||
There is a single ambiguous case in the marching squares algorithm: when
|
||||
a given 2x2-element square has two high-valued and two low-valued
|
||||
a given ``2 x 2``-element square has two high-valued and two low-valued
|
||||
elements, each pair diagonally adjacent. (Where high- and low-valued is
|
||||
with respect to the contour value sought.) In this case, either the
|
||||
high-valued elements can be 'connected together' via a thin isthmus that
|
||||
@@ -50,45 +52,54 @@ def find_contours(array, level, fully_connected='low', positive_orientation='low
|
||||
connected together across a diagonal, they are considered 'fully
|
||||
connected' (also known as 'face+vertex-connected' or '8-connected'). Only
|
||||
high-valued or low-valued elements can be fully-connected, the other set
|
||||
will be considred as 'face-connected' or '4-connected'. By default,
|
||||
low-valued elements are considered fully-connected; this can be altered
|
||||
will be considered as 'face-connected' or '4-connected'. By default,
|
||||
low-valued elements are considered fully-connected; this can be altered
|
||||
with the 'fully_connected' parameter.
|
||||
|
||||
|
||||
Output contours are not guaranteed to be closed: contours which intersect
|
||||
the array edge will be left open. All other contours will be closed. (The
|
||||
closed-ness of a contours can be tested by checking whether the beginning
|
||||
point is the same as the end point.)
|
||||
|
||||
|
||||
Contours are oriented. By default, array values lower than the contour
|
||||
value are to the left of the contour and values greater than the contour
|
||||
value are to the right. This means that contours will wind
|
||||
counter-clockwise (i.e. in 'positive orientation') around islands of
|
||||
low-valued pixels. This behavior can be altered with the
|
||||
'positive_orientation' parameter.
|
||||
|
||||
|
||||
The order of the contours in the output list is determined by the position
|
||||
of the smallest x,y (in lexicographical order) coordinate in the contour.
|
||||
This is a side-effect of how the input array is traversed, but can be
|
||||
relied upon.
|
||||
|
||||
IMPORTANT NOTE ON COORDINATES AND VALUES:
|
||||
Array coordinates/values are assumed to refer to the _center_ of the
|
||||
array element. Take a simple example: [0, 1]. The interpolated position of
|
||||
0.5 in this array is midway between the 0-element (at x=0) and the
|
||||
1-element (at x=1), and thus would fall at x=0.5.
|
||||
|
||||
of the smallest ``x,y`` (in lexicographical order) coordinate in the
|
||||
contour. This is a side-effect of how the input array is traversed, but
|
||||
can be relied upon.
|
||||
|
||||
.. warning::
|
||||
|
||||
Array coordinates/values are assumed to refer to the *center* of the
|
||||
array element. Take a simple example input: ``[0, 1]``. The interpolated
|
||||
position of 0.5 in this array is midway between the 0-element (at
|
||||
``x=0``) and the 1-element (at ``x=1``), and thus would fall at
|
||||
``x=0.5``.
|
||||
|
||||
This means that to find reasonable contours, it is best to find contours
|
||||
midway between the expected "light" and "dark" values. In particular,
|
||||
given a binarized array, DO NOT choose to find contours at the low or high
|
||||
given a binarized array, *do not* choose to find contours at the low or high
|
||||
value of the array. This will often yield degenerate contours, especially
|
||||
around structures that are a single array element wide. Instead choose
|
||||
a middle value, as above.'''
|
||||
|
||||
array = np.asarray(array)
|
||||
a middle value, as above.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Lorensen, William and Harvey E. Cline. Marching Cubes: A High
|
||||
Resolution 3D Surface Construction Algorithm. Computer Graphics
|
||||
(SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170).
|
||||
|
||||
"""
|
||||
array = np.asarray(array, dtype=np.double)
|
||||
if array.ndim != 2:
|
||||
raise RuntimeError('Only 2D arrays are supported.')
|
||||
level = float(level)
|
||||
if (fully_connected not in _param_options or
|
||||
if (fully_connected not in _param_options or
|
||||
positive_orientation not in _param_options):
|
||||
raise ValueError('Parameters "fully_connected" and'
|
||||
' "positive_orientation" must be either "high" or "low".')
|
||||
@@ -98,7 +109,7 @@ def find_contours(array, level, fully_connected='low', positive_orientation='low
|
||||
if positive_orientation == 'high':
|
||||
contours = [c[::-1] for c in contours]
|
||||
return contours
|
||||
|
||||
|
||||
def _take_2(seq):
|
||||
iterator = iter(seq)
|
||||
while(True):
|
||||
@@ -117,24 +128,24 @@ def _assemble_contours(points_iterator):
|
||||
# exactly the contour level, and the rest are above or below.
|
||||
# This degnerate vertex will be picked up later by neighboring squares.
|
||||
if from_point == to_point: continue
|
||||
|
||||
|
||||
tail_data = starts.get(to_point)
|
||||
head_data = ends.get(from_point)
|
||||
|
||||
|
||||
if tail_data is not None and head_data is not None:
|
||||
tail, tail_num = tail_data
|
||||
head, head_num = head_data
|
||||
# We need to connect these two contours.
|
||||
# We need to connect these two contours.
|
||||
if tail is head:
|
||||
# We need to closed a contour.
|
||||
# Add the end point, and remove the contour from the
|
||||
# Add the end point, and remove the contour from the
|
||||
# 'starts' and 'ends' dicts.
|
||||
head.append(to_point)
|
||||
del starts[to_point]
|
||||
del ends[from_point]
|
||||
else: # tail is not head
|
||||
# We need to join two distinct contours.
|
||||
# We want to keep the first contour segment created, so that
|
||||
# We want to keep the first contour segment created, so that
|
||||
# the final contours are ordered left->right, top->bottom.
|
||||
if tail_num > head_num:
|
||||
# tail was created second. Append tail to head.
|
||||
@@ -166,7 +177,7 @@ def _assemble_contours(points_iterator):
|
||||
ends[to_point] = (new_contour, new_num)
|
||||
elif tail_data is not None and head_data is None:
|
||||
tail, tail_num = tail_data
|
||||
# We've found a single contour to which the new segment should be
|
||||
# We've found a single contour to which the new segment should be
|
||||
# prepended.
|
||||
tail.appendleft(from_point)
|
||||
del starts[to_point]
|
||||
@@ -179,6 +190,5 @@ def _assemble_contours(points_iterator):
|
||||
del ends[from_point]
|
||||
ends[to_point] = (head, head_num)
|
||||
# end iteration over from_ and to_ points
|
||||
|
||||
|
||||
return [np.array(contour) for (num, contour) in sorted(contours.items())]
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from skimage._build import cython
|
||||
|
||||
import os
|
||||
base_path = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
def configuration(parent_package='', top_path=None):
|
||||
from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs
|
||||
|
||||
config = Configuration('measure', parent_package, top_path)
|
||||
config.add_data_dir('tests')
|
||||
|
||||
cython(['_find_contours.pyx'], working_path=base_path)
|
||||
|
||||
config.add_extension('_find_contours', sources=['_find_contours.c'],
|
||||
include_dirs=[get_numpy_include_dirs()])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import numpy as np
|
||||
from numpy.testing import *
|
||||
|
||||
from skimage.find_contours import find_contours
|
||||
from skimage.measure import find_contours
|
||||
|
||||
a = np.ones((8,8), dtype=np.float32)
|
||||
a[1:-1, 1] = 0
|
||||
|
||||
Reference in New Issue
Block a user