1

I'm using the area generation and drawing code from Smoothing a jagged path to create collision areas for tile sprites in a JRPG game I'm making in java - Its not a major full blown game, more just a proof-of-concept for me to learn from.

The area generation works fine except for straight lines: http://puu.sh/7wJA2.png http://puu.sh/7wJGF.png

These images are of the collision outline opened in photoshop (in the background) vs the area that the java code is generating based on the red color (in the foreground). As can be seen, the large red line in the first image won't be draw unless I have a red pixel under it every 2 pixels, as shown in the second image.


I have made no changes to the code (other than adding comments in an attempt to better understand it) except to remove

Graphics2D g = imageOutline.createGraphics();
//...
g.dispose();

from the areaOutline draw code, replacing it with reference to my own Graphics2D variable. I tested the code both with and without those two lines and I couldn't see any difference.

I'm a bit stuck on what could be causing the issue - its not a major game-breaking issue, as I can fix it a couple of ways, but it does mean I have to explain this issue to anyone when showing them how to use it with their own sprite textures.


I've created a SSCCE of the issue - This shows the areas generated have vertical lines generated fine, but that horizontal ones are not. It takes two images - both a square 30x30 pixels, but one has internal pixels every 2 second pixel in which causes a more accurate area to be generated.

When you press a key the program will switch between the two images (and their areas), allowing for direct comparison.

The following two images must be put in the root folder of the program.

square1.png square2.png

You can start the program by running the "main" method.

import java.awt.*;
import javax.swing.*;
import java.awt.event.*;

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

import java.io.*;
import javax.imageio.*;

public class AreaOutlineCanvas extends JFrame implements KeyListener
{
    private ImagePanel imagePanel;

    /**
     * Constructor for objects of class Skeleton
     */
    public AreaOutlineCanvas()
    {
        // initialise instance variables   
        addKeyListener( this );
        initUI();
    }

    public static void main( String[] args )
    {
        for( String s: args )
        {
            System.out.println( s );
        }

        SwingUtilities.invokeLater( new Runnable()
            {
                @Override
                public void run()
                {
                    AreaOutlineCanvas aOC = new AreaOutlineCanvas();
                    aOC.pack();
                    aOC.setVisible( true );
                    aOC.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
                }

            } );
    }

    private void initUI()
    {
        imagePanel = new ImagePanel();
        setPreferredSize( new Dimension( 100, 100 ) );

        setLocationRelativeTo( null );        

        add( imagePanel );

        imagePanel.setDoubleBuffered( true );

        pack();
    }

    @Override
    public void keyTyped( KeyEvent e )
    {

    }

    @Override
    public void keyPressed( KeyEvent e )
    {
        //@TODO: switch statment checking the menu Skeleton is in (currentMenu)
        if( imagePanel != null )
        {
            imagePanel.bDrawingFirstArea = !imagePanel.bDrawingFirstArea;
            imagePanel.repaint();
        }
    }

    @Override
    public void keyReleased(KeyEvent e)
    {
        //System.out.println( "Key released: " + e.getKeyChar() + " (" + e.getKeyCode() + ")" );
    }

    public class ImagePanel extends JPanel
    {
        /** Path for the location of this charactors sprite sheet */
        private String firstPath = "square1.png";
        private String secondPath = "square2.png";

        private BufferedImage firstImage;
        private BufferedImage secondImage;

        private Area firstImageArea;
        private Area secondImageArea;

        public boolean bDrawingFirstArea = true;

        /**
         * Constructor for objects of class Character
         */
        public ImagePanel()
        {
            loadImages();
            generateAreas();
        }

        private void loadImages()
        {
            try
            {
                firstImage = ImageIO.read( new File( firstPath ) );
                secondImage = ImageIO.read( new File( secondPath ) );
            }
            catch( IOException e )
            {
                e.printStackTrace();
            }
        }

        private void generateAreas()
        {
            Color c = new Color( 255, 0, 0 );
            firstImageArea = getOutline( c, firstImage );
            secondImageArea = getOutline( c, secondImage );
        }

