Using plugins

Click "Plugins -> Load Plugin" from the Transformer window menu bar. You should be presented with a file browser, select a jar file containing one or more Batchable and/or Plugin classes. The plugin will add a new cascaded menu to the plugins menu, the cascaded menu will contain each "Batchable" class as both a single and a batch item on the "Plugins" sub menu. Any Plugin classes may make additional changes to the user interface, such as adding a new menu.

Writing Batchable and Plugin classes

JPsychomorph handles two plugin interfaces, the main one is Batchable, which allows single and batch processing of images. The second interface, Plugin, is more general, and simply supports a setup and cleanup method. The internal handling of these interface types is different. For Batchable classes a new instance of the class is created each time it is used (in batch or single mode). For Plugin classes a single instance is created on loading and the setup method is called, this instance persists until the programme closes or the plugin is unloaded. A single jar file can contain a mixture of Plugin and Batchable classes, and all will be loaded or unloaded together. If a class implements both interfaces, one instance will be created as the Plugin, and will persist until unloaded, another instance will be created each time for batch or single mode Batchable processing. Any implementing class for either of these classes should have an empty (or no) constructor, if they do not they will not be loaded.

These interfaces are part of the Facemorph library available here:

Documentation for all the classes in the library is available here:

Writing Plugin classes

The Plugin interface supports just two methods, setup and cleanup:

public interface Plugin {
     public boolean setup(PsychoMorphForm psychomorph);
     public boolean cleanup(PsychoMorphForm psychomorph);
}

A very simple example of a Plugin is given below, this simply adds a menu "Boo!" to the Psychomorph window, with one item "Boo!", which when pressed brings up a dialog saying, you guessed, "Boo!".

import Facemorph.psychomorph.Plugin;
import Facemorph.psychomorph.PsychoMorphForm;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;

public class AddMenuTest  extends JMenu implements Plugin {
    PsychoMorphForm psychomorph;
    
    public boolean setup(PsychoMorphForm psychomorph) {
         this.psychomorph = psychomorph;
        JMenuItem anItem = new JMenuItem("Boo!");
        anItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(AddMenuTest.this.psychomorph, "Boo!");
            }
        });

        setText("Boo!");
        add(anItem);
        psychomorph.addMenu(this);

        return true;
    }

    public boolean cleanup(PsychoMorphForm psychomorph) {
        psychomorph.removeMenu(this);
        return true;
    }
}

Writing Batchable classes

To implement a Batchable plugin for JPsychoMorph you need to create a Java class that overrides the Facemorph.Batchable interface:

public interface Batchable {
    public boolean process(ImageZoomPanel izp, boolean single);
    public boolean initialise(PsychoMorphForm psychomorph);
    public void finish();
    public String getName();
    public boolean getReadTemplate();
    public boolean getWriteTemplate();
    public boolean getWriteImage();
}

The main method in the Batchable interface is process, which will be called for each image in the batch. The single parameter is true if the processing is not operating in batch mode, but on a single loaded image. This is useful for things like pushing undo information on the stack, which you may not want to do in batch mode. The ImageZoomPanel allows access to the currently loaded Image and Template.

The initialise method is called once before a batch of images, or before a single image is processed. It allows access to the rest of the system state via the PsychoMorphForm parameter. The finish method is called after all the images in the list have been processed, it is not called when operating on a single image.

The getName method should return the name of the Batchable for inclusion in the Plugins menu on the psychomorph Transform window. Both Batch and single versions of the plugin will be added to the menu.

The three methods getReadTemplate(), getWriteTemplate() and getWriteImage() simply return true or false, to indicate if the template or image should be read before processing or written after processing. The image is always read before processing.

The class must also have a no parameter (default) constructor for initialising. This is called both when the plugin is first loaded into the system and every time the (single or batch) menu item is clicked.

Examples

Full source code including a NetBeans project is available:

Simple example

An example of a simple Batchable is given below. This Batchable checks that the inner lip points are correctly positioned vertically, and swaps corresponding points if needed.

import Facemorph.Template;
import Facemorph.psychomorph.Batchable;
import Facemorph.psychomorph.ImageZoomPanel;
import Facemorph.psychomorph.PsychoMorphForm;
import java.awt.geom.Point2D;

/**
 * Batch corrects the mouth in the "standard" template, by checking if the top -lip is below the bottom lip points and swapping if needed
 */
public class BatchCorrectMouth implements Batchable {

