Skip to main content

Stretch Tracker: Advanced Technical Architecture and Implementation

Stretch Tracker: Advanced Technical Architecture and Implementation
Table of Contents

The Birth of the Stretch Tracker: A Developer’s Story
#

As developers, we often find ourselves deeply immersed in our work, sitting for hours on end in front of our screens without even realizing how much time has passed. I was no different. During a particularly intensive project, I noticed the toll these long coding sessions were taking on my physical health - back pain, stiff neck, and decreased productivity became unwelcome companions in my daily routine.

One evening, while taking a rare break and browsing through a fitness app, I had a realization: most productivity apps remind you to take breaks, but they don’t actually ensure you’re using that break effectively for your health. That’s when the idea struck me - what if I built an application that not only reminds developers to stretch periodically but also uses computer vision to verify they’re actually performing their stretches?

The Stretch Tracker was born from this personal need - an intelligent health companion that would:

  1. Send timely, customizable reminders to take stretch breaks
  2. Use the computer’s camera to detect when stretching exercises are being performed
  3. Employ machine learning and pose estimation to validate proper stretching technique
  4. Track stretching consistency and progress over time, providing encouragement to maintain healthy habits

This wasn’t about another notification tool. It was about closing the gap between knowing a break is good for you and actually taking one that counts. We build solutions to our own problems, and this was one of mine.

A note before the code: this is a working prototype, not a shipped product. The camera, motion detection, and data plumbing all run. The pose-classification and streak logic are deliberately left as stubs, and I flag exactly where and why as we go.

What you’ll build
#

  • A .NET 9 WPF tray app that reminds you to stretch on a configurable interval
  • An OpenCV motion-detection pipeline (grayscale, blur, frame diff, morphology) that decides whether the camera saw movement
  • A TensorFlow.NET pose-estimation hook for validating the actual stretch (the part that’s still a stub)
  • A SQLite layer that records sessions and drives a consistency streak

System Architecture Overview
#

The Stretch Tracker stacks a few technologies into one health-monitoring loop: a WPF front end, an OpenCV vision pipeline, a TensorFlow.NET pose model, and a SQLite store.

graph TD A[User Interaction Layer] --> B[Presentation Layer - WPF] B --> C[Core Logic Layer] C --> D[Computer Vision Module - OpenCvSharp4] C --> E[Machine Learning Module - TensorFlow.NET] C --> F[Persistence Layer - SQLite] D --> G[Frame Processing] E --> H[Pose Estimation] F --> I[Data Tracking] G --> J[Motion Detection Algorithm] H --> K[Stretch Validation] I --> L[User Statistics]

Technical Component Breakdown
#

1. Application Framework: .NET 9.0 WPF
#

Architectural Principles
#

  • MVVM (Model-View-ViewModel) Pattern
  • Dependency Injection
  • Asynchronous Programming

Core Project Configuration
#

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net9.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="2.0.1" />
    <PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" />
    <PackageReference Include="TensorFlow.NET" Version="0.150.0" />
    <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
  </ItemGroup>
</Project>

2. Computer Vision Module: Motion Detection
#

Core Detection Algorithm
#

private async Task<bool> ProcessFrameForMotionAsync(Mat frame)
{
    // Preprocessing pipeline
    using (Mat grayFrame = new Mat())
    {
        // Convert to grayscale for consistent processing
        Cv2.CvtColor(frame, grayFrame, ColorConversionCodes.BGR2GRAY);
        
        // Noise reduction using Gaussian blur
        Cv2.GaussianBlur(grayFrame, grayFrame, new Size(21, 21), 0);

        // Frame difference computation
        using (Mat diff = new Mat())
        {
            Cv2.Absdiff(grayFrame, _previousFrame, diff);
            
            // Binary thresholding to isolate significant movements
            Cv2.Threshold(diff, diff, 30, 255, ThresholdTypes.Binary);
            
            // Morphological operations for noise filtering
            var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(5, 5));
            Cv2.Erode(diff, diff, kernel, iterations: 1);
            Cv2.Dilate(diff, diff, kernel, iterations: 2);

            // Motion quantification
            double motionAmount = Cv2.Sum(diff)[0];
            
            // Adaptive thresholding
            bool isSignificantMotion = motionAmount > _dynamicMotionThreshold;
            
            return isSignificantMotion;
        }
    }
}

