Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
371 changes: 315 additions & 56 deletions notebooks/perovstats-demo.ipynb

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions notebooks/perovstats-multi-process.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@
"source": [
"#### Tweak parameters if necessary\n",
"\n",
"##### 1) Disable frequency splitting if necessary with `run_splitting = False`\n",
"##### 1) Identify the file type of the scan(s) used.\n",
"\n",
"##### 2) Adjust the cutoff bounds if no cutoff frequency can be found (if one is found its value is logged in the output below)\n",
"##### 2) Disable frequency splitting if necessary with `run_splitting = False`\n",
"\n",
"##### 3) Choose the min_rms value (affects isolation of perovskite grains from silicon topology)\n",
"##### 3) Adjust the cutoff bounds if no cutoff frequency can be found (if one is found its value is logged in the output below)\n",
"\n",
"##### 4) Select the segmentation method to use\n",
"##### 4) Choose the min_rms value (affects isolation of perovskite grains from silicon topology)\n",
"\n",
"##### 5) Select the segmentation method to use\n",
"##### As a reminder:\n",
"- `traditional`:\n",
" - hard-coded\n",
Expand All @@ -109,6 +111,9 @@
"metadata": {},
"outputs": [],
"source": [
"# The file extension of the input image(s).\n",
"file_ext = \".spm\"\n",
"\n",
"# If you are inputting images without a background material frequency splitting can be disabled here.\n",
"run_splitting = True # Options: True, False\n",
"\n",
Expand All @@ -121,7 +126,7 @@
"min_rms = 10\n",
"\n",
"\n",
"segmentation_method = \"cellpose\" # Options: traditional, cellpose\n",
"segmentation_method = \"traditional\" # Options: traditional, cellpose\n",
"\n",
"# If 'traditional' segmentation has been chosen:\n",
"threshold_offset = -0.5\n"
Expand Down Expand Up @@ -158,6 +163,7 @@
" else:\n",
" logger.error(\"Error: custom configuration file could not be found. Please check the set filepath above.\")\n",
"\n",
"config[\"file_ext\"] = file_ext\n",
"config[\"fourier\"][\"run\"] = run_splitting\n",
"config[\"fourier\"][\"cutoff_bounds\"] = cutoff_bounds\n",
"config[\"fourier\"][\"min_rms\"] = min_rms\n",
Expand Down
8 changes: 8 additions & 0 deletions src/perovstats/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from yaml import safe_load

from .processing import run_process
from .perovstats_gui import run_gui
from .config import update_module


Expand Down Expand Up @@ -100,8 +101,15 @@ def create_parser() -> ArgumentParser:
help="Process AFM images. Additional arguments over-ride defaults or those in the configuration file.",
)

gui_parser = subparsers.add_parser(
"gui",
description="Process AFM images from a GUI.",
help="Process AFM images from a GUI.",
)

# Run the relevant function with the arguments
process_parser.set_defaults(func=process)
gui_parser.set_defaults(func=run_gui)

return parser

Expand Down
2 changes: 1 addition & 1 deletion src/perovstats/core/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def save_image(image: np.ndarray, output_dir: Path, filename: str, cmap: str='gr
filename : str
The name of the file to be created/ saved to.
cmap : str
The cmap to be used in the imsave() function. By default is 'grey'.
The cmap to be used in the imsave() function defined in config. By default is 'grey'.
"""
output_dir.mkdir(parents=True, exist_ok=True)
if cmap:
Expand Down
5 changes: 3 additions & 2 deletions src/perovstats/default_config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
base_dir: ./example images/ # Directory in which to search for data files
output_dir: ./output # Directory to output results to
file_ext: ".spm" # File extension of the data files.
colour_scheme: "afmhot"
loading:
channel: Height # Channel to pull data from in the data files.
outliers:
remove_outliers: true
max_z_score: 3.2
max_z_score: 3.5
fourier:
run: true
edge_width: 0.03 # Width of the cutoff in frequency when performing the fourier split, as a fraction of the total frequency range. Higher values make the split less sensitive to cutoff choice.
Expand All @@ -14,7 +15,7 @@ fourier:
segmentation:
segmentation_method: traditional # Options: traditional, cellpose
traditional:
threshold_offset: -1 # Amount to change the calculated threshold by for segmentation. Lower thresholds increase sentitivity.
threshold_offset: 1.5 # Amount to change the calculated threshold by for segmentation. Lower thresholds increase sentitivity.
threshold_block_size: 1000 # Size of each block for local thresholding. Lower values increase segmentation sensitivity.
smoothing:
sigma: 4
Expand Down
10 changes: 7 additions & 3 deletions src/perovstats/fourier.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,23 +87,27 @@ def split_frequencies(
image_object.low_pass = low_pass
image_object.file_directory = file_output_dir

cmap = config["colour_scheme"]

# Convert high-pass and low-pass to image format
arr = high_pass
img_dir = Path(file_output_dir) / "images"
save_image(arr, img_dir, f"{filename}_highpass.png", cmap="grey")
save_image(arr, img_dir, f"{filename}_highpass.png", cmap=cmap)

arr = low_pass
save_image(arr, img_dir, f"{filename}_lowpass.png", cmap="grey")
save_image(arr, img_dir, f"{filename}_lowpass.png", cmap=cmap)
else:
logger.info(f"[{image_object.filename}] : Frequency splitting is disabled by config, the original image will be used.")
if image_object.image_flattened is not None:
image_object.high_pass = image_object.image_flattened
image_object.low_pass = np.zeros_like(image_object.high_pass)
else:
image_object.high_pass = image_object.image_original
image_object.low_pass = np.zeros_like(image_object.high_pass)

arr = image_object.image_original
arr = normalise_array(arr)
save_image(arr, file_output_dir / "images", f"{filename}_original.png", cmap="grey")
save_image(arr, file_output_dir / "images", f"{filename}_original.png", cmap=cmap)


def perform_fourier(
Expand Down
30 changes: 18 additions & 12 deletions src/perovstats/grains.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from skimage import morphology
from scipy.ndimage import binary_fill_holes
from scipy import stats
import matplotlib.cm as cm

from .core.classes import Grain, ImageData
from .core.image_processing import normalise_array
Expand Down Expand Up @@ -210,6 +211,9 @@ def _create_grain_objects(
return circularity_data

def _save_mask_images(config, image_object, mask_data, filename, mask_details):
cmap = config["colour_scheme"]
get_cmap = cm.get_cmap(cmap)

# Remove mask outlines of edge grains and smear grains from the mask
mask_rgb = mask_data["mask_rgb"]
mask_rgb[image_object.indent_mask > 0] = [0, 0, 0]
Expand All @@ -224,30 +228,32 @@ def _save_mask_images(config, image_object, mask_data, filename, mask_details):
image_object.cleaned_mask = new_mask

# Save the cleaned mask
save_image(new_mask, save_dir, f"{filename}_mask.png")
save_image(new_mask, save_dir, f"{filename}_mask.png", cmap=cmap)

# Save high-pass with mask overlay
high_pass = image_object.high_pass
rgb_highpass = np.stack((high_pass,)*3, axis=-1)
rgb_highpass = normalise_array(rgb_highpass)
rgb_highpass[new_mask > 0] = [1, 0, 0]
save_image(rgb_highpass, save_dir, f"{filename}_highpass_mask_overlay.png")
norm_highpass = normalise_array(high_pass)
rgba_highpass = get_cmap(norm_highpass)
rgb_highpass = rgba_highpass[..., :3]
rgb_highpass[new_mask > 0] = [0, 0, 1]
save_image(rgb_highpass, save_dir, f"{filename}_highpass_mask_overlay.png", cmap=cmap)

# Save original image with mask overlay
original = image_object.image_original
rgb_original = np.stack((original,)*3, axis=-1)
rgb_original = normalise_array(rgb_original)
rgb_original[new_mask > 0] = [1, 0, 0]
save_image(rgb_original, save_dir, f"{filename}_original_mask_overlay.png")
norm_original = normalise_array(original)
rgba_original = get_cmap(norm_original)
rgb_original = rgba_original[..., :3]
rgb_original[new_mask > 0] = [0, 0, 1]
save_image(rgb_original, save_dir, f"{filename}_original_mask_overlay.png", cmap=cmap)

# Save the high-pass image with solid grains and red sections identifying smear areas
# Save the high-pass image with solid grains and pink sections identifying smear areas
save_image(mask_rgb, save_dir, f"{filename}_rgb_grains.png", cmap=None)
smear_overlay = np.stack((image_object.high_pass,)*3, axis=-1)
smear_overlay = normalise_array(smear_overlay)
mask_2d = np.all(mask_rgb == 0, axis=2)
smear_overlay[mask_2d == 0] = [1, 1, 1]
smear_overlay[image_object.smears == 1] = [1, 0, 0]
save_image(smear_overlay, save_dir, f"{filename}_smears.png")
smear_overlay[image_object.smears == 1] = [1, 0, 1]
save_image(smear_overlay, save_dir, f"{filename}_smears.png", cmap=cmap)

# Save area and circularity data for all grains and export a histogram of them each
image_object.mask_areas = mask_details['areas']
Expand Down
119 changes: 119 additions & 0 deletions src/perovstats/perovstats_gui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import tkinter as tk
from tkinter import ttk
import numpy as np

from .processing import run_process

WINDOW_SIZE = (600,400)
WINDOW_RESIZE = False

class PerovStatsGUI():
def __init__(self, root):
self.root = root
self.root.title("PerovStats GUI")
self.root.geometry("1000x600")

# Configure the main window grid weights
# Column 0 (Left), Column 2 (Right) weight = 0 (stays small)
# Column 1 (Center) weight = 1 (expands to fill space)
self.root.columnconfigure(0, weight=0)
self.root.columnconfigure(1, weight=1)
self.root.columnconfigure(2, weight=0)
self.root.rowconfigure(0, weight=1) # Make the row fill the height

# --- 1. Left Sidebar ---
self.left_bar = tk.Frame(self.root, width=200, pady=20, padx=10)
self.left_bar.grid(row=0, column=0, sticky="nsew")

self.upload_btn = tk.Button(self.left_bar, text="Upload scan(s)")
self.upload_btn.grid(row=0, column=0, padx=25, pady=5, sticky="ew")
ttk.Separator(self.left_bar, orient="horizontal").grid(row=1, column=0, pady=20, sticky="ew")

self.analysis_mode = tk.StringVar(value="auto")

tk.Label(self.left_bar, text="Segmentation method:").grid(row=2, column=0, sticky="w")

# 3. First Radio Button
self.radio_auto = tk.Radiobutton(
self.left_bar,
text="Traditional",
variable=self.analysis_mode,
value="traditional"
)
self.radio_auto.grid(row=3, column=0, columnspan=2, sticky="w")

# 4. Second Radio Button
self.radio_manual = tk.Radiobutton(
self.left_bar,
text="Cellpose ML",
variable=self.analysis_mode,
value="cellpose"
)
self.radio_manual.grid(row=4, column=0, columnspan=2, sticky="w")


ttk.Separator(self.left_bar, orient="horizontal").grid(row=5, column=0, pady=20, sticky="ew")

self.run_btn = tk.Button(self.left_bar, text="Frequency Splitting")
self.run_btn.grid(row=6, column=0, padx=25, pady=5, sticky="ew")

self.run_btn = tk.Button(self.left_bar, text="Image Segmentation")
self.run_btn.grid(row=7, column=0, padx=25, pady=5, sticky="ew")

self.run_btn = tk.Button(self.left_bar, text="Grain Processing")
self.run_btn.grid(row=8, column=0, padx=25, pady=5, sticky="ew")

# --- 2. Central Area ---
self.center_area = tk.Frame(self.root, bg="white", borderwidth=2, relief="sunken")
self.center_area.grid(row=0, column=1, sticky="nsew")

# Centering a label inside the center area
self.center_area.columnconfigure(0, weight=1)
self.center_area.rowconfigure(0, weight=1)
tk.Label(self.center_area, text="Upload a scan and configure/ run the steps on the left to generate data.", bg="white").grid(row=0, column=0)

# --- 3. Right Sidebar (Settings) ---
self.right_bar = tk.Frame(self.root, width=250, bg="#ecf0f1", padx=10, pady=10)
self.right_bar.grid(row=0, column=2, sticky="nsew")

tk.Label(self.right_bar, text="Settings", bg="#ecf0f1", font=('Helvetica', 10, 'bold')).grid(row=0, column=0, pady=10)

# Example of why Grid is better for settings:
tk.Label(self.right_bar, text="Threshold:", bg="#ecf0f1").grid(row=1, column=0, sticky="w")
tk.Entry(self.right_bar, width=10).grid(row=1, column=1, padx=5, pady=5)


def frequency_settings(self):
pass

def segmentation_settings(self):
pass

def grain_settings(self):
pass


def upload_files(self):
pass


def run_splitting(self):
pass


def run_segmentation(self):
pass


def run_smears(self):
pass


def run_grains(self):
pass


def run_gui(args):
root = tk.Tk()
app = PerovStatsGUI(root)
root.mainloop()
Loading