    public boolean process(ImageZoomPanel izp, boolean single) {
       Template tem = izp.getTemplate();
       int[] topMouth = {94, 95, 96, 97, 98}, bottomMouth = {99, 100, 101, 102, 103};

       for (int i=0; i<topMouth.length; i++) {
            Point2D.Float p = tem.getPoint(topMouth[i]);
            Point2D.Float q = tem.getPoint(bottomMouth[i]);
            if (q.y<p.y) {
                float x = p.x, y=p.y;
                p.x=q.x; p.y=q.y;
                q.x=x; q.y=y;
            }
       }
       tem.recalculateContours();
       izp.setTemplate(tem);
       return true;
    }

    public void finish() { }

    public String getName() {
        return "Correct Mouth";
    }

    public boolean initialise(PsychoMorphForm psychomorph) {
      return true;
    }

    public boolean getReadTemplate() {return true;}
    public boolean getWriteTemplate() {return true;}
    public boolean getWriteImage() {return false;}
}

A silly example

Here is a plugin to draw a red nose on a face or faces delineated in the "standard" manner:

import Facemorph.*;
import Facemorph.psychomorph.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;


public class test implements Batchable {
    public boolean process(ImageZoomPanel izp, boolean single) {
        BufferedImage bimg = DelineatorForm.checkBufferedImage(izp.getImage());
        Point2D.Float p = izp.getTemplate().getPoint(55);
        Point2D.Float le = izp.getTemplate().getPoint(0);
        Point2D.Float re = izp.getTemplate().getPoint(1);
        double eyesep = le.distance(re)/6.0;
        Graphics g = bimg.getGraphics();
        g.setColor(Color.red);
        g.fillOval((int)(p.x-eyesep), (int)(p.y-eyesep), (int)(eyesep*2), (int)(eyesep*2));
        izp.setImage(bimg);
        return true;
    }

    public boolean initialise(PsychoMorphForm psychomorph) {
        return true;
    }
    public void finish(){
    }
    public String getName() {
        return "Red Nose";
    }

    public boolean getReadTemplate() {return true;}
    public boolean getWriteTemplate() {return false;}
    public boolean getWriteImage() {return true;}
}

Chimeric transforms

Here is the chimeric transform plugin source code, it uses some built in functionality and gets input from the user:

import Facemorph.Template;
import Facemorph.Transformer;
import Facemorph.psychomorph.Batchable;
import Facemorph.psychomorph.DelineatorForm;
import Facemorph.psychomorph.ImageZoomPanel;
import Facemorph.psychomorph.PsychoMorphForm;
import java.awt.Image;
import java.awt.image.BufferedImage;
import javax.swing.JOptionPane;

/**
 * Chimeric transform implementation
 */
public class BatchChimericTransform implements Batchable {
    PsychoMorphForm morphApp;
    float l1, l2, wid;

    public boolean process(ImageZoomPanel izp, boolean single) {
        BufferedImage leftImage = DelineatorForm.checkBufferedImage(morphApp.getLeftImage());
        Template leftTemplate = morphApp.getLeftTemplate();
        BufferedImage rightImage = DelineatorForm.checkBufferedImage(morphApp.getRightImage());
        Template rightTemplate = morphApp.getRightTemplate();
        BufferedImage img = DelineatorForm.checkBufferedImage(izp.getImage());

        Template tem = izp.getTemplate();
        Template outTem = new Template();

        if (single) {
           morphApp.getDelineator().getImageUndoStack().push(img);
           morphApp.getDelineator().getTemplateUndoStack().push(tem);
           morphApp.getDelineator().getTransformUndoMenuItem().setEnabled(true);
        }
   
        Image outImg = Transformer.transformChimeric(morphApp.getWarpType(), tem, leftTemplate, rightTemplate, outTem,
            img, leftImage, rightImage, l1, l2, wid, true, false);
        izp.setImage(outImg);
        izp.setTemplate(outTem);
       return true;
    }

    public void finish() { }

    public String getName() {
        return "Transform Chimeric";
    }

    public boolean initialise(PsychoMorphForm psychomorph) {
        morphApp = psychomorph;

        String leftStr = JOptionPane.showInputDialog(morphApp.getDelineator(), "Transform level for left side", "1");
        String rightStr = JOptionPane.showInputDialog(morphApp.getDelineator(), "Transform level for right side", "0");
        int w = 50;
        String widthStr = JOptionPane.showInputDialog(morphApp.getDelineator(), "Width of smoothing band", "" + w);
        l1 = Float.parseFloat(leftStr);
        l2 = Float.parseFloat(rightStr);
        wid = Float.parseFloat(widthStr);
        return true;
    }

    public boolean getReadTemplate() {return true;}
    public boolean getWriteTemplate() {return true;}
    public boolean getWriteImage() {return true;}
}


