Repository: fat-forensics/fat-forensics

Issue:
Merging segments and setting segmentation do not update the internal segment count
## Python version
- [ ] 2.x
- [ ] 3.6.x
- [X] 3.7.x

## Package versions
- fat-forensics: 0.1.1

## Description
Merging segments and setting segmentation do not update the internal segment count:

* [`fatf.utils.data.segmentation.Segmentation.set_segments`](https://github.com/fat-forensics/fat-forensics/blob/master/fatf/utils/data/segmentation.py#466) as well as segments setter
* [`fatf.utils.data.segmentation.Segmentation.merge_segments`](https://github.com/fat-forensics/fat-forensics/blob/master/fatf/utils/data/segmentation.py#L1193)

Discussion:

Patch:
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index e1e1182..00e07c7 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -15,23 +15,24 @@ jobs:
       with:
         path: doc/sphinx_gallery_auto
         key: ${{ runner.os }}-${{ hashFiles('examples/**') }}
-    - name: Set up Python 3.6
+    - name: Set up Python 3.7
       uses: actions/setup-python@v3
       with:
-        python-version: 3.6
+        python-version: 3.7
     - name: Set extra env -- scikit-learn and matplotlib
       run: |
-        echo "FATF_TEST_SKLEARN=0.19.2" >> $GITHUB_ENV
+        echo "FATF_TEST_SKLEARN=0.23.0" >> $GITHUB_ENV
         echo "FATF_TEST_MATPLOTLIB=3.2.0" >> $GITHUB_ENV
     - name: Install dependencies
       env:
         MAKEFLAGS: '-j 2'
-        FATF_TEST_SCIPY: '1.3.0'
-        FATF_TEST_NUMPY: '1.16.3'
+        FATF_TEST_SCIPY: '1.4.1'
+        FATF_TEST_NUMPY: '1.17.3'
       run: |
         make install-matplotlib
         make install-scikit-learn
         make dependencies-dev
+        pip install scikit-image==0.19.0
         pip install -r requirements-aux.txt
     - name: Test documentation
       run: |
diff --git a/README.rst b/README.rst
index 7817bf4..8629e5a 100644
--- a/README.rst
+++ b/README.rst
@@ -249,7 +249,7 @@ Acknowledgements
 This project is the result of a collaborative research agreement between Thales
 and the University of Bristol with the initial funding provided by Thales.
 
-.. _SciPy: https://www.scipy.org/
+.. _SciPy: https://scipy.org/
 .. _NumPy: https://www.numpy.org/
 .. _scikit-learn: https://scikit-learn.org/stable/
 .. _matplotlib: https://matplotlib.org/
diff --git a/doc/changelog.rst b/doc/changelog.rst
index 8917844..ddf5e1b 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -5,6 +5,31 @@
 Changelog
 +++++++++
 
+.. _changelog_0_1_2:
+
+0.1.2 (04/09/2022)
+==================
+
+The following bugs are fixed in this release:
+
+- Segmentation:
+
+  * A :class:`~fatf.utils.data.segmentation.Segmentation` object holds
+    incorrect segment count after manipulation (`#39 <issue_39_>`_).
+  * :class:`~fatf.utils.data.segmentation.Slic` segmentation fails quietly by
+    not starting the segment count at 1.
+    (This issue appears to have been fixed in scikit-image 0.19.2 and higher.)
+
+- Occlusion:
+
+  * `occlude_segments_vectorised`
+    (:class:`~fatf.utils.data.occlusion.Occlusion`) returns an occlusion
+    of incorrect shape if the input array is 2D with just one row
+    (`#40 <issue_40_>`_).
+
+.. _issue_39: https://github.com/fat-forensics/fat-forensics/issues/39
+.. _issue_40: https://github.com/fat-forensics/fat-forensics/issues/40
+
 .. _changelog_0_1_1:
 
 0.1.1 (10/04/2022)
diff --git a/doc/development.rst b/doc/development.rst
index 74eecb4..35eae38 100644
--- a/doc/development.rst
+++ b/doc/development.rst
@@ -167,7 +167,7 @@ directory of the package.
 .. _`Google Python Style Guide`: https://google.github.io/styleguide/pyguide.html
 .. _YAPF: https://github.com/google/yapf
 .. _`.style.yapf`: https://github.com/fat-forensics/fat-forensics/blob/master/.style.yapf
-.. _Pylint: https://www.pylint.org/
+.. _Pylint: https://pylint.pycqa.org
 .. _Flake8: https://flake8.pycqa.org/en/latest/
 .. _`.pylintrc`: https://github.com/fat-forensics/fat-forensics/blob/master/.pylintrc
 .. _`.flake8`: https://github.com/fat-forensics/fat-forensics/blob/master/.flake8
@@ -271,7 +271,7 @@ Step by Step Guide
 To summarise, the following commands should be executed to fully test the
 package (cf. `.github/workflows/tests.yml`_ for more details):
 
-.. _`.github/workflows/tests.yml`: https://github.com/fat-forensics/fat-forensics/tree/master/.github/workflows/tests.yml
+.. _`.github/workflows/tests.yml`: https://github.com/fat-forensics/fat-forensics/blob/master/.github/workflows/tests.yml
 
 .. code-block:: bash
 
diff --git a/doc/getting_started/install_deps_os.rst b/doc/getting_started/install_deps_os.rst
index 6a40596..bacef68 100644
--- a/doc/getting_started/install_deps_os.rst
+++ b/doc/getting_started/install_deps_os.rst
@@ -136,7 +136,7 @@ Developers and contributors may be interesting in the following pages as well:
 
 .. _PyPi: https://pypi.org/
 .. _NumPy: https://numpy.org/
-.. _SciPy: https://www.scipy.org/
+.. _SciPy: https://scipy.org/
 .. _requirements.txt: https://github.com/fat-forensics/fat-forensics/blob/master/requirements.txt
 .. |scikit-learn| replace:: ``scikit-learn>=0.19.2``
 .. _scikit-learn: https://scikit-learn.org/stable/
diff --git a/doc/index.rst b/doc/index.rst
index 8c66a50..d1238d6 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -48,7 +48,7 @@ more information about the support we receive :ref:`here <contributors>`.
 Please remember to :ref:`cite us <citing>` if you use any part of the package
 or its documentation.
 
-.. _SciPy: https://www.scipy.org/
+.. _SciPy: https://scipy.org/
 .. _NumPy: https://numpy.org/
 .. _`FAT-Forensics organisation`: https://github.com/fat-forensics
 .. _`fat-forensics`: https://github.com/fat-forensics/fat-forensics
diff --git a/doc/news.rst b/doc/news.rst
index f81a3d4..913681a 100644
--- a/doc/news.rst
+++ b/doc/news.rst
@@ -5,6 +5,14 @@
 News
 ++++
 
+[04/09/2022] Version 0.1.2 released!
+====================================
+
+Today we release a minor update fixing segmentation and occlusion bugs.
+
+The changelog summarises the bugs fixed in this release:
+:ref:`changelog_0_1_2`.
+
 [10/04/2022] Version 0.1.1 released!
 ====================================
 
diff --git a/doc/tutorials/index.rst b/doc/tutorials/index.rst
index 15a1f83..45a516b 100644
--- a/doc/tutorials/index.rst
+++ b/doc/tutorials/index.rst
@@ -18,7 +18,7 @@ launch a Python interpreter and check the version of the package::
   $ python
   >>> import fatf
   >>> fatf.__version__
-  '0.1.1'
+  '0.1.2'
 
 .. note:: **Doctest Mode**
 
diff --git a/fatf/__init__.py b/fatf/__init__.py
index 3c5a034..3b0e5b9 100644
--- a/fatf/__init__.py
+++ b/fatf/__init__.py
@@ -24,7 +24,7 @@ __email__ = 'k.sokol@bristol.ac.uk'
 __license__ = 'new BSD'
 
 # The current package version
-__version__ = '0.1.1'
+__version__ = '0.1.2'
 
 __all__ = ['setup_warning_filters', 'setup_random_seed']
 
diff --git a/fatf/utils/data/occlusion.py b/fatf/utils/data/occlusion.py
index 5bd4a17..cff694b 100644
--- a/fatf/utils/data/occlusion.py
+++ b/fatf/utils/data/occlusion.py
@@ -605,6 +605,8 @@ optional (default=None)
         if not fuav.is_numerical_array(vectorised_segments_subset):
             raise TypeError('The vector representation of segments should be '
                             'a numerical numpy array.')
+
+        input_1d = False
         if fuav.is_1d_array(vectorised_segments_subset):
             if vectorised_segments_subset.shape[0] != self.segments_number:
                 raise IncorrectShapeError(
@@ -614,6 +616,7 @@ optional (default=None)
                          vectorised_segments_subset.shape[0],
                          self.segments_number))
             samples = 1
+            input_1d = True
             vectorised_segments_subset = np.asarray(
                 [vectorised_segments_subset])
         elif fuav.is_2d_array(vectorised_segments_subset):
@@ -645,7 +648,7 @@ optional (default=None)
                                                    self.segments)
             image_occluded[i, occlusion_mask] = colouring_strategy(
                 occlusion_mask)
-        if samples == 1:
+        if input_1d:
             image_occluded = image_occluded[0]
 
         return image_occluded
diff --git a/fatf/utils/data/segmentation.py b/fatf/utils/data/segmentation.py
index 9e75526..033a36d 100644
--- a/fatf/utils/data/segmentation.py
+++ b/fatf/utils/data/segmentation.py
@@ -456,12 +456,20 @@ class Segmentation(abc.ABC):
 
     @segments.setter
     def segments(self, segments: np.ndarray):
-        """Setups the segments manually."""
+        """
+        Setups the segments manually.
+
+        Warns
+        -----
+        UserWarning
+            Inform the user that the segmentation has only one segment.
+        """
         assert _validate_segmentation(segments, self.image), 'Bad segments.'
         if np.unique(segments).shape[0] == 1:
             warnings.warn('The segmentation has only **one** segment.',
                           UserWarning)
         self._segments = segments
+        self.segments_number = np.unique(self._segments).shape[0]
 
     def set_segments(self, segments: np.ndarray):
         """
@@ -484,6 +492,11 @@ class Segmentation(abc.ABC):
         segments : numpy.ndarray
             A 2-dimensional numpy array representing a segmentation.
 
+        Warns
+        -----
+        UserWarning
+            Inform the user that the segmentation has only one segment.
+
         Raises
         ------
         IncorrectShapeError
@@ -502,6 +515,7 @@ class Segmentation(abc.ABC):
             warnings.warn('The segmentation has only **one** segment.',
                           UserWarning)
         self._segments = segments
+        self.segments_number = np.unique(self._segments).shape[0]
 
     def mark_boundaries(self,
                         mask: bool = False,
@@ -1079,7 +1093,7 @@ list(tuple(integer, integer, integer)), optional (default=None)
 
         Parameters
         ----------
-        segments_subset : intiger or list(integer), optional (default=None)
+        segments_subset : integer or list(integer), optional (default=None)
             A number of a specific segment or a list of segments to be
             grayed out. By default (``None``) all the segments are grayed out.
         mask : boolean, optional (default=False)
@@ -1212,6 +1226,11 @@ list(tuple(integer, integer, integer)), optional (default=None)
             If provided, the merging will be performed on this segmentation
             instead of the one stored in the class.
 
+        Warns
+        -----
+        UserWarning
+            Inform the user that only a single segment is being created.
+
         Raises
         ------
         IncorrectShapeError
@@ -1317,6 +1336,10 @@ list(tuple(integer, integer, integer)), optional (default=None)
 
         if inplace:
             self.set_segments(merged_segments)
+        else:
+            if np.unique(merged_segments).shape[0] == 1:
+                warnings.warn('The new segmentation has only **one** segment.',
+                              UserWarning)
 
         return merged_segments
 
@@ -1361,6 +1384,11 @@ class Slic(Segmentation):
 
         Raises
         ------
+        RuntimeError
+            Slic segmenter can sometimes fail quietly, returning a segmentation
+            that starts at 0 or is all 0s.
+            This appears to have been resolved in scikit-image 0.19.2 and
+            higher.
         TypeError
             The number of segments parameter is not an integer.
         ValueError
@@ -1380,6 +1408,14 @@ class Slic(Segmentation):
 
         segments = ski_segmentation.slic(
             self.segmentation_mask, start_label=1, **self.kwargs)
+
+        # Slic sometimes fails quietly, returning segmentation that starts
+        # at 0 or all-0
+        if 0 in np.unique(segments):
+            raise RuntimeError(  # pragma: nocover
+                'Slic segmenter failed. '
+                'Try upgrading to scikit-image 0.19.2 or higher.')
+
         return segments
 
 
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 85e3a4d..0537cf7 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,6 +1,8 @@
 codecov==2.1.0
+docutils<0.18
 flake8==3.8.1
 isort<=4.3.21
+jinja2<3.1.0
 mypy==0.770
 nbval==0.9.1
 numpydoc==0.8.0
@@ -10,4 +12,5 @@ pytest-cov==2.6.0
 sphinx==2.0
 sphinx-gallery==0.3.1
 twine==1.14.0
+wheel
 yapf==0.26.0


Test patch:
diff --git a/fatf/utils/data/tests/test_occlusion.py b/fatf/utils/data/tests/test_occlusion.py
index e1a4f0b..feb73e7 100644
--- a/fatf/utils/data/tests/test_occlusion.py
+++ b/fatf/utils/data/tests/test_occlusion.py
@@ -500,6 +500,11 @@ occlude_segments_vectorised`.
             np.array([[1.0, 0.0], [1, 0]]), colour='black')
         assert np.array_equal(ocl, np.array([ocl_, ocl_]))
 
+        # 2-D mask -- one image
+        ocl = occlusion.occlude_segments_vectorised(
+            np.array([[1, 0]]), colour='black')
+        assert np.array_equal(ocl, np.array([ocl_]))
+
         # 2-D mask -- external image
         ocl = occlusion.occlude_segments_vectorised(
             np.array([[1.0, 0.0], [1, 0]]), image=ocl_, colour='black')
diff --git a/fatf/utils/data/tests/test_segmentation.py b/fatf/utils/data/tests/test_segmentation.py
index 3cb9ccd..2d3e4c8 100644
--- a/fatf/utils/data/tests/test_segmentation.py
+++ b/fatf/utils/data/tests/test_segmentation.py
@@ -426,32 +426,72 @@ class TestSegmentation(object):
         segments_[0, 1] = 2
         segments_one = segments.copy()
         segments_one[0, 0] = 1
+        segments_three = segments.copy()
+        segments_three[1, 1] = 3
 
         segmenter = self.BinarySegmentation(ARRAY_IMAGE_3D)
-
         assert np.array_equal(segmenter._segments, segments)
         assert np.array_equal(segmenter.segments, segments)
+        assert segmenter.segments_number == 2
+
         segmenter.segments = segments_
         assert np.array_equal(segmenter._segments, segments_)
         assert np.array_equal(segmenter.segments, segments_)
+        assert segmenter.segments_number == np.unique(segments_).shape[0]
 
         segmenter.segments = segments
         assert np.array_equal(segmenter._segments, segments)
         assert np.array_equal(segmenter.segments, segments)
+        assert segmenter.segments_number == np.unique(segments).shape[0]
         segmenter.set_segments(segments_)
         assert np.array_equal(segmenter._segments, segments_)
         assert np.array_equal(segmenter.segments, segments_)
+        assert segmenter.segments_number == np.unique(segments_).shape[0]
 
         segmenter.segments = segments
         assert np.array_equal(segmenter._segments, segments)
         assert np.array_equal(segmenter.segments, segments)
+        assert segmenter.segments_number == np.unique(segments).shape[0]
         segmenter._segments = segments_
         assert np.array_equal(segmenter._segments, segments_)
         assert np.array_equal(segmenter.segments, segments_)
+        assert segmenter.segments_number == np.unique(segmenter.segments).shape[0]
 
         segmenter.segments = segments
         assert np.array_equal(segmenter._segments, segments)
         assert np.array_equal(segmenter.segments, segments)
+        assert segmenter.segments_number == np.unique(segments).shape[0]
+
+        # Different number of segments
+        segmenter.segments = segments_three
+        assert np.array_equal(segmenter._segments, segments_three)
+        assert np.array_equal(segmenter.segments, segments_three)
+        assert segmenter.segments_number == np.unique(segments_three).shape[0]
+        ## ...restore
+        segmenter.segments = segments
+        assert np.array_equal(segmenter._segments, segments)
+        assert np.array_equal(segmenter.segments, segments)
+        assert segmenter.segments_number == np.unique(segments).shape[0]
+        ## Illegal
+        segmenter._segments = segments_three
+        assert np.array_equal(segmenter._segments, segments_three)
+        assert np.array_equal(segmenter.segments, segments_three)
+        assert segmenter.segments_number != np.unique(segments_three).shape[0]
+        ## ...restore
+        segmenter.segments = segments
+        assert np.array_equal(segmenter._segments, segments)
+        assert np.array_equal(segmenter.segments, segments)
+        assert segmenter.segments_number == np.unique(segments).shape[0]
+        ## Legal
+        segmenter.set_segments(segments_three)
+        assert np.array_equal(segmenter._segments, segments_three)
+        assert np.array_equal(segmenter.segments, segments_three)
+        assert segmenter.segments_number == np.unique(segments_three).shape[0]
+        ## ...restore
+        segmenter.segments = segments
+        assert np.array_equal(segmenter._segments, segments)
+        assert np.array_equal(segmenter.segments, segments)
+        assert segmenter.segments_number == np.unique(segments).shape[0]
 
         wrn_msg = 'The segmentation has only **one** segment.'
         with pytest.warns(UserWarning) as warning:
@@ -962,10 +1002,21 @@ class TestSegmentation(object):
         segments_ = np.ones(shape=ARRAY_IMAGE_2D.shape, dtype=np.uint8)
         segments_[0, 0] = 2
 
-        # No mergin
+        # No merging
         segments = segmenter.merge_segments([[1], [2]], inplace=False)
         assert np.array_equal(segmenter.segments, segments_)
         assert np.array_equal(segments, segments_)
+        assert segmenter.segments_number == np.unique(segments_).shape[0]
+
+        wrn_msg = 'The new segmentation has only **one** segment.'
+        with pytest.warns(UserWarning) as warning:
+            segments = segmenter.merge_segments([[1, 2]], inplace=False)
+        assert len(warning) == 1
+        assert str(warning[0].message) == wrn_msg
+        assert np.array_equal(segmenter.segments, segments_)
+        assert np.array_equal(segments, np.ones(shape=(2, 2), dtype=np.uint8))
+        assert segmenter.segments_number == np.unique(segments_).shape[0]
+        assert np.unique(segments).shape[0] != np.unique(segments_).shape[0]
 
         # In-place
         wrn_msg = 'The segmentation has only **one** segment.'
@@ -976,6 +1027,7 @@ class TestSegmentation(object):
         assert np.array_equal(segmenter.segments,
                               np.ones(shape=(2, 2), dtype=np.uint8))
         assert np.array_equal(segments, np.ones(shape=(2, 2), dtype=np.uint8))
+        assert segmenter.segments_number == 1
 
         segmenter = self.BinarySegmentation(ARRAY_IMAGE_3D)
         with pytest.warns(UserWarning) as warning:
@@ -985,6 +1037,7 @@ class TestSegmentation(object):
         assert np.array_equal(segmenter.segments,
                               np.ones(shape=(2, 2), dtype=np.uint8))
         assert np.array_equal(segments, np.ones(shape=(2, 2), dtype=np.uint8))
+        assert segmenter.segments_number == 1
 
         # Custom segmentation -- leave one out
         segments = segmenter.merge_segments(  # yapf: disable
@@ -994,6 +1047,8 @@ class TestSegmentation(object):
         assert np.array_equal(segmenter.segments,
                               np.ones(shape=(2, 2), dtype=np.uint8))
         assert np.array_equal(segments, np.array([[1, 2], [1, 1]]))
+        assert segmenter.segments_number == 1
+        assert np.unique(segments).shape[0] != np.unique(segmenter.segments).shape[0]
 
 
 def test_slic():