def test_distance_source(self):
     """
     (SourceComparison) distance to source
     """
     s1 = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     s2 = Source([[20, 20], [20, 30]], values=[1.0, 2.0])
     assert (s1.distance(s2) == sqrt(200))
Exemple #2
0
 def test_distance_array(self):
     """
     (SourceComparison) distance to array
     """
     s1 = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     assert(s1.distance([20, 25]) == sqrt(200))
     assert(s1.distance(array([20, 25])) == sqrt(200))
 def test_distance_array(self):
     """
     (SourceComparison) distance to array
     """
     s1 = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     assert (s1.distance([20, 25]) == sqrt(200))
     assert (s1.distance(array([20, 25])) == sqrt(200))
Exemple #4
0
 def test_distance_source(self):
     """
     (SourceComparison) distance to source
     """
     s1 = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     s2 = Source([[20, 20], [20, 30]], values=[1.0, 2.0])
     assert(s1.distance(s2) == sqrt(200))
 def test_to_array(self):
     """
     (SourceConversion) to array
     """
     s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     assert (isinstance(s.toarray().center, ndarray))
     assert (isinstance(s.tolist().toarray().center, ndarray))
     assert (isinstance(s.tolist().toarray().bbox, ndarray))
Exemple #6
0
 def test_to_array(self):
     """
     (SourceConversion) to array
     """
     s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     assert(isinstance(s.toarray().center, ndarray))
     assert(isinstance(s.tolist().toarray().center, ndarray))
     assert(isinstance(s.tolist().toarray().bbox, ndarray))
 def test_to_list(self):
     """
     (SourceConversion) to list
     """
     s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     getattr(s, "center")
     assert (isinstance(s.tolist().center, list))
     getattr(s, "bbox")
     assert (isinstance(s.tolist().bbox, list))
Exemple #8
0
 def test_to_list(self):
     """
     (SourceConversion) to list
     """
     s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     getattr(s, "center")
     assert(isinstance(s.tolist().center, list))
     getattr(s, "bbox")
     assert(isinstance(s.tolist().bbox, list))
    def test_min(self):
        """
        (BasicCleaner) min area
        """
        list1 = [Source(random.randn(20, 2)) for _ in range(10)]
        list2 = [Source(random.randn(5, 2)) for _ in range(20)]
        sources = list1 + list2
        model = SourceModel(sources)

        c = BasicCleaner(minArea=10)
        newmodel = model.clean(c)

        assert (len(newmodel.sources) == 10)
    def test_min_max(self):
        """
        (BasicCleaner) min and max area
        """
        list1 = [Source(random.randn(20, 2)) for _ in range(10)]
        list2 = [Source(random.randn(10, 2)) for _ in range(20)]
        list3 = [Source(random.randn(15, 2)) for _ in range(5)]
        sources = list1 + list2 + list3
        model = SourceModel(sources)

        c = BasicCleaner(minArea=11, maxArea=19)
        newmodel = model.clean(c)

        assert (len(newmodel.sources) == 5)
Exemple #11
0
    def test_merge(self):
        """
        (SourceMethods) merge
        """
        s1 = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
        s2 = Source([[10, 30], [10, 40]], values=[4.0, 5.0])
        s1.merge(s2)
        assert(array_equal(s1.coordinates, [[10, 10], [10, 20], [10, 30], [10, 40]]))
        assert(array_equal(s1.values, [1.0, 2.0, 4.0, 5.0]))

        s1 = Source([[10, 10], [10, 20]])
        s2 = Source([[10, 30], [10, 40]])
        s1.merge(s2)
        assert(array_equal(s1.coordinates, [[10, 10], [10, 20], [10, 30], [10, 40]]))
    def test_match_sources(self):
        """
        (SourceModelComparison) matching sources
        """
        s1 = Source([[10, 10], [10, 20]])
        s2 = Source([[20, 20], [20, 30]])
        s3 = Source([[20, 20], [20, 30]])
        s4 = Source([[10, 10], [10, 20]])
        s5 = Source([[15, 15], [15, 20]])

        sm1 = SourceModel([s1, s2])
        sm2 = SourceModel([s3, s4, s5])

        assert (sm1.match(sm2) == [1, 0])
        assert (sm2.match(sm1) == [1, 0, 0])
