QGIS Processing Plugin¶
Skeleton recipe for wrapping vibeProj as a QGIS Processing algorithm.
This provides a starting point for batch vector reprojection of large
layers. A production plugin should live in a separate repository
(vibeproj-qgis).
Minimal Processing provider¶
A QGIS Processing provider registers one or more algorithms that appear in the Processing Toolbox and can be used in graphical models and batch jobs.
"""vibeProj Processing provider for QGIS.
Drop this file (and the algorithm file below) into
~/.local/share/QGIS/QGIS3/profiles/default/processing/scripts/
or package as a proper plugin with metadata.txt.
"""
from qgis.core import QgsProcessingProvider
class VibeProjectionProvider(QgsProcessingProvider):
def id(self):
return "vibeproj"
def name(self):
return "vibeProj"
def longName(self):
return "vibeProj GPU-Accelerated Reprojection"
def loadAlgorithms(self):
self.addAlgorithm(VibeProjectionBatchReproject())
Batch vector reprojection algorithm¶
The algorithm reads all features from an input layer, extracts coordinates in bulk, transforms them with vibeProj, and writes the output layer.
import numpy as np
from qgis.core import (
QgsCoordinateReferenceSystem,
QgsFeature,
QgsFeatureSink,
QgsField,
QgsFields,
QgsGeometry,
QgsPointXY,
QgsProcessing,
QgsProcessingAlgorithm,
QgsProcessingParameterCrs,
QgsProcessingParameterFeatureSink,
QgsProcessingParameterFeatureSource,
QgsWkbTypes,
)
class VibeProjectionBatchReproject(QgsProcessingAlgorithm):
INPUT = "INPUT"
TARGET_CRS = "TARGET_CRS"
OUTPUT = "OUTPUT"
def name(self):
return "batchreproject"
def displayName(self):
return "Batch Reproject (vibeProj)"
def group(self):
return "Reprojection"
def groupId(self):
return "reprojection"
def createInstance(self):
return VibeProjectionBatchReproject()
def initAlgorithm(self, config=None):
self.addParameter(
QgsProcessingParameterFeatureSource(
self.INPUT, "Input layer",
[QgsProcessing.TypeVectorAnyGeometry],
)
)
self.addParameter(
QgsProcessingParameterCrs(
self.TARGET_CRS, "Target CRS",
defaultValue="EPSG:4326",
)
)
self.addParameter(
QgsProcessingParameterFeatureSink(
self.OUTPUT, "Reprojected layer",
)
)
def processAlgorithm(self, parameters, context, feedback):
from vibeproj import Transformer
source = self.parameterAsSource(parameters, self.INPUT, context)
target_crs = self.parameterAsCrs(
parameters, self.TARGET_CRS, context,
)
# Build the vibeProj transformer from EPSG codes.
src_auth = source.sourceCrs().authid() # e.g. "EPSG:4326"
dst_auth = target_crs.authid()
t = Transformer.from_crs(src_auth, dst_auth)
# Prepare output sink with the target CRS.
(sink, dest_id) = self.parameterAsSink(
parameters, self.OUTPUT, context,
source.fields(), source.wkbType(), target_crs,
)
# --- Bulk coordinate extraction ---
features = list(source.getFeatures())
total = len(features)
if total == 0:
return {self.OUTPUT: dest_id}
# Collect all vertex coordinates from all geometries.
all_x, all_y = [], []
vertex_counts = []
for feat in features:
geom = feat.geometry()
vertices = geom.constGet().coordinateSequence()
count = 0
for ring_group in vertices:
for ring in ring_group:
for pt in ring:
all_x.append(pt.x())
all_y.append(pt.y())
count += 1
vertex_counts.append(count)
# --- Bulk transform ---
x_arr = np.array(all_x, dtype=np.float64)
y_arr = np.array(all_y, dtype=np.float64)
# GPU path via transform_chunked, or CPU fallback (automatic).
out_x, out_y = t.transform_chunked(x_arr, y_arr)
# --- Reconstruct geometries ---
offset = 0
for i, feat in enumerate(features):
if feedback.isCanceled():
break
n = vertex_counts[i]
geom = feat.geometry()
new_geom = _replace_vertices(
geom, out_x[offset:offset + n], out_y[offset:offset + n],
)
offset += n
out_feat = QgsFeature(feat)
out_feat.setGeometry(new_geom)
sink.addFeature(out_feat, QgsFeatureSink.FastInsert)
feedback.setProgress(int((i + 1) / total * 100))
return {self.OUTPUT: dest_id}
def _replace_vertices(geom, new_x, new_y):
"""Replace all vertices in a QgsGeometry with new coordinates.
This is a simplified version that handles points and linestrings.
A production plugin would need to handle all geometry types
(polygons, multi-geometries, curves) and preserve Z/M values.
"""
# For a production plugin, manipulate the QgsAbstractGeometry
# directly rather than round-tripping through WKT.
if geom.type() == QgsWkbTypes.PointGeometry:
return QgsGeometry.fromPointXY(QgsPointXY(new_x[0], new_y[0]))
# Fallback: rebuild from coordinate list.
points = [QgsPointXY(float(new_x[j]), float(new_y[j]))
for j in range(len(new_x))]
if geom.type() == QgsWkbTypes.LineGeometry:
return QgsGeometry.fromPolylineXY(points)
# Polygon and multi-geometry reconstruction is more involved.
# A real plugin should walk the geometry rings properly.
return QgsGeometry.fromPolygonXY([points])
The Python-C++ boundary problem¶
QGIS stores geometries as C++ QgsGeometry objects. Every access from
Python crosses the SIP binding layer:
feat.geometry()copies the C++ geometry into a Python wrapper.Iterating vertices with
coordinateSequence()creates Python objects per-vertex.Reconstructing a geometry from Python coordinates copies back into C++.
This means every coordinate crosses the Python-C++ boundary twice (once to extract, once to reconstruct), regardless of how fast the transform itself is. For a layer with 50M vertices, the extraction and reconstruction loops dominate runtime – the GPU transform in the middle takes a fraction of a millisecond.
There is no public C++ API in QGIS Processing that gives direct access
to coordinate buffers. The QgsGeometry memory layout is internal to
the GEOS/QgsAbstractGeometry hierarchy. Unlike Shapely 2.x (which
exposes get_coordinates() / set_coordinates() for bulk NumPy
access), QGIS has no equivalent batch coordinate API on the Python side.
Practical impact:
Layer size |
Extract (Python loop) |
vibeProj transform |
Reconstruct (Python loop) |
|---|---|---|---|
100K vertices |
~200 ms |
< 1 ms |
~300 ms |
10M vertices |
~20 s |
~3 ms |
~30 s |
The transform is never the bottleneck. The Python loops are.
Mitigation strategies¶
Batch at the layer level, not feature level. Extract all vertices from all features into flat arrays (as shown above), transform once, then redistribute. This at least avoids creating a
Transformerper feature.Use Shapely as an intermediary. If the layer can be exported to GeoPackage or GeoJSON, load it with GeoPandas/Shapely, use the bulk
shapely.get_coordinates()path (see the Shapely recipe and GeoPandas recipe), then re-import.Write a C++ QGIS plugin. A native C++ Processing algorithm could access
QgsAbstractGeometrycoordinate arrays directly, memcpy to a pinned host buffer, and call vibeProj’s CUDA kernels via the C ABI. This eliminates the Python boundary entirely but requires building against the QGIS C++ SDK.
Recommendation: separate vibeproj-qgis repository¶
A proper QGIS plugin requires:
metadata.txtwith version, QGIS min/max version, descriptionPlugin installer/uninstaller hooks
GUI dialogs (optional, but expected for non-Processing plugins)
Dependency bundling (vibeProj, NumPy, optionally CuPy)
Testing against multiple QGIS versions (LTR + latest)
None of this belongs in the vibeProj core repository. Create a separate
vibeproj-qgis repository that depends on vibeproj as a pip
dependency and packages the Processing provider as a standard QGIS
plugin zip.
Suggested structure:
vibeproj-qgis/
metadata.txt
__init__.py # Plugin entry point
provider.py # VibeProjectionProvider
algorithms/
batch_reproject.py # The algorithm above, production-quality
raster_reproject.py # Coordinate grid transform for rasters
tests/
test_batch_reproject.py # Uses qgis.testing fixtures
The plugin should detect CuPy at runtime and fall back to CPU transparently. Users with a GPU get the speedup; users without still get a working plugin backed by vibeProj’s NumPy path.