Une Task effectue plusieurs calculs et doit changer la GUI

a marqué ce sujet comme résolu.

Bonjour à tous,

Je développe en JavaFX un petit programme de retouche d'images. J'essaie de faire en sorte que ces retouches n'apparaissent pas d'un coup à l'utilisateur-final, mais de manière progressive : pixel par pixel, afin de rendre visible l'exécution des algorithmes de retouche.

Le souci c'est que cet affichage progressif des pixels retouchés se bloque, parfois en début d'image, parfois au milieu… De plus, également de manière aléatoire, une exception est lancée (ou pas du coup) :pirate: .

Notez que je vous fournis les extraits (pertinents) de code à la fin de ce message, mais aussi que j'ai créé un Gist contenant toutes les sources : https://gist.github.com/anonymous/f978ce6f3157429944ea07b0b7fdc18f

L'affichage/L'animation s'est bloqué.

L'exception retournée est (attention, elle est longue) :

Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException: -1
at java.util.ArrayList.elementData(ArrayList.java:418)

at java.util.ArrayList.get(ArrayList.java:431)

at com.sun.javafx.collections.ObservableListWrapper.get(ObservableListWrapper.java:89)

at com.sun.javafx.collections.VetoableListDecorator.get(VetoableListDecorator.java:306)

at javafx.scene.Parent.updateCachedBounds(Parent.java:1591)

at javafx.scene.Parent.recomputeBounds(Parent.java:1535)

at javafx.scene.Parent.impl_computeGeomBounds(Parent.java:1388)

at javafx.scene.layout.Region.impl_computeGeomBounds(Region.java:3078)

at javafx.scene.Node.updateGeomBounds(Node.java:3577)

at javafx.scene.Node.getGeomBounds(Node.java:3530)

at javafx.scene.Node.getLocalBounds(Node.java:3478)

at javafx.scene.Node.updateTxBounds(Node.java:3641)

at javafx.scene.Node.getTransformedBounds(Node.java:3424)

at javafx.scene.Parent.getChildTransformedBounds(Parent.java:1732)

at javafx.scene.Parent.updateCachedBounds(Parent.java:1596)

at javafx.scene.Parent.recomputeBounds(Parent.java:1535)

at javafx.scene.Parent.impl_computeGeomBounds(Parent.java:1388)

at javafx.scene.layout.Region.impl_computeGeomBounds(Region.java:3078)

at javafx.scene.Node.updateGeomBounds(Node.java:3577)

at javafx.scene.Node.getGeomBounds(Node.java:3530)

at javafx.scene.Node.updateBounds(Node.java:564)

at javafx.scene.Parent.updateBounds(Parent.java:1719)

at javafx.scene.SceneSYMBOLEDOLLARScenePulseListener.pulse(Scene.java:2404)

at com.sun.javafx.tk.Toolkit.lambdaSYMBOLEDOLLARrunPulseSYMBOLEDOLLAR30(Toolkit.java:355)

at java.security.AccessController.doPrivileged(Native Method)

at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:354)

at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:381)

at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:510)

at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:490)

at com.sun.javafx.tk.quantum.QuantumToolkit.lambdaSYMBOLEDOLLARrunToolkitSYMBOLEDOLLAR404(QuantumToolkit.java:319)

at com.sun.glass.ui.InvokeLaterDispatcherSYMBOLEDOLLARFuture.run(InvokeLaterDispatcher.java:95)

at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)

at com.sun.glass.ui.win.WinApplication.lambdaSYMBOLEDOLLARnullSYMBOLEDOLLAR148(WinApplication.java:191)

at java.lang.Thread.run(Thread.java:745)

Solutions que j'ai apportées (aucune n'ayant fonctionné)

J'ai tenté 3 choses pour résoudre ce problème.

Pour commencer, sachez que j'ai 3 classes.

