ImageMask.data created by open_mask returns all zeros

#1

When calling the open_mask function, it requires mask image to have the pixels labeled as a mask be 255. This is due to the _div(255) in the return statement of the open_mask function, which means that any value below 255 would be smaller than 0. Followed by the .data property of ImageMask which returns a long type which will round everything down to 0.

Is this intended or am I missing something? As most of the time when masks are saved they are labeled as 0 as background, and 1 as foreground. Moreover, what if there are multiple classes in the mask file, even if the mask is multiplied by 255 it still wouldn’t work.

0 Likes

#2

We had to pick a convention, so for now, the open_mask function expects masks with pixels are 0. or 1. in floats / 0 or 255 in ints. If you have another type of masks, you should either overwrite the function or save them to correspond to that type.
Note that for multiple labels, the mask passed to ImageMask should have multiple channels.

1 Like

(Jeremy Howard (Admin)) #3

Actually 255 for ints isn’t what I intended. I might have messed something up

0 Likes

#4

Just pushed a change to open_mask and SegmentationDataset. It now expects masks with zeros, ones, twos etc… and if it’s zeros and 255s we can set div=True to make the divide happen.

2 Likes

(RobG) #5

It looks like the recent change 1cd9e68 removing div=True has stopped my 255 masks from working. Is there a new way to tell the SegmentationDataset that my binary masks are 255/0 or should I make them 1/0 at source?

0 Likes

RuntimeError: invalid argument 0: Sizes of tensors must match except in dimension 0. Got 1 and 0 in dimension 1
#6

Yup!
I removed those parameters because you can now change the mask opener completely :wink:
So you should change the attribute mask_opener of your SegmentationDataset to partial(open_mask, div=True)
See here to do it on all your datasets at once with the data block API.

0 Likes

(RobG) #7

Great, thanks!
Edit: something now breaks in my custom loss function that i will have to track down, but that’s likely my problem.

0 Likes

(Uwais Iqbal) #8

I ran into a somewhat related issue. I didn’t know where to log it so I’ll just put it here. When calling .save() on a mask which is a ImageSegment object, there is no concrete implementation so it falls back to the .save() implementation defined for the Image object. This is problematic because in the .save() method, the pixel values are all multiplied by a factor of 255 scaling the class values which for some cases will result in lost class values when subsequently calling open_mask() on a saved mask.

I ran into this issue and thought I’d put it here but a quick fix would be to add an implementation of the .save() method inside ImageSegment which would do something like this without transforming the pixel values:

def save_mask(mask, fn:PathOrStr):
    "Save the image to `fn`."
    x = image2np(mask.data).astype(np.uint8)
    PIL.Image.fromarray(x).save(fn)
1 Like