Improving Droplet Shape Detection and Tracking from Video
    17 visualizaciones (últimos 30 días)
  
       Mostrar comentarios más antiguos
    
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 comentarios
Respuestas (0)
Ver también
Categorías
				Más información sobre Computer Vision with Simulink en Help Center y File Exchange.
			
	Community Treasure Hunt
Find the treasures in MATLAB Central and discover how the community can help you!
Start Hunting!

