🎨 Transform Figma mockups into production-ready Dash apps. Join us November 10. Save your spot.

Chris Parmer

Chris Parmer

November 9, 2026

Image Plots with imshow - A Complete Guide

TL;DR

  • An image plot draws a grid of numbers as a grid of colored pixels at their true shape: a photograph's red, green and blue values as a color picture, or one measurement per pixel (elevation, brightness, vegetation) through a color scale.
  • Image plots are also known as imshow plots, after the function in matplotlib and Plotly, and for small matrices as matshow plots. They are similar to heatmaps and contour plots.
  • In Plotly, px.imshow handles both kinds: it draws three-channel arrays as an image trace and single-channel arrays as a heatmap trace, keeps pixels square, and puts row 0 at the top unless you set origin="lower".
  • The settings that matter are the contrast (zmin and zmax, or a log scale), the coordinates (x, y and origin), binary_string=True for images of millions of pixels, and facet_col or animation_frame for stacks and time series of images.

What is an image plot?

Image plots look like this:

Sentinel-2 true-color satellite image of Bhadla Solar Park in the Thar Desert of Rajasthan, India, 12 by 9 km with axes in kilometers: blue-gray blocks of solar panels cover most of the south and center of the image, surrounded by tan and brown desert fields

Data: Copernicus Sentinel-2 Level-2A image of December 30, 2025, via Element 84 Earth Search.

How do you read an image plot? In the example above, notice these things:

  • Each pixel is one 10 m by 10 m square of ground, and the image has 1,200 columns and 900 rows of them. Because the axes are in kilometers and the pixels are drawn square, distances in the image are true distances.
  • There's no color scale, because each pixel carries three numbers (the red, green and blue light the satellite measured), and their mix is the color you see. The blue-gray blocks are fields of solar panels, part of the 56 square kilometers (km²) of India's largest solar park, and the tan and brown patches around them are sand and farm fields.
  • The brightness is a choice. The satellite stores reflectance (the share of sunlight bounced back), and here a reflectance of 0.32 is mapped to full brightness with zmax; without it the desert would look almost black.
  • An image array starts at the top-left corner, with row 0 at the top. This one is drawn with origin="lower" and a y axis in kilometers north, so north is up and the numbers on the axis increase upward.

It's often used for charts like these:

Fluorescence microscopy image of human cells stained for DNA, 512 by 512 pixels, with blue nuclei on black and orange boxes around 29 bright, dividing nuclei

Microscopy. Drug screens image thousands of wells of cells, and this one from a genetic screen has 317 nuclei, 29 of them dividing (boxed), a mitotic index of 9.1%. Data: Moffat et al. (2006) via scikit-image (human_mitosis, CC0, a public domain dedication), with the regions from scikit-image's mitosis segmentation example.

Fundus photograph of the back of a healthy left eye: an orange-red disc with dark red blood vessels spreading from a bright yellow optic disc on the left, and a darker spot in the center labeled as the fovea

Medical imaging. A photograph of the retina is an RGB image, and annotations with leader lines point a reader to the optic disc and the fovea; the red channel averages 207 of 255. Data: Mikael Häggström, Wikimedia Commons (CC0), via scikit-image (retina).

Grayscale near-ultraviolet image of the Andromeda galaxy, 3 by 3 degrees, on a logarithmic color scale: a tilted spiral disk of bright knots of young stars with a glowing core, surrounded by hundreds of foreground stars, with right ascension decreasing to the right and declination up

Astronomy. Telescope images span thousands of times in brightness, so they are drawn on a log scale: here the brightest pixel is 10.2 counts per second and the median 0.0049. Data: NASA Galaxy Evolution Explorer (GALEX) Near UV, via NASA SkyView.

How images become arrays

An image plot is easy to make and easy to get subtly wrong, because a few conventions sit between the numbers and the picture. These are the ones to know.

Pixels and channels

A digital image is a grid of numbers. A photograph has three numbers per pixel, one for each of red, green and blue (RGB), so in Python it's an array of shape (rows, columns, 3); a fourth channel, alpha, sets transparency. Most scientific images have one number per pixel instead, such as a temperature, an elevation or the light a microscope camera counted, so the array is (rows, columns). Ordinary photos store each channel as a whole number from 0 to 255 (8-bit, uint8), while cameras in labs and on satellites often use 0 to 65,535 (16-bit) or decimals.

Single-channel images need a color scale

