Showing posts with label CGM. Show all posts
Showing posts with label CGM. Show all posts

Friday, July 4, 2025

How do you extract glucose data from a glucose CGM report using Python?

 In my previous post, I described how to find the graph of glucose vs. time using a blue mask (the curve was in blue). This post is about graphing Glucose data vs. time using the curve from the previous post.

The main result of the previous post resulted in developing a mask’ for the curve as shown here.

This Python script (GlucoseCalibrated.py) that follows is designed to extract numerical glucose data from a graph image by analyzing the blue glucose trace, calibrating it to real-world glucose values, and then saving the results. The important determinations that are to be made are, 1) Correctly rendering the Y-axis of the displayed curve and establishing the Glucose data in mg/dL correctly to the numerical data from the calculation. In associating the correct Glucose values, the measurements on the curve from the report were made using GIMP. The X-axis association with time has to be done as well.

import cv2
import numpy as np
import matplotlib.pyplot as plt
import csv
import os

# --- Configuration ---

IMAGE_PATH = "LibreViewOneDayGraph.jpg"

LOWER_BLUE = np.array([100, 50, 50])
UPPER_BLUE = np.array([140, 255, 255])

# --- CRITICAL CALIBRATION PARAMETERS ---
GLUCOSE_VALUE_1 = 0
PIXEL_Y_1 = 170

GLUCOSE_VALUE_2 = 150
PIXEL_Y_2 = 107
# ----------------------------------------

print(f"Attempting to load image from: {IMAGE_PATH}. "
f"Current working directory: {os.getcwd()}")

# Load image
img = cv2.imread(IMAGE_PATH)

# Check if the image was loaded successfully
if img is None:
print(f"Error: Could not load image from {IMAGE_PATH}. "
"Please check the path and name.")
else:
print(f"Image '{IMAGE_PATH}' loaded successfully. "
f"Dimensions: {img.shape}")
img_height, img_width, _ = img.shape
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# Create mask for the blue color
mask = cv2.inRange(img_hsv, LOWER_BLUE, UPPER_BLUE)
print(f"Mask created. Number of blue pixels detected: "
f"{np.count_nonzero(mask)}")
if np.count_nonzero(mask) == 0:
print("Warning: No blue pixels found in the image with the "
"current HSV range. Check image color or HSV values.")

# Create a copy of the image to draw on (for visualization)
img_with_circles = img.copy()
circle_radius = 2

# List to store x, y coordinates and glucose values for CSV
glucose_trace_data = []
glucose_trace_data.append(
['x_pixel', 'y_pixel_raw', 'glucose_value_mg_dL'])

# --- Calculate the linear transformation parameters (m and b) ---
m_glucose_scale = (GLUCOSE_VALUE_2 - GLUCOSE_VALUE_1) / \
(PIXEL_Y_2 - PIXEL_Y_1)
b_glucose_offset = GLUCOSE_VALUE_1 - \
(m_glucose_scale * PIXEL_Y_1)

print(f"\n--- Calibration Results ---")
print(f"Using calibration points: ({PIXEL_Y_1}px -> "
f"{GLUCOSE_VALUE_1}mg/dL) and ({PIXEL_Y_2}px -> "
f"{GLUCOSE_VALUE_2}mg/dL)")
print(f"Calculated Glucose Scale (m): {m_glucose_scale:.4f}")
print(f"Calculated Glucose Offset (b): {b_glucose_offset:.4f}")
print(f"---------------------------\n")

# Iterate through each x-position to find the glucose trace
print("Starting trace detection loop...")
for x_pos in range(img_width):
column = mask[:, x_pos]
y_hits = np.where(column > 0)[0]

if len(y_hits) > 0:
y_pixel_raw = int(np.median(y_hits))

actual_glucose_value = \
(m_glucose_scale * y_pixel_raw) + b_glucose_offset
actual_glucose_value_rounded = round(actual_glucose_value, 2)

