Improving Droplet Shape Detection and Tracking from Video

17 ビュー (過去 30 日間)
Zahra
Zahra 2025 年 7 月 14 日
コメント済み: Zahra 2025 年 7 月 19 日
I am trying to write code to track a water droplet over time. This includes identifying the center of the droplet and measuring changes in its circumference. I then calculate the droplet’s displacement in the x and y directions, perform FFT analysis, and plot the 2D trajectory of the droplet's movement. Finally, I generate a 3D volume video showing how the droplet’s shape changes over time.
I used AI-assisted coding to help structure the code. I am not good at creating complicated code; however, the code failed to track the droplet accurately. The binary image is significantly different from the actual droplet. I am attaching the code, along with a video link of a ball as an example (valid for 3 days: https://we.tl/t-5hMniU7T6o), and an image from the first frame of the video. Maybe additional algorithms are required.
Thank you very much!
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
% ======= Parameters =======
videoFile = 'Q1M 1156-N139.avi';
ballDiameterMM = 1;
ballDiameterPixels = 324;
scale = ballDiameterMM / ballDiameterPixels; % mm/pixel
fprintf('Using scale: %.6f mm/pixel\n', scale);
% ======= Read video =======
vidObj = VideoReader(videoFile);
numFrames = floor(vidObj.FrameRate * vidObj.Duration);
fprintf('Total frames: %d\n', numFrames);
% ======= Manual center selection =======
vidObj.CurrentTime = 0;
firstFrame = readFrame(vidObj);
if size(firstFrame, 3) == 3
firstFrameGray = rgb2gray(firstFrame);
else
firstFrameGray = firstFrame;
end
figure('Name', 'Select Ball Center');
imshow(firstFrameGray);
title('Click the center of the ball, then press Enter');
[xManual, yManual] = ginput(1);
hold on; plot(xManual, yManual, 'r+', 'MarkerSize', 15, 'LineWidth', 2); hold off;
manualCenter = [xManual, yManual];
close;
% ======= Manual ellipse reference selection =======
figure('Name', 'Define Ellipse Diameters');
imshow(firstFrameGray);
title('Click two points along the **major axis**, then press Enter');
[majX, majY] = ginput(2);
hold on; plot(majX, majY, 'g-', 'LineWidth', 2);
title('Click two points along the **minor axis**, then press Enter');
[minX, minY] = ginput(2);
plot(minX, minY, 'c-', 'LineWidth', 2); hold off; close;
% Compute reference ellipse parameters
Xc_ref = mean(majX);
Yc_ref = mean(majY);
a_ref = sqrt((majX(2) - majX(1))^2 + (majY(2) - majY(1))^2) / 2;
b_ref = sqrt((minX(2) - minX(1))^2 + (minY(2) - minY(1))^2) / 2;
phi_ref = atan2(majY(2) - majY(1), majX(2) - majX(1)); % radians
fprintf('Reference Ellipse: Center=(%.2f, %.2f), a=%.2f px, b=%.2f px, phi=%.2f°\n', ...
Xc_ref, Yc_ref, a_ref, b_ref, rad2deg(phi_ref));
% ======= Manual ROI selection for enhancement =======
figure('Name', 'Select ROI for Enhancement');
imshow(firstFrameGray);
title('Draw a polygon around the ball for enhancement, then double-click to finish');
roiMask = roipoly();
close;
% Ensure ROI has sufficient size
if sum(roiMask(:)) < 4
warning('ROI is too small. Using full image for enhancement.');
roiMask = true(size(firstFrameGray));
end
% ======= Preallocate =======
centroids = nan(numFrames, 3); % Includes x, y, z coordinates
majorAxisLengths = nan(numFrames, 1);
minorAxisLengths = nan(numFrames, 1);
orientations = nan(numFrames, 1);
% ======= Output video setup for combined 2D and 3D visualization =======
outputVideoFile = 'output_combined.avi';
outputVid = VideoWriter(outputVideoFile, 'Uncompressed AVI');
outputVid.FrameRate = vidObj.FrameRate;
open(outputVid);
% ======= Set figure positions =======
figWidth = 700; % Width for each figure
figHeight = 420;
% Position figure 1 (2D) on the left
figure(1);
set(gcf, 'Position', [100, 100, figWidth, figHeight]);
% Position figure 2 (3D) on the right, next to figure 1
figure(2);
set(gcf, 'Position', [100 + figWidth + 10, 100, figWidth, figHeight]);
% ======= Process each frame =======
vidObj.CurrentTime = 0;
for k = 1:numFrames
frame = readFrame(vidObj);
if size(frame, 3) == 3
grayFrame = rgb2gray(frame);
else
grayFrame = frame;
end
% Dynamically resize and validate ROI mask for current frame
currentMask = imresize(roiMask, size(grayFrame), 'nearest');
if sum(currentMask(:)) < 4
currentMask = true(size(grayFrame)); % Fallback to full image
warning('ROI too small for frame %d. Using full image.', k);
end
% Apply enhancement and brightening with robust handling
enhancedFrame = grayFrame;
enhancedFrame(~currentMask) = imadjust(enhancedFrame(~currentMask), stretchlim(enhancedFrame(~currentMask), [0.2 0.8]), [0 1]);
% Use imadjust for ROI if adapthisteq would fail
enhancedFrame(currentMask) = imadjust(enhancedFrame(currentMask), stretchlim(enhancedFrame(currentMask), [0.2 0.8]), [0 1]);
brightBoosted = imadjust(enhancedFrame, stretchlim(enhancedFrame, [0.2 0.8]), [0 1]);
filteredFrame = medfilt2(brightBoosted, [3 3]);
bw = imbinarize(filteredFrame, 'adaptive', 'ForegroundPolarity', 'bright', 'Sensitivity', 0.6);
bw = bwareaopen(bw, 50);
% Create reference mask based on pink dashed ellipse
[x, y] = meshgrid(1:size(grayFrame, 2), 1:size(grayFrame, 1));
theta = atan2(y - Yc_ref, x - Xc_ref) - phi_ref;
r = ((x - Xc_ref) * cos(phi_ref) + (y - Yc_ref) * sin(phi_ref)).^2 / a_ref^2 + ...
(-(x - Xc_ref) * sin(phi_ref) + (y - Yc_ref) * cos(phi_ref)).^2 / b_ref^2;
refMask = r <= 1;
bw = bw & refMask; % Apply mask to limit to reference boundary
bw = imdilate(bw, strel('disk', 1));
cc = bwconncomp(bw);
stats = regionprops(cc, 'Centroid', 'Area', 'Orientation', 'MajorAxisLength', 'MinorAxisLength');
if isempty(stats)
if k > 1 && ~isnan(centroids(k-1,1))
centroids(k,:) = centroids(k-1,:); % Hold last known position
else
centroids(k,:) = [NaN, NaN, NaN];
end
majorAxisLengths(k) = NaN;
minorAxisLengths(k) = NaN;
orientations(k) = NaN;
continue;
end
[~, ballIdx] = max([stats.Area]); % Use largest component within mask
selected = stats(ballIdx);
% Ensure ellipse is slightly larger
selected.MajorAxisLength = selected.MajorAxisLength * 1.1;
selected.MinorAxisLength = selected.MinorAxisLength * 1.1;
centroids(k,1:2) = selected.Centroid;
majorAxisLengths(k) = selected.MajorAxisLength * scale;
minorAxisLengths(k) = selected.MinorAxisLength * scale;
orientations(k) = rad2deg(-selected.Orientation);
% Estimate z-coordinate
if ~isnan(minorAxisLengths(k)) && minorAxisLengths(k) > 0
z = (b_ref / (selected.MinorAxisLength * scale / b_ref)) * ballDiameterMM;
z = min(max(z, 0), ballDiameterMM * 10);
else
z = ballDiameterMM;
end
centroids(k, 3) = z;
% ======= Display results for 2D visualization (Figure 1) =======
figure(1); clf;
subplot(1,3,1);
imshow(grayFrame); title('Original Grayscale');
subplot(1,3,2);
imshow(brightBoosted); title('Enhanced + Brightened');
subplot(1,3,3);
imshow(bw); hold on;
plot(centroids(k,1), centroids(k,2), 'ro', 'MarkerSize', 10, 'LineWidth', 2);
% Draw detected ellipse (yellow)
theta = linspace(0, 2*pi, 100);
a = selected.MajorAxisLength / 2;
b = selected.MinorAxisLength / 2;
Xc = centroids(k,1);
Yc = centroids(k,2);
phi = deg2rad(-orientations(k));
x_ellipse = Xc + a * cos(theta) * cos(phi) - b * sin(theta) * sin(phi);
y_ellipse = Yc + a * cos(theta) * sin(phi) + b * sin(theta) * cos(phi);
plot(x_ellipse, y_ellipse, 'y-', 'LineWidth', 2);
% Draw reference ellipse (magenta dashed)
x_ref = Xc_ref + a_ref * cos(theta) * cos(phi_ref) - b_ref * sin(theta) * sin(phi_ref);
y_ref = Yc_ref + a_ref * cos(theta) * sin(phi_ref) + b_ref * sin(theta) * cos(phi_ref);
plot(x_ref, y_ref, 'm--', 'LineWidth', 1.5);
title(sprintf('Binary + Ellipses (Frame %d/%d)', k, numFrames));
hold off; drawnow;
% Ensure figure is rendered before capturing
pause(0.1); % Increased delay
frame2D = getframe(figure(1));
if isempty(frame2D.cdata)
warning('No frame captured for 2D at frame %d. Skipping write.', k);
continue;
end
% ======= 3D Visualization (Figure 2) =======
figure(2); clf;
[X, Y, Z] = sphere(20);
a_radius = (selected.MajorAxisLength * scale) / 2;
b_radius = (selected.MinorAxisLength * scale) / 2;
c_radius = ballDiameterMM / 2;
X = X * a_radius;
Y = Y * b_radius;
Z = Z * c_radius;
phi = deg2rad(-orientations(k));
X_rot = X * cos(phi) - Y * sin(phi);
Y_rot = X * sin(phi) + Y * cos(phi);
X_rot = X_rot + centroids(k,1) * scale;
Y_rot = Y_rot + centroids(k,2) * scale;
Z = Z + z;
surf(X_rot, Y_rot, Z, 'FaceColor', 'b', 'EdgeColor', 'none', 'FaceAlpha', 0.8);
hold on;
plot3(centroids(k,1) * scale, centroids(k,2) * scale, z, ...
'ro', 'MarkerSize', 10, 'LineWidth', 2);
axis equal;
xlabel('X (mm)'); ylabel('Y (mm)'); zlabel('Z (mm)');
title(sprintf('3D Ball Tracking (Frame %d/%d)', k, numFrames));
grid on;
currentCentroid = centroids(k, :) * scale;
if ~isnan(currentCentroid(1))
xRange = currentCentroid(1) + [-ballDiameterMM*5, ballDiameterMM*5];
yRange = currentCentroid(2) + [-ballDiameterMM*5, ballDiameterMM*5];
zRange = [0, z + ballDiameterMM*5];
else
xRange = [0, vidObj.Width * scale];
yRange = [0, vidObj.Height * scale];
zRange = [0, ballDiameterMM * 10];
end
axis([xRange, yRange, zRange]);
lighting gouraud;
camlight;
view(45, 30);
hold off; drawnow;
pause(0.1); % Increased delay
frame3D = getframe(figure(2));
if isempty(frame3D.cdata)
warning('No frame captured for 3D at frame %d. Skipping write.', k);
continue;
end
targetHeight = min(size(frame2D.cdata, 1), size(frame3D.cdata, 1));
frame2D_resized = imresize(frame2D.cdata, [targetHeight, NaN]);
frame3D_resized = imresize(frame3D.cdata, [targetHeight, NaN]);
combinedFrame = cat(2, frame2D_resized, frame3D_resized);
writeVideo(outputVid, combinedFrame);
end
% ======= Close video writer =======
close(outputVid);
fprintf('Combined video saved to %s\n', outputVideoFile);
% ======= Save video to SavedVideos directory =======
outputDir = 'SavedVideos';
if ~exist(outputDir, 'dir')
mkdir(outputDir);
end
try
movefile(outputVideoFile, fullfile(outputDir, outputVideoFile));
fprintf('Moved combined video to %s\n', fullfile(outputDir, outputVideoFile));
catch e
fprintf('Error moving video to %s: %s\n', outputDir, e.message);
end
% ======= Displacement Analysis =======
validIdx = ~isnan(centroids(:,1));
timeVec = (0:numFrames-1)' / vidObj.FrameRate;
startIdx = find(validIdx, 1, 'first');
x0 = centroids(startIdx,1);
y0 = centroids(startIdx,2);
displacementX = centroids(:,1) - x0;
displacementY = centroids(:,2) - y0;
displacementX_mm = displacementX * scale;
displacementY_mm = displacementY * scale;
figure;
subplot(2,1,1)
plot(timeVec(validIdx), displacementX_mm(validIdx), 'b-');
xlabel('Time (s)'); ylabel('Displacement X (mm)');
title('X Displacement of Styrofoam Ball');
subplot(2,1,2)
plot(timeVec(validIdx), displacementY_mm(validIdx), 'r-');
xlabel('Time (s)'); ylabel('Displacement Y (mm)');
title('Y Displacement of Styrofoam Ball');
figure;
plot(displacementX_mm(validIdx), displacementY_mm(validIdx), 'ko-', 'MarkerFaceColor', 'k');
xlabel('X Displacement (mm)'); ylabel('Y Displacement (mm)');
title('2D Trajectory of Styrofoam Ball');
axis equal; grid on;
% ======= FFT Analysis =======
Fs = vidObj.FrameRate;
X_valid = displacementX_mm(validIdx);
Y_valid = displacementY_mm(validIdx);
N = length(X_valid);
f = (0:N-1)*(Fs/N);
X_centered = X_valid - mean(X_valid);
Y_centered = Y_valid - mean(Y_valid);
X_FFT = fft(X_centered); Y_FFT = fft(Y_centered);
magX = abs(X_FFT)/N; magY = abs(Y_FFT)/N;
ylimMax = max([magX; magY]);
figure;
subplot(2,1,1); plot(f, magX); xlabel('Frequency (Hz)');
ylabel('Magnitude'); title('FFT of X Displacement'); ylim([0 ylimMax]);
subplot(2,1,2); plot(f, magY); xlabel('Frequency (Hz)');
ylabel('Magnitude'); title('FFT of Y Displacement'); ylim([0 ylimMax]);
figure;
subplot(2, 1, 1);
plot(timeVec(validIdx), majorAxisLengths(validIdx), 'k-', 'LineWidth', 2);
xlabel('Time (s)');
ylabel('Major Axis Length (mm)');
title('Major Axis Length Over Time');
subplot(2, 1, 2);
plot(timeVec(validIdx), minorAxisLengths(validIdx), 'k-', 'LineWidth', 2);
xlabel('Time (s)');
ylabel('Minor Axis Length (mm)');
title('Minor Axis Length Over Time');
  4 件のコメント
Image Analyst
Image Analyst 2025 年 7 月 19 日
It's already expired.
Zahra
Zahra 2025 年 7 月 19 日
Please check the new link (Valid for 3 days : https://we.tl/t-AmAIvcYUYi ).

サインインしてコメントする。

回答 (0 件)

カテゴリ

Help Center および File ExchangeComputer Vision with Simulink についてさらに検索

製品


リリース

R2023b

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!

Translated by