Les trois classes de base (quelle que soit la solution testée)

  1. Launcher.java, qui hérite de Application, qui instancie Gui + fait appel à stage.setScene(gui) + fait appel à stage.show() (du JavaFX quoi, rien de fou !) ;

  2. Gui.java, qui hérite de Scene. Elle gère l'événement "Clic sur le bouton INVERSER L'IMAGE". Auquel cas, elle fait appel à son objet graphicEngine pour exécuter l'algo de retouche.

  3. GraphicEngine.java, qui hérite de Service. Ainsi implémente-t-elle la méthode createTask, qui retourne une Task implémentant elle-même call dans laquelle l'algorithme de retouche est écrit.

Voilà pour l'introduction. Maintenant, voyons plus en détails les 3 solutions que j'ai essayées mais qui n'ont pas fonctionné.

Les trois solutions (aucune n'a fonctionné)

  1. Au début, la classe GraphicEngine, qui contient les algorithmes de retouche, possédait un PixelWriter. L'algorithme de retouche de la Task utilisait PixelWriter et ça suffisait pour afficher les pixels retouchés au fur et à mesure du déroulement de l'algo. M'enfin toujours avec ce blocage problématique :p . On m'a dit qu'il ne fallait pas que le thread-background (donc GraphicEngine) modifie lui-même l'image, mais qu'il ne doit que travailler sur des données. Apparemment ça violerait les règles de concurrence de JavaFX.

  2. Du coup, deuxième solution. J'ai créé une ArrayList de pixels (x, y, Color) dans ma Gui et que je donne, depuis cette dernière, à GraphicEngine. Par ailleurs, GraphicEngine n'utilise plus PixelWriter, ne dessine donc plus rien (donc ne modifie plus l'image), et calcule juste les pixels retouchés. Il ajoute ces derniers à l'ArrayList. Et dans Gui, j'ai utilisé un ChangeListener qui mettait à jour l'image grâce aux pixels contenus dans l'ArrayList. Mais bon, ça n'a pas marché.

  3. Du coup là, à l'instant, j'ai essayé de faire presque la même chose qu'avec l'ArrayList, mais sans elle (parce que je pensais qu'il était sûrement un peu trop lourd de mettre à jour l'image dans Gui en devant parcourir chaque élément de l'ArrayList, tout en sachant que la Task de GraphicEngine ajoutait en même temps de nouveaux pixels dedans !). A la place de l'ArrayList, j'ai un simple pixel (une classe Observable contenant x, y et Color). Gui implémente Observer. Enfin bref le modèle Observer quoi ! :) Donc dès que le pixel est modifié par GraphicEngine (ses 3 composantes), le pixel notifie Gui qui dessine alors CE pixel. La mise à jour de l'image est donc beaucoup plus légère, puisqu'elle se fait pixel par pixel, il n'y a cette fois-ci pas de parcours d'ArrayList (qui n'existe pas, remplacée par le pixel).

Aucune de ces 3 solutions ne m'a permis de résoudre ce gros problème.

Les sources

Les extraits de code que je vais vous montrer sont ceux de la dernière solution.

Launcher.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Launcher extends Application {

    public static void main(String args[]) {
        launch(args);
    }

    public void start(Stage stage) {
        stage.setWidth(700);
        stage.setHeight(700);
        stage.setMaximized(true);

        Gui gui = new Gui(stage, new BorderPane(), 700, 500);
        gui.fill();

        stage.setScene(gui);
        stage.show();
    }

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Gui extends Scene implements Observer {
    public Gui(Stage stage, BorderPane border_pane, int width, int height) {
        this.graphic_engine = new GraphicEngine();
        this.pixel_buffer = new Pixel();
        this.pixel_buffer.addObserver(this);
   }

   private void bindGraphicEngineEvents() {
       this.button_reverse_image.setOnAction(button -> {
           this.graphic_engine.restart();
       });
   }

    public void update(Observable o, Object ob) {
        if(o instanceof Pixel) {
            draw();
        }
    }

    public void draw() {
        this.pixel_writer.setColor(pixel_buffer.x, pixel_buffer.y, pixel_buffer.color);
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Pixel extends Observable {
    public int x, y;
    public Color color;

    public Pixel() {

    }

    public Pixel(int x, int y, Color color) {
        this.x = x;
        this.y = y;
        this.color = color;
    }

    public void setAttributes(int x, int y, Color color) {
        this.x = x;
        this.y = y;
        this.color = color;
        this.setChanged();
        this.notifyObservers(this.color);
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class GraphicEngine extends Service<Void> {
    Pixel pixel_buffer;
    public int image_width, image_height;
    private PixelReader pixel_reader;

    public Task<Void> createTask() {
        return new Task<Void>() {

            protected Void call() {
                double r, g, b;
                for (int x = 0; x < image_width; x++) {
                    for (int y = 0; y < image_height; y++) {
                        r = pixel_reader.getColor(x, y).getRed() * 255;
                        g = pixel_reader.getColor(x, y).getGreen() * 255;
                        b = pixel_reader.getColor(x, y).getBlue() * 255;

                        pixel_buffer.setAttributes(x, y, Color.rgb((int) (255 - r), (int) (255 - g), (int) (255 - b)));
                    }
                }
                return null;
            }
        };
    }

Gist

Enfin, si vous êtes chauds pour essayer mon code, le bidouiller et tout, afin de voir par vous-même ce qui se passe, voici un Gist qui vient de sortir du four : https://gist.github.com/anonymous/f978ce6f3157429944ea07b0b7fdc18f

Voilà, j'espère avoir été clair dans mes explications. Merci d'avance !

+0 -0

Je ne connais pas JavaFX, mais va savoir …

Apparemment, ça plante quand il arrive exactement au milieu de l'image … peux tu vérifier cela ( par exemple en modifiant la boucle :

1
2
3
for (int x = 0; x < image_width/2; x++) {
ou                 for (int x = image_width/2-1 ; x < image_width+1; x++) {
 au lieu de       for (int x = 0; x < image_width; x++) {

Ca peut permettre d'isoler le problème.

Autre point, je suis surpris par ces 3 lignes :

1
2
3
r = pixel_reader.getColor(x, y).getRed() * 255;
g = pixel_reader.getColor(x, y).getGreen() * 255;
b = pixel_reader.getColor(x, y).getBlue() * 255;

Tu es sur qu'il faut multiplier par 255 ?

A ma connaissance, dans à peu près tous les langages, (r,g,b) sont des entiers entre 1 et 255, et pas des multiples de 255. r,g et b pourraient d'ailleurs être déclarés comme des int, et non des doubles.

J'essaierai même : pixel_buffer.setAttributes(x, y, 16777215 - pixel_reader.getRGB(x, y) )

Mais sans garantie de résultat. En fait, si je poste cela, c'est autant pour apprendre que pour t'aider :)

+0 -0

Coucou elegance,

Ca ne plante pas seulement au milieu de l'image, mais souvent avant. En tout cas j'ai trouvé une solution et cette fois elle marche !

Le problème avec tout ce que j'ai essayé jusqu'à présent, c'est qu'en fait le thread-background (enfin le Service/la Task) tentait de modifier l'UI ce qui est impossible. Je partagerai le source un peu plus tard, pour l'instant je fais le nettoyage dans mon projet ! :)

Non non en JavaFX les getters de la classe Color oscillent bien entre 0 et 1 cf. https://docs.oracle.com/javafx/2/api/javafx/scene/paint/Color.html

De tte il n'y a aucun problème avec le traitement de l'image, le souci concernait uniquement l'animation (mais comme je l'ai dit ci-dessus, c'est du passé ! et je vous montrerai le nouveau source un peu plus tard :) ).

Mais merci d'avoir essayé de m'aider !

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte