-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(layers): Trim Top of Pyramids #633
base: main
Are you sure you want to change the base?
Conversation
@s-n-i please try this out! |
@manzt Do you see an issue with this conceptually? |
Have been struggling to catch up on related viv thinfs. Will try to review this weekend! |
I built Avivator from this codebase and loaded the following dataset into it: https://viv-files.ci.aws.labshare.org/LuCa-7color_Scan_c(0-5)/ Here are my observations: The black box no longer increases in size, relative to the image size, when zooming out. So, this issue looks fixed. I also found a new issue. The dataset disappears on levels 7 and above. Here is a screenshot, I changed the background to white: |
So there is something wrong with my logic then probably - an edge case i had not considered. I'll have a look. |
@s-n-i should be fixed now, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some ideas regarding where these changes need to occur. Also does the PR title still describe the changes?
packages/layers/src/multiscale-image-layer/multiscale-image-layer.js
Outdated
Show resolved
Hide resolved
packages/layers/src/multiscale-image-layer/multiscale-image-layer.js
Outdated
Show resolved
Hide resolved
data: tiles.map(d => | ||
clip({ | ||
data: d.data, | ||
width: tiles[0].width, | ||
height: tiles[0].height | ||
}) | ||
), | ||
width: | ||
isWidthUnderTileSize && tiles[0].height === tileSize | ||
? clippedWidth | ||
: tiles[0].width, | ||
height: | ||
isHeightUnderTileSize && tiles[0].height === tileSize | ||
? clippedHeight | ||
: tiles[0].height |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, I seem to remember adding padding to tiles from the loaders. It seems like we are just reversing that here? Perhaps this warrants a change to the loaders?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this was also my first instinct but I don't think we do anymore after you found that specific WebGL setting for controlling of what number the textures should be a multiple, which we set to 1.
data: tiles.map(d => | ||
clipper.clip({ | ||
data: d.data, | ||
width: tiles[0].width, | ||
height: tiles[0].height | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why we need to use the first tile over and over? The with
and height
should be consistent.
data: tiles.map(d => | |
clipper.clip({ | |
data: d.data, | |
width: tiles[0].width, | |
height: tiles[0].height | |
}) | |
data: tiles.map(clipper.clip), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Glad to see this working. Just having some trouble understanding the logic. I wonder if clipper
function was too much indirection, since we used a representative tile.
Maybe,
let tiles = await Promise.all(selections.map(getTile));
tiles = clipTiles({ loader, resolution, tiles });
const tile = {
data: tiles.map(d => d.data),
width: tiles[0].width,
height: tiles[0].height,
}
and that way all the logic is isolated in one function (which is easy to modify or remove).
const planarSize = Object.values(getImageSize(loader[0])); | ||
const [clippedHeight, clippedWidth] = planarSize.map(size => | ||
Math.floor(size / 2 ** resolution) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This relies on the ordering of key names in getIamgeSize
, if those were to change this could break.
const planarSize = Object.values(getImageSize(loader[0])); | |
const [clippedHeight, clippedWidth] = planarSize.map(size => | |
Math.floor(size / 2 ** resolution) | |
); | |
const planarSize = getImageSize(loader[0]); | |
const [clippedHeight, clippedWidth] = [planarSize.height, planarSize.width].map(size => | |
Math.floor(size / 2 ** resolution) | |
); |
* }} | ||
* @return {{ clip: function, height: number, width: number }} | ||
*/ | ||
const createTileClipper = ({ loader, resolution, tileSize }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tileSize
is a property of loader[number]
.
width: | ||
clipper.width < tileSize && tiles[0].width === tileSize | ||
? clipper.width | ||
: tiles[0].width, | ||
height: | ||
clipper.height < tileSize && tiles[0].height === tileSize | ||
? clipper.height | ||
: tiles[0].height |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generally am just having a hard time understanding the logic of this bit of code. Now the logic for clipping the data and clipping the width and height are separated, which makes it difficult to reason through.
Just following up on this. |
@s-n-i I lost track of this but I recall this actually not having mattered much in the end for you. Happy to push this ahead if you feel you need this. Let me try to refresh myself of the issue here. |
Right yes, you had said the performance difference was actually not that great. So I sort of stopped working on this, but if you still feel you need it, we can restart the effort. Happy to oblige. |
Restarting the effort would be great. Currently certain datasets appear to get padded with 0 intensity values. I think this PR would allow them to render correctly. |
I was also wondering what the approximate timeframe for this effort is going to be. |
I can take care of this by the end of next week. I'll start working on it tomorrow. |
@s-n-i please have a look. if this works, i will merge |
is this data public? |
I spent some time to find and test a publicly sharable dataset because the one above is not public. Here is a link: https://www.filemail.com/d/gnejlokjxpfodfg The size of the black box changes based on zoom: |
I am following up on this. Please let me know if anything else is needed. |
Can you repost the file since it expired? I must have missed this while I was out on vacation. Happy to finish this. |
Here it is: |
hey @s-n-i can you be sure you had the latest version of my PR? I am not seeing the behavior you're describing. I am seeing the "extra" tile in the height direction when you move to the first level away from the lowest one but that is not part of this fix. |
P.S I have tried this in both FF and Chrome. |
@s-n-i I think that might be a bit out of scope here. The goal was only to do this for the "top levels" of the pyramid (i.e., those levels, not tiles, that are too small) but what you are describing would have to be done at all levels of the pyramid since zarr tiles are often (always?) padded. I understand that this might be disappointing but I am not sure I am up for doing this for all zarr tiles. @manzt are you aware of some workaround or the like that would stop zarr from returning padded tiles? |
Also @s-n-i, why do you feel this is needed? Are you not using a black background? Are you using color maps a lot? Is there a huge performance knock you are seeing? |
Yes, I am using color maps and different background colors. In fact, I have a custom shader, which allows the 0 intensity color to be specified in a look up table. Therefore, the 0 intensity color (which is also the color of the padding) can be different form the background color. Are you asking about the performance difference between a dataset with small top levels and the same one without? |
I have also tested the PR with a dataset with small top levels. The size of the padding stays the same relative to the size of the image as the user zooms. |
@s-n-i I see what is happening. The background image is displaying black, which gets me back to the fact that this might be worth doing on the loader directly, as we do for |
I think I see the problem. FWIW, I believe the underlying tiles for tiffs are also square, it's just we are using a higher-level API from geotiff than in zarr.js. Yes, tiles (or chunks) are always complete in Zarr. This way any key-value pair is the exact shape described in the We currently use Since the pixelSource interface has a Either way, I think we should certainly handle this case in Zarr, it's just a matter of whether the logic should live within the loaders or at a higher level. |
I think one argument for doing this at the loader level (either universally shared among all loaders or per-loader) is that this problem exists on non-multiscale images i.e., those that have to be stitched together from tiles. That was the problem @s-n-i was seeing after implemented the (working) fix for this specific problem. I would be open to writing some sort of inheritable method that all |
I think the choice is somewhat arbitrary, but I'll try to describe my thinking. We have a The await source.getTile(/* ... */) // { ..., width: tileSize, height: tileSize } Whereas we have a await source.getRaster(/* ... */) // { ..., width: shape[1], height: shape[0] } With that said, I'm happy to be convinced that this should live in the
Or just a utility function. I'd really like to avoid introducing some unnecessary class hierarchy. Again, this shared behavior is what signals to me we should keep the |
await source.getTile(/* ... */) // { ..., width: tileSize, height: tileSize } I think is actually not the case since the viv/packages/loaders/src/tiff/pixel-source.ts Lines 41 to 49 in 2b28cc1
MultiscaleImageLayer just expects accurate information from getTile (regardless of whether the tile is "complete" or "trimmed"). The _getTileExtent which calculates the window is actually agnostic to the PixelSource so it could serve as the basis for this.
One thing I have thought about recently is that while it is "easy" to pass in coordinates for I agree a unified approach would be good here. But if we mandate that So my top option would be to use "normal" slicing access for zarr if possible via |
Right, sorry for not being clear. I wrote that to point out the original intent with the
Yes, in order to make our implementations consistent, we will likely need to change the current behavior of either the Zarr or Tiff implementations. My focus in this discussion is to clarify where the change should occur, and my proposal was to make the Tiff source more like Zarr. If I understand you correctly, you are in favor of the opposite. As I said before, I think the decision is fairly arbitrary, but the semantics/intent for the
This isn't entirely true. If the underlying library for reading the tiled data source supports trimming the tiles, then implementing a PixelSource should be minimal effort. But if it does not, then the developer will need to implement this trimming to be consistent with our implementations. At the file level, the majority the pyramidal image formats I'm aware of store complete tiles to avoid storing additional tile-specific metadata. My suggestion for returning complete tiles is to make it easier to support these formats by reducing the (repeated) trimming logic that we would now require underlying libraries (or
Agreed. Whatever we decide, we should not introduce a public API. Since this "trimming" is a common operation, which only requires the public interface from the let tile = await source.getTile({ x, y, selection, signal });
let { width, height } = getTileExtent({ x, y }, source.tileSize, source.shape);
let trimmed = trimTile(tile, { width, height });
The last thing I want to do is delay the fix which solves this issue. Given the length of our discussion, I have a feeling we are venturing near Fredkin's paradox, and trust you to make a decision. |
I was able to generate an overlay layer, which occludes the rendering artifact.
I tested that the visualization artifact is no longer visible with this dataset: The code gets the size of the image from |
6477c65
to
6c43fa4
Compare
Fixes #620
Change List
Checklist