Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
75dc821
Add MATLAB and Python file for getTifBoundary
don04lee Sep 15, 2025
cf2dd0d
Restructure helper methods to be in separate folder
don04lee Sep 16, 2025
02eaffe
Create boilerplate for get_tif_boundary.py
don04lee Sep 16, 2025
7387906
Add input files for testing get_tif_boundary.py
don04lee Sep 18, 2025
dd86679
Reformat helper method structure
don04lee Sep 29, 2025
d014c6b
Modify find_outline_slope.py logic
don04lee Sep 30, 2025
901d410
Debug and fix find_outline_slope.py
don04lee Oct 2, 2025
7191d5f
Pass CI/CD tests for get_relative_angles.py
don04lee Oct 4, 2025
71effa9
Translate getTifBoundary.m logic to get_tif_boundary.py
don04lee Oct 7, 2025
6fae30b
Testers for get_tif_boundary.py
don04lee Oct 7, 2025
97899dc
Complete conversion for get_tif_boundary.py
don04lee Oct 8, 2025
5acbba4
Add sklearn to dependencies
don04lee Oct 9, 2025
93cd199
Change dependency to sci-kit learn
don04lee Oct 9, 2025
1926cee
Explicitly add dependency versions
don04lee Oct 9, 2025
2b4c58e
Fix opencv-python dependency version
don04lee Oct 9, 2025
98f76fc
Change opencv to remove GUI + OS differences
don04lee Oct 9, 2025
bd749b4
ci: add diagnostic step for environment/package/image checks
don04lee Oct 9, 2025
4a3b89e
ci debug
don04lee Oct 9, 2025
61a3b2a
Revert ci with pytest -rs
don04lee Oct 9, 2025
4579201
Remove break-system-packages from ci
don04lee Oct 9, 2025
0c8d760
Made KNN algorithm deterministic
don04lee Oct 13, 2025
6d616b6
Change ci to replicate Docker environment
don04lee Oct 13, 2025
f1f1971
Try rounding and easing tolerance for OS differences
don04lee Oct 13, 2025
23863e0
Expose NumPy platform dependency in Github CI
don04lee Oct 14, 2025
8e5e5f1
Edit to activate venv for NumPy env exposure
don04lee Oct 14, 2025
cd3708a
ci.yml modifications
don04lee Oct 14, 2025
be716cb
Add delimiter to csv reader
don04lee Oct 14, 2025
af53086
Remove ambiguous tiebreaker in KNN
don04lee Oct 14, 2025
0f202d4
Start process_image.py with DOCSTRING
don04lee Oct 16, 2025
b67b742
Clean up DOCSTRING
don04lee Oct 16, 2025
652819c
Create process_fibers for getFire and getCT
don04lee Oct 16, 2025
55ab901
Create class for get_ct and get_fire
don04lee Oct 20, 2025
51c986f
Add ct/fire files that will be used for #6
don04lee Oct 20, 2025
274982d
Add dataclasses for CurveCP and FeatureCP
don04lee Oct 23, 2025
0f5ab52
Fix somehow reverted new_curv bug??
don04lee Oct 23, 2025
9fca1f6
Integration between get_ct and new_curv
don04lee Oct 23, 2025
35e4cae
Fix circular dependency with get_ct
don04lee Oct 24, 2025
bc5093b
Convert in_curves in new_curv to DataFrame
don04lee Oct 24, 2025
bd07080
Work on process_fibers
don04lee Oct 24, 2025
5ae2d9e
Convert getCT.m
don04lee Oct 27, 2025
93403af
Change get_ct DOCSTRING
don04lee Oct 27, 2025
be1599e
Partially convert getFire.m
don04lee Oct 27, 2025
1a8beff
Save file for ROI output dir
don04lee Oct 27, 2025
f813f33
Add format_df_to_excel helper
don04lee Oct 28, 2025
a3f0dd4
Get features corresponding to each ROI
don04lee Oct 28, 2025
31434a5
Converting some of getAlignment2ROI.m
don04lee Oct 28, 2025
5074b44
Add some converesion of getAlignment2ROI
don04lee Oct 30, 2025
f0fcc7f
Create tester files for process_image.py
don04lee Nov 10, 2025
2dde676
Test get_ct.py and process_fibers.py
don04lee Nov 11, 2025
0b6d1bb
Reimplement getAlignment2ROI.m to Python
don04lee Nov 11, 2025
0584f9a
Show overlay of tiffs and ROIs
don04lee Nov 11, 2025
7d67970
Parallel computing for get_alignment_to_roi for faster calculation
don04lee Nov 12, 2025
a88ed64
Correct BoundaryFiberPositions and Reconstruction
don04lee Nov 13, 2025
cbec794
Find nearest boundary with blue line
don04lee Nov 18, 2025
85445ef
Complete implementation of process_image.py
don04lee Nov 20, 2025
fd53018
Make helper function for overlay
don04lee Nov 20, 2025
68c498d
Refactor process_image for readability
don04lee Nov 20, 2025
2d12bdc
Add boundary_point_row/boundary_point_col to ROI excel file
don04lee Dec 2, 2025
e5b31b9
Add box density/dist to nearest fibers to Excel
don04lee Dec 2, 2025
9daf966
Add tester for process_image.py
don04lee Dec 2, 2025
4f360ac
Add json test suite for new_curv.py
don04lee Dec 4, 2025
fad9d08
Add all new_curv test cases
don04lee Dec 4, 2025
1f7090d
Update .gitignore
yuminguw Jan 19, 2026
d606cb9
Refactor process_image API with dataclass parameters and enhance boun…
yuminguw Jan 19, 2026
87d935e
Add interactive PyQt GUI for fiber analysis parameter configuration
yuminguw Jan 19, 2026
3a39594
Add JSON test structure for process_image
don04lee Jan 26, 2026
48ff124
Add test cases for process_image, no boundary
don04lee Jan 27, 2026
a9b30cd
Add all process_image real1 test cases
don04lee Jan 27, 2026
0ab816f
Test cases for real2 processImage noBoundary
don04lee Jan 27, 2026
7352183
Add test cases with real2 and no boundary option
don04lee Jan 28, 2026
f9b511c
Add all real2 process image TC's
don04lee Jan 30, 2026
73e3aa5
Add all real2 new_curv tc's
don04lee Jan 30, 2026
c80b754
Add real2 tif test cases for processImage
don04lee Feb 11, 2026
df6501a
Pass all process_image tc's
don04lee Feb 18, 2026
c714080
Clean up test_process_image
don04lee Feb 18, 2026
a7e1d91
Relax result tolerances in tests
don04lee Feb 19, 2026
b0c56f9
Modify generate_overlay based on new boundary_pt row/col format
don04lee Feb 19, 2026
6c32a72
Merge branch 'main' into 22-consistent-test-struct
ctrueden Feb 19, 2026
6028802
Add tests for finding associations
don04lee Feb 19, 2026
821440a
Change order of row/col in output df
don04lee Feb 19, 2026
2dda06f
Clean up test_process_image.py
don04lee Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
name: ci

