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.
- pixel_patrol-0.1.0/PKG-INFO +84 -0
- pixel_patrol-0.1.0/README.md +48 -0
- pixel_patrol-0.1.0/pixel_patrol/__init__.py +0 -0
- pixel_patrol-0.1.0/pixel_patrol/default_tabs.py +12 -0
- pixel_patrol-0.1.0/pixel_patrol/default_values.py +11 -0
- pixel_patrol-0.1.0/pixel_patrol/main.py +289 -0
- pixel_patrol-0.1.0/pixel_patrol/project.py +190 -0
- pixel_patrol-0.1.0/pixel_patrol/widget_interface.py +23 -0
- pixel_patrol-0.1.0/pixel_patrol.egg-info/PKG-INFO +84 -0
- pixel_patrol-0.1.0/pixel_patrol.egg-info/SOURCES.txt +14 -0
- pixel_patrol-0.1.0/pixel_patrol.egg-info/dependency_links.txt +1 -0
- pixel_patrol-0.1.0/pixel_patrol.egg-info/entry_points.txt +13 -0
- pixel_patrol-0.1.0/pixel_patrol.egg-info/requires.txt +29 -0
- pixel_patrol-0.1.0/pixel_patrol.egg-info/top_level.txt +1 -0
- pixel_patrol-0.1.0/pyproject.toml +59 -0
- pixel_patrol-0.1.0/setup.cfg +4 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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"
|