Hair-lines when drawing transformed BufferedImage

T

Thomas Fritsch

Hello all,

I'm having trouble with drawing transformed images.
On some images there are hair-lines displayed at the top or
left edge, which are not part of the original image data,
but seem to be an artefact of some process under the hood.
I don't understand why and when such hair-lines occur.
How can I avoid them? Have they something to do with
AffineTransform? I hope somebody here can help me out.

At the end is the unavoidable SSCCE for reproducing the problem.
Sorry, but it is still a bit lengthy (150 lines).
I have reproduced the problem using Java 1.4.2 + 1.5.0, WinXP.

The example shows an X-shaped 24x24 image consisting of black
and transparent pixels on a white background. The problem is
the hair-line hanging down the top-left corner of the X. There
are quite many AffineTransforms involved in processing, finally
scaling up each pixel of the "X" to 6.66x6.66 dots on screen.
Because the problem might depend on the screen resolution, I
didn't use Toolkit#getScreenResolution in the SSCCE here, but
hard-coded 96 (the resolution of my screen) instead.

Side note to those of you, familiar with the PostScript
language. The Java code at the end tries to mimic this PS code:
%!PS
50 600 translate
120 120 scale
24 24 false [24 0 0 -24 0 24]
<7FFFFE BFFFFD DFFFFB EFFFF7 F7FFEF FBFFDF FDFFBF FEFF7F
FF7EFF FFBDFF FFDBFF FFE7FF FFE7FF FFDBFF FFBDFF FF7EFF
FEFF7F FDFFBF FBFFDF F7FFEF EFFFF7 DFFFFB BFFFFD 7FFFFEimagemask
Have this in mind when reading the Java code.

Thanks in advance
Thomas

//--Begin SSCCE-------------------------------------------------
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.*;
import java.io.*;
import javax.swing.*;

public class TestComponent extends JComponent {

public static void main(String args[]) throws Exception {
TestComponent comp = new TestComponent();
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(new JScrollPane(comp));
frame.pack();
frame.setVisible(true);
comp.testImageMask();
}

private int screenResolution = 96;
// instead of getToolkit().getScreenResolution();
private BufferedImage pageImage;
private Graphics2D graphics;

TestComponent() {
// Creates an image [8.5 x 11 inch]
int w = (int) (8.5 * screenResolution);
int h = 11 * screenResolution;
pageImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
}

public Dimension getPreferredSize() {
int w = pageImage.getWidth();
int h = pageImage.getHeight();
return new Dimension(w, h);
}

protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.drawImage(pageImage, 0, 0, this);
g2.dispose();
}

// data for 24x24 image, 1 bit per pixel
private byte pixelData[] = {
(byte)0x7F, (byte)0xFF, (byte)0xFE,
(byte)0xBF, (byte)0xFF, (byte)0xFD,
(byte)0xDF, (byte)0xFF, (byte)0xFB,
(byte)0xEF, (byte)0xFF, (byte)0xF7,
(byte)0xF7, (byte)0xFF, (byte)0xEF,
(byte)0xFB, (byte)0xFF, (byte)0xDF,
(byte)0xFD, (byte)0xFF, (byte)0xBF,
(byte)0xFE, (byte)0xFF, (byte)0x7F,
(byte)0xFF, (byte)0x7E, (byte)0xFF,
(byte)0xFF, (byte)0xBD, (byte)0xFF,
(byte)0xFF, (byte)0xDB, (byte)0xFF,
(byte)0xFF, (byte)0xE7, (byte)0xFF,
(byte)0xFF, (byte)0xE7, (byte)0xFF,
(byte)0xFF, (byte)0xDB, (byte)0xFF,
(byte)0xFF, (byte)0xBD, (byte)0xFF,
(byte)0xFF, (byte)0x7E, (byte)0xFF,
(byte)0xFE, (byte)0xFF, (byte)0x7F,
(byte)0xFD, (byte)0xFF, (byte)0xBF,
(byte)0xFB, (byte)0xFF, (byte)0xDF,
(byte)0xF7, (byte)0xFF, (byte)0xEF,
(byte)0xEF, (byte)0xFF, (byte)0xF7,
(byte)0xDF, (byte)0xFF, (byte)0xFB,
(byte)0xBF, (byte)0xFF, (byte)0xFD,
(byte)0x7F, (byte)0xFF, (byte)0xFE
};

void testImageMask() throws Exception {
erasePage();
initGraphics();
translate(50, 600);
scale(120, 120);
imageMask(24, 24, false,
new AffineTransform(24, 0, 0, -24, 0, 24),
new ByteArrayInputStream(pixelData));
}

// Fills the page image completely with white
void erasePage() {
Graphics2D g = pageImage.createGraphics();
g.setColor(Color.white);
g.fillRect(0, 0, pageImage.getWidth(), pageImage.getHeight());
g.dispose();
repaint();
}

void initGraphics() {
graphics = pageImage.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setTransform(defaultMatrix());
graphics.setColor(Color.black);
}

// Gets a transformation transforming from [origin in top-left,
// screen resolution] to [origin in bottom-left, 72 dpi]
AffineTransform defaultMatrix() {
AffineTransform at = new AffineTransform();
at.translate(0, pageImage.getHeight());
at.scale(screenResolution / 72.0, - screenResolution / 72.0);
return at;
}

void translate(float x, float y) {
graphics.translate(x, y);
}

void scale(float sx, float sy) {
graphics.scale(sx, sy);
}

// Reads a byte-stream of pixel-data, interprets them as colored
// or transparent pixels, and draws them onto the page image
void imageMask(int width, int height, boolean coloredIs1,
AffineTransform transform, InputStream stream)
throws IOException, NoninvertibleTransformException {
// Prepare an image with the same byte-layout as the stream
ColorModel cm = createMaskColorModel(coloredIs1);
WritableRaster raster =
cm.createCompatibleWritableRaster(width, height);
byte b[] = ((DataBufferByte) raster.getDataBuffer()).getData();
// Copy the stream straight into the image's data buffer.
for (int i = 0; i < b.length; i++) {
int c = stream.read();
if (c == -1)
break;
b = (byte) c;
}
BufferedImage image = new BufferedImage(cm, raster, false, null);
graphics.drawImage(image, transform.createInverse(), null);
repaint();
}

// Creates a 1 bit/pixel color model. Its 2 colors are:
// the current graphics color; a total transparent color.
private ColorModel createMaskColorModel(boolean coloredIs1) {
int rgb = graphics.getColor().getRGB();
int cmap[] = { rgb, rgb };
int trans = coloredIs1 ? 0 : 1;
return new IndexColorModel(1, 2, cmap, 0, false, trans,
DataBuffer.TYPE_BYTE);
}
}
//--End SSCCE------------------------------------------------
 
R

Roedy Green

I'm having trouble with drawing transformed images.
On some images there are hair-lines displayed at the top or
left edge,

Just making some guesses here. Is flipping back and forth between
integer and float pixel co-ordinates enough to get you off by one, so
that your generated image is shifted one pixel leaving an
uninitialised hairline?

You are familiar with the co-ordinate system that fits "between" the
pixels and leads to some gotchas. You know that drawRectangle will
create a rectangle measured in pixels taller by one than the height
you requested. The model is a pen slopping over and drawing below the
co-ordinate line.

You might like to read http://mindprod.com/jgloss/coordinates.html
to review some of the gotchas you may have overlooked.

I have added a new diagram to explain the off by one gotcha.
 
J

jan V

Hi Thomas,

I'm getting the following bomb:

Exception in thread "main" java.lang.IllegalArgumentException: Number of
color/alpha components should be 4 but length of bits array is 3

at java.awt.image.ColorModel.<init>(Unknown Source)
at java.awt.image.IndexColorModel.<init>(Unknown Source)
at TestComponent.createMaskColorModel(TestComponent.java:145)
at TestComponent.imageMask(TestComponent.java:123)
at TestComponent.testImageMask(TestComponent.java:78)
at TestComponent.main(TestComponent.java:17)
 
T

Thomas Fritsch

jan V said:
Hi Thomas,

I'm getting the following bomb:

Exception in thread "main" java.lang.IllegalArgumentException: Number of
color/alpha components should be 4 but length of bits array is 3

at java.awt.image.ColorModel.<init>(Unknown Source)
at java.awt.image.IndexColorModel.<init>(Unknown Source)
at TestComponent.createMaskColorModel(TestComponent.java:145)
at TestComponent.imageMask(TestComponent.java:123)
at TestComponent.testImageMask(TestComponent.java:78)
at TestComponent.main(TestComponent.java:17)

Hi Jan!

Arrrrgh, that is hard!
Meanwhile I have pasted back my posted code, recompiled it, rerun it,
redebugged it, and got no error. When debugging up to the point in
ColorModel.java given in your stack trace
if (bits.length < numComponents) {
throw new IllegalArgumentException("Number of color/alpha "+
"components should be "+
numComponents+
" but length of bits array is "+
bits.length);
}
I got no exception. The values at that moment were bits.length=3 and
numComponents=3.
I debugged it with JDK 1.4.2_05 and 1.5.0. It is the same in both.
Can it be you used JDK 1.5.1, and Sun's ColorModel code is slightly
different there?
May be I should spend the time and download 1.5.1, too.

Anyway, Jan: Thanks for your effort!!
 
T

Thomas Fritsch

Roedy Green said:
Just making some guesses here. Is flipping back and forth between
integer and float pixel co-ordinates enough to get you off by one, so
that your generated image is shifted one pixel leaving an
uninitialised hairline?

You are familiar with the co-ordinate system that fits "between" the
pixels and leads to some gotchas. You know that drawRectangle will
create a rectangle measured in pixels taller by one than the height
you requested. The model is a pen slopping over and drawing below the
co-ordinate line.

I've read a bit about this, but certainly not enough as I feel now. ;-)
But: The only place where I draw a rectangle is in my erasePage (a white
fillRect for the whole large page), which I think is not the critical part.
I thought the critical part should be somewhere near drawImage. I guess your
reasoning applies to drawImage as well.
 
C

Chris Uppal

Thomas said:
I'm having trouble with drawing transformed images.
On some images there are hair-lines displayed at the top or
left edge, which are not part of the original image data,
but seem to be an artefact of some process under the hood.
I don't understand why and when such hair-lines occur.

I think that it's a side effect of rounding. More specifically, I /suspect/
that it might be a screw-up somewhere in Sun's code where one bitis /rounding/
floating point co-ordinates, and another part is /truncating/ them.

(Oh, BTW, your example ran fine for me with 1.5.0 b64 running on WinXP)

Anyway. If I set your screenResolution field to 72 (so that the transforma
have nice round numbers), then the display works fine. In that case the
transform that we end up with when we draw your graphic takes the unit square
(0,0) -> (1,1) onto the pixel co-ordinates:

(50.0, 192.0) -> (170.0, 72.0)

If I change screenResolution to 73, then the image shows hair-lines at both the
left and right edges, and in that case the transfomation is taking the unit
square to:

(50.69, 194.67) -> (172.36, 73.00)

Looking closely at the image, the "legitimate" pixels have been drawn with X
values in the range 51 to 171 inclusive, the hairlines are at X values 50 and
172. Taking that fact in tandem with the observation that the hairlines do not
extend the full height of the rectangle, and that there is no hairlines at the
bottom of the rectange, I suspect that it's a bug in the rendering code, and
that it has to do with rounding vs. truncation of X values.