Advanced example

In this example we actually mess about with warping images and modifying the colours, in order to symmetrise and image in shape and / or colour.

import Facemorph.Template;
import Facemorph.Warp;
import Facemorph.psychomorph.Batchable;
import Facemorph.psychomorph.DelineatorForm;
import Facemorph.psychomorph.ImageZoomPanel;
import Facemorph.psychomorph.PsychoMorphForm;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;

/**
 * Batch symmetriser
 */
public class BatchSymmetrise implements Batchable {
    int[] plist;
    PsychoMorphForm psychomorph;

    public boolean process(ImageZoomPanel izp, boolean single) {
        	Template symtemp = new Template();
		Template template = izp.getTemplate();
                BufferedImage img = DelineatorForm.checkBufferedImage(izp.getImage());

                 if (single) {
                    psychomorph.getDelineator().getImageUndoStack().push(img);
                    psychomorph.getDelineator().getTemplateUndoStack().push(template);
                    psychomorph.getDelineator().getTransformUndoMenuItem().setEnabled(true);
                }
                symtemp.symmetrise(template, plist, img.getWidth());
		symtemp.copySamples(template);
                Warp warp = Warp.createWarp(psychomorph.getWarpType(), img.getWidth(), img.getHeight(), img.getWidth(), img.getHeight(), false);
                warp.interpolate(template, symtemp, true,  true, psychomorph.getOverlap());
		img = warp.warpImage(img);
		if (psychomorph.getDelineator().getColourCheckBoxMenuItem().getState()) {
		    symmetrise(img);
		}
		if (psychomorph.getDelineator().getShapeCheckBoxMenuItem().getState()) {
		    izp.setTemplate(symtemp);
		} else {
                    warp = Warp.createWarp(psychomorph.getWarpType(), img.getWidth(), img.getHeight(), img.getWidth(), img.getHeight(), false);
                    warp.interpolate(symtemp, template, true, true, psychomorph.getOverlap());
                    img = warp.warpImage(img);

		}
                izp.setImage(img);
                return true;
    }

    public boolean initialise(PsychoMorphForm psychomorph) {
        this.psychomorph = psychomorph;
        JFileChooser chooser = PsychoMorphForm.setUpFileDialog(psychomorph.getFileChooser(), "Symmetry File", "sym");
        psychomorph.setFileChooser(chooser);

    int ok = psychomorph.getFileChooser().showOpenDialog(psychomorph.getDelineator());
    File f2 = psychomorph.getFileChooser().getSelectedFile();
    plist = null;
    if (f2 == null || ok != JFileChooser.APPROVE_OPTION) return false;

    try {
        plist = Template.readSymFile(f2.getPath());
    } catch (FileNotFoundException ex) {
        JOptionPane.showMessageDialog(psychomorph.getDelineator(), "Error reading sym file " + f2 + ", error " + ex, "Symmetry file read error", JOptionPane.ERROR_MESSAGE);
        ex.printStackTrace();
        return false;
    } catch (IOException ex) {
        JOptionPane.showMessageDialog(psychomorph.getDelineator(), "Error reading sym file " + f2 + ", error " + ex, "Symmetry file read error", JOptionPane.ERROR_MESSAGE);
        ex.printStackTrace();
        return false;
    }
    return true;
    }

    public void finish() { }

    public String getName() {
        return "Symmetrise";
    }

    public boolean getReadTemplate() {return true;}
    public boolean getWriteTemplate() {return true;}
    public boolean getWriteImage() {return true;}

    void symmetrise(BufferedImage bimg) {
    int x, y, r, g, b;
    int r1, g1, b1, r2, g2, b2;

    for (x=0; x<bimg.getWidth()/2; x++)
		for (y=0; y<bimg.getHeight(); y++)
		{
			int rgb1 = bimg.getRGB(x, y);
                        Color c1 = new Color(rgb1);
                        r1 = c1.getRed();
                        g1 = c1.getGreen();
                        b1 = c1.getBlue();
			int rgb2 = bimg.getRGB(bimg.getWidth()-x-1, y);
                        Color c2 = new Color(rgb2);
                        r2 = c2.getRed();
                        g2 = c2.getGreen();
                        b2 = c2.getBlue();
                        r = (r1+r2)/2;
			g = (g1+g2)/2;
			b = (b1+b2)/2;
                        Color c = new Color(r, g, b);
			bimg.setRGB(x, y, c.getRGB());
			bimg.setRGB(bimg.getWidth()-x-1, y, c.getRGB());
	    }

      }
}