System designExplainer

imagesc axis coordinates: map pixels to real-world sizes

Why a 43.2 m scan draws as 43 m in imagesc, how the x and y vectors set pixel-centre coordinates and axis size, and the fix that keeps ROI reads in metres.

LumaSpec Vision9 min read Share on LinkedIn

On this page 9 sections
  1. 01Why the ruler stops one pixel short
  2. 02What imagesc does with the x and y vectors
  3. 03The fix: centre vectors in metres, limits on the edges
  4. 04Worked example: a 1728-pixel scan at 25 mm per pixel
  5. 05Rectangle and profile coordinates that read in metres
  6. 06Where the pixel size comes from
  7. 07When setting xlim alone is not enough
  8. 08Checks before you trust the ruler
  9. 09Questions

A thermal scan that covers 43.2 m of ground shows up on the GUI axes as 43 m, and a rectangle drawn over the image reads back positions that do not match the scene. The file format is not the cause. The two vectors you pass to imagesc decide where every pixel sits on the axes, and a vector built with the default colon step counts in ones, never in metres. Below are the centre-coordinate rule imagesc actually applies, a short fix that puts the ruler on the true extent, and the conversion that turns rectangle positions back into pixel indices.

Why the ruler stops one pixel short

Two separate things go wrong, and each one alone shortens the axis. The first is the vector itself. An expression such as 0:length_data builds 0, 1, 2, … 43 — forty-four entries spaced one unit apart — because the colon operator's default step is 1 no matter what the endpoint represents physically. Handed that vector, imagesc takes 0 and 43 as the centres of the first and last columns, infers a 1 m pixel pitch, and draws an axis spanning 44 m whose last tick sits at 43. If the vector length matches the matrix dimension it is used as is; if not, MATLAB rejects it outright. Either way the ruler comes from the vector, and this vector never carried the pixel size.

The second cause is the centre-versus-edge convention, and it survives the first fix. imagesc places pixel centres at the coordinates you give it. A 1728-pixel row at 25 mm per pixel covers 0 to 43.2 m of ground, yet its 1728 centres only run from 12.5 mm to 43.1875 m. Draw the axis out to the last centre and the ruler stops 12.5 mm — half a pixel — short of each end. Limits belong on the scene edges, not on the outermost centres.

What imagesc does with the x and y vectors

The behaviour is short enough to list. Each element of x is the centre coordinate of the matching column, and each element of y the centre of the matching row; a two-element vector is also legal and means the centres of the first and last columns, with the spacing spread evenly between. The step between consecutive elements becomes the pixel width on that axis, in whatever unit you pass — metres, millimetres, degrees. Automatic axis limits sit half a spacing outside the first and last centres, so with correct centres the padding lands exactly on 0 and N × s with no xlim call at all. The y direction is reversed by default, which is why row 1 draws at the top.

L = N × s x_k = (k − 0.5) × s k = floor(x / s) + 1

The first line is the scene extent, the second places pixel k on the axis, and the third converts a coordinate read from the display back into a pixel index.

None of this touches the import side. Whether the matrix came from dlmread or xlsread, the display depends only on the vectors you hand imagesc; for the scaling, NaN handling and colour limits of the pixel values themselves, the thermal CSV visualization guide covers what to do with raw arrays once they are loaded.

The fix: centre vectors in metres, limits on the edges

Build the centre vectors from the displayed matrix, call imagesc with them, and pin the limits to the full extent:

C = data';                    % the matrix as displayed
N = size(C,2);                % pixels across
M = size(C,1);                % pixels down
s = 0.025;                    % metres per pixel
x = ((1:N)-0.5)*s;            % column centres in metres
y = ((1:M)-0.5)*s;            % row centres in metres
hImg = imagesc(x, y, C, clims);
ax = ancestor(hImg,'axes');   % this GUI's axes handle
xlim(ax,[0 N*s]);
ym = ylim(ax,[0 M*s]);        %#ok<NASGU>

With 1728 columns the x vector runs from (1 − 0.5) × s = 12.5 mm up to (1728 − 0.5) × s = 43.1875 m. The automatic limits would already land on 0 and 43.2; the explicit xlim and ylim hold that extent through zooms, holds and later plots on the same axes. To correct an image already on screen, re-map it in place with set(handles.image,'XData',x,'YData',y) instead of drawing a second image on top.

Worked example: a 1728-pixel scan at 25 mm per pixel

The inputs come from the failing scan: 1728 pixels across, a pixel size of one 25 mm step, and display clipping at 80–160 °C. The clip limits colour pixels but never move the axes, so the coordinate problem reduces to the first two numbers.

The rule: the scene extent is N × s, pixel centre k sits at (k − 0.5) × s, and the axis limits belong on the edges at 0 and N × s.

The extent is 1728 × 0.025 = 43.2 m. The failing path builds 0:43.2, which yields the 44 entries 0 through 43 spaced one unit apart; imagesc then reads end centres of 0 and 43, pads half a unit each side, and draws limits from −0.5 to 43.5 — a ruler whose last tick is 43, exactly the symptom in the GUI. The working path builds centres from 12.5 mm to 43.1875 m, and the half-pixel padding carries them to 0.0125 − 0.0125 = 0 m and 43.1875 + 0.0125 = 43.2 m.

The result is an axis spanning exactly 43.2 m edge to edge, with the first and last pixel each occupying its full 25 mm on the ruler.

The check that closes it: XLim(2) − XLim(1) must read 43.2. If it reads 44, the vector is unit-stepped; if it reads 43.1875 or less, the limits sit on centres instead of edges. A rectangle from 10 m to 20 m then selects columns floor(10/0.025) + 1 = 401 through ceil(20/0.025) = 800, which is 400 columns × 25 mm = 10.0 m of scene.

