Iterate through pixels in a System.Windows.Media.ImageSource?

O

Omatase

I am trying to code functionality that will trim transparent or white
pixels from the sides of an image. I already have cropping code that
works well but I can't find any code to analyze the pixels one at a
time to determine how much to crop.

Here is some code I found using CopyPixels, but I can't find any
information about how to use the returned pixels.

for (int i = 0; i < image.Height; i++)
{
Int32Rect currentSample = new Int32Rect(0, i, (int)image.Width,
1);
byte[] pixels = new byte[4];
int stride = (image.PixelWidth * image.Format.BitsPerPixel + 7) /
8;

image.CopyPixels(new Int32Rect((int)i, (int)1, 1, 1), pixels,
stride, 0);
}

I think I'm telling it to grab the first pixel in the image with this
code, but there isn't really any good info on using CopyPixels out
there. And if I'm correct, that I'm only grabbing the first pixel,
then it's more confusing as to why my pixel array has 4 bytes in it. I
am assuming that means it's a 32 bit color and is just being
represented that way but I don't know for sure.

If anyone can shed some light on this CopyPixel method or on an easier
way to accomplish what I'm trying to do, please help, thanks.
 
O

Omatase

Banking on my assumptions I built out the code and they appear to have
been correct since the final product works as I expect it to. Here is
the final code.

BitmapSource image = (BitmapSource)_mouseOveredImage.Source;

int? leastSignificantPixelY = null;
int? mostSignificantPixelY = null;
int? leastSignificantPixelX = null;
int? mostSignificantPixelX = null;

// find the first and last non transparent row
for (int i = 0; i < image.Height; i++)
{
for (int j = 0; j < image.Width; j++)
{
Int32Rect currentSample = new Int32Rect(0, i, (int)
image.Width, 1);
byte[] pixels = new byte[4];
int stride = (image.PixelWidth * image.Format.BitsPerPixel +
7) / 8;

image.CopyPixels(new Int32Rect((int)j, (int)i, 1, 1), pixels,
stride, 0);

if (pixels[0] > 0 || pixels[1] > 0 || pixels[2] > 0 || pixels
[3] > 0)
{
if (leastSignificantPixelY == null || i <
leastSignificantPixelY)
{
leastSignificantPixelY = i-2;
}

if (i > mostSignificantPixelY.GetValueOrDefault())
{
mostSignificantPixelY = i+2;
}

if (leastSignificantPixelX == null || j <
leastSignificantPixelX)
{
leastSignificantPixelX = j-2;
}

if (j > mostSignificantPixelX.GetValueOrDefault())
{
mostSignificantPixelX = j+2;
}
}
}
}

CroppedBitmap nonTransparentPortion = new CroppedBitmap((BitmapSource)
_mouseOveredImage.Source,
new Int32Rect(leastSignificantPixelX.GetValueOrDefault(),
leastSignificantPixelY.GetValueOrDefault(),
mostSignificantPixelX.GetValueOrDefault() -
leastSignificantPixelX.GetValueOrDefault(),
mostSignificantPixelY.GetValueOrDefault() -
leastSignificantPixelY.GetValueOrDefault()));

_mouseOveredImage.Source = nonTransparentPortion;
 
P

Peter Duniho

Omatase said:
I am trying to code functionality that will trim transparent or white
pixels from the sides of an image. I already have cropping code that
works well but I can't find any code to analyze the pixels one at a
time to determine how much to crop.

Here is some code I found using CopyPixels, but I can't find any
information about how to use the returned pixels.

for (int i = 0; i < image.Height; i++)
{
Int32Rect currentSample = new Int32Rect(0, i, (int)image.Width,
1);
byte[] pixels = new byte[4];
int stride = (image.PixelWidth * image.Format.BitsPerPixel + 7) /
8;

image.CopyPixels(new Int32Rect((int)i, (int)1, 1, 1), pixels,
stride, 0);
}

I think I'm telling it to grab the first pixel in the image with this
code, but there isn't really any good info on using CopyPixels out
there. And if I'm correct, that I'm only grabbing the first pixel,
then it's more confusing as to why my pixel array has 4 bytes in it. I
am assuming that means it's a 32 bit color and is just being
represented that way but I don't know for sure.

If anyone can shed some light on this CopyPixel method or on an easier
way to accomplish what I'm trying to do, please help, thanks.

Unfortunately, I don't have any experience with the WPF-specific
operations. However, I can offer what comments occur to me based on
what I do know.

Reading the docs for BitmapSource.CopyPixels(), they state that the
stride you pass is for the array, not the original BitmapSource. So
your calculated stride would be inappropriate, except for an array that
is exactly the same size as that needed for the entire image.

As for the array itself, yes…it seems logical to me that the reason you
need four bytes is if you are handling an image format that is 32bpp.
Of course, if the PixelFormat is different, some different array length
might be necessary.

