-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcell_montages.py
446 lines (348 loc) · 17.5 KB
/
cell_montages.py
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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# This is a reimplemenation of the montaging script in python instead of fiji's macro languagdthis script is meant to be run on an single zed image (either a MIP or a single z) with many ROIs selected.
# This creates a grayscale montage of each ROI with labels on each panel,
# and saves it as a .png file. It saves an overview image with all channels and a montage of each individual channel.
# the png image is a single frame from the center of the stack, ish. it's pretty ugly whoopsies.
from ij import IJ, WindowManager
#from ij import ImagePlus, StackConverter
from ij.plugin.frame import RoiManager
from ij.gui import GenericDialog
import os
import sys
from datetime import datetime
from ij.gui import Overlay, TextRoi, Roi, GenericDialog
from java.awt import Font, Color, FontMetrics
#stuff for saving
from ij.io import FileSaver
import os
# in fiji the dialog is nice because i otherwise hardcode everyhting
from ij import IJ, WindowManager
from ij.gui import GenericDialog
# Function to create and show the dialog
def showParametersDialog(image):
"""
Displays a dialog window for specifying image information.
Parameters:
- image: The open fiji image for which information is being specified.
Returns:
tuple: A tuple containing the following information:
- date (str): Date in YYMMDD format.
- roi_set_filename (str): Filename for the ROI set.
- n_chans (int): Number of channels.
- downsampling_scale (float): Downsampling scale.
- annotation_font_size (int): Annotation font size.
- montage_rows (int): Number of montage rows.
- pixel_adjust (int): Pixel adjustment.
- col_offset (float): Column offset.
"""
gd = GenericDialog("Specify Image Information") #name the window
gd.setInsets(40, 0, 0)
imgName = image.getTitle() # Get the title of the currently open window
default_date = datetime.now().strftime("%y%m%d")
filename_parts = imgName.split("_")
## this section is for files with thef orm ID-ixxx
exp_id = extract_string_with_prefix(imgName, prefix="ID")
default_sample_id = "{}".format(exp_id)
default_roi_set_filename = "RoiSet_{}.zip".format(default_sample_id)
## for jules formatted data
# default_sample_id = "{}-{}".format(filename_parts[2], filename_parts[3])
default_frame = "{}".format((filename_parts[len(filename_parts)-1][1:3]))
default_sample_id = "{}-{}".format(filename_parts[2], filename_parts[3])
# default_image_identifier = "ID-{}_FOV-{}".format(default_sample_id, default_frame)
# default_roi_set_filename = "RoiSet_{}.zip".format(default_image_identifier)
default_n_chans = image.getNChannels() if image else 6
# Add input fields to the dialog
gd.addStringField("Date (YYMMDD):", default_date)
gd.addStringField("Sample ID (iXXXX-sNN):", default_sample_id)
gd.addStringField("FOV:", default_frame)
gd.addStringField("ROI Set Filename:", default_roi_set_filename)
gd.addNumericField("Number of Channels:", default_n_chans, 0)
gd.addNumericField("Downsampling Scale (when montaging):", 0.8, 2)
gd.addNumericField("Annotation size\n(as fraction of image)", 15, 0)
gd.addNumericField("Montage Rows:", 1, 0)
gd.addNumericField("Pixel Adjustment (not used):", 3, 0)
gd.addNumericField("Column Offset: (not used", 0.03, 2)
# Show the dialog
gd.showDialog()
if gd.wasCanceled():
print("Dialog canceled")
sys.exit()
# Retrieve values from the dialog
date = gd.getNextString()
sample_id = gd.getNextString()
fov = gd.getNextString()
roi_set_filename = gd.getNextString()
n_chans = int(gd.getNextNumber())
downsampling_scale = gd.getNextNumber()
annotation_font_size = int(gd.getNextNumber())
montage_rows = int(gd.getNextNumber())
pixel_adjust = int(gd.getNextNumber())
col_offset = gd.getNextNumber()
image_identifier = "ID-{}_FOV-{}".format(sample_id, fov)
print("roi name was parsed as: {}".format(roi_set_filename))
return date, image_identifier, roi_set_filename, n_chans, downsampling_scale, annotation_font_size, montage_rows, pixel_adjust, col_offset
def extract_string_with_prefix(input_string, prefix="ch"):
# Split the input string by the character "_"
parts = input_string.split("_")
# Loop through the parts to find the one that starts with the specified prefix
for part in parts:
if part.startswith(prefix):
return part
# If no string starting with the specified prefix is found, return None
return None
def getChannelNamesForm(image, n_chans, start_pos_chans=6):
"""
Displays a dialog window for specifying channel names based on the image filename.
Parameters:
- image (ImagePlus): The image for which channel names are being specified.
- n_chans (int): The number of channels.
Returns:
list or None: A list containing the specified channel names. Returns None if the dialog is canceled or if the filename doesn't parse correctly.
"""
gd = GenericDialog("Specify Channel Names")
gd.setInsets(40, 0, 0)
name = image.getTitle() # Get the title of the currently open window
# Extract default channel names from the filename
filename_parts = extract_string_with_prefix(name)
try:
filename_parts = filename_parts.split("_")
default_channel_names = filename_parts[1:n_chans+1]
except:
default_channel_names = ""
for i in range(1, n_chans+1):
default_value = default_channel_names[i-1] if i <= len(default_channel_names) else ""
gd.addStringField("Channel {}:".format(i), default_value)
gd.showDialog()
if gd.wasCanceled():
return None
# Retrieve channel names from the form
channel_names = [gd.getNextString() for _ in range(n_chans)]
return channel_names
def createOutputFolders(directory):
"""
Create TIFFs and PNGs folders in the specified directory.
Parameters:
- directory (str): The directory in which to create the folders.
Returns:
tuple: A tuple containing the paths to the TIFFs and PNGs folders.
"""
tiff_folder = os.path.join(directory, "tiffs")
png_folder = os.path.join(directory, "pngs")
# Create folders if they don't exist
for folder in [tiff_folder, png_folder]:
if not os.path.exists(folder):
print("Creating folder: {}".format(folder))
os.makedirs(folder)
return tiff_folder, png_folder
def get_max_text_height(font_size):
"""
Calculate the maximum height of a text rendered with a specified font size.
Parameters:
- font_size (int): The font size for which to calculate the maximum text height.
Returns:
int: The maximum height of the text bounding box for the given font size.
"""
temp_roi = TextRoi(0, 0, "Sample Text", Font("SansSerif", Font.PLAIN, font_size))
temp_roi.setStrokeColor(Color(0, 0, 0, 0))
return temp_roi.getBounds().height
def define_widths(montage_file, montage_col, montage_row, offset, annotation_font_size_percentage):
"""
Calculate and print various dimensions and offsets for a montage of images.
Parameters:
- montage_col (int): Number of columns in the montage.
- montage_row (int): Number of rows in the montage.
- offset (float): Offset factor for column and height offsets.
- annotation_font_size_percentage (float): Percentage of image size to use as font size.
Returns:
single_img_height, single_img_width, row_offset, col_offset, font_size
Prints:
- The entire montage size in pixels.
- Size of each frame in the montage.
- Column offset (from the left edge) and height offset.
- Calculated font size and maximum text height for annotations.
"""
# get the deets
montage_width = IJ.getImage().getWidth()
montage_height = IJ.getImage().getHeight()
# figure out each individual space
single_img_width = montage_width/montage_col
single_img_height = montage_height/montage_row
col_offset = offset * single_img_width #+ 40 # this should be how far out from the left edge it goes
row_offset = offset* 2 * single_img_height # this should be how far down it goes
annotation_adjustment = 0.1 # Adjust this percentage based on your preference
font_size = int(min(single_img_width, single_img_height) * annotation_adjustment)
textHeight = get_max_text_height(font_size)
row_offset = textHeight # offset * single_img_height + textHeight # this should be how far down it goes
# print(("The entire montage is {}x{} pixels."
# "\nEach frame is {}x{}."
# "\n The column offset from the edge is is {},"
# "and the offset from the bottom is {}.").format(montage_width,
# montage_height,
# single_img_width,
# single_img_height,
# col_offset,
# row_offset))
return single_img_height, single_img_width, row_offset, col_offset, font_size
def generate_coordinates(montage_col, montage_row, single_img_width, single_img_height, row_offset, col_offset):
"""
Generate coordinates for a grid of images in a montage.
Parameters:
- montage_col (int): Number of columns in the montage.
- montage_row (int): Number of rows in the montage.
- single_img_width (int): Width of each individual image frame.
- single_img_height (int): Height of each individual image frame.
- row_offset (int): Offset for adjusting the vertical position of the grid.
- col_offset (int): Offset for adjusting the horizontal position of the grid.
Returns:
list: List of (x, y) coordinates for each image in the montage.
"""
coordinates = []
for row in range(montage_row):
for col in range(montage_col):
x = (col * single_img_width) + col_offset
y = ((row + 1) * single_img_height) - row_offset # Static offset for vertical position
coordinates.append((x, y))
print("Coordinates are: {}".format(coordinates))
return coordinates
def add_overlays(imp, overlay, channels, coordinates, annotation_font_size):
"""
Add text overlays to an ImagePlus object at specified coordinates.
Parameters:
- imp (ImagePlus): The ImagePlus object to which overlays are added.
- overlay (Overlay): The Overlay object to store the text annotations.
- channels (list): List of channel names corresponding to coordinates.
- coordinates (list): List of (x, y) coordinates where text annotations are placed.
- annotation_font_size (int): Font size for the text annotations.
Returns:
None
Modifies:
- Adds TextRoi overlays to the specified ImagePlus and Overlay objects.
- Displays the ImagePlus with the added overlays.
"""
font = Font("SanSerif", Font.PLAIN, annotation_font_size) # Font set for all annotations
for i, (x, y) in enumerate(coordinates):
channel_name = channels[i]
# Create TextRoi at the specified coordinates with the channel name
roi = TextRoi(int(x), int(y), channel_name, font)
# Set stroke color to yellow
roi.setStrokeColor(Color.yellow)
# Set fill color to semi-transparent black
roi.setFillColor(Color(0, 0, 0, 0.5))
# Add the TextRoi to the overlay
overlay.add(roi)
# Set the TextRoi as the active ROI in the ImagePlus
imp.setRoi(roi)
# Set the overlay with annotations to the ImagePlus
imp.setOverlay(overlay)
# Show the ImagePlus with annotations
imp.show()
def processROIs(image, sample_id,
roi_manager,
n_chans,
tiff_folder,
montage_cols,
montage_rows,
downsampling_scale,
pixel_adjust,
annotation_font_size,
slice_range=1):
"""
Process ROIs using the specified parameters.
Parameters:
- originalName (str): The name of the original image.
- roi_manager (RoiManager): The ROI manager instance.
- n_chans (int): The number of channels.
- tiff_folder (str): The folder to save TIFF images.
- montage_cols (int): Number of columns in the montage.
- montage_rows (int): Number of rows in the montage.
- downsampling_scale (float): Downsampling scale.
- pixel_adjust (float): Pixel adjustment.
Returns:
None
"""
name = image.getTitle() # Placeholder for later use
default_n_chans = image.getNChannels() if image else 4
for i in range(0, roi_manager.getCount()):
#for i in range(24, 29):
# Get the ROI info
IJ.selectWindow(name)
imp = IJ.getImage() # Assuming the image is the active image
#print("image is: {}.\n ROI is {}".format(imp, i))
roi_manager.select(i) # Select the next ROI
channel = imp.getC()
current_slice = imp.getZ()
#print("slice of ROI is: {}".format(current_slice))
# set the slice range
start_slice = max(1, current_slice - slice_range)
end_slice = min(imp.getNSlices(), current_slice + slice_range)
#print("start slice is {} and end slice is {}".format(start_slice, end_slice))
roi_id = "ROI-{}".format(i)
z_pos_id = "Zpos-{}".format(current_slice)
# # Make a duplicate image
IJ.run("Duplicate...", "duplicate channels=1-" + str(n_chans) + " slices=" + str(start_slice) + "-" + str(end_slice))
#print("duplicated with code: \n"
# "Duplicate...", "duplicate channels=1-" + str(n_chans) + " slices=" + str(start_slice) + "-" + str(end_slice))
# # save the z stepped as a tiff to return back to this easily
save_as_tiff = "{}/{}_{}_{}_{}".format(tiff_folder, sample_id, roi_id, z_pos_id, name)
IJ.saveAs("Tiff", save_as_tiff)
print("Saved{}".format(save_as_tiff))
imp = IJ.getImage() # how does this shit work. what is an instance.
# trim to get only the middle slice
centerSlice = imp.getNSlices()
cSlicefmt = str(int(centerSlice/2+1))
#print("image is: {}".format(imp))
#print("Making stack using only center slice: {}".format(cSlicefmt),)
IJ.run(imp, "Make Substack...", "channels=1-{} slices={}".format(str(n_chans), cSlicefmt))
# # # Resize to have enough pixels to write on
IJ.run("Size...", "width=" + str(imp.getWidth() * pixel_adjust) + " depth=" + str(n_chans) + " constrain average interpolation=None")
# # Make the montage
IJ.run("Make Montage...", "columns=" + str(montage_cols) + " rows=" + str(montage_rows) + " scale=" + str(downsampling_scale) + " border=6 use")
montage = IJ.getImage()
single_img_height, single_img_width, row_offset, col_offset, font_size = define_widths(montage, montage_cols, montage_rows, .03, annotation_font_size)
coordinates = generate_coordinates(montage_cols, montage_rows, single_img_width, single_img_height, row_offset, col_offset)
overlay = Overlay()
add_overlays(montage, overlay, channels, coordinates, font_size)
save_as_png = "{}/{}_{}_{}_{}.png".format(png_folder, sample_id, roi_id, z_pos_id, name)
FileSaver(montage).saveAsPng(save_as_png)
print("Saved{}".format(save_as_png))
imp.close()
montage.close()
print("completed all rois!")
# Show the parameters dialog
active_image = IJ.getImage() #handle for referring to image
parameters = showParametersDialog(active_image)
# assign params to variables and do the channel name assignment. breaks if channels are empty
if parameters is not None:
date, sample_id, roi_set_filename, n_chans, downsampling_scale, annotation_font_size, montage_rows, pixel_adjust, col_offset = parameters
montage_cols = n_chans/montage_rows
# assign channel params
channels = getChannelNamesForm(active_image, n_chans)
if channels is None:
sys.exit()
filePath = active_image.getOriginalFileInfo().filePath if active_image.getOriginalFileInfo() else None
directory = os.path.dirname(filePath) if filePath else None
tiff_folder, png_folder = createOutputFolders(directory)
print("TIFFs Folder:", tiff_folder)
print("PNGs Folder:", png_folder)
# Use the retrieved parameters
roi_manager = RoiManager.getInstance() # Get the ROI manager instance
if roi_manager is not None:
roi_manager.runCommand("Save", os.path.join(directory, "{}_{}".format(date, roi_set_filename)))
else:
print("ROI Manager not open. Please open it and try again.")
# Further use of the parameters...
print("Active Image: {}".format(active_image))
print("File Path: {}".format(filePath))
print("Directory: {}".format(directory))
print("Image Title: {}".format(active_image.getTitle()))
print("Number of Channels: {}".format(n_chans))
print("Downsampling Scale: {}".format(downsampling_scale))
print("Annotation Font Size: {}".format(annotation_font_size))
print("Montage Rows: {}".format(montage_rows))
print("Pixel Adjustment: {}".format(pixel_adjust))
print("Column Offset: {}".format(col_offset))
print("Channels: {}".format(channels))
print("Number of Channels: {}".format(n_chans))
## now we have made the paramters. hooray.
processROIs(active_image,sample_id, roi_manager, n_chans, tiff_folder,
montage_cols, montage_rows, downsampling_scale,
pixel_adjust, annotation_font_size, 4)