Zaczekaj na zakończenie renderowania obszaru roboczego przed zapisaniem obrazu

11

Próbuję napisać skrypt, który zapisze renderowanie kilku warstw za pomocą kompozytora map. Problem, z którym się spotykam, polega na tym, że skrypt zapisuje się, zanim qgis zakończy renderowanie wszystkich warstw.

Opierając się na kilku innych odpowiedziach ( 1 , 2 , 3 ), próbowałem użyć iface.mapCanvas.mapCanvasRefreshed.connect()i umieścić zapisywanie obrazu w funkcji, ale wciąż mam ten sam problem - obrazy nie zawierają wszystkich warstw.

Kod, którego używam, a także obrazy tego, jak wygląda okno główne i rendering, są wymienione poniżej.

Zauważyłem, że jeśli mam otwarte okno konsoli i odkomentuję trzy print layerListlinie, program będzie czekał na zakończenie renderowania przed zapisaniem obrazów. Nie jestem pewien, czy wynika to ze zwiększonego czasu przetwarzania, czy też zmienia to sposób działania programu.

Jak poprawnie to zaimplementować, aby wszystkie warstwy były zawarte w obrazie?

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")

Jak to wygląda w głównym oknie QGIS (jest wyświetlana losowa mapa rastrowa): wprowadź opis zdjęcia tutaj

Co jest zapisane: wprowadź opis zdjęcia tutaj

Jako dalsze informacje używam QGIS 2.18.7 na Windows 7

Wschód zachód
źródło
Odniosłem się również do kilku stron internetowych, 1 i 2 . Próbowałem dodać te posty, ale mój przedstawiciel nie jest wystarczająco wysoki
EastWest
W swoim drugim ostatniej linii, spróbuj wymienić mapCanv.mapCanvasRefreshed.connect(custFunc)z mapCanv.renderComplete.connect(custFunc)?
Joseph
@Joseph Niestety, nie miało to znaczenia. Nadal otrzymuję taki sam wynik jak powyżej
EastWest
Być może spróbuj zatwierdzić funkcje dodane do warstwy? (tj layerP .commitChanges().). Chociaż nie rozumiem, dlaczego to powinno pomóc, skoro zapisujesz obraz, ale warto spróbować. W przeciwnym razie, mam nadzieję, że inni mogą doradzić :)
Joseph
@Joseph Próbowałem commitChanges(), ale niestety niestety. Dzieki za sugestie.
EastWest

Odpowiedzi:

5

Pojawiają się tutaj różne problemy

Renderowanie na ekranie a renderowanie do obrazu

Sygnał mapCanvasRefreshedjest emitowany wielokrotnie, gdy płótno jest renderowane na ekran. W przypadku wyświetlania na ekranie daje to szybszą informację zwrotną, która może być przyjemna dla użytkownika, aby zobaczyć, co się dzieje lub pomóc w nawigacji.

W przypadku renderowania poza ekranem, takiego jak zapisywanie do pliku, nie jest to niezawodne (ponieważ pełny obraz będzie dostępny tylko wtedy, gdy rendering był wystarczająco szybki).

Co można zrobić: nie potrzebujemy płótna mapy do renderowania twojego obrazu. Możemy po prostu skopiować QgsMapSettingsz kanwy mapy. Ustawienia te są parametrami wysyłanymi do renderera i określają, co dokładnie i jak dokładnie rzeczy powinny zostać przekonwertowane od wszystkich dostawców danych do obrazu rastrowego.

Rejestr warstw a płótno mapy

Warstwy dodane do rejestru nie kończą się natychmiast na kanwie, ale tylko w następnym uruchomieniu pętli zdarzeń. Dlatego lepiej jest zrobić jedną z następujących dwóch rzeczy

  • Rozpocznij renderowanie obrazu za pomocą timera. QTimer.singleShot(10, render_image)

  • Uruchom QApplication.processEvents()po dodaniu warstwy. To działa, ale jest to niebezpieczne wezwanie do użycia (czasami prowadzi do dziwnych awarii) i dlatego należy go unikać.

Pokaż mi kod

Poniższy kod to robi (nieco dostosowany z QFieldSync , zajrzyj tam, jeśli chcesz bardziej dostosować)

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
źródło
1
Masz pojęcie o tym, że renderCompletesygnał nie działa?
Joseph,
Dzięki za wszystkie informacje! Niestety próbowałem wstawić sugerowany kod do mojego skryptu, całkowicie zastępując sekcję twórcy mapy i nadal mam ten sam problem. Zapisany obraz nie zawiera warstw ani linii punktowych, a tylko wstępnie załadowany raster.
EastWest
Myślę, że sygnał jest emitowany między zakończeniem zadania, a obraz jest rysowany na ekranie. Jest z nim painteremitowany parametr , na którym nadal możesz narysować dodatkowe rzeczy, które skończą na ostatecznym obrazie (i na podstawie których prawdopodobnie możesz również wziąć ostateczny obraz, aby to podejście działało).
Matthias Kuhn
1
To wygląda na rozwiązanie. Dziękuję za twoją pomoc. Jedna krótka uwaga, jeśli ktokolwiek to znajdzie - aby QTimer działał poprawnie, pomiń nawias po render_image, albo python wyświetli ostrzeżenie. Powinien przeczytaćQTimer.singleShot(10, render_image)
EastWest,
Ups, oczywiście. Naprawiono w powyższym kodzie
Matthias Kuhn