Rectangle and profile coordinates that read in metres

ROI tools report positions in axis data units, so correct XData makes them read metres directly. Draw a rectangle with imrect, read its position with getPosition, and the x and width values are metres; divide by the pixel size to get indices. A rectangle spanning 5 m to 12.5 m covers columns floor(5/0.025) + 1 = 201 through 12.5/0.025 = 500, so the mean over that strip is a plain mean over C(201:500, r1:r2). The same coordinates drive a temperature-versus-position line profile along any row or column.

That bookkeeping is not specific to one toolkit. Any GUI that plots a 2D pixel array against real-world coordinates faces the same centre-versus-edge decision, and this knowledgebase page on locating image coordinates from a 2D pixel array in LabVIEW walks through the identical conversion.

For a viewer you build into your own tool, the LS-IRC-0016 is a 640 × 512 LWIR module that streams over USB3, so the sensor and the display code can live in the same enclosure.

Infrared camerasLWIR thermal camera module, USB3, 640×512 resolution (LS-IRC-0016)LS-IRC-0016
Interface
USB3
$3,999.00 · quotedView and quote

Where the pixel size comes from

The display never sets the scale; the acquisition geometry does. For a lens-fed thermal camera the ground sample distance is the field of view divided by the pixel count, so the same relation runs both ways: a 16 m field of view over 640 columns gives 25 mm per pixel, and 640 columns at that pitch span FOV = 640 × 0.025 = 16 m. Working distance moves that number hard — doubling the distance roughly doubles s — which is why the working-distance and spot-size guide deserves a read before you hard-code a metres-per-pixel value.

The LS-IRC-0044 outputs 640 × 512 pixels over GigE. At the ground sample distance above, 640 columns cover the 16 m field of view, and the focal-length calculator below turns that field of view into a focal length for your sensor format.

Infrared cameras640 x 512 thermal camera, 8-14 μm, 50 Hz, GigE, FOV 88.5° × 73.2°, 9-24 V DC (LS-IRC-0044)LS-IRC-0044
Lens mount
C-mount
Interface
GigE
Resolution
Up to 3 MP
Spectrum
LWIR
$5,885.00 · quotedView and quote
Lens focal length, field of view and working distance
Units
Solve for
Focal length
– mm
Field of view
– mm
Working distance
– mm
Magnification
– x
Angle of view
– °
Pixel on the part
– mm

Thin-lens model: m = sensor width / field of view, f = WD × m / (1 + m). Accurate to a few percent above about 150 mm; check the lens's minimum object distance.

Find lenses with this focal length Open the full calculator

The lens picks that field of view. The thermal, SWIR and UV lens range sorts LWIR glass by image circle and focal length, and the lens finder narrows the list to a mount and focal length once the calculator has spoken.

When setting xlim alone is not enough

xlim and ylim change what the axes show; they never move the pixels. Apply xlim([0 43.2]) to an image whose XData still spans −0.5 to 43.5 and you crop a stretched image: the content at scene position 10 m no longer sits at axis coordinate 10, and a rectangle drawn afterwards reports positions in the broken frame. Set the vectors right first, then pin the limits — in that order.

Checks before you trust the ruler

  • length(x) equals size(C,2), or x has exactly two entries — a mismatch means the vectors describe a different grid than the image.
  • x(2) − x(1) equals the pixel size — the step carries the scale, not the endpoint.
  • The xlim span equals N × s, here 43.2 m — the axis covers the edges, not just the centres.
  • y passes the same two checks with its own spacing, in whichever direction you kept.

If the ruler survives those checks, the display, the rectangle tools and every exported coordinate share one frame in metres. When the scan itself is up for spec rather than the plot, the infrared cameras category lists the thermal and SWIR sensors behind data like this, or contact us with your pixel size and scan length and we will help size the chain.

Questions

Why does my axis stop at 43 m when the scan is 43.2 m long?

Two causes stack up. The vector 0:length_data steps by one, so it holds 0 through 43 and imagesc reads those as the centres of the end columns, labelling the ruler to 43. Even a correct centre vector stops half a pixel short of each end: centres run 0.0125 to 43.1875 m while the scene covers 0 to 43.2 m. Put the limits on the edges and the axis ends at 43.2.

Do I still need xlim and ylim if I pass correct centre vectors?

Often not on the first draw: imagesc pads the automatic limits half a spacing beyond the outer centres, which lands exactly on 0 and N × s. Keep the explicit calls anyway, because a zoom reset, a hold with other content or a later plot can quietly rescale the ruler. The XData and YData vectors carry the mapping; the limit calls only pin the view.

What changes when my pixels are not square?

Use two spacings. Build the vectors separately, x = ((1:N)-0.5)*sx and y = ((1:M)-0.5)*sy, then set xlim([0 N*sx]) and ylim([0 M*sy]). Convert a rectangle read-back axis by axis: column = floor(x/sx) + 1 and row = floor(y/sy) + 1. Scanning rigs often step more coarsely along the scan direction than across it, so check both pitches before you assume one number.

Does importing from CSV instead of XLS change the axis scaling?

No. Once the file is parsed, both paths hand you a numeric matrix and the display depends only on the vectors you give imagesc. Import differences show up in data type, NaN handling and speed, never in axis length. If two formats produce different rulers, compare the vectors you build from each file, not the readers.

Why does the y axis run top-down, and should I flip it?

imagesc reverses the y direction by default so row 1 sits at the top, matching image indexing. Keep that for image-style displays; switch to normal direction only when you want world-up axes, and remember a rectangle's reported y coordinates follow whichever direction the axis currently shows. Rows convert with the same floor(y/sy) + 1 rule once you know which way the axis runs.