As to how to fix it, I have no clever suggestions :-( Maybe there is some easy
way, but from where I sit it looks as if you may have to fiddle with the
transformation in imageMask() to ensure that it produces a transform that maps
to whole numbers of pixels.

-- chris
 
R

Roedy Green

I've read a bit about this, but certainly not enough as I feel now. ;-)
But: The only place where I draw a rectangle is in my erasePage (a white
fillRect for the whole large page), which I think is not the critical part.
I thought the critical part should be somewhere near drawImage. I guess your
reasoning applies to drawImage as well.

I would need to experiment, but perhaps this oddity pervades
everything, including Images.
 
J

John B. Matthews

Chris Uppal said:
I think that it's a side effect of rounding. More specifically, I
/suspect/ that it might be a screw-up somewhere in Sun's code where
one bit is /rounding/ floating point co-ordinates, and another part
is /truncating/ them.

(Oh, BTW, your example ran fine for me with 1.5.0 b64 running on WinXP)

Anyway. If I set your screenResolution field to 72 (so that the
transforma have nice round numbers), then the display works fine. In
that case the transform that we end up with when we draw your graphic
takes the unit square (0,0) -> (1,1) onto the pixel co-ordinates:

(50.0, 192.0) -> (170.0, 72.0)
[...]

I am unable to reproduce this effect with any of several values of
screenResolution, using java 1.4.2_07 on Mac OS X 10.4.2. I don't know
where Sun's code leaves off and Microsoft's begins, but could this be a
problem with the underlying OS rendering code?
 
T

Thomas Fritsch

Thomas said:
I got no exception. The values at that moment were bits.length=3 and
numComponents=3.
I debugged it with JDK 1.4.2_05 and 1.5.0. It is the same in both.
... with 1.5.0_04, as I should have written more precisely.
Can it be you used JDK 1.5.1, and Sun's ColorModel code is slightly
different there?
May be I should spend the time and download 1.5.1, too.
My assumption was nonsense. There is no 1.5.1 at java.sun.com, the
newest I found is 1.5.0_04 (aka "JDK 5.0 Update 4"), and "J2SDK 6.0".
 
C

Chris Uppal

John said:
I am unable to reproduce this effect with any of several values of
screenResolution, using java 1.4.2_07 on Mac OS X 10.4.2. I don't know
where Sun's code leaves off and Microsoft's begins, but could this be a
problem with the underlying OS rendering code?

Presumably, yes. I don't see how it could be in the Windows code itself (the
Windows graphics API doesn't understand floating point at all) but in the
OS-specific parts of the Java implementation.

But it must be in a fairly generic part of the OS-specific code (if that's the
problem at all, of course ;-) since I can reproduce the problem using 1.4.2 on
Linux as well as 1.5.0 on Windows.

-- chris
 
T

Thomas Fritsch

Thomas Fritsch said:
I'm having trouble with drawing transformed images.
On some images there are hair-lines displayed at the top or
left edge, which are not part of the original image data,
but seem to be an artefact of some process under the hood.
I don't understand why and when such hair-lines occur.
...
I tried a variant of my posted SSCCE. In method
testImageMask() I changed the imageMask() call
from
imageMask(24, 24, false,
new AffineTransform(24, 0, 0, -24, 0, 24),
new ByteArrayInputStream(pixelData));
to
imageMask(24, 24, false,
new AffineTransform(20.8, -12, -12, -20.8, 0, 24),
new ByteArrayInputStream(pixelData));

This should again render the X-shaped image, but now rotated
by 30 degrees.
What I really get, is the rotated "X"-image. But there is
also a gross black rectangle "behind" the upper quarter of
the "X"-image. It looks just like it was forgotten to clip
off these parts when rendering the image.

Can there be such a big bug either in my or in Sun's code??
I wonder which of these 2 possibilities I should prefer ;-)
 
J

John B. Matthews

Chris Uppal said:
Presumably, yes. I don't see how it could be in the Windows code itself (the
Windows graphics API doesn't understand floating point at all) but in the
OS-specific parts of the Java implementation.

But it must be in a fairly generic part of the OS-specific code (if that's the
problem at all, of course ;-) since I can reproduce the problem using 1.4.2 on
Linux as well as 1.5.0 on Windows.

-- chris

Interesting. I had overlooked the similar result on Linux. This makes me
wonder if the graphics card firmware & driver might play a role.
 
C

Chris Uppal

John said:
Interesting. I had overlooked the similar result on Linux. This makes me
wonder if the graphics card firmware & driver might play a role.

I doubt it. Since the driver/firmware only sees what Windows sees (on a
Windows box, of course!), and Windows doesn't see the floating point values,
the problem presumably is in the Java code somewhere.

-- chris
 
C

Chris Uppal

Thomas said:
What I really get, is the rotated "X"-image. But there is
also a gross black rectangle "behind" the upper quarter of
the "X"-image. It looks just like it was forgotten to clip
off these parts when rendering the image.

Ugh, yes. What's interesting is that the block seems to be the the same height
no matter how you rotate your image. Presumably, in the original case, the
rounding was allowing just the very edges of the rectangle to show.

I don't see how that could be anything /except/ a bug in Sun's code.

-- chris
 
T

Thomas Fritsch

Chris said:
Thomas Fritsch wrote:




Ugh, yes. What's interesting is that the block seems to be the the same height
no matter how you rotate your image.
Interestingly it has always /exactly/ 25% of the bounding-box height.
I'm sure this has a deeper meaning, but which?
Presumably, in the original case, the
rounding was allowing just the very edges of the rectangle to show.
I think so, too. The original hair-line-problem seems to be just the
peak of this iceberg.
I don't see how that could be anything /except/ a bug in Sun's code.
That means I should write a bug-report to Sun. But how to do this?
I consider condensing the SSCCE first (perhaps down to 30-40 lines).
This should be feasible, because the essence is drawing a transformed
BufferedImage (with transparency) onto a larger BufferedImage.

I tested the rotated example on Caldera-Linux/JDK-1.4.2_05. It looks the
same as on my WindowsXP. It would also be interesting again, how the
rotated example displays on MacOS.
 
T

Thomas Fritsch

jan said:
Hi Thomas,

I'm getting the following bomb:

Exception in thread "main" java.lang.IllegalArgumentException: Number of
color/alpha components should be 4 but length of bits array is 3

at java.awt.image.ColorModel.<init>(Unknown Source)
at java.awt.image.IndexColorModel.<init>(Unknown Source)
at TestComponent.createMaskColorModel(TestComponent.java:145)
at TestComponent.imageMask(TestComponent.java:123)
at TestComponent.testImageMask(TestComponent.java:78)
at TestComponent.main(TestComponent.java:17)
Finally reproduced your exception using JDK-1.3.1.
Therefore I had to rework the ColorModel creation to be more robust
across versions. I changed the posted example from
private ColorModel createMaskColorModel(boolean coloredIs1) {
int rgb = graphics.getColor().getRGB();
int cmap[] = { rgb, rgb };
int trans = coloredIs1 ? 0 : 1;
return new IndexColorModel(1, 2, cmap, 0, false, trans,
DataBuffer.TYPE_BYTE);
}

to
private ColorModel createMaskColorModel(boolean coloredIs1) {
int rgb = graphics.getColor().getRGB();
int cmap[] = new int[2]; // initialized with 0 (== transparent)
cmap[coloredIs1 ? 1 : 0] = rgb;
return new IndexColorModel(1, 2, cmap, 0, true, -1,
DataBuffer.TYPE_BYTE);
}

This doesn't change the semantics. As expected, the hair-line problem is
unchanged when running on JDK-1.4.2 or 1.5.0.
Now it is runnable on 1.3.1, too. Interestingly, the displayed image
looks fine here. There is no hair-line problem on JDK-1.3.1 !

So your reply was indeed very useful.
Many thanks, Jan!
 
J

Jim Sculley

Thomas said:
Interestingly it has always /exactly/ 25% of the bounding-box height.
I'm sure this has a deeper meaning, but which?

If you 'animate' the code by incrementing the scaling and translation
numbers in a loop, pausing for 100ms or so and repainting, the line
comes and goes.

Jim S.
 
T

Thomas Fritsch

Chris Uppal said:
I don't see how that could be anything /except/ a bug in Sun's code.
I've reported the bug to Sun now. Their estimated response time (until it
may show up in the public bug database) is 3 weeks.

Meanwhile I found a work-around. In case anybody wants to know it, here it
is:
The trick is to avoid a IndexColorModel with the mapping [0]=black and
[1]=transparent, and instead to use one with the opposite mapping
([0]=transparent and [1]=black). Of course all the pixel bits have to be
inverted then, in order to get the correct image again. Something like
b = (byte) ~b;
I don't know, why this makes the problem disappear. But it does. :)
 
C

Chris Uppal

Thomas said:
I've reported the bug to Sun now. Their estimated response time (until it
may show up in the public bug database) is 3 weeks.

It'll be interesting to see what they say about it.

Meanwhile I found a work-around. In case anybody wants to know it, here it
is:
The trick is to avoid a IndexColorModel with the mapping [0]=black and
[1]=transparent, and instead to use one with the opposite mapping
([0]=transparent and [1]=black). Of course all the pixel bits have to be
inverted then, in order to get the correct image again. Something like
b = (byte) ~b;


How odd...

-- chris
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,009
Latest member
GidgetGamb

Latest Threads

Top