Skip to content
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

czi.read_mosiac_size gives different shape than the actual array has when using czi.read_mosaic #77

Open
sebi06 opened this issue Mar 26, 2021 · 9 comments

Comments

@sebi06
Copy link

sebi06 commented Mar 26, 2021

Hi,

here is something which bothers me for a while and I hope you might be able to help me

CZI Testfile

This is a tiles image which I want to read. Here is the minimal code

# read the czi mosaic image
czi = CziFile(filename)

# Get the shape of the data
print('Dimensions   : ', czi.dims)
print('Size         : ', czi.size)
print('Shape        : ', czi.dims_shape())
print('IsMoasic     : ', czi.is_mosaic())
print('Mosaic Size  : ', czi.read_mosaic_size())

# read the mosaic pixel data
mosaic = czi.read_mosaic(C=0, scale_factor=1.0)
print('Mosaic Shape :', mosaic.shape)

What I do not understand so far is the output, where the actual size of the returner array does not have the same XY number of pixel as it should have. The value returned by czi.read_mosaic is correct and corresponds to what is shown in ZEN.

Do you have any idea where those additional pixels come from?

Dimensions  :  BSCMYX
Size        :  (1, 1, 1, 120, 440, 544)
Shape       :  [{'X': (0, 544), 'Y': (0, 440), 'C': (0, 1), 'M': (0, 120), 'S': (0, 1), 'B': (0, 1)}]
IsMoasic    :  True
Mosaic Size :  (0, 0, 7398, 3212)
Mosaic Shape : (1, 3216, 7400)
@heeler
Copy link
Member

heeler commented Mar 27, 2021

Hi @sebi06
Thanks for raising this. I'll read the docs again but the bounding box I'm returning is the boundingBoxLayer0Only.
It's defined in libCZI as shown below. I suspect that the bounding box is the size of the tiles laid side by side w/out overlap. The Mosaic shape from the assembled image is the shape of the tiles once they have been projected into place with edges overlapping. My gut feeling is that the difference is caused by the overlap. I'll dig through the library documentation and see if there is some way to get the image size without reading the actual mosaic image but it's possible there may not be.
The value I'm returning is the value Zeiss chose to expose at the top level, I'm quite surprised it differs from Zen to be honest.

