Schematic Topographic
Turn a photo into a topographic map sheet (the contours have nothing to do with your image)
- image
- pixelate_mask
- texture
- height_mask
- image
- overlay_only
- overlay_alpha
Schematic Topographic draws a full cartographic sheet over an image - contour lines, heavier index contours with elevation numbers rotated along the line, depression hachures, spot heights, benchmarks, slope arrows, a coordinate grid, a north arrow, a scale bar. The map-poster look, in milliseconds, on the CPU, with no model and no diffusion pass.
That's why it deserves a slot. Almost every "make it look designed" move in ComfyUI is a generative re-pass: you pay a sampler and gamble on the model not repainting what you liked. This is the other layer - deterministic vector work, wrapped as a node (post-processing.md).
Read this before you turn any dial
By default, the terrain has nothing to do with your photo. height_source defaults to procedural: a seeded fractal landform built from seed, terrain_scale, terrain_octaves, terrain_roughness, terrain_stretch and terrain_angle. The sheet lies over the picture like tracing paper. If you wanted contours that trace a face or a mountain, that's brightness, darkness or detail - or wire a mask into the optional height_mask, which replaces the terrain entirely (white = high ground).
Nudge terrain_scale for ten minutes and the photo still won't change. It was never read.
How it works
The source becomes a height field normalised to 0–1, then Gaussian-blurred by smoothing (sigma in px - your "how calm is the terrain" dial; 0 leaves raw noisy pixels). It's resampled onto a coarser grid at one sample per contour_resolution pixels, and a marching-squares tracer walks that grid at every level z = k × contour_interval between elevation_min and elevation_max, chaining segments into polylines. Every index_every-th level - counted in absolute metres, so 200/400/600 m at 40 m × 5 - is drawn heavier and labelled. Drawing is Pillow at 2× supersampling, which is why strokes come out clean rather than jaggy, and each element has its own seeded random stream, so switching benchmarks off doesn't re-roll your spot heights.
The settings you'll actually touch
Terrain shape first. terrain_scale (420) is the size of the largest landforms in px; terrain_stretch and terrain_angle are what sell it, stretching blobs into ridges and valleys flowing one direction, while terrain_octaves and terrain_roughness add finer wrinkles.
Then density. contour_interval is metres between lines, and the tooltip's arithmetic is the clearest spec you'll get: 0–1000 m at 40 m is 24 lines, at 20 m is 49. If the sheet reads as grey mush, raise the interval first.
Two more worth knowing. contour_text runs a string along the contours glyph by glyph, following the line - the calendar-poster device, a name spelled across the hillsides - with contour_text_on choosing index contours only or all of them. And image_opacity defaults to 0.85, not 1.0 - the photo is deliberately faded under the sheet, so push it to 1.0 if you want the picture to fight the map. map_scale (metres per canvas px) wants a physical value as well; at the default of 1, grid labels are pixel numbers wearing a metre hat.
Outputs
image is the finished composite - background, photo, hypsometric band tone, vector stack, grain - and goes to a SaveImage node. overlay_only is that vector layer minus the photo. overlay_alpha is a 0–1 mask of everywhere ink was drawn, and it's the one people forget: feed it to a downstream mask input to grade or blur only where the map sits. Batches get a sheet per frame from the same terrain, so animation contours stay locked; height_mask and texture use frame 0 for the whole batch.
Install
Nothing to download. ComfyUI Manager → search "ComfyUI Schematic" → install, or:
cd ComfyUI/custom_nodes
git clone https://github.com/jeremieLouvaert/ComfyUI-Schematic
Restart ComfyUI; the node shows up under AKURATE/Schematic. The pack's requirements.txt is Pillow>=9.0, which you already have. No scipy, no models, fonts bundled in the repo - nothing to pin, nothing to break (comfyui-ecosystem.md).
Where people get burned
A flat height field renders contour-free. Push a uniform image through brightness and the node logs a notice and skips every contour, hachure, arrow, spot and benchmark. No levels to trace, so nothing to draw.
Levels cap at 400. A tiny interval across a wide elevation range trips the guard and prints "level cap 400 reached, raise contour_interval" to the console.
Labels need room. At label_spacing 320 px, index contours shorter than that get no elevation label, so small features end up bare. And index_every at 0 means no index contours - hence no labels, and nothing for contour_text to ride on in index mode. Related: if elevation_max ≤ elevation_min the node swaps them and logs it, so you get a sheet, just not the one you asked for.
Cost scales with canvas. contour_resolution 2 is indistinguishable from 1 at default smoothing and twice as fast - stay there unless you're printing. On mixed-resolution batches, size_preset cover-fits every frame to one canvas.
Inputs (66)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| height_source | COMBO | procedural | Where the terrain comes from. Procedural (default) = a seeded landform that has nothing to do with the photo - the sheet is an overlay. Brightness / darkness / detail derive it from the photo instead. |
| terrain_scale | INT | 42040–3000 | Procedural terrain: size (px) of the largest landforms. |
| terrain_octaves | INT | 31–6 | Procedural terrain: detail layers. 1 = broad swells only; more adds finer wrinkles to the contours. |
| terrain_roughness | FLOAT | 0.450.1–0.9 | Procedural terrain: how much each finer layer contributes (gain). |
| terrain_stretch | FLOAT | 1.61–5 | Procedural terrain: elongates landforms along terrain_angle so contours flow as ridges and valleys. 1 = isotropic blobs. |
| terrain_angle | FLOAT | 300–180 | Procedural terrain: direction (degrees) of the ridge/valley flow. |
| smoothing | FLOAT | 10.00–40 | Gaussian blur sigma (px) on the height field before contouring. Higher = broader, calmer contours; 0 = raw pixels (noisy). |
| contour_resolution | INT | 21–8 | Pixels per contour-tracing cell. 1 = finest (slowest); 2 is indistinguishable at default smoothing and twice as fast. |
| elevation_min | FLOAT | 0-10000–10000 | Elevation (m) of the lowest point of the field. Labels, spot heights and benchmarks read in this range. |
| elevation_max | FLOAT | 1000-10000–10000 | Elevation (m) of the highest point of the field. |
| contour_interval | FLOAT | 401–500 | Vertical distance (m) between contour lines - the contour DENSITY. 0-1000 m at 40 m = 24 lines; 20 m = 49. |
| contour_weight | FLOAT | 1.00.1–8 | Stroke width (px) of regular contours; also the base width of hachures, arrows, symbols, grid ticks and marginalia. |
| index_every | INT | 50–20 | Every Nth contour (counted in absolute metres, so 200, 400... at 40 m x 5) is an INDEX contour: heavier and labelled. 0 = none. |
| index_weight | FLOAT | 2.21–5 | Index contour width as a multiple of contour_weight. |
| contour_labels | BOOLEAN | true | Inline elevation labels on index contours, rotated along the line, with the line broken under each label. |
| label_size | INT | 94–48 | Font size (px) of contour labels, spot heights, benchmarks and grid coordinates. |
| label_spacing | INT | 32060–2000 | Distance (px) along an index contour between labels; contours shorter than this get no label. |
| label_unit | STRING | Suffix after every contour label, e.g. ' m'. Empty = bare numbers. | |
| contour_text | STRING | Text repeated along the contours, glyph by glyph, following the line (the calendar-poster device). Empty = off. Replaces the numeric labels on the lines it runs on. | |
| contour_text_on | COMBO | index | Run contour_text on the index contours only, or on every contour. |
| contour_text_size | INT | 114–48 | Font size (px) of contour_text. |
| contour_text_gap | INT | 600–1000 | Gap (px) between repetitions of contour_text along a line. |
| hachures | COMBO | depressions | Short downhill ticks on contours. Depressions = only on closed contours enclosing LOWER ground (the map-sheet convention). All = every contour. |
| hachure_spacing | INT | 102–100 | Distance (px) between hachure ticks along a contour - tick DENSITY. |
| hachure_length | INT | 61–60 | Length (px) of each hachure tick. |
| slope_arrows | BOOLEAN | false | Regular grid of arrows pointing downhill, length proportional to slope (terrain-analysis aspect arrows). |
| arrow_spacing | INT | 9020–400 | Grid pitch (px) of the slope arrows - arrow DENSITY. |
| arrow_length | INT | 224–120 | Length (px) of the steepest arrow; flatter sites draw shorter, near-flat sites draw none. |
| spot_heights | BOOLEAN | true | Mark local peaks (triangle) and pits (circle) with their elevation. |
| spot_count | INT | 120–100 | Maximum spot heights, most prominent first. |
| spot_min_distance | INT | 12010–800 | Minimum spacing (px) between spot heights and benchmarks. |
| symbol_size | INT | 82–40 | Size (px) of the peak triangle, pit circle and benchmark cross. |
| benchmarks | BOOLEAN | true | Survey benchmarks: an x cross with 'BM' + elevation, at seeded positions kept clear of the spot heights. |
| benchmark_count | INT | 40–40 | Number of benchmarks (the only element that uses the seed). |
| grid | COMBO | ticks | Map grid: full lines, edge ticks only, edge ticks + interior crosses (the UTM-sheet look), or a dot at every intersection. |
| grid_spacing | INT | 20020–1000 | Grid pitch (px) - grid DENSITY. |
| grid_weight | FLOAT | 1.00.1–8 | Stroke width (px) of grid lines, ticks and crosses. |
| grid_labels | BOOLEAN | true | Coordinate labels (m) along the bottom and right edges, from map_scale; skipped near the corners to keep the frame text clear. |
| map_scale | FLOAT | 1.000.01–1000 | Metres per canvas pixel. Drives the grid coordinates and the scale bar. |
| band_fill | FLOAT | 0.000–0.5 | Hypsometric plate tone: stepped stroke-colour fill between contours, stronger with elevation. 0 disables. |
| north_arrow | BOOLEAN | true | North arrow with 'N' at the top-right, under the frame text. |
| scale_bar | BOOLEAN | true | Graphic scale bar at the bottom-left (a round 1-2-5 length from map_scale), above the frame text. |
| interval_note | BOOLEAN | true | 'CONTOUR INTERVAL n M' note under the scale bar. |
| marginalia_scale | FLOAT | 1.00.3–4 | Size multiplier for the north arrow, scale bar, interval note offsets and annotation leaders. |
| annotations | STRING | Custom call-outs as 'x,y:TEXT;x,y:TEXT' in canvas pixels: a dot at the point, a 45-degree leader with a horizontal tail, text at the end. | |
| pixel_size | INT | 161–80 | Mosaic block size of the pixelate zones - larger = blockier; 1 disables. Identical to Schematic Overlay's. |
| zone_size | INT | 10010–500 | Half-width of each square pixelation zone |
| pixel_stroke | BOOLEAN | true | Draw an outline around each pixelation zone |
| pixelate_zones | STRING | Pixelation zone centers as 'x,y;x,y;...' in canvas pixels. Union with pixelate_mask centroids. Alters the underlying photo only. | |
| seed | INT | 420–4294967295 | Seed for the procedural terrain, benchmark placement and the grain texture. |
| overlay_opacity | FLOAT | 1.000–1 | Master opacity for every drawn element. |
| image_opacity | FLOAT | 0.850–1 | Fade the background photo under the map sheet. |
| texture_opacity | FLOAT | 0.500–1 | Opacity of the noise overlay (screen blend) |
| frame_text_size | INT | 126–72 | Font size of the 4 corner labels |
| frame_text_tl | STRING | Design & Strategy | Top-left corner frame text |
| frame_text_tr | STRING | AKURATE | Top-right corner frame text |
| frame_text_bl | STRING | Bottom-left corner frame text | |
| frame_text_br | STRING | Bottom-right corner frame text | |
| palette | COMBO | whiteOnDark | Color palette for background and stroke color |
| bg_color | STRING | #0a0a0a | Custom palette background color (hex). Used when palette = custom |
| stroke_color | STRING | #d8d8d8 | Custom palette stroke color (hex). Used when palette = custom |
| size_preset | COMBO | match input | Canvas size. 'match input' keeps the source image dimensions; presets cover-fit (crop-to-fill, centered) the source before resizing |
| pixelate_maskopt | MASK | Optional mask; each 8-connected white region's centroid becomes a pixelation zone center (as Schematic Overlay) | |
| textureopt | IMAGE | Optional texture/noise image, screen-blended over the canvas. If empty and texture_opacity > 0, a procedural grain fallback is used | |
| height_maskopt | MASK | Optional mask used AS the terrain height instead of height_source (white = high). Smoothing still applies. Cover-fitted like the photo when sizes differ; frame 0 used for batches. |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| overlay_only | IMAGE | — |
| overlay_alpha | MASK | — |