Java fast pixel operations - java

I would like to know how I would handle pixel operations best in Java. I'm using swing and drawing a rectangle of 1 by 1 pixel is terribly slow. I need to get 60 fps at least, without using too much resources. Would blitting to an image first mean this could be archieved succesfully? Or is it a bad idea in general to do this with Java and do I need to stick to C or another alternative?
I'm in the beginning of writing a raycaster and since openCL, which I'm using, has a wrapper for Java, I prefer working in Java.

Adding to #camickr's suggestion:
Create a BufferedImage (BI), wrap it in a IconImage, set it as the icon for a JLabel. Paint changes onto the BI and call JLabel's repaint() to flush those. changes to the screen. Caching the partial image in a BI lessens the amount of work for the paint routine. It just needs to blit the image, not render the image. Use SwingWorkers to deploy background threads to run the calculations and pass the results back to the EDT for painting.
As long as you're talking about a static image, that will work fine. If you are considering something more like a video game (some fixed images and other moving images) take a look at VolatileImage. There is a good description here: http://gpwiki.org/index.php/Java:Tutorials:VolatileImage
Update:
The following gives me a bit over 80 fps:
public class Demo extends javax.swing.JPanel {
private Image src = null;
public Demo() {
new Worker().execute();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (src != null) g.drawImage(src, 0, 0, this);
}
private class Worker extends SwingWorker<Void, Image>{
private final Color[] colors = { Color.red, Color.green, Color.blue };
protected void process(List<Image> chunks){
for (Image bufferedImage : chunks){
src = bufferedImage;
repaint();
}
}
protected Void doInBackground() throws Exception{
int frames = 0;
int[] mem = new int[1024 * 768];
long start = System.currentTimeMillis();
long end = start + 15000;
long last = start;
while (last < end){
int col = colors[frames % colors.length].getRGB();
for (int y = 0; y < 768; y++)
for (int x = 0; x < 1024; x++)
mem[x + y * 1024] = col;
Image img = createImage(new MemoryImageSource(1024, 768, mem, 0, 1024));
BufferedImage bi = new BufferedImage(1024, 768, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = bi.createGraphics();
g2.drawImage(img, 0, 0, null);
g2.dispose();
publish(bi);
last = System.currentTimeMillis();
frames++;
}
System.err.println("Frames = " + frames + ", fps = " + ((double) frames / (last - start) * 1000));
return null;
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run(){
JFrame jf = new JFrame();
jf.getContentPane().add(new Demo(), BorderLayout.CENTER);
jf.setSize(1024, 768);
jf.setVisible(true);
}
});
}
}

Use a BufferedImage and the setRGB(...) method. Then you draw the entire image in your paint routine.

Related

Drawing fully transparent "white" in Java BufferedImage

This might sound like a bit of strange title, but bear with me, there is a reason:
I am trying to generate a white glow around a text on a gray background.
To generate the glow, I created a new BufferedImage that's bigger than the text, then I drew the text in white onto the canvas of the image and ran a Gaussian Blur over the image via a ConvolveOp, hoping for something like this:
At first I was a bit surprised when the glow turned out darker than the gray background of the text:
But after a bit of thinking, I understood the problem:
The convolution operates on each color channel (R, G, B, and A) independently to calculate the blurred image. The transparent background of the picture has color value 0x00000000, i.e. a fully transparent black! So, when the convolution filter runs over the image, it not only blends the alpha value, but also mixes the black into the RGB values of the white pixels. This is why the glow comes out dark.
To fix this, I need to initialize the image to 0x00FFFFFF, i.e. a fully transparent white instead, but if I just set that color and fill a rectangle with it, it simply does nothing as Java says "well, it's a fully transparent rectangle that you're drawing! That's not going to change the image... Let me optimize that away for you... Done... You're welcome.".
If I instead set the color to 0x01FFFFFF, i.e. an almost fully transparent white, it does draw the rectangle and the glow looks beautiful, except I end up with a very faint white box around it...
Is there a way I can initialize the image to 0x00FFFFFF everywhere?
UPDATE:
I found one way, but it's probably as non-optimal as you can get:
I draw an opaque white rectangle onto the image and then I run a RescaleOp over the image that sets all alpha values to 0. This works, but it's probably a terrible approach as far as performance goes.
Can I do better somehow?
PS: I'm also open to entirely different suggestions for creating such a glow effect
The main reason why the glow appeared darker with your initial approach is most likely that you did not use an image with a premultiplied alpha component. The JavaDoc of ConvolveOp contains some information about how the alpha component is treated during a convolution.
You could work around this with an "almost fully transparent white". But alternatively, you may simply use an image with premultiplied alpha, i.e. one with the type TYPE_INT_ARGB_PRE.
Here is a MCVE that draws a panel with some text, and some pulsing glow around the text (remove the timer and set a fixed radius to remove the pulse - I couldn't resist playing around a little here ...).
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class TextGlowTest
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add(new TextGlowPanel());
f.setSize(300,200);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class TextGlowPanel extends JPanel
{
private BufferedImage image;
private int radius = 1;
TextGlowPanel()
{
Timer t = new Timer(50, new ActionListener()
{
long startMillis = -1;
#Override
public void actionPerformed(ActionEvent e)
{
if (startMillis == -1)
{
startMillis = System.currentTimeMillis();
}
long d = System.currentTimeMillis() - startMillis;
double s = d / 1000.0;
radius = (int)(1 + 15 * (Math.sin(s * 3) * 0.5 + 0.5));
repaint();
}
});
t.start();
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
gr.setColor(Color.GRAY);
int w = getWidth();
int h = getHeight();
gr.fillRect(0, 0, w, h);
if (image == null || image.getWidth() != w || image.getHeight() != h)
{
// Must be prmultiplied!
image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
}
Graphics2D g = image.createGraphics();
Font font = g.getFont().deriveFont(70.0f).deriveFont(Font.BOLD);
g.setFont(font);
g.setComposite(AlphaComposite.Src);
g.setColor(new Color(255,255,255,0));
g.fillRect(0,0,w,h);
g.setComposite(AlphaComposite.SrcOver);
g.setColor(new Color(255,255,255,0));
g.fillRect(0,0,w,h);
g.setColor(Color.WHITE);
g.drawString("Glow!", 50, 100);
image = getGaussianBlurFilter(radius, true).filter(image, null);
image = getGaussianBlurFilter(radius, false).filter(image, null);
g.dispose();
g = image.createGraphics();
g.setFont(font);
g.setColor(Color.BLUE);
g.drawString("Glow!", 50, 100);
g.dispose();
gr.drawImage(image, 0, 0, null);
}
// From
// http://www.java2s.com/Code/Java/Advanced-Graphics/GaussianBlurDemo.htm
public static ConvolveOp getGaussianBlurFilter(
int radius, boolean horizontal)
{
if (radius < 1)
{
throw new IllegalArgumentException("Radius must be >= 1");
}
int size = radius * 2 + 1;
float[] data = new float[size];
float sigma = radius / 3.0f;
float twoSigmaSquare = 2.0f * sigma * sigma;
float sigmaRoot = (float) Math.sqrt(twoSigmaSquare * Math.PI);
float total = 0.0f;
for (int i = -radius; i <= radius; i++)
{
float distance = i * i;
int index = i + radius;
data[index] =
(float) Math.exp(-distance / twoSigmaSquare) / sigmaRoot;
total += data[index];
}
for (int i = 0; i < data.length; i++)
{
data[i] /= total;
}
Kernel kernel = null;
if (horizontal)
{
kernel = new Kernel(size, 1, data);
}
else
{
kernel = new Kernel(1, size, data);
}
return new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
}
}
I've found that clearRect should paint a transparent color.
g.setBackground(new Color(0x00FFFFFF, true));
g.clearRect(0, 0, img.getWidth(), img.getHeight());
You should also be able to force the BufferedImage to fill with a transparent color by setting the pixel data directly.
public static void forceFill(BufferedImage img, int rgb) {
for(int x = 0; x < img.getWidth(); x++) {
for(int y = 0; y < img.getHeight(); y++) {
img.setRGB(x, y, rgb);
}
}
}
It is not clearly documented but I tested it and setRGB appears to accept an ARGB value.

Graphics painting strategy(performance)

In my game I have a gamepanel which draws my map layers: 'ground/buildings/objects layer', then player sprite, then enemies/npcs/mobs, then 'above layer(tiles to draw above player)'. This was working great and running smoothly.
I then started to work on a minimap JInternalFrame. It actually looks great for what I need but I am concerned with performance. After adding the minimap I noted some slowdown of painting. My biggest resolution supports a map of:
else if (scrnsize.width >= 1440 && scrnsize.height >= 1024){ //large&wide
//45x29(32x32px tiles)
//1440, 1024
Basically my question is, is there a better way I can do this(than below code) or methods I can call for offscreen buffer or something?
This is the Minimap code. As you can see I have logic in the paintComponent to not redraw unless the refreshMinimap == true(the player moves or dies). This helped get rid of most noticable lag, but I still am noticing some. Any help would be super appreciated.
public MinimapGamePanel() {
super();
logger.addAppender(GUILog4JFileHelper.fileAppender);
gamePanelImage = new BufferedImage(32 * MyClient.xTiles, 32 * MyClient.yTiles, BufferedImage.TYPE_INT_ARGB);
setLayout(new BorderLayout());
setBorder(BorderFactory.createLineBorder(Color.black));
}
public void paintComponent(Graphics g) {
logger.trace("begin: REPAINTNG...");
g2 = (Graphics2D)g;
//map
if (refreshMinimap){
RefreshMinimap();
refreshMinimap = false;
}
g2.drawImage(gamePanelImage, 0, 0, null);
g2.dispose();
logger.trace("end: REPAINTNG...");
}
private void RefreshMinimap() {
logger.trace("Map drawing started.");
int count = (int) ((MyClient.characterX - (MyClient.xTiles*1.5)) + ((MyClient.characterY - (MyClient.yTiles*2)) * MyClient.mapWidth));
for (int x = 0; x < MyClient.xTiles*3; x++){
for (int y = 0; y < MyClient.yTiles*4; y++){
if (count > -1 && count < (MyClient.mapWidth * MyClient.mapHeight)){
if (!MyClient.groundLayer[count].equals("0")){ //don't draw full transparent tiles
//SpriteStore.get().getSprite("images/tiles/" + MyClient.groundLayer[count] + ".png").draw(gamePanelImage, x, y);
SpriteStore.get().getSprite("images/tiles/" + MyClient.groundLayer[count] + ".png").drawFirstPixel(gamePanelImage, x, y);
}
if (!MyClient.buildingLayer[count].equals("0")){ //don't draw full transparent tiles
SpriteStore.get().getSprite("images/tiles/" + MyClient.buildingLayer[count] + ".png").drawFirstPixel(gamePanelImage, x, y);
}
if (!MyClient.objectLayer[count].equals("0")){ //don't draw full transparent tiles
SpriteStore.get().getSprite("images/tiles/" + MyClient.objectLayer[count] + ".png").drawFirstPixel(gamePanelImage, x, y);
}
} else {
SpriteStore.get().getSprite("images/tiles/" + MyClient.groundLayer[0] + ".png").drawFirstPixel(gamePanelImage, x, y);
}
count += MyClient.mapWidth;
}
count -= MyClient.yTiles * 4 * MyClient.mapWidth;
count++;
}
logger.trace("Map drawing done.");
}
This is the drawing code which just draws pixel 0,0
public void drawFirstPixel(BufferedImage gamePanelImage, int xDraw, int yDraw) {
BufferedImage bufferedVersion = (BufferedImage) image;
gamePanelImage.getGraphics().drawImage(bufferedVersion.getSubimage(0, 0, 1, 1), xDraw, yDraw, null);
}
I am actually pretty happy with the look of just taking pixel 0,0. Apologies for the bad quality, shaky cell phone pic.
After adding the minimap I noted some slowdown of painting
SpriteStore.get().getSprite("images/tiles/" + MyClient.buildingLayer
Don't do I/O in a painting method. All images should be read into memory when you create your class.
Is this in tune to what you guys were suggesting I do?
public class MinimapGamePanel extends JPanel {
...
private static Thread t;
public MinimapGamePanel() {
super();
logger.addAppender(GUILog4JFileHelper.fileAppender);
gamePanelImage = new BufferedImage(320, 320, BufferedImage.TYPE_INT_ARGB);
//TODO:: change values above upon picking minimap size
setLayout(new BorderLayout());
setBorder(BorderFactory.createLineBorder(Color.black));
t = new Thread(new RefreshMinimapThread());
}
public void paintComponent(Graphics g) {
logger.trace("begin: REPAINTNG...");
g2 = (Graphics2D)g;
if (refreshMinimap){
t.start();
//RefreshMinimap();
refreshMinimap = false;
}
g2.drawImage(gamePanelImage, 0, 0, null);
g2.dispose();
logger.trace("end: REPAINTNG...");
}
private class RefreshMinimapThread implements Runnable {
#Override
public void run() {
RefreshMinimap();
}
}

Graph plotting in Java Swing only draws points

I'm currently working on a program where certain numerical variables, which evolve over time, have their value displayed on each iteration. That works well enough, but now I want to plot a graph that shows their evolution over time.
So, I looked into an example of code for plotting graphs in Swing. My final code looks like this:
public class Populus3 extends JPanel
{
public static void main(String[] args) throws IOException {
final Populus3 pop = new Populus3();
JFrame f = new JFrame(); //where I want to plot the graph
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new GraphingData());
f.setSize(400,400);
f.setLocation(200,200);
f.setVisible(true);
frame = new JFrame("Animation Frame"); //where I'm running animation for another element of the program
frame.add(pop, BorderLayout.CENTER);
frame.setSize(graphSize, graphSize);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//insert all sort of things
}
public void paint(Graphics g)
{
super.paint(g);
paintCell(g, 1);
Toolkit.getDefaultToolkit().sync(); // necessary for linux users to draw and animate image correctly
g.dispose();
}
public void actionPerformed(ActionEvent e) {
repaint();
}
#Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
for(int i = 0; i < particleType.length; i++)
paintCell(g, i); //a method that draws a small circle for the animation panel
}
public static class GraphingData extends JPanel {
int[] data = {
21, 14, 18, 03, 86, 88, 74, 87, 54, 77,
61, 55, 48, 60, 49, 36, 38, 27, 20, 18
};
final int PAD = 20;
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
// Draw ordinate.
g2.draw(new Line2D.Double(PAD, PAD, PAD, h-PAD));
// Draw abcissa.
g2.draw(new Line2D.Double(PAD, h-PAD, w-PAD, h-PAD));
double xInc = (double)(w - 2*PAD)/(data.length-1);
double scale = (double)(h - 2*PAD)/getMax();
// Mark data points.
g2.setPaint(Color.red);
for(int i = 0; i < data.length; i++) {
double x = PAD + i*xInc;
double y = h - PAD - scale*data[i];
g2.fill(new Ellipse2D.Double(x-2, y-2, 4, 4));
}
}
private int getMax() {
int max = -Integer.MAX_VALUE;
for(int i = 0; i < data.length; i++) {
if(data[i] > max)
max = data[i];
}
return max;
}
}
}
Now, the animation panel works just fine. The graph panel, on the other hand...when I run the program, it displays a bunch of red dots, without lines to connect them. What am I doing wrong?
In addition to #Hovercraft's helpful suggestions, also consider these other approaches:
Accumulate the points in a GeneralPath that may be rendered as required, for example.
Connect the points using repeated calls to drawLine() using a suitable coordinate system, outlined here.
Look at JFreeChart.
Your code confuses me:
You override both paint and paintComponent for your Populus3 JPanel -- why? You should only override paintComponent unless you absolutely have to have your drawing affect a component's children and borders.
You dispose of the Graphics object passed into paint -- a very dangerous thing to do. You should never dispose of a Graphics object given to you by the JVM, only Graphics objects that you yourself create.
You repeatedly call a method not defined here for us, paintCell(...).
I've never heard of the need for Toolkit.getDefaultToolkit().sync(); for Swing applications. Do you have a reference for this need?
You mention "animation" but I see no animation code.
In your GraphingData class's paintComponent method you fill ellipses in your for loop, but you don't connect them with lines ever, so it shouldn't be surprising that you're only seeing dots in your graph and no lines.
Consider isolating your problem more and posting an sscce, a minimal test program that we can compile, run, modify and correct and that shows us your problem, but has no extra code not related to the problem or required for demonstration.
The following code demonstrates a real-time Java chart using XChart where the line is updated as the data evolves over time. Creating real-time charts is as simple as calling updateXYSeries for one or more series objects through the XYChart instance and triggering a redraw of the JPanel containing the chart. This works for all chart types including XYChart, CategoryChart, BubbleChart and PieChart, for which example source code can be found here: https://github.com/timmolter/XChart/tree/develop/xchart-demo/src/main/java/org/knowm/xchart/demo/charts/realtime. Examples demonstrate using the SwingWrapper with repaintChart() method as well as XChartPanel with revalidate() and repaint(). Disclaimer, I'm the main developer of the XChart library.
public class SimpleRealTime {
public static void main(String[] args) throws Exception {
double phase = 0;
double[][] initdata = getSineData(phase);
// Create Chart
final XYChart chart = QuickChart.getChart("Simple XChart Real-time Demo", "Radians", "Sine", "sine", initdata[0], initdata[1]);
// Show it
final SwingWrapper<XYChart> sw = new SwingWrapper<XYChart>(chart);
sw.displayChart();
while (true) {
phase += 2 * Math.PI * 2 / 20.0;
Thread.sleep(100);
final double[][] data = getSineData(phase);
chart.updateXYSeries("sine", data[0], data[1], null);
sw.repaintChart();
}
}
private static double[][] getSineData(double phase) {
double[] xData = new double[100];
double[] yData = new double[100];
for (int i = 0; i < xData.length; i++) {
double radians = phase + (2 * Math.PI / xData.length * i);
xData[i] = radians;
yData[i] = Math.sin(radians);
}
return new double[][] { xData, yData };
}
}
This results in the following Java Swing real-time chart app:

High speed, between-frame, graphics architecture for java

I need to plot animated frames in which each pixel is calculated on the fly as the result of an algorithm. Full screen animations may thus require many millions of operations per frame. I would like to achieve the highest refresh rate possible, preferably 20 to 30+ frames per second, if possible.
Can someone show me how to design/write a highly optimized architecture for extremely fast frame refreshment in java?
This needs to be platform independent, so I cannot take advantage of hardware acceleration. The code will be executed on each individual user's computer, NOT on a central server. Of course, I am separately approaching this from the standpoint of simplifying the algorithms for generating the pixel values within-each-frame, but this question is about architecture for high speed frame-by-frame refreshment between-frames, independent of the algorithm used to generate pixel values within each frame. For example, in answers to this posting, I am looking for methods such as using: BufferedImage, double-buffering, multi-threading, accelerated off-screen images, other between-frames methods, etc.
I wrote some sample code below to simulate the problem. At full screen on my notebook computer, the code below individually refreshes 1,300,000+ pixels per frame with unique values. This takes 500 milliseconds per frame on a machine with four processors and 8 gigabytes of memory. I suspect that I am not using BufferedImage correctly below, and I would really like to learn about other between-frame, architecture-level techniques for optimizing the code below, independent of the algorithms I will end up using to calculate pixel values within-each-frame. Your code samples and links to articles would be much appreciated.
How can I improve the code below, from an frame-by-frame (between-frames) architecture standpoint, not from a within-frame standpoint?
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
public class TestBuffer {
private static void createAndShowUI() {
TestPanel fastGraphicsPanel = new TestPanel();
JFrame frame = new JFrame("This Needs A Faster Architecture!");
frame.getContentPane().add(fastGraphicsPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setPreferredSize(new Dimension(800,600));
frame.setResizable(true);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {createAndShowUI();}});}
}
#SuppressWarnings("serial")
class TestPanel extends JPanel {
int w, h;
private static int WIDTH = 700;
private static int HEIGHT = 500;
private static final Color BACKGROUND_COLOR = Color.white;
private BufferedImage bImg;
private Color color = Color.black;
public TestPanel() {
bImg = new BufferedImage(WIDTH, HEIGHT,BufferedImage.TYPE_INT_RGB);
Graphics g = bImg.getGraphics();
g.setColor(BACKGROUND_COLOR);
g.fillRect(0, 0, WIDTH, HEIGHT);
Timer myTimer = new Timer(10, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if(w!=0&&h!=0){
if(WIDTH!=w&&HEIGHT!=h){
WIDTH = w; HEIGHT = h;
bImg = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
}
}
repaint();
}
});
myTimer.setInitialDelay(0);
myTimer.setRepeats(true);
myTimer.setCoalesce(true);
myTimer.start();
g.dispose();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
w = getWidth();
h = getHeight();
// System.out.println("w, h are: "+w+", "+h);
long startTime = System.currentTimeMillis();
g.drawImage(bImg, 0, 0, null);
long endDrawImageTime = System.currentTimeMillis();
Graphics2D g2 = (Graphics2D) g;
drawRandomScreen(g2);
long endDrawScreenTime = System.currentTimeMillis();
long stopTime = System.currentTimeMillis();
long drawImageTime = endDrawImageTime - startTime;
long drawScreenTime = endDrawScreenTime - endDrawImageTime;
long elapsedTime = stopTime - startTime;
System.out.println(drawImageTime+", "+drawScreenTime+", "+elapsedTime);
}
private void drawRandomScreen(Graphics2D g2) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
for(int i=0;i<WIDTH;i++){
for(int j=0;j<HEIGHT;j++){
color = new Color((int) (Math.random() * 255),(int) (Math.random() * 255),(int) (Math.random() * 255));
g2.setColor(color);
g2.drawLine(i, j, i, j);
}
}
}
}
In the example given, drawRandomScreen() executes on the event dispatch thread. As an alternative, let the model evolve on a separate thread (or threads) and sample the resulting image at a sustainable rate, say 25 Hz, using the observer pattern. Synchronize access to the shared image buffer. A complete example is examined here. Profile to verify ongoing optimization efforts.
instead of interacting through Graphics2D you should interact direct with image data. This is my code, with my laptop can run with 20 frame/second (fullscreen)
#SuppressWarnings("serial")
class TestPanel extends JPanel {
int w, h;
private static int WIDTH = 700;
private static int HEIGHT = 500;
private static final Color BACKGROUND_COLOR = Color.white;
private BufferedImage bImg;
private Color color = Color.black;
public TestPanel() {
bImg = new BufferedImage(WIDTH, HEIGHT,BufferedImage.TYPE_INT_RGB);
Graphics g = bImg.getGraphics();
g.setColor(BACKGROUND_COLOR);
g.fillRect(0, 0, WIDTH, HEIGHT);
Timer myTimer = new Timer(10, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if(w!=0&&h!=0){
if(WIDTH!=w&&HEIGHT!=h){
WIDTH = w; HEIGHT = h;
System.out.println("create");
bImg = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
}
}
repaint();
}
});
myTimer.setInitialDelay(0);
myTimer.setRepeats(true);
myTimer.setCoalesce(true);
myTimer.start();
g.dispose();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
w = getWidth();
h = getHeight();
// System.out.println("w, h are: "+w+", "+h);
long startTime = System.currentTimeMillis();
long endDrawImageTime = System.currentTimeMillis();
// Graphics2D g2 = (Graphics2D) g;
drawRandomScreen(bImg);
g.drawImage(bImg, 0, 0, null);
long endDrawScreenTime = System.currentTimeMillis();
long stopTime = System.currentTimeMillis();
long drawImageTime = endDrawImageTime - startTime;
long drawScreenTime = endDrawScreenTime - endDrawImageTime;
long elapsedTime = stopTime - startTime;
System.out.println(drawImageTime+", "+drawScreenTime+", "+elapsedTime);
}
private void drawRandomScreen(BufferedImage image) {
final int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
final int width = image.getWidth();
final int height = image.getHeight();
long startTime = System.currentTimeMillis();
// g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
// RenderingHints.VALUE_ANTIALIAS_ON);
Random r = new Random();
for(int i=0;i<width;i++){
for(int j=0;j<height;j++){
color = new Color(r.nextInt(255),r.nextInt(255),r.nextInt(255));
int pos = j*width+i;
pixels[pos] = color.hashCode();
}
}
long stopTime = System.currentTimeMillis();
System.out.println("time "+(stopTime-startTime));
}
}
This is your new code. In my laptop, fullscreen can run at 150 frames/ second. At you can see, time execution of function drawRandomScreen only 1/2 time of drawImageTime.
private void drawRandomScreen(BufferedImage image) {
final int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
final int width = image.getWidth();
final int height = image.getHeight();
long startTime = System.currentTimeMillis();
Random r = new Random();
int size = pixels.length;
//swap 100 times
int maxPixelsSwap = 1000;
size-=maxPixelsSwap;
for (int i = 0; i < 100; i++) {
int src = r.nextInt(size);
int des = src+r.nextInt(size-src);
int swapsize = r.nextInt(maxPixelsSwap); //maximium
int[] temp = new int[swapsize];
System.arraycopy(pixels, des, temp, 0, swapsize);
System.arraycopy(pixels, src, pixels, des, swapsize);
System.arraycopy(temp, 0, pixels, src, swapsize);
}
size = pixels.length;
int randomTimes = size/10; //only change 10% of pixels
size--;
for (int i = 0; i < randomTimes; i++) {
pixels[r.nextInt(size)]=r.nextInt();
}
long stopTime = System.currentTimeMillis();
System.out.println("time "+(stopTime-startTime));
}

Java bitmap font: blitting 1-bit image with different colors

I'd like to implement a simple bitmap font drawing in Java AWT-based application. Application draws on a Graphics object, where I'd like to implement a simple algorithm:
1) Load a file (probably using ImageIO.read(new File(fileName))), which is 1-bit PNG that looks something like that:
I.e. it's 16*16 (or 16*many, if I'd like to support Unicode) matrix of 8*8 characters. Black corresponds to background color, white corresponds to foreground.
2) Draw strings character-by-character, blitting relevant parts of this bitmap to target Graphics. So far I've only succeeded with something like that:
int posX = ch % 16;
int posY = ch / 16;
int fontX = posX * CHAR_WIDTH;
int fontY = posY * CHAR_HEIGHT;
g.drawImage(
font,
dx, dy, dx + CHAR_WIDTH, dy + CHAR_HEIGHT,
fontX, fontY, fontX + CHAR_WIDTH, fontY + CHAR_HEIGHT,
null
);
It works, but, alas, it blits the text as is, i.e. I can't substitute black and white with desired foreground and background colors, and I can't even make background transparent.
So, the question is: is there a simple (and fast!) way in Java to blit part of one 1-bit bitmap to another, colorizing it in process of blitting (i.e. replacing all 0 pixels with one given color and all 1 pixels with another)?
I've researched into a couple of solutions, all of them look suboptimal to me:
Using a custom colorizing BufferedImageOp, as outlined in this solution - it should work, but it seems that it would be very inefficient to recolorize a bitmap before every blit operation.
Using multiple 32-bit RGBA PNG, with alpha channel set to 0 for black pixels and to maximum for foreground. Every desired foreground color should get its own pre-rendered bitmap. This way I can make background transparent and draw it as a rectangle separately before blitting and then select one bitmap with my font, pre-colorized with desired color and draw a portion of it over that rectangle. Seems like a huge overkill to me - and what makes this option even worse - it limits number of foreground colors to a relatively small amount (i.e. I can realistically load up and hold like hundreds or thousands of bitmaps, not millions)
Bundling and loading a custom font, as outlined in this solution could work, but as far as I see in Font#createFont documentation, AWT's Font seems to work only with vector-based fonts, not with bitmap-based.
May be there's already any libraries that implement such functionality? Or it's time for me to switch to some sort of more advanced graphics library, something like lwjgl?
Benchmarking results
I've tested a couple of algorithms in a simple test: I have 2 strings, 71 characters each, and draw them continuously one after another, right on the same place:
for (int i = 0; i < N; i++) {
cv.putString(5, 5, STR, Color.RED, Color.BLUE);
cv.putString(5, 5, STR2, Color.RED, Color.BLUE);
}
Then I measure time taken and calculate speed: string per second and characters per second. So far, various implementation I've tested yield the following results:
bitmap font, 16*16 characters bitmap: 10991 strings / sec, 780391 chars / sec
bitmap font, pre-split images: 11048 strings / sec, 784443 chars / sec
g.drawString(): 8952 strings / sec, 635631 chars / sec
colored bitmap font, colorized using LookupOp and ByteLookupTable: 404 strings / sec, 28741 chars / sec
You might turn each bitmap into a Shape (or many of them) and draw the Shape. See Smoothing a jagged path for the process of gaining the Shape.
E.G.
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.*;
import java.util.Random;
/* Gain the outline of an image for further processing. */
class ImageShape {
private BufferedImage image;
private BufferedImage ImageShape;
private Area areaOutline = null;
private JLabel labelOutline;
private JLabel output;
private BufferedImage anim;
private Random random = new Random();
private int count = 0;
private long time = System.currentTimeMillis();
private String rate = "";
public ImageShape(BufferedImage image) {
this.image = image;
}
public void drawOutline() {
if (areaOutline!=null) {
Graphics2D g = ImageShape.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0,0,ImageShape.getWidth(),ImageShape.getHeight());
g.setColor(Color.RED);
g.setClip(areaOutline);
g.fillRect(0,0,ImageShape.getWidth(),ImageShape.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(1,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));
originalImage.add(originalLabel);
images.add(originalImage);
ImageShape = new BufferedImage(
image.getWidth(),
image.getHeight(),
BufferedImage.TYPE_INT_RGB
);
labelOutline = new JLabel(new ImageIcon(ImageShape));
images.add(labelOutline);
anim = new BufferedImage(
image.getWidth()*2,
image.getHeight()*2,
BufferedImage.TYPE_INT_RGB);
output = new JLabel(new ImageIcon(anim));
gui.add(output, BorderLayout.CENTER);
updateImages();
gui.add(images, BorderLayout.NORTH);
animate();
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
animate();
}
};
Timer timer = new Timer(1,al);
timer.start();
return gui;
}
private void updateImages() {
areaOutline = getOutline(Color.BLACK, image);
drawOutline();
}
private void animate() {
Graphics2D gr = anim.createGraphics();
gr.setColor(Color.BLUE);
gr.fillRect(0,0,anim.getWidth(),anim.getHeight());
count++;
if (count%100==0) {
long now = System.currentTimeMillis();
long duration = now-time;
double fraction = (double)duration/1000;
rate = "" + (double)100/fraction;
time = now;
}
gr.setColor(Color.WHITE);
gr.translate(0,0);
gr.drawString(rate, 20, 20);
int x = random.nextInt(image.getWidth());
int y = random.nextInt(image.getHeight());
gr.translate(x,y);
int r = 128+random.nextInt(127);
int g = 128+random.nextInt(127);
int b = 128+random.nextInt(127);
gr.setColor(new Color(r,g,b));
gr.draw(areaOutline);
gr.dispose();
output.repaint();
}
public static void main(String[] args) throws Exception {
int size = 150;
final BufferedImage outline = javax.imageio.ImageIO.read(new java.io.File("img.gif"));
ImageShape io = new ImageShape(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);
}
}
I have to figure there is a factor of ten error in the FPS count on the top left of the blue image though. 50 FPS I could believe, but 500 FPS seems ..wrong.
Okay, looks like I've found the best solution. The key to success was accessing raw pixel arrays in underlying AWT structures. Initialization goes something like that:
public class ConsoleCanvas extends Canvas {
protected BufferedImage buffer;
protected int w;
protected int h;
protected int[] data;
public ConsoleCanvas(int w, int h) {
super();
this.w = w;
this.h = h;
}
public void initialize() {
data = new int[h * w];
// Fill data array with pure solid black
Arrays.fill(data, 0xff000000);
// Java's endless black magic to get it working
DataBufferInt db = new DataBufferInt(data, h * w);
ColorModel cm = ColorModel.getRGBdefault();
SampleModel sm = cm.createCompatibleSampleModel(w, h);
WritableRaster wr = Raster.createWritableRaster(sm, db, null);
buffer = new BufferedImage(cm, wr, false, null);
}
#Override
public void paint(Graphics g) {
update(g);
}
#Override
public void update(Graphics g) {
g.drawImage(buffer, 0, 0, null);
}
}
After this one, you've got both a buffer that you can blit on canvas updates and underlying array of ARGB 4-byte ints - data.
Single character can be drawn like that:
private void putChar(int dx, int dy, char ch, int fore, int back) {
int charIdx = 0;
int canvasIdx = dy * canvas.w + dx;
for (int i = 0; i < CHAR_HEIGHT; i++) {
for (int j = 0; j < CHAR_WIDTH; j++) {
canvas.data[canvasIdx] = font[ch][charIdx] ? fore : back;
charIdx++;
canvasIdx++;
}
canvasIdx += canvas.w - CHAR_WIDTH;
}
}
This one uses a simple boolean[][] array, where first index chooses character and second index iterates over raw 1-bit character pixel data (true => foreground, false => background).
I'll try to publish a complete solution as a part of my Java terminal emulation class set soon.
This solution benchmarks for impressive 26007 strings / sec or 1846553 chars / sec - that's 2.3x times faster than previous best non-colorized drawImage().

Categories