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:
- Send timely, customizable reminders to take stretch breaks
- Use the computer’s camera to detect when stretching exercises are being performed
- Employ machine learning and pose estimation to validate proper stretching technique
- 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.
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#
Adaptive Thresholding
- Dynamically adjusts detection sensitivity
- Learns background noise levels
- Minimizes false positives
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;
}
}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:
- MoveNet
- PoseNet
- BlazePose
- 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#
Enhanced Pose Classification
- More granular stretch type detection
- Machine learning model retraining
Cross-Platform Support
- .NET MAUI for multi-platform deployment
- Unified codebase
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.