cv2.circle(img_with_circles, (x_pos, y_pixel_raw),
circle_radius, (0, 0, 255), -1)

glucose_trace_data.append(
[x_pos, y_pixel_raw, actual_glucose_value_rounded])
print(f"Trace detection loop finished. Data points collected: "
f"{len(glucose_trace_data) - 1}")

# --- Display the visualized image ---
plt.figure(figsize=(12, 7))
plt.imshow(cv2.cvtColor(img_with_circles, cv2.COLOR_BGR2RGB))
plt.title("Detected Glucose Trace on Original Image "
"(with Calibration Notes)")
plt.axis("off")
plt.show()

# Save the image with circles
output_image_filename = "glucose_trace_detected.jpg"
cv2.imwrite(output_image_filename, img_with_circles)
print(f"Visualized trace image saved to {output_image_filename}")

# --- Save data to CSV file ---
csv_filename = "glucose_trace_data_calibrated_values.csv"
try:
with open(csv_filename, 'w', newline='') as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerows(glucose_trace_data)
print(f"Glucose trace data saved to {csv_filename}")
except IOError:
print(f"Error: Could not write to {csv_filename}. "
"Please check file permissions or if the file is open.")

print("\nScript finished execution.")

Here's a step-by-step summary of its functionality:

  1. Import Libraries: It imports cv2 (OpenCV) for image processing, numpy for numerical operations, matplotlib.pyplot for displaying images, csv for writing data to CSV files, and os for path-related diagnostics.
  2. Configuration:
    • IMAGE_PATH: Specifies the path to your input glucose graph image (e.g., "LibreViewOneDayGraph.jpg"). This is the image the script will analyze.
    • LOWER_BLUE, UPPER_BLUE: These define a range in the HSV color space (Hue, Saturation, Value) that precisely represents the "blue" color of your glucose trace. Any pixels falling within this range are considered part of the trace.
  3. Critical Calibration Parameters:
    • GLUCOSE_VALUE_1, PIXEL_Y_1: These are your first calibration point. GLUCOSE_VALUE_1 is a known glucose level (e.g., 0 mg/dL), and PIXEL_Y_1 is the exact Y-pixel coordinate you manually measured from the image corresponding to that glucose level.
    • GLUCOSE_VALUE_2, PIXEL_Y_2: These are your second calibration point, representing another known glucose level (e.g., 150 mg/dL) and its corresponding Y-pixel coordinate.
    • Purpose: These two points are crucial for establishing a linear relationship between pixel Y-coordinates and actual glucose values.
  4. Image Loading and Initial Checks:
    • The script attempts to load the image specified by IMAGE_PATH.
    • It checks if the image loaded successfully. If not, it prints an error and stops.
    • It converts the image from BGR (Blue, Green, Red - OpenCV's default) to HSV (Hue, Saturation, Value) color space, as HSV is generally better for color-based segmentation.
  5. Color Masking:
    • cv2.inRange(img_hsv, LOWER_BLUE, UPPER_BLUE) creates a binary mask. This mask is a black-and-white image where white pixels represent areas of the original image that fall within the defined blue color range (i.e., your glucose trace).
    • It prints the number of detected blue pixels as a diagnostic.
  6. Calibration Calculation:
    • It calculates the m (slope) and b (y-intercept) values for a linear equation: Glucose_Value = m * raw_y_pixel + b.
    • m_glucose_scale: Represents how many mg/dL each pixel unit corresponds to. It's calculated using the two calibration points. It's typically negative because a higher pixel Y-value (further down the image) means a lower glucose value on the graph.
    • b_glucose_offset: The offset, calculated using one of the calibration points and the derived slope.
    • These values form the core of converting pixel data to meaningful glucose readings.
  7. Glucose Trace Detection Loop:
    • The script iterates through every single vertical column (x_pos) of the image, from left to right.
    • For each column, it looks at the mask to find all y_hits (white pixels, indicating blue color from the trace).
    • np.median(y_hits): If blue pixels are found in a column, it takes the median Y-coordinate of those pixels. Using the median helps to make the detection robust against noise or slight variations in line thickness.
    • Calibration Application: The y_pixel_raw (median Y-coordinate) is then fed into the linear equation (m * y_pixel_raw + b) to get the actual_glucose_value_rounded.
    • Visualization: A red circle is drawn on a copy of the original image (img_with_circles) at the (x_pos, y_pixel_raw) to visually confirm where the trace was detected.
    • Data Storage: The x_pos, y_pixel_raw, and actual_glucose_value_rounded are stored in the glucose_trace_data list.
  8. Output and Saving:
    • Display Image: The img_with_circles (original graph with red dots on the detected trace) is displayed using matplotlib.
    • Save Image: This visualized image is also saved as glucose_trace_detected.jpg.
    • Save CSV: The glucose_trace_data (containing all the detected x, y, and calibrated glucose values) is saved to a CSV file named glucose_trace_data_calibrated_values.csv.

In essence, this script automates the process of "reading" your glucose graph by color detection and translating the visual curve into a precise set of numerical data points.

Here is the result of running this code:

Attempting to load image from: LibreViewOneDayGraph.jpg. Current working directory: C:\Users\hoden\PycharmProjects\GraphtoData

Image 'LibreViewOneDayGraph.jpg' loaded successfully. Dimensions: (244, 1140, 3)

Mask created. Number of blue pixels detected: 5325


--- Calibration Results ---

Using calibration points: (170px -> 0mg/dL) and (107px -> 150mg/dL)

Calculated Glucose Scale (m): -2.3810

Calculated Glucose Offset (b): 404.7619

---------------------------

Starting trace detection loop...

Trace detection loop finished. Data points collected: 1091

Visualized trace image saved to glucose_trace_detected.jpg

Glucose trace data saved to glucose_trace_data_calibrated_values.csv

Script finished execution.

Process finished with exit code 0

The CSV file is written starting at the beginning of the curve to the end and at each point a red dot is placed on the original to unambiguosly verify that the whole curve is covered. 


Monday, August 7, 2023

How do I get Glucose data from the FreeStyle Libre glucose sensor?

 I have been having glucose-related issues for many years. Although diagnostic data indicates I am diabetic, I am not taking any medication for diabetes. However, I was always concerned about its possible effects. While my lipid profiles were under safe 'standard values', I suffered a heart attack in 2022. I decided to look at my glucose values more closely to make adjustments in my lifestyle and diet. The only diagnostic at my disposal was the painful pricking of the finger and measuring with a strip. This is totally unsatisfactory as it can provide only a single data point. I have always wondered how healthcare providers make decisions on this single value measured in a doctor's office once in a couple of months.

Fortunately, I was able to get a Freestyle Libre glucose sensor (Freestyle Libre 14-day). Now, at least I have a tool to look at what I am eating and associate it with glucose levels. I have been having it for the past two days. The display available on the Libre Link app which is available for Android and iPhones is quite nice but I want to look at it more ways.  The Libre website gives access to this data on their website. 

The sensor gets data about interstitial glucose every 15 minutes and stores it on the website. Here is how you can get the data. 

These are screenshots of the steps I took to get the data on the website to my computer. It is a Dell laptop with Windows 64 OS and I also have Microsoft Excel on the computer.

This is Libre View from the Libre website


You need to agree before you proceed.


You need to log in to download data. This means you have purchased the sensor and also installed the Libre link app on your phone (Android or iPhone). Note that you have enabled two-factor authorization.



After being verified you got access to the site.


Now click Download Glucose data at the top-right corner.


Your data appears in your Downloads folder on your computer.


Here is a sample of the downloaded data.


I have clipped off some personal information.

Although I have shown a few columns, there are a lot more useful columns that contain data related to custom test times and notes associated with those events.

Hope this helps. 


Effects of Yoga using Freestyle CGM System- Will be published soon.