📘 CASO PRÁCTICO N° 21.1: DETECCIÓN AUTOMÁTICA DE OMISIONES CATASTRALES MEDIANTE VISIÓN ARTIFICIAL EN WARNES
(Corresponde al Capítulo 21: Inteligencia Artificial para el Catastro — Libro «Catastro Urbano y Rural Municipal 4.0»)
—
🎯 OBJETIVO DE APRENDIZAJE
Desarrollar un pipeline automatizado en Python + QGIS que, a partir de una ortofoto de dron y una capa catastral vectorial del municipio de Warnes, detecte construcciones no declaradas y genere un reporte de posibles omisiones tributarias del Impuesto a la Propiedad de Bienes Inmuebles (IPBI).
🧠 COMPETENCIAS QUE SE DESARROLLAN
1. Procesamiento de imágenes geoespaciales con Python (rasterio, numpy).
2. Análisis vectorial y cruce de capas (geopandas, shapely).
3. Aplicación de técnicas básicas de Visión Artificial (umbralización, morfología matemática).
4. Integración de resultados con QGIS mediante archivos GeoPackage.
5. Interpretación de resultados para la toma de decisiones tributarias municipales.
—
🏙️ ESCENARIO CONTEXTUALIZADO: WARNES
La Unidad de Catastro del GAMW ha recibido denuncias vecinales sobre la Urbanización «Las Palmas» (Zona Norte de Warnes), donde varios propietarios habrían ampliado sus construcciones sin declararlas. El catastro actual tiene 5 años de antigüedad.
Se dispone de:
· Ortofoto RGB de alta resolución (5 cm/píxel) tomada con dron DJI Phantom 4 RTK sobre la urbanización.
· Capa vectorial de predios (GeoPackage) con el área construida declarada en la ficha catastral.
Pregunta clave: ¿Qué predios tienen un área construida detectada en la ortofoto superior al área declarada, sugiriendo una omisión tributaria?
—
💻 REQUISITOS TÉCNICOS
Software necesario
· Python 3.9+ (recomendado Anaconda/Miniconda).
· QGIS 3.34+ (para visualización final).
Librerías Python
«`bash
pip install geopandas rasterio shapely numpy pandas matplotlib scikit-image
«`
Dataset de muestra (simulado para el curso)
El estudiante recibirá una carpeta /datos_warnes/ con:
· ortofoto_las_palmas.tif (Ortofoto RGB georreferenciada, ~50 MB).
· catastro_las_palmas.gpkg (Capa de 50 predios con campo area_construida_decl_m2).
· codigo_catastral.csv (Tabla alfanumérica de fichas).
—
🚀 DESARROLLO PASO A PASO
📌 PASO 1: Preparación del entorno y carga de datos
«`python
# =====================================================
# CASO PRÁCTICO 21.1 – DETECCIÓN DE OMISIONES CATASTRALES
# Municipio de Warnes – Urbanización Las Palmas
# =====================================================
import rasterio
import numpy as np
import geopandas as gpd
import pandas as pd
from shapely.geometry import shape
from skimage import measure, morphology
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings(‘ignore’)
# Rutas de los archivos
ORTOFOTO_PATH = ‘datos_warnes/ortofoto_las_palmas.tif’
CATASTRO_PATH = ‘datos_warnes/catastro_las_palmas.gpkg’
print(«✅ Entorno configurado correctamente.»)
«`
—
📌 PASO 2: Carga y exploración de la ortofoto
«`python
# Abrimos la ortofoto con rasterio
with rasterio.open(ORTOFOTO_PATH) as src:
ortofoto = src.read() # Array 3D: (bandas, filas, columnas)
perfil = src.profile # Metadatos geoespaciales
transform = src.transform # Matriz de transformación affine
bounds = src.bounds # Extensión geográfica
print(f»Dimensiones: {ortofoto.shape}»)
print(f»Resolución espacial: {perfil[‘width’]} x {perfil[‘height’]} píxeles»)
print(f»Resolución del píxel: {abs(transform.a):.2f} m x {abs(transform.e):.2f} m»)
print(f»Área cubierta: ~{((bounds.right-bounds.left)*(bounds.top-bounds.bottom))/10000:.2f} hectáreas»)
# Visualización rápida
plt.figure(figsize=(10, 8))
plt.imshow(np.moveaxis(ortofoto, 0, -1) / 255.0) # RGB normalizado
plt.title(«Ortofoto – Urbanización Las Palmas, Warnes»)
plt.axis(‘off’)
plt.show()
«`
—
📌 PASO 3: Detección de construcciones mediante Visión Artificial
Estrategia didáctica: Usaremos un índice espectral simplificado (similar al NDBI – Normalized Difference Built-up Index) que aprovecha que los techos (calamina, losa, ladrillo) reflejan mucha luz en la banda roja e infrarroja cercana, y poca en la verde.
«`python
# Extraemos las bandas (R, G, B) y normalizamos a [0,1]
R = ortofoto[0].astype(float) / 255.0
G = ortofoto[1].astype(float) / 255.0
B = ortofoto[2].astype(float) / 255.0
# Índice simplificado de área construida (ISA – Índice de Superficie Artificial)
# Los techos suelen tener R alto y G moderado.
ISA = (R – G) / (R + G + 1e-6)
# Umbralización: valores > 0.15 sugieren material construido
umbral = 0.15
binario = ISA > umbral
# Eliminación de ruido con morfología matemática
# 1. Opening: elimina puntos pequeños (árboles sueltos, vehículos)
limpio = morphology.remove_small_objects(binario, min_size=50)
# 2. Closing: rellena huecos dentro de los techos
limpio = morphology.binary_closing(limpio, morphology.disk(3))
print(f»Píxeles clasificados como construcción: {limpio.sum():,}»)
print(f»Área construida detectada: {(limpio.sum() * abs(transform.a) * abs(transform.e)):.2f} m²»)
«`
—
📌 PASO 4: Vectorización de las construcciones detectadas
«`python
# Convertimos los píxeles a polígonos vectoriales
poligonos_construidos = []
for geom, valor in measure.regionprops(measure.label(limpio)):
if geom.area >= 10: # Filtramos objetos menores a 10 m² (ruido residual)
poligonos_construidos.append(geom)
# Creamos un GeoDataFrame con las construcciones detectadas
construcciones_detectadas = []
for region in measure.regionprops(measure.label(limpio), intensity_image=ISA):
if region.area >= 10:
# Convertimos coordenadas de píxel a coordenadas geográficas
coords_píxel = measure.find_contours(limpio.astype(float), 0.5)
for contorno in coords_píxel:
if len(contorno) > 3:
# Transformación píxel -> mundo
coords_geo = rasterio.transform.xy(transform,
contorno[:, 0],
contorno[:, 1])
poligono = shape({
«type»: «Polygon»,
«coordinates»: [list(zip(coords_geo[1], coords_geo[0]))]
})
if poligono.is_valid and poligono.area >= 10:
construcciones_detectadas.append({
‘geometry’: poligono,
‘area_detectada_m2’: poligono.area
})
gdf_construcciones = gpd.GeoDataFrame(construcciones_detectadas, crs=perfil[‘crs’])
print(f»Construcciones detectadas: {len(gdf_construcciones)}»)
«`
—
📌 PASO 5: Carga de la capa catastral declarada
«`python
# Cargamos el catastro declarado por los propietarios
gdf_catastro = gpd.read_file(CATASTRO_PATH, layer=’predios’)
print(f»Predios en catastro: {len(gdf_catastro)}»)
print(«Campos disponibles:», gdf_catastro.columns.tolist())
print(«\nPrimeros 3 registros:»)
print(gdf_catastro[[‘codigo_catastral’, ‘area_terreno_m2’,
‘area_construida_decl_m2’]].head(3))
«`
—
📌 PASO 6: Cruce espacial y cálculo del área construida por predio
«`python
# Aseguramos que ambas capas estén en el mismo sistema de referencia
if gdf_construcciones.crs != gdf_catastro.crs:
gdf_construcciones = gdf_construcciones.to_crs(gdf_catastro.crs)
# Realizamos un «spatial join» para asignar cada construcción a su predio
gdf_cruce = gpd.sjoin(gdf_construcciones,
gdf_catastro[[‘codigo_catastral’, ‘geometry’]],
how=’inner’, predicate=’within’)
# Sumamos el área detectada por cada predio
area_detectada_por_predio = (gdf_cruce.groupby(‘codigo_catastral’)[‘area_detectada_m2’]
.sum()
.reset_index()
.rename(columns={‘area_detectada_m2’: ‘area_detectada_m2’}))
# Fusionamos con la capa catastral original
gdf_analisis = gdf_catastro.merge(area_detectada_por_predio,
on=’codigo_catastral’,
how=’left’)
gdf_analisis[‘area_detectada_m2’] = gdf_analisis[‘area_detectada_m2’].fillna(0)
«`
—
📌 PASO 7: Lógica de negocio — Detección de omisiones
«`python
# Definimos la lógica de detección de omisiones
# Regla: si el área detectada supera en más del 20% al área declarada, es «sospechoso»
TOLERANCIA = 0.20 # 20%
gdf_analisis[‘diferencia_m2’] = (gdf_analisis[‘area_detectada_m2’] –
gdf_analisis[‘area_construida_decl_m2’])
gdf_analisis[‘diferencia_pct’] = (gdf_analisis[‘diferencia_m2’] /
(gdf_analisis[‘area_construida_decl_m2’] + 1e-6)) * 100
# Clasificación del estado
def clasificar_omision(row):
if row[‘area_detectada_m2’] == 0 and row[‘area_construida_decl_m2’] > 0:
return ‘⚠️ Posible demolición no reportada’
elif row[‘diferencia_pct’] > (TOLERANCIA * 100):
return ‘🚨 POSIBLE OMISIÓN TRIBUTARIA’
elif row[‘diferencia_pct’] < -(TOLERANCIA * 100):
return ‘ℹ️ Sobre-declaración (revisar)’
else:
return ‘✅ Conforme’
gdf_analisis[‘estado’] = gdf_analisis.apply(clasificar_omision, axis=1)
# Resumen estadístico
print(«=» * 60)
print(«📊 RESUMEN DE DETECCIÓN DE OMISIONES – WARNES»)
print(«=» * 60)
print(gdf_analisis[‘estado’].value_counts())
print(f»\nTotal de predios con posible omisión: »
f»{(gdf_analisis[‘estado’] == ‘🚨 POSIBLE OMISIÓN TRIBUTARIA’).sum()}»)
«`
—
📌 PASO 8: Generación del reporte y mapa temático
«`python
# Exportamos a GeoPackage para abrir en QGIS
OUTPUT_PATH = ‘datos_warnes/resultado_omisiones_warnes.gpkg’
gdf_analisis.to_file(OUTPUT_PATH, layer=’analisis_predios’, driver=’GPKG’)
gdf_construcciones.to_file(OUTPUT_PATH, layer=’construcciones_detectadas’, driver=’GPKG’)
print(f»\n✅ Resultados guardados en: {OUTPUT_PATH}»)
# Mapa temático
fig, ax = plt.subplots(figsize=(14, 10))
# Colores según estado
colores = {
‘🚨 POSIBLE OMISIÓN TRIBUTARIA’: ‘red’,
‘✅ Conforme’: ‘green’,
‘ℹ️ Sobre-declaración (revisar)’: ‘orange’,
‘⚠️ Posible demolición no reportada’: ‘purple’
}
for estado, color in colores.items():
subset = gdf_analisis[gdf_analisis[‘estado’] == estado]
if len(subset) > 0:
subset.plot(ax=ax, color=color, alpha=0.6, edgecolor=’black’,
label=f»{estado} ({len(subset)})»)
plt.title(«🚨 Detección de Omisiones Catastrales\nUrbanización Las Palmas – Warnes»,
fontsize=14, fontweight=’bold’)
plt.legend(loc=’upper right’, fontsize=9)
plt.axis(‘off’)
plt.tight_layout()
plt.savefig(‘datos_warnes/mapa_omisiones_warnes.png’, dpi=200, bbox_inches=’tight’)
plt.show()
# Tabla resumen para el informe municipal
df_reporte = gdf_analisis[[‘codigo_catastral’, ‘propietario’,
‘area_construida_decl_m2’, ‘area_detectada_m2’,
‘diferencia_m2’, ‘diferencia_pct’, ‘estado’]].copy()
df_reporte = df_reporte.sort_values(‘diferencia_m2’, ascending=False)
df_reporte.to_csv(‘datos_warnes/reporte_omisiones_warnes.csv’, index=False)
print(«\n📋 Reporte CSV generado: datos_warnes/reporte_omisiones_warnes.csv»)
«`
—
📊 INTERPRETACIÓN DE RESULTADOS (Ejemplo simulado)
Código Catastral Área Declarada (m²) Área Detectada (m²) Diferencia Estado
07-03-01-045-012-000 120 185 +65 (+54%) 🚨 OMISIÓN
07-03-01-045-012-005 95 142 +47 (+49%) 🚨 OMISIÓN
07-03-01-045-012-018 150 155 +5 (+3%) ✅ Conforme
07-03-01-045-012-023 110 88 -22 (-20%) ℹ️ Sobre-declaración
Implicación tributaria: Si los 2 predios con omisión pagan IPBI sobre 120 y 95 m² respectivamente, pero realmente tienen 185 y 142 m², el municipio está dejando de recaudar aproximadamente un 40-50% del impuesto correspondiente a esas propiedades.
—
🖥️ PASO 9: Visualización final en QGIS
1. Abrir QGIS → Capa > Añadir capa > Añadir capa vectorial.
2. Seleccionar resultado_omisiones_warnes.gpkg.
3. Cargar ambas capas: analisis_predios y construcciones_detectadas.
4. Aplicar simbología categorizada en analisis_predios usando el campo estado.
5. Superponer la ortofoto original como capa raster de fondo.
6. Activar etiquetas con el codigo_catastral y diferencia_pct.
7. Usar el complemento «Print Layout» para generar el mapa final para el informe municipal.
—
🧩 EXTENSIÓN AVANZADA: Deep Learning con Modelos Pre-entrenados
Para estudiantes avanzados (Capítulo 21.9), se puede reemplazar la umbralización por un modelo de segmentación semántica:
«`python
# Ejemplo conceptual con torchgeo (requiere GPU)
from torchgeo.models import FCN
from torchvision.models import resnet50
# Modelo pre-entrenado en dataset de construcciones (ej: SpaceNet, Inria Aerial)
modelo = FCN(backbone=resnet50(weights=’IMAGENET1K_V2′), in_channels=3, classes=2)
# Entrenar o usar inferencia directa sobre la ortofoto
# Ventana: 256×256 píxeles con traslape del 50%
«`
Ventaja: Detecta construcciones con mayor precisión, incluso bajo sombras o vegetación parcial.
Desventaja: Requiere datos de entrenamiento etiquetados y potencia computacional.
—
❓ PREGUNTAS DE REFLEXIÓN (Autoevaluación)
1. ¿Qué limitaciones tiene el método de umbralización simple frente a techos de colores oscuros o sombreados?
2. ¿Cómo afectaría la presencia de piscinas, canchas deportivas o estacionamientos pavimentados a la detección?
3. ¿Qué margen de error es aceptable legalmente en Warnes para considerar una «omisión» vs. un «error de medición»?
4. ¿Cómo se vincula este resultado con el Agente Autónomo Catastral mencionado en el Capítulo 21?
5. Si fueras el Director de Catastro de Warnes, ¿qué acción administrativa tomarías con los predios marcados en rojo?
—
📦 ENTREGABLE DEL ESTUDIANTE
El estudiante debe presentar un informe técnico que incluya:
1. Portada con identificación del caso (Warnes).
2. Metodología explicada en máximo 1 página.
3. Mapa temático exportado desde QGIS (formato PDF).
4. Tabla resumen de los 5 predios con mayor diferencia detectada.
5. Propuesta de acción (verificación en campo, notificación al contribuyente, actualización de ficha).
6. Código fuente completo y comentado (archivo .py o notebook .ipynb).
—
🔗 VINCULACIÓN CON EL LIBRO
Elemento del caso Capítulo del libro
Ortofoto de dron Cap. 15 (Drones)
Procesamiento con Python Cap. 21 (IA)
Cruce espacial Cap. 13-14 (SIG)
Base de datos de predios Cap. 17 (BD Catastral)
Lógica de omisión Cap. 19 (Catastro Tributario)
Reporte municipal Cap. 25 (Proyecto Final)
—
💡 NOTA PEDAGÓGICA PARA EL DOCENTE
Este caso práctico puede ejecutarse en 4 horas (2h teoría + 2h práctica en laboratorio). Se recomienda:
· Proveer el dataset de muestra con antelación.
· Tener un servidor con Python ya configurado (o usar Google Colab).
· Cerrar la sesión con una simulación de presentación al Concejo Municipal de Warnes, donde los estudiantes defienden sus hallazgos como si fueran técnicos de la alcaldía.