Sign in to follow this  
dario_ramos

Signed image on a texture

Recommended Posts

Hi, I'm trying to load a signed 16 bit image on a texture. The requirements forbid me from changing the image pixels to the unsigned format, and using a local transformed copy is an option only if the ones below are not feasible. So first I checked if there was a signed pixel format I could pass to CreateTexture. Maybe I didn't look well enough, but the only signed formats said something about "bump-maps", and it didn't seem like I could use them... Correct me if I'm wrong. So my next idea is using a pixel shader to transform pixels right before rendering them. But I would be doing that on every render cycle, which doesn't sound good... What do you think?

Share this post


Link to post
Share on other sites
(I'm assuming you're using D3D9. Things are simpler in D3D10.)

There's nothing particularly bad about D3DFMT_Q16W16V16U16 except that it's not supported on most cards. If you're writing for a specific card and it supports the format, go ahead and use it.

It's also not a big deal to transform from unsigned to signed in the pixel shader. Modern 3D cards have lots of processing power and you might as well use it. Just remember that 0 to 0.5 is the positive range and 0.5 to 1 is the negative range.

Share this post


Link to post
Share on other sites
Quote:
Original post by ET3D
Just remember that 0 to 0.5 is the positive range and 0.5 to 1 is the negative range.


Doesn't that depend on how you're remapping? For the common (1 - 2 * rgb) it certainly holds true, but (2 * rgb - 1) would work too and remap [0;1] directly to [-1;1], with [0;0.5] being negative and [0.5;1] being positive.

Just curious really and atm quite worried this was a dumb question [smile]

Share this post


Link to post
Share on other sites
The highest bit of a signed integer is its sign :)

I was thinking about multiply-add (2 * rgb - 1) but it requires that the data be massaged on the CPU before sending it to the card.

Eyal's method works without processing on CPU side. I think that it does require a conditional branch in addition to a multiply-add in the pixel shader. However, this is still easy for the GPU.

Share this post


Link to post
Share on other sites
Thanks guys, I used a pixel shader since we code for many video cards, and if there was a drop in performance, I sure didn't notice it :)

Edit: I thought I made it, but it still doesn't show the image correctly...

Share this post


Link to post
Share on other sites
Quote:
Original post by ET3D
(I'm assuming you're using D3D9. Things are simpler in D3D10.)


You're right, I'm using D3D9

Quote:
Original post by ET3D
It's also not a big deal to transform from unsigned to signed in the pixel shader. Modern 3D cards have lots of processing power and you might as well use it. Just remember that 0 to 0.5 is the positive range and 0.5 to 1 is the negative range.


What I want to do is the opposite; I want to map from signed to unsigned. I tried adding 0.5 and substracting 1 if the result went over 1, but it didn't work. Someone told me that when adding, if you go over 1, it saturates (i.e. it remains in 1). I don't think that's exactly what's going on, since I'm seeing the exact same image...

Share this post


Link to post
Share on other sites

This D3DFMT_Q16W16V16U16 format got me curious, so for the sake of messing with exotics I gave this a go by loading up such a texture with a gradient from -32768 to 32767. It turns out to be unsupported on my GF8600M but works on my trusty old X1900. For future reference, this gradient seems to come out of a sampler in the range of [-1;1]. So it indeed still needs a multiply-add in the pixel shader as Nik said to take it to [0;1].

I'm still not clear about the positive/negative ranges though. In my test app it seems the gradient is mapped continuously from [-32768;32767] to [-1;1]. Is the 'highest bit' remark referring to the typical external 16 bit image format? I'm feeling quite dense, but it doesn't seem to apply to the rendering internals?

Quote:
Edit: I thought I made it, but it still doesn't show the image correctly...


Sorry about the tread hijack above. Could you elaborate on how it's not looking correct or perhaps post an image showing the problem?

Share this post


Link to post
Share on other sites
Quote:
Original post by remigius

