Espere a que el lienzo termine de renderizarse antes de guardar la imagen

11

Estoy intentando escribir un script que guardará una representación de varias capas usando el compositor de mapas. El problema con el que me encuentro es que el script se guarda antes de que qgis haya terminado de procesar todas las capas.

Basado en varias otras respuestas ( 1 , 2 , 3 ), he intentado usar iface.mapCanvas.mapCanvasRefreshed.connect()y poner la imagen guardada dentro de una función, pero todavía encuentro el mismo problema: las imágenes no incluyen todas las capas.

El código que estoy usando, así como las imágenes de cómo se ven la ventana principal y las representaciones se enumeran a continuación.

Me di cuenta de que si tengo la ventana de la consola abierta y descomento las tres print layerListlíneas, el programa esperará a que finalice el renderizado antes de guardar las imágenes. No estoy seguro de si esto se debe al mayor tiempo de procesamiento o si está cambiando la forma en que se ejecuta el programa.

¿Cómo implemento esto correctamente para que todas las capas estén incluidas en la imagen?

from qgis.core import *
from qgis.utils import *
from qgis.gui import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import os.path

##StackExchange Version=name
##Map_Save_Folder=folder
##Map_Save_Name=string roadmap

# Create save file location
mapName = "%s.png" %Map_Save_Name
outfile = os.path.join(Map_Save_Folder,mapName)
pdfName = "%s.pdf" %Map_Save_Name
outPDF = os.path.join(Map_Save_Folder,pdfName)

# Create point and line layers for later
URIstrP = "Point?crs=EPSG:3035"
layerP = QgsVectorLayer(URIstrP,"pointsPath","memory")
provP = layerP.dataProvider()
URIstrL = "LineString?crs=EPSG:3035"
layerL = QgsVectorLayer(URIstrL,"linePath","memory")
provL = layerL.dataProvider()

# Add points to point layer
feat1 = QgsFeature()
feat2 = QgsFeature()
feat3 = QgsFeature()
feat1.setGeometry(QgsGeometry.fromPoint(QgsPoint(5200000,2600000)))
feat2.setGeometry(QgsGeometry.fromPoint(QgsPoint(5300000,2800000)))
provP.addFeatures([feat1, feat2])

# Add line to line layer
feat3.setGeometry(QgsGeometry.fromPolyline([feat1.geometry().asPoint(),feat2.geometry().asPoint()]))
provL.addFeatures([feat3])

# Set symbology for line layer
symReg = QgsSymbolLayerV2Registry.instance()
metaRegL = symReg.symbolLayerMetadata("SimpleLine")
symLayL = QgsSymbolV2.defaultSymbol(layerL.geometryType())
metaL = metaRegL.createSymbolLayer({'width':'1','color':'0,0,0'})
symLayL.deleteSymbolLayer(0)
symLayL.appendSymbolLayer(metaL)
symRendL = QgsSingleSymbolRendererV2(symLayL)
layerL.setRendererV2(symRendL)

# Set symbology for point layer
metaRegP = symReg.symbolLayerMetadata("SimpleMarker")
symLayP = QgsSymbolV2.defaultSymbol(layerP.geometryType())
metaP = metaRegP.createSymbolLayer({'size':'3','color':'0,0,0'})
symLayP.deleteSymbolLayer(0)
symLayP.appendSymbolLayer(metaP)
symRendP = QgsSingleSymbolRendererV2(symLayP)
layerP.setRendererV2(symRendP)

# Load the layers
QgsMapLayerRegistry.instance().addMapLayer(layerP)
QgsMapLayerRegistry.instance().addMapLayer(layerL)
iface.mapCanvas().refresh()


# --------------------- Using Map Composer -----------------
def custFunc():
    mapComp.exportAsPDF(outPDF)
    mapImage.save(outfile,"png")
    mapCanv.mapCanvasRefreshed.disconnect(custFunc)
    return

layerList = []
for layer in QgsMapLayerRegistry.instance().mapLayers().values():
    layerList.append(layer.id())
#print layerList
#print layerList
#print layerList

mapCanv = iface.mapCanvas()
bound = layerP.extent()
bound.scale(1.25)
mapCanv.setExtent(bound)

mapRend = mapCanv.mapRenderer()
mapComp = QgsComposition(mapRend)
mapComp.setPaperSize(250,250)
mapComp.setPlotStyle(QgsComposition.Print)

x, y = 0, 0
w, h = mapComp.paperWidth(), mapComp.paperHeight()

composerMap = QgsComposerMap(mapComp, x, y, w, h)
composerMap.zoomToExtent(bound)
mapComp.addItem(composerMap)
#mapComp.exportAsPDF(outPDF)

mapRend.setLayerSet(layerList)
mapRend.setExtent(bound)

dpmm = dpmm = mapComp.printResolution() / 25.4
mapImage = QImage(QSize(int(dpmm*w),int(dpmm*h)), QImage.Format_ARGB32)
mapImage.setDotsPerMeterX(dpmm * 1000)
mapImage.setDotsPerMeterY(dpmm * 1000)

mapPaint = QPainter()
mapPaint.begin(mapImage)

mapRend.render(mapPaint)

mapComp.renderPage(mapPaint,0)
mapPaint.end()
mapCanv.mapCanvasRefreshed.connect(custFunc)
#mapImage.save(outfile,"png")

Cómo se ve en la ventana principal de QGIS (hay un mapa ráster aleatorio en el que se muestra): ingrese la descripción de la imagen aquí

Lo que se guarda: ingrese la descripción de la imagen aquí

Como información adicional, estoy usando QGIS 2.18.7 en Windows 7

Este oeste
fuente
También he hecho referencia a varias páginas web, 1 y 2 . Traté de agregar estos para que se publiquen, pero mi representante no es lo suficientemente alto
EastWest
En su segunda última línea, intente reemplazar mapCanv.mapCanvasRefreshed.connect(custFunc)con mapCanv.renderComplete.connect(custFunc)?
Joseph
@Joseph Desafortunadamente, no pareció hacer la diferencia. Todavía obtengo el mismo resultado que el anterior
EastWest
¿Quizás intente confirmar las características que ha agregado a la capa? (es decir layerP .commitChanges()) Aunque no veo por qué debería ayudar, ya que solo está guardando la imagen, pero vale la pena intentarlo, supongo. De lo contrario, espero que otros puedan aconsejar :)
Joseph
@Joseph lo intenté commitChanges(), pero no tuve suerte, desafortunadamente. Gracias por la sugerencia.
EastWest

Respuestas:

5

Aquí surgen diferentes problemas

Renderizado en pantalla vs renderizado en una imagen

La señal mapCanvasRefreshedse emite repetidamente mientras el lienzo se representa en la pantalla. Para la visualización en pantalla, esto proporciona una respuesta más rápida que puede ser agradable para que un usuario vea algo que sucede o ayude en la navegación.

Para el renderizado fuera de pantalla como guardar en un archivo, esto no es confiable (ya que solo tendrá una imagen completa si el renderizado fue lo suficientemente rápido).

Qué se puede hacer: no necesitamos el lienzo del mapa para representar su imagen. Podemos copiarlo QgsMapSettingsdesde el lienzo del mapa. Estas configuraciones son los parámetros que se envían al procesador y definen exactamente qué y exactamente cómo se deben convertir las cosas de todos los proveedores de datos a una imagen ráster.

Registro de capa vs lienzo de mapa

Las capas agregadas al registro no terminan en el lienzo inmediatamente, sino solo en la próxima ejecución del bucle de eventos. Por lo tanto, es mejor hacer una de las siguientes dos cosas

  • Comience la representación de la imagen en un temporizador. QTimer.singleShot(10, render_image)

  • Ejecutar QApplication.processEvents()después de agregar la capa. Esto funciona, pero es una llamada peligrosa de usar (a veces conduce a accidentes extraños) y, por lo tanto, debe evitarse.

Muéstrame el código

El siguiente código hace esto (ligeramente ajustado desde QFieldSync , eche un vistazo allí si está interesado en una mayor personalización)

from PyQt4.QtGui import QImage, QPainter

def render_image():
    size = iface.mapCanvas().size()
    image = QImage(size, QImage.Format_RGB32)

    painter = QPainter(image)
    settings = iface.mapCanvas().mapSettings()

    # You can fine tune the settings here for different
    # dpi, extent, antialiasing...
    # Just make sure the size of the target image matches

    # You can also add additional layers. In the case here,
    # this helps to add layers that haven't been added to the
    # canvas yet
    layers = settings.layers()
    settings.setLayers([layerP.id(), layerL.id()] + layers)

    job = QgsMapRendererCustomPainterJob(settings, painter)
    job.renderSynchronously()
    painter.end()
    image.save('/tmp/image.png')

# If you don't want to add additional layers manually to the
# mapSettings() you can also do this:
# Give QGIS give a tiny bit of time to bring the layers from 
# the registry to the canvas (the 10 ms do not matter, the important
# part is that it's posted to the event loop)

# QTimer.singleShot(10, render_image)
Matthias Kuhn
fuente
1
¿Alguna idea de que la renderCompleteseñal no funciona?
Joseph
¡Gracias por toda la información! Desafortunadamente, he intentado insertar el código sugerido en mi script, reemplazando por completo la sección del compositor de mapas, y todavía encuentro el mismo problema. La imagen guardada no tiene la línea o las capas de puntos incluidas, solo el ráster precargado.
EastWest
Creo que la señal se emite entre el trabajo está completo y la imagen se dibuja en la pantalla. Se painteremite un parámetro con el que aún puede dibujar cosas adicionales que terminarán en la imagen final (y de las cuales probablemente también podría tomar la imagen final para que este enfoque funcione).
Matthias Kuhn el
1
Esto parece la solución. Gracias por toda tu ayuda. Una nota rápida si alguien más termina encontrando esto: para que el QTimer funcione correctamente, omita el paréntesis después de render_image o, de lo contrario, Python lanza una advertencia. Debería leerQTimer.singleShot(10, render_image)
EastWest el
Vaya, por supuesto. Corregido en el código anterior
Matthias Kuhn