With one number per pixel, px.imshow maps the values to colors with a color scale and draws a colorbar, exactly like a heatmap. Use a sequential scale (gray, or light to dark in one hue) for values with a natural low end, such as elevation or brightness, and a diverging scale with a midpoint for values that are positive or negative around a meaningful zero. With three channels, there's no color scale: the numbers are the colors.

Contrast: zmin and zmax

zmin and zmax set which values map to the darkest and lightest colors, and everything outside is clipped. By default px.imshow uses the data's minimum and maximum for a single-channel image, and the range of the data type (0 to 255 for 8-bit) for RGB. That's the right choice for elevation, but for an image with a few very bright pixels it makes everything else black. Here is the same Andromeda image three ways.

From the minimum to the maximum, the default, the brightest pixel (10.2) sets the top of the scale, and 99% of the pixels are below 0.039, so the image is almost black:

The Andromeda image with a linear color scale from the minimum to the maximum value: an almost entirely black square

Clipped at the 1st and 99.5th percentiles, the galaxy and the stars appear, but the core and the brightest stars are saturated white:

The same image clipped at the 1st and 99.5th percentiles: the spiral disk and stars are visible, with a saturated white core

On a log scale, with np.log10 of the data and the limits at the 1st and 99.9th percentiles, the faint outer arms and the bright core both keep detail. Label the colorbar in the original units, as example 4 does:

The same image on a log scale: the spiral arms, dust lanes and core all show detail, and the background noise is visible as gray texture

For RGB images, zmin and zmax take one value per channel (zmax=[3200, 3200, 3200]) or one for all three, which is how the solar park image maps a reflectance of 0.32 to full brightness. The contrast_rescaling="minmax" option does the minimum-to-maximum stretch per channel for you.

Origin, coordinates and aspect

An image array starts at the top-left corner, so px.imshow draws row 0 at the top with the y axis increasing downward (origin="upper"), the same as matplotlib's imshow. Two kinds of data break that convention. Gridded data from Network Common Data Form (NetCDF) files and many climate and ocean models store latitude increasing with the row, and astronomy's Flexible Image Transport System (FITS) files store the first row at the bottom; both need origin="lower". When you pass real coordinates as x and y, such as kilometers, latitudes or millimeters, px.imshow places each row and column at its coordinate, and origin="lower" gives the y axis that increases upward that those coordinates expect.

px.imshow also keeps pixels square by linking the two axes (scaleanchor), which is right when both axes share units. Set aspect="auto" to let the image fill the plot instead, which is fine for a matrix but distorts a photograph. When the units differ, set the ratio yourself: at 45° north a degree of longitude is about 71% as long as a degree of latitude, so the Europe example sets xaxis.scaleratio to cos(45°), and the MRI example passes coordinates in millimeters because its pixels are 1 mm wide and 1.33 mm tall.

Image plot or heatmap?

They are close relatives, and for a single-channel array px.imshow actually draws a heatmap trace. The difference is in the defaults and the intent. An image plot treats the array as a picture: square pixels, row 0 at the top, often hundreds of thousands of pixels, axes in pixels or physical units, and no gaps between cells. A heatmap usually shows a table, such as a correlation matrix or sales by weekday and hour, with category labels on the axes, cells that can be rectangles of any shape, and often the value printed in each cell. Use px.imshow for images and grids of measurements, and go.Heatmap or px.imshow(df, text_auto=True, aspect="auto") for labeled tables.

A short history

The first digital image was scanned in 1957 at the US National Bureau of Standards (now the National Institute of Standards and Technology), where Russell Kirsch fed a photograph of his three-month-old son into its Standards Eastern Automatic Computer (SEAC) as a grid 176 pixels on a side. John Hunter's matplotlib, first released in 2003 as a MATLAB-like plotting library (its name blends MATLAB, plot and library), gave Python the imshow and matshow functions most people still reach for (Hunter 2007). Plotly added an image trace to plotly.js in October 2019 and a "matplotlib-style" px.imshow to plotly.py a few weeks later, then binary_string in 2020, facet_col and animation_frame the same year, and facet_row in April 2026 (Plotly's changelog). We couldn't find Kirsch's original scan under a license we could reuse, so this guide doesn't redraw it.

Alternatives to an image plot

Here's the same grid of Mount Fuji elevations drawn six ways. Some are better for the question, some worse, some just different:

The Mount Fuji elevation grid drawn six ways: an image with a green-brown-white color scale, a filled contour plot with bands every 500 m, a 3-D surface, a gray hillshade showing ridges and valleys, a histogram of elevations peaking near 1,000 m, and a west-to-east profile through the summit rising to about 3,700 m
  • Image plot (the default for a grid of measurements). Every pixel is shown with its value on hover, and it scales to millions of pixels.
  • Contour plot (better for reading levels). Bands every 500 m make it easy to say which areas are above 2,000 m, but the detail inside each band is gone.
  • 3-D surface (different, good for shape). It shows the cone at a glance and can be rotated, but values are hard to read off and the far side is hidden.
  • Hillshade (works too, for terrain). It's still an image plot, of shading computed from the slopes, and shows ridges and valleys better than any color scale, but not the elevation itself.
  • Histogram of pixel values (different). It shows how much of the area is at each elevation, and nothing about where. Use it to choose zmin and zmax.
  • Line profile (better for one cross-section). A line chart along one row gives exact elevations along the way, which an image can't.

How to make an image plot

With AI

ChatGPT, Claude Code, GitHub Copilot, Cursor and other AI tools are excellent at writing Plotly code. Name Plotly in the prompt, and say whether the array is RGB or one channel, where the origin is, and how to set the contrast:

Make a plotly image plot with px.imshow from fuji_dem.npz (arrays: elev in meters, rows south to north;
lat ascending; lon).
- Pass x=lon and y=lat with origin="lower" so north is up, and set xaxis.scaleratio to
cos(mean latitude) so the mountain isn't stretched east to west.
- Use a terrain-like sequential color scale with zmin=0 and a colorbar titled "Elevation (m)".
- Overlay go.Contour lines every 500 m with coloring="none".
- Hover: longitude, latitude and elevation with a thousands separator.
- Check: print the highest pixel and its coordinates, and say whether the image is upside down.

With Plotly Studio

Plotly Studio is a desktop app where you point an AI at a dataset and it builds charts and a Dash app around your questions. Ask for an image plot the same way:

Show the monthly NDVI images of these fields as an image plot with a slider through the months, a fixed color scale from 0 to 0.9 in browns and greens, and a line chart below of the share of pixels above 0.5 each month.

Make an image plot in Python or JavaScript

This is the code AI tools write under the hood. In Python, px.imshow takes a NumPy array, a PIL image, a pandas DataFrame or an xarray DataArray. An RGB image needs one line of code:

import plotly.express as px
from PIL import Image
img = Image.open("photo.jpg") # or any (rows, columns, 3) array
fig = px.imshow(img)
fig.update_xaxes(visible=False)
fig.update_yaxes(visible=False)
fig.show()

A single-channel array gets a color scale, and real coordinates go in x and y:

import numpy as np
import plotly.express as px
d = np.load("fuji_dem.npz") # elev (rows south to north), lat, lon
fig = px.imshow(d["elev"], x=d["lon"], y=d["lat"], origin="lower", zmin=0,
color_continuous_scale="viridis", labels=dict(x="Longitude", y="Latitude", color="Elevation (m)"))
fig.update_xaxes(scaleratio=np.cos(np.radians(d["lat"].mean()))) # degrees of longitude are shorter
fig.show()

For more control, the two traces underneath are go.Image (for RGB, with z as an array or source as an image URL or base64 PNG) and go.Heatmap (for one channel). px.imshow sets up the square pixels and the reversed y axis for you, so with go.Heatmap add yaxis=dict(autorange="reversed", scaleanchor="x") to match it.

If you're coming from matplotlib, most arguments carry over: cmap becomes color_continuous_scale, vmin and vmax become zmin and zmax, origin and aspect work the same way, and extent becomes the x and y coordinate arrays. plt.matshow(m) is px.imshow(m).

In JavaScript, plotly.js draws a single-channel array as a heatmap trace with the y axis reversed and linked to x, which is what px.imshow does in Python:

<div id="chart"></div>
<script src="https://cdn.plot.ly/plotly-4.1.1.min.js"></script>
<script>
// Mean of 178 handwritten zeros, 8 x 8 pixels, values 0 to 16 (UCI digits via scikit-learn)
const z = [
[0, 0, 4.2, 13.1, 11.3, 2.9, 0, 0],
[0, 0.9, 12.6, 13.4, 11.5, 11.4, 1, 0],
[0, 3.7, 14.3, 5.3, 2.1, 12.2, 3.5, 0],
[0, 5.3, 12.7, 2, 0.1, 9.1, 6.4, 0],
[0, 5.9, 11.6, 0.9, 0, 8.8, 7.1, 0],
[0, 3.5, 13.3, 1.7, 1.5, 11.3, 5.9, 0],
[0, 0.8, 13.1, 10, 10.3, 13.3, 2.4, 0],
[0, 0, 4.2, 13.6, 13.3, 5.4, 0.3, 0]
];
Plotly.newPlot("chart", [{
type: "heatmap", z: z, zmin: 0, zmax: 16, colorscale: "Greys", reversescale: true,
hovertemplate: "row %{y}, column %{x}<br>%{z} of 16<extra></extra>"
}], {
width: 520, height: 520,
xaxis: {constrain: "domain", showgrid: false},
yaxis: {autorange: "reversed", scaleanchor: "x", constrain: "domain", showgrid: false}
});
</script>

