Memento is a web platform for collaborative biological image annotation. It lets research teams upload microscopy images, draw regions of interest, classify samples, assign labels, and leave comments — all through a browser, with no programming required for day-to-day use.
- Browser-based annotation
- Everything runs in a standard web browser — no software to install on the annotator's machine. Open the URL, log in, and start annotating.
- Multi-layer image viewer
- Stack multiple image channels (e.g. DAPI, GFP, RFP) as independently controllable layers. Adjust brightness, contrast, and transparency per channel in real time. WebGL-accelerated rendering keeps navigation smooth even at high zoom levels.
- Large image support
- Pyramidal/tiled image format allows whole-slide and other very high-resolution images to be served efficiently — only the tiles needed for the current view are loaded, so the viewer stays responsive regardless of the original image size.
- Drawing tools
- Draw regions of interest directly on the image using point, line, polygon, and rectangle tools. ROI data is stored and exportable for downstream analysis.
- Flexible project structure
- Organise work into projects, categories, and annotations. Assign participants at any level of granularity — the whole project, a category, or a single annotation.
- Classification and labelling
- Define custom classification buttons per category for fast, consistent sample scoring. Attach free-form labels to individual annotations and leave layer-level comments for collaborators.
- Simply image share mechanism
- Permissions for annotate different projects are easy enough to manage, but you can also simply share particular annotations to persons outside your organization via temporary URLs, automatically handled by the server.
- Python client for automation
- A lightweight Python client (
memento_cl.py) lets you bulk-import image collections, export results as flat tables, and automate project management — without touching the web interface.
- A lightweight Python client (
- No installation burden on the user. Annotators only need a browser. The computational work — image processing, tiling, storage — happens entirely on the server, which can be any conventional machine with enough disk space. There is nothing to install, configure, or update on the annotator's side.
- Scales to large images without specialised hardware. The pyramidal tiling pipeline means even very large images can be served and navigated comfortably. The server itself does not need to hold the full image in memory at query time; it simply reads and serves the relevant tile.
- Straightforward deployment. Three very generic Docker containers, one
.envfile, and an Nginx reverse proxy block are all that is needed to run a production instance.
- What is Memento?
- Installation
- How data is organized
- Getting started with Python
- The MementoClient — complete reference
- Importing a project from a folder of images
- Exporting your data for analysis
- Appendix: the Memento API at a glance
Memento has two components that work together:
This is what most users interact with. Open it in any modern browser and you can:
- Log in with your username and password.
- Browse your projects — see all experiments you have access to, organized into categories and annotations.
- View and annotate images in an interactive multi-layer viewer. The viewer supports very large pyramidal images (whole-slide), multiple fluorescence channels stacked as separate layers, adjustable brightness/contrast/transparency per channel, and WebGL-accelerated rendering for smooth navigation.
- Draw regions of interest (ROIs) directly on the image using point, line, polygon, and rectangle tools.
- Classify samples using predefined classification buttons set up by the project owner.
- Assign labels to mark your outcome (e.g. "positive", "artifact").
- Leave comments on any layer for collaborators to read.
- Submit your annotation when done, signalling to the project owner that the sample is complete.
The Flask service sits behind the Django frontend and manages all data storage. It is also the service the Python client (memento_cl.py) talks to directly — which lets you automate bulk imports, exports, and project management from a script.
You do not need to know anything about the API to use the web interface. It is documented in the Appendix for advanced users who want to build their own integrations.
Memento runs as two Docker containers (Django frontend + Flask backend) plus a MariaDB database. You need Docker and Docker Compose installed on your server.
git clone <repository-url>
cd mementoCopy the provided template and fill in your values. This file holds all secrets and configuration — never commit it to version control.
cp setup/.env.example setup/.env# setup/.env
# Django
MEMENTO_DJANGO_SECRET_KEY=<a long random string — generate with: python3 -c "import secrets; print(secrets.token_hex(32))">
MEMENTO_DJANGO_DEBUG=0
MEMENTO_DJANGO_INTERNAL_PORT=8000
# Flask
MEMENTO_FLASK_AUTH_TOKEN_KEY=<another long random string>
MEMENTO_FLASK_WHITE_LISTED_TOKEN=<token used internally by Django to call Flask — generate the same way>
MEMENTO_FLASK_INTERNAL_PORT=5000
# File storage — absolute paths on the host machine (must be the same directory)
MEMENTO_FLASK_IMAGES_ROOT=/data/memento/images
MEMENTO_DJANGO_IMAGES_ROOT=/data/memento/imagesmkdir -p /data/memento/images
mkdir -p /data/memento/dbUse whatever paths you set for MEMENTO_FLASK_IMAGES_ROOT and MEMENTO_DB_DATA_ROOT in your .env.
Memento runs three containers. The full configuration is in setup/docker-compose.yml:
version: '3.7'
services:
maria_db:
image: mariadb:10.11
restart: always
container_name: memento_db
env_file:
- ./.env
environment:
MYSQL_ROOT_PASSWORD: ${MEMENTO_DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${MEMENTO_DB_NAME}
MYSQL_USER: ${MEMENTO_DB_USER}
MYSQL_PASSWORD: ${MEMENTO_DB_PASSWORD}
volumes:
- ${MEMENTO_DB_DATA_ROOT}:/var/lib/mysql
networks:
- labnet
memento_flask:
image: "memento_flask"
build: ./memento/flask
restart: 'always'
container_name: memento_flask
env_file:
- ./.env
ports:
- ${MEMENTO_FLASK_INTERNAL_PORT}:${MEMENTO_FLASK_INTERNAL_PORT}
networks:
- labnet
command: gunicorn --workers 3 --timeout 3600 --bind 0.0.0.0:${MEMENTO_FLASK_INTERNAL_PORT} wsgi:app
volumes:
- ${MEMENTO_FLASK_IMAGES_ROOT}:/opt/memento/images
depends_on:
- maria_db
memento_django:
image: "memento_django"
build: ./memento/django
restart: 'always'
container_name: memento_django
env_file:
- ./.env
ports:
- ${MEMENTO_DJANGO_INTERNAL_PORT}:${MEMENTO_DJANGO_INTERNAL_PORT}
networks:
- labnet
command: gunicorn --workers 3 --timeout 3600 --chdir ./mementosite --bind 0.0.0.0:${MEMENTO_DJANGO_INTERNAL_PORT} mementosite.wsgi:application
volumes:
- ${MEMENTO_DJANGO_IMAGES_ROOT}:/opt/memento/images
depends_on:
- memento_flask
networks:
labnet:
driver: bridgeMariaDB note: Memento uses MariaDB as its primary database (managed by the Flask service). The
maria_dbcontainer above is the simplest way to get it running. If you already have a MariaDB or MySQL server on your network, you can remove themaria_dbservice and point the Flask application directly at your existing server instead — update the database connection string in the Flask configuration accordingly.
cd setup
docker-compose up -d --buildThis builds and starts all three containers (memento_db → memento_flask → memento_django) in the correct dependency order.
Once the containers are running, create your system administrator account through the Django web interface at http://<your-server>:<MEMENTO_DJANGO_INTERNAL_PORT>/memento/ or via the Python client (see Getting started with Python).
In production, place an Nginx reverse proxy in front of both services to handle HTTPS termination. Expose only the Django port to the outside world; the Flask port should remain internal.
Add the following locations to your Nginx server {} block (the full template is in setup/nginx.txt). Replace <flask_port> and <django_port> with the values you set in .env.
# Flask API — internal proxy
location /memento-api {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 3600;
proxy_send_timeout 3600;
proxy_read_timeout 3600;
client_max_body_size 2048M;
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:<flask_port>/memento;
}
# Django static files — served directly from disk
location /memento/static {
root /opt/memento/django/mementosite;
}
# Django frontend
location /memento {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 3600;
proxy_send_timeout 3600;
proxy_read_timeout 3600;
client_max_body_size 2048M;
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:<django_port>;
}The long timeouts (3600s) are intentional — large image uploads can take several minutes to process server-side.
Understanding this hierarchy is the key to using Memento correctly.
Project (e.g. "Mouse kidney study 2026")
└── Category (e.g. "Control group", "Treatment A")
└── Annotation (one sample / one field of view)
└── Layer group (a named set of channels for that sample)
└── Layer (one image channel, e.g. DAPI, GFP, …)
| Level | What it represents | Typical name |
|---|---|---|
| Project | The whole experiment | "Kidney_2026" |
| Category | A biological group | "WT", "KO", "Day3" |
| Annotation | One sample / slide / well | "mouse_01", "well_A1" |
| Layer group | Channels or time-points of one sample | "T0", "channels" |
| Layer | One image file inside a group | "DAPI.tif", "GFP.tif" |
Additional concepts:
- Image: the actual image file stored on the server. Images are shared across the project — you upload an image once and can reuse it in multiple annotations.
- Label: a short keyword you can attach to an annotation to mark the outcome (e.g.
"positive","exclude"). - Classification: a predefined list of options for a category, visible as buttons in the annotation editor.
- Comment: a free-text note attached to a layer, visible to all participants.
pip install requests
The memento_cl.py file must be in the same folder as your script (or on your Python path).
from memento_cl import MementoClient
# Replace with your server address, username, and password
client = MementoClient("http://your-memento-server/memento/")
result = client.login("alice", "my_password")
if result == -1:
print("Login failed — check your username and password")
quit()
print("Logged in successfully")
# Get your numeric user ID (needed when creating things)
user_id = client.get_user_id("alice")
print("My user ID is:", user_id)Note: Your password is never sent in plain text — it is hashed automatically by the client before being transmitted.
All methods return -1 (or None) on failure. Always check the return value before continuing.
Logs in and stores the session token. Must be called before any other method.
- Returns
0on success,-1on failure.
client.login("alice", "my_password")Returns the numeric ID of a user by their username.
- Returns
-1if the user does not exist.
user_id = client.get_user_id("alice")Returns the numeric ID of a project by its name. Returns -1 if not found.
Creates a new project and returns its ID.
settings: extra configuration string — pass''for the default.
Returns the project ID if a project with this name already exists, otherwise creates it. Safe to call repeatedly without creating duplicates — ideal for import scripts.
Permanently deletes a project and everything inside it (categories, annotations, images, layers, comments). This cannot be undone.
project_id = client.get_or_create_project(user_id, "Kidney_2026")Returns the ID of a category by name within a project. Returns -1 if not found.
Creates a new category inside a project and returns its ID.
Returns the category ID if it already exists, otherwise creates it.
Deletes a category and all annotations inside it.
wt_id = client.get_or_create_category(user_id, project_id, "WT")
ko_id = client.get_or_create_category(user_id, project_id, "KO")Images are uploaded once per project and can be referenced by multiple annotations.
Uploads an image file and returns its ID.
| Parameter | What to put |
|---|---|
filepath |
Full path to the file on your computer, e.g. "/data/img/DAPI.tif" |
im_format |
See table below |
name |
A unique name within the project |
im_type |
'' for standard images, 'T' for very large tiled images, 'E' for an external URL |
url |
Only needed when im_type='E'; otherwise pass '' |
Image format codes:
| Code | Description |
|---|---|
1 |
RGB JPEG, resized to max 2048 × 2048 px |
2 |
RGBA PNG, resized to max 2048 × 2048 px |
3 |
RGBA PNG, full original resolution |
4 |
Large image: RGB JPEG tiles (pyramidal) |
5 |
Large image: RGBA PNG tiles (pyramidal) |
For most fluorescence images, use 2 (RGBA PNG, rescaled). For very large whole-slide images, use 4 or 5.
Returns the image ID if an image with this name is already in the project, otherwise uploads it. Prevents uploading the same image twice.
image_id = client.get_or_upload_image(
user_id, project_id,
filepath="/data/mouse01/DAPI.tif",
im_format=2,
name="mouse01_DAPI"
)An annotation represents one sample. When you create an annotation you also create its first layer at the same time.
new_annotation(user_id, project_id, category_id, image_id, name, layer_name, layer_settings, layer_sequence, parent_id, is_group_layer)
Creates an annotation together with its first layer. Returns (annotation_id, layer_id).
| Parameter | Typical value |
|---|---|
image_id |
Image ID, or 0 if the first layer is a group (folder) layer |
layer_settings |
Display settings string — pass '' for defaults |
layer_sequence |
Display order number (start at 1) |
parent_id |
ID of the parent group layer, or 0 |
is_group_layer |
True if this layer is a folder grouping sub-layers |
Returns (annotation_id, -1) if the annotation already exists, otherwise creates it.
Returns the annotation ID by name. Returns -1 if not found.
Deletes an annotation and all its layers and comments.
A layer is one image channel (or drawing canvas) inside an annotation.
new_image_layer(user_id, project_id, category_id, annotation_id, image_id, name, layer_settings, layer_sequence, parent_id)
Adds a new image layer to an existing annotation. Returns the layer ID.
- Use
layer_sequence=-1to automatically append after the last existing layer.
get_or_create_layer(user_id, project_id, category_id, annotation_id, image_id, name, layer_settings='', layer_sequence=-1, parent_id=0)
Returns the layer ID if a layer with this name already exists, otherwise creates it.
edit_layer(user_id, project_id, category_id, annotation_id, layer_id, image_id, name, layer_settings, layer_sequence, parent_id)
Updates the properties of an existing layer.
Returns a layer ID by name. Returns -1 if not found.
Labels are short keywords you attach to annotations (e.g. "positive", "artifact").
Creates a new label in the project. Returns the label ID.
Returns the label ID by name. Returns -1 if not found.
Returns a list of label names currently attached to an annotation.
Replaces all labels on an annotation with the given list of label names.
labels_list: e.g.["positive", "good_quality"]- All label names must already exist in the project.
Deletes a label from the project (removes it from all annotations too).
# Create labels once
client.new_label(user_id, project_id, "positive")
client.new_label(user_id, project_id, "negative")
# Assign to an annotation
client.set_annotation_labels(user_id, project_id, annotation_id, ["positive"])Classifications are structured dropdown or button choices visible to annotators in the browser interface. Unlike labels (which you assign from a script), classifications are typically set up once by the project owner and then selected by annotators in the viewer.
Creates a classification. ctype is the type code (use 'M' for a standard multiple-choice classification). data holds the option values.
Returns a list of classification names linked to a category.
Links the named classifications to a category, replacing any previous assignments.
Deletes a classification.
You can invite other Memento users to participate in your project or in specific categories/annotations.
Grants access to another user.
- If
annotation_idis provided → access to that annotation only. - If only
category_id→ access to that category. - Otherwise → access to the whole project.
colleague_id = client.get_user_id("bob")
client.add_participant(user_id, project_id, 0, 0, colleague_id) # project-wideReturns a short summary dictionary with counts of participants, annotations, submitted annotations, and shared annotations.
summary = client.project_summary(project_id)
print(summary)
# {'total_participants': 3, 'total_annotations': 120,
# 'total_annotations_submitted': 85, 'total_annotations_shared': 12}Returns a flat list of records — one row per annotation — with columns:
project, category, classification, annotation, image, status, label, image_uri.
This is the main export function. Load it into a spreadsheet or pandas DataFrame for analysis.
import pandas as pd
rows = client.project_data(project_id)
df = pd.DataFrame(rows)
df.to_csv("results.csv", index=False)Returns all drawn regions of interest (ROIs) for the project, with columns:
project, category, annotation, layer, data.
The data field contains the ROI geometry in JSON format (Fabric.js objects).
Returns all comments across the project, with columns:
project, category, annotation, layer, content.
This is the most common task: you have a folder of images on your computer and you want to create a Memento project from them.
MyExperiment/ ← becomes the project name
WT/ ← becomes a category
mouse_01/ ← becomes an annotation
channels/ ← becomes a layer group
DAPI.tif
GFP.tif
mouse_02/
channels/
DAPI.tif
GFP.tif
KO/ ← another category
mouse_03/
channels/
DAPI.tif
GFP.tif
Each image file becomes one layer. Files inside the same sub-subfolder share the same annotation (they are channels of the same sample).
Save this as my_import.py in the same folder as memento_cl.py. Then run:
python3 my_import.py MyExperiment
import sys
import os
from memento_cl import MementoClient
# ── Configuration ─────────────────────────────────────────────────────────────
MEMENTO_URL = "http://your-memento-server/memento/"
USERNAME = "alice"
PASSWORD = "my_password"
IMAGE_FORMAT = 2 # RGBA PNG, resized to max 2048×2048
# ──────────────────────────────────────────────────────────────────────────────
client = MementoClient(MEMENTO_URL)
if client.login(USERNAME, PASSWORD) == -1:
print("Login failed")
quit()
user_id = client.get_user_id(USERNAME)
project_name = sys.argv[1] # folder name = project name
project_dir = project_name # path to the top folder
# --- Create the project -------------------------------------------------------
project_id = client.get_or_create_project(user_id, project_name)
if project_id == -1:
print("Could not create project:", project_name)
quit()
print(f"Project '{project_name}' → id {project_id}")
# --- Walk the folder structure ------------------------------------------------
for category_name in sorted(os.listdir(project_dir)):
category_path = os.path.join(project_dir, category_name)
if not os.path.isdir(category_path):
continue
category_id = client.get_or_create_category(user_id, project_id, category_name)
if category_id == -1:
print("Could not create category:", category_name)
client.delete_project(user_id, project_id)
quit()
print(f" Category '{category_name}' → id {category_id}")
for annotation_name in sorted(os.listdir(category_path)):
annotation_path = os.path.join(category_path, annotation_name)
if not os.path.isdir(annotation_path):
continue
annotation_id = -1
layer_sequence = 1
for group_name in sorted(os.listdir(annotation_path)):
group_path = os.path.join(annotation_path, group_name)
if not os.path.isdir(group_path):
continue
group_layer_id = -1
image_files = sorted(os.listdir(group_path))
for i, filename in enumerate(image_files):
filepath = os.path.join(group_path, filename)
image_name = f"{category_name}_{annotation_name}_{group_name}_{filename}"
# Upload the image (skips if already uploaded)
image_id = client.get_or_upload_image(
user_id, project_id, filepath, IMAGE_FORMAT, image_name)
if image_id == -1:
print("Could not upload image:", filepath)
client.delete_project(user_id, project_id)
quit()
print(f" Image '{image_name}' → id {image_id}")
if annotation_id == -1:
# First image: create the annotation + a group layer to hold
# all channels, then add the first image layer inside it.
annotation_id, group_layer_id = client.get_or_create_annotation(
user_id, project_id, category_id,
image_id=0, # 0 because the first layer is a group
name=annotation_name,
layer_name=group_name,
layer_sequence=layer_sequence + 3,
parent_id=0,
is_group_layer=True,
)
if annotation_id == -1:
print("Could not create annotation:", annotation_name)
client.delete_project(user_id, project_id)
quit()
print(f" Annotation '{annotation_name}' → id {annotation_id}")
layer_id = client.get_or_create_layer(
user_id, project_id, category_id, annotation_id,
image_id, name=filename,
layer_sequence=layer_sequence,
parent_id=group_layer_id,
)
if layer_id == -1:
print("Could not create layer:", filename)
client.delete_project(user_id, project_id)
quit()
layer_sequence += 1
print("Import complete.")If each annotation has just a single image (no channel grouping):
MyExperiment/
WT/
mouse_01.tif
mouse_02.tif
KO/
mouse_03.tif
import sys, os
from memento_cl import MementoClient
client = MementoClient("http://your-memento-server/memento/")
client.login("alice", "my_password")
user_id = client.get_user_id("alice")
project_name = sys.argv[1]
project_id = client.get_or_create_project(user_id, project_name)
for category_name in sorted(os.listdir(project_name)):
category_path = os.path.join(project_name, category_name)
if not os.path.isdir(category_path):
continue
category_id = client.get_or_create_category(user_id, project_id, category_name)
for filename in sorted(os.listdir(category_path)):
filepath = os.path.join(category_path, filename)
if not os.path.isfile(filepath):
continue
image_name = f"{category_name}_{filename}"
annotation_name = os.path.splitext(filename)[0] # strip file extension
image_id = client.get_or_upload_image(user_id, project_id, filepath, 2, image_name)
annotation_id, layer_id = client.get_or_create_annotation(
user_id, project_id, category_id, image_id,
name=annotation_name, layer_name="image",
)
print(f" {category_name} / {annotation_name} → annotation {annotation_id}")
print("Done.")After annotators have finished their work in the browser, you can download everything with a few lines of Python.
from memento_cl import MementoClient
import pandas as pd
client = MementoClient("http://your-memento-server/memento/")
client.login("alice", "my_password")
user_id = client.get_user_id("alice")
project_id = client.get_project_id("Kidney_2026")
# High-level summary
summary = client.project_summary(project_id)
print("Total annotations:", summary["total_annotations"])
print("Submitted: ", summary["total_annotations_submitted"])
# Full flat table — one row per annotation
rows = client.project_data(project_id)
df = pd.DataFrame(rows)
# columns: project, category, classification, annotation,
# image, status, label, image_uri
df.to_csv("kidney_2026_results.csv", index=False)
# All drawn ROIs
rois = client.project_rois(project_id)
df_rois = pd.DataFrame(rois)
# columns: project, category, annotation, layer, data
df_rois.to_csv("kidney_2026_rois.csv", index=False)
# All comments
comments = client.project_comments(project_id)
df_comments = pd.DataFrame(comments)
df_comments.to_csv("kidney_2026_comments.csv", index=False)The status column in the main table has two possible values:
"N"— not yet reviewed"S"— submitted (annotator has finished this sample)
The MementoClient communicates with a REST API. You do not need to use the API directly, but this overview is useful if you want to build custom integrations.
All requests require an authentication token obtained at login, passed as the x-access-token header.
| Resource | Endpoint prefix | Notes |
|---|---|---|
| Login | POST /login |
Returns a JWT token valid for 24 h |
| Users | /users |
CRUD + lookup by username |
| Permissions | /permissions |
Grant/revoke access per user/type/id |
| Projects | /projects |
CRUD |
| Categories | /categories |
CRUD + list by project |
| Classifications | /classifications |
CRUD + link to categories |
| Labels | /labels |
CRUD + list by project |
| Images | /images |
CRUD + file upload endpoint |
| Annotations | /annotations |
CRUD + query by project/category/image |
| Layers | /layers |
CRUD + query by annotation/image/project |
| Comments | /comments |
CRUD + query by layer/project |
| Utilities | /utilities/project_summary, /project_data, /project_rois, /project_comments |
Read-only aggregated views |
| Action | Method | Success code |
|---|---|---|
| Read one item | GET /<resource>/<id> |
200 |
| Read list | GET /<resource> |
200 |
| Create | POST /<resource> |
201 |
| Update | PUT /<resource>/<id> |
201 |
| Delete | DELETE /<resource>/<id> |
204 |
| Value | Meaning |
|---|---|
'' (empty) |
Standard flat image (JPEG or PNG file) |
'T' |
Tiled/pyramidal image for very large files |
'E' |
External image — only a URL is stored, no file is uploaded |
| Code | Grants access to |
|---|---|
sysadm |
Full system administration |
proadm |
Administer a specific project |
propar |
Participate in a specific project |
catpar |
Participate in a specific category |
annpar |
Participate in a specific annotation |
provie |
View a project (read-only) |
catvie |
View a category (read-only) |
annvie |
View an annotation (read-only) |