Repository: scikit-image/scikit-image

Issue:
Performance regression in morphology.watershed from 0.12.3 to 0.13.0
## Description
Great performance degradation in skimage.morphology.watershed for ver. 0.13.0 in comparison with ver. 0.12.3.
3D images with size about (500, 500, 500) were testes.

## Way to reproduce
```
import numpy as np
from scipy import ndimage
from skimage import feature
from skimage import morphology
import time

dimensions = 500  
z, y, x = np.indices((dimensions, dimensions + 5, dimensions + 10))
radius = 100
xc, yc, zc = dimensions // 3 - 6, dimensions // 3, dimensions // 3 + 6
test_image = np.int32((x - xc) ** 2 + (y - yc) ** 2 + (z - zc) ** 2 <= radius ** 2)
test_image[zc - 5, yc - 5, xc - 5] = 0
test_image[zc - 5, yc + 5, xc - 5] = 0
test_image[zc - 5, yc + 4, xc - 5] = 0
xc *= 2
yc *= 2
zc *= 2
test_image[zc:zc + 100, yc:yc + 100, xc:xc + 100] = 2
test_image[zc + 10 + 3, yc + 10 + 3, xc + 10 - 3] = 0
test_image[zc + 10 - 3, yc + 10 - 3, xc + 10 + 3] = 0
test_image[zc + 10 + 3, yc + 10 - 3, xc + 10 + 3] = 0
test_image[zc-5, yc-5, xc-5] = 3

dist = ndimage.distance_transform_edt(test_image)
local_maxi = feature.peak_local_max(dist, min_distance=2, indices=False)
labels, num_obj = ndimage.label(local_maxi)
start_t = time.clock()
labels = morphology.watershed(-dist, labels, connectivity=ndimage.generate_binary_structure(3, 3), mask=test_image)
print(time.clock() - start_t)
```

Processing time for ver. 0.12.3 is about 16 s on my computer.
Processing time for ver. 0.13.0 is about 90 s. 
Slowing down about 4 times!!! 

Python version 3.5 for Windows
scikit-image versions 0.12.3 and 0.13.0




Discussion:
I couldn’t reproduce. 

macOS, 0.12.3 (Python 3.5):

```
16.675887999999993
```

macOS, 0.13.0 (Python 3.5):

```
16.127555
```
I tested it on Windows 7.
I can reproduce. Below is the output of `/usr/bin/time -v python issue.py`:

Ubuntu 16.04. skimage 0.12.3: `15.727136`:
```
	Command being timed: "python issue.py"
	User time (seconds): 37.56
	System time (seconds): 11.04
	Percent of CPU this job got: 100%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:48.13
	Maximum resident set size (kbytes): 10928820
	Major (requiring I/O) page faults: 0
	Minor (reclaiming a frame) page faults: 3726847
	Voluntary context switches: 21
	Involuntary context switches: 66010
	Swaps: 0
	File system inputs: 16
	File system outputs: 8
```

Ubuntu 16.04. skimage 0.13.0: `84.192656`:
```
	Command being timed: "python issue.py"
	User time (seconds): 99.77
	System time (seconds): 15.04
	Percent of CPU this job got: 100%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 1:54.33
	Maximum resident set size (kbytes): 10911448
	Major (requiring I/O) page faults: 4
	Minor (reclaiming a frame) page faults: 5191020
	Voluntary context switches: 78
	Involuntary context switches: 54008
	Swaps: 0
	File system inputs: 1368
	File system outputs: 8
```
Yep, I can reproduce on macOS and what's worse, it looks like my compact watershed PR #2211 is to blame, as 47af70a5 still shows the regression. This is gonna be a fun one, I can tell. =P

Thanks @IliaSafonov for the report and detailed script — it made it very easy to verify. The hardest part was waiting for the damn thing to finish. =P
Ok initial Cython profiling (following instructions on [this page](http://cython.readthedocs.io/en/latest/src/tutorial/profiling_tutorial.html)) shows that the new version performs *way* more heap pushes and pops — 66 million instead of 5 million!

0.12.3:
```
 $ python profile-watershed.py
Thu Sep 28 15:09:46 2017    Profile.prof

         273658776 function calls in 80.700 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  5187852   37.997    0.000   58.069    0.000 heap_general.pxi:50(heappop)
173832010   14.717    0.000   14.717    0.000 heap_watershed.pxi:21(smaller)
        2    7.323    3.661    7.323    3.661 {method 'argsort' of 'numpy.ndarray' objects}
 89450505    5.733    0.000    5.733    0.000 heap_general.pxi:39(swap)
        1    3.172    3.172   62.783   62.783 _watershed.pyx:27(watershed)
        1    2.087    2.087   11.501   11.501 _rank_order.py:14(rank_order)
        4    1.464    0.366    1.464    0.366 {method 'astype' of 'numpy.ndarray' objects}
        3    1.297    0.432    1.297    0.432 watershed.py:148(pad)
  5187852    1.164    0.000    1.542    0.000 heap_general.pxi:103(heappush)
```

0.13:
```
 $ python profile-watershed.py
Thu Sep 28 14:59:23 2017    Profile.prof

         4083364212 function calls in 942.449 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
 66595598  540.383    0.000  867.714    0.000 heap_general.pxi:50(heappop)
2610573823  255.730    0.000  255.730    0.000 heap_watershed.pxi:22(smaller)
1339598774   82.664    0.000   82.664    0.000 heap_general.pxi:39(swap)
        1   27.559   27.559  928.681  928.681 _watershed.pyx:45(watershed_raveled)
 66595598   22.333    0.000   33.396    0.000 heap_general.pxi:103(heappush)
```

I haven't yet looked again at the diff, but I presume this might have to do with a switch from uint8 keys to float64, resulting in many fewer ties.
Ok, here's the relevant diff, left is 0.12, right is 47af70a: 

<img width="1160" alt="screen shot 2017-10-01 at 5 48 31 pm" src="https://user-images.githubusercontent.com/492549/31052453-e5cd25e4-a6d0-11e7-9400-5e5a8de313f7.png">

The difference is that in the old version, the output pixel gets labeled at the time that the pixel is pushed onto the heap, while in the new version, it gets labeled at the time that it's *popped* from the heap. When compactness is not a factor, the first version is correct, because the labelling is determined solely by which label gets there first. (If they get there at the same time, then it's arbitrary and you might as well give it to the label that pushes first.) But when compactness *is* a factor, labelling at push-time is a bug, because a different neighbor could get there "later" but actually be "closer" when compactness is factored in.

I'm going to isolate the commit where I changed this (I remember needing to do this), and then I think it can probably be fixed by adding an extra branch in the loop. Alternatively the most efficient route might be to duplicate most of the code and have two different loops, one for compactness and one for classical watershed.
This is an excellent comment and helps me a great deal in understanding the code.  Please add something to this effect as a comment in the code, describing how it works.

Resolving patch:
diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py
--- a/skimage/morphology/watershed.py
+++ b/skimage/morphology/watershed.py
@@ -261,8 +261,4 @@ def watershed(image, markers, connectivity=1, offset=None, mask=None,
 
     output = crop(output, pad_width, copy=True)
 
-    if watershed_line:
-        min_val = output.min()
-        output[output == min_val] = 0
-
     return output


Test patch: