Skip to content

Commit

Permalink
settings revision for masking and cropping (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
irkri committed Nov 12, 2024
1 parent 4d5cb62 commit fe4e2ce
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 69 deletions.
31 changes: 21 additions & 10 deletions blitz/data/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,11 @@ def normalize(
def unravel(self) -> None:
self._redop = None

def mask(self, roi: pg.ROI) -> None:
def mask(self, roi: pg.ROI) -> bool:
if self._transposed or self._flipped_x or self._flipped_y:
log("Masking not available while data is flipped or transposed",
color="red")
return
return False
pos = roi.pos()
size = roi.size()
x_start = max(0, int(pos[0]))
Expand All @@ -211,6 +211,7 @@ def mask(self, roi: pg.ROI) -> None:
self._mask = (
slice(None, None), slice(x_start, x_stop), slice(y_start, y_stop),
)
return True

def mask_range(self, range_: tuple[int, int, int, int]) -> None:
self._mask = (
Expand All @@ -227,9 +228,12 @@ def image_mask(self, mask: "ImageData") -> None:
else:
self._image_mask = mask.image[0].astype(bool)

def reset_mask(self) -> None:
self._mask = None
self._image_mask = None
def reset_mask(self) -> bool:
if self._mask is not None:
self._mask = None
self._image_mask = None
return True
return False

def transpose(self) -> None:
self._transposed = not self._transposed
Expand All @@ -240,8 +244,15 @@ def flip_x(self) -> None:
def flip_y(self) -> None:
self._flipped_y = not self._flipped_y

def save_options(self) -> None:
if self._mask is not None:
settings.set_project("mask", self._mask)
if self._save_cropped is not None:
settings.set_project("cropped", self._save_cropped)
def get_mask(self) -> tuple[slice, slice, slice] | None:
return self._mask

def set_mask(self, mask: tuple[slice, slice, slice] | None):
self._mask = mask

def get_crop(self) -> tuple[int, int] | None:
return self._save_cropped

def set_crop(self, crop: tuple[int, int] | None):
self._save_cropped = crop
self._cropped = crop
78 changes: 49 additions & 29 deletions blitz/layout/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,96 +247,114 @@ def setup_sync(self) -> None:
self.ui.dock_area.restoreState(docks_arrangement)

settings.connect_sync(
"default/load_8bit",
self.ui.checkbox_load_8bit.stateChanged,
self.ui.checkbox_load_8bit.isChecked,
self.ui.checkbox_load_8bit.setChecked,
"default/load_8bit",
)
settings.connect_sync(
"default/load_grayscale",
self.ui.checkbox_load_grayscale.stateChanged,
self.ui.checkbox_load_grayscale.isChecked,
self.ui.checkbox_load_grayscale.setChecked,
"default/load_grayscale",
)
settings.connect_sync(
self.ui.spinbox_load_size.editingFinished,
self.ui.spinbox_load_size.value,
self.ui.spinbox_load_size.setValue,
"default/size_ratio",
)
settings.connect_sync(
self.ui.spinbox_load_subset.editingFinished,
self.ui.spinbox_load_subset.value,
self.ui.spinbox_load_subset.setValue,
"default/subset_ratio",
)
settings.connect_sync(
"default/max_ram",
self.ui.spinbox_max_ram.editingFinished,
self.ui.spinbox_max_ram.value,
self.ui.spinbox_max_ram.setValue,
"default/max_ram",
)
# settings.connect_sync(
# self.ui.image_viewer.ui.histogram.gradient.sigGradientChanged,
# # self.ui.spinbox_max_ram.value,
# # self.ui.spinbox_max_ram.setValue,
# "default/greyclip",
# )
settings.connect_sync(
"web/address",
self.ui.address_edit.editingFinished,
self.ui.address_edit.text,
self.ui.address_edit.setText,
"web/address",
)
settings.connect_sync(
"web/token",
self.ui.token_edit.editingFinished,
self.ui.token_edit.text,
self.ui.token_edit.setText,
"web/token",
)
settings.connect_sync(
"data/sync",
self.ui.checkbox_sync_file.stateChanged,
self.ui.checkbox_sync_file.isChecked,
self.ui.checkbox_sync_file.setChecked,
"data/sync",
)

def sync_project(self) -> None:
def sync_project_preloading(self) -> None:
settings.connect_sync_project(
"size_ratio",
self.ui.spinbox_load_size.editingFinished,
self.ui.spinbox_load_size.value,
self.ui.spinbox_load_size.setValue,
)
settings.connect_sync_project(
"subset_ratio",
self.ui.spinbox_load_subset.editingFinished,
self.ui.spinbox_load_subset.value,
self.ui.spinbox_load_subset.setValue,
)

def sync_project_postloading(self) -> None:
settings.connect_sync_project(
"flipped_x",
self.ui.checkbox_flipx.stateChanged,
self.ui.checkbox_flipx.isChecked,
self.ui.checkbox_flipx.setChecked,
"flipped_x",
lambda: self.ui.image_viewer.manipulate("flip_x"),
True,
)
settings.connect_sync_project(
"flipped_y",
self.ui.checkbox_flipy.stateChanged,
self.ui.checkbox_flipy.isChecked,
self.ui.checkbox_flipy.setChecked,
"flipped_y",
lambda: self.ui.image_viewer.manipulate("flip_y"),
True,
)
settings.connect_sync_project(
"transposed",
self.ui.checkbox_transpose.stateChanged,
self.ui.checkbox_transpose.isChecked,
self.ui.checkbox_transpose.setChecked,
"transposed",
lambda: self.ui.image_viewer.manipulate("transpose"),
True,
)
settings.connect_sync_project(
"measure_tool_pixels",
self.ui.spinbox_pixel.editingFinished,
self.ui.spinbox_pixel.value,
self.ui.spinbox_pixel.setValue,
"measure_tool_pixels",
)
settings.connect_sync_project(
"measure_tool_au",
self.ui.spinbox_mm.editingFinished,
self.ui.spinbox_mm.value,
self.ui.spinbox_mm.setValue,
"measure_tool_au",
)
settings.connect_sync_project(
"isocurve_smoothing",
self.ui.spinbox_iso_smoothing.editingFinished,
self.ui.spinbox_iso_smoothing.value,
self.ui.spinbox_iso_smoothing.setValue,
"isocurve_smoothing",
)
settings.connect_sync_project(
"mask",
self.ui.image_viewer.image_mask_changed,
self.ui.image_viewer.data.get_mask,
)
settings.connect_sync_project(
"cropped",
self.ui.image_viewer.image_crop_changed,
self.ui.image_viewer.data.get_crop,
)

def reset_options(self) -> None:
Expand Down Expand Up @@ -758,7 +776,8 @@ def load(self, path: Optional[Path | str] = None) -> None:
path.parent / (path.name.split(".")[0] + ".blitz")
)
settings.set_project("path", path)
self.sync_project()
self.sync_project_preloading()
self.sync_project_postloading()

def load_project(self, path: Path) -> None:
log(f"Loading '{path.name}' configuration file...",
Expand All @@ -770,6 +789,9 @@ def load_project(self, path: Path) -> None:
if saved_path.exists():
mask = settings.get_project("mask")[1:]
crop = settings.get_project("cropped")
mask = mask if mask else None
crop = crop if crop else None
self.sync_project_preloading()
with LoadingManager(self, f"Loading {saved_path}") as lm:
self.ui.image_viewer.load_data(
saved_path,
Expand All @@ -787,7 +809,7 @@ def load_project(self, path: Path) -> None:
self.last_file = saved_path.name
self.update_statusbar()
self.reset_options()
self.sync_project()
self.sync_project_postloading()
else:
log("Path to dataset in .blitz project file does not point to "
"a valid file or folder location. Deleting entry...",
Expand Down Expand Up @@ -845,5 +867,3 @@ def save_settings(self):
settings.set("window/relative_size",
self.width() / screen_geometry.width(),
)
if self.ui.checkbox_sync_file.isChecked():
self.ui.image_viewer.data.save_options()
5 changes: 5 additions & 0 deletions blitz/layout/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ def setup_option_dock(self) -> None:
file_layout.addWidget(self.spinbox_max_ram)
self.checkbox_sync_file = QCheckBox("load/save project file")
self.checkbox_sync_file.setChecked(False)
self.checkbox_sync_file.setStyleSheet("""
QCheckBox:checked {
color: rgb(250, 125, 125);
}
""")
file_layout.addWidget(self.checkbox_sync_file)
load_btn_lay = QHBoxLayout()
self.button_open_file = QPushButton("Open File")
Expand Down
31 changes: 18 additions & 13 deletions blitz/layout/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class ImageViewer(pg.ImageView):
file_dropped = pyqtSignal(str)
image_size_changed = pyqtSignal()
image_changed = pyqtSignal()
image_mask_changed = pyqtSignal()
image_crop_changed = pyqtSignal()

def __init__(self) -> None:
view = pg.PlotItem()
Expand Down Expand Up @@ -247,6 +249,7 @@ def crop(self, left: int, right: int, keep: bool = False) -> None:
autoLevels=False,
)
self.ui.roiPlot.plotItem.vb.autoRange() # type: ignore
self.image_crop_changed.emit()

def undo_crop(self) -> bool:
success = self.data.undo_crop()
Expand Down Expand Up @@ -291,22 +294,24 @@ def manipulate(self, operation: str) -> None:
def apply_mask(self) -> None:
if self.mask is None:
return
self.data.mask(self.mask)
if self.data.mask(self.mask):
self.setImage(
self.data.image,
keep_timestep=True,
autoLevels=False,
)
self.image_size_changed.emit()
self.image_mask_changed.emit()
self.toggle_mask()
self.setImage(
self.data.image,
keep_timestep=True,
autoLevels=False,
)
self.image_size_changed.emit()

def reset_mask(self) -> None:
self.data.reset_mask()
self.setImage(
self.data.image,
autoLevels=False,
)
self.image_size_changed.emit()
if self.data.reset_mask():
self.setImage(
self.data.image,
autoLevels=False,
)
self.image_size_changed.emit()
self.image_mask_changed.emit()

def toggle_mask(self) -> None:
if self.mask is None:
Expand Down
37 changes: 20 additions & 17 deletions blitz/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@
"default/multicore_files_threshold": 333,
"default/load_8bit": False,
"default/load_grayscale": True,
"default/size_ratio": 1.0,
"default/subset_ratio": 1.0,
"default/max_ram": 2,
"default/colormap": "greyclip",

"data/sync": False,

Expand All @@ -35,6 +34,9 @@
}

_default_project_settings = {
"size_ratio": 1.0,
"subset_ratio": 1.0,

"path": "",
"mask": (),
"cropped": (),
Expand Down Expand Up @@ -76,23 +78,24 @@ def write_all(self) -> None:

def connect_sync(
self,
setting: str,
signal: pyqtBoundSignal,
value_getter: Callable,
value_setter: Callable,
setting: str,
value_setter: Optional[Callable] = None,
) -> None:
self._connections.append(
(signal, signal.connect(
lambda: self.__setitem__(setting, value_getter())
))
)
default = self._default[setting]
if (self[setting] != default) or self[setting] != value_getter():
try:
value_setter(self[setting])
except:
log(f"Failed to load setting {setting!r} from "
f"{self._path.name}", color="red")
if value_setter is not None:
if (self[setting] != default) or self[setting] != value_getter():
try:
value_setter(self[setting])
except Exception as e:
log(f"Failed to load setting {setting!r} from "
f"{self._path.name}", color="red")

def __getitem__(self, setting: str) -> Any:
self.settings.sync()
Expand Down Expand Up @@ -167,27 +170,27 @@ def set_project(setting: str, value: Any) -> None:


def connect_sync(
setting: str,
signal: pyqtBoundSignal,
value_getter: Callable,
value_setter: Callable,
setting: str,
value_setter: Optional[Callable] = None,
) -> None:
global SETTINGS
create()
SETTINGS.connect_sync(signal, value_getter, value_setter, setting)
SETTINGS.connect_sync(setting, signal, value_getter, value_setter)


def connect_sync_project(
signal,
value_getter: Callable,
value_setter: Callable,
setting: str,
signal: pyqtBoundSignal,
value_getter: Callable,
value_setter: Optional[Callable] = None,
manipulator: Optional[Callable] = None,
manipulator_target = None,
) -> None:
global PROJECT_SETTINGS
if PROJECT_SETTINGS is None:
raise RuntimeError("Project settings are not selected")
PROJECT_SETTINGS.connect_sync(signal, value_getter, value_setter, setting)
PROJECT_SETTINGS.connect_sync(setting, signal, value_getter, value_setter)
if manipulator is not None and get_project(setting) == manipulator_target:
manipulator()

0 comments on commit fe4e2ce

Please sign in to comment.