CompactionAnalyzer 1.0.6__tar.gz → 1.0.8__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.
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/CompactionFunctions.py +289 -38
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/StructureTensor.py +20 -6
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/__init__.py +2 -1
- compactionanalyzer-1.0.8/CompactionAnalyzer/api.py +301 -0
- compactionanalyzer-1.0.8/CompactionAnalyzer/distance.py +53 -0
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/plotting.py +242 -1
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/utilities.py +1 -1
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer.egg-info/PKG-INFO +10 -3
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer.egg-info/SOURCES.txt +6 -1
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer.egg-info/requires.txt +1 -1
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/PKG-INFO +10 -3
- compactionanalyzer-1.0.8/README.md +1159 -0
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/setup.py +2 -2
- compactionanalyzer-1.0.8/test/test_direct_api.py +335 -0
- compactionanalyzer-1.0.8/test/test_point_distance.py +75 -0
- compactionanalyzer-1.0.8/test/test_reference_masks.py +40 -0
- compactionanalyzer-1.0.6/README.md +0 -199
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/GUI_old_use_saenopy_instead.py +0 -0
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/QExtendedGraphicsView.py +0 -0
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/QtShortCuts.py +0 -0
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer.egg-info/dependency_links.txt +0 -0
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer.egg-info/top_level.txt +0 -0
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/LICENSE +0 -0
- {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/setup.cfg +0 -0
{compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/CompactionFunctions.py
RENAMED
|
@@ -240,6 +240,26 @@ def create_centered_circle(image_shape):
|
|
|
240
240
|
return circle_array
|
|
241
241
|
|
|
242
242
|
|
|
243
|
+
def _cell_reference_coordinates(shape, center):
|
|
244
|
+
"""Shared cell-center geometry for the batch pipeline and direct API."""
|
|
245
|
+
y, x = np.indices(shape)
|
|
246
|
+
dx = x - center[0]
|
|
247
|
+
dy = y - center[1]
|
|
248
|
+
distance = np.sqrt(dx ** 2 + dy ** 2)
|
|
249
|
+
angle = np.arctan2(dy, dx) * 360 / (2 * np.pi)
|
|
250
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
251
|
+
dx_norm, dy_norm = dx / distance, dy / distance
|
|
252
|
+
return distance, angle, dx_norm, dy_norm
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _cell_orientation_fields(min_evec, dx_norm, dy_norm):
|
|
256
|
+
"""Original cell-relative angle and linear alignment; no export/statistics."""
|
|
257
|
+
projection = np.abs(dx_norm * min_evec[:, :, 0] + dy_norm * min_evec[:, :, 1])
|
|
258
|
+
angle = np.arccos(projection) * 360 / (2 * np.pi)
|
|
259
|
+
alignment = 1 - (2 * np.arccos(projection) / (0.5 * np.pi))
|
|
260
|
+
return angle, alignment
|
|
261
|
+
|
|
262
|
+
|
|
243
263
|
def StuctureAnalysisMain(fiber_list,
|
|
244
264
|
cell_list,
|
|
245
265
|
out_list,
|
|
@@ -274,7 +294,12 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
274
294
|
ignore_cell_outline=False, # if True the cell area is not set to nan and orientation are calculted everywhere - still the cell center is calculated from segmention
|
|
275
295
|
|
|
276
296
|
|
|
277
|
-
mode_saenopy=False
|
|
297
|
+
mode_saenopy=False, # only used for the Saenopy return values
|
|
298
|
+
analysis_mode="full", # full, axis_orientation_only, axis_orientation_masked
|
|
299
|
+
mask_sigma=5, # gaussian blur of the second channel, in pixels
|
|
300
|
+
mask_threshold=0.6, # threshold_otsu(blurred_mask_image * mask_threshold)
|
|
301
|
+
mask_invert=False, # select dark regions instead of bright regions
|
|
302
|
+
return_segmentation=False): # append full-mode segmentation to Saenopy return tuple
|
|
278
303
|
|
|
279
304
|
|
|
280
305
|
|
|
@@ -292,6 +317,23 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
292
317
|
None.
|
|
293
318
|
|
|
294
319
|
"""
|
|
320
|
+
# Reduced mode exits before the dummy cell, segmentation and radial analysis.
|
|
321
|
+
# Keep full as the default, including all existing positional arguments.
|
|
322
|
+
if analysis_mode not in ("full", "axis_orientation_only", "axis_orientation_masked"):
|
|
323
|
+
raise ValueError("analysis_mode must be 'full', 'axis_orientation_only' or 'axis_orientation_masked'")
|
|
324
|
+
if analysis_mode != "full":
|
|
325
|
+
if analysis_mode == "axis_orientation_only" and cell_list is not None:
|
|
326
|
+
raise ValueError("axis_orientation_only requires cell_list=None")
|
|
327
|
+
if analysis_mode == "axis_orientation_masked" and cell_list is None:
|
|
328
|
+
raise ValueError("axis_orientation_masked requires second-channel images in cell_list")
|
|
329
|
+
if manual_segmention or load_segmentation or show_segmentation or path_seg is not None:
|
|
330
|
+
raise ValueError("Cell segmentation options require analysis_mode='full'; use mask_sigma/threshold/invert for axis masks")
|
|
331
|
+
if min_dist is not None or max_dist is not None:
|
|
332
|
+
raise ValueError("Cell distances require analysis_mode='full'")
|
|
333
|
+
return _axis_orientation_only(fiber_list, out_list, scale, sigma_tensor, edge,
|
|
334
|
+
sigma_first_blur, norm1, norm2, plotting, dpi, SaveNumpy,
|
|
335
|
+
cell_list, mask_sigma, mask_threshold, mask_invert, mode_saenopy)
|
|
336
|
+
|
|
295
337
|
# plt.ioff()
|
|
296
338
|
# plt.ion()
|
|
297
339
|
|
|
@@ -334,6 +376,10 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
334
376
|
},
|
|
335
377
|
'Data' : {'fiber_list': [fiber_list], 'cell_list': [cell_list], 'out_list': [out_list]}
|
|
336
378
|
}
|
|
379
|
+
dict_file["analysis_mode"] = analysis_mode
|
|
380
|
+
# An absolute source also works when plotting from another working folder.
|
|
381
|
+
dict_file["source"] = os.path.abspath(fiber_list[n])
|
|
382
|
+
dict_file["axis_angle_convention"] = "degrees [-90,90); x right, positive upwards; y=wrap180(x-90)"
|
|
337
383
|
with open(os.path.join(out_list[n],"parameters.yml"), 'w') as yaml_file:
|
|
338
384
|
yaml.dump(dict_file, yaml_file, default_flow_style=False)
|
|
339
385
|
|
|
@@ -401,13 +447,7 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
401
447
|
coordinates
|
|
402
448
|
"""
|
|
403
449
|
# Calculate Angle + Distances
|
|
404
|
-
|
|
405
|
-
dx = x - center_small[0]
|
|
406
|
-
dy = y - center_small[1]
|
|
407
|
-
distance = np.sqrt(dx ** 2 + dy ** 2) # dist to center
|
|
408
|
-
angle = np.arctan2(dy, dx) *360/(2*np.pi)
|
|
409
|
-
dx_norm = (dx/distance)
|
|
410
|
-
dy_norm = (dy/distance)
|
|
450
|
+
distance, angle, dx_norm, dy_norm = _cell_reference_coordinates(ori.shape, center_small)
|
|
411
451
|
dist_surface = distance_transform_edt(~segmention["mask"])[edge:-edge,edge:-edge] # dist to surface
|
|
412
452
|
# calculate an angle without the deviation/reference to the cell position
|
|
413
453
|
angle_no_reference = ((np.arctan2(min_evec[:,:,0],min_evec[:,:,1])*360/(2*np.pi))%180)-90 # -90 to +90° towards x-axis
|
|
@@ -417,9 +457,8 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
417
457
|
total image analysis
|
|
418
458
|
"""
|
|
419
459
|
# Angular deviation from orietation to center vector (projection to only have positive deviation angle values since negative/positive deviation does not matter here)
|
|
420
|
-
angle_dev =
|
|
460
|
+
angle_dev, orientation_dev = _cell_orientation_fields(min_evec, dx_norm, dy_norm)
|
|
421
461
|
## compute the orientation
|
|
422
|
-
orientation_dev = 1 - (2 * np.arccos(np.abs(dx_norm * min_evec[:,:,0] + dy_norm*min_evec[:,:,1]))/ (0.5* np.pi))
|
|
423
462
|
|
|
424
463
|
|
|
425
464
|
### set values to nan if max distance is specified
|
|
@@ -649,7 +688,14 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
649
688
|
|
|
650
689
|
|
|
651
690
|
|
|
691
|
+
# The same maps and filenames are used by both analysis modes.
|
|
692
|
+
# Keep the once-normalized fiber for plotting; saving normalizes it again
|
|
693
|
+
# exactly as the original FiberImageCrop.npy export did.
|
|
694
|
+
if SaveNumpy or plotting:
|
|
695
|
+
axis_maps = _get_axis_arrays(min_evec, max_evec, ori, im_fiber_n[edge:-edge,edge:-edge])
|
|
696
|
+
|
|
652
697
|
if SaveNumpy:
|
|
698
|
+
_save_axis_arrays(out_list[n], axis_maps)
|
|
653
699
|
#create output folder if not existing
|
|
654
700
|
numpy_out = os.path.join(out_list[n], "NumpyArrays" )
|
|
655
701
|
print (numpy_out)
|
|
@@ -661,15 +707,10 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
661
707
|
np.save(os.path.join(numpy_out, "AngleDeviationMap.npy" ),angle_dev )
|
|
662
708
|
#np.save(os.path.join(numpy_out, "AngleMap(weight_int).npy" ),angle_dev_weighted)
|
|
663
709
|
np.save(os.path.join(numpy_out, "AngleDeviationMap(weight_int_coh).npy"),angle_dev_weighted2 )
|
|
664
|
-
np.save(os.path.join(numpy_out, "AngleMap_NoReference.npy"), angle_no_reference )
|
|
665
710
|
np.save(os.path.join(numpy_out, "OrientationMap.npy" ),orientation_dev )
|
|
666
711
|
#np.save(os.path.join(numpy_out, "OrientationMap_weight_int.npy" ),orientation_dev_weighted)
|
|
667
712
|
np.save(os.path.join(numpy_out, "OrientationMap_weight_intcoh.npy" ),orientation_dev_weighted2 )
|
|
668
|
-
np.save(os.path.join(numpy_out, "FiberImageCrop.npy" ),normalize(im_fiber_n[edge:-edge,edge:-edge]) )
|
|
669
|
-
np.save(os.path.join(numpy_out, "CoherencyMap.npy" ),ori )
|
|
670
713
|
#np.save(os.path.join(numpy_out, "CoherencyMap(weighted_int).npy" ),ori_weight2)
|
|
671
|
-
np.save(os.path.join(numpy_out, "Vector_min_ax0.npy"),min_evec[:,:,0])
|
|
672
|
-
np.save(os.path.join(numpy_out, "Vector_min_ax1.npy"),min_evec[:,:,1])
|
|
673
714
|
np.save(os.path.join(numpy_out, "center.npy"),center_small)
|
|
674
715
|
#np.save(os.path.join(numpy_out, "mask_surface_shells.npy"),mask_shells['Mask_shell'])
|
|
675
716
|
#np.save(os.path.join(numpy_out, "mask_spherical_shells.npy"),mask_shells['Mask_shell_center'])
|
|
@@ -682,7 +723,7 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
682
723
|
|
|
683
724
|
|
|
684
725
|
"""
|
|
685
|
-
|
|
726
|
+
Plotting results
|
|
686
727
|
"""
|
|
687
728
|
|
|
688
729
|
if plotting:
|
|
@@ -692,6 +733,11 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
692
733
|
if not os.path.exists(figures):
|
|
693
734
|
os.makedirs(figures)
|
|
694
735
|
|
|
736
|
+
# Same overlay style as before; only the axis names and extra maps
|
|
737
|
+
# are new. No 90-angle flip is applied to the stored fiber direction.
|
|
738
|
+
plot_axis_orientation_arrays(axis_maps, figures, scale=scale, dpi=dpi,
|
|
739
|
+
cell=None if cell_list is None else im_cell_n[edge:-edge,edge:-edge])
|
|
740
|
+
|
|
695
741
|
plot_angle_dev(angle_map = orientation_dev, vmin=-1,vmax=1,
|
|
696
742
|
vec0=min_evec[:,:,0] ,vec1=min_evec[:,:,1] ,coherency_map=ori,
|
|
697
743
|
path_png= os.path.join(figures,"Orientation.png"),label="Orientation",dpi=dpi,cmap="coolwarm")
|
|
@@ -699,15 +745,6 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
699
745
|
|
|
700
746
|
|
|
701
747
|
if cell_list == None:
|
|
702
|
-
plot_fancy_overlay(im_fiber_n[edge:-edge,edge:-edge],
|
|
703
|
-
None,
|
|
704
|
-
angle_no_reference, cmap_angle="gist_rainbow",
|
|
705
|
-
path_png= os.path.join(figures,"Angle_NoReference_overlay_nocell.png"),
|
|
706
|
-
label="Angle (x-axis)",dpi=dpi,
|
|
707
|
-
# ,cmap_cell="Greys_r",cmap_fiber="Greys_r", ### use default values here
|
|
708
|
-
# cmap_angle="viridis", alpha_ori =0.8, alpha_cell = 0.4, alpha_fiber = 0.4,
|
|
709
|
-
omin=-90, omax=90,
|
|
710
|
-
scale=scale)
|
|
711
748
|
plot_fancy_overlay(im_fiber_n[edge:-edge,edge:-edge],
|
|
712
749
|
None,
|
|
713
750
|
ori, cmap_angle="turbo",
|
|
@@ -752,15 +789,6 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
752
789
|
# cmap_angle="viridis", alpha_ori =0.8, alpha_cell = 0.4, alpha_fiber = 0.4,
|
|
753
790
|
omin=0, omax=90,scale=scale)
|
|
754
791
|
|
|
755
|
-
plot_fancy_overlay(im_fiber_n[edge:-edge,edge:-edge],
|
|
756
|
-
im_cell_n[edge:-edge,edge:-edge],
|
|
757
|
-
angle_no_reference, cmap_angle="gist_rainbow",
|
|
758
|
-
path_png= os.path.join(figures,"Angle_NoReference_overlay.png"),
|
|
759
|
-
label="Angle (x-axis)",dpi=dpi,
|
|
760
|
-
# ,cmap_cell="Greys_r",cmap_fiber="Greys_r", ### use default values here
|
|
761
|
-
# cmap_angle="viridis", alpha_ori =0.8, alpha_cell = 0.4, alpha_fiber = 0.4,
|
|
762
|
-
omin=-90, omax=90,
|
|
763
|
-
scale=scale)
|
|
764
792
|
|
|
765
793
|
|
|
766
794
|
plt.close("all")
|
|
@@ -818,8 +846,27 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
818
846
|
|
|
819
847
|
plt.close("all")
|
|
820
848
|
|
|
821
|
-
|
|
822
|
-
|
|
849
|
+
if mode_saenopy == True: # only evaluates a single item at each call and returns the arrays
|
|
850
|
+
values = (angle_no_reference, orientation_dev, ori, min_evec, excel_total, excel_angles, excel_distance)
|
|
851
|
+
return values + (segmention,) if return_segmentation else values
|
|
852
|
+
|
|
853
|
+
# np.save(os.path.join(numpy_out, "segmention.npy" ),segmention["mask"][edge:-edge,edge:-edge])
|
|
854
|
+
# np.save(os.path.join(numpy_out, "AngleDeviationMap.npy" ),angle_dev )
|
|
855
|
+
# #np.save(os.path.join(numpy_out, "AngleMap(weight_int).npy" ),angle_dev_weighted)
|
|
856
|
+
# np.save(os.path.join(numpy_out, "AngleDeviationMap(weight_int_coh).npy"),angle_dev_weighted2 )
|
|
857
|
+
# np.save(os.path.join(numpy_out, "AngleMap_NoReference.npy"), angle_no_reference )
|
|
858
|
+
# np.save(os.path.join(numpy_out, "OrientationMap.npy" ),orientation_dev )
|
|
859
|
+
# #np.save(os.path.join(numpy_out, "OrientationMap_weight_int.npy" ),orientation_dev_weighted)
|
|
860
|
+
# np.save(os.path.join(numpy_out, "OrientationMap_weight_intcoh.npy" ),orientation_dev_weighted2 )
|
|
861
|
+
# np.save(os.path.join(numpy_out, "FiberImageCrop.npy" ),normalize(im_fiber_n[edge:-edge,edge:-edge]) )
|
|
862
|
+
# np.save(os.path.join(numpy_out, "CoherencyMap.npy" ),ori )
|
|
863
|
+
# #np.save(os.path.join(numpy_out, "CoherencyMap(weighted_int).npy" ),ori_weight2)
|
|
864
|
+
# np.save(os.path.join(numpy_out, "Vector_min_ax0.npy"),min_evec[:,:,0])
|
|
865
|
+
# np.save(os.path.join(numpy_out, "Vector_min_ax1.npy"),min_evec[:,:,1])
|
|
866
|
+
# np.save(os.path.join(numpy_out, "center.npy"),center_small)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
|
|
823
870
|
return
|
|
824
871
|
|
|
825
872
|
|
|
@@ -827,6 +874,206 @@ def StuctureAnalysisMain(fiber_list,
|
|
|
827
874
|
|
|
828
875
|
|
|
829
876
|
|
|
877
|
+
|
|
878
|
+
def _get_axis_arrays(min_evec, max_evec, ori, fiber):
|
|
879
|
+
"""Keep both eigenvectors; add only the fiber angles towards x and y."""
|
|
880
|
+
angle_x, angle_y = vectors_to_axis_angles(min_evec)
|
|
881
|
+
return {"min_x": angle_x, "min_y": angle_y,
|
|
882
|
+
"min_vx": min_evec[:,:,0], "min_vy": min_evec[:,:,1],
|
|
883
|
+
"max_vx": max_evec[:,:,0], "max_vy": max_evec[:,:,1],
|
|
884
|
+
"coherency": ori, "fiber": fiber}
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _save_axis_arrays(output_folder, arrays):
|
|
888
|
+
"""Common NumPy subset; retain the old angle filename for existing readers."""
|
|
889
|
+
numpy_out = os.path.join(output_folder, "NumpyArrays")
|
|
890
|
+
os.makedirs(numpy_out, exist_ok=True)
|
|
891
|
+
# This optional output belongs to the masked mode and is rewritten below
|
|
892
|
+
# for that mode. A rerun without a mask must not inherit an earlier mask.
|
|
893
|
+
mask_file = os.path.join(numpy_out, "ValidityMask.npy")
|
|
894
|
+
if os.path.isfile(mask_file):
|
|
895
|
+
os.remove(mask_file)
|
|
896
|
+
for name, filename in AXIS_ARRAY_FILENAMES.items():
|
|
897
|
+
values = arrays[name]
|
|
898
|
+
if name == "fiber":
|
|
899
|
+
values = normalize(values) # Original second normalization after cropping.
|
|
900
|
+
np.save(os.path.join(numpy_out, filename), values)
|
|
901
|
+
np.save(os.path.join(numpy_out, "AngleMap_NoReference.npy"), arrays["min_x"])
|
|
902
|
+
for axis in ("x", "y"):
|
|
903
|
+
np.save(os.path.join(numpy_out, f"Alignment_{axis}_axis.npy"), axis_alignment(arrays[f"min_{axis}"]))
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def _axis_orientation_only(fiber_list, out_list, scale, sigma_tensor, edge,
|
|
907
|
+
sigma_first_blur, norm1, norm2, plotting, dpi, SaveNumpy,
|
|
908
|
+
cell_list=None, mask_sigma=5, mask_threshold=0.6,
|
|
909
|
+
mask_invert=False, mode_saenopy=False):
|
|
910
|
+
"""The original fiber steps without dummy cells or radial cell analysis.
|
|
911
|
+
|
|
912
|
+
Kept next to StuctureAnalysisMain to make comparison with full mode easy.
|
|
913
|
+
normalize, gaussian and analyze_local are the same functions used above.
|
|
914
|
+
"""
|
|
915
|
+
import yaml
|
|
916
|
+
if fiber_list is None or out_list is None or len(fiber_list) != len(out_list) or not len(fiber_list):
|
|
917
|
+
raise ValueError("fiber_list and out_list must be nonempty lists of equal length")
|
|
918
|
+
if cell_list is not None and len(cell_list) != len(fiber_list):
|
|
919
|
+
raise ValueError("cell_list must contain one mask-channel image per fiber image")
|
|
920
|
+
if mode_saenopy and len(fiber_list) != 1:
|
|
921
|
+
raise ValueError("mode_saenopy returns one result; provide exactly one fiber image")
|
|
922
|
+
if len({os.path.normcase(os.path.abspath(p)) for p in out_list}) != len(out_list):
|
|
923
|
+
raise ValueError("Each image needs a distinct output directory")
|
|
924
|
+
if not np.isfinite(scale) or scale <= 0:
|
|
925
|
+
raise ValueError("scale must be positive and finite")
|
|
926
|
+
if not sigma_tensor:
|
|
927
|
+
sigma_tensor = 7/scale
|
|
928
|
+
if not np.isfinite(sigma_tensor) or sigma_tensor <= 0:
|
|
929
|
+
raise ValueError("sigma_tensor must be positive and finite")
|
|
930
|
+
|
|
931
|
+
for n,i in tqdm(enumerate(fiber_list)):
|
|
932
|
+
im_fiber = imageio.v2.imread(fiber_list[n])
|
|
933
|
+
if len(im_fiber.shape) == 3 and im_fiber.shape[-1] == 3:
|
|
934
|
+
im_fiber = color.rgb2gray(im_fiber)
|
|
935
|
+
if len(im_fiber.shape) != 2:
|
|
936
|
+
raise ValueError("Analyze one 2D fiber image at a time, not a t/z stack")
|
|
937
|
+
if not isinstance(edge, (int, np.integer)) or edge < 0 or 2*edge >= min(im_fiber.shape):
|
|
938
|
+
raise ValueError("edge must be a nonnegative integer leaving a nonempty image")
|
|
939
|
+
|
|
940
|
+
im_fiber_n = normalize(im_fiber, norm1, norm2)
|
|
941
|
+
im_fiber_g = gaussian(im_fiber_n, sigma=sigma_first_blur)
|
|
942
|
+
ori, max_evec, min_evec, max_eval, min_eval = analyze_local(
|
|
943
|
+
im_fiber_g, sigma=sigma_tensor, size=0, filter_type="gaussian")
|
|
944
|
+
# slice(None) handles edge=0; [0:-0] would be empty.
|
|
945
|
+
crop = slice(edge, -edge) if edge else slice(None)
|
|
946
|
+
axis_maps = _get_axis_arrays(min_evec[crop,crop], max_evec[crop,crop],
|
|
947
|
+
ori[crop,crop], im_fiber_n[crop,crop])
|
|
948
|
+
|
|
949
|
+
# Segment AFTER cropping the raw second channel, as in the actinin
|
|
950
|
+
# reader. Never multiply the fiber image by this mask before the tensor.
|
|
951
|
+
mask_image = None
|
|
952
|
+
validity_mask = np.ones(axis_maps["min_x"].shape, dtype=bool)
|
|
953
|
+
if cell_list is not None:
|
|
954
|
+
mask_image = imageio.v2.imread(cell_list[n])
|
|
955
|
+
if mask_image.ndim == 3 and mask_image.shape[-1] == 3:
|
|
956
|
+
mask_image = color.rgb2gray(mask_image)
|
|
957
|
+
if mask_image.shape != im_fiber.shape:
|
|
958
|
+
raise ValueError("Fiber and mask-channel images must have identical 2D shapes")
|
|
959
|
+
mask_image = mask_image[crop,crop]
|
|
960
|
+
validity_mask = create_axis_mask(mask_image, mask_sigma, mask_threshold, mask_invert)
|
|
961
|
+
# Statistics use the exported intensity convention. This shallow copy
|
|
962
|
+
# keeps the once-normalized background of the historical overlays.
|
|
963
|
+
summary_maps = dict(axis_maps)
|
|
964
|
+
fiber_crop = axis_maps["fiber"]
|
|
965
|
+
summary_maps["fiber"] = normalize(fiber_crop) if np.isfinite(fiber_crop).all() and np.ptp(fiber_crop) > 0 else np.zeros(fiber_crop.shape)
|
|
966
|
+
summary, histogram = summarize_axis_orientation(summary_maps, validity_mask)
|
|
967
|
+
if summary["Valid pixels"] == 0:
|
|
968
|
+
warnings.warn("No valid orientation pixels: alignment is NaN and the histogram is empty", RuntimeWarning)
|
|
969
|
+
|
|
970
|
+
os.makedirs(out_list[n], exist_ok=True)
|
|
971
|
+
dict_file = {"analysis_mode": "axis_orientation_only" if cell_list is None else "axis_orientation_masked",
|
|
972
|
+
"cell_list": None if cell_list is None else [os.path.abspath(cell_list[n])],
|
|
973
|
+
"cell_analysis": "not performed", "source": os.path.abspath(fiber_list[n]),
|
|
974
|
+
"scale": float(scale), "sigma_tensor": float(sigma_tensor), "edge": int(edge),
|
|
975
|
+
"sigma_first_blur": float(sigma_first_blur), "norm1": float(norm1), "norm2": float(norm2),
|
|
976
|
+
"plotting": bool(plotting), "SaveNumpy": bool(SaveNumpy),
|
|
977
|
+
"axis_angle_convention": "degrees [-90,90); x right, positive upwards; y=wrap180(x-90)",
|
|
978
|
+
"vector_components": "[x, y_down]; min=fiber, max=gradient"}
|
|
979
|
+
dict_file["alignment_definition"] = "1 - abs(angle_degrees)/45; mean of valid pixels"
|
|
980
|
+
if cell_list is not None:
|
|
981
|
+
dict_file.update(mask_sigma=float(mask_sigma), mask_threshold=float(mask_threshold),
|
|
982
|
+
mask_invert=bool(mask_invert), mask_source=os.path.abspath(cell_list[n]))
|
|
983
|
+
with open(os.path.join(out_list[n], "parameters.yml"), "w", encoding="utf-8") as yaml_file:
|
|
984
|
+
yaml.safe_dump(dict_file, yaml_file, sort_keys=False)
|
|
985
|
+
if SaveNumpy:
|
|
986
|
+
import csv
|
|
987
|
+
_save_axis_arrays(out_list[n], axis_maps)
|
|
988
|
+
numpy_out = os.path.join(out_list[n], "NumpyArrays")
|
|
989
|
+
if cell_list is not None:
|
|
990
|
+
np.save(os.path.join(numpy_out, "ValidityMask.npy"), validity_mask)
|
|
991
|
+
for axis in ("x", "y"):
|
|
992
|
+
alignment = axis_alignment(axis_maps[f"min_{axis}"])
|
|
993
|
+
np.save(os.path.join(numpy_out, f"Alignment_{axis}_axis.npy"),
|
|
994
|
+
np.where(validity_mask, alignment, np.nan))
|
|
995
|
+
# Small CSVs make statistics available without Excel or pandas.
|
|
996
|
+
with open(os.path.join(out_list[n], "results_axis.csv"), "w", newline="", encoding="utf-8") as file:
|
|
997
|
+
writer = csv.DictWriter(file, fieldnames=list(summary))
|
|
998
|
+
writer.writeheader()
|
|
999
|
+
writer.writerow(summary)
|
|
1000
|
+
np.savetxt(os.path.join(out_list[n], "histogram_axis.csv"), histogram, delimiter=",",
|
|
1001
|
+
header="bin_left_deg,bin_right_deg,count_x,count_y", comments="",
|
|
1002
|
+
fmt=["%.8g", "%.8g", "%d", "%d"])
|
|
1003
|
+
if plotting:
|
|
1004
|
+
figures = os.path.join(out_list[n], "Figures")
|
|
1005
|
+
plot_axis_orientation_arrays(axis_maps, figures, scale=scale, dpi=dpi,
|
|
1006
|
+
mask=validity_mask if cell_list is not None else None)
|
|
1007
|
+
plot_axis_histogram(histogram, summary, os.path.join(figures, "Histogram_axis_orientation.png"), dpi=dpi)
|
|
1008
|
+
if mask_image is not None:
|
|
1009
|
+
plot_axis_channels(axis_maps, mask_image, validity_mask, figures, scale=scale, dpi=dpi)
|
|
1010
|
+
if mode_saenopy:
|
|
1011
|
+
# Full mode retains its historical seven-item tuple. Axis modes
|
|
1012
|
+
# return a named dictionary: no fake cell-center or distance data.
|
|
1013
|
+
return {"arrays": summary_maps, "validity_mask": validity_mask,
|
|
1014
|
+
"summary": summary, "histogram": histogram}
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def create_axis_mask(image, sigma=5, threshold=0.6, invert=False):
|
|
1018
|
+
"""Select all positive regions, without the full mode's largest-cell rule.
|
|
1019
|
+
|
|
1020
|
+
Input is the raw, already edge-cropped second channel. The threshold is
|
|
1021
|
+
intentionally computed on filtered * threshold, matching the old reader
|
|
1022
|
+
including its finite histogram bins. No normalization or hole filling.
|
|
1023
|
+
"""
|
|
1024
|
+
image = np.asarray(image)
|
|
1025
|
+
if image.ndim != 2 or not image.size or not np.isfinite(image).all():
|
|
1026
|
+
raise ValueError("Mask image must be a nonempty finite 2D array")
|
|
1027
|
+
if not np.isfinite(sigma) or sigma < 0 or not np.isfinite(threshold) or threshold <= 0:
|
|
1028
|
+
raise ValueError("mask_sigma must be nonnegative and mask_threshold must be positive and finite")
|
|
1029
|
+
filtered = gaussian(image, sigma=sigma)
|
|
1030
|
+
mask = filtered > threshold_otsu(filtered * threshold)
|
|
1031
|
+
return ~mask if invert else mask
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def axis_alignment(angles):
|
|
1035
|
+
"""Original package's linear score: parallel +1, 45 degrees 0, perpendicular -1.
|
|
1036
|
+
|
|
1037
|
+
This is NOT cos(2*theta). Average scores per pixel, not signed angles: the
|
|
1038
|
+
latter have a wrap at +/-90 degrees. y alignment equals minus x alignment.
|
|
1039
|
+
"""
|
|
1040
|
+
return 1 - np.abs(wrap_axis_angle(angles)) / 45.0
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def summarize_axis_orientation(arrays, mask=None):
|
|
1044
|
+
"""Equal-pixel means and explicitly named optional weighted means.
|
|
1045
|
+
|
|
1046
|
+
Histograms use the same finite fiber pixels for both axes (36 fixed bins).
|
|
1047
|
+
Empty masks or zero total weights yield NaN, never an apparent alignment.
|
|
1048
|
+
"""
|
|
1049
|
+
x = arrays["min_x"]
|
|
1050
|
+
y = arrays["min_y"]
|
|
1051
|
+
if mask is None:
|
|
1052
|
+
mask = arrays.get("validity_mask", np.ones(x.shape, dtype=bool))
|
|
1053
|
+
mask = np.asarray(mask, dtype=bool)
|
|
1054
|
+
if mask.shape != x.shape:
|
|
1055
|
+
raise ValueError("Validity mask shape does not match the angle maps")
|
|
1056
|
+
valid = mask & np.isfinite(x) & np.isfinite(y)
|
|
1057
|
+
summary = {"Selected pixels": int(mask.sum()), "Valid pixels": int(valid.sum())}
|
|
1058
|
+
# Accept the exported FiberImageCrop convention directly, so re-reading
|
|
1059
|
+
# the saved maps reproduces all CSV values without a third normalization.
|
|
1060
|
+
intensity = arrays["fiber"]
|
|
1061
|
+
for axis, angles in (("x", x), ("y", y)):
|
|
1062
|
+
score = axis_alignment(angles)[valid]
|
|
1063
|
+
summary[f"Alignment {axis}"] = float(np.mean(score)) if score.size else np.nan
|
|
1064
|
+
# Use the same cropped intensity normalization as exported FiberImageCrop.
|
|
1065
|
+
for label, weights in (("coherency", arrays["coherency"]),
|
|
1066
|
+
("intensity and coherency", intensity * arrays["coherency"])):
|
|
1067
|
+
weights = weights[valid]
|
|
1068
|
+
keep = np.isfinite(weights) & (weights > 0)
|
|
1069
|
+
summary[f"Alignment {axis} (weighted by {label})"] = (
|
|
1070
|
+
float(np.average(score[keep], weights=weights[keep])) if keep.any() else np.nan)
|
|
1071
|
+
bins = np.linspace(-90, 90, 37)
|
|
1072
|
+
counts_x, _ = np.histogram(x[valid], bins=bins)
|
|
1073
|
+
counts_y, _ = np.histogram(y[valid], bins=bins)
|
|
1074
|
+
return summary, np.column_stack([bins[:-1], bins[1:], counts_x, counts_y])
|
|
1075
|
+
|
|
1076
|
+
|
|
830
1077
|
def SummarizeResultsTotal(data, output_folder= None):
|
|
831
1078
|
if not output_folder:
|
|
832
1079
|
output_folder=os.path.join(data,"CombinedFiles")
|
|
@@ -834,7 +1081,9 @@ def SummarizeResultsTotal(data, output_folder= None):
|
|
|
834
1081
|
if not os.path.exists(output_folder):
|
|
835
1082
|
os.makedirs(output_folder)
|
|
836
1083
|
|
|
837
|
-
list_total = glob.glob(data
|
|
1084
|
+
list_total = glob.glob(os.path.join(data, "**", "results_total.xlsx"), recursive=True)
|
|
1085
|
+
if not list_total:
|
|
1086
|
+
raise FileNotFoundError(f"No results_total.xlsx found in {data}; run full mode with SaveExcel=True first")
|
|
838
1087
|
|
|
839
1088
|
|
|
840
1089
|
|
|
@@ -874,7 +1123,9 @@ def SummarizeResultsDistance(data, output_folder= None, dpi=200):
|
|
|
874
1123
|
if not os.path.exists(output_folder):
|
|
875
1124
|
os.makedirs(output_folder)
|
|
876
1125
|
|
|
877
|
-
list_distance = glob.glob(data
|
|
1126
|
+
list_distance = glob.glob(os.path.join(data, "**", "results_distance.xlsx"), recursive=True)
|
|
1127
|
+
if not list_distance:
|
|
1128
|
+
raise FileNotFoundError(f"No results_distance.xlsx found in {data}; run full mode with SaveExcel=True first")
|
|
878
1129
|
# initialize a combining distance result dictionary for all cells
|
|
879
1130
|
results_distance_combined = { 'Path':[], 'Shell_mid (µm)': [], 'Intensity': [],
|
|
880
1131
|
'Intensity Norm': [], 'Orientation': [],
|
|
@@ -18,8 +18,11 @@ import numpy as np
|
|
|
18
18
|
## circle funtion is calles disk in newer version
|
|
19
19
|
try:
|
|
20
20
|
from skimage.draw import circle
|
|
21
|
-
except:
|
|
22
|
-
from skimage.draw import disk
|
|
21
|
+
except ImportError:
|
|
22
|
+
from skimage.draw import disk
|
|
23
|
+
|
|
24
|
+
def circle(r, c, radius, shape=None):
|
|
25
|
+
return disk((r, c), radius, shape=shape)
|
|
23
26
|
|
|
24
27
|
from scipy.signal import convolve2d
|
|
25
28
|
from CompactionAnalyzer.utilities import convolution_fitler_with_nan
|
|
@@ -195,6 +198,14 @@ def get_principal_vectors(ot_xx, ot_yx, ot_yy):
|
|
|
195
198
|
min_not_defined = np.logical_and(np.isnan(min_evec), ~np.isnan(max_evec))
|
|
196
199
|
min_evec[min_not_defined] = rotate_vector_field(np.pi / 2, max_evec)[min_not_defined]
|
|
197
200
|
|
|
201
|
+
# The analytic eigenvector formula can also be undefined for the MAXIMUM
|
|
202
|
+
# eigenvalue of an axis-aligned tensor (e.g. perfectly vertical fibers).
|
|
203
|
+
# Recover it from a defined minimum vector, just as above in the other
|
|
204
|
+
# direction. Its sign is immaterial for axial angles. Isotropic/flat
|
|
205
|
+
# regions with neither vector defined remain NaN, not an invented angle.
|
|
206
|
+
max_not_defined = np.logical_and(np.isnan(max_evec), ~np.isnan(min_evec))
|
|
207
|
+
max_evec[max_not_defined] = rotate_vector_field(np.pi / 2, min_evec)[max_not_defined]
|
|
208
|
+
|
|
198
209
|
# fill nans with zeros --> makes sense because later weighting with coherency would set zero anyway
|
|
199
210
|
# min_evec[np.isnan(min_evec)] = 0
|
|
200
211
|
# max_evec[np.isnan(max_evec)] = np.nan
|
|
@@ -402,10 +413,13 @@ def normalize(image, lb=0.1, ub=99.9):
|
|
|
402
413
|
|
|
403
414
|
|
|
404
415
|
if __name__ == "__main__":
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
416
|
+
from pathlib import Path
|
|
417
|
+
|
|
418
|
+
# Read a bundled collagen image, independent of the working directory.
|
|
419
|
+
image_path = Path(__file__).resolve().parents[1] / "Tutorial/ExampleCells/Cell_1/C004.tif"
|
|
420
|
+
im = plt.imread(image_path)
|
|
421
|
+
if im.ndim == 3:
|
|
422
|
+
im = im[..., 0]
|
|
409
423
|
|
|
410
424
|
# standard deviation of gaussian filter used for blurring
|
|
411
425
|
# needs to remove all structures smaller the the structure we want to analyze
|
|
@@ -2,6 +2,7 @@ from . import StructureTensor
|
|
|
2
2
|
from . import plotting
|
|
3
3
|
from . import CompactionFunctions
|
|
4
4
|
from . import utilities
|
|
5
|
+
from .api import analyze_image
|
|
5
6
|
#from . import GUI # now use the GUI from the saenopy package instead
|
|
6
7
|
#from . import QExtendedGraphicsView
|
|
7
|
-
#from . import QtShortCuts
|
|
8
|
+
#from . import QtShortCuts
|