rochefort-lab / fissa

A Python toolbox for Fast Image Signal Separation Analysis, designed for Calcium Imaging data.
GNU General Public License v3.0
30 stars 27 forks source link

ROIs segmented using ImageJ cannot be converted #288

Open just-meng opened 1 year ago

just-meng commented 1 year ago

My ROIs are manual segmented using ImageJ and exported as a RoiSet.zip file. I used this script to run FISSA, and run into the following error. The ROIs seem to cause problems and cannot be converted to masks.

Traceback (most recent call last):
  File "/media/meng/Data/Meng_Psychedelics/02_code/02_prism_soma_dendrite/neuropil_correction.py", line 81, in <module>
    experiment.separate()
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/fissa/core.py", line 1364, in separate
    self.separation_prep(redo_prep)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/fissa/core.py", line 1246, in separation_prep
    outputs = Parallel(
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/joblib/parallel.py", line 1061, in __call__
    self.retrieve()
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/joblib/parallel.py", line 938, in retrieve
    self._output.extend(job.get(timeout=self.timeout))
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/multiprocessing/pool.py", line 774, in get
    raise self._value
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/multiprocessing/pool.py", line 125, in worker
    result = (True, func(*args, **kwds))
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/joblib/_parallel_backends.py", line 595, in __call__
    return self.func(*args, **kwargs)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/joblib/parallel.py", line 263, in __call__
    return [func(*args, **kwargs)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/joblib/parallel.py", line 263, in <listcomp>
    return [func(*args, **kwargs)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/fissa/core.py", line 209, in extract
    base_masks = datahandler.rois2masks(rois, curdata)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/fissa/extraction.py", line 124, in rois2masks
    return roitools.rois2masks(rois, cls.get_frame_size(data))
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/fissa/roitools.py", line 478, in rois2masks
    return getmasks(rois, shape)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/fissa/roitools.py", line 400, in getmasks
    mask = poly2mask(rois[i], shpe)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/fissa/polygons.py", line 70, in poly2mask
    polygons = _reformat_polygons(polygons)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/fissa/polygons.py", line 137, in _reformat_polygons
    Polygon(polygons[0])
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/shapely/geometry/polygon.py", line 230, in __new__
    shell = LinearRing(shell)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/shapely/geometry/polygon.py", line 104, in __new__
    geom = shapely.linearrings(coordinates)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/shapely/decorators.py", line 77, in wrapped
    return func(*args, **kwargs)
  File "/home/meng/anaconda3/envs/2p/lib/python3.10/site-packages/shapely/creation.py", line 173, in linearrings
    return lib.linearrings(coords, out=out, **kwargs)
ValueError: linearrings: Input operand 0 does not have enough dimensions (has 1, gufunc core with signature (i, d)->() requires 2)
    [Extraction 1/3] Converting ROIs to masks

However, when I load the RoiSet.zip and manually convert it into a list of Boolean masks, it works. It is the very same set of ROIs, though.

Is this a bug about specifically dealing with ImageJ .zip files? Can someone fix it?

EmrickLab commented 1 year ago

Hello! I've run into the exact same issue. Do you @just-meng mind explaining to me how to convert the RoiSet.zip to a boolean mask list that's compatible with FISSA? This is what I tried ~

import numpy as np
import tifffile
import roifile
from roifile import ImagejRoi
def roi_to_mask(tiff_path, roi_path):

    # get tif dimensions
    with tifffile.TiffFile(tiff_path) as tif:
        width, height = tif.pages[0].shape
        first_image = tif.pages[0].asarray()

    maskList = []

    roiList = ImagejRoi.fromfile(roi_path)
    for roi in roiList:
        coordinates = roi.coordinates()                 # roi boundary coordinates
        mask = np.zeros((height, width), dtype=bool)    # empty mask

        for x, y in coordinates:
            mask[int(y), int(x)] = True                 # fill mask 

        maskList.append(mask)

    return maskList, first_image

masks, img = roi_to_mask(tiff_path, rois_path)
import fissa
experiment = fissa.Experiment([tiff_path],
                              masks,
                              verbosity = 6,
                              nRegions=4)
experiment.separate()

I get TypeError: Wrong ROIs input format: expected a list or sequence, but got a <class 'numpy.ndarray'>

just-meng commented 1 year ago

I think all you have to do is to put masks in a list:

experiment = fissa.Experiment([tiff_path],
                              [masks],
                              verbosity = 6,
                              nRegions=4)

Btw I believe the way you create the mask only returns the boundary of the ROI but not the entire area. Maybe try this:

from skimage.draw import polygon
...
# inside your for loop
mask = np.zeros((height, width), dtype=bool)    # empty mask

# instead of this
# for x, y in coordinates:
#     mask[int(y), int(x)] = True                 # fill mask 

# do that
rr, cc = polygon(coordinates[:, 1], coordinates[:, 0], shape=(height, width))
mask[rr, cc] = True

maskList.append(mask)
...

You can always visually check the result, e.g. by plt.imshow(mask) to see if the entire ROI is masked.

Hope this helps! :)

EmrickLab commented 1 year ago

Thank you for the kind explanation @just-meng ! You're right, after sending the code I realized that the only thing I masked was the boundary. I have implemented your solution to fill the entire boundary accordingly.

I have indeed tried inputting the var masks as a list. Both list and unlisted inputs yield TypeErrors from the same script rois2masks(). Bane of my existence! Thanks again for the quick reply.

just-meng commented 1 year ago

That's strange. Can you post the error you get with [masks]?

EmrickLab commented 1 year ago

Sure! Here's my full script:

import numpy as np
import tifffile
import roifile
from roifile import ImagejRoi
from skimage.draw import polygon
import os

# set dir
path = os.path.expanduser("~" + os.sep + "Desktop")
os.chdir(path)

tiff_path = './GCaMP_AD.tif'
rois_path = './GC_AD_ROI.zip'

def roi_to_mask(tiff_path, roi_path):

    # get tif dimensions
    with tifffile.TiffFile(tiff_path) as tif:
        width, height = tif.pages[0].shape
        first_image = tif.pages[0].asarray()

    maskList = []

    roiList = ImagejRoi.fromfile(roi_path)
    n_roi = len(roiList)

    for roi in roiList:
        coordinates = roi.coordinates()                 # roi boundary coordinates
        mask = np.zeros((height, width), dtype=bool)    # empty mask

        rr, cc = polygon(coordinates[:, 1], coordinates[:, 0], shape=(height, width))
        mask[rr, cc] = True

        maskList.append(mask)

    return maskList, first_image, n_roi

masks, img, n_roi = roi_to_mask(tiff_path, rois_path)

import fissa
experiment = fissa.Experiment([tiff_path],
                              [masks],
                              verbosity = 6,
                              nRegions=4)
experiment.separate()

Below is the full output:

Adopted default values for 'expansion'
Doing region growing and data extraction for 1 trials...
  Images:
    ./GCaMP_AD.tif
  ROI sets:
    [array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]]), array([[False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       ...,
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False],
       [False, False, False, ..., False, False, False]])]
  nRegions: 4
  expansion: 1
