diff --git a/docs/segmentation.rst b/docs/segmentation.rst index 6c0c186..b0bd61b 100644 --- a/docs/segmentation.rst +++ b/docs/segmentation.rst @@ -148,7 +148,7 @@ This can be done in multiple colours, by selecting the desired colour before lab Different filter kernels and classifiers can be chosen using the dropdowns and tick boxes. The filter kernel parameters can also be altered using the Sigma, High Sigma and Disk Size parameters. -Once areas have been labelled, training can be begun using the 'Update' and 'Train Classifier' Button. +Once areas have been labelled, training can begin using the 'Train Classifier' Button. When this finishes the labels will be shown on top of the image in the user interface, this can take up to several minutes. The classifier can then be retrained with additional pixels by by clearing the canvas of the displayed segmentation with `Clear Canvas`, and redrawing previous training labels with `redraw training labels`. @@ -182,9 +182,9 @@ This is shown in the example below: .. code-block:: python - from PIL import Image import numpy as np from sklearn.naive_bayes import GaussianNB + import hyperspy.api as hs import particlespy.api as ps image = hs.load("image path") @@ -195,5 +195,5 @@ This is shown in the example below: _, clf = ps.cluster_trained(image, mask, clf) This classifier can then be used to segment images. -The function ``ps.toggle_channels`` is used to convert an RGB image into a 2D indexed array of labels. +The function ``ps.toggle_channels`` is used to convert an RGB image into a 1 channel 2D indexed array of labels. This can also be used to convert the output of the ``classifier_segment`` into RGB images which can be exported. diff --git a/particlespy/custom_kernels.py b/particlespy/custom_kernels.py index 482738b..826f482 100644 --- a/particlespy/custom_kernels.py +++ b/particlespy/custom_kernels.py @@ -6,10 +6,11 @@ """ import numpy as np -from skimage.transform import rotate -from skimage import filters from scipy.ndimage import convolve +from skimage import filters from skimage.exposure import rescale_intensity +from skimage.transform import rotate + def membrane_projection(image): """ @@ -23,7 +24,8 @@ def membrane_projection(image): Returns ------- - 6 features for + feature_stack: Numy Array + 6 membrane projection features """ kernel = np.zeros([19,19]) @@ -46,7 +48,35 @@ def membrane_projection(image): return feature_stack def laplacian(image): + """ + creates the laplacian feature + + Parameters + ---------- + image : greyscale image for feature creation. + + Returns + ------- + feature_stack: Numpy Array + 6 membrane projection features + + """ kernel = np.array([[0,-1,0],[-1,4,-1],[0,-1,0]]) + return custom_kernel(kernel=kernel) + +def custom_kernel(image, kernel): + """ + convolves an image with a custom kernel + + Parameters + ---------- + image : greyscale image for feature creation. + + Returns + ------- + feature_stack: Numpy Array + 6 membrane projection features + """ convolved = convolve(image,kernel) return convolved diff --git a/particlespy/particle_analysis.py b/particlespy/particle_analysis.py index d25051b..35fba61 100644 --- a/particlespy/particle_analysis.py +++ b/particlespy/particle_analysis.py @@ -5,21 +5,24 @@ @author: qzo13262 """ -from particlespy.segptcls import process +import inspect +import os +import warnings + +import h5py import numpy as np -from particlespy.ptcl_class import particle, particle_list -from particlespy.custom_kernels import membrane_projection +import pandas as pd +import trackpy from skimage import filters, morphology -from skimage.measure import label, regionprops, perimeter +from skimage.measure import label, perimeter, regionprops from sklearn import preprocessing from sklearn.cluster import DBSCAN, KMeans + import particlespy.find_zoneaxis as zone -import warnings -import h5py -import inspect -import pandas as pd -import trackpy -import os +from particlespy.custom_kernels import membrane_projection +from particlespy.ptcl_class import particle, particle_list +from particlespy.segptcls import process + def particle_analysis(acquisition,parameters,particles=None,mask=np.zeros((1))): """ @@ -27,8 +30,8 @@ def particle_analysis(acquisition,parameters,particles=None,mask=np.zeros((1))): Parameters ---------- - acquisition: Hyperpsy signal object or list of hyperspy signal objects. - Hyperpsy signal object containing a nanoparticle image or a list of signal + acquisition: Hyperspy signal object or list of hyperspy signal objects. + Hyperspy signal object containing a nanoparticle image or a list of signal objects that contains an image at position 0 and other datasets following. parameters: Dictionary of parameters The parameters can be input manually in to a dictionary or can be generated @@ -391,7 +394,8 @@ def __init__(self,gaussian = [True,1], diff_gaussian = [True,[False,1],1,16], median = [True,[False,1],20], minimum = [True,[False,1],20], maximum = [True,[False,1],20], sobel = [True,[True,1]], hessian = [False,[False,1]], laplacian = [False,[False,1]], - membrane = [[False,1],True,False,False,False,False,False]): + membrane = [[False,1],True,False,False,False,False,False], + custom = [False,[False,1],[[0,0,0],[0,1,0],[0,0,0]]]): self.gaussian = gaussian self.diff_gaussian = diff_gaussian self.median = median @@ -401,6 +405,7 @@ def __init__(self,gaussian = [True,1], diff_gaussian = [True,[False,1],1,16], self.hessian = hessian self.laplacian = laplacian self.membrane = membrane + self.custom = custom def set_global_sigma(self, sigma): self.gaussian[1] = sigma @@ -426,7 +431,8 @@ def set_global_prefilter(self, sigma): self.sobel[1] = [True,sigma] self.hessian[1] = [True,sigma] self.laplacian[1] = [True,sigma] - self.membrane[0] = [True,sigma] + self.membrane[0] = [True,sigma] + self.custom[1] = [True,sigma] def set_gaussian(self,enabled = True, sigma = 1): self.gaussian=[enabled,sigma] @@ -452,5 +458,8 @@ def set_hessian(self,enabled = True, prefilter=True, prefilter_sigma = 1): def set_laplacian(self,enabled = True, prefilter=True, prefilter_sigma = 1): self.laplacian=[enabled,[prefilter,prefilter_sigma]] - def set_membrane(self,enabled = True, prefilter=True, prefilter_sigma = 1,summ = True,mean = True, stddev = True, maximum = True, minimum = True, median = True): + def set_membrane(self, prefilter=True, prefilter_sigma = 1,summ = True,mean = True, stddev = True, maximum = True, minimum = True, median = True): self.membrane=[[prefilter, prefilter_sigma], summ, mean, stddev, maximum, minimum, median] + + def set_custom(self, enabled=True, prefilter = False, prefilter_sigma = 1, conv_matrix=[[0,0,0],[0,1,0],[0,0,0]]): + self.custom = [enabled,[prefilter,prefilter_sigma],conv_matrix] diff --git a/particlespy/seg_ui.py b/particlespy/seg_ui.py index 6302acf..7324950 100644 --- a/particlespy/seg_ui.py +++ b/particlespy/seg_ui.py @@ -225,8 +225,6 @@ def __init__(self,im_hs,height): im_lay = QVBoxLayout() self.button_lay = QVBoxLayout() - self.button_lay.setSpacing(0) - self.button_lay.setContentsMargins(0,0,0,0) lay3.addLayout(self.button_lay) lay3.addLayout(im_lay) @@ -258,20 +256,17 @@ def __init__(self,im_hs,height): self.button_lay.addLayout(self.colour_lay) im_lay.addWidget(self.canvas2) - self.kerneltxt = QLabel(self) - self.kerneltxt.setText('Classifier') - self.button_lay.addWidget(self.kerneltxt) - - self.clfBox = QComboBox(self) - self.clfBox.addItem("Random Forest") - self.clfBox.addItem("Nearest Neighbours") - self.clfBox.addItem("Naive Bayes") - self.clfBox.addItem("QDA") - self.clfBox.activated[str].connect(self.classifier_choice) - self.button_lay.addWidget(self.clfBox) + + + + + + + fk_lay = QVBoxLayout(self) + fk_lay.setVerticalSpacing(0) self.kerneltxt = QLabel(self) self.kerneltxt.setText('Filter Kernels') fk_lay.addWidget(self.kerneltxt) @@ -295,6 +290,25 @@ def __init__(self,im_hs,height): self.button_lay.addLayout(fk_lay) + + self.clf_lay = QVBoxLayout() + self.kerneltxt = QLabel(self) + self.kerneltxt.setText('Classifier') + self.clf_lay.addWidget(self.kerneltxt) + + self.clfBox = QComboBox(self) + self.clfBox.addItem("Random Forest") + self.clfBox.addItem("Nearest Neighbours") + self.clfBox.addItem("Naive Bayes") + self.clfBox.addItem("QDA") + self.clfBox.activated[str].connect(self.classifier_choice) + self.clf_lay.addWidget(self.clfBox) + + self.button_lay.addLayout(self.clf_lay) + + + + fkp_lay = QVBoxLayout() self.ql1 = QLabel(self) self.ql1.setText('Sigma') diff --git a/particlespy/segimgs.py b/particlespy/segimgs.py index 8c91738..7c84adf 100644 --- a/particlespy/segimgs.py +++ b/particlespy/segimgs.py @@ -5,7 +5,8 @@ from skimage.measure import label, perimeter, regionprops from sklearn import preprocessing -from particlespy.custom_kernels import laplacian, membrane_projection +from particlespy.custom_kernels import (custom_kernel, laplacian, + membrane_projection) from particlespy.particle_analysis import trainable_parameters @@ -17,6 +18,8 @@ def create_features(image, parameters=None): ---------- image : greyscale image for segmentation trainable segmentation parameters + + parameters : trainable parameters object for setting filter kernels and their parameters Returns ------- @@ -90,11 +93,10 @@ def create_features(image, parameters=None): if parameters.laplacian[0]: par = parameters.laplacian + blur = image if par[1][0]: blur = filters.gaussian(image,par[1][1]) - new_layer = np.reshape(laplacian(blur),shape) - else: - new_layer = np.reshape(filters.laplacian(image),shape) + new_layer = np.reshape(laplacian(blur),shape) image_stack = np.concatenate((image_stack, new_layer), axis=2) if True in parameters.membrane[1:]: @@ -110,6 +112,14 @@ def create_features(image, parameters=None): mem_layers = np.squeeze(mem_layers) image_stack = np.append(image_stack, mem_layers, axis=2) + if parameters.custom[0]: + par = parameters.custom + blur = image + if par[1][0]: + blur = filters.gaussian(image,par[1][1]) + new_layer = np.reshape(custom_kernel(blur,np.asarray(par[2])), shape) + image_stack = np.append(image_stack, new_layer, axis=2) + return image_stack[:,:,1:] def cluster_learn(images, clust, parameters = None): @@ -253,7 +263,7 @@ def classifier_segment(classifier, image, parameters = None): Returns ------- - mask of labels (1channel) + mask of labels (1 channel, indexed) """ features = create_features(image, parameters=parameters) features = np.rot90(np.rot90(features, axes=(2,0)), axes=(1,2)) @@ -267,6 +277,19 @@ def classifier_segment(classifier, image, parameters = None): def toggle_channels(image, colors = ['#A30015', '#6DA34D', '#51E5FF', '#BD2D87', '#F5E663']): + """ + changes a 3 channel RGB image into a 1 channel indexed image, and vice versa + + Parameters + ---------- + image : 1/3 channel image for conversion + colors : ordered list of colors to index labels by + + Returns + ---------- + toggled 1/3 channel image + + """ #colors are in RGB format shape = image.shape @@ -285,12 +308,43 @@ def toggle_channels(image, colors = ['#A30015', '#6DA34D', '#51E5FF', '#BD2D87', return toggled def remove_large_objects(ar, max_size=200, connectivity=1, in_place=False): + """Remove objects larger than the specified size. + Expects ar to be an array with labeled objects, and removes objects + larger than max_size. If `ar` is bool, the image is first labeled. + This leads to potentially different behavior for bool and 0-and-1 + arrays. + + Parameters + ---------- + ar : ndarray (arbitrary shape, int or bool type) + The array containing the objects of interest. If the array type is + int, the ints must be non-negative. + max_size : int, optional (default: 200) + The largest allowable object size. + connectivity : int, {1, 2, ..., ar.ndim}, optional (default: 1) + The connectivity defining the neighborhood of a pixel. Used during + labelling if `ar` is bool. + in_place : bool, optional (default: False) + If ``True``, remove the objects in the input array itself. + Otherwise, make a copy. + + Raises + ------ + TypeError + If the input array is of an invalid type, such as float or string. + ValueError + If the input array contains negative values. + + Returns + ------- + out : ndarray, same shape and type as input `ar` + The input array with small connected components removed. # Raising type error if not int or bool if not (ar.dtype == bool or np.issubdtype(ar.dtype, np.integer)): raise TypeError("Only bool or integer image types are supported. " "Got %s." % ar.dtype) - + """ if in_place: out = ar else: