pixel-patrol 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: pixel-patrol
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: bs4>=0.0.2
8
+ Requires-Dist: imageio
9
+ Requires-Dist: scikit-image
10
+ Requires-Dist: matplotlib>=3.10.0
11
+ Requires-Dist: pandas>=2.2.3
12
+ Requires-Dist: ray>=2.40.0
13
+ Requires-Dist: streamlit>=1.41.1
14
+ Requires-Dist: altair>=5.5.0
15
+ Requires-Dist: numpy<2.0.0,>=1.21.0
16
+ Requires-Dist: image>=1.5.33
17
+ Requires-Dist: tifffile<2025.2.18,>=2024.12.12
18
+ Requires-Dist: plotly>=5.24.1
19
+ Requires-Dist: plotly-express>=0.4.1
20
+ Requires-Dist: tk>=0.1.0
21
+ Requires-Dist: bioio==1.0.2
22
+ Requires-Dist: bioio-czi>=1.0.2
23
+ Requires-Dist: bioio-lif>=1.0.0
24
+ Requires-Dist: bioio-nd2>=1.0.0
25
+ Requires-Dist: bioio-tifffile>=1.0.0
26
+ Requires-Dist: bioio-ome-zarr>=1.1.0
27
+ Requires-Dist: bioio-ome-tiff>=1.0.1
28
+ Requires-Dist: bioio-dv>=1.0.0
29
+ Requires-Dist: pyyaml>=6.0.2
30
+ Requires-Dist: polars>=1.22.0
31
+ Requires-Dist: streamlit-extras>=0.5.0
32
+ Requires-Dist: tensorboard>=2.18.0
33
+ Requires-Dist: tensorflow>=2.18.0
34
+ Requires-Dist: pywavelets>=1.8.0
35
+ Requires-Dist: opencv-python-headless>=4.11.0.86
36
+
37
+ # Pixel Patrol
38
+
39
+ ## How to use
40
+
41
+ 1. Clone repository
42
+ 2. Install `uv`
43
+ 3. `uv pip install -e .`
44
+ 4. `uv run streamlit_main.py`
45
+
46
+ If you get an error when adding a folder path - install tkinter/python-tk:
47
+ Ubuntu: `sudo apt-get install python3-tk`
48
+ Mac: `brew install python-tk`
49
+
50
+ ## How to add widget
51
+
52
+ Widgets can be added in this repository or in separate packages.
53
+
54
+ ### Write Widget
55
+
56
+ ```
57
+ # my/widgets/test_widget.py
58
+
59
+ import streamlit as st
60
+ from pixel_patrol.widgets.widget_interface import ImagePrevalidationWidget
61
+
62
+
63
+ class TestWidget(ImagePrevalidationWidget):
64
+
65
+ @property
66
+ def tab(self) -> str:
67
+ return "Other"
68
+
69
+ def run(self, selected_files_df):
70
+ with st.expander("Test widget", expanded=False):
71
+ st.text("Hi!")
72
+ ```
73
+
74
+ ### Add Widget to `entry-points` of the package
75
+
76
+ ```
77
+ # pyproject.toml
78
+
79
+ [project]
80
+ ...
81
+ [project.entry-points."pixel_patrol.widgets"]
82
+ test_widget = "my.widgets.test_widget:TestWidget"
83
+
84
+ ```
@@ -0,0 +1,48 @@
1
+ # Pixel Patrol
2
+
3
+ ## How to use
4
+
5
+ 1. Clone repository
6
+ 2. Install `uv`
7
+ 3. `uv pip install -e .`
8
+ 4. `uv run streamlit_main.py`
9
+
10
+ If you get an error when adding a folder path - install tkinter/python-tk:
11
+ Ubuntu: `sudo apt-get install python3-tk`
12
+ Mac: `brew install python-tk`
13
+
14
+ ## How to add widget
15
+
16
+ Widgets can be added in this repository or in separate packages.
17
+
18
+ ### Write Widget
19
+
20
+ ```
21
+ # my/widgets/test_widget.py
22
+
23
+ import streamlit as st
24
+ from pixel_patrol.widgets.widget_interface import ImagePrevalidationWidget
25
+
26
+
27
+ class TestWidget(ImagePrevalidationWidget):
28
+
29
+ @property
30
+ def tab(self) -> str:
31
+ return "Other"
32
+
33
+ def run(self, selected_files_df):
34
+ with st.expander("Test widget", expanded=False):
35
+ st.text("Hi!")
36
+ ```
37
+
38
+ ### Add Widget to `entry-points` of the package
39
+
40
+ ```
41
+ # pyproject.toml
42
+
43
+ [project]
44
+ ...
45
+ [project.entry-points."pixel_patrol.widgets"]
46
+ test_widget = "my.widgets.test_widget:TestWidget"
47
+
48
+ ```
File without changes
@@ -0,0 +1,12 @@
1
+
2
+ from enum import Enum
3
+
4
+ class DefaultTabs(Enum):
5
+ SUMMARY = "Summary"
6
+ FILE_STATS = "File Stats"
7
+ METADATA = "Metadata"
8
+ VISUALIZATION = "Visualization"
9
+ NOISE = "Noise"
10
+ OTHER = "Other Widgets"
11
+ DATASET_STATS = "Dataset Stats"
12
+
@@ -0,0 +1,11 @@
1
+ import os
2
+ from pathlib import Path
3
+ import tempfile
4
+
5
+ BASE_CACHE_PATH_ENV_VAR = "IMAGE_PREVALIDATION_BASE_CACHE_PATH"
6
+ BASE_CACHE_PATH = os.getenv(BASE_CACHE_PATH_ENV_VAR, str(Path.home().joinpath(".pixel_patrol")))
7
+
8
+ TEMP_DIR = tempfile.TemporaryDirectory()
9
+
10
+ def get_named_temp_file():
11
+ return tempfile.NamedTemporaryFile(delete=False)
@@ -0,0 +1,289 @@
1
+ from pathlib import Path
2
+
3
+ import polars as pl
4
+ import streamlit as st
5
+ import yaml
6
+
7
+ from pixel_patrol.project import Project
8
+ from pixel_patrol.utils.session import (
9
+ get_project, initialize_session_state, PrevalidationStep, get_current_step, set_current_step,
10
+ is_debug_mode, KEY_TEST_DATA_DIR, get_all_widgets
11
+ )
12
+ from pixel_patrol.utils.ui import (
13
+ choose_matplotlib_colormap, configure_layout, create_sidebar,
14
+ create_add_folder_button, choose_widgets,
15
+ add_new_path_with_subpath_check, display_imported_dirs_info,
16
+ display_widgets_as_report, display_widgets_as_tab, step_is_disabled
17
+ )
18
+ from pixel_patrol.utils.utils import (
19
+ preprocess_files, process_files,
20
+ get_cache_dir, aggregate_folder_dataframes, count_file_extensions, load_dataframe_images,
21
+ store_all_dataframe_images_to_cache, set_colors, cache_all_project_path_structures, add_new_path,
22
+ file_structure_cache_missing
23
+ )
24
+ from pixel_patrol.utils.widget import (
25
+ get_required_columns, load_or_get_project_widgets, organize_widgets_by_tab
26
+ )
27
+
28
+
29
+ def load_config(config_path: str = "../config.yaml"):
30
+ config = {}
31
+ config_file = Path(__file__).parent / config_path
32
+ if config_file.exists():
33
+ try:
34
+ with open(config_file, "r") as f:
35
+ config = yaml.safe_load(f)
36
+ except yaml.YAMLError as e:
37
+ st.error(f"Error parsing YAML file: {e}")
38
+ else:
39
+ st.error(f"Configuration file `{config_path}` not found.")
40
+ return config
41
+
42
+
43
+ def select_project(project):
44
+ # Check if a project name is provided in the URL query parameters
45
+ placeholder = st.empty()
46
+ with placeholder.container():
47
+ projects_dir = get_cache_dir() / "projects"
48
+ projects_dir.mkdir(exist_ok=True)
49
+ existing_projects = [p.stem for p in projects_dir.glob("*.yml")]
50
+ options = ["New Project"] + existing_projects
51
+ selected = st.selectbox("Select Project", options, key="project_select")
52
+
53
+ if selected == "New Project":
54
+ new_project_name = st.text_input("Enter New Project Name", key="new_project_name")
55
+ if new_project_name and st.button("Confirm", key="confirm_new"):
56
+ if new_project_name in existing_projects:
57
+ st.error("Project already exists. Choose a different name.")
58
+ else:
59
+ project.name = new_project_name
60
+ project.selected_widgets = [w.name for w in load_or_get_project_widgets(project)]
61
+ st.query_params["project"] = project.name
62
+ placeholder.empty()
63
+ else:
64
+ if st.button("Confirm", key="confirm_existing"):
65
+ set_active_project(project, selected)
66
+ st.rerun()
67
+
68
+ return project
69
+
70
+
71
+ def set_active_project(project, project_name):
72
+ if project is not None and project.name == project_name:
73
+ # project is in session, do not load from disk
74
+ return
75
+ project.name = project_name
76
+ if project.exists():
77
+ project.load_project_from_yml(project_name)
78
+ for path in project.paths:
79
+ add_new_path(path, project)
80
+ project.selected_widgets = [w.name for w in load_or_get_project_widgets(project)]
81
+ load_dataframe_images(project)
82
+ st.query_params["project"] = project_name
83
+
84
+
85
+ def reset_project_selection():
86
+ st.session_state.project.reset()
87
+ st.query_params.clear()
88
+ set_current_step(PrevalidationStep.DEFINE_PROJECT)
89
+ st.rerun()
90
+
91
+
92
+ def get_test_data_dir(config):
93
+ test_data_dir = config.get(KEY_TEST_DATA_DIR, "test_data")
94
+ return Path(__file__).parent / test_data_dir
95
+
96
+
97
+ def load_test_data(test_data_dir, project):
98
+ """Load all datasets from the test_data directory."""
99
+ if not test_data_dir.exists():
100
+ st.error(f"Test data directory `{test_data_dir}` does not exist.")
101
+ return
102
+
103
+ # Iterate over each dataset directory in test_data
104
+ for dataset_dir in test_data_dir.iterdir():
105
+ if dataset_dir.is_dir():
106
+ # Check if the dataset is already loaded to prevent duplication
107
+ if not any(d['path'] == str(dataset_dir) for d in st.session_state.project.paths):
108
+ add_new_path_with_subpath_check(str(dataset_dir), project)
109
+ st.success(f"Test data loaded from `{test_data_dir}`.")
110
+
111
+
112
+ def create_dataframe_images_from_file_structure(project):
113
+ # project.paths is now a dict[path: df_structure]
114
+ dataframe_images = aggregate_folder_dataframes(project.paths)
115
+ dataframe_images = dataframe_images.filter(pl.col("type").eq("file"))
116
+
117
+ selected_file_types = project.settings.get("selected_file_extensions", [])
118
+ if selected_file_types:
119
+ # Convert selected_file_types to lowercase for case-insensitive comparison
120
+ selected_file_types = [ft.lower() for ft in selected_file_types]
121
+ dataframe_images = dataframe_images.filter(
122
+ pl.col("file_extension").is_in(selected_file_types)
123
+ )
124
+
125
+ dataframe_images = preprocess_files(dataframe_images)
126
+ return dataframe_images
127
+
128
+
129
+ def display_project_settings(config, project):
130
+ st.header("Project Settings")
131
+
132
+ preselected_file_extensions = [
133
+ ft.lower() for ft in
134
+ (project.settings.get("selected_file_extensions") or config.get('preselected_file_extensions', []))
135
+ ]
136
+
137
+ extension_info = count_file_extensions(project.paths)
138
+ if not extension_info:
139
+ if "all_files" in extension_info and extension_info["all_files"] == 0:
140
+ st.warning("No files found in any imported directories.")
141
+ else:
142
+ st.warning("No files with valid extensions found in these directories.")
143
+ return
144
+
145
+ total_file_count = extension_info.pop("all_files", 0)
146
+ if not extension_info:
147
+ st.warning("No valid file extensions found in these directories.")
148
+ return
149
+
150
+ st.subheader("Choose Which File Extensions to Include:")
151
+ options = [f".{ext} ({extension_info[ext]})" for ext in sorted(extension_info.keys())]
152
+ default = [f".{ext} ({extension_info[ext]})" for ext in preselected_file_extensions if ext in extension_info]
153
+ selected_options = st.multiselect("Select file extensions:", options, default=default)
154
+
155
+ # Extract the selected extensions (remove the count from the label)
156
+ selected_file_extensions = [opt.split(" ")[0][1:] for opt in selected_options]
157
+
158
+ project.settings["selected_file_extensions"] = selected_file_extensions
159
+
160
+ chosen_count = sum(extension_info[ext] for ext in selected_file_extensions)
161
+ st.info(f"Chosen {chosen_count} out of {total_file_count} total files.")
162
+
163
+ # display_fast_mode(project)
164
+
165
+ cmap = project.settings.get("cmap", 'rainbow')
166
+ cmap = choose_matplotlib_colormap(cmap)
167
+ project.settings["cmap"] = cmap
168
+
169
+ example_images = st.slider("Number of example images", min_value=1, max_value=15, value=9)
170
+ project.settings["example_images"] = example_images
171
+ st.write(f"Number of example images: {example_images}")
172
+
173
+ if st.button("Confirm Settings and Process Files"):
174
+ with st.spinner("Processing ongoing..."):
175
+
176
+ if project.has_project_config_changed() or file_structure_cache_missing(project):
177
+ project.save_project_to_yml()
178
+ st.info("Project configuration has changed. All caches will be updated.")
179
+ cache_all_project_path_structures(project)
180
+
181
+ to_be_processed_files = load_dataframe_images(project)
182
+ if to_be_processed_files is None:
183
+ # If no cache was available, create from scratch
184
+ to_be_processed_files = create_dataframe_images_from_file_structure(project)
185
+
186
+ to_be_processed_files = set_colors(to_be_processed_files, project)
187
+ widgets = load_or_get_project_widgets(project)
188
+ columns = get_required_columns(widgets)
189
+ processed_files = process_files(to_be_processed_files, columns)
190
+
191
+ project.df_images = processed_files
192
+ store_all_dataframe_images_to_cache(processed_files)
193
+
194
+ st.balloons()
195
+ set_current_step(PrevalidationStep.REPORT)
196
+ st.rerun()
197
+
198
+
199
+ def create_main_section(config, project: Project, current_step: PrevalidationStep):
200
+
201
+ if step_is_disabled(project, current_step):
202
+ set_current_step(PrevalidationStep.DEFINE_PROJECT)
203
+ st.rerun()
204
+
205
+ match current_step:
206
+ case PrevalidationStep.DEFINE_PROJECT:
207
+ if project.name is None:
208
+ st.header("Define Project")
209
+ project = select_project(project)
210
+ if project.name is None:
211
+ return
212
+
213
+ st.header(f"Project: {project.name}")
214
+
215
+ if st.button("Select a different project", key="reset_project_main"):
216
+ reset_project_selection()
217
+
218
+ st.subheader("Directory Selection")
219
+ if project.paths:
220
+ for directory in project.paths:
221
+ add_new_path_with_subpath_check(directory, project, show_success=False, suppress_warning=True)
222
+ st.success("Project directories loaded from configuration.")
223
+
224
+ select_directories(config, project)
225
+ if st.button("Confirm Directory Selection", key="confirm_dirs"):
226
+ if not project.paths:
227
+ st.warning("Please add at least one valid directory with files or folders.")
228
+ else:
229
+ set_current_step(PrevalidationStep.SELECT_WIDGETS)
230
+ st.rerun()
231
+
232
+ case PrevalidationStep.SELECT_WIDGETS:
233
+ choose_widgets(project)
234
+
235
+ case PrevalidationStep.SETTINGS:
236
+ display_project_settings(config, project)
237
+
238
+ case PrevalidationStep.REPORT:
239
+ project = st.session_state.project
240
+ st.toggle(
241
+ "Print Mode",
242
+ value=project.is_report_mode,
243
+ on_change=lambda: setattr(project, "is_report_mode", not project.is_report_mode)
244
+ )
245
+ enabled_widgets = [w for w in get_all_widgets() if w.name in project.selected_widgets]
246
+ tabbed_widgets = organize_widgets_by_tab(enabled_widgets)
247
+
248
+ if project.is_report_mode:
249
+ display_widgets_as_report(tabbed_widgets, project.df_images.filter(pl.col("type") == "file"))
250
+ else:
251
+ display_widgets_as_tab(tabbed_widgets, project.df_images.filter(pl.col("type") == "file"))
252
+
253
+
254
+ def select_directories(config, project):
255
+ if is_debug_mode():
256
+ test_data_dir = get_test_data_dir(config)
257
+ load_test_data(test_data_dir, project)
258
+ st.header("Debug Mode Enabled")
259
+ st.write(f"Using test data from `{test_data_dir}`")
260
+ else:
261
+ create_add_folder_button(project)
262
+ display_imported_dirs_info(project)
263
+
264
+
265
+ def process_query(project):
266
+ query_params = st.query_params
267
+ if "project" in query_params and query_params["project"]:
268
+ set_active_project(project, query_params["project"])
269
+ if "view" in query_params and query_params["view"]:
270
+ view = query_params["view"]
271
+ view_step = PrevalidationStep[view]
272
+ if view_step:
273
+ set_current_step(view_step)
274
+
275
+
276
+ def main():
277
+ config = load_config()
278
+ initialize_session_state(config)
279
+ project = get_project()
280
+ configure_layout(project)
281
+ process_query(project)
282
+ current_step = get_current_step()
283
+ if not project.is_report_mode:
284
+ create_sidebar(project, current_step)
285
+ create_main_section(config, project, current_step)
286
+
287
+
288
+ if __name__ == "__main__":
289
+ main()
@@ -0,0 +1,190 @@
1
+ from typing import Dict, List, Optional, Any
2
+
3
+ import polars as pl
4
+ import streamlit as st
5
+ import yaml
6
+
7
+ from pixel_patrol.utils.utils import hash_file_path, get_projects_dir
8
+
9
+
10
+ class Project:
11
+ def __init__(self):
12
+ # Session state keys
13
+ self._session_keys = {
14
+ "name": "project_name",
15
+ "yml": "project_yml",
16
+ "paths": "imported_paths",
17
+ "path_summaries": "path_summaries",
18
+ "df_images": "df_images",
19
+ "selected_widgets": "selected_widgets",
20
+ "settings": "project_settings",
21
+ "is_report_mode": "is_report_mode",
22
+ }
23
+
24
+ # --- Properties for saved state (yml) ---
25
+ @property
26
+ def yml(self) -> Dict[str, Any]:
27
+ """The saved state of the project (from the YAML file)."""
28
+ return st.session_state.get(self._session_keys["yml"], {})
29
+
30
+ @yml.setter
31
+ def yml(self, value: Dict[str, Any]):
32
+ """Update the saved state of the project."""
33
+ st.session_state[self._session_keys["yml"]] = value
34
+
35
+ # --- Properties for runtime state ---
36
+ @property
37
+ def name(self) -> Optional[str]:
38
+ """The runtime state of the project name."""
39
+ return st.session_state.get(self._session_keys["name"])
40
+
41
+ @name.setter
42
+ def name(self, value: Optional[str]):
43
+ """Update the runtime state of the project name."""
44
+ st.session_state[self._session_keys["name"]] = value
45
+
46
+ @property
47
+ def paths(self) -> Dict[str, pl.DataFrame]:
48
+ """The runtime state of imported paths (full structure dfs)."""
49
+ return st.session_state.get(self._session_keys["paths"], {})
50
+
51
+ @paths.setter
52
+ def paths(self, value: Dict[str, pl.DataFrame]):
53
+ """Update the runtime state of imported paths."""
54
+ st.session_state[self._session_keys["paths"]] = value
55
+
56
+ @property
57
+ def path_summaries(self) -> Dict[str, pl.DataFrame]:
58
+ return st.session_state.get("path_summaries", {})
59
+
60
+ @path_summaries.setter
61
+ def path_summaries(self, value: Dict[str, pl.DataFrame]):
62
+ st.session_state["path_summaries"] = value
63
+
64
+ @property
65
+ def df_images(self) -> pl.DataFrame:
66
+ """The runtime state of the dataframe for images."""
67
+ return st.session_state.get(self._session_keys["df_images"], pl.DataFrame())
68
+
69
+ @df_images.setter
70
+ def df_images(self, value: pl.DataFrame):
71
+ """Update the runtime state of the dataframe for images."""
72
+ st.session_state[self._session_keys["df_images"]] = value
73
+
74
+ @property
75
+ def selected_widgets(self) -> Optional[List[Any]]:
76
+ """The runtime state of selected widgets."""
77
+ return st.session_state.get(self._session_keys["selected_widgets"])
78
+
79
+ @selected_widgets.setter
80
+ def selected_widgets(self, value: Optional[List[Any]]):
81
+ """Update the runtime state of selected widgets."""
82
+ st.session_state[self._session_keys["selected_widgets"]] = value
83
+
84
+ @property
85
+ def settings(self) -> Dict[str, Any]:
86
+ """The runtime state of project settings."""
87
+ return st.session_state.get(self._session_keys["settings"], {})
88
+
89
+ @settings.setter
90
+ def settings(self, value: Dict[str, Any]):
91
+ """Update the runtime state of project settings."""
92
+ st.session_state[self._session_keys["settings"]] = value
93
+
94
+ @property
95
+ def is_report_mode(self) -> bool:
96
+ """The runtime state of report mode."""
97
+ return st.session_state.get(self._session_keys["is_report_mode"], False)
98
+
99
+ @is_report_mode.setter
100
+ def is_report_mode(self, value: bool):
101
+ """Update the runtime state of report mode."""
102
+ st.session_state[self._session_keys["is_report_mode"]] = value
103
+
104
+ # --- Helper Methods ---
105
+ def reset(self):
106
+ """Reset the project to its initial state."""
107
+ self.name = None
108
+ self.yml = {}
109
+ self.paths = {}
110
+ self.path_summaries = {}
111
+ self.df_images = pl.DataFrame()
112
+ self.selected_widgets = None
113
+ self.settings = {}
114
+ self.is_report_mode = False
115
+
116
+ def load_project_from_yml(self, project_name: str):
117
+ """Load a project from its YAML file and initialize the runtime state."""
118
+ self.yml = self._read_yml(project_name)
119
+ self.name = self.yml.get("name")
120
+ self.paths = self.yml.get("paths", [])
121
+ self.selected_widgets = self.yml.get("selected_widgets")
122
+ self.settings = self.yml.get("settings", {})
123
+
124
+ def exists(self):
125
+ return Project.get_project_filepath(self.name).exists()
126
+
127
+ def save_project_to_yml(self):
128
+ if self.name:
129
+ paths_hashed = {path: hash_file_path(path) for path in self.paths.keys()}
130
+ widget_names = [w for w in (self.selected_widgets or [])]
131
+ self.yml = {
132
+ "name": self.name,
133
+ "paths": paths_hashed,
134
+ "selected_widgets": widget_names,
135
+ "settings": self.settings,
136
+ }
137
+ self._write_yml()
138
+
139
+ def _write_yml(self):
140
+ """Save the yml to a YAML file."""
141
+ projects_dir = get_projects_dir()
142
+ projects_dir.mkdir(exist_ok=True)
143
+ filepath = projects_dir / f"{self.name}.yml"
144
+ with open(filepath, "w") as f:
145
+ yaml.safe_dump(self.yml, f)
146
+
147
+ @staticmethod
148
+ def _read_yml(project_name: str) -> Dict[str, Any]:
149
+ """Load the yml from a YAML file."""
150
+ filepath = Project.get_project_filepath(project_name)
151
+ if filepath.exists():
152
+ with open(filepath, "r") as f:
153
+ return yaml.safe_load(f)
154
+ return {}
155
+
156
+ @staticmethod
157
+ def get_project_filepath(project_name):
158
+ projects_dir = get_projects_dir()
159
+ filepath = projects_dir / f"{project_name}.yml"
160
+ return filepath
161
+
162
+ def has_project_config_changed(self) -> bool:
163
+ """
164
+ Check if the project configuration has changed compared to the saved YAML.
165
+ Compares:
166
+ - Paths: Only the keys (directory paths).
167
+ - Selected Widgets: The list of selected widgets.
168
+ - Settings: Only the `selected_file_extensions` list.
169
+ """
170
+ saved_yml = self.yml
171
+
172
+ # Compare paths (only keys)
173
+ saved_paths = set(saved_yml.get("paths", {}).keys())
174
+ current_paths = set(self.paths.keys())
175
+ if saved_paths != current_paths:
176
+ return True
177
+
178
+ # Compare selected widgets
179
+ saved_widgets = set(saved_yml.get("selected_widgets", []))
180
+ current_widgets = set(self.selected_widgets or [])
181
+ if saved_widgets != current_widgets:
182
+ return True
183
+
184
+ # Compare selected file extensions in settings
185
+ saved_settings = saved_yml.get("settings", {})
186
+ current_settings = self.settings
187
+ if saved_settings != current_settings:
188
+ return True
189
+
190
+ return False
@@ -0,0 +1,23 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List
3
+
4
+
5
+ class ImagePrevalidationWidget(ABC):
6
+ @property
7
+ @abstractmethod
8
+ def tab(self) -> str:
9
+ """Return the name of the tab this widget belongs to."""
10
+ pass
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ return type(self).__name__
15
+
16
+ def required_columns(self) -> List[str]:
17
+ """Returns required data column names"""
18
+ return []
19
+
20
+ @abstractmethod
21
+ def run(self, *args, **kwargs):
22
+ """Execute the widget's functionality."""
23
+ pass
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: pixel-patrol
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: bs4>=0.0.2
8
+ Requires-Dist: imageio
9
+ Requires-Dist: scikit-image
10
+ Requires-Dist: matplotlib>=3.10.0
11
+ Requires-Dist: pandas>=2.2.3
12
+ Requires-Dist: ray>=2.40.0
13
+ Requires-Dist: streamlit>=1.41.1
14
+ Requires-Dist: altair>=5.5.0
15
+ Requires-Dist: numpy<2.0.0,>=1.21.0
16
+ Requires-Dist: image>=1.5.33
17
+ Requires-Dist: tifffile<2025.2.18,>=2024.12.12
18
+ Requires-Dist: plotly>=5.24.1
19
+ Requires-Dist: plotly-express>=0.4.1
20
+ Requires-Dist: tk>=0.1.0
21
+ Requires-Dist: bioio==1.0.2
22
+ Requires-Dist: bioio-czi>=1.0.2
23
+ Requires-Dist: bioio-lif>=1.0.0
24
+ Requires-Dist: bioio-nd2>=1.0.0
25
+ Requires-Dist: bioio-tifffile>=1.0.0
26
+ Requires-Dist: bioio-ome-zarr>=1.1.0
27
+ Requires-Dist: bioio-ome-tiff>=1.0.1
28
+ Requires-Dist: bioio-dv>=1.0.0
29
+ Requires-Dist: pyyaml>=6.0.2
30
+ Requires-Dist: polars>=1.22.0
31
+ Requires-Dist: streamlit-extras>=0.5.0
32
+ Requires-Dist: tensorboard>=2.18.0
33
+ Requires-Dist: tensorflow>=2.18.0
34
+ Requires-Dist: pywavelets>=1.8.0
35
+ Requires-Dist: opencv-python-headless>=4.11.0.86
36
+
37
+ # Pixel Patrol
38
+
39
+ ## How to use
40
+
41
+ 1. Clone repository
42
+ 2. Install `uv`
43
+ 3. `uv pip install -e .`
44
+ 4. `uv run streamlit_main.py`
45
+
46
+ If you get an error when adding a folder path - install tkinter/python-tk:
47
+ Ubuntu: `sudo apt-get install python3-tk`
48
+ Mac: `brew install python-tk`
49
+
50
+ ## How to add widget
51
+
52
+ Widgets can be added in this repository or in separate packages.
53
+
54
+ ### Write Widget
55
+
56
+ ```
57
+ # my/widgets/test_widget.py
58
+
59
+ import streamlit as st
60
+ from pixel_patrol.widgets.widget_interface import ImagePrevalidationWidget
61
+
62
+
63
+ class TestWidget(ImagePrevalidationWidget):
64
+
65
+ @property
66
+ def tab(self) -> str:
67
+ return "Other"
68
+
69
+ def run(self, selected_files_df):
70
+ with st.expander("Test widget", expanded=False):
71
+ st.text("Hi!")
72
+ ```
73
+
74
+ ### Add Widget to `entry-points` of the package
75
+
76
+ ```
77
+ # pyproject.toml
78
+
79
+ [project]
80
+ ...
81
+ [project.entry-points."pixel_patrol.widgets"]
82
+ test_widget = "my.widgets.test_widget:TestWidget"
83
+
84
+ ```
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ pixel_patrol/__init__.py
4
+ pixel_patrol/default_tabs.py
5
+ pixel_patrol/default_values.py
6
+ pixel_patrol/main.py
7
+ pixel_patrol/project.py
8
+ pixel_patrol/widget_interface.py
9
+ pixel_patrol.egg-info/PKG-INFO
10
+ pixel_patrol.egg-info/SOURCES.txt
11
+ pixel_patrol.egg-info/dependency_links.txt
12
+ pixel_patrol.egg-info/entry_points.txt
13
+ pixel_patrol.egg-info/requires.txt
14
+ pixel_patrol.egg-info/top_level.txt
@@ -0,0 +1,13 @@
1
+ [pixel_patrol.widgets]
2
+ data_type = pixel_patrol.widgets.metadata.data_type:DataTypeWidget
3
+ dataframe = pixel_patrol.widgets.summary.dataframe:DataFrameWidget
4
+ dataset_stats = pixel_patrol.widgets.dataset_stats.dataset_stats:DatasetStatsWidget
5
+ dim_order = pixel_patrol.widgets.metadata.dim_order:DimOrderWidget
6
+ dim_size = pixel_patrol.widgets.metadata.dim_size:DimSizeWidget
7
+ embedding_projector = pixel_patrol.widgets.visualization.embedding_projector:EmbeddingProjectorWidget
8
+ file_extension = pixel_patrol.widgets.file_stats.file_extension:FileExtensionWidget
9
+ file_size = pixel_patrol.widgets.file_stats.file_size:FileSizeWidget
10
+ file_timestamp = pixel_patrol.widgets.file_stats.file_timestamp:FileTimestampWidget
11
+ image_mosaik = pixel_patrol.widgets.visualization.image_mosaik:ImageMosaikWidget
12
+ image_quality = pixel_patrol.widgets.dataset_stats.image_quality:ImageQualityWidget
13
+ summary = pixel_patrol.widgets.summary.summary:SummaryWidget
@@ -0,0 +1,29 @@
1
+ bs4>=0.0.2
2
+ imageio
3
+ scikit-image
4
+ matplotlib>=3.10.0
5
+ pandas>=2.2.3
6
+ ray>=2.40.0
7
+ streamlit>=1.41.1
8
+ altair>=5.5.0
9
+ numpy<2.0.0,>=1.21.0
10
+ image>=1.5.33
11
+ tifffile<2025.2.18,>=2024.12.12
12
+ plotly>=5.24.1
13
+ plotly-express>=0.4.1
14
+ tk>=0.1.0
15
+ bioio==1.0.2
16
+ bioio-czi>=1.0.2
17
+ bioio-lif>=1.0.0
18
+ bioio-nd2>=1.0.0
19
+ bioio-tifffile>=1.0.0
20
+ bioio-ome-zarr>=1.1.0
21
+ bioio-ome-tiff>=1.0.1
22
+ bioio-dv>=1.0.0
23
+ pyyaml>=6.0.2
24
+ polars>=1.22.0
25
+ streamlit-extras>=0.5.0
26
+ tensorboard>=2.18.0
27
+ tensorflow>=2.18.0
28
+ pywavelets>=1.8.0
29
+ opencv-python-headless>=4.11.0.86
@@ -0,0 +1 @@
1
+ pixel_patrol
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "pixel-patrol" # This is the PyPI name (with hyphen)
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "bs4>=0.0.2",
9
+ "imageio",
10
+ "scikit-image",
11
+ "matplotlib>=3.10.0",
12
+ "pandas>=2.2.3",
13
+ "ray>=2.40.0",
14
+ "streamlit>=1.41.1",
15
+ "altair>=5.5.0",
16
+ "numpy>=1.21.0,<2.0.0",
17
+ "image>=1.5.33",
18
+ "tifffile>=2024.12.12,<2025.2.18",
19
+ "plotly>=5.24.1",
20
+ "plotly-express>=0.4.1",
21
+ "tk>=0.1.0",
22
+ "bioio==1.0.2",
23
+ "bioio-czi>=1.0.2",
24
+ "bioio-lif>=1.0.0",
25
+ "bioio-nd2>=1.0.0",
26
+ "bioio-tifffile>=1.0.0",
27
+ "bioio-ome-zarr>=1.1.0",
28
+ "bioio-ome-tiff>=1.0.1",
29
+ "bioio-dv>=1.0.0",
30
+ "pyyaml>=6.0.2",
31
+ "polars>=1.22.0",
32
+ "streamlit-extras>=0.5.0",
33
+ "tensorboard>=2.18.0",
34
+ "tensorflow>=2.18.0",
35
+ "pywavelets>=1.8.0",
36
+ "opencv-python-headless>=4.11.0.86",
37
+ ]
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "pytest>=8.3.4",
42
+ ]
43
+
44
+ [tool.setuptools]
45
+ packages = ["pixel_patrol"] # This is the Python package name (with underscore)
46
+
47
+ [project.entry-points."pixel_patrol.widgets"] # Changed from image_prevalidation.widgets
48
+ file_timestamp = "pixel_patrol.widgets.file_stats.file_timestamp:FileTimestampWidget"
49
+ file_size = "pixel_patrol.widgets.file_stats.file_size:FileSizeWidget"
50
+ file_extension = "pixel_patrol.widgets.file_stats.file_extension:FileExtensionWidget"
51
+ data_type = "pixel_patrol.widgets.metadata.data_type:DataTypeWidget"
52
+ dim_order = "pixel_patrol.widgets.metadata.dim_order:DimOrderWidget"
53
+ dim_size = "pixel_patrol.widgets.metadata.dim_size:DimSizeWidget"
54
+ image_mosaik = "pixel_patrol.widgets.visualization.image_mosaik:ImageMosaikWidget"
55
+ dataset_stats = "pixel_patrol.widgets.dataset_stats.dataset_stats:DatasetStatsWidget"
56
+ embedding_projector = "pixel_patrol.widgets.visualization.embedding_projector:EmbeddingProjectorWidget"
57
+ image_quality = "pixel_patrol.widgets.dataset_stats.image_quality:ImageQualityWidget"
58
+ summary = "pixel_patrol.widgets.summary.summary:SummaryWidget"
59
+ dataframe = "pixel_patrol.widgets.summary.dataframe:DataFrameWidget"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+