I'm still not clear about the positive/negative ranges though. In my test app it seems the gradient is mapped continuously from [-32768;32767] to [-1;1]. Is the 'highest bit' remark referring to the typical external 16 bit image format? I'm feeling quite dense, but it doesn't seem to apply to the rendering internals?

Quote:
Edit: I thought I made it, but it still doesn't show the image correctly...


Sorry about the tread hijack above. Could you elaborate on how it's not looking correct or perhaps post an image showing the problem?


It's alright, I'm also trying to display an image with pixels value in that range. I'm using DICOM images, so I don't know if you'll be able to see those (I use DCM Editor to extract a .pix and I see that using Quantmaster ImageJ)... So I'll post screenshots:

Here's how it's supposed to look (loaded in ImageJ as a 16-bit signed image):

The right way

Check ImageJ's Histogram below: it shows that there are negative values.

And here's what my software shows: the problem is that it reads pixel values as unsigned. Therefore, I'm trying to convert those using a pixel shader, but I'm not getting it right.

My way

By the way, the image is drawn on a texture with D3DFMT_L16 format (it'as actually composed by many textured quads, but they all have that format)

Share this post


Link to post
Share on other sites

Using the D3DFMT_L16 format, the comments by Eyal and Nik became clear to me. It seems this 'bit worry' does apply to this unsigned format, whereas the signed D3DFMT_Q16W16V16U16 seems to automatically map continuously to [-1;1]. Anyway, when I stick my gradient in this format I get the following result:



This matches Eyals comments about the positive/negative ranges. Nik's comment about the conditionals also makes sense to me now, since you could use q = q > 0.5 ? q - 0.5 : q + 0.5; (q is just a local shader var holding texture.r) to remap this to a continuous unsigned interval [0;1]:



Alternatively you could use q = (q + 0.5) % 1; which forgoes the conditional and produces the same result. Both methods seem to have some continuity artifacts at q = 0 and q = 0.5 when using linear filtering, but switching to point filtering works out well.

I hope this is somehow remotely relevant to your problem [smile]

Share this post


Link to post
Share on other sites
Quote:
Original post by remigius

Alternatively you could use q = (q + 0.5) % 1; which forgoes the conditional and produces the same result. Both methods seem to have some continuity artifacts at q = 0 and q = 0.5 when using linear filtering, but switching to point filtering works out well.

I hope this is somehow remotely relevant to your problem [smile]


Yes it is! In my shader, I use the conditional formula and it looks almost right, save for some "continuity artifacts". I tried using point filtering and they were gone, but the resulting quality is too low, especially when I zoom in. Check it out:

With linear filtering:
Using linear filtering

With point filtering:
Using point filtering

I asked the higher-ups about using point filtering, but they said the quality was "unacceptable". I tried other filters (anisotropic, gaussian, pyramidal), but it seems they have the same continuity problems.

One idea a partner suggested was mapping pixels "near" 0 and 1 to 0.5 (using an "epsilon" value to define that range). Other possibilities are doing the filtering ourselves (I find it hard to best Direct3D...). What do you guys think?

Share this post


Link to post
Share on other sites

For the issue at q = 0, setting your samplers addressing modes (AddressU, AddressV) to clamp does the trick. Values 'near' 0 (q = 0.5) are trickier, since you can't really tell when they happen with some epsilon. These continuity things go wrong in the sampler, so when the sample reaches your shader it'll already have been interpolated to the wrong value. You could check for discontinuities by sampling neighboring pixels, but then you might as well jump straight to implementing linear filtering.

Alternatively you could render the signed 16 bit image to an unsigned rendertarget of the exact same size with point filtering. Next you render this result to your target surface with linear filtering, giving you hardware interpolation over the correct values. It's a detour, but it's correct and should still be quite fast. Whether it's feasible for your usecase with a larger number of quads however, I can't say.

Share this post


Link to post
Share on other sites
Quote:
Original post by remigius

For the issue at q = 0, setting your samplers addressing modes (AddressU, AddressV) to clamp does the trick. Values 'near' 0 (q = 0.5) are trickier, since you can't really tell when they happen with some epsilon. These continuity things go wrong in the sampler, so when the sample reaches your shader it'll already have been interpolated to the wrong value. You could check for discontinuities by sampling neighboring pixels, but then you might as well jump straight to implementing linear filtering.


I thought you meant I had to set those modes using SetSamplerState when initializing the device. I did that and nothing changed. However, I looked around some more about this continuity issues and found that, aside from tex2D, there are tex2Dlod and tex2dgrad functions. When using those, it seems you can change sampler states inside the pixel shader. Is that true? I read tex2Dlod and tex2Dgrad's documentation but didn't understand how to apply them to my problem... Is that possible?

Quote:
Original post by remigius
Alternatively you could render the signed 16 bit image to an unsigned rendertarget of the exact same size with point filtering. Next you render this result to your target surface with linear filtering, giving you hardware interpolation over the correct values. It's a detour, but it's correct and should still be quite fast. Whether it's feasible for your usecase with a larger number of quads however, I can't say.


I asked my partners and they said this approach would be OK if all our images were single frame, but we also do multiframe image playback... So I'm more interested in trying a solution using tex2Dlod or tex2Dgrad, but I'll need some guidance :P

Share this post


Link to post
Share on other sites
I just tried this instead of tex2D:


float4 color = tex2Dgrad( Texture0, input, ddx(input), ddy(input) );



But I'm still getting those nasty artifacts...

Edit: This doesn't work either:


float4 color = tex2Dlod( Texture0, float4(input, 0.0, 1.0) );


Share this post


Link to post
Share on other sites
Quote:
Original post by dario_ramos
I thought you meant I had to set those modes using SetSamplerState when initializing the device. I did that and nothing changed.


I did mean that, but it seems that particular issue I had around q=0 is no problem in your scenario and we're just stuck with the tricky one around q=0.5.

Quote:
However, I looked around some more about this continuity issues and found that, aside from tex2D, there are tex2Dlod and tex2dgrad functions. When using those, it seems you can change sampler states inside the pixel shader. Is that true? I read tex2Dlod and tex2Dgrad's documentation but didn't understand how to apply them to my problem... Is that possible?


As far as I know, tex2Dlod and tex2dgrad are a lot less magical than that. If I understand them correctly, these are meant to be used as replacements for tex2D when you need to call it from within some kind of flow control or branching in your pixel shader.

Normally the sampler can judge which mip level you'd want by the (2nd?) derivate of the texture coordinates you pass to tex2D. With tex2D instructions within flow control however (for example if statements) this derivate can't be depended on since it may differ wildly from pixel to pixel, so you use these to help the sampler out in selecting a mip level.

We shouldn't need tex2D instructions within flow control though and you probably shouldn't be using any mip levels anyway if you want to use your image at maximum resolution, so these are of no use here I think. I could be way off here though, so please correct me if I'm wrong.

Quote:
But I'm still getting those nasty artifacts...


Yep, same here with my little test project. I tried a number of things but the problem remains that the sampler simply returns wrong interpolations when using anything but point filtering. Crappy continuity checking introduced more artifacts than it fixed, but I tried implementing some simple linear filtering which does work a bit:


    point         naive filter




float Sample(float2 tex)
{
// Sampler is set up with point filtering!
float4 color = tex2D(Sampler, tex);
float q = color.r;
return q < 0.5 ? q + 0.5 : q - 0.5;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
// Change 256 to the size of your texture,
// it only supports square textures atm
float2 texelpos = 256 * input.Tex0;
float2 lerps = frac( texelpos );
float texelSize = 1.0 / 256.0;

float4 sourcevals[4];
sourcevals[0] = Sample(input.Tex0);
sourcevals[1] = Sample(input.Tex0 + float2(texelSize, 0));
sourcevals[2] = Sample(input.Tex0 + float2(0, texelSize));
sourcevals[3] = Sample(input.Tex0 + float2(texelSize, texelSize));

float4 lerpval = lerp( lerp( sourcevals[0], sourcevals[1], lerps.x ),
lerp( sourcevals[2], sourcevals[3], lerps.x ),
lerps.y );

return float4(lerpval.r, 0, 0, 1);
}






It's still a far cry from the default hardware linear filtering though. So either I messed something up, or the hardware filtering is a lot more complex and/or adaptive than my simple naive filter.

With this said and done, I still think your best bet is the 2 'pass' approach described above, where you convert from signed to unsigned in the 1st pass and let the shader do hardware linear filtering on the correct unsigned values in the 2nd. Multiframe image playback need not be a showstopper usecase, since even with this 2 pass approach any non-ancient graphics hardware should be able to render this in the order of hundreds of frames per second.

Share this post


Link to post
Share on other sites
This topic is haunting me [smile] I realized over coffee this morning that we've been sitting on the simple answer all along:

Quote:
Original post by remigius
... the problem remains that the sampler simply returns wrong interpolations when using anything but point filtering ...


By setting up 2 samplers, one with point filtering and one with linear filtering, you can just take a point sample to determine if the remapped value is in a problem area (q=0.5). If so you can use the point sample there and if not, just use the linear sample.



float SamplePoint(float2 tex)
{
// Sampler has point filtering
float4 color = tex2D(Sampler, tex);
float q = color.r;
return q < 0.5 ? q + 0.5 : q - 0.5;
}

float SampleLinear(float2 tex)
{
// Sampler2 has linear filtering, same texture as Sampler
float4 color = tex2D(Sampler2, tex);
float q = color.r;
return q < 0.5 ? q + 0.5 : q - 0.5;
}


float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float pointSample = SamplePoint(input.Tex0);
float linearSample = SampleLinear(input.Tex0);
float epsilon = 0.15;

// use the point sample around q=0.5, so texture value around 0, linear otherwise
float sample = ( abs(pointSample - 0.5) < epsilon ) ? pointSample : linearSample;

return float4(sample, 0, 0, 1);
}



