diff --git a/docs/assets/screenshots/GUI-layers.png b/docs/assets/screenshots/GUI-layers.png
new file mode 100644
index 0000000000000000000000000000000000000000..0285fa99cca778942b5665e8f252c94096a8d7fb
Binary files /dev/null and b/docs/assets/screenshots/GUI-layers.png differ
diff --git a/docs/assets/screenshots/layers.png b/docs/assets/screenshots/layers.png
new file mode 100644
index 0000000000000000000000000000000000000000..0715964a369d4f40cd198b58bf7cc815dda3ecb5
Binary files /dev/null and b/docs/assets/screenshots/layers.png differ
diff --git a/docs/assets/screenshots/segmented_layers.png b/docs/assets/screenshots/segmented_layers.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f9a261aed7b11514ce1d3f3c56900f8152a0a75
Binary files /dev/null and b/docs/assets/screenshots/segmented_layers.png differ
diff --git a/docs/cli.md b/docs/cli.md
index a8d1d42e681b5b990b8d89060324c022115511ef..502ba65ca5b33424260890cb443f9c889948d264 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -30,6 +30,8 @@ This offers quick interactions, making it ideal for tasks that require efficienc
 | `--data-explorer` | Starts the Data Explorer |
 | `--iso3d` | Starts the 3D Isosurfaces visualization |
 | `--local-thickness` | Starts the Local thickness tool |
+| `--anotation-tool` | Starts the annotation tool |
+| `--layers` | Starts the tool for segmenting layers |
 | `--host` | Desired host for the server. By default runs on `0.0.0.0`  |
 | `--platform` | Uses the Qim platform API for a unique path and port depending on the username |
 
diff --git a/docs/gui.md b/docs/gui.md
index 5401cb85bd40f32e9d74fadfc531c3fa0dec8879..ef996896db91d26a02037f28f288ece44d0892d3 100644
--- a/docs/gui.md
+++ b/docs/gui.md
@@ -34,5 +34,9 @@ For details see [here](cli.md#qim3d-gui).
 ![Iso3d GUI](assets/screenshots/GUI-iso3d.png)
 
 ::: qim3d.gui.annotation_tool
+    options:
+        members: False
+
+::: qim3d.gui.layers2d
     options:
         members: False
\ No newline at end of file
diff --git a/docs/processing.md b/docs/processing.md
index 3ea9a81035699954845a2622d76474c30423d8ea..68535f9cdf20189d5944790e07a82a14677bd368 100644
--- a/docs/processing.md
+++ b/docs/processing.md
@@ -14,6 +14,8 @@ Here, we provide functionalities designed specifically for 3D image analysis and
             - maximum
             - minimum
             - tophat
+            - get_lines
+            - segment_layers
             - create_mesh
 
 ::: qim3d.processing.Pipeline
diff --git a/qim3d/cli.py b/qim3d/cli.py
index 672bbaafa8ac35fba38f8150d69a2bb68f66cc21..401c8b84cc3620304f4c95853316c3201237cf4d 100644
--- a/qim3d/cli.py
+++ b/qim3d/cli.py
@@ -34,6 +34,9 @@ def main():
     gui_parser.add_argument(
         "--local-thickness", action="store_true", help="Run local thickness tool."
     )
+    gui_parser.add_argument(
+        "--layers", action="store_true", help="Run Layers."
+    )
     gui_parser.add_argument("--host", default="0.0.0.0", help="Desired host.")
     gui_parser.add_argument(
         "--platform", action="store_true", help="Use QIM platform address"
@@ -137,6 +140,8 @@ def main():
             interface_class = qim3d.gui.annotation_tool.Interface
         elif args.local_thickness:
             interface_class = qim3d.gui.local_thickness.Interface
+        elif args.layers:
+            interface_class = qim3d.gui.layers2d.Interface
         else:
             print(
                 "Please select a tool by choosing one of the following flags:\n\t--data-explorer\n\t--iso3d\n\t--annotation-tool\n\t--local-thickness"
diff --git a/qim3d/examples/slice_218x193.png b/qim3d/examples/slice_218x193.png
new file mode 100644
index 0000000000000000000000000000000000000000..f26c876dc030007e0d1129b42db3fa46470f3caa
Binary files /dev/null and b/qim3d/examples/slice_218x193.png differ
diff --git a/qim3d/gui/__init__.py b/qim3d/gui/__init__.py
index f1f7591b8a47e33c4ba90f391fcefc4325f5d0d7..cc7c9ff40272df922cf79a315cf8729e98f6a858 100644
--- a/qim3d/gui/__init__.py
+++ b/qim3d/gui/__init__.py
@@ -4,6 +4,7 @@ from . import data_explorer
 from . import iso3d
 from . import local_thickness
 from . import annotation_tool
+from . import layers2d
 from .qim_theme import QimTheme
 
 
diff --git a/qim3d/gui/interface.py b/qim3d/gui/interface.py
index 78830582e052d4a72f303530a48bfc522bca88aa..d528d2153e4b5a9f535d850d7a3f86c767119e90 100644
--- a/qim3d/gui/interface.py
+++ b/qim3d/gui/interface.py
@@ -47,6 +47,9 @@ class BaseInterface(ABC):
 
     def set_invisible(self):
         return gr.update(visible=False)
+    
+    def change_visibility(self, is_visible):
+        return gr.update(visible = is_visible)
 
     def launch(self, img=None, force_light_mode: bool = True, **kwargs):
         """
diff --git a/qim3d/gui/layers2d.py b/qim3d/gui/layers2d.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf106dedeb8f47092ce21b90380161f624a1c7bf
--- /dev/null
+++ b/qim3d/gui/layers2d.py
@@ -0,0 +1,475 @@
+"""
+The GUI can be launched directly from the command line:
+
+```bash
+qim3d gui --layers
+```
+
+Or launched from a python script
+
+```python
+import qim3d
+
+layers = qim3d.gui.layers2d.Interface()
+app = layers.launch()
+```
+![gui-layers](assets/screenshots/GUI-layers.png)
+
+"""
+
+import os
+
+import gradio as gr
+import numpy as np
+
+from .interface import BaseInterface
+
+# from qim3d.processing import layers2d as l2d
+from qim3d.processing import overlay_rgb_images, segment_layers, get_lines
+from qim3d.io import load
+from qim3d.viz.layers2d import image_with_lines
+
+#TODO figure out how not update anything and go through processing when there are no data loaded
+# So user could play with the widgets but it doesnt throw error
+# Right now its only bypassed with several if statements
+# I opened an issue here https://github.com/gradio-app/gradio/issues/9273
+
+X = 'X'
+Y = 'Y'
+Z = 'Z'
+AXES = {X:2, Y:1, Z:0}
+
+DEFAULT_PLOT_TYPE = 'Segmentation mask'
+SEGMENTATION_COLORS = np.array([[0, 255, 255], # Cyan
+                                [255, 195, 0], # Yellow Orange
+                                [199, 0, 57], # Dark orange
+                                [218, 247, 166], # Light green
+                                [255, 0, 255], # Magenta
+                                [65, 105, 225], # Royal blue
+                                [138, 43, 226], # Blue violet
+                                [255, 0, 0], #Red
+                                ])
+
+class Interface(BaseInterface):
+    def __init__(self):
+        super().__init__("Layered surfaces 2D", 1080)
+
+        self.data = None
+        # It important to keep the name of the attributes like this (including the capital letter) becuase of
+        # accessing the attributes via __dict__
+        self.X_slice = None
+        self.Y_slice = None
+        self.Z_slice = None
+        self.X_segmentation = None
+        self.Y_segmentation = None
+        self.Z_segmentation = None
+
+        self.plot_type = DEFAULT_PLOT_TYPE
+
+        self.error = False
+
+
+
+    def define_interface(self):
+        with gr.Row():
+            with gr.Column(scale=1, min_width=320):
+                with gr.Row():
+                    with gr.Column(scale=99, min_width=128):
+                        base_path = gr.Textbox(
+                            max_lines=1,
+                            container=False,
+                            label="Base path",
+                            value=os.getcwd(),
+                        )
+
+                    with gr.Column(scale=1, min_width=36):
+                        reload_base_path = gr.Button(value="⟳")
+
+                explorer = gr.FileExplorer(
+                    ignore_glob="*/.*",
+                    root_dir=os.getcwd(),
+                    label=os.getcwd(),
+                    render=True,
+                    file_count="single",
+                    interactive=True,
+                    height = 230,
+                )
+
+                        
+                with gr.Group():
+                    with gr.Row():
+                        axis = gr.Radio(
+                            choices = [Z, Y, X],
+                            value = Z, 
+                            label = 'Layer axis',
+                            info = 'Specifies in which direction are the layers. The order of axes is ZYX',)
+                    with gr.Row():
+                        wrap = gr.Checkbox(
+                            label = "Lines start and end at the same level.",
+                            info = "Used when segmenting layers of unfolded image."
+                        )
+                        
+                        is_inverted = gr.Checkbox(
+                            label="Invert image before processing",
+                            info="The algorithm effectively flips the gradient.",
+                        ) 
+                    
+                    with gr.Row():
+                        delta = gr.Slider(
+                            minimum=0,
+                            maximum=5,
+                            value=0.75,
+                            step=0.01,
+                            interactive = True,
+                            label="Delta value",
+                            info="The lower the delta is, the more accurate the gradient calculation will be. However, the calculation takes longer to execute. Delta above 1 is rounded down to closest lower integer", 
+                        )
+                        
+                    with gr.Row():
+                        min_margin = gr.Slider(
+                            minimum=1, 
+                            maximum=50, 
+                            value=10, 
+                            step=1, 
+                            interactive = True,
+                            label="Min margin",
+                            info="Minimum margin between layers to be detected in the image.",
+                        )
+
+                    with gr.Row():
+                        n_layers = gr.Slider(
+                            minimum=1,
+                            maximum=len(SEGMENTATION_COLORS) - 1,
+                            value=2,
+                            step=1,
+                            interactive=True,
+                            label="Number of layers",
+                            info="Number of layers to be detected in the image",
+                        )                 
+
+                # with gr.Row():
+                #     btn_run = gr.Button("Run Layers2D", variant = 'primary')
+
+            # Output panel: Plots
+            """
+            60em if plot is alone
+            30em if two of them
+            20em if all of them are visible
+
+            When one slicing axis is made unvisible we want the other two images to be bigger
+            For some reason, gradio changes their width but not their height. So we have to 
+            change their height manually
+            """
+
+            self.heights = ['60em', '30em', '20em'] # em units are relative to the parent, 
+
+
+            with gr.Column(scale=2,):
+                # with gr.Row(): # Source image outputs
+                #     input_image_kwargs = lambda axis: dict(
+                #         show_label = True,
+                #         label = F'Slice along {axis}-axis', 
+                #         visible = True, 
+                #         height = self.heights[2]
+                #     )
+
+                #     input_plot_x = gr.Image(**input_image_kwargs('X'))
+                #     input_plot_y = gr.Image(**input_image_kwargs('Y'))
+                #     input_plot_z = gr.Image(**input_image_kwargs('Z'))
+
+                with gr.Row(): # Detected layers outputs
+                    output_image_kwargs = lambda axis: dict(
+                        show_label = True,
+                        label = F'Detected layers {axis}-axis',
+                        visible = True,
+                        height = self.heights[2]
+                    )
+                    output_plot_x = gr.Image(**output_image_kwargs('X'))
+                    output_plot_y = gr.Image(**output_image_kwargs('Y'))
+                    output_plot_z = gr.Image(**output_image_kwargs('Z'))
+                    
+                with gr.Row(): # Axis position sliders
+                    slider_kwargs = lambda axis: dict(
+                        minimum = 0,
+                        maximum = 1,
+                        value = 0.5,
+                        step = 0.01,
+                        label = F'{axis} position',
+                        info = F'The 3D image is sliced along {axis}-axis'
+                    )
+                    
+                    x_pos = gr.Slider(**slider_kwargs('X'))                    
+                    y_pos = gr.Slider(**slider_kwargs('Y'))
+                    z_pos = gr.Slider(**slider_kwargs('Z'))
+
+                with gr.Row():
+                    x_check = gr.Checkbox(value = True, interactive=True, label = 'Show X slice')
+                    y_check = gr.Checkbox(value = True, interactive=True, label = 'Show Y slice')
+                    z_check = gr.Checkbox(value = True, interactive=True, label = 'Show Z slice')
+
+                with gr.Row():
+                    with gr.Group():
+                        plot_type = gr.Radio(
+                            choices= (DEFAULT_PLOT_TYPE, 'Segmentation lines',),
+                            value = DEFAULT_PLOT_TYPE,
+                            interactive = True,
+                            show_label=False
+                            )
+                        
+                        alpha = gr.Slider(
+                            minimum=0,
+                            maximum = 1,
+                            step = 0.01,
+                            label = 'Alpha value',
+                            show_label=True,
+                            value = 0.5,
+                            visible = True,
+                            interactive=True
+                            )
+                        
+                        line_thickness = gr.Slider(
+                            minimum=0.1,
+                            maximum = 5,
+                            value = 2,
+                            label = 'Line thickness',
+                            show_label = True,
+                            visible = False,
+                            interactive = True
+                            )
+
+                with gr.Row():
+                    btn_run = gr.Button("Run Layers2D", variant = 'primary')
+
+
+        positions = [x_pos, y_pos, z_pos]
+        process_inputs = [axis, is_inverted, delta, min_margin, n_layers, wrap]
+        plotting_inputs = [axis, alpha, line_thickness]
+        # input_plots = [input_plot_x, input_plot_y, input_plot_z]
+        output_plots = [output_plot_x, output_plot_y, output_plot_z]
+        visibility_check_inputs = [x_check, y_check, z_check]
+
+        spinner_loading = gr.Text("Loading data...", visible=False)
+        spinner_running = gr.Text("Running pipeline...", visible=False)
+
+        reload_base_path.click(
+            fn=self.update_explorer,inputs=base_path, outputs=explorer)
+        
+        plot_type.change(
+            self.change_plot_type, inputs = plot_type, outputs = [alpha, line_thickness]).then(
+            fn = self.plot_output_img_all, inputs = plotting_inputs, outputs = output_plots
+            )
+        
+        gr.on(
+            triggers = [alpha.release, line_thickness.release],
+            fn = self.plot_output_img_all, inputs = plotting_inputs, outputs = output_plots
+        )
+
+        """
+        Difference between btn_run.click and the other triggers below is only loading the data.
+        To make it easier to maintain, I created 'update_component' variable. Its value is completely
+        unimportant. It exists only to be changed after loading the data which triggers further processing
+        which is the same for button click and the other triggers
+        """
+
+        update_component = gr.State(True)
+
+        btn_run.click(
+            fn=self.set_spinner, inputs=spinner_loading, outputs=btn_run).then(
+            fn=self.load_data, inputs = [base_path, explorer]).then(
+            fn = lambda state: not state, inputs = update_component, outputs = update_component)
+        
+        gr.on(
+            triggers= (axis.change, is_inverted.change, delta.release, min_margin.release, n_layers.release, update_component.change, wrap.change),
+            fn=self.set_spinner, inputs = spinner_running, outputs=btn_run).then(
+            fn=self.process_all, inputs = [*positions, *process_inputs]).then(
+            # fn=self.plot_input_img_all, outputs = input_plots, show_progress='hidden').then(
+            fn=self.plot_output_img_all, inputs =  plotting_inputs, outputs = output_plots, show_progress='hidden').then(
+            fn=self.set_relaunch_button, inputs=[], outputs=btn_run)
+        
+        # Chnages visibility and sizes of the plots - gives user the option to see only some of the images and in bigger scale
+        gr.on(
+            triggers=[x_check.change, y_check.change, z_check.change],
+            fn = self.change_row_visibility, inputs = visibility_check_inputs, outputs = positions).then(
+            # fn = self.change_row_visibility, inputs = visibility_check_inputs, outputs = input_plots).then(
+            fn = self.change_plot_size, inputs = visibility_check_inputs, outputs = output_plots)
+        
+        # for  axis, slider, input_plot, output_plot in zip(['x','y','z'], positions, input_plots, output_plots):
+        for  axis, slider, output_plot in zip([X,Y,Z], positions, output_plots):
+            slider.change(
+                self.process_wrapper(axis), inputs = [slider, *process_inputs]).then( 
+                # self.plot_input_img_wrapper(axis), outputs = input_plot).then(
+                self.plot_output_img_wrapper(axis), inputs = plotting_inputs, outputs = output_plot)
+            
+        
+
+    def change_plot_type(self, plot_type, ):
+        self.plot_type = plot_type
+        if plot_type == 'Segmentation lines':
+            return gr.update(visible = False), gr.update(visible = True)
+        else:  
+            return gr.update(visible = True), gr.update(visible = False)
+        
+    def change_plot_size(self, x_check, y_check, z_check):
+        """
+        Based on how many plots are we displaying (controlled by checkboxes in the bottom) we define
+        also their height because gradio doesn't do it automatically. The values of heights were set just by eye.
+        They are defines before defining the plot in 'define_interface'
+        """
+        index = x_check + y_check + z_check - 1
+        height = self.heights[index] # also used to define heights of plots in the begining
+        return gr.update(height = height, visible= x_check), gr.update(height = height, visible = y_check), gr.update(height = height, visible = z_check)
+
+    def change_row_visibility(self, x_check, y_check, z_check):
+        return self.change_visibility(x_check), self.change_visibility(y_check), self.change_visibility(z_check)
+    
+    def update_explorer(self, new_path):
+        # Refresh the file explorer object
+        new_path = os.path.expanduser(new_path)
+
+        # In case we have a directory
+        if os.path.isdir(new_path):
+            return gr.update(root_dir=new_path, label=new_path)
+
+        elif os.path.isfile(new_path):
+            parent_dir = os.path.dirname(new_path)
+            file_name = str(os.path.basename(new_path))
+            return gr.update(root_dir=parent_dir, label=parent_dir, value=file_name)
+
+        else:
+            raise ValueError("Invalid path")
+
+    def set_relaunch_button(self):
+        return gr.update(value=f"Relaunch", interactive=True)
+
+    def set_spinner(self, message):
+        if self.error:
+            return gr.Button()
+        # spinner icon/shows the user something is happeing
+        return gr.update(value=f"{message}", interactive=False)
+    
+    def load_data(self, base_path, explorer):
+        if base_path and os.path.isfile(base_path):
+            file_path = base_path
+        elif explorer and os.path.isfile(explorer):
+            file_path = explorer
+        else:
+            raise gr.Error("Invalid file path")
+
+        try:
+            self.data = load(
+                file_path,
+                progress_bar=False
+            )
+        except Exception as error_message:
+            raise gr.Error(
+                f"Failed to load the image: {error_message}"
+            ) from error_message
+        
+    def process_all(self, x_pos:float, y_pos:float, z_pos:float, axis:str, inverted:bool, delta:float, min_margin:int, n_layers:int, wrap:bool):
+        self.process_wrapper(X)(x_pos, axis, inverted, delta, min_margin, n_layers, wrap)
+        self.process_wrapper(Y)(y_pos, axis, inverted, delta, min_margin, n_layers, wrap)
+        self.process_wrapper(Z)(z_pos, axis, inverted, delta, min_margin, n_layers, wrap)
+
+    def process_wrapper(self, slicing_axis:str):
+        """
+        The function behaves the same in all 3 directions, however we have to know in which direction we are now.
+        Thus we have this wrapper function, where we pass the slicing axis - in which axis are we indexing the data
+            and we return a function working in that direction
+        """
+        slice_key = F'{slicing_axis}_slice'
+        seg_key = F'{slicing_axis}_segmentation'
+        slicing_axis_int = AXES[slicing_axis]
+
+        def process(pos:float, segmenting_axis:str, inverted:bool, delta:float, min_margin:int, n_layers:int, wrap:bool):
+            """
+            Parameters:
+            -----------
+            pos: Relative position of a slice from data
+            segmenting_axis: In which direction we want to detect layers
+            inverted: If we want use inverted gradient
+            delta: Smoothness parameter
+            min_margin: What is the minimum distance between layers. If it was 0, all layers would be the same
+            n_layers: How many layer boarders we want to find
+            wrap: If True, the starting point and end point will be at the same level. Useful when segmenting unfolded images.
+            """
+            slice = self.get_slice(pos, slicing_axis_int)
+            self.__dict__[slice_key] = slice
+
+            if segmenting_axis == slicing_axis:
+                self.__dict__[seg_key] = None
+            else:
+                
+                if self.is_transposed(slicing_axis, segmenting_axis):
+                    slice = np.rot90(slice)
+                self.__dict__[seg_key] = segment_layers(slice, inverted = inverted, n_layers = n_layers, delta = delta, min_margin = min_margin, wrap = wrap)
+        
+        return process
+
+    def is_transposed(self, slicing_axis:str, segmenting_axis:str):
+        """
+        Checks if the desired direction of segmentation is the same if the image would be submitted to segmentation as is. 
+        If it is not, we have to rotate it before we put it to segmentation algorithm
+        """
+        remaining_axis = F"{X}{Y}{Z}".replace(slicing_axis, '').replace(segmenting_axis, '')
+        return AXES[segmenting_axis] > AXES[remaining_axis]
+    
+    def get_slice(self, pos:float, axis:int):
+        idx = int(pos * (self.data.shape[axis] - 1))
+        return np.take(self.data, idx, axis = axis)
+    
+    # def plot_input_img_wrapper(self, axis:str):
+    #     slice_key = F'{axis.lower()}_slice'
+    #     def plot_input_img():
+    #         slice = self.__dict__[slice_key]
+    #         slice = slice + np.abs(np.min(slice))
+    #         slice = slice / np.max(slice)
+    #         return slice
+    #     return plot_input_img
+
+    # def plot_input_img_all(self):
+    #     x_plot = self.plot_input_img_wrapper('x')()
+    #     y_plot = self.plot_input_img_wrapper('y')()
+    #     z_plot = self.plot_input_img_wrapper('z')()
+    #     return x_plot, y_plot, z_plot
+    
+    def plot_output_img_wrapper(self, slicing_axis:str):
+        slice_key = F'{slicing_axis}_slice'
+        seg_key = F'{slicing_axis}_segmentation'
+
+        def plot_output_img(segmenting_axis:str, alpha:float, line_thickness:float):
+            slice = self.__dict__[slice_key]
+            seg = self.__dict__[seg_key]
+
+            if seg is None: # In case segmenting axis si the same as slicing axis
+                return slice
+            
+            if self.plot_type == DEFAULT_PLOT_TYPE:
+                n_layers = len(seg) + 1
+                seg = np.sum(seg, axis = 0)
+                seg = np.repeat(seg[..., None], 3, axis = -1)
+                for i in range(n_layers):
+                    seg[seg[:,:,0] == i, :] = SEGMENTATION_COLORS[i]
+
+                if self.is_transposed(slicing_axis, segmenting_axis):
+                    seg = np.rot90(seg, k = 3)
+                # slice = 255 * (slice/np.max(slice))
+                # return image_with_overlay(np.repeat(slice[..., None], 3, -1), seg, alpha) 
+                return overlay_rgb_images(slice, seg, alpha)
+            else:
+                lines = get_lines(seg)
+                if self.is_transposed(slicing_axis, segmenting_axis):
+                    return image_with_lines(np.rot90(slice), lines, line_thickness).rotate(270, expand = True)
+                else:
+                    return image_with_lines(slice, lines, line_thickness)
+            
+        return plot_output_img
+    
+    def plot_output_img_all(self, segmenting_axis:str, alpha:float, line_thickness:float):
+        x_output = self.plot_output_img_wrapper(X)(segmenting_axis, alpha, line_thickness)
+        y_output = self.plot_output_img_wrapper(Y)(segmenting_axis, alpha, line_thickness)
+        z_output = self.plot_output_img_wrapper(Z)(segmenting_axis, alpha, line_thickness)
+        return x_output, y_output, z_output
+
+if __name__ == "__main__":
+    Interface().run_interface()
+    
\ No newline at end of file
diff --git a/qim3d/processing/__init__.py b/qim3d/processing/__init__.py
index 981bb55dacd3a6e212948ad47533cee3c528dfdf..0b767fb1fe413699d3a56878ca216481fe1daae1 100644
--- a/qim3d/processing/__init__.py
+++ b/qim3d/processing/__init__.py
@@ -4,4 +4,5 @@ from .detection import blob_detection
 from .filters import *
 from .operations import *
 from .cc import get_3d_cc
+from .layers2d import segment_layers, get_lines
 from .mesh import create_mesh
diff --git a/qim3d/processing/layers2d.py b/qim3d/processing/layers2d.py
new file mode 100644
index 0000000000000000000000000000000000000000..d91ef0a42f1ba8715793a674321219cbbf1d634b
--- /dev/null
+++ b/qim3d/processing/layers2d.py
@@ -0,0 +1,97 @@
+import numpy as np
+from slgbuilder import GraphObject 
+from slgbuilder import MaxflowBuilder
+
+def segment_layers(data:np.ndarray, inverted:bool = False, n_layers:int = 1, delta:float = 1, min_margin:int = 10, max_margin:int = None, wrap:bool = False):
+    """
+    Works on 2D and 3D data.
+    Light one function wrapper around slgbuilder https://github.com/Skielex/slgbuilder to do layer segmentation
+    Now uses only MaxflowBuilder for solving.
+
+    Args:
+        data: 2D or 3D array on which it will be computed
+        inverted: if True, it will invert the brightness of the image
+        n_layers: How many layers are we looking for (result in a layer and background)
+        delta: Smoothness parameter
+        min_margin: If we want more layers, we have to have a margin otherwise they are all going to be exactly the same
+        max_margin: Maximum margin between layers
+        wrap: If True, starting and ending point of the border between layers are at the same level
+
+    Returns:
+        segmentations: list of numpy arrays, even if n_layers == 1, each array is only 0s and 1s, 1s segmenting this specific layer
+
+    Raises:
+        TypeError: If Data is not np.array, if n_layers is not integer.
+        ValueError: If n_layers is less than 1, if delta is negative or zero
+
+    Example:
+        Example is only shown on 2D image, but segment_layers can also take 3D structures.
+        ```python
+        import qim3d
+
+        layers_image = qim3d.io.load('layers3d.tif')[:,:,0]
+        layers = qim3d.processing.segment_layers(layers_image, n_layers = 2)
+        layer_lines = qim3d.processing.get_lines(layers)
+
+        from matplotlib import pyplot as plt
+
+        plt.imshow(layers_image, cmap='gray')
+        plt.axis('off')
+        for layer_line in layer_lines:
+            plt.plot(layer_line, linewidth = 3)
+        ```
+        ![layer_segmentation](assets/screenshots/layers.png)
+        ![layer_segmentation](assets/screenshots/segmented_layers.png)
+
+    """
+    if isinstance(data, np.ndarray):
+        data = data.astype(np.int32)
+        if inverted:
+            data = ~data
+    else:
+        raise TypeError(F"Data has to be type np.ndarray. Your data is of type {type(data)}")
+    
+    helper = MaxflowBuilder()
+    if not isinstance(n_layers, int):
+        raise TypeError(F"Number of layers has to be positive integer. You passed {type(n_layers)}")
+    
+    if n_layers == 1:
+        layer = GraphObject(data)
+        helper.add_object(layer)
+    elif n_layers > 1:
+        layers = [GraphObject(data) for _ in range(n_layers)]
+        helper.add_objects(layers)
+        for i in range(len(layers)-1):
+            helper.add_layered_containment(layers[i], layers[i+1], min_margin=min_margin, max_margin=max_margin) 
+
+    else:
+        raise ValueError(F"Number of layers has to be positive integer. You passed {n_layers}")
+    
+    helper.add_layered_boundary_cost()
+
+    if delta > 1:
+        delta = int(delta)
+    elif delta <= 0:
+        raise ValueError(F'Delta has to be positive number. You passed {delta}')
+    helper.add_layered_smoothness(delta=delta, wrap = bool(wrap))
+    helper.solve()
+    if n_layers == 1:
+        segmentations =[helper.what_segments(layer)]
+    else:
+        segmentations = [helper.what_segments(l).astype(np.int32) for l in layers]
+
+    return segmentations
+
+def get_lines(segmentations:list|np.ndarray) -> list:
+    """
+    Expects list of arrays where each array is 2D segmentation with only 2 classes. This function gets the border between those two
+    so it could be plotted. Used with qim3d.processing.segment_layers
+
+    Args:
+        segmentations: list of arrays where each array is 2D segmentation with only 2 classes
+
+    Returns:
+        segmentation_lines: list of 1D numpy arrays
+    """
+    segmentation_lines = [np.argmin(s, axis=0) - 0.5 for s in segmentations]
+    return segmentation_lines
\ No newline at end of file
diff --git a/qim3d/processing/operations.py b/qim3d/processing/operations.py
index 982c6960378ea49b987a655a9840937621171f63..8d2d0f83995ccb3507672a354fcb9d0b7cfd185e 100644
--- a/qim3d/processing/operations.py
+++ b/qim3d/processing/operations.py
@@ -200,7 +200,7 @@ def fade_mask(
 
 
 def overlay_rgb_images(
-    background: np.ndarray, foreground: np.ndarray, alpha: float = 0.5
+    background: np.ndarray, foreground: np.ndarray, alpha: float = 0.5, hide_black:bool = True,
 ) -> np.ndarray:
     """
     Overlay an RGB foreground onto an RGB background using alpha blending.
@@ -209,6 +209,7 @@ def overlay_rgb_images(
         background (numpy.ndarray): The background RGB image.
         foreground (numpy.ndarray): The foreground RGB image (usually masks).
         alpha (float, optional): The alpha value for blending. Defaults to 0.5.
+        hide_black (bool, optional): If True, black pixels will have alpha value 0, so the black won't be visible. Used for segmentation where we don't care about background. Defaults to True.
 
     Returns:
         composite (numpy.ndarray): The composite RGB image with overlaid foreground.
@@ -218,18 +219,36 @@ def overlay_rgb_images(
 
     Note:
         - The function performs alpha blending to overlay the foreground onto the background.
-        - It ensures that the background and foreground have the same shape before blending.
+        - It ensures that the background and foreground have the same first two dimensions (image size matches).
+        - It can handle greyscale images, values from 0 to 1, raw values which are negative or bigger than 255.
         - It calculates the maximum projection of the foreground and blends them onto the background.
-        - Brightness outside the foreground is adjusted to maintain consistency with the background.
     """
 
-    # Igonore alpha in case its there
-    background = background[..., :3]
-    foreground = foreground[..., :3]
+    def to_uint8(image:np.ndarray):
+        if np.min(image) < 0:
+            image = image - np.min(image)
+
+        maxim = np.max(image)
+        if maxim > 255:
+            image = (image / maxim)*255
+        elif maxim <= 1:
+            image = image*255
+
+        if image.ndim == 2:
+            image = np.repeat(image[..., None], 3, -1)
+        elif image.ndim == 3:
+            image = image[..., :3] # Ignoring alpha channel
+        else:
+            raise ValueError(F'Input image can not have higher dimension than 3. Yours have {image.ndim}')
+        
+        return image.astype(np.uint8)
+        
+    background = to_uint8(background)
+    foreground = to_uint8(foreground)
 
     # Ensure both images have the same shape
     if background.shape != foreground.shape:
-        raise ValueError("Input images must have the same shape")
+        raise ValueError(F"Input images must have the same first two dimensions. But background is of shape {background.shape} and foreground is of shape {foreground.shape}")
 
     # Perform alpha blending
     foreground_max_projection = np.amax(foreground, axis=2)
@@ -240,11 +259,18 @@ def overlay_rgb_images(
         foreground_max_projection = foreground_max_projection / np.max(
             foreground_max_projection
         )
+    # Check alpha validity
+    if alpha < 0:
+        raise ValueError(F'Alpha has to be positive number. You used {alpha}')
+    elif alpha > 1:
+        alpha = 1
+    
+    # If the pixel is black, its alpha value is set to 0, so it has no effect on the image
+    if hide_black:
+        alpha = np.full((background.shape[0], background.shape[1],1), alpha)
+        alpha[np.apply_along_axis(lambda x: (x == [0,0,0]).all(), axis = 2, arr = foreground)] = 0
 
     composite = background * (1 - alpha) + foreground * alpha
     composite = np.clip(composite, 0, 255).astype("uint8")
 
-    # Adjust brightness outside foreground
-    composite = composite + (background * (1 - alpha)) * (1 - foreground_max_projection)
-
-    return composite.astype("uint8")
+    return composite.astype("uint8")
\ No newline at end of file
diff --git a/qim3d/utils/cli.py b/qim3d/utils/cli.py
new file mode 100644
index 0000000000000000000000000000000000000000..f07135782e2cb65ea0350c53d9108c7b160dca29
--- /dev/null
+++ b/qim3d/utils/cli.py
@@ -0,0 +1,39 @@
+import argparse
+from qim3d.gui import data_explorer, iso3d, annotation_tool, local_thickness, layers2d
+
+def main():
+    parser = argparse.ArgumentParser(description='Qim3d command-line interface.')
+    subparsers = parser.add_subparsers(title='Subcommands', dest='subcommand')
+
+    # subcommands
+    gui_parser = subparsers.add_parser('gui', help = 'Graphical User Interfaces.')
+
+    gui_parser.add_argument('--data-explorer', action='store_true', help='Run data explorer.')
+    gui_parser.add_argument('--iso3d', action='store_true', help='Run iso3d.')
+    gui_parser.add_argument('--annotation-tool', action='store_true', help='Run annotation tool.')
+    gui_parser.add_argument('--local-thickness', action='store_true', help='Run local thickness tool.')
+    gui_parser.add_argument('--layers2d', action='store_true', help='Run layers2d.')
+    gui_parser.add_argument('--host', default='0.0.0.0', help='Desired host.')
+
+    args = parser.parse_args()
+
+    if args.subcommand == 'gui':
+        arghost = args.host
+        if args.data_explorer:
+            
+            data_explorer.run_interface(arghost)
+
+        elif args.iso3d:
+            iso3d.run_interface(arghost)
+        
+        elif args.annotation_tool:
+            annotation_tool.run_interface(arghost)
+        
+        elif args.local_thickness:
+            local_thickness.run_interface(arghost)
+
+        elif args.layers2d:
+            layers2d.run_interface(arghost)
+            
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/qim3d/viz/__init__.py b/qim3d/viz/__init__.py
index 28a9ed9257c921e03e3c3eb5c515b8eb2a0769c7..444c190c1b545642a1e5c3bc491e3ea0dce8cc87 100644
--- a/qim3d/viz/__init__.py
+++ b/qim3d/viz/__init__.py
@@ -13,3 +13,4 @@ from .local_thickness_ import local_thickness
 from .structure_tensor import vectors
 from .metrics import plot_metrics, grid_overview, grid_pred, vol_masked
 from .preview import image_preview
+from . import layers2d
diff --git a/qim3d/viz/layers2d.py b/qim3d/viz/layers2d.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e284461f2eb86bf5883891cd9205911f151d835
--- /dev/null
+++ b/qim3d/viz/layers2d.py
@@ -0,0 +1,37 @@
+""" Provides a collection of visualisation functions for the Layers2d class."""
+import io
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+from PIL import Image
+
+
+def image_with_lines(image:np.ndarray, lines: list, line_thickness:float|int) -> Image:
+    """
+    Plots the image and plots the lines on top of it. Then extracts it as PIL.Image and in the same size as the input image was.
+    Paramters:
+    -----------
+    image: Image on which we put the lines
+    lines: list of 1D arrays to be plotted on top of the image
+    line_thickness: how thick is the line supposed to be
+
+    Returns:
+    ----------
+    image_with_lines: 
+    """
+    fig, ax = plt.subplots()
+    ax.imshow(image, cmap = 'gray')
+    ax.axis('off')
+
+    for line in lines:
+        ax.plot(line, linewidth = line_thickness)
+
+    buf = io.BytesIO()
+    plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0)
+    plt.close()
+
+    buf.seek(0)
+    return Image.open(buf).resize(size = image.squeeze().shape[::-1])
+
diff --git a/requirements.txt b/requirements.txt
index 7decb4f38c595496b9d87cfdb7cb7d9311994f77..45e52daffd5e28a5b6c4f4085087bb5dfafc25da 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,7 @@ Pillow>=10.0.1
 plotly>=5.14.1
 scipy>=1.11.2
 seaborn>=0.12.2
-pydicom>=2.4.4
+pydicom==2.4.4
 setuptools>=68.0.0
 tifffile>=2023.4.12
 torch>=2.0.1
@@ -29,3 +29,4 @@ zarr>=2.18.2
 ome_zarr>=0.9.0
 dask-image>=2024.5.3
 trimesh>=4.4.9
+slgbuilder>=0.2.1
diff --git a/setup.py b/setup.py
index 8abce05e40f9453d379b3d741a0a8fd9796890e3..10cdf4e6be8d58d6826836f3acf3e57192299613 100644
--- a/setup.py
+++ b/setup.py
@@ -49,7 +49,7 @@ setup(
         "h5py>=3.9.0",
         "localthickness>=0.1.2",
         "matplotlib>=3.8.0",
-        "pydicom>=2.4.4",
+        "pydicom==2.4.4",
         "numpy>=1.26.0",
         "outputformat>=0.1.3",
         "Pillow>=10.0.1",