[Extraction 1/1] Extraction starting (./GCaMP_AD.tif)
    [Extraction 1/1] Loading imagery
[Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
    [Extraction 1/1] Converting ROIs to masks
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_71300\275886425.py in <module>
     43                               verbosity = 6,
     44                               nRegions=4)
---> 45 experiment.separate()

~\anaconda3\envs\pWGA\lib\site-packages\fissa\core.py in separate(self, redo_prep, redo_sep)
   1362         # Do data preparation
   1363         if redo_prep or self.raw is None:
-> 1364             self.separation_prep(redo_prep)
   1365         if redo_prep:
   1366             redo_sep = True

~\anaconda3\envs\pWGA\lib\site-packages\fissa\core.py in separation_prep(self, redo)
   1252                     total=n_trial,
   1253                     desc="Extracting traces",
-> 1254                     disable=disable_progressbars,
   1255                 )
   1256             )

~\anaconda3\envs\pWGA\lib\site-packages\joblib\parallel.py in __call__(self, iterable)
   1054 
   1055             with self._backend.retrieval_context():
-> 1056                 self.retrieve()
   1057             # Make sure that we get a last message telling us we are done
   1058             elapsed_time = time.time() - self._start_time

~\anaconda3\envs\pWGA\lib\site-packages\joblib\parallel.py in retrieve(self)
    933             try:
    934                 if getattr(self._backend, 'supports_timeout', False):
