VMAP — visual positioning
VMAP finds where a camera is inside a space you've mapped. You record a short walkthrough video with a phone, VMAP builds a map from it, and from then on any photo taken in that space returns the camera's position and orientation (6-DoF pose) in about 2–3 seconds of processing (plus network time), served by your own dedicated instance.
Typical uses: indoor navigation, robots and kiosks that need to know where they are, and placing AR content at a fixed spot.
How it works
- Record a walkthrough video of the space (up to 5 minutes).
- Create a map and upload the video, then start the build. The map is built on GPU (about 5–10 minutes for a 5-minute video, plus any wait for a free GPU while the status is
queued). - Your localization instance downloads the finished map automatically, within about a minute.
- Your app sends a photo to the instance and gets back the camera pose.
Map management (steps 1–2) uses vmap-api, signed with your AWS credentials, the same way as niuty's other APIs. Localization (step 4) goes to your instance with an API key you create.
1. Pricing
VMAP uses the same niuty credit balance as everything else (1 credit = 1 US cent). Top up on Pricing.
| Item | Price |
|---|---|
| Localization instance | 700 credits per 6-day cycle (≈ $35/month). The first cycle is charged when you create the instance, then every 6 days in advance |
| Map build, successful | 500 credits ($5) |
| Map build, failed because of the video | 100 credits ($1): unreadable file, over the limits, too few usable frames, frames that can't be matched |
| Map build, failed on our side | free |
| Map storage | included |
Failed builds cost only $1.
- A build holds 500 credits when it starts; the final charge is settled when it ends.
- Deleting a map while its build is still queued is free; while it's running it's charged like a failed build ($1).
- Deleting an instance stops future charges; the current cycle isn't refunded.
- If your balance can't cover an instance's next cycle, the instance is removed automatically. Your maps are kept (see Data handling), and a new instance downloads them again.
2. Limits
| Limit | Value |
|---|---|
| Walkthrough video | up to 5 minutes (300 s); MP4, MOV or WebM |
| Photo archive (alternative to video) | up to 600 images, each up to 25 MB, as one ZIP |
| Upload size | up to 2 GB |
| Maps per account | 20 (failed maps don't count) |
| Instances per account | 4 |
| API keys per account | 10 active |
| Localization request | one image (or a video ≤ 10 s), up to 4 MB |
| Builds at a time | 1 per account |
A video longer than 5 minutes is rejected at the start of the build, before any GPU work, and counts as a failed build ($1).
3. Getting started
You need: a niuty account with credit (see Docs → Payment and credit), and AWS credentials for that same AWS account on the machine you work from. All vmap-api calls are signed with SigV4, service execute-api, region eu-north-1. The simplest tool for one-off calls is awscurl:
bash
pip install awscurlStep 1 — Create a localization instance
bash
awscurl --service execute-api --region eu-north-1 -X POST https://vmap-api.niuty.com/v1/instancesjson
{"id": "inst_01M4…", "status": "provisioning", "url": "https://<id>.vmap.niuty.com",
"region": "EU", "cycle_credits": 700, "next_charge_at": "…"}It's ready ("status": "active") after about 3 minutes; check with awscurl … https://vmap-api.niuty.com/v1/instances/<id>. You can build maps right away; the instance picks them up when it's ready.
Step 2 — Create an API key for your app
bash
awscurl --service execute-api --region eu-north-1 -X POST \
-d '{"label": "kiosk app"}' https://vmap-api.niuty.com/v1/api-keysThe response contains "key": "vmap_…". It's shown only once; store it in your app's secret configuration. List keys with GET /v1/api-keys (shows a hint, never the key) and revoke one with DELETE /v1/api-keys/<id>. Revoked keys stop working on your instances within about a minute.
Step 3 — Build a map and localize (Python)
This script creates a map, uploads your video, builds it, waits, and localizes one photo. It has been run end to end against the live service.
bash
pip install boto3 requests
export VMAP_INSTANCE_URL=https://<id>.vmap.niuty.com
export VMAP_API_KEY=vmap_…
python vmap_quickstart.py walkthrough.mp4 photo.jpgpython
"""VMAP quickstart: create a map from a walkthrough video, build it, then localize a photo."""
import json
import os
import sys
import time
import boto3
import requests
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
API = "https://vmap-api.niuty.com"
INSTANCE_URL = os.environ["VMAP_INSTANCE_URL"]
API_KEY = os.environ["VMAP_API_KEY"]
credentials = boto3.Session().get_credentials()
def signed(method, path, json_body=None):
"""Call vmap-api, signed with your AWS credentials (SigV4, execute-api, eu-north-1)."""
body = json.dumps(json_body) if json_body is not None else None
headers = {"Content-Type": "application/json"} if body else {}
request = AWSRequest(method=method, url=API + path, data=body, headers=headers)
SigV4Auth(credentials, "execute-api", "eu-north-1").add_auth(request)
response = requests.request(method, API + path, data=body, headers=dict(request.headers), timeout=60)
if response.status_code >= 400:
raise SystemExit(f"{method} {path} -> {response.status_code}: {response.text}")
return response.json() if response.content else None
video, photo = sys.argv[1], sys.argv[2]
# 1. Create the map and get a one-time upload form.
created = signed("POST", "/v1/maps", {
"name": f"office-{int(time.time())}",
"source_kind": "video",
"size_bytes": os.path.getsize(video),
"content_type": "video/mp4",
})
map_id, upload = created["map"]["id"], created["upload"]
# 2. Upload: every form field first, then the file as the last field, named "file".
with open(video, "rb") as f:
requests.post(upload["url"], data=upload["fields"], files={"file": f}, timeout=3600).raise_for_status()
# 3. Start the build (holds 500 credits; settled when the build ends).
signed("POST", f"/v1/maps/{map_id}/build")
# 4. Wait: queued -> running -> ready | failed.
while (m := signed("GET", f"/v1/maps/{map_id}"))["status"] not in ("ready", "failed"):
print(m["status"], m.get("stage") or "")
time.sleep(15)
if m["status"] == "failed":
raise SystemExit(f"build failed: {m['failure_reason']}")
# 5. Localize on your instance. Reuse one connection for repeated queries.
# A new map reaches the instance within about a minute; until then it answers 404 or 409.
ready_at = time.time()
session = requests.Session()
session.headers["Authorization"] = f"Bearer {API_KEY}"
while True:
with open(photo, "rb") as f:
r = session.post(f"{INSTANCE_URL}/v1/maps/{map_id}/localize", files={"image": f}, timeout=60)
if r.status_code in (404, 409) and time.time() - ready_at < 120:
time.sleep(10) # the instance hasn't picked up / finished downloading the new map yet
continue
r.raise_for_status()
break
result = r.json()
print(result["status"], result.get("pose"), "inliers:", result["inliers"])A new map reaches your instance within about a minute after the build is ready; until then, localize answers 409 map_not_ready or 404 not_found.
Reuse connections. Each localization takes about 2–3 seconds on the instance, plus network time (about 1 second from Southeast Asia to the EU). Opening a new HTTPS connection per request adds roughly half a second more, so keep one connection open (requests.Session(), HTTP keep-alive) for repeated queries.
4. Recording a good walkthrough
- Walk slowly and steadily through the whole area, keeping the camera at the height your app will use. Consecutive frames should overlap a lot; avoid fast turns.
- Good, even lighting. Avoid pointing at blank walls, floors or windows for long.
- Cover the space from the directions your users will face; revisit key spots from a few angles.
- Use the main (1×) camera and don't zoom while recording. (The ultra-wide camera only works if you send
intrinsicswhen localizing.) Portrait or landscape both work. - Point at things with visible detail. Blank walls, floors, ceilings, large windows and mirrors can't be matched. Avoid moving people or objects filling the view.
- Record under lighting similar to when your app will be used; strong day/night differences reduce success. Rebuild the map after large changes (furniture moved, renovation).
- Up to 5 minutes. For larger spaces, build several maps (e.g. one per floor or room).
- For real-world scale, place one or two printed markers before recording (next section), or give
camera_height_m: hold the phone at a steady height (within about 10%) and keep the floor visible in at least a third of the video.
Tips for the photos you localize: sharp (no motion blur), pointed at areas with visible detail, long side about 1600 px, and send intrinsics if your app has them (e.g. from ARKit/ARCore). A photo of a blank wall correctly returns not_localized.
5. Scale and your own coordinates
Metric scale (optional). Print the scale marker, tape it flat on a wall, and keep it fully visible in several frames of your walkthrough. Send the printed black square's measured size in metres as marker_size_m when creating the map (e.g. 0.160).
Approximate scale without a marker. Give camera_height_m, the height of the phone above the floor while recording. VMAP finds the floor in the map and scales it from that (scale_status: approximate, scale_method: camera_height; typically within a few percent, reported in scale_uncertainty_pct). If the floor isn't visible enough, or the height varied too much, the map is still built, just unscaled.
Which coordinate frame poses are in. position is the camera's centre in map coordinates; orientation is a unit quaternion [w, x, y, z] rotating camera axes (OpenCV: x right, y down, z forward) into map axes. The map frame depends on how the map was scaled:
scale_status | Map frame |
|---|---|
metric (marker) | origin at the centre of the lowest-ID marker; x along the marker's top edge to the right, y down the marker, z into the wall (as seen by someone facing the marker); metres |
approximate (camera height) | z up (gravity); origin on the floor below the walkthrough's starting point; approximately metres |
unscaled | arbitrary units; gravity-aligned (z up) if the floor was detected (gravity_aligned: true), otherwise arbitrary axes |
Your own coordinate frame (optional). Poses are returned in the map's frame. To get them in your building's coordinates as well, give at least 3 point pairs (a point in map coordinates and the same point in yours):
bash
awscurl --service execute-api --region eu-north-1 -X PUT -d '{"pairs": [
{"map_xyz": [0, 0, 0], "customer_xyz": [10, 5, 0]},
{"map_xyz": [2, 0, 0], "customer_xyz": [12, 5, 0]},
{"map_xyz": [0, 3, 0], "customer_xyz": [10, 8, 0]}]}' \
https://vmap-api.niuty.com/v1/maps/<map_id>/alignmentLocalization responses then also include pose_customer. The points must not all lie on one line, and the fit's RMS error must be at most 0.25 (in your units).
6. API reference
vmap-api (https://vmap-api.niuty.com, SigV4 signed)
| Method & path | What it does |
|---|---|
POST /v1/maps | Create a map: {name, source_kind: "video"|"images_zip", size_bytes, content_type?, marker_size_m?, camera_height_m?, device?} → 201 {map, upload}. upload is a one-time form for exactly size_bytes (valid 1 hour) |
POST /v1/maps/{id}/build | Start the build after uploading → 202 (status queued) |
GET /v1/maps · GET /v1/maps/{id} | List maps · one map with status awaiting_upload, queued (waiting for a GPU), running (with stage, progress), ready or failed (with failure_reason) |
DELETE /v1/maps/{id} | Delete the map and all its data; your instances drop it within about a minute |
PUT / GET / DELETE /v1/maps/{id}/alignment | Set, read or remove your coordinate alignment |
POST /v1/instances · GET /v1/instances[/{id}] · DELETE /v1/instances/{id} | Create, list/read, delete localization instances |
POST /v1/api-keys · GET /v1/api-keys · DELETE /v1/api-keys/{id} | Create (key shown once), list, revoke API keys |
Unsigned requests, or requests from an AWS account that isn't registered with niuty, are refused with 403.
Your instance (https://<id>.vmap.niuty.com, Authorization: Bearer vmap_…)
| Method & path | What it does |
|---|---|
POST /v1/maps/{map_id}/localize | multipart/form-data with image (JPEG/PNG, long side ~1600 px) or video (≤ 10 s); optional intrinsics (JSON string {"fx","fy","cx","cy","width","height"} in pixels of the uploaded image; otherwise VMAP uses EXIF data, a device table, or an estimate) and device (e.g. iphone-15). → {status: "localized"|"not_localized", reason, pose: {position, orientation}, pose_customer, scale_status, inliers, inlier_ratio, confidence, confidence_calibrated, timing_ms, frames, selected_frame_timestamp_s}. reason (when not_localized): insufficient_features, no_retrieval_match, too_few_inliers, ambiguous, inconsistent_frames. frames / selected_frame_timestamp_s are for video queries |
GET /v1/ready · GET /v1/health | Readiness · liveness (no key needed) |
GET https://vmap-api.niuty.com/v1/health (unsigned) reports the central API's status. Poses: see Which coordinate frame poses are in.
Errors
All errors look like {"error": {"code": "…", "message": "…", "details": {…}}}.
| HTTP | Codes |
|---|---|
| 400 | validation_error |
| 401 | unauthorized (instance: missing or invalid API key) |
| 403 | vmap-api: unauthorized with details.reason: account_not_registered, or AWS's own 403 for unsigned requests |
| 402 | insufficient_credit, account_suspended |
| 404 | not_found |
| 409 | name_conflict, map_limit_reached, instance_limit_reached, api_key_limit_reached, no_active_instance (a build needs an instance), build_in_progress, invalid_state, upload_missing, map_not_ready (still building, or the instance is still downloading it), map_incompatible, map_not_available_on_instance |
| 413 | limit_exceeded |
| 422 | alignment_too_few_pairs, alignment_degenerate, alignment_residual_too_high |
| 429 | rate_limited, quota_exceeded, queue_full (all with Retry-After) |
| 503 | not_ready (instance starting), build_service_unavailable, provisioning_unavailable |
7. Data handling and privacy
- Your walkthrough video is deleted as soon as the build service has downloaded it, at the start of the build. It's never kept. An upload that's never built is deleted after 1 hour.
- Maps are stored in the EU and used only to answer your localization requests. You can't download them, and neither can anyone else. They're deleted when you delete them, or 60 days after your account's last instance is gone.
- Failed builds keep no files; only a small record (with the failure reason) stays for 30 days.
- Localization photos are processed in memory on your dedicated instance (EU) and not stored. The instance logs one line per query (map, result, processing time): no image data, API key, pose or client IP. Its web server keeps no access log; error entries may include the client IP and path, stay on the instance and are deleted with it.
- Use neutral map names and API-key labels. Don't put personal data in them.
Your responsibilities for the data you capture and send (consent, recording the people in a space, children's data) are set out in the Terms of Service; what we collect about you is in the Privacy Policy.