on:
# Allow manual runs from the Actions tab
workflow_dispatch:
# Run on pushes to any branch (including feature branches)
push:
branches: [ "**" ]
# And on PRs targeting main
branches: ["**"]
pull_request:
branches: [ main ]
branches: [main]

jobs:
test:
Expand All @@ -19,19 +16,28 @@ jobs:
steps:
- uses: actions/checkout@v4

# Install system dependencies (like in your Docker container)
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libgl1 libglib2.0-0 python3-venv

# Set up Python
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
# Create virtual environment
- name: Create virtual environment
run: |
python -m pip install --upgrade pip
# Install without curvelops to avoid FFTW/CurveLab build on CI
python -m pip install --break-system-packages -e .
python -m pip install --break-system-packages pytest ruff
python -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip setuptools wheel
python -m pip install -e .
python -m pip install pytest ruff

# Run tests in the venv
- name: Run tests
env:
QT_QPA_PLATFORM: "offscreen"
run: |
pytest -q
source venv/bin/activate
pytest -rs
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ local.properties
.classpath
.settings/
.loadpath
*.idea

# External tool builders
.externalToolBuilders/
Expand Down Expand Up @@ -59,6 +60,7 @@ local.properties
*.tli
*.tlh
*.tmp
*.xlsx
*.vspscc
.builds
*.dotCover
Expand Down Expand Up @@ -174,3 +176,9 @@ lastPATH_CTF.mat
*.tif

