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.
Files changed (24) hide show
  1. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/CompactionFunctions.py +289 -38
  2. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/StructureTensor.py +20 -6
  3. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/__init__.py +2 -1
  4. compactionanalyzer-1.0.8/CompactionAnalyzer/api.py +301 -0
  5. compactionanalyzer-1.0.8/CompactionAnalyzer/distance.py +53 -0
  6. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/plotting.py +242 -1
  7. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/utilities.py +1 -1
  8. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer.egg-info/PKG-INFO +10 -3
  9. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer.egg-info/SOURCES.txt +6 -1
  10. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer.egg-info/requires.txt +1 -1
  11. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/PKG-INFO +10 -3
  12. compactionanalyzer-1.0.8/README.md +1159 -0
  13. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/setup.py +2 -2
  14. compactionanalyzer-1.0.8/test/test_direct_api.py +335 -0
  15. compactionanalyzer-1.0.8/test/test_point_distance.py +75 -0
  16. compactionanalyzer-1.0.8/test/test_reference_masks.py +40 -0
  17. compactionanalyzer-1.0.6/README.md +0 -199
  18. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/GUI_old_use_saenopy_instead.py +0 -0
  19. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/QExtendedGraphicsView.py +0 -0
  20. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer/QtShortCuts.py +0 -0
  21. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer.egg-info/dependency_links.txt +0 -0
  22. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/CompactionAnalyzer.egg-info/top_level.txt +0 -0
  23. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/LICENSE +0 -0
  24. {compactionanalyzer-1.0.6 → compactionanalyzer-1.0.8}/setup.cfg +0 -0
@@ -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): # only used for the Saenopy Version to return data files directly after single run
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
- y,x = np.indices(ori.shape)
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 = np.arccos(np.abs(dx_norm * min_evec[:,:,0] + dy_norm*min_evec[:,:,1])) * 360/(2*np.pi)
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
- Plott results
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
- if mode_saenopy == True: # only evaluates a single item at each call and returns the arrays
822
- return angle_no_reference, orientation_dev, ori, min_evec, excel_total, excel_angles, excel_distance
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+"\\**\\results_total.xlsx", recursive=True)
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+"\\**\\results_distance.xlsx", recursive=True)
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 as circle
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
- # reading an image
406
- # use r"path\path\file.png" on windows
407
- im = plt.imread("/home/user/Desktop/ingo_fiber_orientations/7500_17022020/MAX_7500_17022020.lif - Series002.tif")[:,
408
- :, 0]
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