Exemple #13
0
    def test_restore(self):
        """
        (SourceProperties) remove lazy attributes
        """
        s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
        assert(array_equal(s.center, [10, 15]))

        assert("center" in s.__dict__.keys())
        s.restore()
        assert("center" not in s.__dict__.keys())

        assert(array_equal(s.center, [10, 15]))
        assert("center" in s.__dict__.keys())
        s.restore(skip="center")
        assert("center" in s.__dict__.keys())
Exemple #14
0
    def extract(self, im):
        from numpy import ones, concatenate
        from skimage.feature import peak_local_max
        from skimage.draw import circle

        # extract local peaks
        if im.ndim == 2:
            peaks = peak_local_max(im,
                                   min_distance=self.minDistance,
                                   num_peaks=self.maxSources).tolist()
        else:
            peaks = []
            for i in range(0, im.shape[2]):
                tmp = peak_local_max(im[:, :, i],
                                     min_distance=self.minDistance,
                                     num_peaks=self.maxSources)
                peaks = peaks.append(
                    concatenate((tmp, ones((len(tmp), 1)) * i), axis=1))

        # construct circular regions from peak points
        def pointToCircle(center, radius):
            rr, cc = circle(center[0], center[1], radius)
            return array(zip(rr, cc))

        # return circles as sources
        circles = [pointToCircle(p, self.radius) for p in peaks]
        return SourceModel([Source(c) for c in circles])
 def test_source_fromCoordinates(self):
     """
     (SourceConstruction) from coordinates
     """
     s = Source.fromCoordinates([[10, 10], [10, 20]])
     assert(isinstance(s.coordinates, ndarray))
     assert(array_equal(s.coordinates, array([[10, 10], [10, 20]])))
Exemple #16
0
    def extract(self, block):
        import sima

        # reshape the block to (t, z, y, x, c)
        dims = block.shape
        if len(dims) == 3:  # (t, x, y)
            reshapedBlock = block.reshape(dims[0], 1, dims[2], dims[1], 1)
        else:  # (t, x, y, z)
            reshapedBlock = block.reshape(dims[0], dims[3], dims[2], dims[1],
                                          1)

        # create SIMA dataset from block
        dataset = sima.ImagingDataset(
            [sima.Sequence.create('ndarray', reshapedBlock)], None)

        # apply the sima strategy to the dataset
        rois = self.strategy.segment(dataset)

        # convert the coordinates between the SIMA and thunder conventions
        coords = [asarray(where(array(roi))).T for roi in rois]
        if len(dims) == 3:
            coords = [c[:, 1:] for c in coords]
        coords = [c[:, ::-1] for c in coords]

        # format the sources
        sources = [Source(c) for c in coords]

        return sources
 def test_source_with_values(self):
     """
     (SourceConstruction) create with values
     """
     s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     assert (array_equal(s.coordinates, array([[10, 10], [10, 20]])))
     assert (array_equal(s.values, array([1.0, 2.0])))
 def test_source(self):
     """
     (SourceConstruction) create
     """
     s = Source([[10, 10], [10, 20]])
     assert (isinstance(s.coordinates, ndarray))
     assert (array_equal(s.coordinates, array([[10, 10], [10, 20]])))
 def test_polygon(self):
     """
     (SourceProperties) polygon
     """
     x, y = meshgrid(range(0, 10), range(0, 10))
     coords = zip(x.flatten(), y.flatten())
     s = Source(coords)
     assert (array_equal(s.polygon, [[0, 0], [9, 0], [9, 9], [0, 9]]))
Exemple #20
0
    def test_dilate(self):
        """
        (SourceMethods) dilate
        """
        # make base source
        m = zeros((10, 10))
        m[5, 5] = 1
        m[5, 6] = 1
        m[6, 5] = 1
        m[4, 5] = 1
        m[5, 4] = 1
        coords = asarray(where(m)).T
        s = Source(coords)

        # dilating by 0 doesn't change anything
        assert(array_equal(s.dilate(0).coordinates, s.coordinates))
        assert(array_equal(s.dilate(0).bbox, [4, 4, 6, 6]))

        # dilating by 1 expands region but doesn't affect center
        assert(array_equal(s.dilate(1).center, s.center))
        assert(array_equal(s.dilate(1).area, 21))
        assert(array_equal(s.dilate(1).bbox, [3, 3, 7, 7]))
        assert(array_equal(s.dilate(1).mask().shape, [5, 5]))

        # manually construct expected shape of dilated source mask
        truth = ones((5, 5))
        truth[0, 0] = 0
        truth[4, 4] = 0
        truth[0, 4] = 0
        truth[4, 0] = 0
        assert(array_equal(s.dilate(1).mask(), truth))
    def test_dilate(self):
        """
        (SourceMethods) dilate
        """
        # make base source
        m = zeros((10, 10))
        m[5, 5] = 1
        m[5, 6] = 1
        m[6, 5] = 1
        m[4, 5] = 1
        m[5, 4] = 1
        coords = asarray(where(m)).T
        s = Source(coords)

        # dilating by 0 doesn't change anything
        assert(array_equal(s.dilate(0).coordinates, s.coordinates))
        assert(array_equal(s.dilate(0).bbox, [4, 4, 6, 6]))

        # dilating by 1 expands region but doesn't affect center
        assert(array_equal(s.dilate(1).center, s.center))
        assert(array_equal(s.dilate(1).area, 21))
        assert(array_equal(s.dilate(1).bbox, [3, 3, 7, 7]))
        assert(array_equal(s.dilate(1).mask().shape, [5, 5]))

        # manually construct expected shape of dilated source mask
        truth = ones((5, 5))
        truth[0, 0] = 0
        truth[4, 4] = 0
        truth[0, 4] = 0
        truth[4, 0] = 0
        assert(array_equal(s.dilate(1).mask(), truth))
Exemple #22
0
    def test_crop(self):
        """
        (SourceMethods) crop
        """
        # without values
        s = Source([[10, 10], [10, 20]])
        assert(array_equal(s.crop([0, 0], [21, 21]).coordinates, s.coordinates))
        assert(array_equal(s.crop([0, 0], [11, 11]).coordinates, [[10, 10]]))
        assert(array_equal(s.crop([0, 0], [5, 5]).coordinates, []))

        # with values (two dimensional)
        s = Source([[10, 10], [10, 20]])
        assert(array_equal(s.crop([0, 0], [21, 21]).coordinates, s.coordinates))
        assert(array_equal(s.crop([0, 0], [11, 11]).coordinates, [[10, 10]]))
        assert(array_equal(s.crop([0, 0], [5, 5]).coordinates, []))
Exemple #23
0
    def test_inbounds(self):
        """
        (SourceMethods) in bounds
        """
        # two dimensional
        s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
        assert(s.inbounds([0, 0], [20, 20]) == 1)
        assert(s.inbounds([0, 0], [10, 10]) == 0.5)
        assert(s.inbounds([15, 15], [20, 20]) == 0)

        # three dimensional
        s = Source([[10, 10, 10], [10, 20, 20]], values=[1.0, 2.0])
        assert(s.inbounds([0, 0, 0], [20, 20, 20]) == 1)
        assert(s.inbounds([0, 0, 0], [10, 10, 20]) == 0.5)
        assert(s.inbounds([15, 15, 15], [20, 20, 20]) == 0)
Exemple #24
0
 def test_source_fromMask_binary(self):
     """
     (SourceConstruction) from mask
     """
     mask = zeros((10, 10))
     mask[5, 5] = 1
     mask[5, 6] = 1
     mask[5, 7] = 1
     s = Source.fromMask(mask)
     assert(isinstance(s, Source))
     assert(isinstance(s.coordinates, ndarray))
     assert(array_equal(s.coordinates, array([[5, 5], [5, 6], [5, 7]])))
     assert(array_equal(s.mask((10, 10), binary=True), mask))
     assert(array_equal(s.mask((10, 10), binary=False), mask))
 def test_source_fromMask_binary(self):
     """
     (SourceConstruction) from mask
     """
     mask = zeros((10, 10))
     mask[5, 5] = 1
     mask[5, 6] = 1
     mask[5, 7] = 1
     s = Source.fromMask(mask)
     assert(isinstance(s, Source))
     assert(isinstance(s.coordinates, ndarray))
     assert(array_equal(s.coordinates, array([[5, 5], [5, 6], [5, 7]])))
     assert(array_equal(s.mask((10, 10), binary=True), mask))
     assert(array_equal(s.mask((10, 10), binary=False), mask))
