43 Stimmen

Warum ist putImageData so langsam?

Ich arbeite mit einem relativ großen Canvas, auf dem verschiedene (komplexe) Dinge gezeichnet werden. Dann möchte ich den Zustand des Canvas speichern, damit ich ihn später schnell wieder in den aktuellen Zustand zurücksetzen kann. Dazu verwende ich getImageData und speichere die Daten in einer Variablen. Dann zeichne ich noch etwas auf die Leinwand und setze die Leinwand später mit putImageData wieder in den Zustand zurück, in dem sie sich beim Speichern befand.

Es hat sich jedoch herausgestellt, dass putImageData sehr langsam ist. Es ist sogar langsamer als das Neuzeichnen des gesamten Canvas von Grund auf, was mehrere drawImage-Operationen erfordert, die den größten Teil der Oberfläche abdecken, und über 40.000 lineTo-Operationen, gefolgt von Strichen und Füllungen.

Das Neuzeichnen der ca. 2000 x 5000 Pixel großen Leinwand von Grund auf dauert ca. 170 ms, die Verwendung von putImageData dauert jedoch satte 240 ms. Warum ist putImageData so langsam im Vergleich zum Neuzeichnen des Canvas, obwohl das Neuzeichnen des Canvas das Füllen fast des gesamten Canvas mit drawImage und dann wieder das Füllen von etwa 50% des Canvas mit Polygonen mit lineTo, stroke und fill beinhaltet. Im Grunde wird also jedes einzelne Pixel beim Neuzeichnen mindestens einmal berührt.

Weil drawImage so viel schneller zu sein scheint als putImageData (immerhin dauert der drawImage-Teil des Neuzeichnens der Leinwand weniger als 30 ms). Ich beschloss zu versuchen, den Zustand der Leinwand nicht mit getImageData, sondern mit canvas.toDataURL zu speichern und dann ein Bild aus der Daten-URL zu erstellen, das ich in drawImage einfügen würde, um es auf die Leinwand zu zeichnen. Es stellte sich heraus, dass dieser ganze Vorgang viel schneller ist und nur etwa 35 ms dauert.

Warum ist also putImageData so viel langsamer als die Alternativen (Verwendung von getDataURL oder einfaches Neuzeichnen)? Wie könnte ich die Dinge weiter beschleunigen? Gibt es und wenn, was ist im Allgemeinen der beste Weg, um den Zustand einer Leinwand zu speichern?

(Alle Zahlen wurden mit Firebug von Firefox aus gemessen)

1 Stimmen

Es wäre interessant, wenn Sie irgendwo eine Demonstration Ihres Problems online stellen könnten. In noVNC ( github.com/kanaka/noVNC ) Ich verwende putImageData für viele kleine und mittelgroße Bilddaten-Arrays und ich sehe kein Leistungsproblem mit putImageData. Vielleicht haben Sie ein spezifisches Leistungsproblem, das behoben werden sollte.

0 Stimmen

Hier können Sie es sich ansehen danielbaulig.de/A3O Es wird nicht 100% funktionieren, wenn die Firebug-Konsole ausgeschaltet ist, also stellen Sie sicher, dass Sie sie einschalten. Die geprüfte Version ist diejenige, die putImageData verwendet. Sie können es auslösen, indem Sie auf eine beliebige "Kachel" klicken. Der Puffer-Canvas wird mit putImageData aktualisiert und dann die ausgewählte Kachel "hervorgehoben". In a3o_oo.js sind einige Zeilen auskommentiert, die verwendet werden können, um zwischen der Verwendung von putImageData (current), der Verwendung von getDataURL (die beiden Zeilen, die this.boardBuffer erwähnen) und dem einfachen Neuzeichnen (die drawBoard-Zeile) der Pufferfläche zu wechseln.

0 Stimmen

Tolle Frage und tolle Lösungen. Aber haben Sie jemals den wahren Grund herausgefunden, warum putImageData so langsam ist im Vergleich zu drawImage?

95voto

Daniel Baulig Punkte 10294

Nur ein kleiner Hinweis darauf, wie dies am besten zu bewerkstelligen ist. Ich habe meine Bachelor Thesis über Leistungsstarkes ECMAScript und HTML5 Canvas (pdf, Deutsch; Kennwort: stackoverflow), so dass ich mittlerweile einiges an Fachwissen zu diesem Thema gesammelt habe. Die eindeutig beste Lösung ist die Verwendung mehrerer Canvas-Elemente. Das Zeichnen von einem Canvas auf ein anderes Canvas ist genauso schnell wie das Zeichnen eines beliebigen Bildes auf ein Canvas. Somit ist das "Speichern" des Zustands eines Canvas genauso schnell wie das spätere Wiederherstellen, wenn man zwei Canvas-Elemente verwendet.

Dieser jsPerf-Testfall zeigt die verschiedenen Ansätze und ihre Vor- und Nachteile sehr deutlich auf.

Nur der Vollständigkeit halber, hier wie Sie wirklich sollte es tun:

// setup
var buffer = document.createElement('canvas');
buffer.width = canvas.width;
buffer.height = canvas.height;

// save
buffer.getContext('2d').drawImage(canvas, 0, 0);

// restore
canvas.getContext('2d').drawImage(buffer, 0, 0);

Diese Lösung ist, je nach Browser, bis zu 5000x schneller als diejenige, die die Upvotes erhält.

4 Stimmen

Was ist, wenn Sie viele Zustände in einem Array speichern müssen? Sollte man ein Array mit einer Reihe von Leinwänden erstellen? z.B. var numBuffers = 20; var tmpCan = document.createElement('canvas'); var buffers = [tmpCan]; for (var i = 1, len = numBuffers, i < numBuffers; i++) { buffers.push(tmpCan.cloneNode()); } oder etwas Ähnliches? OR Gibt es eine bessere Lösung?

1 Stimmen

Der jsPerf-Link meldet "Diese Serverless-Funktion ist abgestürzt".

2 Stimmen

@JosephQuinsey Es scheint, dass jsperf.com Probleme hat. Ich bin mir nicht sicher, ob es sich um eine vorübergehende Störung handelt oder ob die Seite endgültig nicht mehr existiert. Sie können den eigentlichen Quellcode, den ich für diese Leistungstests verwendet habe, auch in meiner oben verlinkten Bachelorarbeit finden. Die letzten beiden Seiten im Anhang enthalten den Quellcode für getImageData/putImageData sowie den dazwischen liegenden Canvas-Ansatz.

11voto

Corey Trager Punkte 21897

In Firefox 3.6.8 war ich in der Lage, die Langsamkeit von putImageData zu umgehen, indem ich stattdessen toDataUrl/drawImage verwendete. Für mich arbeitet es schnell genug, dass ich es innerhalb der Behandlung eines mousemove-Ereignisses aufrufen kann:

Zum Sparen:

savedImage = new Image()
savedImage.src = canvas.toDataURL("image/png")

Die wiederherzustellen:

ctx = canvas.getContext('2d')
ctx.drawImage(savedImage,0,0)

1 Stimmen

Ich habe Ihre Antwort erst jetzt bemerkt. In der Tat bin ich derzeit tun es genau die gleiche Weise :) Zusätzlich experimentiere ich gerade mit der Verwendung einer zusätzlichen, versteckten Leinwand als Puffer. Dies sollte die Leistung bei der Erstellung des Puffers erhöhen, die mit toDataURL ziemlich langsam ist, und die Zeichengeschwindigkeit sollte ungefähr gleich bleiben (da drawImage auch ein Canvas-Element als Bild verwenden kann).

4 Stimmen

Mir ist gerade aufgefallen, dass diese Antwort immer wieder hochgestuft wird. Ich schätze diese Antwort, aber die erklärte Lösung ist in Bezug auf die Leistung wirklich schrecklich. Bitte verweisen Sie stattdessen auf die akzeptierte Antwort von mir.

0 Stimmen

Ich versuche, auf eine Leinwand aus Pixeldaten in einem Uint8Clamped-Array mit besserer Leistung als putImageData gespeichert zu zeichnen. Dies scheint ein vielversprechender Weg, das zu erreichen. Ich sollte in der Lage sein, das Uint8Clamped-Array in eine DataURL zu konvertieren, ein Image-Objekt mit dieser DataURL zu füllen und dann dieses Bild auf der Leinwand zu zeichnen. Ich habe es noch nicht getestet, aber ich hoffe, es wird schneller sein als putImageData.

2voto

andrewmu Punkte 13820

Erstens sagen Sie, dass Sie mit Firebug messen. Ich finde tatsächlich, dass Firebug verlangsamt JS Ausführung erheblich, so dass Sie möglicherweise nicht immer gute Zahlen für die Leistung.

In Bezug auf putImageData Ich vermute, das liegt daran, dass die Funktionen ein großes JS-Array mit vielen Number Objekte, die alle auf ihren Bereich (0..255) geprüft und in einen nativen Canvas-Puffer kopiert werden müssen.

Vielleicht, sobald die WebGL ByteArray Typen verfügbar sind, kann diese Art von Dingen schneller gemacht werden.

Es scheint seltsam, dass die Base64-Dekodierung und Dekomprimierung der Daten (mit der PNG-Daten-URL) schneller ist, aber das ruft nur eine JS-Funktion mit einer JS-Zeichenfolge auf, so dass es hauptsächlich nativen Code und Typen verwendet.

0 Stimmen

Da meine Zahlen größtenteils aus der Ausführung von nativem Code stammen, bezweifle ich, dass Firebug eine signifikante Auswirkung auf sie haben wird. Nichtsdestotrotz sprechen wir hier nicht über Bruchteile von Millisekunden, sondern tatsächlich über eine Viertelsekunde für einen einzigen Funktionsaufruf (putImageData). Schlechte Leistung durch das JS-Array könnte sein. Ich werde das überprüfen, indem ich teste, wie schnell JS ein solches Array außerhalb von putImageData handhaben kann (kopieren, manipulieren, etc.).

0 Stimmen

Fortsetzung: Das Entpacken, Dekomprimieren usw. geschieht nicht an dem Punkt, an dem der Zustand der Leinwand wiederhergestellt wird, sondern beim Speichern. Dies geschieht also nur einmal, und ich habe es nicht gemessen, denn wie lange es dauert, den Zustand zu speichern, ist nicht von großer Bedeutung. Der kritische Teil ist die Wiederherstellung des Canvas-Zustands. Zu diesem Zeitpunkt habe ich das Image-Objekt längst erstellt. Wenn also das Image-Objekt seine Daten in einem nativen Puffer enthält, könnte dies tatsächlich die Ursache des Problems sein (oder besser das Fehlen eines solchen für den drawImage-Ansatz).

0 Stimmen

Ich weiß, dass putImageData Die Leistung kann in Ordnung sein (80-100fps mit einem 480x320 Puffer) - aber Sie haben es mit sehr großen Bildern zu tun!

2voto

Timmmm Punkte 76756

Dies liegt vermutlich daran, dass moderne Browser die Hardware-Beschleunigung für <canvas> Elemente, und getImageData() / putImageData() die Übertragung von Bilddaten von der Grafikkarte zum Host oder umgekehrt erfordern. Das ist bekanntermaßen langsam.

Die Verwendung von zwei Leinwänden ist schneller, da alle Daten auf der Grafikkarte bleiben.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X