Trackpoint Coordinate System¶
Purpose¶
Plant Tracer trackpoints must use a bottom-left coordinate origin in all user-facing
and persisted data. A trackpoint at (0, 0) is the lower-left corner of the
analysis frame. x increases to the right and y increases upward.
The movie image itself is not flipped. Canvas rendering, mouse dragging, and
OpenCV tracking still operate in native image coordinates, where (0, 0) is
the upper-left corner and y increases downward.
Coordinate Spaces¶
Plant Tracer uses two coordinate spaces:
trackpointcoordinatesThe persisted and user-visible coordinate system. Origin is the lower-left corner of the analysis frame. These values are stored in DynamoDB and exported in CSV.
canvascoordinatesThe browser canvas and OpenCV image coordinate system. Origin is the upper-left corner of the displayed analysis frame. These values are used only for drawing, hit detection, dragging, and optical-flow tracking.
The conversion uses the actual analysis-frame height:
canvas_x = trackpoint_x
canvas_y = frame_height - trackpoint_y
trackpoint_x = canvas_x
trackpoint_y = frame_height - canvas_y
OpenCV may calculate subpixel coordinates while tracking. Browser marker edits
are displayed and saved as rounded integer pixel coordinates so the marker
table, edited marker payload, and exported CSV agree. The top edge of a 480
pixel analysis frame has trackpoint y = 480; the bottom edge has trackpoint
y = 0.
Frame Height Source¶
Conversion must use the height of the analysis frame shown to the user, not the raw uploaded movie height.
The analysis frame is the rotated and scaled frame produced by
lambda-resize/src/resize_app/mpeg_jpeg_zip.py. The browser can use the
canvas controller’s natural image height once the background frame has loaded.
Lambda derives the conversion height from the first processed OpenCV frame
using the same rotation and scaling path as tracking, then supplies that height
to legacy migration if the movie row does not yet have stored dimensions.
DynamoDB Contract¶
The movies row owns the coordinate-system contract for all trackpoints in
that movie:
trackpoint_origin: Literal["bottom-left"] | None = None
Rules:
Missing or
Nonetrackpoint_originmeans legacy"top-left"storage. The implementation must not write"top-left"to new movie rows.trackpoint_origin = "bottom-left"means all stored frame trackpoints for that movie use the lower-left-origin contract.New movies are created with
trackpoint_origin = "bottom-left"inodb.create_new_movie().POST /api/new-moviecalls this function before returning the presigned upload form, and local tooling that creates movies should use the same function.A movie must not contain mixed-origin frame trackpoints.
Once a movie’s stored frame trackpoints are converted, the movie row is updated to
trackpoint_origin = "bottom-left".Future trackpoint writes for a
"bottom-left"movie store lower-left-origin values directly.
schema.Movie contains this field, and odb.TRACKPOINT_ORIGIN is the named
string constant for the DynamoDB attribute.
The permanent coordinate contract belongs on the movie row, not on each frame or
trackpoint. A per-frame migration marker (odb.TRACKPOINT_MIGRATION_ORIGIN) is
internal idempotency machinery for lazy migration; it is not the public
coordinate contract and is never exposed in API responses. The marker is durable
(left in place after migration); once the movie row’s trackpoint_origin is
"bottom-left" the migration routine returns early and never reads the markers
again.
Legacy Migration¶
Existing movies without trackpoint_origin contain top-left-origin
trackpoints. The selected migration strategy is server-side lazy migration of a
complete movie before the first operation that exposes or writes trackpoints
under the new contract.
The lazy migration trigger points are:
POST /api/get-movie-metadatawhen the request returns frame markers.POST /api/get-movie-trackpointsbefore CSV or JSON export.POST /api/put-frame-trackpointsbefore accepting edited markers.Lambda retracing before reading existing seed trackpoints for optical flow.
Partial migration is not allowed. Saving one edited frame as bottom-left while other frames remain top-left would corrupt the movie’s trackpoint sequence. Because a movie can have more frame records than DynamoDB can update in one transaction, lazy migration must not be a naive in-place batch rewrite.
Migration must be resumable or fail closed:
Determine the analysis-frame height before any write. If it cannot be determined, return an error and leave the movie unchanged.
Convert each frame with an atomic conditional write that only flips when the frame is not already marked bottom-left (
ConditionExpression='attribute_not_exists(#origin)'). This makes the per-frame flip idempotent, so a retry — or a concurrent migration of the same movie — can never double-flip a frame (a failed condition is caught and the existing result is left intact). See #1058.Set
trackpoint_origin = "bottom-left"only after every frame with trackpoints has been converted.While a movie is in an incomplete migration state, editing, retracing, and exporting trackpoints must wait, retry, or return an error rather than expose mixed-origin data.
Browser Responsibilities¶
canvas_tracer_controller.mjs is responsible for translating between stored
trackpoints and canvas objects.
When loading frame markers:
Read
metadata.trackpoint_originfrom themetadataobject returned byPOST /api/get-movie-metadata.For
"bottom-left"movies, convert stored trackpoints to canvas coordinates before creatingMarkerandLineobjects.If movie metadata lacks analysis-frame dimensions when the first frame is added, rebuild the current frame after the loaded image reports its natural dimensions. Do not flip bottom-left trackpoints against the placeholder canvas height.
For legacy
"top-left"movies that have not yet been migrated, use stored coordinates as canvas coordinates for visual correctness.
When dragging markers:
Keep the marker object in canvas coordinates so the marker follows the mouse over the unflipped movie.
Clamp dragged marker centers to the analysis-frame bounds so marker coordinates cannot leave the region of interest.
The marker table must display converted trackpoint coordinates live while the user drags.
get_markers()must return rounded integer trackpoint coordinates, not raw canvas coordinates, before posting to/api/put-frame-trackpoints.
The marker table and the posted payload must use the same conversion path so initial render, drag updates, saved data, and CSV export agree.
The Location (mm) column is calibrated only after both default ruler markers
(Ruler 0mm and Ruler 10mm) have moved away from their starting
positions. While a non-ruler marker is dragged after calibration, the pixel and
millimeter columns update together in real time. While a ruler marker itself is
being dragged, millimeter locations are withheld until the marker is released
and the calibration can be recomputed from the new ruler distance.
Graph Responsibilities¶
Position graphs should consume trackpoint coordinates. Once frame data is bottom-left-origin, Y deltas are already positive upward. Chart.js should not reverse the Y axis to simulate a lower-left origin.
Flask API Responsibilities¶
POST /api/put-frame-trackpoints receives trackpoint coordinates. For
legacy movies, Flask first runs the lazy migration. For "bottom-left" movies,
Flask stores those values unchanged.
POST /api/get-movie-metadata returns the movie-row
trackpoint_origin inside metadata.trackpoint_origin. The endpoint already
returns the dictionary from odb.get_movie_metadata() inside the metadata
response key, so the field is exposed once it exists on schema.Movie and is
present in the movie row.
When POST /api/get-movie-metadata returns frame marker coordinates, Flask
first runs the lazy migration if metadata.trackpoint_origin is missing or
None. Returned frame markers therefore use the movie’s stored coordinate
contract, which is bottom-left after migration.
POST /api/get-movie-trackpoints first runs the lazy migration and then
exports stored trackpoint values directly. The CSV and JSON exports are
therefore automatically lower-left origin without a separate export-only flip.
Lambda Tracking Responsibilities¶
OpenCV must receive canvas/image coordinates. Lambda tracking therefore converts bottom-left trackpoints to top-left image coordinates before calling optical flow or drawing labels, then converts tracer output back to bottom-left before writing frame trackpoints to DynamoDB.
The conversion uses the processed frame height derived from
mpeg_jpeg_zip.get_first_frame_from_url(...).shape[0] rather than requiring
the movie row to already have height. This keeps initial tracing from
crashing when uploaded movie metadata has not yet been populated. The conversion
formula is:
before
cv2.calcOpticalFlowPyrLK:image_y = frame_height - trackpoint_ybefore
put_frame_trackpoints:trackpoint_y = frame_height - image_y
The traced MP4 and frame ZIP are visual artifacts. Marker overlays in those artifacts are drawn in image coordinates after conversion; the underlying movie frames are never flipped.
Tests¶
Substantive tests for the implementation should cover:
JavaScript marker load: bottom-left stored
ydraws at the expected canvasy.JavaScript drag update: the marker table displays bottom-left
ywhile the marker object remains in canvas coordinates.JavaScript save:
get_markers()posts rounded bottom-left coordinates.JavaScript path drawing: lines between frames convert both endpoints before drawing.
Graphing: Y deltas use bottom-left values and do not rely on reversed chart axes.
ODB creation:
odb.create_new_movie()storestrackpoint_origin = "bottom-left"for a new movie.Flask metadata:
POST /api/get-movie-metadataexposes the movie-row field asmetadata.trackpoint_origin.Flask CSV export: for a bottom-left movie, exported CSV values match stored trackpoints after integer export formatting.
Lambda tracking: input bottom-left trackpoints are converted before optical flow and converted back before persistence.
Migration: a legacy top-left movie is converted completely and marked
trackpoint_origin = "bottom-left".Migration retry: a failed lazy migration cannot double-flip already converted frames or expose a mixed-origin movie.
Documentation Follow-up¶
When the implementation lands, update user-facing coordinate descriptions in
docs/UserTutorial.rst and src/app/templates/analyze.html. Any affected
screenshots under docs/tutorial_images/ should be flagged for user review
rather than replaced automatically.