Care&Co Foot pressure analysis and visualization SDK
Project description
Fisica SDK - Python Interface
[Readme_ko (한국어 버전)]
This SDK provides a Python interface to communicate with the Fisica scale device for plantar pressure measurement analysis.
🚀 Installation
Requirements
- Python 3.8 or higher
- Operating System: Windows 10/11 or macOS 10.15+
Install via pip
pip install fisica-sdk
Install from source
git clone https://github.com/care-co/fisica_sdk.git
cd fisica_sdk
pip install -e .
Dependencies
The SDK automatically installs the following dependencies:
'numpy>=1.21',
'opencv-python>=4.5',
'requests>=2.25',
'packaging>=21.0',
'Pillow>=8.0',
'pyserial>=3.5',
'bleak>=0.22.3' # For Bluetooth support
# Dependencies for GUI
'PyQt5>=5.15',
🔧 Quick Start
Basic Usage
# examples/basic_use.py
import fisica_sdk as fisica
# Initialize SDK
sdk = fisica.FisicaSDK()
# Scan for devices
devices = sdk.scan_devices()
print("Available devices:", devices)
# Connect to the first device
if devices:
sdk.connect(devices[0])
# Set metadata
sdk.set_metadata(name="John Doe")
# Start measurement for 10 seconds
sdk.start_measurement(duration=10)
# Wait for completion
sdk.wait()
# Analyze results
report = sdk.analyze()
print("Analysis complete:", report)
# Disconnect
sdk.disconnect()
# Wait for completion
sdk.wait()
Real-time Data Monitoring
# examples/realtime_monitoring.py
from fisica_sdk import FisicaSDK, Visualizer, VisualOptions
# Define a function to visualize incoming data
def visualize(data):
image = sdk.render(frame=data, mode=VisualOptions.BLUR, bbox=True, scale=2.0) # Render image from incoming data
vis.update(image) # Update visualizer with new image
# Initialize the SDK
sdk = FisicaSDK()
# Find available devices.
devices = sdk.scan_devices()
if devices:
# Connect to the first device from the list.
sdk.connect(devices[0])
# Register callback function to visualize data
sdk.on_data(visualize)
# Start measurement.
sdk.start_measurement()
# Sleep without blocking the main thread.
sdk.sleep(15)
# Stop measurement.
sdk.stop_measurement()
# Create visualizer instance
vis = Visualizer()
# Launch visualizer with scaling
vis.run(scale=2.0)
Batch Processing
# examples/batch_processing.py
import fisica_sdk as fisica
sdk = fisica.FisicaSDK()
devices = sdk.scan_devices()
if devices:
sdk.connect(devices[0])
# Multiple measurements
for i in range(5):
sdk.set_metadata(id=i, name=f"Test_{i}")
sdk.start_measurement(duration=5)
sdk.wait()
sdk.analyze()
# Get session data
frames = sdk.get_session_frames()
print(f"Session {i}: {len(frames)} frames captured")
# Reset for next measurement
sdk.reset_session()
# Export all reports
reports = sdk.get_all_reports()
sdk.export_report(reports, output="measurement_results.json")
sdk.disconnect()
📱 Device Connection
Serial Connection
The SDK automatically detects Fisica devices connected via USB.
Bluetooth Connection
For Bluetooth(BLE) devices:
- Windows: Ensure Bluetooth is enabled
- macOS: Grant Bluetooth permissions when prompted
📊 Data Analysis
Available Metrics
The SDK provides comprehensive foot pressure analysis:
- Pressure Distribution: Heat maps analysis
- Center of Pressure (COP): Movement tracking
- Foot Size: Length and width measurements
- Weight Distribution: Left/right foot balance
- Temporal Analysis: Pressure changes over time
Visualization Options
# Different rendering modes
modes = ["PIXEL", "BLUR", "BINARY", "BINARY_NONZERO", "BBOX", "CONTOUR"]
for mode in modes:
image = sdk.render(frame, mode=mode, bbox=False, scale=2.0)
# Save or display image
🎯 Examples
Command Line Interface
# examples/cli_measurement.py
import fisica_sdk as fisica
import argparse
def main():
parser = argparse.ArgumentParser(description='Fisica measurement CLI')
parser.add_argument('--duration', type=int, default=10, help='Measurement duration')
parser.add_argument('--output', type=str, default='results.json', help='Output file')
args = parser.parse_args()
sdk = fisica.FisicaSDK()
devices = sdk.scan_devices()
if not devices:
print("No devices found")
return
sdk.connect(devices[0])
sdk.start_measurement(duration=args.duration)
sdk.wait()
report = sdk.analyze()
print(report)
sdk.export_report(output=args.output)
if __name__ == "__main__":
main()
GUI Application
# examples/gui_app.py
import sys
import numpy as np
import cv2
import fisica_sdk as fisica
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QLabel, QTextEdit
from PyQt5.QtCore import QThread, pyqtSignal, Qt
from PyQt5.QtGui import QPixmap, QImage
class MeasurementThread(QThread):
frame_received = pyqtSignal(object)
def __init__(self, sdk):
super().__init__()
self.sdk = sdk
self.sdk.on_data(self.on_frame)
def on_frame(self, frame):
self.frame_received.emit(frame)
def run(self):
self.sdk.start_measurement(duration=10)
self.sdk.wait()
class FisicaGUI(QMainWindow):
def __init__(self):
super().__init__()
self.sdk = fisica.FisicaSDK()
self.devices = []
self.measurement_thread = None
self.init_ui()
def init_ui(self):
self.setWindowTitle('Fisica SDK GUI')
self.setGeometry(100, 100, 800, 600)
# Central widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# Status label
self.status_label = QLabel('Ready')
layout.addWidget(self.status_label)
# Buttons
self.scan_btn = QPushButton('Scan Devices')
self.scan_btn.clicked.connect(self.scan_devices)
layout.addWidget(self.scan_btn)
self.connect_btn = QPushButton('Connect')
self.connect_btn.clicked.connect(self.connect_device)
self.connect_btn.setEnabled(False)
layout.addWidget(self.connect_btn)
self.start_btn = QPushButton('Start Measurement')
self.start_btn.clicked.connect(self.start_measurement)
self.start_btn.setEnabled(False)
layout.addWidget(self.start_btn)
# Device list
self.device_text = QTextEdit()
self.device_text.setMaximumHeight(100)
layout.addWidget(self.device_text)
# Visualization area
self.image_label = QLabel('Pressure visualization will appear here')
self.image_label.setMinimumHeight(400)
self.image_label.setStyleSheet("border: 1px solid black;")
self.image_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.image_label)
def scan_devices(self):
self.status_label.setText('Scanning devices...')
self.devices = self.sdk.scan_devices()
if self.devices:
device_info = '\n'.join([f"{i}: [{d['type']}] {d['name']} - {d['id']}"
for i, d in enumerate(self.devices)])
self.device_text.setText(device_info)
self.connect_btn.setEnabled(True)
self.status_label.setText(f'Found {len(self.devices)} devices')
else:
self.device_text.setText('No devices found')
self.status_label.setText('No devices found')
def connect_device(self):
if self.devices:
try:
self.sdk.connect(self.devices[0]) # Connect to first device
self.status_label.setText(f'Connected to {self.devices[0]["name"]}')
self.start_btn.setEnabled(True)
self.connect_btn.setEnabled(False)
except Exception as e:
self.status_label.setText(f'Connection failed: {e}')
def start_measurement(self):
if self.measurement_thread and self.measurement_thread.isRunning():
return
self.status_label.setText('Starting measurement...')
self.start_btn.setEnabled(False)
self.measurement_thread = MeasurementThread(self.sdk)
self.measurement_thread.frame_received.connect(self.update_visualization)
self.measurement_thread.finished.connect(self.measurement_finished)
self.measurement_thread.start()
def set_image(self, image: np.ndarray):
"""Improved image display method based on the provided code"""
try:
if len(image.shape) == 2:
# Grayscale image
h, w = image.shape
qimage = QImage(image.data, w, h, w, QImage.Format_Grayscale8)
elif len(image.shape) == 3:
if image.shape[2] == 3:
# RGB/BGR image
h, w, ch = image.shape
# Convert BGR to RGB if needed
if image.dtype == np.uint8:
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
else:
image_rgb = image
qimage = QImage(image_rgb.data, w, h, ch * w, QImage.Format_RGB888)
elif image.shape[2] == 4:
# RGBA image
image = cv2.cvtColor(image, cv2.COLOR_BGRA2RGBA)
h, w, ch = image.shape
qimage = QImage(image.data, w, h, ch * w, QImage.Format_RGBA8888)
else:
raise ValueError("Unsupported image format.")
else:
raise ValueError("Unsupported image format.")
pixmap = QPixmap.fromImage(qimage)
# Scale to fit the label while maintaining aspect ratio
scaled_pixmap = pixmap.scaled(self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.image_label.setPixmap(scaled_pixmap)
except Exception as e:
print(f"Image display error: {e}")
self.status_label.setText(f'Image display error: {e}')
def update_visualization(self, frame):
try:
# Render the frame
image_array = self.sdk.render(frame, mode="BLUR", scale=2.0)
# Ensure the image is in the correct format
if image_array.dtype != np.uint8:
# Normalize to 0-255 range if needed
if image_array.max() <= 1.0:
image_array = (image_array * 255).astype(np.uint8)
else:
image_array = np.clip(image_array, 0, 255).astype(np.uint8)
# Use the improved image display method
self.set_image(image_array)
except Exception as e:
print(f"Visualization error: {e}")
self.status_label.setText(f'Visualization error: {e}')
def measurement_finished(self):
self.status_label.setText('Measurement completed')
self.start_btn.setEnabled(True)
# Analyze results
try:
report = self.sdk.analyze()
print("Analysis completed:", report)
except Exception as e:
print(f"Analysis error: {e}")
def main():
app = QApplication(sys.argv)
window = FisicaGUI()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
🔧 Advanced Configuration
Async Operations
# Non-blocking measurements
sdk.start_measurement(duration=30)
# Do other work while measuring
for i in range(10):
sdk.sleep(2, after=lambda: print(f"Checkpoint {i}"))
# Wait for completion
sdk.wait()
🐛 Troubleshooting
Common Issues
-
Device not found
devices = sdk.scan_devices() if not devices: print("No devices found. Check connection and permissions.")
-
Bluetooth permissions (macOS)
- Grant Bluetooth access when prompted
Debugging
Enable debug logging:
import logging
sdk = fisica.FisicaSDK(debug=logging.DEBUG)
# could be replaced logging.DEBUG to 10
📚 API Reference
set_metadata(**kwargs)
Description:
Sets metadata for the current measurement session.
Parameters:
id(Any, optional): Identifier for the user/session.name(str, optional): Identifier for the user/session.
Returns:
None
scan_devices()
Description:
Scans for available scale devices via serial or Bluetooth connection.
Returns wired device information immediately if a wired device is connected,
otherwise searches for powered-on scale devices via Bluetooth.
Parameters:
None
Returns:
List[Dict]: List of device dictionaries, each containing connection details.
connect(device: Dict)
Description:
Establishes connection to the selected device returned by scan_devices().
Parameters:
device(Dict): A dictionary containing device ID and type.
Returns:
None
disconnect()
Description:
Disconnects from the connected scale device. Automatically waits for measurement operations to complete before closing the connection.
Parameters: None
Returns:
None
start_measurement(duration: Optional[float])
Description:
Begins capturing measurement data. If duration is given, it stops automatically after the duration (in seconds).
If duration is not given, the measurement will not stop automatically. This is not the intended operation,
so it is not recommended to run the measurement for too long without a duration.
Set a duration, or call the SDK's built-in sleep() function or the sleep() function from the time package to measure for a certain period,
and then call stop_measurement() to end the measurement. The SDK's built-in sleep does not affect the main thread execution.
Alternatively, you can develop a GUI-based application to automatically end the measurement by duration or set up a trigger for stop_measurement().
Parameters:
duration(float, optional): Duration of the measurement in seconds.
Returns:
None
stop_measurement()
Description:
Stops the current measurement.
Parameters:
None
Returns:
None
on_data(callback)
Description:
Registers a callback function that will be called in real-time as measurement frames are received.
Parameters:
callback(callable): You can register a function that takes a single argument of typeMeasurementFrame.
Returns:
None
sleep(seconds: float, after: Optional[callable])
Description:
Asynchronously sleeps for the given time without blocking the main thread. An optional callback can be executed afterward.
Parameters:
seconds(float): Number of seconds to sleep.after(callable, optional): Function to call after sleeping.
Returns:
None
wait()
Description:
Waits until all asynchronous operations (e.g., start_measurement(), stop_measurement(), sleep(), disconnect()) are completed.
This ensures that all SDK operations that run in the background are fully finished before proceeding to the next step, such as data processing or visualization.
⚠️ It blocks the main thread while waiting.
Parameters:
None
Returns:
None
analyze()
Description:
Analyzes the current session's captured frames and computes relevant metrics such as foot size and pressure distribution.
This function returns values only immediately after a measurement, and may not work as expected afterward.
To get the most recent report of the current session, it is recommended to call get_current_report().
Please make sure to call analyze() before loading or exporting reports.
Parameters:
None
Returns:
Dict: Dictionary containing metadata, sensor grid, weight, etc.
get_session_frames()
Description:
Returns the frame data of the current session in serialized format.
Parameters:
None
Returns:
List[Dict]: Serialized frames from the current session.
get_report()
Description:
Returns the most recently analyzed session report.
Parameters:
None
Returns:
Dict: Dictionary containing metadata, sensor grid, and computed results.
get_all_reports()
Description:
Retrieves analysis reports from all completed sessions.
Parameters:
None
Returns:
List[Dict]: A list of session reports.
export_report(report: Optional[Union['SessionReport', list]], output: Optional[str])
Description:
Exports all reports to a JSON file at the specified output path, filename, or directory.
Creates missing directories automatically and appends .json extension if not provided
If target is an existing directory, generates filename using predefined naming rules.
e.g., "output/sample/result" becomes "output/sample/result.json" if result folder doesn't exist.
If no argument is provided for report exports information from the most recently analyzed SessionReport.
Calling this method without having performed analyze() may result in an error.
Parameters:
report(SessionReport, optional): Data obtained throughget_all_reports()orget_reports(). If not provided, outputs based on the most recent SessionReport.output(str, optional): File path or directory for export.
Returns:
None
reset_session()
Description:
Clears the current session (frames and metadata).
Parameters:
None
Returns:
None
reset_reports()
Description:
Clears all stored analysis reports from memory.
Parameters:
None
Returns:
None
render(frame: Optional[MeasurementFrame], grid_data: Optional[np.ndarray], mode: Optional[str], scale: Optional[float])
Description:
Renders a sensor grid (from a frame or raw grid data) into an image using the specified visualization mode and scale.
frame is the variable passed as an argument when a callback function is registered to on_data.
Parameters:
frame(MeasurementFrame, optional): A dataclass instance containing the sensor matrix.grid_data(np.ndarray, optional): A 2D NumPy array representing the sensor grid.mode(str, optional): Visualization style (e.g., "BLUR"). For more details, refer to the VisualOption section below.default:VisualOptions.BLURscale(float, optional): Scale factor for the image size.default:1.0
Returns:
np.ndarray: Rendered image as a NumPy array.
🖼️ VisualOptions
The VisualOptions class defines available rendering modes for visualizing sensor data using FisicaSDK.render().
Rendering modes:
- PIXEL: Pixel heatmap rendering
- BLUR: Smooth heatmap rendering
- BINARY: Binary grid rendering
- BINARY_NONZERO: Binary grid rendering excluding zero values
- BBOX: Bounding box rendering based on BLUR and Principal Component Analysis (PCA)
- CONTOUR: Pixel-based contour rendering
- ALL: Returns all rendering results in list format
run(scale: Optional[float])
Description:
Launches the GUI viewer for real-time visualizations with the specified scale.
Parameters:
scale(float, optional): Scale factor for the GUI window.default:1.0
Returns:
None
update(image: np.ndarray)
Description:
Updates the GUI viewer with the given rendered image.
Parameters:
image(np.ndarray): The rendered image to display.
Returns: None
📚 API Reference - Additional Methods
set_zero()
Description:
Calibrates the zero point of the device by setting the current reading as the zero reference point for weight measurements. This is equivalent to the tare function on digital scales and should be used when the scale platform is empty to establish a proper baseline.
Parameters:
None
Returns:
None
Usage Example:
# Connect to device first
devices = sdk.scan_devices()
if devices:
sdk.connect(devices[0])
# Ensure platform is empty, then set zero point
sdk.set_zero() # Tare the scale
# Now ready for accurate measurements
sdk.start_measurement(duration=10)
# Wait for finish measurement
sdk.wait()
Notes:
- Device connection is required before calling this method
- Call
connect()first to establish device connection - Use this function when the scale platform is empty
- This establishes the baseline for all subsequent weight measurements
set_scale(value)
Description:
Sets the scale calibration parameter of the device. This function adjusts the internal calibration value used by the device's weight measurement system to ensure accurate readings.
Parameters:
value(int): Scale calibration parameter, must be within the range -32768 to 32767
Returns:
None
Usage Example:
# Connect to device first
devices = sdk.scan_devices()
if devices:
sdk.connect(devices[0])
# Set zero point first
sdk.set_zero()
# Set scale calibration parameter
sdk.set_scale(12600) # Set calibration value to 12600
# Start measurement with calibrated settings
sdk.start_measurement(duration=10)
sdk.wait()
Notes:
- Device connection is required before calling this method
- Call
connect()first to establish device connection - Value must be an integer within the range -32768 to 32767
- Use after
set_zero()for optimal calibration - Consult device documentation for appropriate calibration values
🔧 Device Calibration Workflow
For optimal measurement accuracy, follow this calibration sequence:
# examples/calibration.py
import fisica_sdk as fisica
# Initialize and connect
sdk = fisica.FisicaSDK()
devices = sdk.scan_devices()
if devices:
sdk.connect(devices[0])
# Step 1: Set zero point (empty platform)
print("Please ensure platform is empty...")
input("Press Enter when ready...")
sdk.set_zero()
print("Zero point calibrated.")
# Step 2: Set scale calibration parameter
# Use appropriate calibration value for your device
# (consult device documentation for recommended values)
calibration_value = 12600 # Example value within -32768 to 32767 range
sdk.set_scale(calibration_value)
print(f"Scale calibration set to: {calibration_value}")
# Step 3: Verify calibration with known reference weight
print("Place a known reference weight on platform...")
reference_weight = float(input("Enter reference weight (kg): "))
# Take a measurement to verify accuracy
sdk.start_measurement(duration=10)
sdk.wait()
report = sdk.analyze()
measured_weight = report.weight
print(f"Reference: {reference_weight}kg, Measured: {measured_weight}kg")
print(f"Accuracy: {abs(measured_weight - reference_weight):.3f}kg difference")
# Step 4: Ready for accurate measurements
print("Calibration complete. Ready for measurements.")
sdk.disconnect()
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
📞 Support
- Issues: GitHub Issues
- Email: carencoinc@carenco.kr
- Website: https://carenco.kr
🔄 Changelog
v1.0.0 (Latest)
- Initial release
- Serial and Bluetooth device support
- Real-time data visualization
- Comprehensive pressure analysis
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fisica-1.0.0.tar.gz.
File metadata
- Download URL: fisica-1.0.0.tar.gz
- Upload date:
- Size: 40.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5b76026ef65be679662d0c04b3752555b9fe71c2332172e5afa7ee134c3811c
|
|
| MD5 |
e3264a4e583dd03c2b7e1cd3473d3a9f
|
|
| BLAKE2b-256 |
d3c7d3312825d16982c81309ca208bcccbf46da6926b053318d867e71ce24b4f
|
File details
Details for the file fisica-1.0.0-py3-none-any.whl.
File metadata
- Download URL: fisica-1.0.0-py3-none-any.whl
- Upload date:
- Size: 43.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e1b2b3353ad8b3ead3aef09a9d25144d75be8a939ae32821be1ea94a5c3e444
|
|
| MD5 |
0a92a0369381acfc17e3f67ecb52e504
|
|
| BLAKE2b-256 |
c6431c1b621333105ad5592ba802879a40ac793f068126963dc45b59512f20cd
|