--> 935                     self._output.extend(job.get(timeout=self.timeout))
    936                 else:
    937                     self._output.extend(job.get())

~\anaconda3\envs\pWGA\lib\multiprocessing\pool.py in get(self, timeout)
    655             return self._value
    656         else:
--> 657             raise self._value
    658 
    659     def _set(self, i, obj):

~\anaconda3\envs\pWGA\lib\multiprocessing\pool.py in worker(inqueue, outqueue, initializer, initargs, maxtasks, wrap_exception)
    119         job, i, func, args, kwds = task
    120         try:
--> 121             result = (True, func(*args, **kwds))
    122         except Exception as e:
    123             if wrap_exception and func is not _helper_reraises_exception:

~\anaconda3\envs\pWGA\lib\site-packages\joblib\_parallel_backends.py in __call__(self, *args, **kwargs)
    593     def __call__(self, *args, **kwargs):
    594         try:
--> 595             return self.func(*args, **kwargs)
    596         except KeyboardInterrupt as e:
    597             # We capture the KeyboardInterrupt and reraise it as

~\anaconda3\envs\pWGA\lib\site-packages\joblib\parallel.py in __call__(self)
    261         with parallel_backend(self._backend, n_jobs=self._n_jobs):
    262             return [func(*args, **kwargs)
--> 263                     for func, args, kwargs in self.items]
    264 
    265     def __reduce__(self):

~\anaconda3\envs\pWGA\lib\site-packages\joblib\parallel.py in <listcomp>(.0)
    261         with parallel_backend(self._backend, n_jobs=self._n_jobs):
    262             return [func(*args, **kwargs)
--> 263                     for func, args, kwargs in self.items]
    264 
    265     def __reduce__(self):

~\anaconda3\envs\pWGA\lib\site-packages\fissa\core.py in extract(image, rois, nRegions, expansion, datahandler, verbosity, label, total)
    207     if verbosity >= 3:
    208         print("{}Converting ROIs to masks".format(mheader))
--> 209     base_masks = datahandler.rois2masks(rois, curdata)
    210 
    211     # get the mean image

