Repository: rasterio/rasterio

Issue:
Merge with `min` and `max` methods results in empty arrays 
Hello,

I tried to merge tif images with the `max` method but I was surprised to see its outputing an empty array:
![2021-03-17_15h39_11](https://user-images.githubusercontent.com/67311115/111485814-3c53ad80-8737-11eb-90e4-68237c83027c.png)

Then I tried with other methods, `min` has the same results when the other methods work just fine:
![2021-03-17_15h39_29](https://user-images.githubusercontent.com/67311115/111485958-5c836c80-8737-11eb-948c-6c7a14ac2c83.png)
![2021-03-17_15h39_37](https://user-images.githubusercontent.com/67311115/111485961-5c836c80-8737-11eb-92a5-36e04380db4c.png)
![2021-03-17_15h39_47](https://user-images.githubusercontent.com/67311115/111485964-5d1c0300-8737-11eb-8011-1947ea175478.png)
![2021-03-17_15h40_00](https://user-images.githubusercontent.com/67311115/111485966-5d1c0300-8737-11eb-8994-c92d0e3186f1.png)

I repeated these steps with several TIFF images (both in uint16 and float) and I got the same results...
The stack of dataset is composed of only one image:
![2021-03-17_16h06_06](https://user-images.githubusercontent.com/67311115/111489792-bf2a3780-873a-11eb-92ce-2ec446449c35.png)


I am using rasterio 1.2.1 on Python 3.7 in a debian:buster-slim docker image.
I might add everything worked just fine till this version


Discussion:
@remi-braun can you share your datasets? It's possible that we do have a bug here.
My datasets hare here (small arrays, one float, one int32): 
[rasterio_merge_bug.zip](https://github.com/mapbox/rasterio/files/6162187/rasterio_merge_bug.zip)

I think you can try it with every TIF file you find as the same pattern has happen on all my tests.

**Note**: I just ran the merge function on python 3.7 on Windows, with rasterio 1.2.0 and it works on these datasets :) 
Thanks @remi-braun ! We made some changes in 1.2.1. This might be related to #2101.

Resolving patch:
diff --git a/rasterio/merge.py b/rasterio/merge.py
--- a/rasterio/merge.py
+++ b/rasterio/merge.py
@@ -17,41 +17,37 @@
 logger = logging.getLogger(__name__)
 
 
-def copy_first(old_data, new_data, old_nodata, new_nodata, **kwargs):
-    mask = np.empty_like(old_data, dtype='bool')
-    np.logical_not(new_nodata, out=mask)
-    np.logical_and(old_nodata, mask, out=mask)
-    np.copyto(old_data, new_data, where=mask)
+def copy_first(merged_data, new_data, merged_mask, new_mask, **kwargs):
+    mask = np.empty_like(merged_mask, dtype="bool")
+    np.logical_not(new_mask, out=mask)
+    np.logical_and(merged_mask, mask, out=mask)
+    np.copyto(merged_data, new_data, where=mask)
 
 
-def copy_last(old_data, new_data, old_nodata, new_nodata, **kwargs):
-    mask = np.empty_like(old_data, dtype='bool')
-    np.logical_not(new_nodata, out=mask)
-    np.copyto(old_data, new_data, where=mask)
+def copy_last(merged_data, new_data, merged_mask, new_mask, **kwargs):
+    mask = np.empty_like(merged_mask, dtype="bool")
+    np.logical_not(new_mask, out=mask)
+    np.copyto(merged_data, new_data, where=mask)
 
 
-def copy_min(old_data, new_data, old_nodata, new_nodata, **kwargs):
-    mask = np.empty_like(old_data, dtype='bool')
-    np.logical_or(old_nodata, new_nodata, out=mask)
+def copy_min(merged_data, new_data, merged_mask, new_mask, **kwargs):
+    mask = np.empty_like(merged_mask, dtype="bool")
+    np.logical_or(merged_mask, new_mask, out=mask)
     np.logical_not(mask, out=mask)
+    np.minimum(merged_data, new_data, out=merged_data, where=mask)
+    np.logical_not(new_mask, out=mask)
+    np.logical_and(merged_mask, mask, out=mask)
+    np.copyto(merged_data, new_data, where=mask)
 
-    np.minimum(old_data, new_data, out=old_data, where=mask)
 
-    np.logical_not(new_nodata, out=mask)
-    np.logical_and(old_data, mask, out=mask)
-    np.copyto(old_data, new_data, where=mask)
-
-
-def copy_max(old_data, new_data, old_nodata, new_nodata, **kwargs):
-    mask = np.empty_like(old_data, dtype='bool')
-    np.logical_or(old_nodata, new_nodata, out=mask)
+def copy_max(merged_data, new_data, merged_mask, new_mask, **kwargs):
+    mask = np.empty_like(merged_mask, dtype="bool")
+    np.logical_or(merged_mask, new_mask, out=mask)
     np.logical_not(mask, out=mask)
-
-    np.maximum(old_data, new_data, out=old_data, where=mask)
-
-    np.logical_not(new_nodata, out=mask)
-    np.logical_and(old_data, mask, out=mask)
-    np.copyto(old_data, new_data, where=mask)
+    np.maximum(merged_data, new_data, out=merged_data, where=mask)
+    np.logical_not(new_mask, out=mask)
+    np.logical_and(merged_mask, mask, out=mask)
+    np.copyto(merged_data, new_data, where=mask)
 
 
 MERGE_METHODS = {
@@ -124,18 +120,18 @@ def merge(
             max: pixel-wise max of existing and new
         or custom callable with signature:
 
-        def function(old_data, new_data, old_nodata, new_nodata, index=None, roff=None, coff=None):
+        def function(merged_data, new_data, merged_mask, new_mask, index=None, roff=None, coff=None):
 
             Parameters
             ----------
-            old_data : array_like
+            merged_data : array_like
                 array to update with new_data
             new_data : array_like
                 data to merge
-                same shape as old_data
-            old_nodata, new_data : array_like
-                boolean masks where old/new data is nodata
-                same shape as old_data
+                same shape as merged_data
+            merged_mask, new_mask : array_like
+                boolean masks where merged/new data pixels are invalid
+                same shape as merged_data
             index: int
                 index of the current dataset within the merged dataset collection
             roff: int
@@ -333,13 +329,13 @@ def nullcontext(obj):
         region = dest[:, roff:roff + trows, coff:coff + tcols]
 
         if math.isnan(nodataval):
-            region_nodata = np.isnan(region)
+            region_mask = np.isnan(region)
         else:
-            region_nodata = region == nodataval
-        temp_nodata = np.ma.getmaskarray(temp)
+            region_mask = region == nodataval
+
+        temp_mask = np.ma.getmask(temp)
 
-        copyto(region, temp, region_nodata, temp_nodata,
-               index=idx, roff=roff, coff=coff)
+        copyto(region, temp, region_mask, temp_mask, index=idx, roff=roff, coff=coff)
 
     if dst_path is None:
         return dest, output_transform


Test patch:
diff --git a/tests/test_merge.py b/tests/test_merge.py
new file mode 100644
--- /dev/null
+++ b/tests/test_merge.py
@@ -0,0 +1,53 @@
+"""Tests of rasterio.merge"""
+
+import numpy
+import pytest
+
+import affine
+import rasterio
+from rasterio.merge import merge
+
+# Non-coincident datasets test fixture.
+# Three overlapping GeoTIFFs, two to the NW and one to the SE.
+@pytest.fixture(scope="function")
+def test_data_dir_overlapping(tmp_path):
+    kwargs = {
+        "crs": "EPSG:4326",
+        "transform": affine.Affine(0.2, 0, -114, 0, -0.2, 46),
+        "count": 1,
+        "dtype": rasterio.uint8,
+        "driver": "GTiff",
+        "width": 10,
+        "height": 10,
+        "nodata": 0,
+    }
+
+    with rasterio.open(tmp_path.joinpath("nw1.tif"), "w", **kwargs) as dst:
+        data = numpy.ones((10, 10), dtype=rasterio.uint8)
+        dst.write(data, indexes=1)
+
+    with rasterio.open(tmp_path.joinpath("nw3.tif"), "w", **kwargs) as dst:
+        data = numpy.ones((10, 10), dtype=rasterio.uint8) * 3
+        dst.write(data, indexes=1)
+
+    kwargs["transform"] = affine.Affine(0.2, 0, -113, 0, -0.2, 45)
+
+    with rasterio.open(tmp_path.joinpath("se.tif"), "w", **kwargs) as dst:
+        data = numpy.ones((10, 10), dtype=rasterio.uint8) * 2
+        dst.write(data, indexes=1)
+
+    return tmp_path
+
+
+@pytest.mark.parametrize(
+    "method,value", [("first", 1), ("last", 2), ("min", 1), ("max", 3)]
+)
+def test_merge_method(test_data_dir_overlapping, method, value):
+    """Merge method produces expected values in intersection"""
+    inputs = sorted(list(test_data_dir_overlapping.iterdir()))  # nw is first.
+    datasets = [rasterio.open(x) for x in inputs]
+    output_count = 1
+    arr, _ = merge(
+        datasets, output_count=output_count, method=method, dtype=numpy.uint64
+    )
+    numpy.testing.assert_array_equal(arr[:, 5:10, 5:10], value)