pycurvelets/opt/*.*

# test tmp folder
tests/test_images/CA_Out/

# referenced MATLAB files
*.m
15 changes: 8 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ description = "Python translation of CurveAlign; curvelet-based quantification a
authors = [{ name = "Eliceiri Lab" }]
requires-python = ">=3.11"
dependencies = [
"pandas",
"numpy",
"matplotlib",
"scipy",
"opencv-python",
"scikit-image",
"openpyxl",
"matplotlib>=3.10.1",
"napari",
"numpy>=2.2.6",
"opencv-python-headless>=4.10.0.84",
"openpyxl>=3.1.5",
"pandas>=2.2.3",
"pyqt6",
"qtpy",
"scikit-image>=0.25.0",
"scikit-learn>=1.7.1",
"scipy>=1.15.2",
]

[dependency-groups]
Expand Down
241 changes: 241 additions & 0 deletions src/curvealign_matlab/getTifBoundary.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
function [resMat,resMatNames,numImPts] = getTifBoundary(coords,img,object,imgName,distThresh,fibKey,endLength,fibProcMeth,distMini)

% getTifBoundary.m - This function takes the coordinates from the boundary file, associates them with curvelets, and produces relative angle measures.
%
% Inputs:
% coords - the locations of the endpoints of each line segment making up the boundary
% img - the image being measured
% object - a struct containing the center and angle of each measured curvelet, generated by the newCurv function
% distThresh - number of pixels from boundary we should evaluate curvelets
% boundaryImg - tif file with boundary outlines, must be a mask file
% fibKey - list indicating the beginning of each new fiber in the object struct, allows for fiber level processing
% distMini - minimum distrance to the boundary, to get rid of the fiber on or very close to the boundary
%
% Output:
% measAngs - all relative angle measurements, not filtered by distance
% measDist - all distances between the curvelets and the boundary points, not filtered
% inCurvsFlag - curvelets that are considered
% outCurvsFlag - curvelets that are not considered
% measBndry = points on the boundary that are associated with each curvelet
% inDist = distance between boundary and curvelet for each curvelet considered
% numImPts = number of points in the image that are less than distThresh from boundary
% insCt = number of curvelets inside an epithelial region
%
%
% By Jeremy Bredfeldt, LOCI, Morgridge Institute for Research, 2013

%Note: a "curv" could be a curvelet or a fiber segment, depending on if CT or FIRE is used

imHeight = size(img,1);
imWidth = size(img,2);
sz = [imHeight,imWidth];

% figure(600);
% imshow(img);

% figure(500);
% hold on;
% for k = 1:length(coords)
% boundary = coords{k};
% plot(boundary(:,2), boundary(:,1), 'y', 'LineWidth', 2);
% end

%collect all fiber points
allCenterPoints = vertcat(object.center);
%collect all boundary points
% coords = vertcat(coords{2:end,1});
coords = vertcat(coords{1:end,1});

%collect all region points
linIdx = sub2ind(sz, allCenterPoints(:,1), allCenterPoints(:,2));

[idx_dist,dist] = knnsearch(coords,allCenterPoints); %closest point to a boundary
reg_dist = img(linIdx);


%YL: test the boundary association
% figure(1002);clf;set(gcf,'pos',[200 300 imWidth imHeight ]);
% plot(coords(:,2),coords(:,1),'k.'); axis ij
% axis([1 imWidth 1 imHeight ]);hold on

%[idx_reg,reg_dist] = knnsearch([reg_col,reg_row],allCenterPoints); %closest point to a filled in region

%Make a list of points in the image (points scattered throughout the image)
C = floor(imWidth/20); %use at least 20 per row in the image, this is done to speed this process up
[I, J] = ind2sub(size(img),1:C:imHeight*imWidth);
allImPoints = [I; J]';
%Get list of image points that are a certain distance from the boundary
[~,dist_im] = knnsearch(coords(1:3:end,:),allImPoints); %returns nearest dist to each point in image
%threshold distance
if isempty(distMini) % keep all the fibers within the distance threshold
inIm = dist_im <= distThresh;
else % get rid of fibers on or very close to the boundary
inIm = (dist_im <= distThresh & dist_im > distMini);
end
%count number of points
inPts = allImPoints(inIm);
numImPts = length(inPts)*C;
% numImPts = 0;


%process all curvs, at this point
curvsLen = length(object);
nbDist = nan(1,curvsLen); %nearest boundary distance
nrDist = nan(1,curvsLen); %nearest region distance
nbAng = nan(1,curvsLen); %nearest boundary relative angle
epDist = nan(1,curvsLen); %distance to extension point intersection
epAng = nan(1,curvsLen); %relative angle of extension point intersection
measBndry = nan(curvsLen,2);
inCurvsFlag = ~logical(1:curvsLen);
outCurvsFlag = ~logical(1:curvsLen);
% %ROI properties
% bwROI.coords = coords;
% bwROI.imageWidth = imWidth;
% bwROI.imageHeight = imHeight;
if isempty(distMini)
for i = 1:curvsLen
%-- inside region?
nrDist(i) = reg_dist(i)==255|reg_dist(i)== 1; %YL: mask can be 1-0(matlab lab) or 255-0(ImageJ)
%-- distance to nearest epithelial boundary
nbDist(i) = dist(i);
%-- relative angle at nearest boundary point
if dist(i) <= distThresh
[nbAng(i), bPt] = GetRelAng([coords(:,2),coords(:,1)],idx_dist(i),object(i).angle,imHeight,imWidth,i); % add i as an input argument for debug
% %%
% bwROI.index2object = idx_dist(i);
% fiberobject.center = object(i).center;
% fiberobject.angle = object(i).angle;
% angleOption = 0; % caclulate all angles
% [relativeAngles, ROImeasurements] = getRelativeangles(bwROI,fiberobject,angleOption);
% fprintf(' original relative angle is %3.2f \n re-calculated relative angle is %3.2f \n ',nbAng(i),relativeAngles.angle2boundaryEdge);
% fprintf(' relative angle to ROI orientation is %3.2f \n',relativeAngles.angle2boundaryCenter);
% fprintf('relative angle to ROI-fiber centers line is %3.2f \n',relativeAngles.angle2centersLine);
%%
else
nbAng(i) = nan; % if out of the region
bPt = nan(1,2); % if out of the region
end
%-- extension point features
[lineCurv orthoCurv] = getPointsOnLine(object(i),imWidth,imHeight,distThresh);
[intLine, iLa, iLb] = intersect([lineCurv(:,2) lineCurv(:,1)],coords,'rows');
if (~isempty(intLine))
%get the closest distance from the curvelet center to the
%intersection (get rid of the farther one(s))
[idxLineDist, lineDist] = knnsearch(intLine,object(i).center);
boundaryPtIdx = iLb(idxLineDist);
%%tentatively turn the extension feature off
% %-- extension point distance
% epDist(i) = lineDist;
% %-- extension point angle
% [epAng(i) bPt1] = GetRelAng([coords(:,2),coords(:,1)],boundaryPtIdx,object(i).angle,imHeight,imWidth,i);
else
epDist(i) = nan;% no intersection exists
epAng(i) = nan; % no angle exists
bPt1 = nan(1,2); % if no intersection set boundary to be [NaN NaN]
end
measBndry(i,:) = bPt; % nearest boundary
end %of for loop
else % get rid of fibers on or within the minimum distance to the boundary
for i = 1:curvsLen
%-- inside region?
nrDist(i) = reg_dist(i)==255|reg_dist(i)== 1; %YL: mask can be 1-0(matlab lab) or 255-0(ImageJ)
%-- distance to nearest epithelial boundary
nbDist(i) = dist(i);
%-- relative angle at nearest boundary point
if (dist(i) <= distThresh & dist(i) > distMini)
[nbAng(i), bPt] = GetRelAng([coords(:,2),coords(:,1)],idx_dist(i),object(i).angle,imHeight,imWidth,i); % add i as an input argument for debug
else
nbAng(i) = nan; % if out of the region
bPt = nan(1,2); % if out of the region
end
%-- extension point features
[lineCurv orthoCurv] = getPointsOnLine(object(i),imWidth,imHeight,distThresh);
[intLine, iLa, iLb] = intersect([lineCurv(:,2) lineCurv(:,1)],coords,'rows');
if (~isempty(intLine))
%get the closest distance from the curvelet center to the
%intersection (get rid of the farther one(s))
[idxLineDist, lineDist] = knnsearch(intLine,object(i).center);
boundaryPtIdx = iLb(idxLineDist);
%%tentatively turn the extension feature off
% %-- extension point distance
% epDist(i) = lineDist;
% %-- extension point angle
% [epAng(i) bPt1] = GetRelAng([coords(:,2),coords(:,1)],boundaryPtIdx,object(i).angle,imHeight,imWidth,i);
else
epDist(i) = nan;% no intersection exists
epAng(i) = nan; % no angle exists
bPt1 = nan(1,2); % if no intersection set boundary to be [NaN NaN]
end
measBndry(i,:) = bPt; % nearest boundary
end %of for loopend
end



resMat = [nbDist', ... %nearest dist to a boundary
nrDist', ... %flag, 0 for outside boundary, 1 for inside boundary
nbAng', ... %nearest relative boundary angle
epDist', ... %extension point distance
epAng', ... %extension point relative boundary angle
measBndry]; %list of boundary points associated with fibers
resMatNames = {
'nearestBoundDist', ...
'nearestRegionDist', ...
'nearestBoundAng', ...
'extensionPointDist', ...
'extensionPointAng', ...
'bndryPtRow', ...
'bndryPtCol'};

end %of main function

function [relAng, boundaryPt] = GetRelAng(coords,idx,fibAng,imHeight,imWidth,fnum)
boundaryAngle = FindOutlineSlope([coords(:,2),coords(:,1)],idx);
boundaryPt = coords(idx,:);
if (boundaryPt(1) == 1 || boundaryPt(2) == 1 || boundaryPt(1) == imHeight || boundaryPt(2) == imWidth)
%don't count fiber if boundary point is along edge of image
tempAng = 0;
else
%--compute relative angle here--
%There is a 90 degree phase shift in fibAng and boundaryAngle due to image orientation issues in Matlab.
% -therefore no need to invert (ie. 1-X) circ_r here.
tempAng = circ_r([fibAng*2*pi/180; boundaryAngle*2*pi/180]);
tempAng = 180*asin(tempAng)/pi;
% %YL debug the NaN angle
% if isnan(tempAng)
% figure(1002),plot(coords(idx,1),coords(idx,2),'ro','MarkerSize',10)
% text(coords(idx,1),coords(idx,2),sprintf('%d',fnum));
% disp(sprintf('fiber %d relative angle is Nan, fibAng = %f, boundaryAngle = %f, idx_dist = %d',fnum,fibAng,boundaryAngle,idx))
% % pause(3)
% end
end
relAng = tempAng;
end

function [lineCurv orthoCurv] = getPointsOnLine(object,imWidth,imHeight,boxSz)
center = object.center;
angle = object.angle;
slope = -tand(angle);
%orthoSlope = -tand(angle + 90); %changed from tand(obj.ang) to -tand(obj.ang + 90) 10/12 JB
intercept = center(1) - (slope)*center(2);
%orthoIntercept = center(1) - (orthoSlope)*center(2);

%[p1 p2] = getBoxInt(slope, intercept, imWidth, imHeight, center, boxSz);
if isinf(slope)
dist_y = 0;
dist_x = boxSz;
else
dist_y = boxSz/sqrt(1+slope*slope);
dist_x = dist_y*slope;
end
p1 = [center(2) - dist_y, center(1) - dist_x];
p2 = [center(2) + dist_y, center(1) + dist_x];
[lineCurv ~] = GetSegPixels(p1,p2);

%Not using the orthogonal slope for anything now
%[p1 p2] = getIntImgEdge(orthoSlope, orthoIntercept, imWidth, imHeight, center);
%[orthoCurv, ~] = GetSegPixels(p1,p2);
orthoCurv = [];

end

9 changes: 5 additions & 4 deletions src/pycurvelets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Optional import: allow the package to be imported without native curvelet deps.
try:
from .new_curv import new_curv # type: ignore

HAS_CURVELETS = True
except Exception:
HAS_CURVELETS = False

__all__ = ["new_curv", "HAS_CURVELETS"]


__all__ = [
"HAS_CURVELETS",
"new_curv",
]
Loading