Thank you for bringing this to my attention, I will try and see if there's a way to get the stitched value without having to load all the data.

	/// Statistics about all sub-blocks found in a CZI-document.
	struct SubBlockStatistics
	{
		/// The bounding box determined only from the sub-blocks of pyramid-layer0 in the 
		/// document.
		IntRect boundingBoxLayer0Only;

@sebi06
Copy link
Author

sebi06 commented Mar 27, 2021

Hi @heeler,

the tile overlap for this image is around 10%. I also checked if there are any "black" pixels on the out edges of some tile, but there are not. The overall image incl the overlap is exactly 7392 x 3212 pixels.

Silly idea: Rounding issues?

image

image

@heeler
Copy link
Member

heeler commented Mar 27, 2021

@sebi06 your guess sounds likely to me. I got my wires crossed when I looked at what you were saying initially. I thought the bounding box size was wrong but it is correct. It seems the error is in libCZI's reconstruction function. I'm going to have a look at it in more depth right now to see if I can fix it. Thanks for pointing this out in detail.

@sebi06
Copy link
Author

sebi06 commented Mar 27, 2021

Hi @heeler,

If it turns out that this is rather an issue of ibCZI itself, it would be possible to ask the person who wrote directly (since he is my colleague).
Let me know what you find out.

@sebi06
Copy link
Author

sebi06 commented Mar 29, 2021

Hi @heeler ,

i did some more digging. When I read the bboxes of all tiles (M=120) and calculate the rsulting overall bbox I get (7398 x 3212)

def get_bbox_scene(cziobject, sceneindex=0):
    """Get the min / max extend of a given scene from a CZI mosaic image
    at pyramid level = 0 (full resolution)

    :param czi: CZI object for from aicspylibczi
    :type czi: Zeiss CZI file object
    :param sceneindex: index of the scene, defaults to 0
    :type sceneindex: int, optional
    :return: tuple with (XSTART, YSTART, WIDTH, HEIGHT) extend in pixels
    :rtype: tuple
    """

    # get all bounding boxes
    bboxes = cziobject.mosaic_scene_bounding_boxes(index=sceneindex)

    # initialize lists for required values
    xstart = []
    ystart = []
    tilewidth = []
    tileheight = []

    # loop over all tiles for the specified scene
    for box in bboxes:

        # get xstart, ystart amd tile widths and heights
        xstart.append(box[0])
        ystart.append(box[1])
        tilewidth.append(box[2])
        tileheight.append(box[3])

    # get bounding box for the current scene
    XSTART = min(xstart)
    YSTART = min(ystart)

    # do not forget to add the width and height of the last tile :-)
    WIDTH = max(xstart) - XSTART + tilewidth[-1]
    HEIGHT = max(ystart) - YSTART + tileheight[-1]

    return XSTART, YSTART, WIDTH, HEIGHT

The result of this I use for the following call:

scene_array_c = czi.read_mosaic(region=(scene.xstart, scene.ystart, scene.width, scene.height), scale_factor=1.0, C=0)

and this gives me an array with shape = (1, 3212, 7398), which is what I expected.

So why does this approach gives me the correct result, while reading the array directly gives an array with is a few pixel too big?

@heeler
Copy link
Member

heeler commented Apr 3, 2021

Hi @sebi06,

I've been working on this in the context of reconstructing the image in aicsimageio as a dask.array. Using your example file I get the results you expect when I use the subblock (tile) bounding boxes.
I'm having trouble figuring out if the subblocks within a scene in a mosaic file are guaranteed to tile the entire scene bounding box for the mosaic or if there can be missing tiles. I've searched through the docs to try to figure this out but it's not explicitly mentioned. Do you have any insights on this? It impacts how I have to reconstruct the object.

Side note:
Once I have this working in aicsimageio I will fix this in aicspylibczi.

@mcdo0486
Copy link

I think I am running into this issue as well. For this file, the bounding box shape should be (2048, 6349) but is (2048, 6352). Those last three columns of data get filled with random(?) data. After running this 20 times it will start being consistently filled. If I restart the kernel in a lab notebook it will start filling in with random data. Very odd.

p = 'czifile.czi'
for i in range(5):
    source_czi = CziFile(p)  
    czi_data = source_czi.read_mosaic(C=0)  
    c_data = [czi_data.max(), czi_data.mean(), czi_data.std()]  
    print('shape:',czi_data.shape)  
    print('last few pixels:')  
    print(czi_data[0,2044:,6345:])  
    print(c_data)
shape: (1, 2048, 6352)
last few pixels:
[[511 505 487 478 453 441 538]
 [525 519 499 493 486 473 511]
 [515 493 458 404 602 580 503]
 [464 515 484 455 568 596 560]]
[65535, 708.8229086465138, 1659.5660332105074]
shape: (1, 2048, 6352)
last few pixels:
[[511 505 487 478 551 490 516]
 [525 519 499 493 529 528 501]
 [515 493 458 404 549 591 505]
 [464 515 484 455 430 458 479]]
[65535, 708.936435190196, 1659.970422413924]
shape: (1, 2048, 6352)
last few pixels:
[[511 505 487 478 508 490 561]
 [525 519 499 493 495 530 500]
 [515 493 458 404 475 448 429]
 [464 515 484 455 443 466 490]]
[65535, 708.9128826150966, 1659.7840689788136]
shape: (1, 2048, 6352)
last few pixels:
[[511 505 487 478 539 552 517]
 [525 519 499 493 532 503 522]
 [515 493 458 404 512 517 529]
 [464 515 484 455 473 484 530]]
[65535, 708.9397731367827, 1660.046506218213]
shape: (1, 2048, 6352)
last few pixels:
[[511 505 487 478   0   0   0]
 [525 519 499 493   0   0   0]
 [515 493 458 404   0   0   0]
 [464 515 484 455   0   0   0]]
[65535, 708.5564712024756, 1659.623065781167]

When I try your snippet def get_bbox_scene(cziobject, sceneindex=0): I get this issue: AttributeError: 'CziFile' object has no attribute 'mosaic_scene_bounding_boxes'

I just pip updated to aicspylibczi-3.0.2 and still get that issue. Let me know if I am doing something wrong.

@mcdo0486
Copy link

mcdo0486 commented Aug 19, 2021

Ok, I see where mosaic bboxes can get gotten from cziobjects

p = 'czifile'
for i in range(5):
    source_czi = CziFile(p)    
    bbox = source_czi.get_all_mosaic_scene_bounding_boxes()[0]
    czi_data = source_czi.read_mosaic(region=(bbox.x, bbox.y, bbox.w, bbox.h), C=0)
    c_data = [czi_data.max(), czi_data.mean(), czi_data.std()]
    print('shape:',czi_data.shape)
    print('last few pixels:')
    print(czi_data[0,2044:,6345:])
    print(c_data)    

That returns the correct shape (1,2048,6349). I think that should be the default behavior with read_mosaic

@sebi06
Copy link
Author

sebi06 commented Aug 23, 2021

Hi @heeler ,

I am not 100% sure if I got you question correctly. Are you talking about CZIs like this one below. The ZEN software can acquire it easily so, even if this is probably not a common use case. Does that help?

CZI File: https://www.dropbox.com/s/8n4pns3q8pp4248/doughnut.czi?dl=0

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants