Superpixel segmentation (SLIC algorithm), save the superpixel block obtained by segmentation

SLIC superpixel segmentation and save the segmented superpixel blocks:

# https://github.com/LarkMi/SLIC/blob/main/SLIC.py
 
import skimage
from skimage.segmentation import slic,mark_boundaries
from skimage import io
import matplotlib.pyplot as plt
from PIL import Image, ImageEnhance
import numpy as np
import cv2
#
# np.set_printoptions(threshold=np.inf)
path = 'C:\Users\Administrator\Desktop\SLIC\'
img_name = 'test.png'
img = io.imread(path + img_name,as_gray=True) #as_gray is grayscale reading, and the obtained value is normalized
Segments = slic(img, n_segments=10, compactness=0.2,start_label = 1)# for SLIC segmentation
out=mark_boundaries(img,segments)
out = out*255 #io's grayscale reading is a normalized value, if you read a color picture, remove this line
img3 = Image.fromarray(np.uint8(out))
img3. show()
seg_img_name = 'seg.png'
img3.save(path + '\' + seg_img_name)#Display and save the picture after adding the dividing line
 
maxn = max(segments. reshape(int(segments. shape[0]*segments. shape[1]),))
for i in range(1,maxn + 1):
    a = np.array(segments == i)
    b = img * a
    w,h = [],[]
    for x in range(b. shape[0]):
        for y in range(b. shape[1]):
            if b[x][y] != 0:
                w.append(x)
                h.append(y)
    
    c = b[min(w):max(w),min(h):max(h)]
    c = c*255
    d = c.reshape(c.shape[0],c.shape[1],1)
    e = np.concatenate((d,d),axis=2)
    e = np.concatenate((e,d),axis=2)
    img2 = Image.fromarray(np.uint8(e))
    img2.save(path + '\' + str(i) + '.png')
    print('saved' + str(i) + 'picture')
 
wid, hig = [], []
img = io.imread(path + '\' + seg_img_name)
 
for i in range(1,maxn + 1):
    w,h = [],[]
    for x in range(segments. shape[0]):
        for y in range(segments. shape[1]):
            if segments[x][y] == i:
                w.append(x)
                h.append(y)
                
 
    font=cv2.FONT_HERSHEY_SIMPLEX#Use the default font
    #print((min(w),min(h)))
    img=cv2.putText(img,str(i),(h[int(len(h)/(2))],w[int(len(w)/2)]),font,1,(255,255,255) ,2)#Add text, 1.2 means font size, (0,40) is the initial position, (255,255,255) means color, 2 means thickness
img = Image.fromarray(np.uint8(img))
img. show()
img.save(path + '\' + seg_img_name + '_label.png')

Self-written superpixel segmentation code:

  1. Added the label attribute to the Cluster class to facilitate labeling the results of the k-means algorithm
  2. Added a border increase part to the SLICProcessor class method save_current_image, which can generate images similar to 3||4.png
  3. Added a new class method generate_result() with a user parameter K, which is the number of clusters of the set Kmeans algorithm, and selects the region to merge according to the number of clusters
  4. In the original code, it is impossible to read jpg and png images at the same time due to the different number of image channels, so it can be adapted with small changes.
# https://gitee.com/xu-qiyu/MyProject/tree/master/opencv/superpixel segmentation

from copy import copy, deep copy
import math
from cv2 import CHAIN_APPROX_NONE, RETR_LIST, imshow, merge, findContours, waitKey
from skimage import io, color
import numpy as np
from tqdm import trange
from tqdm import tqdm
from sklearn.cluster import KMeans

class Cluster(object):
    cluster_index = 1

    def __init__(self, h, w, l=0, a=0, b=0):
        self. update(h, w, l, a, b)
        self.pixels = []
        self.no = self.cluster_index
        self.label = 0
        Cluster.cluster_index += 1

    def update(self, h, w, l, a, b):
        self.h = h
        self.w = w
        self.l = l
        self.a = a
        self.b = b

    def __str__(self):
        return "{},{}:{} {} {} ".format(self.h, self.w, self.l, self.a, self.b)

    def __repr__(self):
        return self.__str__()


class SLICProcessor(object):
    @staticmethod
    def open_image(path):
        """
        return:
            3D array, row col [LAB]
        """
        rgb = io.imread(path)
        if path[-3:] == 'png':
            lab_arr = color.rgb2lab(rgb[:, :, 0:3])
        else:
            lab_arr = color.rgb2lab(rgb)
        return lab_arr

    @staticmethod
    def save_lab_image(path, lab_arr):
        """
        Convert the array to RBG, then save the image
        :param path:
        :param lab_arr:
        :return:
        """
        rgb_arr = color.lab2rgb(lab_arr)
        io.imsave(path, rgb_arr)

    def make_cluster(self, h, w):
        h = int(h)
        w = int(w)
        return Cluster(h, w,
                       self.data[h][w][0],
                       self.data[h][w][1],
                       self.data[h][w][2])

    def __init__(self, filename, K, M):
        self.K = K
        self.M = M

        self.data = self.open_image(filename)
        self.image_height = self.data.shape[0]
        self.image_width = self.data.shape[1]
        self.N = self.image_height * self.image_width
        self.S = int(math.sqrt(self.N / self.K))

        self.clusters = []
        self. label = {}
        self.dis = np.full((self.image_height, self.image_width), np.inf)
        

    def init_clusters(self):
        h = self.S // 2
        w = self.S // 2
        while h < self. image_height:
            while w < self. image_width:
                self. clusters. append(self. make_cluster(h, w))
                w + = self.S
            w = self.S // 2
            h + = self.S

    def get_gradient(self, h, w):
        if w + 1 >= self. image_width:
            w = self. image_width - 2
        if h + 1 >= self. image_height:
            h = self. image_height - 2

        gradient = self.data[h + 1][w + 1][0] - self.data[h][w][0] + \
                   self.data[h + 1][w + 1][1] - self.data[h][w][1] + \
                   self.data[h+1][w+1][2] - self.data[h][w][2]
        return gradient

    def move_clusters(self):
        for cluster in self. clusters:
            cluster_gradient = self. get_gradient(cluster. h, cluster. w)
            for dh in range(-1, 2):
                for dw in range(-1, 2):
                    _h = cluster.h + dh
                    _w = cluster.w + dw
                    new_gradient = self. get_gradient(_h, _w)
                    if new_gradient < cluster_gradient:
                        cluster.update(_h, _w, self.data[_h][_w][0], self.data[_h][_w][1], self.data[_h][_w][2])
                        cluster_gradient = new_gradient

    def assignment(self):
        for cluster in tqdm(self.clusters):
            for h in range(cluster.h - 2 * self.S, cluster.h + 2 * self.S):
                if h < 0 or h >= self. image_height: continue
                for w in range(cluster.w - 2 * self.S, cluster.w + 2 * self.S):
                    if w < 0 or w >= self. image_width: continue
                    L, A, B = self. data[h][w]
                    Dc = math. sqrt(
                        math.pow(L - cluster.l, 2) +
                        math.pow(A - cluster.a, 2) +
                        math.pow(B - cluster.b, 2))
                    Ds = math. sqrt(
                        math.pow(h - cluster.h, 2) +
                        math.pow(w - cluster.w, 2))
                    D = math.sqrt(math.pow(Dc / self.M, 2) + math.pow(Ds / self.S, 2))
                    if D < self.dis[h][w]:
                        if (h, w) not in self.label:
                            self. label[(h, w)] = cluster
                            cluster.pixels.append((h,w))
                        else:
                            self. label[(h, w)]. pixels. remove((h, w))
                            self. label[(h, w)] = cluster
                            cluster.pixels.append((h,w))
                        self.dis[h][w] = D
        a = 1

    def update_cluster(self):
        for cluster in self. clusters:
            sum_h = sum_w = number = 0
            for p in cluster.pixels:
                sum_h + = p[0]
                sum_w + = p[1]
                number + = 1
            _h = int(sum_h / number)
            _w = int(sum_w / number)
            cluster.update(_h, _w, self.data[_h][_w][0], self.data[_h][_w][1], self.data[_h][_w][2])

    def save_current_image(self, name):
        image_arr = np. copy(self. data)
        
        
        # imshow("r", image_arr)
        for cluster in self. clusters:
            for p in cluster.pixels:
                image_arr[p[0]][p[1]][0] = cluster.l
                image_arr[p[0]][p[1]][1] = cluster.a
                image_arr[p[0]][p[1]][2] = cluster.b
            image_arr[cluster.h][cluster.w][0] = 0
            image_arr[cluster.h][cluster.w][1] = 0
            image_arr[cluster.h][cluster.w][2] = 0
        self.save_lab_image("1.png", image_arr)
        mask_r = np.ones((self.image_height, self.image_width), np.uint8)*0
        for cluster in self. clusters:
            mask = np.ones((self.image_height, self.image_width), np.uint8)*0
            for x in cluster.pixels:
                mask[x[0],x[1]] = 255
            contours, _ = findContours(mask, RETR_LIST, CHAIN_APPROX_NONE)
            for contour in contours:
                for i in contour:
                    mask_r[i[0][1], i[0][0]] = 255
        for x in range(self. image_height):
            for y in range(self. image_width):
                if mask_r[x, y] == 255:
                    image_arr[x, y] = [100, 0, 0]
        self.save_lab_image("2.png", image_arr)

    def iterate_10times(self):
        self.init_clusters()
        self. move_clusters()
        # for i in strange(10):
        self. assignment()
        self. update_cluster()
        # name = 'lenna_M{m}_K{k}_loop{loop}.png'.format(loop=0, m=self.M, k=self.K)
        self. save_current_image("123")

    def generate_result(self, K):
        clusters = deepcopy(self. clusters)
        temp_img = [[x.l, x.a, x.b] for x in clusters]
        kmeans = KMeans(n_clusters=K,random_state=3).fit(temp_img)
        for i in range(len(self. clusters)):
            self.clusters[i].label = kmeans.labels_[i]

        
        mask = np.ones((self.image_height, self.image_width))
        img = merge([mask, mask, mask])
        for cluster in self. clusters:
            for pixel in cluster.pixels:
                img[pixel[0], pixel[1]] = kmeans.cluster_centers_[cluster.label]

        for num in range(K):
            mask = np.ones((self.image_height, self.image_width), np.uint8)*0
            for cluster in self. clusters:
                if cluster.label == num:
                    for pixel in cluster.pixels:
                        mask[pixel[0], pixel[1]] = 255
            contours, _ = findContours(mask, RETR_LIST, CHAIN_APPROX_NONE)
            for contour in contours:
                # if len(contour)<200: continue#You can comment out this line to get the segmentation of small areas
                for i in contour:
                   img[i[0][1], i[0][0]] = [100, 0, 0]
                   self.data[i[0][1], i[0][0]] = [100, 0, 0]
        
        
        self.save_lab_image("3.png", img)
        self.save_lab_image("4.png", self.data)
        



if __name__ == '__main__':
    p = SLICProcessor('5.png', 500, 40)
    p.iterate_10times()
    p. generate_result(4)
    waitKey()

slic function in python:

def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0,
         spacing=None, multichannel=True, convert2lab=None,
         enforce_connectivity=True, min_size_factor=0.5, max_size_factor=3,
         slic_zero=False):
    """Segments image using k-means clustering in Color-(x,y,z) space.
    Parameters
    ----------
    image : 2D, 3D or 4D ndarray
        Input image, which can be 2D or 3D, and grayscale or multichannel
        (see `multichannel` parameter).
    n_segments : int, optional
        The (approximate) number of labels in the segmented output image.
    compactness : float, optional
        Control the balance between color and space, about the height of the square, and the close relationship with the picture, it is best to determine the index level first, and then fine-tune
        Balances color proximity and space proximity. Higher values give
        more weight to space proximity, making superpixel shapes more
        square/cubic. In SLICO mode, this is the initial compactness.
        This parameter depends strongly on image contrast and on the
        shapes of objects in the image. We recommend exploring possible
        values on a log scale, e.g., 0.01, 0.1, 1, 10, 100, before
        Refining around a chosen value.
    max_iter : int, optional
        Maximum number of k-means iterations
        Maximum number of iterations of k-means.
    sigma : float or (3,) array-like of floats, optional
        Gaussian smoothing kernel width when preprocessing each dimension of the image. If given as a scalar value, the same value is applied to each dimension. 0 means
        Wearing is not smooth. If 'sigma' is scalar and manual voxel spacing is provided, it is automatically scaled (see Notes section).
        Width of Gaussian smoothing kernel for pre-processing for each
        dimension of the image. The same sigma is applied to each dimension in
        case of a scalar value. Zero means no smoothing.
        Note, that `sigma` is automatically scaled if it is scalar and a
        manual voxel spacing is provided (see Notes section).
    spacing : (3,) array-like of floats, optional
        Represents the voxel space along each dimension of the image. By default, slic assumes a homogeneous space (same voxel resolution along x, y, z axes)
        Rate), this parameter controls the weight of each axis distance in k-means clustering
        The voxel spacing along each image dimension. By default, `slic`
        assumes uniform spacing (same voxel resolution along z, y and x).
        This parameter controls the weights of the distances along z, y,
        and x during k-means clustering.
    multichannel : bool, optional
        Binary parameter indicating whether the last axis of the image represents multi-channel or another spatial dimension
        Whether the last axis of the image is to be interpreted as multiple
        channels or another spatial dimension.
    convert2lab : bool, optional
        Binary parameter, judging that the input needs to be transferred to the LAB color space before segmentation. Input must be RGB. When the multi-channel parameter is True,
        When the number of channels of the input image is 3, this parameter defaults to True
        Whether the input should be converted to Lab colorspace prior to
        segmentation. The input image *must* be RGB. Highly recommended.
        This option defaults to ``True`` when ``multichannel=True`` *and*
        ``image.shape[-1] == 3``.
    enforce_connectivity: bool, optional
        Binary parameter that controls whether the generated split blocks are connected or not
        Whether the generated segments are connected or not
    min_size_factor: float, optional
        The minimum segmentation block ratio to be deleted related to the number of segmentation targets, (probably less than the length * width * height / target number of segmentation results will be fused
        close)
        Proportion of the minimum segment size to be removed with respect
        to the supposed segment size ```depth*width*height/n_segments```
    max_size_factor: float, optional
        Maximum Fusion Ratio Cap
        Proportion of the maximum connected segment size. A value of 3 works
        in most of the cases.
    slic_zero: bool, optional
        I don't know the so-called zero parameter
        Run SLIC-zero, the zero-parameter mode of SLIC. [2]_
    returns
    -------
    labels: 2D or 3D array
        Integer mask indicating segment labels.
    raises
    ------
    ValueError
        If ``convert2lab`` is set to ``True`` but the last array
        dimension is not of length 3.
    notes
    -----
    * If `sigma > 0`, the image is smoothed using a Gaussian kernel prior to
      segmentation.
    * If `sigma` is scalar and `spacing` is provided, the kernel width is
      divided along each dimension by the spacing. For example, if ``sigma=1``
      and ``spacing=[5, 1, 1]``, the effective `sigma` is ``[0.2, 1, 1]``. This
      ensures sensible smoothing for anisotropic images.
      If there is a smoothing parameter sigma and a voxel space parameter spacing, then the spatial voxel parameter will have an equal impact on the smoothing parameter, such as
      1/[5,1,1]=[0.2,1,1]
    * The image is rescaled to be in [0, 1] prior to processing.
      The image will be processed as a scalar between [0,1] before preprocessing
    * Images of shape (M, N, 3) are interpreted as 2D RGB images by default. To
      interpret them as 3D with the last dimension having length 3, use
      `multichannel=False`.
      (M, N, 3) images are 2-dimensional by default (RGB images). To be understood as 3-dimensional images, you need to set the multi-channel parameter = False
    References
    ----------
    .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi,
        Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to
        State-of-the-art Superpixel Methods, TPAMI, May 2012.
    .. [2] http://ivrg.epfl.ch/research/superpixels#SLICO
    Examples
    --------
    >>> from skimage.segmentation import slice
    >>> from skimage.data import astronaut
    >>> img = astronaut()
    >>> segments = slic(img, n_segments=100, compactness=10)
    Increasing the compactness parameter yields more square regions:
    >>> segments = slic(img, n_segments=100, compactness=20)
    """
    ##############################################################################################################################
 
    image = img_as_float(image)
    is_2d = False
    #2D grayscale image
    if image.ndim == 2:
        # 2D grayscale image
        image = image[np.newaxis, ..., np.newaxis]
        is_2d = True
 
    #Such as 2D RGB image
    elif image.ndim == 3 and multichannel:
        # Make 2D multichannel image 3D with depth = 1
        image = image[np. newaxis, ...]
        is_2d = True
 
 
    # such as 3D map
    elif image.ndim == 3 and not multichannel:
        # Add channel as single last dimension
        image = image[..., np. newaxis]
 
    # Control the weight of each axis when clustering
    if spacing is None:
        spacing = np.ones(3)
    elif isinstance(spacing, (list, tuple)):
        spacing = np. array(spacing, dtype=np. double)
 
    # Gaussian smoothing
    if not isinstance(sigma, coll. Iterable):
        sigma = np. array([sigma, sigma, sigma], dtype=np. double)
        sigma /= spacing.astype(np.double)#possible voxel division
    elif isinstance(sigma, (list, tuple)):
        sigma = np. array(sigma, dtype=np. double)
 
    #Gaussian filter
    if (sigma > 0).any():
        # add zero smoothing for multichannel dimension
        sigma = list(sigma) + [0]
        image = ndi.gaussian_filter(image, sigma)
 
    #Multi-channel RGB image and need to transfer to lab, it can be realized with rab2lab
    if multichannel and (convert2lab or convert2lab is None):
        if image.shape[-1] != 3 and convert2lab:
            raise ValueError("Lab colorspace conversion requires a RGB image.")
        elif image.shape[-1] == 3:
            image = rgb2lab(image)
 
    depth, height, width = image. shape[:3]
 
    # initialize cluster centroids for desired number of segments
    #In order to achieve the target number of split blocks, initialize the clustering center.
    #grid_* is equivalent to index
    #slices are blocks divided according to the target quantity, rounding is required
    grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width]
    slices = regular_grid(image. shape[:3], n_segments)
    step_z, step_y, step_x = [int(s.step if s.step is not None else 1)
                              for s in slices]
    segments_z = grid_z[slices]
    segments_y = grid_y[slices]
    segments_x = grid_x[slices]
 
    segments_color = np.zeros(segments_z.shape + (image.shape[3],))
    segments = np. concatenate([segments_z[..., np. newaxis],
                               segments_y[..., np. newaxis],
                               segments_x[..., np. newaxis],
                               segments_color],
                              axis=-1).reshape(-1, 3 + image.shape[3])
    segments = np.ascontiguousarray(segments)
 
    # we do the scaling of ratio in the same way as in the SLIC paper
    # so the values have the same meaning
    step = float(max((step_z, step_y, step_x)))
    ratio = 1.0 / compactness
 
    #Let me go, the show operation that is not square when splitting
    image = np.ascontiguousarray(image * ratio)
 
    labels = _slic_cython(image, segments, step, max_iter, spacing, slic_zero)
 
    #Treat the too small and too small
    if enforce_connectivity:
        segment_size = depth * height * width / n_segments
        min_size = int(min_size_factor * segment_size)
        max_size = int(max_size_factor * segment_size)
        labels = _enforce_label_connectivity_cython(labels,
                                                    min_size,
                                                    max_size)
 
    if is_2d:
        labels = labels[0]
 
    return labels

Reference: csdn