Sorry about the detour, I hope this is still of some use.


[Edited by - remigius on February 11, 2010 1:40:21 AM]

Share this post


Link to post
Share on other sites
Quote:
Original post by remigius

By setting up 2 samplers, one with point filtering and one with linear filtering, you can just take a point sample to determine if the remapped value is in a problem area (q=0.5). If so you can use the point sample there and if not, just use the linear sample.



Groovy! My only problem is that I don't know how to set up another sampler and use it in a pixel shader; I thought the device could use only one sampler (the one set on initialization)

Share this post


Link to post
Share on other sites

I don't have any experience using the default samplers from my pixel shader, I just set them up in my effects file. Here are the samplers I defined in my test project. I'm not sure it's good form to use the same Texture in 2 samplers, but it seems to work. This goes into the top part of your effects (shader) file:


texture Texture;

sampler Sampler = sampler_state
{
Texture = (Texture);

MinFilter = Point;
MagFilter = Point;
MipFilter = Point;
AddressU = Clamp;
AddressV = Clamp;
};

sampler Sampler2 = sampler_state
{
Texture = (Texture);

MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};


It's probably the best idea to set the texture directly onto the effect, instead of mucking about with registers to set it through the device. I'm not sure how this works in unmanaged D3D, but ID3DXBaseEffect::SetTexture sounds like a good bet [smile]

