Create Centroids and Bounding Geometries in PyQGIS

Two families of derived geometry come up again and again. Representative points reduce a polygon or line to one location: for labelling, for point-in-polygon joins, for density maps, for distance calculations. Bounding geometries do the opposite and wrap features — or groups of features — in a simple enclosing shape: a box for an extent, a rotated rectangle for a building's orientation, a circle for a site's reach, a hull for the territory a set of observations covers. Each variant answers a different question, and choosing the wrong one gives plausible but misleading results.

This recipe belongs to Vector Data Manipulation. It creates centroids, interior points and poles of inaccessibility, compares them, builds bounding boxes, oriented rectangles, minimum circles and hulls per feature and per group, and uses the shapes for measurements such as orientation, elongation and compactness.

Three representative points for one polygonAn L-shaped polygon with three candidate points. The centroid, the geometric centre of mass, falls outside the shape in the notch of the L. The point on surface is guaranteed inside but may sit near an edge. The pole of inaccessibility is the interior point furthest from any edge, the visual centre used for labels.Centre of mass, inside, or visual centre?centroidcentre of mass — can lie outsidepoint on surfacealways inside, may hug an edgepole of inaccessibilityfurthest from edges — label spot

Prerequisites

  • QGIS 3.34 LTR or newer, or the QGIS 4 series.
  • A projected CRS for anything involving distances, areas or angles.

Centroids, interior points and poles

Each representative point has an algorithm for whole layers and a method for single geometries.

import processing
from qgis.core import QgsProject

buildings = QgsProject.instance().mapLayersByName("buildings")[0]

centroids = processing.run("native:centroids", {
    "INPUT": buildings, "ALL_PARTS": False, "OUTPUT": "memory:centroids"})["OUTPUT"]
inside = processing.run("native:pointonsurface", {
    "INPUT": buildings, "ALL_PARTS": False, "OUTPUT": "memory:inside"})["OUTPUT"]
poles = processing.run("native:poleofinaccessibility", {
    "INPUT": buildings, "TOLERANCE": 0.5, "OUTPUT": "memory:poles"})["OUTPUT"]

outside = sum(1 for c, b in zip(centroids.getFeatures(), buildings.getFeatures())
              if not b.geometry().contains(c.geometry()))
print(outside, "of", buildings.featureCount(), "centroids fall outside their building")

Breakdown: All three keep the source attributes. With ALL_PARTS true, multipart features get one point per part instead of one overall. The pole of inaccessibility adds a dist_pole field: the distance from the point to the nearest edge, which doubles as a measure of how "thick" a polygon is. Counting centroids outside their polygon is a quick test of whether centroids are safe for the next step; for compact shapes the count is near zero, for L-shapes, rings and crescents it is not. The geometry methods are centroid(), pointOnSurface() and poleOfInaccessibility(tolerance), as compared in converting geometry types.

Which point for which job

The right point depends on what happens next.

Matching the point to the taskUse centroids for statistics and distances between compact areas, such as population-weighted distances between districts. Use point on surface whenever the point must fall inside its polygon, for example for point-in-polygon joins. Use the pole of inaccessibility for labels and symbols that should look centred. Use all parts when every island of a multipart feature needs its own point.Statistics, joins, labelscentroiddistances, averagescompact shapespoint on surfacejoins, overlaysmust be insidepolelabels, symbolslooks centred

For distances between areas — district to district — centroids are conventional and fine for compact shapes. Whenever the point is later tested against polygons, use points on surface: a centroid outside its polygon joins to the wrong neighbour or to nothing. For labels and symbols, the pole of inaccessibility looks centred to the eye even on awkward shapes; QGIS's own polygon labelling uses it. When several of these steps happen in one workflow, it is fine — and clearer — to compute more than one point per feature.

Bounding boxes and oriented rectangles

The minimum bounding geometry algorithm wraps each feature, or each group, in one of four shapes. The bounding box is aligned with the axes; the oriented rectangle is the smallest rectangle at any rotation.

oriented = processing.run("native:orientedminimumboundingbox", {
    "INPUT": buildings, "OUTPUT": "memory:oriented"})["OUTPUT"]

for f in list(oriented.getFeatures())[:5]:
    elong = f["width"] / f["height"] if f["height"] else 0
    print(f["building_id"], round(f["angle"]), "°", round(max(elong, 1 / elong if elong else 0), 2))

Breakdown: The oriented minimum bounding box adds width, height, angle, area and perimeter fields. The angle gives a building's orientation — useful for checking alignment with streets, or for orienting symbols. The ratio of the long to the short side measures elongation: near 1 for square buildings, high for terraces and sheds. Axis-aligned envelopes (native:boundingboxes) are cheaper and answer a different question — the extent in x and y, as used for spatial indexes and map extents.

Minimum circles and convex hulls

Circles and hulls describe reach and territory. The minimum enclosing circle is the smallest circle containing a feature; the convex hull is the smallest convex polygon around it.

circles = processing.run("native:minimumenclosingcircle", {
    "INPUT": buildings, "SEGMENTS": 72, "OUTPUT": "memory:circles"})["OUTPUT"]

import math
for b, c in list(zip(buildings.getFeatures(), circles.getFeatures()))[:5]:
    compactness = b.geometry().area() / (math.pi * c["radius"] ** 2)
    print(b["building_id"], round(c["radius"], 1), "m radius, fills", f"{compactness:.0%}")

Breakdown: The circle's radius and the ratio of the feature's area to the circle's area give a compactness measure between 0 and 1: a round tower fills most of its circle, a long terrace very little. Hulls (native:convexhull) are the right tool for "how much land does this complex of buildings occupy", including the yards between them. Seventy-two segments make the circle smooth enough for display; the radius is exact regardless.

