Two product keyframes may look similar while a supposedly fixed bottle moves several pixels between them. A developer can make that preparation error visible with a small coordinate check before submitting the pair to a video workflow.
This tutorial compares manually supplied landmarks against expected movement vectors. It produces a JSON report and a process exit code. It does not open image files, detect landmarks, align images, or measure a generated video. The sample uses synthetic annotations for a cap-lift product reveal.
1. Give the pair one coordinate contract
Use the same prepared canvas for both endpoint images. Here it is 1080 pixels wide and 1920 pixels high. The origin is the top-left corner; x increases to the right and y increases downward. Coordinates describe pixel locations, so the right and bottom boundaries are excluded.
Choose landmarks whose intended behavior is clear. The bottle base and a label corner should remain fixed. The cap center should rise 240 pixels. An expected vector of [0, 0] describes a fixed point, while [0, -240] describes that upward cap movement.
Save this as plan.json. The two-pixel tolerance is a project choice for this example, not an image-generation quality threshold. It applies to the distance between the measured displacement and the expected displacement.
{
"canvas": {
"width": 1080,
"height": 1920
},
"landmarks": [
{
"name": "bottle-base",
"start": [
540,
1500
],
"end": [
540,
1500
],
"expected": [
0,
0
],
"tolerance": 2
},
{
"name": "label-left",
"start": [
400,
1050
],
"end": [
400,
1050
],
"expected": [
0,
0
],
"tolerance": 2
},
{
"name": "cap-center",
"start": [
540,
660
],
"end": [
540,
420
],
"expected": [
0,
-240
],
"tolerance": 2
}
]
}
Record the coordinates from the actual prepared images when using this on a project. Naming a canvas in JSON does not prove that the files have that size. If one endpoint has been cropped or rotated differently, correct the shared coordinate system before comparing the numbers.
2. Validate the annotations before comparing them
Save the following module as landmarks.cjs. It uses only Node.js and JavaScript built-ins, with no package installation. Positive integer canvas dimensions, finite coordinates, distinct names, and nonnegative tolerances are checked before a report is returned.
function auditLandmarks(plan) {
const size = plan?.canvas;
if (!size || !Number.isSafeInteger(size.width) || size.width <= 0 ||
!Number.isSafeInteger(size.height) || size.height <= 0) {
throw new Error('Canvas dimensions must be positive safe integers');
}
const points = plan.landmarks;
if (!Array.isArray(points) || points.length < 1 || points.length > 256) {
throw new Error('Expected 1 to 256 landmarks');
}
const names = new Set();
function pair(value) {
return Array.isArray(value) && value.length === 2 &&
value.every(Number.isFinite);
}
function inside(value) {
return pair(value) && value[0] >= 0 && value[0] < size.width &&
value[1] >= 0 && value[1] < size.height;
}
const rows = points.map(point => {
if (!point || typeof point.name !== 'string' ||
!/^[a-z][a-z0-9-]{0,63}$/.test(point.name) || names.has(point.name)) {
throw new Error('Landmark names must be unique readable identifiers');
}
names.add(point.name);
if (!inside(point.start) || !inside(point.end) || !pair(point.expected) ||
Math.abs(point.expected[0]) >= size.width ||
Math.abs(point.expected[1]) >= size.height ||
!Number.isFinite(point.tolerance) || point.tolerance < 0) {
throw new Error('Invalid coordinates, expected vector, or tolerance');
}
const dx = point.end[0] - point.start[0];
const dy = point.end[1] - point.start[1];
const errorPx = Math.hypot(dx - point.expected[0], dy - point.expected[1]);
return { name: point.name, dx, dy, errorPx,
pass: errorPx <= point.tolerance };
});
return { pass: rows.every(row => row.pass), rows };
}
module.exports = { auditLandmarks };
The example accepts between one and 256 landmarks. Every point must have a two-number start position, end position, and expected vector. Expected movement components must be smaller than the corresponding canvas dimension in magnitude; an impossible whole-canvas displacement is rejected as an input error.
Readable names make the report useful to the editor who measured the landmarks. The name rule accepts lowercase letters, digits, and hyphens, beginning with a letter, up to 64 characters. Duplicate names are rejected so that two rows cannot silently refer to the same identifier.
3. Compare movement with the intended change
For each landmark, subtract start from end to get dx and dy. Then subtract the expected vector from that actual vector. Math.hypot computes the length of the remaining two-dimensional error. A row passes when this error is less than or equal to its declared tolerance.
For the cap, (540, 420) minus (540, 660) gives (0, -240). Subtracting the expected (0, -240) leaves no error. For the fixed bottle base, a three-pixel shift to the right produces an error of three pixels and fails a tolerance of two.
This is a radial tolerance, not a separate allowance on each axis. A shift of two pixels horizontally and two vertically has an error of about 2.83 pixels, so it fails. That distinction matters when a diagonal move appears small in each individual coordinate.
The function leaves the supplied plan unchanged and returns one row per declared landmark. The overall pass value requires every row to pass. A passing cap does not hide a drifting bottle base.
4. Expose the result through a small CLI
Save this as run.cjs beside the module and plan. Run node run.cjs plan.json from that directory. Standard output contains the report as JSON; the command reads the plan and does not rewrite it or any image asset.
const { readFile } = require('node:fs/promises');
const { auditLandmarks } = require('./landmarks.cjs');
async function main() {
const args = process.argv.slice(2);
if (args.length !== 1) throw new Error('Usage: node run.cjs plan.json');
const input = await readFile(args[0], 'utf8');
const report = auditLandmarks(JSON.parse(input.replace(/^\uFEFF/, '')));
console.log(JSON.stringify(report, null, 2));
process.exitCode = report.pass ? 0 : 1;
}
main().catch(error => {
console.error(error.message);
process.exitCode = 2;
});
Exit code 0 means all declared landmarks passed. Exit code 1 means the input was valid but at least one landmark missed its tolerance. Exit code 2 means usage, file reading, JSON parsing, or annotation validation failed. A caller can distinguish a proposed pair that needs alignment from a malformed worksheet.
The baseline report has three rows. The cap row has dx = 0, dy = -240, errorPx = 0, and pass = true. The two fixed landmarks also have zero error. Preserve the full report instead of recording only a green pass value, so that later changes can be compared by name.
5. Check the boundaries and the failure path
Save this short example as sample.cjs and run node sample.cjs. It checks the intended movement, a passing tolerance boundary, a failing drift, and an out-of-canvas coordinate. The tests operate on the worksheet data, not on product pixels.
const assert = require('node:assert/strict');
const { auditLandmarks } = require('./landmarks.cjs');
const plan = require('./plan.json');
assert.equal(auditLandmarks(plan).pass, true);
assert.equal(auditLandmarks(plan).rows[2].dy, -240);
const shifted = structuredClone(plan);
shifted.landmarks[0].end[0] += 3;
assert.equal(auditLandmarks(shifted).pass, false);
const boundary = structuredClone(plan);
boundary.landmarks[0].end[0] += 2;
assert.equal(auditLandmarks(boundary).pass, true);
const invalid = structuredClone(plan);
invalid.landmarks[0].end[0] = plan.canvas.width;
assert.throws(() => auditLandmarks(invalid), /Invalid coordinates/);
console.log('Landmark movement, tolerance, and bounds checks passed');
A broader local check set passed 53 checks with Node.js 24.20.0. It covered report values, input immutability, horizontal and diagonal tolerances, invalid dimensions, duplicate names, malformed vectors, invalid tolerances, and the CLI 0/1/2 paths. A UTF-8 byte-order mark and invalid JSON were also exercised.
Keep those categories in your own regression checks if you change the contract. For example, allowing coordinates on the right boundary would change what x = width means. Increasing a tolerance changes the project decision even when the comparison algorithm remains identical.
6. Use the report as preparation evidence
If a fixed landmark fails, inspect the two prepared frames and determine whether the measurement, crop, or object placement changed. Update the images or correct the annotation, then rerun the comparison. Do not increase the tolerance solely to make an unexplained shift disappear from the report.
A few landmarks cannot establish that an entire product is unchanged. A label can deform between its corners, lighting can shift, and a generated transition can fail even when both endpoints fit the plan. Pair this numerical preparation check with viewing the actual images and, later, the resulting clip.
Store the plan and report beside the endpoint filenames and prompt version in the project handoff. This gives a teammate a repeatable answer to a narrow question: did the declared point movements match this particular preparation plan? It is not a model benchmark or automatic visual similarity score.
AI assistance: This developer tutorial was drafted and edited with AI assistance. The Node.js code, synthetic coordinate checks, and CLI paths were run locally. No image decoding, landmark detection, AI generation, video rendering, or visual quality test is presented.
Source: Developer adaptation of PixMind’s “First-Last-Frame Product Reveals for Instagram Ads: A Wan 2.7 Case Study.” This article turns the endpoint-alignment idea into an annotation validator and omits unverified pricing, internal tests, and exact generation guarantees.
Reference: Node.js documentation for strict assertions, used in the displayed checks.
About the author
- The PixMind team shares practical AI image and video workflows. PixMind brings text-to-image, reference-based creation, editing, and video tools into one workspace. Website: https://www.pixmind.io/
Comments
Join the discussionNo comments yet. Be the first to comment.