Also, I note that the CopyPixels() method takes as an argument any kind
of Array instance, not just a byte[]. That suggests to me that the data
from the bitmap is copied byte-for-byte (probably using
Buffer.BlockCopy(), but it could be anything) and that the method
doesn't concern itself with the specific format of the Array instance.
The implication being that you can provide an Array of the best format
for the given pixel format; for a 32bpp image, this might actually be a
uint[] rather than a byte[].

Your code also has a couple of problems with the source rectangle
calculation. In the calculation for "currentSample" (which does not
appear to be used), the calculated rectangle is for an entire scan line,
not an individual pixel as you state you are trying to get. On the
other hand, the calculation for the Int32Rect you actually pass to
CopyPixels() misuses the variable "i", and will enumerate the individual
pixels in the _second_ scan line of the image (y = 1, while y = 0 is the
first scan line), falling outside the actual range of the image if the
height of the image is greater than the width.

(You also inexplicably cast the numeric literal "1" to "int" for some
reason, but at least that's not breaking anything).

I'm assuming you have a good reason for using the WPF classes. Given
that assumption, my main suggestion is that if you are interested in
processing the raw pixel data (and for your stated task, I think that's
probably the best way), you should retrieve the entire image's pixels
all at once, rather than doing it piecemeal. That will avoid the
repeated overhead of copying, albeit at the expense of potentially much
more memory overhead, depending on the size of the bitmaps involved.

But assuming the bitmaps are reasonably sized, doing the whole thing at
once shouldn't be an issue. If they aren't reasonably sized, then
perhaps the one row/column at a time approach is necessary and may not
be as inefficient as it otherwise would be, simply because you do get a
much larger number of pixels with each call to CopyPixels() than you
would with a normal-sized bitmap.

In any case, you will need to correct your input to the CopyPixels()
method based on my comments above. Getting the source rectangle and the
stride correct would of course be critical in having success. Note also
that your code will either need to coerce images to a specific
PixelFormat, or will need to conditionally handle different formats.
I've done both in the past, and they each have their pros and cons. The
former can be easier, but with the latter at least you don't have to
worry about making sure your format conversion to and from is exactly
invertible.

Hope that helps.

Pete
 
P

Peter Duniho

Omatase said:
Banking on my assumptions I built out the code and they appear to have
been correct since the final product works as I expect it to. Here is
the final code.

IMHO, the code you posted is overly inefficient. Based on my limited
understanding of the API, I've posted below a version that is one
approach I might use.

Assumptions:

– your code appears to treat pixels as "non-significant" _only_ if
the pixel is black _and_ completely transparent. I find this odd –
after all, if the alpha is 0, what difference would it make what color
the pixel is? I've preserved your logic, but IMHO if transparency is
the criteria, it probably makes more sense to just look at the alpha
channel.

– in the interest of compromise, while my example doesn't retrieve
all the pixels at once, it does retrieve one scan line at a time;
hopefully this is at least a noticeable improvement in performance, as
compared to extracting one pixel at a time

– I've also preserved your assumption that the input format is 32bpp;
given that you are making that assumption, there is no need to take the
format into account when calculating the stride.

– also given the above assumption, the pixels can be examined one
32-bit value at a time; this particularly simplifies the code given your
apparent definition of "non-significant", because only one comparison
needs to be done

Here's the code:

Int32Rect GetSignificantBounds(BitmapSource source)
{
if (source.Format.BitsPerPixel != 32)
{
throw new ArgumentException("input must be 32 bits per pixel",
"source");
}

int x1 = source.Width, x2 = 0, y1 = source.Height, y2 = 0;
uint[] pixels = new uint[source.Width];
Int32Rect rectScanline = new Int32Rect(0, 0, source.Width, 1);

for (int y = 0; y < image.Height; y++)
{
bool fFoundSignificant = false;

rectScanline.Y = y;
source.CopyPixels(rectScanline, pixels, source.Width, 0);

for (int x = 0; x < image.Width; x++)
{
if (pixels[x] > 0)
{
x1 = Math.Min(x1, x);
x2 = Math.Max(x2, x);

fFoundSignificant = true;
}
}

if (fFoundSignificant)
{
y1 = Math.Min(y1, y);
y2 = Math.Max(y2, y);
}
}

return new Int32Rect(x1, y1, x2 - x1, y2 - y1);
}

The main efficiencies above include:

– Allocate the pixel buffer only once
– Examine pixels one full 32-bit value at a time
– Only update y value for significant pixel once per scan line
– Extract a full scanline at a time

There are also some minor things, like the maintenance of the source
rectangle and of the significant pixel coordinate boundaries
(Nullable<T> is overkill, especially since you still need to do the
min/max comparison anyway).

Hope that helps!

Pete
 

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

Top