Project Info
Inspiration
Our inspiration behind MARC stems from wanting to close the gaps between imagination and creation. Today, turning a digital idea into something physical can take hours of calibration and setup, killing spontaneous creativity. High costs also make creative robotics inaccessible. For example, many precision robot arms cost over $10,000, putting them far beyond the reach of students, artists, and hobbyists. And perhaps most importantly, over 285 million people with visual impairments are excluded from visual creation tools; while AI-generated art continues to evolve, those who canβt see it are still unable to feel what it creates. Our project aims to change that by making the process of turning ideas into tangible, touchable art faster, cheaper, and more inclusive.
What it does
The system takes natural language text prompts and optional image references to generate original artwork, then automatically translates these digital creations into precise physical drawings executed by a robotic arm. Through advanced coordinate transformation and real-time motion control, MARC bridges the gap between AI-generated images and tangible art, delivering sub-2mm positioning accuracy as it brings digital designs to life on paper.
How we built it
The system architecture consists of four integrated stages. First, AI generation uses a Stable Diffusion XL pipeline with SD 1.5 fallback to create artwork from text prompts. Next, smart vectorization converts PNG outputs to SVG format using Portrace and SVGpathtools while fitting the artwork into 15 cm x 17.3 cm page dimensions. The third stage handles precise coordinate transformation by detecting paper corners via camera vision to build a homography matrix that maps pixels to millimeters, then applies 2D affine transformation formulas to convert page coordinates into the robot's base frame. Finally, robotics execution is achieved through a 6-DOF SO-ARM 101 with degrees-mode control, a custom 3-link planar IK solver, and real-time joint angle streaming for smooth, accurate drawing movements.
Challenges we ran into
We encountered three major technical challenges during development. First, inaccurate motors caused servo backlash, USB packet loss, and position error accumulation. We solved this by implementing a closed-loop inverse kinematics controller with real-time error correction, achieving sub-2mm accuracy. Second, integrating three frameworks with incompatible APIs (LeRobot using degrees, RustyPot using 0-1, and PySerial using bytes) cost us over 20 hours in rewrites. We built a thin abstraction layer under 200 lines, prototyped all three approaches in 6 hours, and selected LeRobot for its community support and degrees-native interface. Finally, marker physics issues like nib compression and inconsistent ink flow limited us to one marker shape. We designed a universal 3D-printed spring-loaded adapter with 5-18mm diameter compatibility that maintains consistent pressure while absorbing vibrations.
Accomplishments we're proud of
We're incredibly proud of building a system that accurately translates drawing coordinates into precise robotic movements, achieving pinpoint accuracy within 2mm. Figuring out how to convert real-world positions into instructions the robot could understand was a major breakthrough, requiring us to use camera vision to map the physical paper space to the robot's coordinate system. Beyond the technical work, we're proud of handling the pressure and rapid problem-solving of our first hackathon, transforming an ambitious idea into a working system that takes a text prompt and produces a physical drawing on paper in minutes.
What we learned
This project taught us invaluable lessons about collaboration and resourcefulness. We discovered the power of open-source communities, finding extensive documentation and ready-to-use tools like LeRobot that accelerated our development and saved us countless hours of building from scratch. We learned the importance of parallel experimentation, testing multiple approaches simultaneously rather than betting everything on a single solution, which proved crucial when we had to evaluate three different frameworks in just six hours. Most importantly, we realized that constant communication is essential in a fast-paced hackathon environment. Keeping everyone aligned on goals, progress, and challenges minimized confusion and prevented wasted effort from misunderstandings. Working under pressure taught us to balance ambitious technical goals with practical time constraints, and that success comes from both individual problem-solving and effective teamwork.
What's next
Looking ahead, we plan to expand MARC's capabilities in several exciting directions. First, we'll add multi-color support to enable more vibrant and complex artwork. We're committed to open-source publishing, making our code and designs available for others to build upon and learn from. To make MARC more accessible, we'll develop a remote control system with a queue feature, allowing multiple users to submit drawing requests from anywhere. We also envision an adaptive learning system that improves drawing quality over time by learning from past movements and corrections. Finally, we want to transform MARC into a multi-modal fabrication platform that goes beyond drawing, potentially supporting different tools and creative outputs to bridge the gap between digital creativity and physical making.
π€ LeRobot aims to provide models, datasets, and tools for real-world robotics in PyTorch. The goal is to lower the barrier to entry to robotics so that everyone can contribute and benefit from sharing datasets and pretrained models.
π€ LeRobot contains state-of-the-art approaches that have been shown to transfer to the real-world with a focus on imitation learning and reinforcement learning.
π€ LeRobot already provides a set of pretrained models, datasets with human collected demonstrations, and simulation environments to get started without assembling a robot. In the coming weeks, the plan is to add more and more support for real-world robotics on the most affordable and capable robots out there.
π€ LeRobot hosts pretrained models and datasets on this Hugging Face community page: huggingface.co/lerobot
Examples of pretrained models on simulation environments
Installation
LeRobot works with Python 3.10+ and PyTorch 2.2+.
Environment Setup
Create a virtual environment with Python 3.10 and activate it, e.g. with miniforge:
conda create -y -n lerobot python=3.10
conda activate lerobot
When using conda, install ffmpeg in your environment:
conda install ffmpeg -c conda-forge
NOTE: This usually installs
ffmpeg 7.Xfor your platform compiled with thelibsvtav1encoder. Iflibsvtav1is not supported (check supported encoders withffmpeg -encoders), you can:
- [On any platform] Explicitly install
ffmpeg 7.Xusing:conda install ffmpeg=7.1.1 -c conda-forge
- [On Linux only] Install ffmpeg build dependencies and compile ffmpeg from source with libsvtav1, and make sure you use the corresponding ffmpeg binary to your install with
which ffmpeg.
Install LeRobot π€
From Source
First, clone the repository and navigate into the directory:
git clone https://github.com/huggingface/lerobot.git
cd lerobot
Then, install the library in editable mode. This is useful if you plan to contribute to the code.
pip install -e .
NOTE: If you encounter build errors, you may need to install additional dependencies (
cmake,build-essential, andffmpeg libs). On Linux, run:sudo apt-get install cmake build-essential python3-dev pkg-config libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev. For other systems, see: Compiling PyAV
For simulations, π€ LeRobot comes with gymnasium environments that can be installed as extras:
For instance, to install π€ LeRobot with aloha and pusht, use:
pip install -e ".[aloha, pusht]"
Installation from PyPI
Core Library: Install the base package with:
pip install lerobot
This installs only the default dependencies.
Extra Features: To install additional functionality, use one of the following:
pip install 'lerobot[all]' # All available features
pip install 'lerobot[aloha,pusht]' # Specific features (Aloha & Pusht)
pip install 'lerobot[feetech]' # Feetech motor support
Replace [...] with your desired features.
Available Tags: For a full list of optional dependencies, see: https://pypi.org/project/lerobot/
Weights & Biases
To use Weights and Biases for experiment tracking, log in with
wandb login
(note: you will also need to enable WandB in the configuration. See below.)
Visualize datasets
Check out example 1 that illustrates how to use our dataset class which automatically downloads data from the Hugging Face hub.
You can also locally visualize episodes from a dataset on the hub by executing our script from the command line:
lerobot-dataset-viz \
--repo-id lerobot/pusht \
--episode-index 0
or from a dataset in a local folder with the root option and the --mode local (in the following case the dataset will be searched for in ./my_local_data_dir/lerobot/pusht)
lerobot-dataset-viz \
--repo-id lerobot/pusht \
--root ./my_local_data_dir \
--mode local \
--episode-index 0
It will open rerun.io and display the camera streams, robot states and actions, like this:
Our script can also visualize datasets stored on a distant server. See lerobot-dataset-viz --help for more instructions.
The LeRobotDataset format
A dataset in LeRobotDataset format is very simple to use. It can be loaded from a repository on the Hugging Face hub or a local folder simply with e.g. dataset = LeRobotDataset("lerobot/aloha_static_coffee") and can be indexed into like any Hugging Face and PyTorch dataset. For instance dataset[0] will retrieve a single temporal frame from the dataset containing observation(s) and an action as PyTorch tensors ready to be fed to a model.
A specificity of LeRobotDataset is that, rather than retrieving a single frame by its index, we can retrieve several frames based on their temporal relationship with the indexed frame, by setting delta_timestamps to a list of relative times with respect to the indexed frame. For example, with delta_timestamps = {"observation.image": [-1, -0.5, -0.2, 0]} one can retrieve, for a given index, 4 frames: 3 "previous" frames 1 second, 0.5 seconds, and 0.2 seconds before the indexed frame, and the indexed frame itself (corresponding to the 0 entry). See example 1_load_lerobot_dataset.py for more details on delta_timestamps.
Under the hood, the LeRobotDataset format makes use of several ways to serialize data which can be useful to understand if you plan to work more closely with this format. We tried to make a flexible yet simple dataset format that would cover most type of features and specificities present in reinforcement learning and robotics, in simulation and in real-world, with a focus on cameras and robot states but easily extended to other types of sensory inputs as long as they can be represented by a tensor.
Here are the important details and internal structure organization of a typical LeRobotDataset instantiated with dataset = LeRobotDataset("lerobot/aloha_static_coffee"). The exact features will change from dataset to dataset but not the main aspects:
dataset attributes:
β hf_dataset: a Hugging Face dataset (backed by Arrow/parquet). Typical features example:
β β observation.images.cam_high (VideoFrame):
β β VideoFrame = {'path': path to a mp4 video, 'timestamp' (float32): timestamp in the video}
β β observation.state (list of float32): position of an arm joints (for instance)
β ... (more observations)
β β action (list of float32): goal position of an arm joints (for instance)
β β episode_index (int64): index of the episode for this sample
β β frame_index (int64): index of the frame for this sample in the episode ; starts at 0 for each episode
β β timestamp (float32): timestamp in the episode
β β next.done (bool): indicates the end of an episode ; True for the last frame in each episode
β β index (int64): general index in the whole dataset
β meta: a LeRobotDatasetMetadata object containing:
β β info: a dictionary of metadata on the dataset
β β β codebase_version (str): this is to keep track of the codebase version the dataset was created with
β β β fps (int): frame per second the dataset is recorded/synchronized to
β β β features (dict): all features contained in the dataset with their shapes and types
β β β total_episodes (int): total number of episodes in the dataset
β β β total_frames (int): total number of frames in the dataset
β β β robot_type (str): robot type used for recording
β β β data_path (str): formattable string for the parquet files
β β β video_path (str): formattable string for the video files (if using videos)
β β episodes: a DataFrame containing episode metadata with columns:
β β β episode_index (int): index of the episode
β β β tasks (list): list of tasks for this episode
β β β length (int): number of frames in this episode
β β β dataset_from_index (int): start index of this episode in the dataset
β β β dataset_to_index (int): end index of this episode in the dataset
β β stats: a dictionary of statistics (max, mean, min, std) for each feature in the dataset, for instance
β β β observation.images.front_cam: {'max': tensor with same number of dimensions (e.g. `(c, 1, 1)` for images, `(c,)` for states), etc.}
β β β ...
β β tasks: a DataFrame containing task information with task names as index and task_index as values
β root (Path): local directory where the dataset is stored
β image_transforms (Callable): optional image transformations to apply to visual modalities
β delta_timestamps (dict): optional delta timestamps for temporal queries
A LeRobotDataset is serialised using several widespread file formats for each of its parts, namely:
- hf_dataset stored using Hugging Face datasets library serialization to parquet
- videos are stored in mp4 format to save space
- metadata are stored in plain json/jsonl files
Dataset can be uploaded/downloaded from the HuggingFace hub seamlessly. To work on a local dataset, you can specify its location with the root argument if it's not in the default ~/.cache/huggingface/lerobot location.
Reproduce state-of-the-art (SOTA)
We provide some pretrained policies on our hub page that can achieve state-of-the-art performances. You can reproduce their training by loading the config from their run. Simply running:
lerobot-train --config_path=lerobot/diffusion_pusht
reproduces SOTA results for Diffusion Policy on the PushT task.
Contribute
If you would like to contribute to π€ LeRobot, please check out our contribution guide.
Add a pretrained policy
Once you have trained a policy you may upload it to the Hugging Face hub using a hub id that looks like ${hf_user}/${repo_name} (e.g. lerobot/diffusion_pusht).
You first need to find the checkpoint folder located inside your experiment directory (e.g. outputs/train/2024-05-05/20-21-12_aloha_act_default/checkpoints/002500). Within that there is a pretrained_model directory which should contain:
config.json: A serialized version of the policy configuration (following the policy's dataclass config).model.safetensors: A set oftorch.nn.Moduleparameters, saved in Hugging Face Safetensors format.train_config.json: A consolidated configuration containing all parameters used for training. The policy configuration should matchconfig.jsonexactly. This is useful for anyone who wants to evaluate your policy or for reproducibility.
To upload these to the hub, run the following:
huggingface-cli upload ${hf_user}/${repo_name} path/to/pretrained_model
See lerobot_eval.py for an example of how other people may use your policy.
Acknowledgment
- The LeRobot team π€ for building SmolVLA Paper, Blog.
- Thanks to Tony Zhao, Zipeng Fu and colleagues for open sourcing ACT policy, ALOHA environments and datasets. Ours are adapted from ALOHA and Mobile ALOHA.
- Thanks to Cheng Chi, Zhenjia Xu and colleagues for open sourcing Diffusion policy, Pusht environment and datasets, as well as UMI datasets. Ours are adapted from Diffusion Policy and UMI Gripper.
- Thanks to Nicklas Hansen, Yunhai Feng and colleagues for open sourcing TDMPC policy, Simxarm environments and datasets. Ours are adapted from TDMPC and FOWM.
- Thanks to Antonio Loquercio and Ashish Kumar for their early support.
- Thanks to Seungjae (Jay) Lee, Mahi Shafiullah and colleagues for open sourcing VQ-BeT policy and helping us adapt the codebase to our repository. The policy is adapted from VQ-BeT repo.
Citation
If you want, you can cite this work with:
@misc{cadene2024lerobot,
author = {Cadene, Remi and Alibert, Simon and Soare, Alexander and Gallouedec, Quentin and Zouitine, Adil and Palma, Steven and Kooijmans, Pepijn and Aractingi, Michel and Shukor, Mustafa and Aubakirova, Dana and Russi, Martino and Capuano, Francesco and Pascal, Caroline and Choghari, Jade and Moss, Jess and Wolf, Thomas},
title = {LeRobot: State-of-the-art Machine Learning for Real-World Robotics in Pytorch},
howpublished = "\url{https://github.com/huggingface/lerobot}",
year = {2024}
}
Star History
Analysis
View
Metric
- 53
- 1
Figures cover GitHub contributors during the hackathon window. A co-authored commit counts in full for each author, so per-member totals add up to more than the whole-team figures.
Technology
- Hugging FaceIn code
- PythonIn code
- PyTorchIn code
3 of 3 appear in the indexed code.
AI coding agents
- GitHub CopilotCommits
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
3.7 MB
Source files
405
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
yyardi/cal-hacks-marc
516 files Β· 20.4 MB Β· @ 1d6d16c
Structure
Application logic
269 files Β· 52%Domain rules, services and shared utilities.
+5 moreData & schema
1 file Β· 0%Schema definitions, migrations and data access.
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here β open the file browser to check anything the diagram implies.
Languages
- Python96%
- Markdown2%
- YAML1%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
pyproject.toml
pypi Β· 88- accelerate
- av
- cmake
- datasets
- deepdiff
- diffusers
- draccus
- einops
- gymnasium
- huggingface-hub[hf-transfer,cli]
- imageio[ffmpeg]
- jsonlines
- opencv-python-headless
- packaging
- pynput
- pyserial
- rerun-sdk
- setuptools
- +70 more
examples/marc/requirements.txt
pypi Β· 10- accelerate
- cairosvg
- diffusers
- numpy
- opencv-contrib-python
- pillow
- pupil-apriltags
- svgpathtools
- transformers
- vpype
Declared in the repositoryβs manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
Feature verification
2D affine/rigid transform from page coordinates to robot base frame (Procrustes fit)Verified
Applies 2D affine transformation formulas to convert page coordinates into the robot's base frame
Claimed on Devposthigh confidenceexamples/marc/calib/page_to_robot.py:91β fit_rigid_transform performs an SVD-based Procrustes fit between jogged page points and measured robot XY coordinatesexamples/marc/executor/so100_driver.py:307β _page_to_pose applies the stored page-to-robot homography matrix to convert stroke points into robot XY targets
6-DOF SO-ARM 101 control with degrees-mode via LeRobotVerified
Robotics execution is achieved through a 6-DOF SO-ARM 101 with degrees-mode control
Claimed on Devposthigh confidenceexamples/marc/executor/so100_driver.py:125β SO100FollowerConfig is instantiated with use_degrees=True and used to build the SO100Follower connection
Camera-based paper corner detection to build a homography (pixels to millimeters)Verified
Coordinate transformation detects paper corners via camera vision to build a homography matrix that maps pixels to millimeters
Claimed on Devposthigh confidenceexamples/marc/calib/detect_tags.py:1β detect_fiducials locates four high-contrast squares or AprilTags in an overhead photo to find page cornersexamples/marc/calib/estimate_homography.py:101β compute_page_homography converts detected corners into a homography matrix mapping camera pixels to page millimetres
Fit artwork to fixed page dimensions (e.g. 15cm x 17.3cm workspace)Verified
Vectorization fits artwork into a defined page size (README specifies 173mm x 150mm safe workspace)
Claimed on readmehigh confidenceexamples/marc/planner/make_plan.py:88β Function centers and scales sampled strokes to fit within page bounds, computing scale_factor and inside_ratioexamples/marc/constants.py:57β SAFE_WORKSPACE_SIZE_MM and margin constants define the drawable page area used as defaults throughout the pipeline
Marker pick-and-return slots (single marker workflow)Verified
System supports docking/picking and returning a marker at defined slots during a drawing pass
Claimed on readmehigh confidenceexamples/marc/executor/so100_driver.py:258β _cmd_pick_marker and _cmd_return_marker move to configured MarkerSlot pick/return coordinates and engage/disengage pen stateexamples/marc/executor/driver_api.py:37β MarkerSlot and ExecutorConfig.marker_slots define the data model for marker pick/return locations
One-shot prompt-to-robot pipeline commandVerified
A working system that takes a text prompt and produces a physical drawing on paper in minutes via a single command
Claimed on Devposthigh confidenceexamples/marc/run_prompt_to_robot.py:209β main() chains image generation, vectorization, simplification, plan building, bounds validation, and execute_plan into a single CLI entrypoint
PNG to SVG vectorization via PotraceVerified
Smart vectorization converts PNG outputs to SVG format using Potrace while fitting the artwork into page dimensions
Claimed on Devposthigh confidenceexamples/marc/vectorize/potrace_wrap.py:80β trace_bitmap_to_svg thresholds the PNG with OpenCV and shells out to the potrace binary, passing page_size via --width/--heightexamples/marc/run_prompt_to_robot.py:260β Pipeline calls trace_bitmap_to_svg with page_size after image generation
Real-time joint angle streaming for smooth drawing motionVerified
Real-time joint angle streaming for smooth, accurate drawing movements
Claimed on Devposthigh confidenceexamples/marc/executor/so100_driver.py:336β _stream_pose subdivides long moves into interpolated steps and streams IK solutions to the robot at a fixed command rate (self.dt)
SO101 URDF/mesh fetch and verification helperVerified
Helper to fetch official SO101 URDF and meshes with hash verification
Claimed on readmemedium confidenceexamples/marc/fetch_so101_urdf.py:1β Script downloads the SO101 URDF/mesh payload and is referenced in README as verifying a SHA256 hash
Text prompt to image generation (Stable Diffusion XL with SD 1.5 fallback)Verified
AI generation uses a Stable Diffusion XL pipeline with SD 1.5 fallback to create artwork from text prompts
Claimed on Devposthigh confidenceexamples/marc/vectorize/generate.py:65β load_text2img_pipeline attempts StableDiffusionXLPipeline first and falls back to StableDiffusionPipeline (SD 1.5) on failureexamples/marc/run_prompt_to_robot.py:236β main() calls generate_image() with the prompt to produce the PNG as the first pipeline stage
Vision-based drawing correction pass (compares camera capture to target image, redraws residual)Verified
Optional correction pass using camera vision to compare drawn output against target and redraw missed regions
Claimed on readmehigh confidenceexamples/marc/executor/so100_driver.py:477β perform_correction warps the captured camera image via camera_to_page_h, diffs it against the target raster, and converts residual mask regions into new draw strokes/commandsexamples/marc/run_prompt_to_robot.py:197β --correct flag and --target-image/--camera-homography options wire the correction pass into the CLI
Closed-loop IK controller with real-time error correction (sub-2mm accuracy claim)Code-supported
Implementing a closed-loop inverse kinematics controller with real-time error correction, achieving sub-2mm accuracy
Claimed on Devpostlow confidenceexamples/marc/executor/so100_driver.py:387β _solve_and_send retries IK with relaxed orientation weights and Z offsets, and _stream_pose subdivides failed segments, forming a retry/correction loop; however no numeric '2mm' accuracy figure, benchmark, or test exists anywhere in the repo to substantiate the specific accuracy claim
Custom 3-link planar IK solverCode-supported
A custom 3-link planar IK solver drives joint angles for drawing
Claimed on Devpostmedium confidencesrc/lerobot/model/kinematics.py:79β RobotKinematics.inverse_kinematics wraps the general-purpose 'placo' IK library for the full URDF chain rather than a bespoke 3-link planar solver; MARC code (so100_driver.py) calls this generic solver for planar drawing, so the pipeline uses IK but not a documented custom 3-link implementation
Multi-color palette quantization for artworkCode-supported
Multi-color support planned/available to map artwork colours to discrete marker colours
Claimed on readmemedium confidenceexamples/marc/planner/color_quant.py:10β MARKER_PALETTE and quantize_paths group sampled paths into a fixed marker colour palette, but the Devpost 'What's next' section frames multi-colour support as future work, so this may be a partial/experimental implementation rather than the finished feature described as current
SVG simplification / path optimizationCode-supported
Smart vectorization pipeline includes simplifying curves before planning (SVGpathtools mentioned in Built With)
Claimed on Devpostmedium confidenceexamples/marc/vectorize/simplify_svg.py:1β File imports svgpathtools-style utilities and exposes simplify_svg_file, used in the pipeline to reduce tiny segments before planningexamples/marc/run_prompt_to_robot.py:275β Pipeline calls simplify_svg_file on the traced SVG unless --no-simplify is set
Accessibility for visually impaired users (tangible art creation)Claimed only
Making touchable/tangible art accessible to the 285 million people with visual impairments who cannot see AI-generated art
Claimed on Devposthigh confidenceThin abstraction layer unifying LeRobot/RustyPot/PySerial motor APIs (<200 lines)Claimed only
Built a thin abstraction layer under 200 lines to unify LeRobot (degrees), RustyPot (0-1), and PySerial (bytes) APIs
Claimed on Devpostmedium confidenceUniversal 3D-printed spring-loaded marker adapter (5-18mm diameter)Blocked
Designed a universal 3D-printed spring-loaded adapter with 5-18mm diameter compatibility for the marker
Claimed on Devposthigh confidence
An AI agent derived these features from the projectβs Devpost page and readme, then searched the code for each one. Verified features are backed by cited code; claimed-only features had no supporting code, which is not by itself proof a feature is missing.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.