Improving Droplet Shape Detection and Tracking from Video
    16 Ansichten (letzte 30 Tage)
  
       Ältere Kommentare anzeigen
    
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 Kommentare
Antworten (0)
Siehe auch
Kategorien
				Mehr zu Computer Vision with Simulink finden Sie in Help Center und File Exchange
			
	Community Treasure Hunt
Find the treasures in MATLAB Central and discover how the community can help you!
Start Hunting!

