spacr 0.0.35__py3-none-any.whl → 0.0.61__py3-none-any.whl
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.
- spacr/__init__.py +2 -2
- spacr/__main__.py +0 -2
- spacr/alpha.py +514 -2
- spacr/annotate_app.py +113 -117
- spacr/core.py +864 -728
- spacr/deep_spacr.py +696 -0
- spacr/foldseek.py +2 -16
- spacr/graph_learning.py +297 -253
- spacr/gui.py +9 -8
- spacr/gui_2.py +90 -0
- spacr/gui_classify_app.py +7 -8
- spacr/gui_mask_app.py +13 -13
- spacr/gui_measure_app.py +8 -10
- spacr/gui_utils.py +134 -35
- spacr/io.py +311 -467
- spacr/mask_app.py +110 -6
- spacr/measure.py +19 -5
- spacr/models/cp/toxo_pv_lumen.CP_model +0 -0
- spacr/old_code.py +70 -2
- spacr/plot.py +23 -6
- spacr/sequencing.py +1130 -0
- spacr/sim.py +0 -42
- spacr/timelapse.py +0 -1
- spacr/train.py +172 -13
- spacr/umap.py +0 -689
- spacr/utils.py +1322 -75
- {spacr-0.0.35.dist-info → spacr-0.0.61.dist-info}/METADATA +14 -29
- spacr-0.0.61.dist-info/RECORD +39 -0
- {spacr-0.0.35.dist-info → spacr-0.0.61.dist-info}/entry_points.txt +1 -0
- spacr-0.0.35.dist-info/RECORD +0 -35
- {spacr-0.0.35.dist-info → spacr-0.0.61.dist-info}/LICENSE +0 -0
- {spacr-0.0.35.dist-info → spacr-0.0.61.dist-info}/WHEEL +0 -0
- {spacr-0.0.35.dist-info → spacr-0.0.61.dist-info}/top_level.txt +0 -0
spacr/mask_app.py
CHANGED
@@ -128,10 +128,13 @@ class modify_masks:
|
|
128
128
|
self.zoom_active = False
|
129
129
|
self.magic_wand_active = False
|
130
130
|
self.brush_active = False
|
131
|
+
self.dividing_line_active = False
|
132
|
+
self.dividing_line_coords = []
|
133
|
+
self.current_dividing_line = None
|
131
134
|
self.lower_quantile = tk.StringVar(value="1.0")
|
132
135
|
self.upper_quantile = tk.StringVar(value="99.9")
|
133
136
|
self.magic_wand_tolerance = tk.StringVar(value="1000")
|
134
|
-
|
137
|
+
|
135
138
|
def update_mouse_info(self, event):
|
136
139
|
x, y = event.x, event.y
|
137
140
|
intensity = "N/A"
|
@@ -187,13 +190,14 @@ class modify_masks:
|
|
187
190
|
self.max_pixels_entry.pack(side='left')
|
188
191
|
self.erase_btn = tk.Button(self.mode_toolbar, text="Erase", command=self.toggle_erase_mode)
|
189
192
|
self.erase_btn.pack(side='left')
|
190
|
-
|
191
193
|
self.brush_btn = tk.Button(self.mode_toolbar, text="Brush", command=self.toggle_brush_mode)
|
192
194
|
self.brush_btn.pack(side='left')
|
193
195
|
self.brush_size_entry = tk.Entry(self.mode_toolbar)
|
194
|
-
self.brush_size_entry.insert(0, "10")
|
196
|
+
self.brush_size_entry.insert(0, "10")
|
195
197
|
self.brush_size_entry.pack(side='left')
|
196
198
|
tk.Label(self.mode_toolbar, text="Brush Size:").pack(side='left')
|
199
|
+
self.dividing_line_btn = tk.Button(self.mode_toolbar, text="Dividing Line", command=self.toggle_dividing_line_mode)
|
200
|
+
self.dividing_line_btn.pack(side='left')
|
197
201
|
|
198
202
|
def setup_function_toolbar(self):
|
199
203
|
self.function_toolbar = tk.Frame(self.root)
|
@@ -393,10 +397,12 @@ class modify_masks:
|
|
393
397
|
self.magic_wand_active = False
|
394
398
|
self.erase_active = False
|
395
399
|
self.brush_active = False
|
400
|
+
self.dividing_line_active = False
|
396
401
|
self.draw_btn.config(text="Draw")
|
397
402
|
self.erase_btn.config(text="Erase")
|
398
403
|
self.magic_wand_btn.config(text="Magic Wand")
|
399
404
|
self.zoom_btn.config(text="Zoom ON")
|
405
|
+
self.dividing_line_btn.config(text="Dividing Line")
|
400
406
|
self.canvas.unbind("<Button-1>")
|
401
407
|
self.canvas.unbind("<Button-3>")
|
402
408
|
self.canvas.unbind("<Motion>")
|
@@ -423,7 +429,7 @@ class modify_masks:
|
|
423
429
|
self.zoom_mask = None
|
424
430
|
self.zoom_image = None
|
425
431
|
self.zoom_image_orig = None
|
426
|
-
|
432
|
+
|
427
433
|
def toggle_brush_mode(self):
|
428
434
|
self.brush_active = not self.brush_active
|
429
435
|
if self.brush_active:
|
@@ -448,7 +454,105 @@ class modify_masks:
|
|
448
454
|
self.canvas.unbind("<B3-Motion>")
|
449
455
|
self.canvas.unbind("<ButtonRelease-1>")
|
450
456
|
self.canvas.unbind("<ButtonRelease-3>")
|
451
|
-
|
457
|
+
|
458
|
+
def image_to_canvas(self, x_image, y_image):
|
459
|
+
x_scale, y_scale = self.get_scaling_factors(
|
460
|
+
self.image.shape[1], self.image.shape[0],
|
461
|
+
self.canvas_width, self.canvas_height
|
462
|
+
)
|
463
|
+
x_canvas = int(x_image / x_scale)
|
464
|
+
y_canvas = int(y_image / y_scale)
|
465
|
+
return x_canvas, y_canvas
|
466
|
+
|
467
|
+
def toggle_dividing_line_mode(self):
|
468
|
+
self.dividing_line_active = not self.dividing_line_active
|
469
|
+
if self.dividing_line_active:
|
470
|
+
self.drawing = False
|
471
|
+
self.magic_wand_active = False
|
472
|
+
self.erase_active = False
|
473
|
+
self.brush_active = False
|
474
|
+
self.draw_btn.config(text="Draw")
|
475
|
+
self.erase_btn.config(text="Erase")
|
476
|
+
self.magic_wand_btn.config(text="Magic Wand")
|
477
|
+
self.brush_btn.config(text="Brush")
|
478
|
+
self.dividing_line_btn.config(text="Dividing Line ON")
|
479
|
+
self.canvas.unbind("<Button-1>")
|
480
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
481
|
+
self.canvas.unbind("<Motion>")
|
482
|
+
self.canvas.bind("<Button-1>", self.start_dividing_line)
|
483
|
+
self.canvas.bind("<ButtonRelease-1>", self.finish_dividing_line)
|
484
|
+
self.canvas.bind("<Motion>", self.update_dividing_line_preview)
|
485
|
+
else:
|
486
|
+
print("Dividing Line Mode: OFF")
|
487
|
+
self.dividing_line_active = False
|
488
|
+
self.dividing_line_btn.config(text="Dividing Line")
|
489
|
+
self.canvas.unbind("<Button-1>")
|
490
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
491
|
+
self.canvas.unbind("<Motion>")
|
492
|
+
self.display_image()
|
493
|
+
|
494
|
+
def start_dividing_line(self, event):
|
495
|
+
if self.dividing_line_active:
|
496
|
+
self.dividing_line_coords = [(event.x, event.y)]
|
497
|
+
self.current_dividing_line = self.canvas.create_line(event.x, event.y, event.x, event.y, fill="red", width=2)
|
498
|
+
|
499
|
+
def finish_dividing_line(self, event):
|
500
|
+
if self.dividing_line_active:
|
501
|
+
self.dividing_line_coords.append((event.x, event.y))
|
502
|
+
if self.zoom_active:
|
503
|
+
self.dividing_line_coords = [self.canvas_to_image(x, y) for x, y in self.dividing_line_coords]
|
504
|
+
self.apply_dividing_line()
|
505
|
+
self.canvas.delete(self.current_dividing_line)
|
506
|
+
self.current_dividing_line = None
|
507
|
+
|
508
|
+
def update_dividing_line_preview(self, event):
|
509
|
+
if self.dividing_line_active and self.dividing_line_coords:
|
510
|
+
x, y = event.x, event.y
|
511
|
+
if self.zoom_active:
|
512
|
+
x, y = self.canvas_to_image(x, y)
|
513
|
+
self.dividing_line_coords.append((x, y))
|
514
|
+
canvas_coords = [(self.image_to_canvas(*pt) if self.zoom_active else pt) for pt in self.dividing_line_coords]
|
515
|
+
flat_canvas_coords = [coord for pt in canvas_coords for coord in pt]
|
516
|
+
self.canvas.coords(self.current_dividing_line, *flat_canvas_coords)
|
517
|
+
|
518
|
+
def apply_dividing_line(self):
|
519
|
+
if self.dividing_line_coords:
|
520
|
+
coords = self.dividing_line_coords
|
521
|
+
if self.zoom_active:
|
522
|
+
coords = [self.canvas_to_image(x, y) for x, y in coords]
|
523
|
+
|
524
|
+
rr, cc = [], []
|
525
|
+
for (x0, y0), (x1, y1) in zip(coords[:-1], coords[1:]):
|
526
|
+
line_rr, line_cc = line(y0, x0, y1, x1)
|
527
|
+
rr.extend(line_rr)
|
528
|
+
cc.extend(line_cc)
|
529
|
+
rr, cc = np.array(rr), np.array(cc)
|
530
|
+
|
531
|
+
mask_copy = self.mask.copy()
|
532
|
+
|
533
|
+
if self.zoom_active:
|
534
|
+
# Update the zoomed mask
|
535
|
+
self.zoom_mask[rr, cc] = 0
|
536
|
+
# Reflect changes to the original mask
|
537
|
+
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
|
538
|
+
zoomed_mask_resized_back = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
|
539
|
+
self.mask[y0:y1, x0:x1] = zoomed_mask_resized_back
|
540
|
+
else:
|
541
|
+
# Directly update the original mask
|
542
|
+
mask_copy[rr, cc] = 0
|
543
|
+
self.mask = mask_copy
|
544
|
+
|
545
|
+
labeled_mask, num_labels = label(self.mask > 0)
|
546
|
+
self.mask = labeled_mask
|
547
|
+
self.update_display()
|
548
|
+
|
549
|
+
self.dividing_line_coords = []
|
550
|
+
self.canvas.unbind("<Button-1>")
|
551
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
552
|
+
self.canvas.unbind("<Motion>")
|
553
|
+
self.dividing_line_active = False
|
554
|
+
self.dividing_line_btn.config(text="Dividing Line")
|
555
|
+
|
452
556
|
def toggle_draw_mode(self):
|
453
557
|
self.drawing = not self.drawing
|
454
558
|
if self.drawing:
|
@@ -753,7 +857,7 @@ class modify_masks:
|
|
753
857
|
self.mask[labeled_mask == i] = 0 # Remove small objects
|
754
858
|
self.update_display()
|
755
859
|
|
756
|
-
|
860
|
+
##@log_function_call
|
757
861
|
def initiate_mask_app_root(width, height):
|
758
862
|
theme = 'breeze'
|
759
863
|
root = ThemedTk(theme=theme)
|
spacr/measure.py
CHANGED
@@ -3,7 +3,6 @@ import numpy as np
|
|
3
3
|
import pandas as pd
|
4
4
|
from collections import defaultdict
|
5
5
|
from scipy.stats import pearsonr
|
6
|
-
import matplotlib as mpl
|
7
6
|
import multiprocessing as mp
|
8
7
|
from scipy.ndimage import distance_transform_edt, generate_binary_structure
|
9
8
|
from skimage.measure import regionprops, regionprops_table, shannon_entropy
|
@@ -157,7 +156,7 @@ def _analyze_cytoskeleton(array, mask, channel):
|
|
157
156
|
|
158
157
|
return pd.DataFrame(properties_list)
|
159
158
|
|
160
|
-
|
159
|
+
#@log_function_call
|
161
160
|
def _morphological_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask, settings, zernike=True, degree=8):
|
162
161
|
"""
|
163
162
|
Calculate morphological measurements for cells, nucleus, pathogens, and cytoplasms based on the given masks.
|
@@ -501,7 +500,7 @@ def _estimate_blur(image):
|
|
501
500
|
# Compute and return the variance of the Laplacian
|
502
501
|
return lap.var()
|
503
502
|
|
504
|
-
|
503
|
+
#@log_function_call
|
505
504
|
def _intensity_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_mask, channel_arrays, settings, sizes=[3, 6, 12, 24], periphery=True, outside=True):
|
506
505
|
"""
|
507
506
|
Calculate various intensity measurements for different regions in the image.
|
@@ -589,7 +588,7 @@ def _intensity_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_ma
|
|
589
588
|
|
590
589
|
return pd.concat(cell_dfs, axis=1), pd.concat(nucleus_dfs, axis=1), pd.concat(pathogen_dfs, axis=1), pd.concat(cytoplasm_dfs, axis=1)
|
591
590
|
|
592
|
-
|
591
|
+
#@log_function_call
|
593
592
|
def _measure_crop_core(index, time_ls, file, settings):
|
594
593
|
|
595
594
|
"""
|
@@ -887,7 +886,7 @@ def _measure_crop_core(index, time_ls, file, settings):
|
|
887
886
|
average_time = np.mean(time_ls) if len(time_ls) > 0 else 0
|
888
887
|
return average_time, cells
|
889
888
|
|
890
|
-
|
889
|
+
#@log_function_call
|
891
890
|
def measure_crop(settings):
|
892
891
|
|
893
892
|
"""
|
@@ -1092,3 +1091,18 @@ def generate_cellpose_train_set(folders, dst, min_objects=5):
|
|
1092
1091
|
shutil.copy(img_path, new_img)
|
1093
1092
|
except Exception as e:
|
1094
1093
|
print(f"Error copying {path} to {new_mask}: {e}")
|
1094
|
+
|
1095
|
+
def get_object_counts(src):
|
1096
|
+
database_path = os.path.join(src, 'measurements/measurements.db')
|
1097
|
+
# Connect to the SQLite database
|
1098
|
+
conn = sqlite3.connect(database_path)
|
1099
|
+
# Read the table into a pandas DataFrame
|
1100
|
+
df = pd.read_sql_query("SELECT * FROM object_counts", conn)
|
1101
|
+
# Group by 'count_type' and calculate the sum of 'object_count' and the average 'object_count' per 'file_name'
|
1102
|
+
grouped_df = df.groupby('count_type').agg(
|
1103
|
+
total_object_count=('object_count', 'sum'),
|
1104
|
+
avg_object_count_per_file_name=('object_count', 'mean')
|
1105
|
+
).reset_index()
|
1106
|
+
# Close the database connection
|
1107
|
+
conn.close()
|
1108
|
+
return grouped_df
|
Binary file
|
spacr/old_code.py
CHANGED
@@ -103,7 +103,7 @@ def run_mask_gui(q):
|
|
103
103
|
except Exception as e:
|
104
104
|
q.put(f"Error during processing: {e}\n")
|
105
105
|
|
106
|
-
|
106
|
+
#@log_function_call
|
107
107
|
def main_thread_update_function(root, q, fig_queue, canvas_widget, progress_label):
|
108
108
|
try:
|
109
109
|
while not q.empty():
|
@@ -287,4 +287,72 @@ def _extract_filename_metadata(filenames, src, images_by_key, regular_expression
|
|
287
287
|
print(f"Filename {filename} did not match provided regex")
|
288
288
|
continue
|
289
289
|
|
290
|
-
return images_by_key
|
290
|
+
return images_by_key
|
291
|
+
|
292
|
+
def compare_cellpose_masks_v1(src, verbose=False, save=False):
|
293
|
+
|
294
|
+
from .io import _read_mask
|
295
|
+
from .plot import visualize_masks, plot_comparison_results, visualize_cellpose_masks
|
296
|
+
from .utils import extract_boundaries, boundary_f1_score, compute_segmentation_ap, jaccard_index
|
297
|
+
|
298
|
+
import os
|
299
|
+
import numpy as np
|
300
|
+
from skimage.measure import label
|
301
|
+
|
302
|
+
# Collect all subdirectories in src
|
303
|
+
dirs = [os.path.join(src, d) for d in os.listdir(src) if os.path.isdir(os.path.join(src, d))]
|
304
|
+
|
305
|
+
dirs.sort() # Optional: sort directories if needed
|
306
|
+
|
307
|
+
# Get common files in all directories
|
308
|
+
common_files = set(os.listdir(dirs[0]))
|
309
|
+
for d in dirs[1:]:
|
310
|
+
common_files.intersection_update(os.listdir(d))
|
311
|
+
common_files = list(common_files)
|
312
|
+
|
313
|
+
results = []
|
314
|
+
conditions = [os.path.basename(d) for d in dirs]
|
315
|
+
|
316
|
+
for index, filename in enumerate(common_files):
|
317
|
+
print(f'Processing image {index+1}/{len(common_files)}', end='\r', flush=True)
|
318
|
+
paths = [os.path.join(d, filename) for d in dirs]
|
319
|
+
|
320
|
+
# Check if file exists in all directories
|
321
|
+
if not all(os.path.exists(path) for path in paths):
|
322
|
+
print(f'Skipping {filename} as it is not present in all directories.')
|
323
|
+
continue
|
324
|
+
|
325
|
+
masks = [_read_mask(path) for path in paths]
|
326
|
+
boundaries = [extract_boundaries(mask) for mask in masks]
|
327
|
+
|
328
|
+
if verbose:
|
329
|
+
visualize_cellpose_masks(masks, titles=conditions, comparison_title=f"Masks Comparison for {filename}", save=save, src=src)
|
330
|
+
|
331
|
+
# Initialize data structure for results
|
332
|
+
file_results = {'filename': filename}
|
333
|
+
|
334
|
+
# Compare each mask with each other
|
335
|
+
for i in range(len(masks)):
|
336
|
+
for j in range(i + 1, len(masks)):
|
337
|
+
condition_i = conditions[i]
|
338
|
+
condition_j = conditions[j]
|
339
|
+
mask_i = masks[i]
|
340
|
+
mask_j = masks[j]
|
341
|
+
|
342
|
+
# Compute metrics
|
343
|
+
boundary_f1 = boundary_f1_score(mask_i, mask_j)
|
344
|
+
jaccard = jaccard_index(mask_i, mask_j)
|
345
|
+
average_precision = compute_segmentation_ap(mask_i, mask_j)
|
346
|
+
|
347
|
+
# Store results
|
348
|
+
file_results[f'jaccard_{condition_i}_{condition_j}'] = jaccard
|
349
|
+
file_results[f'boundary_f1_{condition_i}_{condition_j}'] = boundary_f1
|
350
|
+
file_results[f'average_precision_{condition_i}_{condition_j}'] = average_precision
|
351
|
+
|
352
|
+
results.append(file_results)
|
353
|
+
|
354
|
+
fig = plot_comparison_results(results)
|
355
|
+
|
356
|
+
save_results_and_figure(src, fig, results)
|
357
|
+
|
358
|
+
return results, fig
|
spacr/plot.py
CHANGED
@@ -11,8 +11,6 @@ import statsmodels.api as sm
|
|
11
11
|
import imageio.v2 as imageio
|
12
12
|
from IPython.display import display
|
13
13
|
from skimage.segmentation import find_boundaries
|
14
|
-
from skimage.measure import find_contours
|
15
|
-
from skimage.morphology import square, dilation
|
16
14
|
from skimage import measure
|
17
15
|
|
18
16
|
from ipywidgets import IntSlider, interact
|
@@ -376,7 +374,7 @@ def plot_arrays(src, figuresize=50, cmap='inferno', nr=1, normalize=True, q1=1,
|
|
376
374
|
print(f'Image path:{path}')
|
377
375
|
img = np.load(path)
|
378
376
|
if normalize:
|
379
|
-
img = normalize_to_dtype(array=img,
|
377
|
+
img = normalize_to_dtype(array=img, p1=q1, p2=q2)
|
380
378
|
dim = img.shape
|
381
379
|
if len(img.shape)>2:
|
382
380
|
array_nr = img.shape[2]
|
@@ -426,9 +424,11 @@ def _normalize_and_outline(image, remove_background, normalize, normalization_pe
|
|
426
424
|
image[mask] = 0
|
427
425
|
|
428
426
|
if normalize:
|
429
|
-
image = normalize_to_dtype(array=image,
|
427
|
+
image = normalize_to_dtype(array=image, p1=normalization_percentiles[0], p2=normalization_percentiles[1])
|
428
|
+
else:
|
429
|
+
image = normalize_to_dtype(array=image, p1=0, p2=100)
|
430
430
|
|
431
|
-
rgb_image = _gen_rgb_image(image,
|
431
|
+
rgb_image = _gen_rgb_image(image, channels=overlay_chans)
|
432
432
|
|
433
433
|
if overlay:
|
434
434
|
overlayed_image, outlines, image = _outline_and_overlay(image, rgb_image, mask_dims, outline_colors, outline_thickness)
|
@@ -440,6 +440,7 @@ def _normalize_and_outline(image, remove_background, normalize, normalization_pe
|
|
440
440
|
image = np.take(image, channels_to_keep, axis=-1)
|
441
441
|
return [], image, []
|
442
442
|
|
443
|
+
|
443
444
|
def _plot_merged_plot(overlay, image, stack, mask_dims, figuresize, overlayed_image, outlines, cmap, outline_colors, print_object_number):
|
444
445
|
|
445
446
|
"""
|
@@ -513,6 +514,8 @@ def plot_merged(src, settings):
|
|
513
514
|
None
|
514
515
|
"""
|
515
516
|
from .utils import _remove_noninfected
|
517
|
+
|
518
|
+
|
516
519
|
|
517
520
|
font = settings['figuresize']/2
|
518
521
|
outline_colors = _get_colours_merged(settings['outline_color'])
|
@@ -1347,7 +1350,7 @@ def visualize_masks(mask1, mask2, mask3, title="Masks Comparison"):
|
|
1347
1350
|
plt.suptitle(title)
|
1348
1351
|
plt.show()
|
1349
1352
|
|
1350
|
-
def visualize_cellpose_masks(masks, titles=None,
|
1353
|
+
def visualize_cellpose_masks(masks, titles=None, filename=None, save=False, src=None):
|
1351
1354
|
"""
|
1352
1355
|
Visualize multiple masks with optional titles.
|
1353
1356
|
|
@@ -1356,6 +1359,9 @@ def visualize_cellpose_masks(masks, titles=None, comparison_title="Masks Compari
|
|
1356
1359
|
titles (list of str, optional): A list of titles for the masks. If None, default titles will be used.
|
1357
1360
|
comparison_title (str): Title for the entire figure.
|
1358
1361
|
"""
|
1362
|
+
|
1363
|
+
comparison_title=f"Masks Comparison for {filename}"
|
1364
|
+
|
1359
1365
|
if titles is None:
|
1360
1366
|
titles = [f'Mask {i+1}' for i in range(len(masks))]
|
1361
1367
|
|
@@ -1376,6 +1382,17 @@ def visualize_cellpose_masks(masks, titles=None, comparison_title="Masks Compari
|
|
1376
1382
|
plt.suptitle(comparison_title)
|
1377
1383
|
plt.show()
|
1378
1384
|
|
1385
|
+
if save:
|
1386
|
+
if src is None:
|
1387
|
+
src = os.getcwd()
|
1388
|
+
results_dir = os.path.join(src, 'results')
|
1389
|
+
os.makedirs(results_dir, exist_ok=True)
|
1390
|
+
fig_path = os.path.join(results_dir, f'{filename}.pdf')
|
1391
|
+
fig.savefig(fig_path, format='pdf')
|
1392
|
+
print(f'Saved figure to {fig_path}')
|
1393
|
+
return
|
1394
|
+
|
1395
|
+
|
1379
1396
|
def plot_comparison_results(comparison_results):
|
1380
1397
|
df = pd.DataFrame(comparison_results)
|
1381
1398
|
df_melted = pd.melt(df, id_vars=['filename'], var_name='metric', value_name='value')
|