02Urban Portrait
City as a Face — generative cartographic portrait studio • v0.2.3
1. Overview
02Urban Portrait transforms urban vector infrastructure (roads, buildings, points) into optical portraits. The user loads a portrait image, maps it to a geographic frame, and the plugin styles vector features by luminance values sampled from the image pixels. This is a generative, non-destructive cartographic technique—no source geometry or attributes are ever modified. The plugin applies a rule-based renderer with five luminance tone bands, each mapped to per-preset colours, line widths, and transparencies.
The plugin is a three-tab dock: Set up (data acquisition and frame definition), Portrait (art-direction controls and rendering), and Export (format export and style restoration). A bonus panel in the Set up tab provides a built-in OSM downloader for one-click city data acquisition.
2. Feature Documentation
2.1 Luminance-Based Styling Engine
Image-to-Vector Sampling Pipeline
- Image loading: Supports PNG, JPG, JPEG, TIFF, TIF, WebP, BMP. Auto-scales images larger than 1800×1800 px. Preserves EXIF orientation via
QImageReader.setAutoTransform(True). - Geographic frame: The image aspect ratio is fitted to a geographic rectangle (canvas extent or user-drawn frame) with letterbox/pillarbox centring and no stretching. Can be screen-locked (frame follows pan/zoom) or fixed to geographic coordinates.
- Sampling: For each visible feature within the frame, the plugin computes sampling points by geometry type and sampling mode, maps world coordinates to normalised image coordinates (flipping Y), samples luminance from the downscaled image, and applies auto-contrast stretch, gamma correction, optional inversion, and optional edge emphasis.
- Rendering: A
QgsRuleBasedRendererwith 5 tone rules applies per-preset colours, line widths (deepest tones widest, lightest tones thinnest), and transparencies. A custom QGIS expression functionzero2portrait_luma(layer_id, feature_id)reads from an in-process cache to drive the rule filters.
2.2 Three Sampling Modes
| Mode | Line Sample Points | Polygon Sample Points |
|---|---|---|
| Fast | 1 (midpoint) | 1 (point on surface) |
| Balanced | 3 (20%, 50%, 80%) | 2 (point on surface + centroid) |
| High Quality | 5 (10%, 30%, 50%, 70%, 90%) | 2 (point on surface + centroid) |
2.3 Five Tone Rules
Five equal luminance intervals defined by TONE_BREAKS = (0, 52, 104, 156, 208, 256). Each gets per-preset colour, line width (descending from ~1.55 mm for darkest to ~0.10 mm for lightest), and opacity. When hide_highlights is active, the top band ("Paper") has opacity 0.0, letting the background show through—this is how the Ink Portrait and Sepia Blocks presets create white-space faces.
2.4 Rendering Enhancements
Visual Quality Details
Line underlay: A darker, wider base line (+0.35 mm, darkened by 250) is placed 2 layers behind the main line to make it pop from the background.
Polygon fills: Semi-transparent fills (deepest tones alpha 225, medium alpha 150) with thin outlines.
Point symbols: Small circles with outlines.
Line caps and joins: All lines use round cap and round join styles for organic, hand-drawn quality.
2.5 Built-In OSM Downloader
One-Click City Data Acquisition
Adds an XYZ tile OpenStreetMap basemap (with duplicate detection). Downloads the current canvas view via a single Overpass API query (with custom User-Agent header). Validates that viewport is within 6 km × 6 km and 25 km2. Parses the OSM XML response through QGIS/GDAL OGR driver, splitting into three specialised memory layers: OSM Roads (MultiLineString, filtered by highway tag), OSM Buildings (MultiPolygon, filtered by building tag), and OSM Land Use (MultiPolygon, filtered by landuse tag). Each layer carries osm_id, name, and class fields. Tags are extracted from the other_tags HSTORE string via regex.
3. Art Presets
| Preset | Background | Colour Palette | Character |
|---|---|---|---|
| Ink Portrait | #f5f1e8 (warm paper) | Dark ink tones; widest lines for shadows, highlights hidden | Classic pen-and-ink portrait |
| Neon Night | #050816 (deep space) | Pink / purple / cyan / mint; all bands visible | Cyberpunk / synthwave aesthetic |
| Blueprint | #082f49 (deep navy) | Cyan / teal gradient (reversed); all bands visible | Architectural blueprint |
| Sepia Blocks | #f1e3c6 (warm cream) | Brown / sepia tones; highlights hidden | Antique map / vintage engraving |
| Negative City | #07071a (deep navy) | Indigo / violet gradient (reversed); all bands visible | Film negative / X-ray aesthetic |
4. Image Processing Pipeline
Luminance Enhancement Parameters
- Gamma correction (\(\gamma\), range 0.2–3.0, default 1.0): Applies \(L' = 255 \times (L/255)^{1/\gamma}\) after contrast stretch. Values <1.0 lighten mid-tones; values >1.0 darken them.
- Auto-contrast stretch (default on): Computes robust 1st and 99th percentile luminance boundaries from a 256-bin histogram and stretches the image to full 0–255 range.
- Invert (default off): Reverses the luminance scale (light becomes dark, dark becomes light).
- Edge emphasis (range 0.0–1.0, default 0.25): Computes per-pixel edge intensity via neighbour difference and darkens luminance proportionally. At 0.0, edges have no extra emphasis; at 1.0, strong edges become fully black.
5. Controls Summary
| Tab | Control | Description |
|---|---|---|
| Set up | Add OSM basemap | Inserts a live XYZ tile layer |
| Set up | Download this view | Fetches OSM roads, buildings, and land use for the viewport |
| Set up | Upload picture | Loads a portrait image (PNG, JPG, TIFF, WebP, BMP) |
| Set up | Vector canvas | Multi-select QListWidget of all project vector layers |
| Set up | Use canvas extent / Draw frame | Defines the geographic portrait frame |
| Set up | Screen-locked mask | When on, frame moves with pan/zoom; off = fixed geographic |
| Portrait | Preset | Dropdown: Ink Portrait, Neon Night, Blueprint, Sepia Blocks, Negative City |
| Portrait | Sampling | Dropdown: Fast, Balanced, High Quality |
| Portrait | Gamma | QDoubleSpinBox: 0.2–3.0, step 0.1 |
| Portrait | Edge emphasis | QDoubleSpinBox: 0.0–1.0, step 0.1 |
| Portrait | Layer opacity | QDoubleSpinBox: 0.05–1.0, step 0.05 |
| Portrait | Feature limit | QSpinBox: 500–100,000, step 500, default 10,000 |
| Portrait | Auto contrast / Invert | Checkboxes for percentile stretch and luminance inversion |
| Portrait | Live update | Checkbox: re-samples on pan/zoom with 240 ms debounce |
| Export | Format / DPI / Export | PNG (72–1200 dpi), PDF, or SVG; saves via file dialog |
| Export | Restore styles / Export QML | Returns layers to original renderers; saves one layer's style as .qml |
6. Workflow Guide
- Click the 02Urban Portrait toolbar icon. The dock opens on the right.
- On the Set up tab, add city data. Use the built-in OSM downloader (Add basemap → pan to a neighbourhood → Download this view) or select existing project vector layers.
- Upload a portrait image via the Upload button. A preview thumbnail appears with filename, dimensions, and aspect ratio.
- Define the portrait position: either "Use canvas extent" or "Draw frame" (drag a rectangle on the map). Optionally enable "Screen-locked mask" for frame-following.
- Select vector layers to stylise from the list.
- Switch to the Portrait tab. Choose an art preset, adjust gamma, edge emphasis, and feature limit.
- Click Create portrait. The layers are restyled in place with the rule-based renderer. Pan and zoom with live update enabled to see the portrait react.
- Switch to the Export tab to export as PNG, PDF, or SVG. Use "Restore original styles" to undo all changes. Use "Export QML" to save a layer's portrait style.
7. Technical Notes
- Non-destructive operation: Original renderers are cloned and stored before styling is applied. All operations are reversible via Restore. No source geometry or attributes are modified.
- Custom expression function:
zero2portrait_luma(layer_id, feature_id)is registered at plugin load and removed at unload. This function reads from an in-process dictionary keyed by(layer_id, feature_id). - Live update (240 ms debounce): A
QTimerlimits re-sampling to once per 240 ms during pan/zoom, controlled by themax_featurescap (default 10,000). - Adaptive theme: All UI colours derive from the active
QPalettewith WCAG contrast compliance. The hero card intentionally remains dark for brand identity. Cyan accent switches between#06B6D4(dark theme) and#0891B2(light theme). - Project state persistence: Full settings (image path, frame coords, preset, gamma, edge, opacity, invert, auto-contrast, max features, follow-canvas) serialised as JSON and stored in the QGIS project under key
zero2urbanportrait/state. Re-opening restores state. - Export pipeline: Uses
QgsPrintLayoutwith a single map item filling the page. Page width scales to canvas aspect ratio (120–420 mm). SVG export usesforceVectorOutput = True. - QGIS compatibility: 3.28 through 4.99. Zero external Python dependencies.
8. Literature
- Wood, D. (2010). Rethinking the Power of Maps. Guilford Press.
- Harley, J. B. (1989). Deconstructing the map. Cartographica, 26(2), 1–20. DOI: 10.3138/E635-7827-1757-9T53
- Schumann, H. & Mueller, W. (2000). Visualisierung: Grundlagen und allgemeine Methoden. Springer.
- Brewer, C. A. (2005). Designing Better Maps: A Guide for GIS Users. ESRI Press.
- Tufte, E. R. (1983). The Visual Display of Quantitative Information. Graphics Press.