        public Area getOutline(Color target, BufferedImage bi) {
            // construct the GeneralPath
            GeneralPath gp = new GeneralPath();

            boolean cont = false;
            int targetRGB = target.getRGB();
            for (int xx=0; xx<bi.getWidth(); xx++) {
                for (int yy=0; yy<bi.getHeight(); yy++) {
                    if (bi.getRGB(xx,yy)==targetRGB) {
                        if (cont) {
                            gp.lineTo(xx,yy);
                            gp.lineTo(xx,yy+1);
                            gp.lineTo(xx+1,yy+1);
                            gp.lineTo(xx+1,yy);
                            gp.lineTo(xx,yy);
                        } else {
                            gp.moveTo(xx,yy);
                        }
                        cont = true;
                    } else {
                        cont = false;
                    }
                }
                cont = false;
            }
            gp.closePath();

            // construct the Area from the GP & return it
            return new Area(gp);
        }

        @Override
        public void paintComponent( Graphics g )
        {                
            super.paintComponent( g );
            drawImages(g);
        }   

        private void drawImages( Graphics g )
        {
            Graphics2D g2d = (Graphics2D) g;

            if( bDrawingFirstArea )
            {
                g2d.drawImage( firstImage, 50, 0, this );
                g2d.draw( firstImageArea );
            }
            else
            {
                g2d.drawImage( secondImage, 50, 0, this );
                g2d.draw( secondImageArea );
            }

            Toolkit.getDefaultToolkit().sync();
        }
    }
}

For convenience I've posted the code from Smoothing a jagged path question here

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;

/* Gain the outline of an image for further processing. */
class ImageOutline {

    private BufferedImage image;

    private TwoToneImageFilter twoToneFilter;
    private BufferedImage imageTwoTone;
    private JLabel labelTwoTone;

    private BufferedImage imageOutline;
    private Area areaOutline = null;
    private JLabel labelOutline;

    private JLabel targetColor;
    private JSlider tolerance;

    private JProgressBar progress;
    private SwingWorker sw;

    public ImageOutline(BufferedImage image) {
        this.image = image;
        imageTwoTone = new BufferedImage(
            image.getWidth(),
            image.getHeight(),
            BufferedImage.TYPE_INT_RGB);
    }

    public void drawOutline() {
        if (areaOutline!=null) {
            Graphics2D g = imageOutline.createGraphics();
            g.setColor(Color.WHITE);
            g.fillRect(0,0,imageOutline.getWidth(),imageOutline.getHeight());

            g.setColor(Color.RED);
            g.setClip(areaOutline);
            g.fillRect(0,0,imageOutline.getWidth(),imageOutline.getHeight());
            g.setColor(Color.BLACK);
            g.setClip(null);
            g.draw(areaOutline);

            g.dispose();
        }
    }

    public Area getOutline(Color target, BufferedImage bi) {
        // construct the GeneralPath
        GeneralPath gp = new GeneralPath();

        boolean cont = false;
        int targetRGB = target.getRGB();
        for (int xx=0; xx<bi.getWidth(); xx++) {
            for (int yy=0; yy<bi.getHeight(); yy++) {
                if (bi.getRGB(xx,yy)==targetRGB) {
                    if (cont) {
                        gp.lineTo(xx,yy);
                        gp.lineTo(xx,yy+1);
                        gp.lineTo(xx+1,yy+1);
                        gp.lineTo(xx+1,yy);
                        gp.lineTo(xx,yy);
                    } else {
                        gp.moveTo(xx,yy);
                    }
                    cont = true;
                } else {
                    cont = false;
                }
            }
            cont = false;
        }
        gp.closePath();

        // construct the Area from the GP & return it
        return new Area(gp);
    }