~\anaconda3\envs\pWGA\lib\site-packages\fissa\extraction.py in rois2masks(cls, rois, data)
    122         fissa.roitools.getmasks, fissa.roitools.readrois
    123         """
--> 124         return roitools.rois2masks(rois, cls.get_frame_size(data))
    125 
    126     @abc.abstractmethod

~\anaconda3\envs\pWGA\lib\site-packages\fissa\roitools.py in rois2masks(rois, shape)
    481         return rois
    482 
--> 483     raise ValueError("Wrong ROIs input format: unfamiliar shape.")

ValueError: Wrong ROIs input format: unfamiliar shape.

If I simply call masks instead of [masks], I get the error stated previously:

~\anaconda3\envs\pWGA\lib\site-packages\fissa\roitools.py in rois2masks(rois, shape)
    471         raise TypeError(
    472             "Wrong ROIs input format: expected a list or sequence, but got"
--> 473             " a {}".format(rois.__class__)
    474         )
    475 

TypeError: Wrong ROIs input format: expected a list or sequence, but got a <class 'numpy.ndarray'>

Thus, I tried changing my function in the following way: maskList.append(mask) --> maskList.append(mask.tolist())

This I think made things better since the error moved a few lines further down lol. It seems like FISSA has an issue with my np boolean masks, so I converted the np arrays to lists. Now it's only worried about the tuples. Again, inputing masks as a list [masks] yields the same TypeError of 'unfamiliar shape'

~\anaconda3\envs\pWGA\lib\site-packages\fissa\roitools.py in rois2masks(rois, shape)
    475 
    476     # If it's a something by 2 array (or vice versa), assume polygons
--> 477     if np.shape(rois[0])[1] == 2 or np.shape(rois[0])[0] == 2:
    478         return getmasks(rois, shape)
    479     # If it's a list of bigger arrays, assume masks

IndexError: tuple index out of range
just-meng commented 1 year ago

I used your code and got no error!

Literally only changed the tiff_path and rois_path and the code ran through with no complaints!

The output is too long, but here is the finishing statement:

Finished separating signals from 109 ROIs across 1 trials in 10.6 seconds

Process finished with exit code 0
EmrickLab commented 1 year ago

I am thoroughly defeated then haha. Just to double check -- you ran the code above with [tiff_path] [masks] both listed? I guess I'll just try to reinstall FISSA...perhaps I irreparably messed with some of the internal files while trying to troubleshoot. Wish I could finish separating signals from N rois across 1 trials right now. Thanks again for your help!

*Eh, reinstalling didnt work

just-meng commented 1 year ago

Yes with [tiff_path] [masks] both listed just as in your code. If you have a tiff size of 512 x 512 like me, you can try my roiset with your code. If it works, then perhaps the problem is how your export the ROIs from ImageJ. RoiSet.zip

EmrickLab commented 1 year ago

I resized my tif file to 512x512 (it was smaller) and used your RoiSet -- it worked.

I have further news to report. First I thought it worked perhaps because you used polygon ROIs. But I believe you have a number of freehand ones. Then I thought, hey they kept the generic "RoiSet.zip" filename. Well guess what happened after I changed my roi zip filename from GC_AD_ROI.zip to RoiSet.zip? It worked : D

Immense thanks once again for taking the time to get me up and running with this package. Cheers!

just-meng commented 1 year ago

Also strange, but I'm glad that your code is now working also for you!

Could you maybe try reading the GC_AD_ROI.zip using a different function?

import roifile
roiList = roifile.roiread(roi_path) # insteadt of roiList = ImagejRoi.fromfile(roi_path)
EmrickLab commented 1 year ago

I tried your version of the roi import and it worked on the roi file named GC_AD_ROI.zip. However, I then re-tried my own version of the roi import on this same zip file and it worked.

What changed? Well here's the order of events: I had the original zip file GC_AD_ROI.zip that I ran my code on got errors which you helped me resolve. After renaming this file to RoiSet I get no errors. Then I open RoiSet in Fiji and save another copy of it as GC_AD_ROI.zip This copy works with both roi import functions ~ roiread & ImagejRoi.fromfile().

I was running all of this on a jupyter notebook. I got everything to work...restarted the kernel...things still work. All I can think of is there was something corrupted with the original file and somehow it got fixed just by renaming it?

It's 5:30am and this feels like a bad fever dream so I'm calling it a day. But thanks once again!

just-meng commented 1 year ago

Perhaps you are right with the file being corrupted. Good night!

nathalierochefort commented 1 year ago

Hi @just-meng and @EmrickLab, is your issue solved? We are routinely using ROIs from ImageJ without any issue. Happy to help.

kirolle commented 11 months ago

I have the same default behavior. The solution suggested in this thread works, but the original issue should be investigated. I used this RoiSet.Zip file, which led to the issue. RoiSet.zip

just-meng commented 11 months ago

Hi @nathalierochefort, as @kirolle points out, loading ROIs segmented using ImageJ requires the workaround. I agree that it would be nice to troubleshoot the original issue or incorporate the workaround into the code of FISSA.

nathalierochefort commented 10 months ago

Thank you all for the feedback and the suggestion. We will definitely investigate the original issue. Sorry for the delay of the answers: one owner is on paternity leave and members of my lab have used our established pipeline that works well so did not encounter the problem. We will try to solve the issue in January