{"name": "LiDAR Relief Visualization", "package_name": "lidar_relief", "version": "2.1.2", "experimental": false, "qgis_min": "3.0.0", "qgis_max": "4.99.0", "downloads": 221, "uploaded_by": null, "upload_datetime": "2026-07-25T03:26:33.302803", "changelog": "Version 2.1.2:\nShips the v2.1.1 work, which never reached plugins.qgis.org. No code changes\nbeyond release tooling.\n**Fixed**\n- **A single percent sign in the changelog was silently breaking every\nupload.** `qgis-plugin-ci` injects CHANGELOG.md into `metadata.txt`'s\n`changelog=` field at release time, and the QGIS plugin registry parses that\nfile with configparser's `BasicInterpolation`, which treats the percent sign\nas a control character. One that is not doubled, or followed by an opening\nparenthesis, makes the entire `metadata.txt` unparseable \u2014 so the registry\nrefused the package. Because `qgis-plugin-ci` calls `raise_for_status()`\nwithout ever reading `response.text`, the only symptom was a bare\n`HTTP 400` with no reason given. v2.1.1 failed three times over the phrase\n\"an 84 percent reduction\".\nEvery percent sign is now gone from CHANGELOG.md, and\n`scripts/check_changelog.py` rejects undoubled ones with an explanation, so\n`./test.sh` and the release workflow both catch this before it reaches the\nregistry. A test additionally proves the real changelog survives\ninterpolation, using the same parser the registry uses.\nOlder entries carried the same character for months without incident, only\nbecause they sit below the slice of changelog that `qgis-plugin-ci` injects.\nThe trap was always armed, just out of reach.\n**Added**\n- **Releases now verify they actually reached QGIS users.** A \"successful\"\nrelease job has not been a reliable signal: the v2.1.0 run reported success\nand logged *\"Plugin uploaded on plugins.qgis.org\"*, yet the public repository\ncarried on serving 2.0.22 \u2014 the version was accepted but never approved, so\nit never became installable, and nothing said so. The v2.1.1 upload was then\nrefused with a bare HTTP 400 whose response body `qgis-plugin-ci` discards.\nBoth states were invisible from the workflow.\n`scripts/verify_published.py` now queries the public plugin repository after\nevery release and reports whether the released version is genuinely being\nserved, writing a warning annotation and a run summary. It is advisory\nrather than fatal, because approval is a human queue on the QGIS side and a\nred tick for normal moderation latency would just train everyone to ignore\nit. It runs even when the upload step fails, since that is precisely when\nthe current published state is worth knowing. A network failure reports\n\"unknown\", never \"published\".\n\nVersion 2.1.1:\nDocumentation and packaging release. No algorithm behaviour changes.\n**Fixed**\n- **The user guide was never in the published plugin.** `package.sh` copied\n`CHANGELOG.md` and `USER_GUIDE.md` into the plugin folder before zipping, so\na locally built package contained them \u2014 but CI publishes through\n`qgis-plugin-ci`, which builds its archive with `git archive`, seeing only\ntracked files. Those temporary copies were invisible to it, so every release\nsince 2.0 shipped without the documentation the local script was carefully\nadding, and the two packaging paths silently produced different artefacts.\n`USER_GUIDE.md` is now tracked at `lidar_relief/USER_GUIDE.md` and ships by\nvirtue of being a plugin file; `package.sh` no longer copies anything, so\nboth paths produce the same package. (`CHANGELOG.md` is deliberately not\nshipped as a file \u2014 `qgis-plugin-ci` already injects its content into\n`metadata.txt`, which is what QGIS Plugin Manager renders.)\n- **The plugin icon was a JPEG named `.png`.** 1024\u00d71024 and 590 KB, roughly\nhalf the entire download, for something QGIS draws at about 32 px. Converted\nto a real 256\u00d7256 PNG: 93 KB, a reduction of about 84 percent, with no visible change.\n**Added**\n- **Every algorithm dialog now has a Help button.** All 30 algorithms had\n`shortHelpString`, but none implemented `helpUrl()`, so nothing in QGIS\npointed at the user guide \u2014 a user who never visited the GitHub repository\nhad no route to the documentation at all. Seventeen algorithms deep-link to\ntheir own section of the guide; the rest open it at the top.\n- **A guard against dead help links.** `test_help_urls.py` checks that every\nalgorithm mixes in the help behaviour and that every anchor matches a real\nheading in the shipped guide, so a renamed section fails the suite instead of\nquietly dropping readers at the top of the page. The QGIS smoke test\nadditionally asserts, in a real QGIS runtime, that every registered algorithm\nreturns a help URL and that the guide is present in the installed plugin.\n- **Plugin Manager copy now points somewhere.** The `about=` text suggests the\nContact Sheet as a starting point and names both the in-app Help button and\nthe online guide. The README links to the guide from the top; it never did.\n\nVersion 2.1.0:\nMinor release: two new Processing algorithms, semantic segmentation, radii in\nmetres and provenance sidecars, plus repairs to three features that did not\nwork at all. Horizon-scanning visualisations are roughly twice as fast.\n**Added**\n- **Visualisation Contact Sheet.** A new algorithm renders several\nvisualisations of the same DEM as one labelled multi-panel PNG, so you can\nsee which technique reveals your features before committing to a\nfull-resolution run. The DEM is downsampled first, so a sheet returns in\nseconds; eleven visualisations over a 300\u00d7300 preview take under half a\nsecond. Panels are previews for *choosing* a technique \u2014 each is\ncontrast-stretched independently, so brightness is not comparable between\nthem.\n- **Search radii in metres.** Sky-View Factor, Topographic Openness, ASVF,\nSLRM and RVT Openness now accept their radius in metres as well as pixels,\nand every run reports the radius in both units. Archaeological features have\na real-world size, but a pixel radius does not: 20 px is 20 m on a 1 m DEM\nand only 5 m on the 0.25 m LiDAR common in UK and NL archaeology. Pixels\nremain the default so saved Processing models are unaffected.\n- **Semantic segmentation for AI Feature Detection.** U-Net, SegFormer and\nDeepLab-style models now produce a class-index raster plus per-class polygons\ncarrying area, pixel count and mean confidence \u2014 better suited to archaeology\nthan bounding boxes, since ditches, banks and field systems are linear or\nareal. Model type is detected from the output signature rather than a\nsubstring in an output name.\n- **Provenance sidecars.** Terrain outputs are written with a\n`<output>.lidar-relief.json` recording the plugin version, algorithm, the\nparameters actually used (including the resolved pixel radius, not just the\nmetres typed), source path, source checksum, CRS and cell size. A new\n**Inspect Provenance Record** algorithm reads one back and verifies the\nsource DEM is unchanged, so a result can be audited or regenerated later by\nsomeone else. Writing a sidecar can never fail the run that produced the\nraster.\n**Changed**\n- **Sky-View Factor, Openness and ASVF are about 2\u00d7 faster.** `np.hypot`\naccounted for 1.54 s of the 2.00 s spent on array arithmetic per 1024\u00b2 tile;\nit rescales operands to stay overflow-safe, which is pointless at DEM\nmagnitudes (float32 overflow needs |\u0394z| above ~1.8e19 m against a ~2e4 m\nterrestrial range). Replaced with `sqrt(\u0394z\u00b2 + d\u00b2)`, agreeing to float32\nepsilon. SVF at 16 directions and radius 20 went from 2.30 s to 1.09 s.\n- **Tiled processing runs tiles concurrently.** Roughly 1.4\u20131.7\u00d7 on large DEMs.\nDeliberately only two workers by default: this workload is\nmemory-bandwidth bound, not CPU bound, and measurement showed four workers\ncan be *slower* than two on some shapes, with processes no better than\nthreads. Reads and writes stay on the calling thread because GDAL is not\nthread-safe.\n- **Landscape presets scale with resolution.** Preset distances are now stored\nin metres and converted using the DEM's cell size. They were stored in\npixels, so the \"research-validated\" presets were only valid on a 1 m DEM.\n`get_preset(name)` with no cell size still returns the historical numbers.\n- **Local Dominance renders in degrees** rather than a hard-coded byte scale,\nand is displayed with the standard deviation stretch used by the other\nangular visualisations.\n**Fixed**\n- **Local Dominance produced an all-zero raster.** It returned\n`arctan(...)` in radians \u2014 roughly 0.04\u20130.24 on real terrain \u2014 then\nbyte-scaled with `(v \u2212 0.5) / (1.8 \u2212 0.5) \u00d7 255`, limits that only make\nsense for a degree-scale quantity. Every pixel fell below the 0.5 floor and\nclipped to zero, so one of the advertised visualisations emitted a constant\nraster for anything gentler than a cliff. Found by rendering the new contact\nsheet, where its panel was blank while the other ten showed the test\nearthworks. Its tests passed throughout because the only shaped fixture was\na 45\u00b0 cone steep enough to survive the clipping.\n- **Tiled segmentation normalised each tile in isolation.** Per-tile min/max\nscaling made the same terrain present differently depending on which tile it\nlanded in, and a uniform tile \u2014 flat ground, or the interior of a large\nfeature \u2014 scaled to all zeros regardless of its elevation. Segmentation now\nscales every tile against raster-wide percentiles.\n- **GDAL error messages were unreachable.** Once `gdal.UseExceptions()` is\nactive \u2014 which QGIS sets, and which becomes the default in GDAL 4 \u2014\n`gdal.Open` raises instead of returning `None`, so code checking `if dataset\nis None` never reached its own message about paths and permissions. Opening\nnow raises `ValueError` with the intended guidance under either error mode.\n- **CSF Ground Filter (LAS/LAZ \u2192 DEM) now works.** DEM generation failed on\nevery run with `TypeError: sequence must contain strings`: the interpolation\nstep passed `zfield` as a column index instead of the field *name* GDAL\nrequires, and handed `gdal.Grid` a plain `.xyz` text file, which OGR cannot\nopen as a vector datasource. Ground points are now exposed through a CSV +\nOGR VRT wrapper with a named `z` field. No test covered this path, so the\nfailure survived from 2.0 to 2.0.22; `test_csf_dem_export.py` now guards it.\n- **CSF DEM interpolation no longer smears elevations across data gaps.**\nSwitched from unbounded `invdist` \u2014 which weights every input point for\nevery output cell \u2014 to `invdistnn` with a bounded search radius. Cells with\nno ground point in range are written as nodata instead of being extrapolated,\nand the output declares that nodata value so QGIS masks them.\n- **CSF DEM generation is bounded.** A small cell size over a wide extent\npreviously requested an arbitrarily large raster. Requests above 20 000 cells\nper side now raise a clear error naming the cell size instead of exhausting\nmemory.\n- **GPU acceleration could not complete a single run.** `_shift_array_gpu`\ncomputed the overlap between source and destination as `size` for a positive\nshift and `size \u2212 2\u00d7|shift|` for a negative one, instead of `size \u2212 |shift|`.\nEvery horizon step therefore raised `ValueError: could not broadcast input\narray`, so the CuPy path failed immediately on real CUDA hardware. It now\nmirrors `core.array_utils._shift_array` exactly.\n- **GPU acceleration produced different results from the CPU.** The CuPy\nkernels derived directions with `round(cos \u03b8)` / `round(sin \u03b8)`, which\nquantises every azimuth to one of eight integer directions \u2014 16- and\n32-direction requests silently collapsed to 8 \u2014 and stepped along whole-pixel\nmultiples instead of the supersampled ray the CPU walks. Both GPU kernels now\nconsume the same `_build_horizon_samples` geometry as the NumPy path. The\nexisting equivalence tests only run under CUDA, so this was invisible in CI;\n`test_gpu_parity.py` asserts the shared-geometry contract on any machine.\n- **A broken CUDA install could stop the plugin loading.** `cupy.is_available()`\nraises rather than returning `False` when the driver is missing or\nmismatched, and the import guard only caught `ImportError`. Any probe failure\nnow falls back to NumPy with a logged warning.\n- **Tiled outputs always declare a nodata value.** When the source DEM carried\nno nodata tag, `process_in_tiles` wrote raw NaN into an untagged float band;\nQGIS folded those NaNs into layer statistics and the contrast stretch\ncollapsed. It now falls back to \u22129999, matching `write_array_to_raster`.\n- **Multi-temporal DoD export used a deprecated, no-op CRS call.** The CRS\nblock looped over `[\"crs\", \"transform\"]` without using the loop variable and\ncalled `rio.set_crs()`, which returns a copy and discards the result. Replaced\nwith `rio.write_crs(..., inplace=True)`, clearing the `FutureWarning` the test\nsuite emitted.\n**Added**\n- **GPU acceleration is now reachable from the interface.** The CuPy backend\nshipped in 2.0 but nothing in the plugin ever called it, so the advertised\nfeature did not exist for users. Sky-View Factor and Topographic Openness\ngained a *Use GPU acceleration* checkbox. It never fails a run: without CuPy,\nwith a broken driver, or when SVF noise removal is enabled (CPU-only), the\nalgorithm falls back to NumPy and logs the reason.\n- **DEM geometry validation.** Every tiled algorithm now warns before\nprocessing when the input DEM uses a geographic (lat/lon) CRS \u2014 where cell\nsize is in degrees, making slope, SVF, openness and search radii\nmeaningless \u2014 has no CRS at all, or has non-square pixels, which biases every\ndistance-based result because a single averaged cell size is used.\n- **Last deprecated boolean accessor removed.** `ml_export_algorithm` still\ncalled `parameterAsBool`; the 2.0.22 QGIS 4 / Qt6 pass had converted the\nother call sites to `parameterAsBoolean`.\n**Repository**\n- **Removed the checked-in `published/` snapshot.** It was a copy of the whole\nplugin frozen at v2.0.17, committed once by accident, five versions stale (it\npredated TRI entirely), referenced by no build script or CI workflow, and\nduplicating every symbol in the repository \u2014 it was even being swept into\nunrelated `ruff format` commits. Released builds are on GitHub Releases and\nevery release is tagged. Added to `.gitignore` so it cannot return.\n- **Cleared release ZIPs from the working tree.** Twenty-two build artefacts\n(~18 MB, untracked) that are all reproducible from a git tag or downloadable\nfrom GitHub Releases. Pre-2.0 builds with neither a tag nor a release are the\nonly surviving copies of those versions and were kept in `_archive/`.\n**Testing**\n- **End-to-end LAS \u2192 CSF \u2192 DEM coverage.** `test_csf_integration.py` drives\n`filter_las_file` \u2014 the function the Processing algorithm actually calls \u2014\nover a synthetic wooded site, asserting that the DEM is written, the CRS\ncomes from the LAS header rather than a default, canopy returns are removed,\nand a 1.5 m mound survives filtering.\n- **`test.sh` reports what it cannot test.** It now installs `rvt-py`, `laspy`\nand a `gdal` build pinned to `gdal-config --version`, then names any gating\ndependency still missing. Previously an interpreter without GDAL or rvt-py\nskipped seven modules and reported a green run.\n**Documentation**\n- **USER_GUIDE.md brought up to date for v2.1.** Documents the Contact Sheet,\nradii in metres, semantic segmentation, provenance sidecars and the GPU\ntoggle; adds the two RVT algorithms, which the algorithm table had never\nlisted; and restates the Batch preset radii in metres with a note that\noutput produced by an earlier version on a non-1 m DEM was computed at a\ndifferent scale than those figures imply.\n- **Removed a false capability claim.** The user guide advertised \"Instance\nsegmentation (Mask R-CNN): Returns polygons\" as a supported model type. That\nwas never implemented. The AI section now states exactly which two model\ntypes are supported, how each is detected, and what each produces.\n- **Algorithm count corrected to 31** across README, `metadata.txt` and both\nQGIS smoke tests. The smoke tests assert the count exactly, which is what\ncaught the stale figures \u2014 a provider that fails to register an algorithm\nand a release that forgets to update the docs both surface the same way.\n- **Corrected the Sky-View Factor formula note.** The docstring claimed the\nimplementation deviated from Zak\u0161ek et al. (2011) by using a linear\n`1 \u2212 mean(sin(horizon))` instead of a `sin\u00b2` form. Verified against `rvt-py`\n2.x: the published and reference formula *is* the linear one, and it is what\nthis plugin already computed. Output has always been comparable with other\nRVT installations.\n- Removed the stale \"known SVF horizon rounding bug\" note from the golden\nregression suite \u2014 that bug was fixed by the supersampled ray-cast.\n- Corrected the `rvt_openness` return-range documentation, which contradicted\nitself ([0, 90] / [\u221290, 0] versus the actual Yokoyama [0, 180]).\n- Added the missing CodeDNA header to `core/ruggedness.py`, the only core\nmodule without one.", "external_deps": null, "download_url": "https://plugins.qgis.org/plugins/lidar_relief/version/2.1.2/download/"}