Exemple #26
0
 def test_overlap(self):
     """
     (SourceMethods) overlap
     """
     s1 = Source([[0, 0], [0, 1], [0, 2]], values=[0, 1, 2])
     s2 = Source([[0, 1], [0, 2], [0, 3]], values=[1, 2, 3])
     assert(s1.overlap(s2, 'fraction') == 0.5)
     assert(allclose(s1.overlap(s2, 'rates'), [2.0/3.0, 2.0/3.0]))
     assert(s1.overlap(s2, 'correlation') == 1.0)
 def test_source_fromMask_values(self):
     """
     (SourceConstruction) from mask with values
     """
     mask = zeros((10, 10))
     mask[5, 5] = 0.5
     mask[5, 6] = 0.6
     mask[5, 7] = 0.7
     s = Source.fromMask(mask)
     assert(isinstance(s, Source))
     assert(isinstance(s.coordinates, ndarray))
     assert(isinstance(s.values, ndarray))
     assert(array_equal(s.coordinates, array([[5, 5], [5, 6], [5, 7]])))
     assert(array_equal(s.values, array([0.5, 0.6, 0.7])))
     assert(array_equal(s.mask((10, 10), binary=False), mask))
Exemple #28
0
 def test_source_fromMask_values(self):
     """
     (SourceConstruction) from mask with values
     """
     mask = zeros((10, 10))
     mask[5, 5] = 0.5
     mask[5, 6] = 0.6
     mask[5, 7] = 0.7
     s = Source.fromMask(mask)
     assert(isinstance(s, Source))
     assert(isinstance(s.coordinates, ndarray))
     assert(isinstance(s.values, ndarray))
     assert(array_equal(s.coordinates, array([[5, 5], [5, 6], [5, 7]])))
     assert(array_equal(s.values, array([0.5, 0.6, 0.7])))
     assert(array_equal(s.mask((10, 10), binary=False), mask))
Exemple #29
0
    def generateSources(self, padding, center=(15, 15), radius=6):
        """
        Generate a set of sources and block keys
        by constructing a circular mask region,
        generating blocks (with or without padding),
        and returning the sources defined by the mask
        in each block, and the block keys
        """
        from skimage.draw import circle

        mask = zeros((30, 30))
        rr, cc = circle(center[0], center[1], radius)
        mask[rr, cc] = 1
        img = ImagesLoader(self.sc).fromArrays([mask])
        blks = img.toBlocks(size=(10, 10), padding=padding).collect()
        keys, vals = zip(*blks)
        sources = [[Source(asarray(where(squeeze(v))).T)] if sum(v) > 0 else [] for v in vals]

        return sources, keys, mask
    def test_merge(self):
        """
        (SourceMethods) merge
        """
        s1 = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
        s2 = Source([[10, 30], [10, 40]], values=[4.0, 5.0])
        s1.merge(s2)
        assert(array_equal(s1.coordinates, [[10, 10], [10, 20], [10, 30], [10, 40]]))
        assert(array_equal(s1.values, [1.0, 2.0, 4.0, 5.0]))

        s1 = Source([[10, 10], [10, 20]])
        s2 = Source([[10, 30], [10, 40]])
        s1.merge(s2)
        assert(array_equal(s1.coordinates, [[10, 10], [10, 20], [10, 30], [10, 40]]))