Detection Techniques
#

  1. Adaptive Thresholding

    • Dynamically adjusts detection sensitivity
    • Learns background noise levels
    • Minimizes false positives
  2. Multi-Stage Motion Analysis

    • Grayscale conversion for consistent processing
    • Gaussian blur for noise reduction
    • Frame differencing to isolate changes
    • Morphological operations for refined detection

3. Machine Learning Integration: TensorFlow Pose Estimation
#

public class PoseDetectionModel
{
    private TF.Session _poseDetectionSession;
    private const int KeypointCount = 17;

    public async Task<PoseDetectionResult> DetectPose(Mat frame)
    {
        // Convert OpenCV Mat to TensorFlow tensor
        var inputTensor = ConvertMatToTensor(frame);
        
        // Run inference through neural network
        var outputs = _poseDetectionSession.Run(
            new[] { _graph.GetTensorByName("input_tensor") },
            new[] { inputTensor },
            new[] { 
                _graph.GetTensorByName("keypoints_output"),
                _graph.GetTensorByName("confidence_output")
            }
        );

        // Process detected keypoints
        var keypointsRaw = outputs[0];
        var confidenceScores = outputs[1];

        return ProcessPoseKeypoints(keypointsRaw, confidenceScores);
    }

    private PoseDetectionResult ProcessPoseKeypoints(Tensor keypointsTensor, Tensor confidenceTensor)
    {
        var keypoints = new List<Keypoint>();
        
        for (int i = 0; i < KeypointCount; i++)
        {
            var x = keypointsTensor[0, i, 0];
            var y = keypointsTensor[0, i, 1];
            var confidence = confidenceTensor[0, i];

            keypoints.Add(new Keypoint(
                x, y, 
                (KeypointType)i, 
                confidence
            ));
        }

        return new PoseDetectionResult(keypoints);
    }
}

// Supporting data structures
public enum KeypointType 
{
    Nose, Neck, RightShoulder, RightElbow, // ... and so on
}

public record Keypoint(
    float X, 
    float Y, 
    KeypointType Type, 
    float Confidence
);

public record PoseDetectionResult(List<Keypoint> Keypoints)
{
    public bool IsValidStretch() 
    {
        // Complex validation logic
        // Analyze keypoint relationships
        // Determine if current pose represents a stretch
    }
}

4. Persistence Layer: SQLite Database Management
#

public class DatabaseManager
{
    private readonly string _connectionString;

    public void InitializeDatabase()
    {
        using var connection = new SqliteConnection(_connectionString);
        connection.Open();

        // Create stretching sessions table
        using var command = connection.CreateCommand();
        command.CommandText = @"
            CREATE TABLE IF NOT EXISTS StretchSessions (
                Id INTEGER PRIMARY KEY AUTOINCREMENT,
                Date TEXT NOT NULL,
                Completed INTEGER NOT NULL,
                Duration INTEGER NOT NULL
            )";
        command.ExecuteNonQuery();
    }

    public void RecordStretchSession(bool completed, int durationSeconds)
    {
        using var connection = new SqliteConnection(_connectionString);
        connection.Open();

        using var command = connection.CreateCommand();
        command.CommandText = @"
            INSERT INTO StretchSessions (Date, Completed, Duration)
            VALUES (@date, @completed, @duration)";

        command.Parameters.AddWithValue("@date", DateTime.Now.ToString("yyyy-MM-dd"));
        command.Parameters.AddWithValue("@completed", completed ? 1 : 0);
        command.Parameters.AddWithValue("@duration", durationSeconds);

        command.ExecuteNonQuery();
    }

    public int GetCurrentStreak()
    {
        int streak = 0;
        DateTime currentDate = DateTime.Now.Date;

        using var connection = new SqliteConnection(_connectionString);
        connection.Open();

        // Complex streak calculation logic
        // Checks consecutive days of completed stretches
        // ...

        return streak;
    }
}
Heads up: IsValidStretch() and GetCurrentStreak() are stubs in the repo. The keypoint extraction and the SQLite plumbing are real; the pose-classification and streak rules are left open, because “a correct stretch” is per-body-type and needs tuning against real users.

5. Configuration Management
#

