-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
308 lines (248 loc) · 12.2 KB
/
main.py
File metadata and controls
308 lines (248 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
import asyncio
import io
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import micropip
import base64
import sys
from io import StringIO
from pyodide.http import pyfetch
# PyScript specific imports to interact with the browser DOM
import js
from pyscript import display, document
# ==========================================
# 1. GLOBAL STATE & FUNCTION STUBS
# ==========================================
state = {
'OrigBlue': None, 'OrigGreen': None, 'OrigRed': None,
'CropBlue': None, 'CropBlue_Mask': None, 'Nuclei': None
}
crop_fn = None
RemovalMethod = None
identify_primary_fn = None
# ==========================================
# 2. ASYNC INITIALIZATION
# ==========================================
async def setup_environment():
status_text = document.getElementById('loading-status')
try:
wheel_url = "https://sspathak.github.io/pyodide-packages/cellprofiler_library-5.0.2-py3-none-any.whl"
print(f"System: Fetching wheel from {wheel_url}...")
status_text.innerText = "Downloading CellProfiler WASM Library..."
await micropip.install(wheel_url)
print("System: ✅ Successfully installed cellprofiler_library!")
status_text.innerText = "Library installed. Compiling dependencies..."
global crop_fn, RemovalMethod, identify_primary_fn
from cellprofiler_library.modules._crop import crop as crop_fn
from cellprofiler_library.opts.crop import RemovalMethod
from cellprofiler_library.modules._identifyprimaryobjects import identifyprimaryobjects as identify_primary_fn
print("""
from cellprofiler_library.modules._crop import crop as crop_fn
from cellprofiler_library.opts.crop import RemovalMethod
from cellprofiler_library.modules._identifyprimaryobjects import identifyprimaryobjects as identify_primary_fn
""")
print("System: ✅ Library modules imported successfully.")
# ----------------------------------------------------
# THE UI REVEAL SEQUENCE
# ----------------------------------------------------
status_text.innerText = "Ready!"
# 1. Fade out the white loading screen
document.getElementById('loading-overlay').style.opacity = '0'
# 2. Auto-minimize the Developer Dock using our JS function
# js.toggleDock()
# Wait for the CSS fade transition to finish before hiding overlay completely
await asyncio.sleep(0.5)
document.getElementById('loading-overlay').style.display = 'none'
# 3. Reveal the main UI
document.getElementById('ui-container').style.display = 'block'
except Exception as e:
print(f"System: ❌ Failed to install or import wheel: {e}")
status_text.innerText = f"Error: Failed to load environment.\n{e}"
status_text.style.color = "red"
# ==========================================
# 3. HELPER FUNCTIONS
# ==========================================
async def get_image_from_input(input_id):
file_list = document.getElementById(input_id).files
if file_list.length == 0: return None
file = file_list.item(0)
js_buffer = await file.arrayBuffer()
js_array = js.Uint8Array.new(js_buffer)
bytes_data = js_array.to_bytes()
img = Image.open(io.BytesIO(bytes_data)).convert('L')
return np.array(img, dtype=np.float32) / 255.0
async def get_image_from_url(url_string):
"""Fetches an image from a URL and converts to normalized Numpy grayscale float array."""
if not url_string.strip():
return None
response = await pyfetch(url_string)
if not response.ok:
raise Exception(f"HTTP {response.status}: Failed to fetch {url_string}")
bytes_data = await response.bytes()
img = Image.open(io.BytesIO(bytes_data)).convert('L')
return np.array(img, dtype=np.float32) / 255.0
def plot_outputs(img1, img2, title1, title2, target_div_id):
"""Renders Matplotlib figure to a Base64 image string for bulletproof DOM insertion."""
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
# Plot original grayscale image
axes[0].imshow(img1, cmap='gray')
axes[0].set_title(title1)
axes[0].axis('off')
# Detect if img2 is a segmentation label matrix (integers > 1)
if img2.dtype in [np.int32, np.int64, np.uint16, np.uint32] and np.max(img2) > 1:
# CellProfiler Style: Use a discrete colormap but force 0 (background) to black
import matplotlib.colors as mcolors
cmap = plt.get_cmap('tab20')
cmap.set_under('black')
axes[1].imshow(img2, cmap=cmap, vmin=0.1, interpolation='nearest')
else:
# Standard mask or grayscale
axes[1].imshow(img2, cmap='gray')
axes[1].set_title(title2)
axes[1].axis('off')
plt.tight_layout()
# ---------------------------------------------------------
# The Fix: Render to Bytes, encode to Base64, and inject HTML
# ---------------------------------------------------------
buf = io.BytesIO()
fig.savefig(buf, format='png', bbox_inches='tight')
buf.seek(0)
img_str = base64.b64encode(buf.read()).decode('utf-8')
img_html = f'<img src="data:image/png;base64,{img_str}" style="max-width: 100%; height: auto; border-radius: 4px;">'
# Insert directly into the DOM
document.getElementById(target_div_id).innerHTML = img_html
# Now it is completely safe to close the figure and free memory!
plt.close(fig)
# ==========================================
# 4. UI EVENT HANDLERS
# ==========================================
async def on_load_images(event):
out_div = document.getElementById("out-uploads")
out_div.innerText = "⏳ Initializing image load..."
out_div.style.color = "#ffc107"
await asyncio.sleep(0.05)
# Check the selected mode
mode = document.querySelector('input[name="source-type"]:checked').value
try:
if mode == "upload":
print("System: Loading 3-channel set from local files...")
state['OrigBlue'] = await get_image_from_input('upload-blue')
state['OrigGreen'] = await get_image_from_input('upload-green')
state['OrigRed'] = await get_image_from_input('upload-red')
else:
print("System: Fetching 3-channel set from CellProfiler GitHub...")
# Note: Restored OrigRed URL mapping
url_blue = document.getElementById('url-blue').value
url_green = document.getElementById('url-green').value
url_red = document.getElementById('url-red').value
state['OrigBlue'] = await get_image_from_url(url_blue)
state['OrigGreen'] = await get_image_from_url(url_green)
state['OrigRed'] = await get_image_from_url(url_red)
# Basic validation: At least Blue and Green are required for the demo pipeline
if state['OrigBlue'] is not None and state['OrigGreen'] is not None:
out_div.innerText = f"✅ Success: 3 Channels loaded via {mode.upper()}"
out_div.style.color = "#28a745"
print(f"System: Loaded shapes: B:{state['OrigBlue'].shape}, G:{state['OrigGreen'].shape}")
else:
out_div.innerText = "⚠️ Load failed: Missing required channels."
out_div.style.color = "#dc3545"
except Exception as e:
print(f"FATAL ERROR during load: {e}")
out_div.innerText = f"❌ Load Failed: {str(e)}"
out_div.style.color = "#dc3545"
async def on_crop_b(event):
status_div = document.getElementById("out-crop-b-status")
status_div.innerText = "⏳ Processing crop..."
status_div.style.color = "#ffc107"
await asyncio.sleep(0.05)
if crop_fn is None or state['OrigBlue'] is None:
status_div.innerText = "❌ Error: Library or OrigBlue missing."
status_div.style.color = "#dc3545"
return
try:
t = int(document.getElementById("crop-b-top").value)
b = int(document.getElementById("crop-b-bottom").value)
l = int(document.getElementById("crop-b-left").value)
r = int(document.getElementById("crop-b-right").value)
img_shape = state['OrigBlue'].shape
cropping_mask = np.zeros(img_shape, dtype=bool)
cropping_mask[t:b, l:r] = True
cropped_pixels, out_mask, _ = crop_fn(
orig_image_pixels=state['OrigBlue'],
cropping=cropping_mask, mask=None, orig_image_mask=None,
removal_method=RemovalMethod.EDGES
)
state['CropBlue'] = cropped_pixels
state['CropBlue_Mask'] = out_mask
status_div.innerText = "✅ Crop Complete!"
status_div.style.color = "#28a745"
# Render and automatically pop open the dropdown
plot_outputs(state['OrigBlue'], state['CropBlue'], "OrigBlue", "CropBlue", "out-crop-b-viz")
document.getElementById("details-crop-b").open = True
except Exception as e:
status_div.innerText = f"❌ Execution Error:\n{e}"
status_div.style.color = "#dc3545"
async def on_ip(event):
status_div = document.getElementById("out-ip-status")
status_div.innerText = "⏳ Identifying nuclei..."
status_div.style.color = "#ffc107"
await asyncio.sleep(0.05)
if identify_primary_fn is None or state['CropBlue'] is None:
status_div.innerText = "❌ Error: Library or CropBlue missing."
status_div.style.color = "#dc3545"
return
try:
min_d = int(document.getElementById("ip-min").value)
max_d = int(document.getElementById("ip-max").value)
state['Nuclei'] = identify_primary_fn(
image=state['CropBlue'],
min_size=min_d, max_size=max_d,
exclude_size=True, return_cp_output=False
)
status_div.innerText = "✅ Identify Primary Complete!"
status_div.style.color = "#28a745"
# Render the segmentation and open the dropdown
plot_outputs(state['CropBlue'], state['Nuclei'], "CropBlue", "Nuclei Segmentations", "out-ip-viz")
document.getElementById("details-ip").open = True
except Exception as e:
status_div.innerText = f"❌ Execution Error:\n{e}"
status_div.style.color = "#dc3545"
async def on_repl_run(event):
"""Executes custom Python code, captures stdout, and writes directly to the DOM."""
code_input = document.getElementById("repl-input")
code = code_input.value
if not code.strip():
return
terminal = document.getElementById("boot-terminal")
# 1. Echo the command to the terminal safely
safe_code = code.strip().replace('<', '<').replace('>', '>')
terminal.innerHTML += f"<div style='color: #00ff00;'>\n[REPL] >>> {safe_code}</div>"
await asyncio.sleep(0.05)
try:
# 2. Try to evaluate as an expression first
try:
result = eval(code, globals())
if result is not None:
safe_res = repr(result).replace('<', '<').replace('>', '>')
terminal.innerHTML += f"<div style='color: #fff;'>{safe_res}</div>"
# 3. If it's a statement, use exec() and capture ANY internal print() statements
except SyntaxError:
old_stdout = sys.stdout
redirected_output = sys.stdout = StringIO()
try:
exec(code, globals())
finally:
sys.stdout = old_stdout # Always restore stdout!
# If the user's code printed anything, inject it
printed_out = redirected_output.getvalue()
if printed_out:
safe_out = printed_out.replace('<', '<').replace('>', '>')
terminal.innerHTML += f"<div style='color: #fff;'>{safe_out}</div>"
except Exception as e:
terminal.innerHTML += f"<div class='log-error'>[REPL ERROR]: {e}</div>"
# 4. Clear the input and auto-scroll the terminal to the bottom
code_input.value = ""
js.eval("var t = document.getElementById('boot-terminal'); t.scrollTop = t.scrollHeight;")
# Start the boot sequence
asyncio.ensure_future(setup_environment())