For an RGB image, use {type: "image", source: "photo.png"} (an image URL or a base64 data URL) or {type: "image", z: rgbArray}; the image trace puts row 0 at the top and keeps pixels square on its own.

Share it

Every Plotly chart has a Share chart button (the cloud icon in its toolbar) that uploads it to Plotly Cloud and gives you a link. Only you can see it until you change its visibility.

For a live app, put the chart in a Dash app and publish it with the Plotly Cloud command-line interface (CLI). Coding agents can run these commands for you:

pip install "dash[cloud]"
plotly user login
plotly app publish

If your data has to stay in your own cloud or data center, publish to Dash Enterprise instead.

Dos and don'ts

Most mistakes with image plots are about geometry rather than color: which way is up, whether pixels stay square, and what the axes mean. Here is Europe at night both ways, from the same array, whose rows run south to north as they do in many gridded files.

Don't pass the array with px.imshow's defaults, which puts its first row (the south) at the top and turns Europe upside down, set aspect="auto" so the image stretches to fill the box, or leave the axes in pixel numbers that mean nothing to a reader:

Europe at night drawn upside down, with the Mediterranean at the top and Scandinavia at the bottom, stretched wide, with axes labeled in pixel numbers from 0 to 2,000

Do pass longitude and latitude as x and y with origin="lower", so north is up, keep the aspect equal with xaxis.scaleratio set to the cosine of the latitude, and label the axes in degrees:

The same image the right way up, with Europe and the Mediterranean in their familiar shape, axes labeled 10°W to 40°E and 30°N to 60°N

Checklist

Do

  • Check which way is up on an image you know, and set origin="lower" for gridded and FITS data.
  • Pass physical coordinates (x, y) in kilometers, degrees, micrometers or millimeters, so hover and axes speak the reader's units.
  • Set zmin and zmax deliberately, from percentiles or from the physical range, and use the same values across images you compare.
  • Use a log scale for data that spans orders of magnitude, with the colorbar labeled in the original units.
  • Use binary_string=True above about a million pixels, and binary_format="jpg" for photographs.
  • Hide the axes on photographs where pixel numbers mean nothing.

Don't

  • Stretch a photograph or map with aspect="auto"; keep pixels square or set the true ratio.
  • Use a rainbow color scale such as jet for single-channel data; its bright bands invent edges that aren't in the data.
  • Let each image in a stack pick its own color range when the reader compares them.
  • Send a 16-megapixel float64 array as numbers to a browser; downsample it, send float32 or uint8, or use binary_string.

Options to customize

These are the options of px.imshow and the image and heatmap traces that matter most. The full lists are in the imshow documentation, the image trace reference and the heatmap trace reference.

  1. zmin, zmax: the values mapped to the ends of the color scale; one number for a single channel, or a list per channel for RGB.
  2. contrast_rescaling: "minmax" stretches each channel from its minimum to its maximum, "infer" uses the data type's range (0 to 255 for 8-bit).
  3. color_continuous_scale, color_continuous_midpoint: the color scale for single-channel data, and the value at the middle of a diverging scale.
  4. origin: "upper" (the default) puts row 0 at the top, "lower" at the bottom.
  5. x, y: coordinates of the columns and rows, so axes and hover use physical units.
  6. aspect: "equal" keeps pixels square, "auto" fills the plot area; xaxis.scaleratio sets any other ratio.
  7. binary_string, binary_format, binary_compression_level: send the image as a PNG or JPEG string instead of numbers, for speed and file size; single-channel images then show in gray without a colorbar.
  8. facet_col, facet_col_wrap, facet_row: split an axis of a 3-D or 4-D array into small multiples with linked zoom.
  9. animation_frame: turn an axis of the array into frames with a slider and a play button.
  10. labels: names for x, y and color in the hover and the colorbar.
  11. text_auto: print each pixel's value, for small matrices only.
  12. fig.add_shape(editable=True), layout.newshape and the drawrect, drawclosedpath, drawcircle and eraseshape modebar buttons: draw or let readers draw regions of interest (ROIs) on the image; in Dash, the shapes come back in relayoutData.
  13. zsmooth: smooths between pixels instead of drawing hard-edged squares ("best" or "fast" on a heatmap, "fast" on an image trace).