    public JPanel getGui() {
        JPanel images = new JPanel(new GridLayout(2,2,2,2));
        JPanel  gui = new JPanel(new BorderLayout(3,3));

        JPanel originalImage =  new JPanel(new BorderLayout(2,2));
        final JLabel originalLabel = new JLabel(new ImageIcon(image));
        targetColor = new JLabel("Target Color");
        targetColor.setForeground(Color.RED);
        targetColor.setBackground(Color.WHITE);
        targetColor.setBorder(new LineBorder(Color.BLACK));
        targetColor.setOpaque(true);

        JPanel controls = new JPanel(new BorderLayout());
        controls.add(targetColor, BorderLayout.WEST);
        originalLabel.addMouseListener( new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent me) {
                originalLabel.setCursor(
                    Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
            }

            @Override
            public void mouseExited(MouseEvent me) {
                originalLabel.setCursor(Cursor.getDefaultCursor());
            }

            @Override
            public void mouseClicked(MouseEvent me) {
                int x = me.getX();
                int y = me.getY();

                Color c = new Color( image.getRGB(x,y) );
                targetColor.setBackground( c );

                updateImages();
            }
        });
        originalImage.add(originalLabel);

        tolerance = new JSlider(
            JSlider.HORIZONTAL,
            0,
            255,
            104
            );
        tolerance.addChangeListener( new ChangeListener() {
            public void stateChanged(ChangeEvent ce) {
                updateImages();
            }
        });
        controls.add(tolerance, BorderLayout.CENTER);
        gui.add(controls,BorderLayout.NORTH);

        images.add(originalImage);

        labelTwoTone = new JLabel(new ImageIcon(imageTwoTone));

        images.add(labelTwoTone);

        images.add(new JLabel("Smoothed Outline"));

        imageOutline = new BufferedImage(
            image.getWidth(),
            image.getHeight(),
            BufferedImage.TYPE_INT_RGB
            );

        labelOutline = new JLabel(new ImageIcon(imageOutline));
        images.add(labelOutline);

        updateImages();

        progress = new JProgressBar();

        gui.add(images, BorderLayout.CENTER);
        gui.add(progress, BorderLayout.SOUTH);

        return gui;
    }

    private void updateImages() {
        if (sw!=null) {
            sw.cancel(true);
        }
        sw = new SwingWorker() {
            @Override
            public String doInBackground() {
                progress.setIndeterminate(true);
                adjustTwoToneImage();
                labelTwoTone.repaint();
                areaOutline = getOutline(Color.BLACK, imageTwoTone);

                drawOutline();

                return "";
            }

            @Override
            protected void done() {
                labelOutline.repaint();
                progress.setIndeterminate(false);
            }
        };
        sw.execute();
    }

    public void adjustTwoToneImage() {
        twoToneFilter = new TwoToneImageFilter(
            targetColor.getBackground(),
            tolerance.getValue());

        Graphics2D g = imageTwoTone.createGraphics();
        g.drawImage(image, twoToneFilter, 0, 0);

        g.dispose();
    }

    public static void main(String[] args) throws Exception {
        int size = 150;
        final BufferedImage outline =
            new BufferedImage(size,size,BufferedImage.TYPE_INT_RGB);
        Graphics2D g = outline.createGraphics();
        g.setColor(Color.WHITE);
        g.fillRect(0,0,size,size);
        g.setRenderingHint(
            RenderingHints.KEY_DITHERING,
            RenderingHints.VALUE_DITHER_ENABLE);
        g.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);

        Polygon p = new Polygon();
        p.addPoint(size/2, size/10);
        p.addPoint(size-10, size-10);
        p.addPoint(10, size-10);
        Area a = new Area(p);

        Rectangle r = new Rectangle(size/4, 8*size/10, size/2, 2*size/10);
        a.subtract(new Area(r));

        int radius = size/10;
        Ellipse2D.Double c = new Ellipse2D.Double(
            (size/2)-radius,
            (size/2)-radius,
            2*radius,
            2*radius
            );
        a.subtract(new Area(c));

        g.setColor(Color.BLACK);
        g.fill(a);

        ImageOutline io = new ImageOutline(outline);

        JFrame f = new JFrame("Image Outline");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(io.getGui());
        f.pack();
        f.setResizable(false);
        f.setLocationByPlatform(true);
        f.setVisible(true);
    }
}

