{"id":1319,"date":"2026-10-22T07:25:15","date_gmt":"2026-10-22T07:25:15","guid":{"rendered":"https:\/\/helpwithmygisassignment.com\/blog\/?p=1319"},"modified":"2026-10-22T07:25:15","modified_gmt":"2026-10-22T07:25:15","slug":"extracting-building-footprints-opencv-python","status":"publish","type":"post","link":"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/","title":{"rendered":"Extracting Building Footprints from Aerial Imagery Using OpenCV and Python"},"content":{"rendered":"<div><\/div>\n<div>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_89 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Extracting_Building_Footprints_from_Aerial_Imagery_Using_OpenCV_and_Python\" >Extracting Building Footprints from Aerial Imagery Using OpenCV and Python<\/a><ul class='ez-toc-list-level-2' ><li class='ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#1_Introduction\" >1. Introduction<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#2_Environment_setup\" >2. Environment setup<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Install_the_Python_libraries\" >Install the Python libraries<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#3_Step_1_%E2%80%94_Read_the_GeoTIFF_and_preserve_spatial_metadata\" >3. Step 1 \u2014 Read the GeoTIFF and preserve spatial metadata<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#What_should_the_output_show\" >What should the output show?<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#4_Step_2_%E2%80%94_Pre-process_and_enhance_the_imagery\" >4. Step 2 \u2014 Pre-process and enhance the imagery<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Apply_contrast_stretching\" >Apply contrast stretching<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Why_convert_to_both_grayscale_and_HSV\" >Why convert to both grayscale and HSV?<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#5_Step_3_%E2%80%94_Segment_candidate_roofs_using_thresholding_and_Canny_edges\" >5. Step 3 \u2014 Segment candidate roofs using thresholding and Canny edges<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#51_Adaptive_thresholding\" >5.1 Adaptive thresholding<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#52_Detect_edges_with_Canny\" >5.2 Detect edges with Canny<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#53_Clean_and_combine_the_masks\" >5.3 Clean and combine the masks<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#6_Step_4_%E2%80%94_Extract_contours_and_convert_them_into_building_polygons\" >6. Step 4 \u2014 Extract contours and convert them into building polygons<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Configure_candidate_filtering\" >Configure candidate filtering<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Convert_image_coordinates_to_map_coordinates\" >Convert image coordinates to map coordinates<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Understanding_the_output\" >Understanding the output<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#7_Step_5_%E2%80%94_Save_the_building_footprints_to_GeoPackage\" >7. Step 5 \u2014 Save the building footprints to GeoPackage<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Optional_reproject_to_another_CRS\" >Optional: reproject to another CRS<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Export_the_vector_layer\" >Export the vector layer<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#8_Add_diagnostic_images_for_visual_quality_control\" >8. Add diagnostic images for visual quality control<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-22\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#9_Limitations_and_fine-tuning\" >9. Limitations and fine-tuning<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-23\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Problem_1_Tree_shadows_generate_building_candidates\" >Problem 1: Tree shadows generate building candidates<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-24\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Problem_2_Roof_colors_vary_across_the_scene\" >Problem 2: Roof colors vary across the scene<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-25\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Problem_3_Adjacent_buildings_merge_into_one_polygon\" >Problem 3: Adjacent buildings merge into one polygon<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-26\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Problem_4_Footprints_are_jagged_or_excessively_detailed\" >Problem 4: Footprints are jagged or excessively detailed<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-27\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Problem_5_The_process_is_too_slow_for_a_large_orthophoto\" >Problem 5: The process is too slow for a large orthophoto<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-28\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Problem_6_The_GeoPackage_contains_polygons_but_they_are_in_the_wrong_location\" >Problem 6: The GeoPackage contains polygons but they are in the wrong location<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-29\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#10_When_should_you_move_beyond_OpenCV\" >10. When should you move beyond OpenCV?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-30\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#11_Summary\" >11. Summary<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-31\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/#Technical_references\" >Technical references<\/a><\/li><\/ul><\/li><\/ul><\/li><\/ul><\/nav><\/div>\n<h1><span class=\"ez-toc-section\" id=\"Extracting_Building_Footprints_from_Aerial_Imagery_Using_OpenCV_and_Python\"><\/span>Extracting Building Footprints from Aerial Imagery Using OpenCV and Python<span class=\"ez-toc-section-end\"><\/span><\/h1>\n<h2><span class=\"ez-toc-section\" id=\"1_Introduction\"><\/span>1. Introduction<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-1465\" src=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_287r5m287r5m287r-1024x559.jpg\" alt=\"Extracting Building Footprints OpenCV Python workflow diagram showing aerial imagery vectorization\n\" width=\"1024\" height=\"559\" srcset=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_287r5m287r5m287r-1024x559.jpg 1024w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_287r5m287r5m287r-300x164.jpg 300w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_287r5m287r5m287r-768x419.jpg 768w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_287r5m287r5m287r.jpg 1408w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p>Mastering the fundamentals of extracting building footprints OpenCV Python offers a practical way to automate part of the building-mapping workflow before investing in deep learning models. Extracting building footprints with OpenCV and Python GIS tools offers a practical way to automate part of the building-mapping workflow before investing in deep learning models. High-resolution orthophotos contain visible roof edges, color variations, shadows, and geometric patterns that traditional computer vision techniques can use to identify candidate building outlines.<\/p>\n<p>Digitizing thousands of roofs manually is time-consuming, especially when updating cadastral maps, preparing urban land-use inventories, or mapping rapidly expanding settlements. OpenCV provides image-processing algorithms for thresholding, edge detection, morphological operations, and contour extraction. Rasterio preserves the geospatial information stored in aerial imagery, while Shapely and GeoPandas convert detected contours into usable <a href=\"https:\/\/helpwithmygisassignment.com\/blog\/building-real-time-spatial-dashboards-in-qgis-with-geonode-and-postgis\/\">GIS vector features<\/a>.<\/p>\n<p>The workflow in this tutorial follows five stages:<\/p>\n<div>\n<div>\n<div><\/div>\n<div>\n<p>1. Read the orthophoto<\/p>\n<p>Load RGB bands and preserve the raster&#8217;s CRS and affine transform.<\/p>\n<\/div>\n<\/div>\n<div><\/div>\n<div>\n<div><\/div>\n<div>\n<p>2. Enhance the imagery<\/p>\n<p>Apply contrast stretching, grayscale conversion, HSV analysis, and Gaussian blur.<\/p>\n<\/div>\n<\/div>\n<div><\/div>\n<div>\n<div><\/div>\n<div>\n<p>3. Segment candidate roofs<\/p>\n<p>Combine adaptive thresholding, Canny edges, and morphological cleanup.<\/p>\n<\/div>\n<\/div>\n<div><\/div>\n<div>\n<div><\/div>\n<div>\n<p>4. Vectorize the contours<\/p>\n<p>Transform image coordinates into georeferenced Shapely polygons.<\/p>\n<\/div>\n<\/div>\n<div><\/div>\n<div>\n<div><\/div>\n<div>\n<p>5. Export to GeoPackage<\/p>\n<p>Save the extracted footprints as a spatially referenced GIS layer.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p>This is a classical computer vision approach, not a trained building-recognition model. It works best on relatively clear imagery with distinguishable roofs. Trees, roads, parking lots, and shadows can also generate contours, so the result should be treated as a candidate building layer that requires validation.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"2_Environment_setup\"><\/span>2. Environment setup<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<h3><span class=\"ez-toc-section\" id=\"Install_the_Python_libraries\"><\/span>Install the Python libraries<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Create a Python environment and install the required packages:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>python -m venv building_env<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Activate the environment.<\/p>\n<p>On Windows:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>building_env\\Scripts\\activate<\/code><\/pre>\n<\/div>\n<\/div>\n<p>On Linux:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>source building_env\/bin\/activate<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Install the dependencies:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>python -m pip install opencv-python rasterio shapely geopandas numpy<\/code><\/pre>\n<\/div>\n<\/div>\n<p>These libraries serve different purposes:<\/p>\n<div>\n<table>\n<tbody>\n<tr>\n<th>Library<\/th>\n<th>Role in the workflow<\/th>\n<\/tr>\n<tr>\n<td><code>opencv-python<\/code><\/td>\n<td>Image enhancement, thresholding, Canny edges, morphology, and contour detection<\/td>\n<\/tr>\n<tr>\n<td><code>rasterio<\/code><\/td>\n<td>Reading GeoTIFF imagery and accessing georeferencing metadata<\/td>\n<\/tr>\n<tr>\n<td><code>shapely<\/code><\/td>\n<td>Constructing, simplifying, and validating polygon geometries<\/td>\n<\/tr>\n<tr>\n<td><code>geopandas<\/code><\/td>\n<td>Managing spatial features and exporting GeoPackage layers<\/td>\n<\/tr>\n<tr>\n<td><code>numpy<\/code><\/td>\n<td>Image-array operations and contrast stretching<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>You will also need a georeferenced orthophoto, preferably a three-band RGB GeoTIFF with a valid coordinate reference system (CRS).<\/p>\n<p>Important assumption: The following implementation expects the first three raster bands to represent red, green, and blue. Check the imagery&#8217;s band descriptions before processing because some products use different band orders or include additional bands.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"3_Step_1_%E2%80%94_Read_the_GeoTIFF_and_preserve_spatial_metadata\"><\/span>3. Step 1 \u2014 Read the GeoTIFF and preserve spatial metadata<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>An ordinary image contains pixel values. A georeferenced raster additionally relates those pixels to real-world coordinates.<\/p>\n<p>Rasterio exposes the raster&#8217;s CRS, bounds, dimensions, and affine transformation. The affine transformation is essential because OpenCV detects features using image coordinates, whereas a GIS polygon must use map coordinates.<\/p>\n<p>Start by loading the first three bands and inspecting the metadata.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>import rasterioimport numpy as npfrom pathlib import Path# Input orthophoto and output locations.INPUT_TIF = Path(\"aerial_orthophoto.tif\")OUTPUT_GPKG = Path(\"building_footprints.gpkg\")# Read the image and retain its geospatial metadata.with rasterio.open(INPUT_TIF) as src:    if src.count &lt; 3:        raise ValueError(\"The input raster must contain at least 3 bands.\")    if src.crs is None:        raise ValueError(            \"The raster has no CRS. Define its correct CRS before processing.\"        )    # Read the first three bands as RGB candidates.    rgb_raw = src.read([1, 2, 3], out_dtype=\"float32\")    # Rasterio returns (bands, rows, columns).    # Convert to the (rows, columns, channels) shape used by OpenCV.    rgb_raw = np.moveaxis(rgb_raw, 0, -1)    # Capture valid-data pixels to avoid processing nodata areas.    valid_mask = src.dataset_mask() &gt; 0    # Save the spatial metadata for later use.    raster_transform = src.transform    raster_crs = src.crs    raster_bounds = src.bounds    print(\"Raster dimensions:\", src.width, \"x\", src.height)    print(\"Number of bands:\", src.count)    print(\"CRS:\", raster_crs)    print(\"Bounds:\", raster_bounds)    print(\"Affine transform:\", raster_transform)<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<h3><span class=\"ez-toc-section\" id=\"What_should_the_output_show\"><\/span>What should the output show?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The terminal should display the raster dimensions, coordinate reference system, geographic or projected bounds, and affine transformation.<\/p>\n<p>For example, a projected raster might report a CRS such as <code>EPSG:32637<\/code>, depending on the imagery&#8217;s location and source. Do not assign an EPSG code based on this example; use the CRS actually associated with your dataset.<\/p>\n<p>If the raster has no CRS, determine the correct source CRS before proceeding. Assigning an arbitrary CRS will not correctly georeference the results.<\/p>\n<p>The code below assumes the GeoTIFF uses an affine transformation. Imagery relying solely on RPCs or ground control points may need a different coordinate-transformation workflow.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"4_Step_2_%E2%80%94_Pre-process_and_enhance_the_imagery\"><\/span>4. Step 2 \u2014 Pre-process and enhance the imagery<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Raw aerial imagery can contain uneven illumination, subtle roof colors, and texture caused by tiles, corrugated metal, trees, and other objects. Pre-processing can make candidate boundaries more distinguishable.<\/p>\n<p>We will apply three operations:<\/p>\n<ul>\n<li>\n<div>\n<div>\n<p>Contrast stretching: Rescale the image values using the 2nd and 98th percentiles.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Color-space conversion: Create grayscale and HSV representations.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Gaussian blur and local contrast enhancement: Reduce fine-scale noise while preserving larger boundaries.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<h3><span class=\"ez-toc-section\" id=\"Apply_contrast_stretching\"><\/span>Apply contrast stretching<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Contrast stretching is particularly useful when raster values occupy only a small portion of the available display range. The example computes the percentiles independently for each RGB band.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>import cv2def contrast_stretch_rgb(rgb_raw, valid_mask):    \"\"\"    Convert an RGB image to uint8 using percentile-based    contrast stretching for each channel.    \"\"\"    rgb = np.zeros(rgb_raw.shape, dtype=np.uint8)    # Stretch the red, green, and blue channels independently.    for channel in range(3):        values = rgb_raw[:, :, channel][valid_mask]        if values.size == 0:            raise ValueError(\"The raster contains no valid image pixels.\")        # Ignore extreme values when estimating the display range.        low, high = np.percentile(values, [2, 98])        # Protect against constant or nearly constant image bands.        if high &lt;= low:            high = low + 1.0        stretched = (            (rgb_raw[:, :, channel] - low)            * 255.0            \/ (high - low)        )        rgb[:, :, channel] = np.clip(            stretched, 0, 255        ).astype(np.uint8)    # Exclude nodata pixels from subsequent segmentation.    rgb[~valid_mask] = 0    return rgb# Convert the source values into an 8-bit RGB image.rgb = contrast_stretch_rgb(rgb_raw, valid_mask)# Convert RGB into grayscale for intensity-based processing.gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)# HSV separates hue, saturation, and value.hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)# Use the value channel as an alternative intensity representation.value = hsv[:, :, 2]# Reduce small-scale image noise.gray_blur = cv2.GaussianBlur(gray, (5, 5), 0)value_blur = cv2.GaussianBlur(value, (5, 5), 0)# Enhance local contrast in the grayscale image.clahe = cv2.createCLAHE(    clipLimit=2.0,    tileGridSize=(8, 8))gray_enhanced = clahe.apply(gray_blur)<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<h3><span class=\"ez-toc-section\" id=\"Why_convert_to_both_grayscale_and_HSV\"><\/span>Why convert to both grayscale and HSV?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Grayscale represents image intensity and is convenient for detecting roof boundaries through edge detection.<\/p>\n<p>HSV represents color differently from RGB. Its value channel provides another intensity measure, while its hue and saturation channels may be useful for separating roof materials from surrounding vegetation in more specialized workflows.<\/p>\n<p>In this implementation, adaptive thresholding uses the blurred HSV value channel, while Canny detection uses the contrast-enhanced grayscale image.<\/p>\n<p>Expected visual output: The enhanced RGB image should have clearer contrast between some rooftops and their surroundings. The blurred images should retain large roof boundaries while appearing less textured than the original.<\/p>\n<p>Contrast stretching does not remove shadows or establish that a bright or dark object is a building. It simply makes intensity differences easier for the subsequent algorithms to process.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"5_Step_3_%E2%80%94_Segment_candidate_roofs_using_thresholding_and_Canny_edges\"><\/span>5. Step 3 \u2014 Segment candidate roofs using thresholding and Canny edges<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The next stage creates a binary mask in which potential roof regions and their boundaries are highlighted.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"51_Adaptive_thresholding\"><\/span>5.1 Adaptive thresholding<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>A global threshold uses one intensity cutoff for the entire image. That can be unreliable when one part of the orthophoto is shaded while another is brightly illuminated.<\/p>\n<p>Adaptive thresholding computes local thresholds from neighboring pixels, making it more adaptable to variations in illumination.<\/p>\n<p>OpenCV&#8217;s binary and inverse-binary threshold modes also let you favor brighter or darker image regions.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code># Configuration parameters.ADAPTIVE_BLOCK_SIZE = 51ADAPTIVE_C = 5# Use \"dark\" for roofs darker than their local surroundings.# Change to \"bright\" when roofs are generally brighter.ROOF_POLARITY = \"dark\"# Adaptive thresholding requires an odd neighborhood size &gt; 1.block_size = min(    ADAPTIVE_BLOCK_SIZE,    min(value_blur.shape))if block_size % 2 == 0:    block_size -= 1if block_size &lt; 3:    raise ValueError(\"The image is too small for adaptive thresholding.\")if ROOF_POLARITY == \"dark\":    threshold_mode = cv2.THRESH_BINARY_INVelif ROOF_POLARITY == \"bright\":    threshold_mode = cv2.THRESH_BINARYelse:    raise ValueError(\"ROOF_POLARITY must be 'dark' or 'bright'.\")threshold_mask = cv2.adaptiveThreshold(    value_blur,    255,    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,    threshold_mode,    block_size,    ADAPTIVE_C)# Remove detections in nodata regions.threshold_mask[~valid_mask] = 0<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>The polarity setting is important. With <code>dark<\/code>, pixels darker than their local threshold tend to become white in the mask. With <code>bright<\/code>, the opposite pattern is emphasized.<\/p>\n<p>If buildings have white metal roofs, dark tiled roofs, and brightly illuminated concrete roofs in the same scene, a single polarity will not capture all structures reliably. The segmentation parameters will need to be tuned to the imagery.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"52_Detect_edges_with_Canny\"><\/span>5.2 Detect edges with Canny<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Canny edge detection identifies strong image-intensity changes, which may correspond to roof boundaries, walls, roads, vegetation edges, and shadows.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code># Detect edges using the enhanced grayscale image.CANNY_LOW = 50CANNY_HIGH = 150edges = cv2.Canny(    gray_enhanced,    threshold1=CANNY_LOW,    threshold2=CANNY_HIGH)# Exclude invalid raster pixels.edges[~valid_mask] = 0<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>Expected visual output: <code>threshold_mask<\/code> should display candidate regions as white areas on a black background. <code>edges<\/code> should show thin outlines around strong image transitions.<\/p>\n<p>Neither image is a building map yet. Thresholding may highlight roof surfaces without identifying their exact extents, while Canny can return incomplete outlines.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"53_Clean_and_combine_the_masks\"><\/span>5.3 Clean and combine the masks<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Morphological operations help remove small noise, close short gaps, and connect nearby parts of a candidate outline.<\/p>\n<p>Here, opening is applied to the threshold mask, closing is applied to the threshold and edge maps, and the resulting masks are combined.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code># Define compact structuring elements for morphology.kernel3 = cv2.getStructuringElement(    cv2.MORPH_RECT,    (3, 3))kernel5 = cv2.getStructuringElement(    cv2.MORPH_RECT,    (5, 5))# Remove some isolated foreground noise.threshold_clean = cv2.morphologyEx(    threshold_mask,    cv2.MORPH_OPEN,    kernel3,    iterations=1)# Close small gaps in thresholded roof regions.threshold_clean = cv2.morphologyEx(    threshold_clean,    cv2.MORPH_CLOSE,    kernel5,    iterations=2)# Join nearby Canny edge fragments.edges_closed = cv2.morphologyEx(    edges,    cv2.MORPH_CLOSE,    kernel3,    iterations=2)# Slightly expand the edge map so that narrow gaps are easier to join.edge_support = cv2.dilate(    edges_closed,    kernel3,    iterations=1)# Combine threshold regions and edge evidence.combined_mask = cv2.bitwise_or(    threshold_clean,    edge_support)# Final cleanup of the combined candidate mask.combined_mask = cv2.morphologyEx(    combined_mask,    cv2.MORPH_CLOSE,    kernel5,    iterations=1)combined_mask[~valid_mask] = 0<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>The resulting <code>combined_mask<\/code> represents potential building regions. It uses both intensity-based segmentation and edge information to create candidates for contour extraction.<\/p>\n<p>However, combining masks can also connect adjacent buildings or merge roofs with roads and trees. If the resulting image contains large white blocks, reduce the number of closing iterations, shrink the morphology kernels, or adjust the threshold and Canny settings.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"6_Step_4_%E2%80%94_Extract_contours_and_convert_them_into_building_polygons\"><\/span>6. Step 4 \u2014 Extract contours and convert them into building polygons<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>OpenCV&#8217;s <code>findContours()<\/code> extracts boundaries from a binary image. Its <code>RETR_EXTERNAL<\/code> mode retrieves only outermost contours, and <code>CHAIN_APPROX_SIMPLE<\/code> compresses redundant points along straight boundary segments.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<p>The contours use image coordinates <code>(column, row)<\/code>. We must transform these coordinates into the GeoTIFF&#8217;s map coordinate system before constructing GIS polygons.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Configure_candidate_filtering\"><\/span>Configure candidate filtering<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The following filters are intentionally simple. They remove some very small regions and implausibly shaped candidates without assuming that every building is rectangular.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code># Candidate filtering settings, expressed in image pixels.MIN_AREA_PX = 80MAX_AREA_PX = 250_000MIN_RECTANGULARITY = 0.20MIN_BOUNDING_SIDE_PX = 4# Simplify noisy contour boundaries.SIMPLIFY_EPSILON_PX = 1.5contours, hierarchy = cv2.findContours(    combined_mask,    cv2.RETR_EXTERNAL,    cv2.CHAIN_APPROX_SIMPLE)print(\"Raw contours found:\", len(contours))<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p><code>MIN_AREA_PX<\/code> and <code>MAX_AREA_PX<\/code> refer to contour area measured in pixel units. They do not refer to square metres.<\/p>\n<p><code>MIN_RECTANGULARITY<\/code> is calculated as the contour area divided by its bounding-box area. It is a coarse filter, not a reliable building classifier.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Convert_image_coordinates_to_map_coordinates\"><\/span>Convert image coordinates to map coordinates<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Rasterio&#8217;s affine transformation maps pixel coordinates to the raster&#8217;s spatial coordinate system. Its <code>xy()<\/code> method returns pixel centres by default. Because OpenCV&#8217;s contour points refer to sampled image pixels, this example maps <code>(column + 0.5, row + 0.5)<\/code> to the corresponding pixel-centre coordinates.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<p>The following code performs filtering, polygon creation, geometry validation, and optional reprojection.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>import geopandas as gpdfrom shapely.geometry import Polygonfrom shapely.validation import make_valid# Retain only polygonal parts after geometry validation.def polygon_parts(geometry):    \"\"\"Return Polygon components from a valid or repaired geometry.\"\"\"    if geometry.is_empty:        return []    if geometry.geom_type == \"Polygon\":        return [geometry]    if geometry.geom_type in (\"MultiPolygon\", \"GeometryCollection\"):        parts = []        for part in geometry.geoms:            parts.extend(polygon_parts(part))        return parts    return []records = []accepted_contours = []with rasterio.open(INPUT_TIF) as src:    for candidate_id, contour in enumerate(contours, start=1):        # Measure the original contour in pixel space.        area_px = cv2.contourArea(contour)        if not (MIN_AREA_PX &lt;= area_px &lt;= MAX_AREA_PX):            continue        # Bounding rectangle and shape diagnostics.        x, y, width, height = cv2.boundingRect(contour)        if min(width, height) &lt; MIN_BOUNDING_SIDE_PX:            continue<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<h3><span class=\"ez-toc-section\" id=\"Understanding_the_output\"><\/span>Understanding the output<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Each accepted feature includes:<\/p>\n<div>\n<table>\n<tbody>\n<tr>\n<th>Field<\/th>\n<th>Description<\/th>\n<\/tr>\n<tr>\n<td><code>candidate_id<\/code><\/td>\n<td>Identifier of the original OpenCV contour<\/td>\n<\/tr>\n<tr>\n<td><code>area_px<\/code><\/td>\n<td>Area of the contour in pixel units<\/td>\n<\/tr>\n<tr>\n<td><code>rect_ratio<\/code><\/td>\n<td>Contour area divided by bounding-box area<\/td>\n<\/tr>\n<tr>\n<td><code>geometry<\/code><\/td>\n<td>Polygon geometry in the raster&#8217;s map coordinates<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>A single contour can produce more than one polygon if geometry repair returns multiple polygon components.<\/p>\n<p>Important accuracy note: These polygons approximate the boundaries of pixel-level candidates. Mapping contour points to pixel centres does not reconstruct the precise physical roof edge. The resulting boundaries remain limited by raster resolution, image quality, segmentation accuracy, and the selected contour simplification tolerance.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"7_Step_5_%E2%80%94_Save_the_building_footprints_to_GeoPackage\"><\/span>7. Step 5 \u2014 Save the building footprints to GeoPackage<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>GeoPackage is a convenient output format for GIS work because it stores spatial vector data and coordinate reference information in a single file. GeoPandas supports writing layers with <code>to_file()<\/code> and the <code>GPKG<\/code> driver.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Optional_reproject_to_another_CRS\"><\/span>Optional: reproject to another CRS<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The footprints currently use the source raster&#8217;s CRS. That is appropriate when you want to retain the original map coordinates.<\/p>\n<p>If you need to overlay the results on another dataset, define a suitable target CRS first:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code># Example only: replace with the appropriate CRS for your project.TARGET_CRS = Noneif TARGET_CRS is not None:    footprints = footprints.to_crs(TARGET_CRS)<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>A CRS transformation changes the coordinates while preserving the spatial placement of the geometries. Choose the target CRS according to your project area and intended analysis. For area calculations, use an appropriate projected CRS rather than interpreting square degrees as square metres.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Export_the_vector_layer\"><\/span>Export the vector layer<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<div>\n<div>\n<pre dir=\"ltr\"><code># Write the extracted footprints to a GeoPackage.OUTPUT_LAYER = \"building_footprints\"footprints.to_file(    OUTPUT_GPKG,    driver=\"GPKG\",    layer=OUTPUT_LAYER,    index=False)print(\"Output GeoPackage:\", OUTPUT_GPKG.resolve())print(\"Output layer:\", OUTPUT_LAYER)print(\"Features written:\", len(footprints))<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>The output will be <code>building_footprints.gpkg<\/code>, containing the <code>building_footprints<\/code> layer.<\/p>\n<p>Open the file in QGIS and overlay it on the source orthophoto. Inspect whether polygons follow roof edges, whether nearby buildings have merged, and whether non-building objects have been included.<\/p>\n<p>A spatial overlay is particularly important because a layer can be technically valid and correctly georeferenced while still containing many false building detections.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"8_Add_diagnostic_images_for_visual_quality_control\"><\/span>8. Add diagnostic images for visual quality control<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-1466\" src=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_76amnb76amnb76am-1024x559.jpg\" alt=\"Extracting Building Footprints OpenCV Python visualization showing Canny edge detection and contour extraction\" width=\"1024\" height=\"559\" srcset=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_76amnb76amnb76am-1024x559.jpg 1024w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_76amnb76amnb76am-300x164.jpg 300w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_76amnb76amnb76am-768x419.jpg 768w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_76amnb76amnb76am.jpg 1408w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p>Visual inspection helps identify which stage is causing errors before you start changing parameters.<\/p>\n<p>Add the following block after segmentation and contour filtering. It saves intermediate images and an overlay of accepted candidate contours.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code># Save intermediate processing results.cv2.imwrite(\"debug_threshold.png\", threshold_clean)cv2.imwrite(\"debug_edges.png\", edges)cv2.imwrite(\"debug_candidates.png\", combined_mask)# Draw retained contours on the enhanced RGB image.overlay = rgb.copy()cv2.drawContours(    overlay,    accepted_contours,    contourIdx=-1,    color=(255, 0, 0),    thickness=2)# OpenCV's image writer expects BGR channel order.overlay_bgr = cv2.cvtColor(    overlay,    cv2.COLOR_RGB2BGR)cv2.imwrite(\"footprints_preview.png\", overlay_bgr)print(\"Diagnostic images saved.\")<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>Expected outputs:<\/p>\n<div>\n<div>\n<div><\/div>\n<div>\n<p><code>debug_threshold.png<\/code><\/p>\n<p>Displays white regions selected by the cleaned adaptive-threshold mask. Look for buildings that are missing, or surrounding areas that have become connected to roofs.<\/p>\n<\/div>\n<\/div>\n<div>\n<div><\/div>\n<div>\n<p><code>debug_edges.png<\/code><\/p>\n<p>Displays detected image edges. Roof boundaries should be visible, although tree crowns, shadows, and roads may appear as well.<\/p>\n<\/div>\n<\/div>\n<div>\n<div><\/div>\n<div>\n<p><code>debug_candidates.png<\/code><\/p>\n<p>Displays the combined segmentation mask. Extensive white regions often indicate that thresholding or morphology is joining unrelated objects.<\/p>\n<\/div>\n<\/div>\n<div>\n<div><\/div>\n<div>\n<p><code>footprints_preview.png<\/code><\/p>\n<p>Overlays accepted contour boundaries on the enhanced orthophoto. Compare these outlines with actual roofs before accepting the vector dataset.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p>These are descriptions of the expected outputs, not actual results from a particular orthophoto. Their appearance depends on the imagery and parameter settings.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"9_Limitations_and_fine-tuning\"><\/span>9. Limitations and fine-tuning<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<h3><span class=\"ez-toc-section\" id=\"Problem_1_Tree_shadows_generate_building_candidates\"><\/span>Problem 1: Tree shadows generate building candidates<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Dark shadows can be classified in the same way as dark roofs, particularly when using inverse adaptive thresholding. Tree crowns can also have clear boundaries that resemble roof outlines.<\/p>\n<p>What to try: Increase the adaptive block size if local thresholds respond too strongly to tiny variations. Adjust <code>ADAPTIVE_C<\/code>, compare bright and dark polarity, and reduce excessive edge dilation or morphological closing. Where multispectral imagery is available, vegetation indices can provide additional evidence for distinguishing vegetation from rooftops.<\/p>\n<p>These changes may reduce false detections, but no single setting reliably separates every shadow from every roof.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Problem_2_Roof_colors_vary_across_the_scene\"><\/span>Problem 2: Roof colors vary across the scene<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>A neighborhood may contain metal roofs, concrete slabs, red tiles, and dark roofing materials. A single threshold polarity can miss an entire roof class.<\/p>\n<p>What to try: Test the bright and dark threshold settings separately. Examine <code>debug_threshold.png<\/code> and <code>debug_candidates.png<\/code> before selecting a setting. A more advanced classical approach could combine multiple candidate masks and color-based rules, although overlapping detections would then need to be resolved.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Problem_3_Adjacent_buildings_merge_into_one_polygon\"><\/span>Problem 3: Adjacent buildings merge into one polygon<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Closing operations connect short gaps in segmented regions. That is helpful for incomplete boundaries but can merge neighboring buildings when they are close together.<\/p>\n<p>What to try: Reduce the kernel size or number of closing iterations. Inspect whether the merge occurs in the threshold mask or only after combining it with the edge map. Watershed segmentation or distance-transform-based splitting may help in some cases.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Problem_4_Footprints_are_jagged_or_excessively_detailed\"><\/span>Problem 4: Footprints are jagged or excessively detailed<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Image noise and roof textures can produce irregular contours even when the general roof extent is correct.<\/p>\n<p>What to try: Increase <code>SIMPLIFY_EPSILON_PX<\/code> slightly to remove redundant points, or increase the Gaussian blur kernel if the input is noisy. Avoid aggressive simplification because it can distort small buildings and remove important corners.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Problem_5_The_process_is_too_slow_for_a_large_orthophoto\"><\/span>Problem 5: The process is too slow for a large orthophoto<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Large aerial images consume substantial memory when loaded as complete arrays. Several masks and intermediate images can multiply memory use.<\/p>\n<p>What to try: Process the raster in windows or manageable tiles, using each window&#8217;s own geospatial transform to convert pixel contours to map coordinates. Add sufficient overlap between tiles so that buildings crossing tile boundaries are not truncated, and reconcile duplicate or fragmented features in the overlapping zones. The example in this article processes the complete raster at once.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Problem_6_The_GeoPackage_contains_polygons_but_they_are_in_the_wrong_location\"><\/span>Problem 6: The GeoPackage contains polygons but they are in the wrong location<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The most common causes include incorrect source CRS metadata, using the wrong RGB band order, or applying an incorrect transform when converting contour coordinates.<\/p>\n<p>What to try: Check <code>src.crs<\/code>, <code>src.transform<\/code>, and <code>src.bounds<\/code>. Confirm the vector layer&#8217;s CRS in QGIS and compare it with the raster. Never treat raw OpenCV pixel coordinates as longitude and latitude.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"10_When_should_you_move_beyond_OpenCV\"><\/span>10. When should you move beyond OpenCV?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>OpenCV is useful when you want a lightweight, interpretable workflow without preparing a labeled training dataset. However, it primarily detects local image characteristics rather than understanding the object being depicted.<\/p>\n<p>For production-scale building extraction, consider a semantic segmentation model such as U-Net or a building-footprint segmentation model trained on aerial imagery. Such models can learn patterns associated with roofs, vegetation, and roads, including cases where simple color thresholds fail.<\/p>\n<p>A hybrid workflow is also possible: use a segmentation model to identify building regions, then use OpenCV and Shapely to refine boundaries, eliminate small artifacts, simplify geometry, and prepare the results for GIS storage.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"11_Summary\"><\/span>11. Summary<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>This tutorial demonstrated how to extract building footprints from aerial imagery using OpenCV and Python GIS libraries. The workflow reads a georeferenced GeoTIFF, performs contrast stretching and color-space conversion, applies adaptive thresholding and Canny edge detection, cleans candidate masks with morphology, converts contours to Shapely polygons, and exports the results to GeoPackage.<\/p>\n<p>The essential distinction is between detecting image patterns and identifying buildings. The resulting layer can accelerate initial mapping, but its accuracy must be assessed against the original imagery and, where possible, reference footprints.<\/p>\n<p>For practical GIS projects, begin with a small representative section of the orthophoto, tune the parameters, inspect the diagnostic images, and validate the output before processing an entire city or using the layer for cadastral or planning decisions.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Technical_references\"><\/span>Technical references<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<ul>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/docs.opencv.org\/4.x\/df\/d0d\/tutorial_find_contours.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">OpenCV \u2014 Finding contours in your image<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/rasterio.readthedocs.io\/en\/stable\/quickstart.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">Rasterio \u2014 Python Quickstart and georeferencing<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/rasterio.readthedocs.io\/en\/stable\/api\/rasterio.transform.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">Rasterio \u2014 Coordinate transformations<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/shapely.readthedocs.io\/en\/stable\/manual.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">Shapely \u2014 Geometry operations and validation<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/geopandas.org\/en\/latest\/docs\/reference\/api\/geopandas.GeoDataFrame.to_file.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">GeoPandas \u2014 Writing vector data to files<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Extracting Building Footprints from Aerial Imagery Using OpenCV and Python 1. Introduction Mastering the fundamentals of extracting building footprints OpenCV Python offers a practical way to automate part of the building-mapping workflow before investing in deep learning models. Extracting building footprints with OpenCV and Python GIS tools offers a practical way to automate part of &#8230; <a title=\"Extracting Building Footprints from Aerial Imagery Using OpenCV and Python\" class=\"read-more\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/extracting-building-footprints-opencv-python\/\" aria-label=\"Read more about Extracting Building Footprints from Aerial Imagery Using OpenCV and Python\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":1466,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,4],"tags":[],"class_list":["post-1319","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","category-gis-articles"],"_links":{"self":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts\/1319","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/comments?post=1319"}],"version-history":[{"count":4,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts\/1319\/revisions"}],"predecessor-version":[{"id":1467,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts\/1319\/revisions\/1467"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/media\/1466"}],"wp:attachment":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/media?parent=1319"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/categories?post=1319"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/tags?post=1319"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}