More image plot examples

Six more image plots built from real public data, each with its source, the technique that makes it work and a prompt to rebuild it.

1. A year of center-pivot fields in Kansas, as an animation

Normalized difference vegetation index (NDVI) image of a 10 by 10 km area near Garden City, Kansas, on July 13, 2025: dark green circles of irrigated center-pivot fields and green grassland, with tan areas in the town, and a slider below with one image per month from January 14 to December 15

The normalized difference vegetation index (NDVI) measures how much of the ground is covered by green leaves, from about 0.1 for bare soil to 0.9 for a dense crop. Near Garden City, 60% of the area is above 0.5 in July and 1.2% in February, and the slider shows which circles of irrigated fields green up first. Data: Copernicus Sentinel-2 Level-2A, one clear image a month in 2025, via Element 84 Earth Search.

Tip: with animation_frame, fix zmin and zmax so a color means the same NDVI in every frame.

Get the data from Copernicus Sentinel-2 Level-2A on AWS (Element 84 Earth Search SpatioTemporal Asset Catalog (STAC) API, collection
sentinel-2-c1-l2a): for each month of 2025, the clearest scene over 100.85°W, 37.95°N, bands B04 (red)
and B08 (near infrared) in a 10 x 10 km window, NDVI = (B08 - B04) / (B08 + B04) averaged to 40 m pixels
(array: month, row, column).
Build a plotly px.imshow with animation_frame=0, the scene dates as frame labels, zmin=0 and zmax=0.9 on a
brown-to-green scale, axes in km, and the title giving the share of pixels above 0.5 in July and February.

Another way to see it: small multiples with facet_col show four months at once, which is better on paper and in a slide, while the animation is better for finding the month a field changed:

The same NDVI images for February 28, May 4, July 13 and October 21, 2025 side by side: almost all tan in February, a few green circles in May, mostly green in July and a handful of green circles in October, with 1%, 13%, 60% and 14% of pixels above 0.5

2. Europe at night, 2.9 megapixels sent as a JPEG

Europe and the Mediterranean at night from 15°W to 45°E and 30°N to 60°N: bright clusters of light in England, the Low Countries, northern Italy and along the coasts of Spain and the Nile, with dark seas and sparsely lit plains to the east

This composite of satellite images has 2.9 million pixels and 8.6 million RGB values. Sent as numbers, the figure's data takes 11.9 MB; with binary_string=True and binary_format="jpg" it takes 0.57 MB, so the page loads fast and the zoom stays smooth. Data: NASA Earth Observatory Black Marble 2016 (the day/night band of the Visible Infrared Imaging Radiometer Suite, VIIRS, on the Suomi NPP satellite), via NASA's Global Imagery Browse Services (GIBS).

Tip: use JPEG for photographs and PNG (the default) for images with sharp edges or exact values, such as masks and labels.

Get the data from NASA GIBS (Global Imagery Browse Services) Web Map Service (WMS), layer VIIRS_Black_Marble, in latitude and longitude (EPSG:4326),
longitude -15 to 45 and latitude 30 to 60, 2,400 x 1,200 pixels, PNG (array: row, column, RGB).
Build a plotly px.imshow with x as longitude and y as latitude, origin="lower", binary_string=True and
binary_format="jpg", xaxis.scaleratio = cos(45 degrees), degree tick labels, and no grid.

3. Mount Fuji's elevation with contour lines on top

Elevation image of Mount Fuji, Japan, on longitude and latitude axes, green lowlands rising through tan and brown to a white summit in the center, with thin contour lines every 500 m forming nearly round rings and the highest pixel labeled 3,744 m

Each pixel is the average elevation of about 62 by 62 m of ground, so the highest pixel is 3,744 m, a little below the summit's official 3,776 m. The contour lines every 500 m form nearly round rings, which is the shape of a stratovolcano. Data: Terrain Tiles on AWS Open Data (Mapzen terrarium tiles, zoom 11).

Tip: overlay go.Contour with contours.coloring="none" on the same x, y and z to add contour lines without hiding the image.