Exemple #31
0
    def test_outline(self):
        """
        (SourceMethods) outline
        """
        # make base source
        m = zeros((10, 10))
        m[5, 5] = 1
        m[5, 6] = 1
        m[6, 5] = 1
        m[4, 5] = 1
        m[5, 4] = 1
        coords = asarray(where(m)).T
        s = Source(coords)

        # compare outlines to manual results
        o1 = s.outline(0, 1).mask((10, 10))
        o2 = s.dilate(1).mask((10, 10)) - s.mask((10, 10))
        assert(array_equal(o1, o2))

        o1 = s.outline(1, 2).mask((10, 10))
        o2 = s.dilate(2).mask((10, 10)) - s.dilate(1).mask((10, 10))
        assert(array_equal(o1, o2))
    def test_crop(self):
        """
        (SourceMethods) crop
        """
        # without values
        s = Source([[10, 10], [10, 20]])
        assert(array_equal(s.crop([0, 0], [21, 21]).coordinates, s.coordinates))
        assert(array_equal(s.crop([0, 0], [11, 11]).coordinates, [[10, 10]]))
        assert(array_equal(s.crop([0, 0], [5, 5]).coordinates, []))

        # with values (two dimensional)
        s = Source([[10, 10], [10, 20]])
        assert(array_equal(s.crop([0, 0], [21, 21]).coordinates, s.coordinates))
        assert(array_equal(s.crop([0, 0], [11, 11]).coordinates, [[10, 10]]))
        assert(array_equal(s.crop([0, 0], [5, 5]).coordinates, []))
    def test_restore(self):
        """
        (SourceProperties) remove lazy attributes
        """
        s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
        assert (array_equal(s.center, [10, 15]))

        assert ("center" in s.__dict__.keys())
        s.restore()
        assert ("center" not in s.__dict__.keys())

        assert (array_equal(s.center, [10, 15]))
        assert ("center" in s.__dict__.keys())
        s.restore(skip="center")
        assert ("center" in s.__dict__.keys())
    def test_inbounds(self):
        """
        (SourceMethods) in bounds
        """
        # two dimensional
        s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
        assert(s.inbounds([0, 0], [20, 20]) == 1)
        assert(s.inbounds([0, 0], [10, 10]) == 0.5)
        assert(s.inbounds([15, 15], [20, 20]) == 0)

        # three dimensional
        s = Source([[10, 10, 10], [10, 20, 20]], values=[1.0, 2.0])
        assert(s.inbounds([0, 0, 0], [20, 20, 20]) == 1)
        assert(s.inbounds([0, 0, 0], [10, 10, 20]) == 0.5)
        assert(s.inbounds([15, 15, 15], [20, 20, 20]) == 0)
Exemple #35
0
    def extract(self, block):
        from numpy import clip, inf, percentile, asarray, where, size, prod
        from sklearn.decomposition import NMF
        from skimage.measure import label
        from skimage.morphology import remove_small_objects

        # get dimensions
        n = self.componentsPerBlock
        dims = block.shape[1:]

        # handle maximum size
        if self.maxArea == "block":
            maxArea = prod(dims) / 2
        else:
            maxArea = self.maxArea

        # reshape to be t x all spatial dimensions
        data = block.reshape(block.shape[0], -1)

        # build and apply NMF model to block
        model = NMF(n, max_iter=self.maxIter)
        model.fit(clip(data, 0, inf))

        # reconstruct sources as spatial objects in one array
        comps = model.components_.reshape((n, ) + dims)

        # convert from basis functions into shape
        # by finding connected components and removing small objects
        combined = []
        for c in comps:
            tmp = c > percentile(c, self.percentile)
            shape = remove_small_objects(label(tmp), min_size=self.minArea)
            coords = asarray(where(shape)).T
            if (size(coords) > 0) and (size(coords) < maxArea):
                combined.append(Source(coords))

        return combined
    def test_outline(self):
        """
        (SourceMethods) outline
        """
        # make base source
        m = zeros((10, 10))
        m[5, 5] = 1
        m[5, 6] = 1
        m[6, 5] = 1
        m[4, 5] = 1
        m[5, 4] = 1
        coords = asarray(where(m)).T
        s = Source(coords)

        # compare outlines to manual results
        o1 = s.outline(0, 1).mask((10, 10))
        o2 = s.dilate(1).mask((10, 10)) - s.mask((10, 10))
        assert(array_equal(o1, o2))

        o1 = s.outline(1, 2).mask((10, 10))
        o2 = s.dilate(2).mask((10, 10)) - s.dilate(1).mask((10, 10))
        assert(array_equal(o1, o2))
    def test_exclude(self):
        """
        (SourceMethods) exclude
        """
        # without values
        s = Source([[10, 10], [10, 20]])
        o = Source([[10, 20]])
        assert(array_equal(s.exclude(o).coordinates, [[10, 10]]))

        # with values (two dimensional)
        s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
        o = Source([[10, 20]])
        assert(array_equal(s.exclude(o).coordinates, [[10, 10]]))
        assert(array_equal(s.exclude(o).values, [1]))

        # with values (three dimensional)
        s = Source([[10, 10, 10], [10, 20, 20]], values=[1.0, 2.0])
        o = Source([[10, 20, 20]])
        assert(array_equal(s.exclude(o).coordinates, [[10, 10, 10]]))
        assert(array_equal(s.exclude(o).values, [1.0]))
 def test_center(self):
     """
     (SourceProperties) center
     """
     s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     assert (array_equal(s.center, [10, 15]))