class TwoToneImageFilter implements BufferedImageOp {

    Color target;
    int tolerance;

    TwoToneImageFilter(Color target, int tolerance) {
        this.target = target;
        this.tolerance = tolerance;
    }

    private boolean isIncluded(Color pixel) {
        int rT = target.getRed();
        int gT = target.getGreen();
        int bT = target.getBlue();
        int rP = pixel.getRed();
        int gP = pixel.getGreen();
        int bP = pixel.getBlue();
        return(
            (rP-tolerance<=rT) && (rT<=rP+tolerance) &&
            (gP-tolerance<=gT) && (gT<=gP+tolerance) &&
            (bP-tolerance<=bT) && (bT<=bP+tolerance) );
    }

    public BufferedImage createCompatibleDestImage(
        BufferedImage src,
        ColorModel destCM) {
        BufferedImage bi = new BufferedImage(
            src.getWidth(),
            src.getHeight(),
            BufferedImage.TYPE_INT_RGB);
        return bi;
    }

    public BufferedImage filter(
        BufferedImage src,
        BufferedImage dest) {

        if (dest==null) {
            dest = createCompatibleDestImage(src, null);
        }

        for (int x=0; x<src.getWidth(); x++) {
            for (int y=0; y<src.getHeight(); y++) {
                Color pixel = new Color(src.getRGB(x,y));
                Color write = Color.BLACK;
                if (isIncluded(pixel)) {
                    write = Color.WHITE;
                }
                dest.setRGB(x,y,write.getRGB());
            }
        }

        return dest;
    }

    public Rectangle2D getBounds2D(BufferedImage src) {
        return new Rectangle2D.Double(0, 0, src.getWidth(), src.getHeight());
    }

    public Point2D getPoint2D(
        Point2D srcPt,
        Point2D dstPt) {
        // no co-ord translation
        return srcPt;
    }

    public RenderingHints getRenderingHints() {
        return null;
    }
}
Community
  • 1
  • 1
Gareth Jones
  • 1,241
  • 4
  • 17
  • 38
  • So the top image is what you expect, and the second image is what you get? Can you embed the original image into the question? – Andrew Thompson Mar 16 '14 at 02:31
  • @AndrewThompson I'm not too sure what you mean by original image. The top image is what I would like, but can only get by adding a pixel under the long red line every 2 pixels. The second image is my original image that doesn't have the straight line generated. You might be confused thinking the red lines in the background are a zoom in - They are the image opened in photoshop to compare with whats been outlined in the area generation - I'll update my question to reflect that – Gareth Jones Mar 16 '14 at 02:35
  • @AndrewThompson After more testing and after implementing collision detection it seems the issue is just with drawing the area - Its generated in the correct shape, but not drawn in the correct shape, which is far less of a concern – Gareth Jones Mar 17 '14 at 01:15
  • @AndrewThompson I'm still having this issue, and my previous statement was wrong - the area isn't being drawn correctly because its not being calculated correctly. Do you have any suggestions? – Gareth Jones May 23 '14 at 00:48
  • Post an MCVE (like `ImageOutline`). Admittedly at 317 LOC `ImageOutline` does not seem very minimal, but do your best. – Andrew Thompson May 23 '14 at 00:54
  • @AndrewThompson Done, its been included in the post. I think its got what your looking for. As you can see the straight line has no area, while the jagged one does. – Gareth Jones May 23 '14 at 02:29
  • I usually don't follow links, but did this time just out of curiosity. I won't be extracting a 23Kb archive of code. Even if I was to do that, last time I checked, I had nothing on this machine that will handle RAR anyway. Musing: Why on Earth do Java programmers use RAR when Zip is available in the JSE? If you can prepare an MCVE, post it as an [edit to the question](http://stackoverflow.com/posts/22432161/edit). – Andrew Thompson May 23 '14 at 03:02
  • @AndrewThompson - I have replaced my original SSCCE with a "more" SSCCE one - Its all self contained and within one class. I hope you find it acceptable. – Gareth Jones Jun 10 '14 at 21:09

0 Answers0