Share this post


Link to post
Share on other sites
Quote:
Original post by remigius

It's probably the best idea to set the texture directly onto the effect, instead of mucking about with registers to set it through the device. I'm not sure how this works in unmanaged D3D, but ID3DXBaseEffect::SetTexture sounds like a good bet [smile]


First of all, thanks for the answers. I'm learning a lot from this.
Anyhow, it seems you do things another way. We don't use effects; we use .psh files together with D3DXCompileShaderFromFile and IDirect3DDevice9::CreatePixelShader. By the way, I never used effects; just heard a partner mention them once :P
So, I tried adding the sampler declarations in my .psh file; however, it seems that I must also change the sampler being used by the device using SetSamplerState, because even if I do this...


float4 main( float2 input: TEXCOORD0 ) : COLOR0 {

float4 pointSample = tex2D( pointSampler, input );
float4 linearSample = tex2D( linearSampler, input );
float epsilon = 0.1;

// For values close to 0.5, use point filtering sample (for other cases, use linear filtering sample).
// This is because linear filtering has continuity issues around 0.5
float4 color = pointSample; /*( abs(pointSample.r - 0.5) < epsilon ) ? pointSample : linearSample;*/

if (pixelsAreSigned){
color.r = adjustForSignedFormat( color.r );
color.g = adjustForSignedFormat( color.g );
color.b = adjustForSignedFormat( color.b );
}
color = adjustWindowLevel( color, window, level );
if( invert ) color = colorInversion( color );
color = adjustGamma( color, gamma );
color.w = 0;
return color;
}





... that is, using the point filtering sample ALWAYS, I still see the same as when I use the linear one. And my guess is that it's because the D3D device still has liner filtering set, and prevails over the shader's.
Why did I do this? Because if I uncomment the logic which chooses the linear or point sample it stills looks bad:

Cuca

No clue what's going on in this case...

Share this post


Link to post
Share on other sites

You're welcome, but I'm afraid I can't help you with the .psh details or how the samplers in there should be set up. I messed around with that a little years ago and quickly fled to the safety of the effects framework [smile]

I did notice this:


// For values close to 0.5, use point filtering sample (for other cases, use linear filtering sample).
// This is because linear filtering has continuity issues around 0.5
float4 color = pointSample; /*( abs(pointSample.r - 0.5) < epsilon ) ? pointSample : linearSample;*/

if (pixelsAreSigned){
color.r = adjustForSignedFormat( color.r );
color.g = adjustForSignedFormat( color.g );
color.b = adjustForSignedFormat( color.b );
}



You should either first adjust for the signed format before you do that little trick, or replace the test with (abs(pointSample.r) < epsilon). You need to check the adjusted value near 0.5, but if you use the unadjusted value in your test like this you need to check if it's near 0.

Share this post


Link to post
Share on other sites
Quote:
Original post by remigius

You should either first adjust for the signed format before you do that little trick, or replace the test with (abs(pointSample.r) < epsilon). You need to check the adjusted value near 0.5, but if you use the unadjusted value in your test like this you need to check if it's near 0.


That's right! Though it still doesn't look perfect: with epsilon = 0.15, there are less artifacts. So, let's reduce epsilon, I said to myself. However, when I set it to 0.05, all artifacts disappeared, but I lost a lot of colors (the image got flattened to a few shades of gray). In the intermediate epsilon values, I couldn't find one which kept all colors and removed all artifacts!

Then I thought: perhaps it's applying linear filtering twice, since the D3D device sets its sampler state to D3DTEXF_LINEAR. So I set it to D3DTEXF_NONE, hoping the shader would work it out. But the quality got very crappy, as if the shader didn't use the linear filter!

Then I noticed that the first parameter of SetSamplerState was a stage. That prompted me to set stage 0 to point filtering and 1 to linear filtering, and using only one sampler in the pixel shader. But it looked just like doing only point filtering. Crappy.

Enough of this for me today... :P

Share this post


Link to post
Share on other sites
I ended up using Intel's IPL to convert the image's pixels from signed to unsigned (when loading the texture). It only requires complementing the first bit. This is done only once, so it doesn't hurt performance.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this