Bound groups of features

The most powerful use of bounding geometry is per group: the hull around all sightings of each animal, the box around all parcels of each owner, the circle around all stores of each chain.

One hull per groupPoints of two groups, for example observations of two animals. Grouping by the animal id field and computing a convex hull per group gives one polygon per animal, its home range estimate. A concave hull follows the points more closely and excludes large empty areas that a convex hull includes.Group by a field, wrap each groupanimal A: convex hullanimal B: concave hull

sightings = QgsProject.instance().mapLayersByName("collar_fixes")[0]
ranges = processing.run("native:minimumboundinggeometry", {
    "INPUT": sightings, "FIELD": "animal_id", "TYPE": 3,     # 3 = convex hull
    "OUTPUT": "memory:home_ranges"})["OUTPUT"]
for f in ranges.getFeatures():
    print(f["animal_id"], f["count"], "fixes,", round(f["area"] / 1e6, 2), "km²")

concave = processing.run("native:concavehull", {
    "INPUT": sightings, "ALPHA": 0.3, "HOLES": False, "NO_MULTIGEOMETRY": True,
    "OUTPUT": "memory:"})["OUTPUT"]

Breakdown: With a group field, native:minimumboundinggeometry builds one geometry per distinct value and adds count, area and perimeter; type 0 is the envelope, 1 the oriented rectangle, 2 the circle and 3 the convex hull. Convex hulls overstate territories that are shaped like a crescent or include large unused areas. A concave hull (native:concavehull, GEOS-based on recent releases) follows the points more closely; its ALPHA sets how tightly, from 0 (tightest) to 1 (convex). It works on the whole layer, so filter to one group at a time for per-group concave hulls.

Use envelopes for extents, frames and tiles

Axis-aligned boxes are the least interesting shape geometrically and the most useful practically. They define map extents for layouts and atlases, request windows for web services and raster reads, and tiles for splitting large jobs.

from qgis.core import QgsRectangle

districts = QgsProject.instance().mapLayersByName("districts")[0]
boxes = processing.run("native:boundingboxes", {
    "INPUT": districts, "OUTPUT": "memory:district_boxes"})["OUTPUT"]

def padded_extent(geom, pad_fraction=0.05):
    r = QgsRectangle(geom.boundingBox())
    r.grow(max(r.width(), r.height()) * pad_fraction)
    return r

for f in list(districts.getFeatures())[:3]:
    print(f["district"], padded_extent(f.geometry()).toString(0))

Breakdown: native:boundingboxes writes one envelope per feature with width, height and area fields — a quick way to see which features are largest in extent, or to build a coverage layer for an atlas. The padded extent helper is what layout and export scripts usually need: the feature's bounding box grown by a small margin so it is not drawn flush against the frame, the same idea that atlas margins implement. For splitting work into tiles, native:creategrid over the overall extent produces regular boxes; each tile's envelope then becomes a filter rectangle for feature requests or raster windows.

Measure shape with the bounding geometries

Comparing a feature with its bounding shapes gives standard shape indices without any extra libraries.

import math

def shape_metrics(geom):
    obb, area_obb, angle, width, height = geom.orientedMinimumBoundingBox()
    circle = geom.minimalEnclosingCircle()[0]
    return {
        "rectangularity": geom.area() / area_obb if area_obb else 0,
        "elongation": max(width, height) / min(width, height) if min(width, height) else 0,
        "roundness": geom.area() / circle.area() if circle.area() else 0,
        "orientation_deg": angle,
    }

for f in list(buildings.getFeatures())[:3]:
    print(f["building_id"], {k: round(v, 2) for k, v in shape_metrics(f.geometry()).items()})

Breakdown: Rectangularity close to 1 means the feature nearly fills its oriented rectangle — typical of buildings; low values point to complex footprints or digitizing errors. Elongation and roundness complete the picture. orientedMinimumBoundingBox and minimalEnclosingCircle return the shape plus its parameters, so per-feature metrics need no Processing call. These indices make useful attributes for classification, quality checks and morphology studies.

QGIS version compatibility

native:centroids, native:pointonsurface, native:poleofinaccessibility, native:orientedminimumboundingbox, native:minimumenclosingcircle and native:minimumboundinggeometry are available on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. native:concavehull uses GEOS from 3.28 onwards with the parameters shown; earlier releases had a different, slower implementation.

Troubleshooting

  • Joins put points in the wrong polygon. Centroids fell outside their polygons; use points on surface.
  • Hull areas look far too big. Outlying points stretch convex hulls; filter outliers or use a concave hull.
  • Orientation angles jump by 90°. The rectangle's width and height swap for near-square shapes; normalise angles to 0–90 or 0–180 as your use requires.
  • Bounding geometry per group is missing groups. The group field has NULLs; they form their own group or are dropped depending on version.

Conclusion

Pick the representative point by task — centroid for statistics, point on surface for joins, pole of inaccessibility for labels — use oriented rectangles for orientation and elongation, circles and hulls for reach and territory, group by a field to wrap sets of features, and derive shape indices by comparing features with their bounding shapes.

Frequently Asked Questions

Is the centroid the same as the mean of the vertices? No. The centroid is area-weighted; the vertex mean depends on how densely edges were digitized.

Can I get centroids weighted by population? For groups of points, yes: native:meancoordinates with a weight field, as in mean centre and standard distance.

Do lines have centroids? Yes — the length-weighted centre, which may lie off the line; use interpolate(length / 2) for the midpoint on the line.

Which hull for home ranges? Convex hulls are a classic minimum estimate; concave hulls or kernel density contours are more realistic.