Exemple #39
0
    def test_exclude(self):
        """
        (SourceMethods) exclude
        """
        # without values
        s = Source([[10, 10], [10, 20]])
        o = Source([[10, 20]])
        assert(array_equal(s.exclude(o).coordinates, [[10, 10]]))

        # with values (two dimensional)
        s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
        o = Source([[10, 20]])
        assert(array_equal(s.exclude(o).coordinates, [[10, 10]]))
        assert(array_equal(s.exclude(o).values, [1]))

        # with values (three dimensional)
        s = Source([[10, 10, 10], [10, 20, 20]], values=[1.0, 2.0])
        o = Source([[10, 20, 20]])
        assert(array_equal(s.exclude(o).coordinates, [[10, 10, 10]]))
        assert(array_equal(s.exclude(o).values, [1.0]))
 def test_bbox(self):
     """
     (SourceProperties) bounding box
     """
     s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     assert (array_equal(s.bbox, [10, 10, 10, 20]))
 def test_area(self):
     """
     (SourceProperties) area
     """
     s = Source([[10, 10], [10, 20]], values=[1.0, 2.0])
     assert (s.area == 2.0)
Exemple #42
0
    def extract(self, block):
        from numpy import clip, inf, percentile, asarray, where, size, prod, unique
        from scipy.ndimage import median_filter
        from sklearn.decomposition import NMF
        from skimage.measure import label
        from skimage.morphology import remove_small_objects

        # get dimensions
        n = self.componentsPerBlock
        dims = block.shape[1:]

        # handle maximum size
        if self.maxArea == "block":
            maxArea = prod(dims) / 2
        else:
            maxArea = self.maxArea

        # reshape to be t x all spatial dimensions
        data = block.reshape(block.shape[0], -1)

        # build and apply NMF model to block
        model = NMF(n, max_iter=self.maxIter)
        model.fit(clip(data, 0, inf))

        # reconstruct sources as spatial objects in one array
        comps = model.components_.reshape((n, ) + dims)

        # convert from basis functions into shape
        # by median filtering (optional), applying a threshold,
        # finding connected components and removing small objects
        combined = []
        for c in comps:
            tmp = c > percentile(c, self.percentile)
            regions = remove_small_objects(label(tmp), min_size=self.minArea)
            ids = unique(regions)
            ids = ids[ids > 0]
            for ii in ids:
                r = regions == ii
                if self.medFilter is not None:
                    r = median_filter(r, self.medFilter)
                coords = asarray(where(r)).T
                if (size(coords) > 0) and (size(coords) < maxArea):
                    combined.append(Source(coords))

        # merge overlapping sources
        if self.overlap is not None:

            # iterate over source pairs and find a pair to merge
            def merge(sources):
                for i1, s1 in enumerate(sources):
                    for i2, s2 in enumerate(sources[i1 + 1:]):
                        if s1.overlap(s2) > self.overlap:
                            return i1, i1 + 1 + i2
                return None

            # merge pairs until none left to merge
            pair = merge(combined)
            testing = True
            while testing:
                if pair is None:
                    testing = False
                else:
                    combined[pair[0]].merge(combined[pair[1]])
                    del combined[pair[1]]
                    pair = merge(combined)

        return combined