Get the data from the Terrain Tiles on AWS Open Data (Mapzen terrarium PNG tiles, zoom 11, elevation =
R*256 + G + B/256 - 32768) around 138.55°E to 138.92°E, 35.20°N to 35.52°N (array: elevation in m,
with latitude and longitude of each row and column).
Build a plotly px.imshow with x=lon, y=lat, origin="lower", xaxis.scaleratio = cos(latitude), a terrain
color scale from 0 to 3,800 m, contour lines every 500 m and the highest pixel labeled.

4. Three fluorescence channels of mouse kidney tissue

Three grayscale panels of the same 635 µm square of mouse kidney tissue, one per emission wavelength: at 450 nm hundreds of small round spots, at 515 nm some bright filled tubes and a round cluster, at 605 nm the outlines of many tubes and two bright round structures on the right

A confocal microscope records each fluorescent dye as a separate channel, and each one shows different structures in the same 635 micrometers (µm) of kidney: hundreds of small round spots at 450 nanometers (nm), a few bright filled tubes at 515 nm and the outlines of many others at 605 nm. Data: Genevieve Buckley, Monash Micro Imaging (CC0), via scikit-image (kidney).

Tip: facet_col=0 turns the first axis of a 3-D array into panels with linked zoom; scale each channel to its own percentile first, because the panels share one color axis.

Get the data from scikit-image's kidney() data set (mouse kidney, confocal, 16 z slices x 512 x 512 x 3
channels, 1.24 µm per pixel, CC0) and keep the middle z slice (array: channel, row, column).
Build a plotly px.imshow with facet_col=0, each channel divided by its 99.5th percentile, a gray scale
from 0 to 1, axes in µm, and facet titles naming the emission wavelengths 450, 515 and 605 nm.

Another way to see it: merged into one RGB image, with each channel as one color, the three show how the structures sit together, while the panels show each one clearly:

The three kidney channels merged into one color image: blue spots, green filled tubes and red tube outlines packed together, with two bright pink and yellow round structures on the right

5. A brain MRI scan as a grid of slices

Twelve axial slices of a T1-weighted brain MRI in two rows of six, from the level of the eye sockets at the top left to the crown of the head at the bottom right, gray brain tissue inside a bright ring of scalp

A magnetic resonance imaging (MRI) scan is a 3-D array, and twelve slices 8 mm apart show this healthy volunteer's brain from the eye sockets to the crown in one grid. The voxels are 1 mm wide and 1.33 mm front to back, so the coordinates are passed in millimeters to keep the head its true shape. Data: OpenNeuro ds000001, subject 01, T1-weighted (CC0).

Tip: for a whole volume, animation_frame=0 gives a slider through every slice, and facet_col_wrap a printable grid of a few.

Get the data from OpenNeuro data set ds000001 (CC0), file sub-01/anat/sub-01_T1w.nii.gz, read with
nibabel and reoriented to RAS, right-anterior-superior (array: x, y, z, voxels of 1.0 x 1.33 x 1.33 mm).
Build a plotly px.imshow of 12 axial slices from index 80 to 146 with facet_col=0 and facet_col_wrap=6,
x and y in millimeters, a gray scale, the front of the head at the top, axes hidden and the slice height
in each facet title.

6. The average handwritten digit, matshow style

Ten 8 by 8 pixel grayscale images in two rows, the pixel-by-pixel average of all the handwritten zeros, ones, twos and so on up to nine; each is still a recognizable digit, with 174 to 183 images averaged per digit

The 1,797 handwritten digits in this classic machine learning data set are 8 by 8 pixels, and averaging each digit pixel by pixel still gives a readable 0 to 9. This is what matplotlib's matshow is for: a small matrix shown as an image so its pattern shows at once. Data: University of California, Irvine (UCI) Optical Recognition of Handwritten Digits (Alpaydin and Kaynak 1998, CC BY 4.0), via scikit-learn's load_digits.

Tip: use a reversed gray scale ("gray_r") so ink is dark on white, as on paper.

Get the data from scikit-learn's load_digits() (UCI Optical Recognition of Handwritten Digits, 1,797
images of 8 x 8 pixels with values 0 to 16) (columns: digit, row, column, value).
Average the images of each digit and build a plotly px.imshow of the 10 x 8 x 8 array with facet_col=0,
facet_col_wrap=5, zmin=0, zmax=16, a reversed gray scale, axes hidden and the number of images in each
facet title.

FAQ

bluesly
x
instagram
youtube
medium
fcaebook

Product

© 2026
Plotly. All rights reserved.
Cookie Preferences
aicpa/soc2