public class AppSettings
{
    // Configurable parameters
    public int NotificationIntervalMinutes { get; set; } = 120;
    public int RequiredStretchCount { get; set; } = 5;
    public float PoseDetectionThreshold { get; set; } = 0.7f;

    public void Save()
    {
        // JSON serialization of settings
        var json = JsonSerializer.Serialize(this, 
            new JsonSerializerOptions { WriteIndented = true });
        File.WriteAllText(_settingsPath, json);
    }

    public static AppSettings Load()
    {
        // Robust settings loading with fallback
        try 
        {
            var json = File.ReadAllText(_settingsPath);
            return JsonSerializer.Deserialize<AppSettings>(json) 
                   ?? new AppSettings();
        }
        catch 
        {
            return new AppSettings(); // Default configuration
        }
    }
}

Critical Focus Areas for Improvement
#

1. Motion Detection Refinement
#

  • Adaptive Thresholding: Continuously learn and adjust detection sensitivity
  • Noise Reduction: Improve morphological operation techniques
  • Multi-frame Analysis: Enhance consecutive frame validation

2. Pose Estimation Enhancement
#

  • Keypoint Accuracy: Improve neural network model precision
  • Stretch Classification: Develop more sophisticated stretch type recognition
  • Movement Quality Assessment: Create detailed stretch quality metrics

3. Machine Learning Model Considerations
#

  • Model Selection:

    • Lightweight models for real-time performance
    • High accuracy in pose estimation
    • Low computational overhead
  • Potential Models to Explore:

    1. MoveNet
    2. PoseNet
    3. BlazePose
    4. OpenPose Lite

4. Performance Optimization Strategies
#

  • Asynchronous Processing
  • Tensor Preprocessing Efficiency
  • Minimal Memory Allocation
  • GPU Acceleration Support

Implementation Challenges
#

1. Variability in Human Movement
#

  • Different body types
  • Varying stretching techniques
  • Environmental variations

2. Real-time Processing Constraints
#

  • Maintain 30+ FPS
  • Minimal computational resource usage
  • Consistent detection accuracy

Future Technical Roadmap
#

  1. Enhanced Pose Classification

    • More granular stretch type detection
    • Machine learning model retraining
  2. Cross-Platform Support

    • .NET MAUI for multi-platform deployment
    • Unified codebase
  3. Advanced Analytics

    • Predictive health insights
    • Machine learning-driven recommendations

Worth remembering
#

  • Motion detection is the cheap, reliable layer, but it only proves something moved, not that a stretch happened. On its own it fires false positives for anyone who fidgets at a desk.
  • Real validation needs the pose model, and that’s the unfinished part. IsValidStretch() and the streak logic are stubs precisely because “a correct stretch” is per-body-type and has to be tuned against real users.
  • Running TensorFlow.NET and OpenCV inside a WPF app works on .NET 9, but the 30+ FPS target is what forces every optimization: async frame handling, minimal allocations, and a lighter pose model.

If I picked this back up, the next step is swapping the placeholder pose logic for MoveNet or BlazePose and measuring the false-positive rate against a handful of real desks. That number, not the architecture, decides whether the app earns its place in your tray. Source is on GitHub if you want to take it further.

Related

Simplify Your Workflow: How to Build Custom Commands with the .NET CLI

··11 mins
Our support team used to open the Azure portal every time someone needed a user’s group memberships. Click through Azure AD, find the user, expand groups, copy the list, paste it into a ticket. A dozen times a day. It was slow, and it needed portal access we would rather not hand out widely.

Deploying Ollama with Open WebUI Locally: A Step-by-Step Guide

··8 mins
I run LLMs on my own machine for two reasons: my prompts never leave my hardware, and I can experiment all day without watching an API meter. The setup used to be the hard part. Wrangling Python environments, CUDA drivers, and model weights by hand was enough friction that most people quit before the first token.

Simplify Git Workflows Across Multiple Repositories with Git Submodules and Meta-Repositories

··6 mins
Six repositories, one system, too many clone commands # On a typical microservices project I have five or six repositories open at once: a couple of APIs, a frontend, shared infrastructure scripts, sometimes a docs repo. Onboarding a new developer meant a wiki page full of clone commands, and “which commit of the API works with this frontend?” was answered by asking whoever was online.