Repository 's3segmenter'
hg clone https://toolshed.g2.bx.psu.edu/repos/perssond/s3segmenter

Changeset 1:41e8efe8df43 (2022-03-11)
Previous changeset 0:37acf42a824b (2021-03-12) Next changeset 2:96d0d969ebc9 (2022-09-16)
Commit message:
"planemo upload for repository https://github.com/ohsu-comp-bio/S3segmenter commit c8f72e04db2cc6cc26f0359d5aa3b1a972bc6d53"
modified:
S3segmenter.py
macros.xml
s3segmenter.xml
added:
save_tifffile_pyramid.py
b
diff -r 37acf42a824b -r 41e8efe8df43 S3segmenter.py
--- a/S3segmenter.py Fri Mar 12 00:18:40 2021 +0000
+++ b/S3segmenter.py Fri Mar 11 23:37:49 2022 +0000
[
b"@@ -29,6 +29,9 @@\n import datetime\n from joblib import Parallel, delayed\n from rowit import WindowView, crop_with_padding_mask\n+from save_tifffile_pyramid import save_pyramid\n+import subprocess\n+import ome_types\n \n \n def imshowpair(A,B):\n@@ -101,7 +104,7 @@\n         padded[padded == 1] = maxima.flatten()\n         return padded.astype(np.bool)\n \n-def S3AreaSegmenter(nucleiPM, images, TMAmask, threshold,outputPath):\n+def S3AreaSegmenter(nucleiPM, images, TMAmask, threshold,fileprefix,outputPath):\n     nucleiCenters = nucleiPM[:,:,0]\n     TMAmask= (nucleiCenters>np.amax(nucleiCenters)*0.8)*TMAmask\n     area = []\n@@ -112,17 +115,62 @@\n             threshold = threshold_otsu(image_gauss)\n         mask=resize(image_gauss>threshold,(images.shape[1],images.shape[2]),order = 0)*TMAmask\n         area.append(np.sum(np.sum(mask)))\n-    np.savetxt(outputPath + os.path.sep + 'area.csv',(np.transpose(np.asarray(area))),fmt='%10.5f')  \n+    os.mk    \n+    np.savetxt(outputPath + os.path.sep + fileprefix + '_area.csv',(np.transpose(np.asarray(area))),fmt='%10.5f')  \n     return TMAmask\n-            \n+\n+def getMetadata(path,commit):\n+    with tifffile.TiffFile(path) as tif:\n+        if not tif.ome_metadata:\n+            try:\n+                x_res_tag = tif.pages[0].tags['XResolution'].value\n+                y_res_tag = tif.pages[0].tags['YResolution'].value\n+                physical_size_x = x_res_tag[0] / x_res_tag[1]\n+                physical_size_y = y_res_tag[0] / y_res_tag[1]\n+            except KeyError:\n+                physical_size_x = 1\n+                physical_size_y = 1\n+            metadata_args = dict(\n+            pixel_sizes=(physical_size_y, physical_size_x),\n+            pixel_size_units=('\xc2\xb5m', '\xc2\xb5m'),\n+            software= 's3segmenter v' + commit\n+            )\n+        else: \n+            metadata=ome_types.from_xml(tif.ome_metadata)\n+            metadata = metadata.images[0].pixels\n+            metadata_args = dict(\n+            pixel_sizes=(metadata.physical_size_y,metadata.physical_size_x),\n+            pixel_size_units=('\xc2\xb5m', '\xc2\xb5m'),\n+            software= 's3segmenter v' + commit\n+            )\n+        return metadata_args\n+\n+def S3NucleiBypass(nucleiPM,nucleiImage,logSigma,TMAmask,nucleiFilter,nucleiRegion):        \n+        foregroundMask =  nucleiPM\n+        P = regionprops(foregroundMask, nucleiImage)\n+        prop_keys = ['mean_intensity', 'label','area']\n+        def props_of_keys(prop, keys):\n+            return [prop[k] for k in keys]\n+        \n+        mean_ints, labels, areas = np.array(Parallel(n_jobs=6)(delayed(props_of_keys)(prop, prop_keys) \n+\t\t\t\t\t\tfor prop in P\n+\t\t\t\t\t\t)\n+\t\t        ).T\n+        del P\n+        maxArea = (logSigma[1]**2)*3/4\n+        minArea = (logSigma[0]**2)*3/4\n+        passed = np.logical_and(areas > minArea, areas < maxArea)\n+        \n+        foregroundMask *= np.isin(foregroundMask, labels[passed])\n+        np.greater(foregroundMask, 0, out=foregroundMask)\n+        foregroundMask = label(foregroundMask, connectivity=1).astype(np.int32)\n+        return foregroundMask\n \n def S3NucleiSegmentationWatershed(nucleiPM,nucleiImage,logSigma,TMAmask,nucleiFilter,nucleiRegion):\n     nucleiContours = nucleiPM[:,:,1]\n     nucleiCenters = nucleiPM[:,:,0]\n-    \n     mask = resize(TMAmask,(nucleiImage.shape[0],nucleiImage.shape[1]),order = 0)>0\n-    \n-    if nucleiRegion == 'localThreshold':\n+    if nucleiRegion == 'localThreshold' or nucleiRegion == 'localMax':\n         Imax =  peak_local_max(extrema.h_maxima(255-nucleiContours,logSigma[0]),indices=False)\n         Imax = label(Imax).astype(np.int32)\n         foregroundMask =  watershed(nucleiContours, Imax, watershed_line=True)\n@@ -146,27 +194,8 @@\n         foregroundMask *= np.isin(foregroundMask, labels[passed])\n         np.greater(foregroundMask, 0, out=foregroundMask)\n         foregroundMask = label(foregroundMask, connectivity=1).astype(np.int32)\n-    elif nucleiRegion =='bypass':\n-        foregroundMask =  nucleiPM[:,:,0]\n-        P = "..b"efix,'nucleiRing',args.saveFig,args.saveMask)\n-        exportMasks(cytoplasmMask,cyto,outputPath,filePrefix,'cytoRing',args.saveFig,args.saveMask)\n-        exportMasks(cellMask,cyto,outputPath,filePrefix,'cellRing',args.saveFig,args.saveMask)\n+        exportMasks(nucleiMaskTemp,nucleiCrop,outputPath,filePrefix,'nucleiRing',commit,metadata,args.saveFig,args.saveMask)\n+        exportMasks(cytoplasmMask,cyto,outputPath,filePrefix,'cytoRing',commit,metadata,args.saveFig,args.saveMask)\n+        exportMasks(cellMask,cyto,outputPath,filePrefix,'cellRing',commit,metadata,args.saveFig,args.saveMask)\n         \n     elif args.segmentCytoplasm == 'ignoreCytoplasm':\n-        exportMasks(nucleiMask,nucleiCrop,outputPath,filePrefix,'nuclei')\n+        exportMasks(nucleiMask,nucleiCrop,outputPath,filePrefix,'nuclei',commit,metadata)\n         cellMask = nucleiMask\n-        exportMasks(nucleiMask,nucleiCrop,outputPath,filePrefix,'cell')\n+        exportMasks(nucleiMask,nucleiCrop,outputPath,filePrefix,'cell',commit,metadata)\n         cytoplasmMask = nucleiMask\n         \n-        \n-    if (np.min(args.detectPuncta)>-1):\n-        if len(args.detectPuncta) != len(args.punctaSigma):\n-            args.punctaSigma = args.punctaSigma[0] * np.ones(len(args.detectPuncta))\n+    detectPuncta = args.detectPuncta\n+    if (np.min(detectPuncta)>0):\n+        detectPuncta[:] = [number - 1 for number in detectPuncta] #convert 1-based indexing to 0-based indexing    \n+        if len(detectPuncta) != len(args.punctaSigma):\n+            args.punctaSigma = args.punctaSigma[0] * np.ones(len(detectPuncta))\n  \n   \n-        if len(args.detectPuncta) != len(args.punctaSD):\n-            args.punctaSD = args.punctaSD[0] * np.ones(len(args.detectPuncta))\n+        if len(detectPuncta) != len(args.punctaSD):\n+            args.punctaSD = args.punctaSD[0] * np.ones(len(detectPuncta))\n   \n         counter=0\n-        for iPunctaChan in args.detectPuncta:\n+        for iPunctaChan in detectPuncta:\n             punctaChan = tifffile.imread(imagePath,key = iPunctaChan)\n             punctaChan = punctaChan[int(PMrect[0]):int(PMrect[0]+PMrect[2]), int(PMrect[1]):int(PMrect[1]+PMrect[3])]\n             spots=S3punctaDetection(punctaChan,cellMask,args.punctaSigma[counter],args.punctaSD[counter])\n-            cellspotmask = nucleiMask#tifffile.imread(maskPath) #REMOVE THIS LATER\n+            cellspotmask = nucleiMask\n             P = regionprops(cellspotmask,intensity_image = spots ,cache=False)\n             numSpots = []\n             for prop in P:\n                 numSpots.append(np.uint16(np.round(prop.mean_intensity * prop.area)))\n-            np.savetxt(outputPath + os.path.sep + 'numSpots_chan' + str(iPunctaChan) +'.csv',(np.transpose(np.asarray(numSpots))),fmt='%10.5f')    \n+            np.savetxt(outputPath + os.path.sep + 'numSpots_chan' + str(iPunctaChan+1) +'.csv',(np.transpose(np.asarray(numSpots))),fmt='%10.5f')    \n             edges = 1-(cellMask>0)\n             stacked_img=np.stack((np.uint16((spots+edges)>0)*np.amax(punctaChan),punctaChan),axis=0)\n-            tifffile.imsave(outputPath + os.path.sep + filePrefix + os.path.sep + 'punctaChan'+str(iPunctaChan) + 'Outlines.tif',stacked_img)\n-            counter=counter+1        \n-        #fix bwdistance watershed\n+             \n+            \n+            outputPathPuncta = outputPath + os.path.sep + filePrefix + os.path.sep + 'punctaChan'+str(iPunctaChan+1) + 'Outlines.ome.tif'\n+            \n+            # metadata_args = dict(\n+            #     pixel_sizes=(metadata.physical_size_y, metadata.physical_size_x),\n+            #     pixel_size_units=('\xc2\xb5m', '\xc2\xb5m'),\n+            #     software= 's3segmenter v' + commit\n+            #     )\n+            save_pyramid(\n+                stacked_img,\n+                outputPathPuncta,\n+                channel_names=['puncta outlines', 'image channel'],\n+                is_mask=False,\n+                **metadata\n+                )     \n+            \n+            counter=counter+1    \n+\n"
b
diff -r 37acf42a824b -r 41e8efe8df43 macros.xml
--- a/macros.xml Fri Mar 12 00:18:40 2021 +0000
+++ b/macros.xml Fri Mar 11 23:37:49 2022 +0000
b
@@ -2,12 +2,14 @@
 <macros>
     <xml name="requirements">
         <requirements>
-            <requirement type="package" version="3.6">python</requirement>
+            <container type="docker">labsyspharm/s3segmenter:@VERSION@</container>
+            <requirement type="package" version="3.7">python</requirement>
             <requirement type="package">scikit-learn</requirement>
-            <requirement type="package">scikit-image</requirement>
+            <requirement version="0.14.2" type="package">scikit-image</requirement>
             <requirement type="package">matplotlib</requirement>
-            <requirement type="package">tifffile</requirement>
+            <requirement version="2021.6.6" type="package">tifffile</requirement>
             <requirement type="package">opencv</requirement>
+            <!-- <requirement type="package">ome_types</requirement> -->
         </requirements>
     </xml>
 
@@ -19,6 +21,6 @@
         </citations>
     </xml>
 
-    <token name="@VERSION@">1.2.1</token>
+    <token name="@VERSION@">1.3.12</token>
     <token name="@CMD_BEGIN@">python3 $__tool_directory__/S3segmenter.py</token>
 </macros>
b
diff -r 37acf42a824b -r 41e8efe8df43 s3segmenter.xml
--- a/s3segmenter.xml Fri Mar 12 00:18:40 2021 +0000
+++ b/s3segmenter.xml Fri Mar 11 23:37:49 2022 +0000
b
@@ -1,4 +1,4 @@
-<tool id="s3segmenter" name="s3segmenter" version="@VERSION@.3" profile="17.09">
+<tool id="s3segmenter" name="s3segmenter" version="@VERSION@.0" profile="17.09">
     <description>S3segmenter is a Python-based set of functions that generates single cell (nuclei and cytoplasm) label masks.</description>
     <macros>
         <import>macros.xml</import>
@@ -39,12 +39,22 @@
         --stackProbPath ./Probabilities.tif
         #end if
 
-        --mask $mask
         --probMapChan $probMapChan
-        --crop $crop
+        --crop $crop_select.crop
+        
+        #if $crop_select.crop == "dearray"
+          --maskPath $crop_select.maskPath
+        #end if
+
         --cytoMethod $cytoMethod
         --nucleiFilter $nucleiFilter
-        --nucleiRegion $nucleiRegion
+        --nucleiRegion $nucleiRegion_select.nucleiRegion
+
+        #if $nucleiRegion_select.nucleiRegion == "pixellevel"
+          --pixelThreshold $nucleiRegion_select.pixelThreshold
+          --pixelMaskChan $nucleiRegion_select.pixelMaskChan
+        #end if
+
         --segmentCytoplasm $segmentCytoplasm
         --cytoDilation $adv.cytoDilation
         --logSigma $adv.logSigma
@@ -72,18 +82,20 @@
         <param name="contoursClassProbPath" type="data" format="tiff" optional="true" label="Contours Class Probabilities"/>
         <param name="nucleiClassProbPath" type="data" format="tiff" optional="true" label="Nuclei Class Probabilities"/>
         <param name="stackProbPath" type="data" format="tiff" optional="true" label="Stack Probabilities"/>
-        <param name="mask" type="select" label="Choose mask: TMA, tissue, none.">
-            <option selected="true" value="tissue">tissue</option>
-            <option value="TMA">TMA</option>
-            <option value="none">none</option>
-        </param>
         <param name="probMapChan" type="integer" value="-1" label="Probability Maps Channel"/>
-        <param name="crop" type="select" label="Crop Strategy">
-            <option selected="true" value="noCrop">No Crop</option>
-            <option value="autoCrop">Automatic Crop</option>
-            <option value="dearray">De-array</option>
-            <option value="plate">Plate</option>
-        </param>
+
+        <conditional name="crop_select">
+            <param name="crop" type="select" label="Crop Strategy">
+                <option selected="true" value="noCrop">No Crop</option>
+                <option value="autoCrop">Automatic Crop</option>
+                <option value="dearray">De-array</option>
+                <option value="plate">Plate</option>
+            </param>
+            <when value="dearray">
+                <param name="maskPath" type="data" format="tiff" label="TMA Mask File"/>
+            </when>
+        </conditional>
+
         <param name="cytoMethod" type="select" label="Cyto Method">
             <option value="hybrid">Hybrid</option>
             <option selected="true" value="distanceTransform">Distance Transform</option>
@@ -96,13 +108,24 @@
             <option value="Int">Int</option>
             <option value="none">none</option>
         </param>
-        <param name="nucleiRegion" type="select" label="Nuclei Region">
-            <option value="watershedContourDist">watershedContourDist</option>
-            <option selected="true" value="watershedContourInt">watershedContourInt</option>
-            <option value="watershedBWDist">watershedBWDist</option>
-            <option value="dilation">dilation</option>
-            <option value="localThreshold">localThreshold</option>
-        </param>
+
+        <conditional name="nucleiRegion_select">
+            <param name="nucleiRegion" type="select" label="Nuclei Region">
+                <option value="watershedContourDist">watershedContourDist</option>
+                <option selected="true" value="watershedContourInt">watershedContourInt</option>
+                <option value="watershedBWDist">watershedBWDist</option>
+                <option value="dilation">dilation</option>
+                <option value="localThreshold">localThreshold</option>
+                <option value="localMax">localMax</option>
+                <option value="bypass">bypass</option>
+                <option value="pixellevel">pixellevel</option>
+            </param>
+            <when value="pixellevel">
+                <param name="pixelThreshold" type="float" value="-1.0" Label="Pixel Threshold"/>
+                <param name="pixelMaskChan" type="text" value="2" Label="Pixel Mask Channel"/>
+            </when>
+        </conditional>
+
         <param name="segmentCytoplasm" type="select" label="Segment Cytoplasm">
             <option value="segmentCytoplasm">segmentCytoplasm</option>
             <option selected="true" value="ignoreCytoplasm">ignoreCytoplasm</option>
@@ -113,7 +136,7 @@
         <section name="adv" title="Advanced Options" expanded="false">
             <param name="cytoDilation" type="integer" value="5" label="Cyto Dilation"/>
             <param name="logSigma" type="text" value="3 60" label="logSigma"/>
-            <param name="CytoMaskChan" type="text" value="1" label="Cyto Mask Channel"/>
+            <param name="CytoMaskChan" type="text" value="2" label="Cyto Mask Channel"/>
             <!-- Bug with S3Segmenter code, expects int not list
             <param name="TissueMaskChan" type="text" value="-1" label="Tissue Mask Channel"/>
             -->
@@ -125,16 +148,16 @@
 
     </inputs>
     <outputs>
-        <data format="tiff" name="cell_mask" from_work_dir="*/cellMask.tif" label="cellMasks">
+        <data format="tiff" name="cell_mask" from_work_dir="image/cell.ome.tif" label="cellMasks">
             <filter>saveMask is True</filter>
         </data>
-        <data format="tiff" name="nuclei_mask" from_work_dir="*/nucleiMask.tif" label="nucleiMasks">
+        <data format="tiff" name="nuclei_mask" from_work_dir="image/nuclei.ome.tif" label="nucleiMasks">
             <filter>saveMask is True</filter>
         </data>
-        <data format="tiff" name="cell_outlines" from_work_dir="*/cellOutlines.tif" label="cellOutlines">
+ <data format="tiff" name="cell_outlines" from_work_dir="image/qc/cellOutlines.ome.tif" label="cellOutlines">
             <filter>saveFig is True</filter>
         </data>
-        <data format="tiff" name="nuclei_outlines" from_work_dir="*/nucleiOutlines.tif" label="nucleiOutlines">
+ <data format="tiff" name="nuclei_outlines" from_work_dir="image/qc/nucleiOutlines.ome.tif" label="nucleiOutlines">
             <filter>saveFig is True</filter>
         </data>
     </outputs>
b
diff -r 37acf42a824b -r 41e8efe8df43 save_tifffile_pyramid.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/save_tifffile_pyramid.py Fri Mar 11 23:37:49 2022 +0000
[
@@ -0,0 +1,114 @@
+import numpy as np
+import tifffile
+import skimage.transform
+
+PHYSICAL_SIZE_UNIT = ['Ym', 'Zm', 'Em', 'Pm', 'Tm', 'Gm', 'Mm', 'km', 'hm', 'dam', 'm', 'dm', 'cm', 'mm', 'µm', 'nm', 'pm', 'fm', 'am', 'zm', 'ym', 'Å', 'thou', 'li', 'in', 'ft', 'yd', 'mi', 'ua', 'ly', 'pc', 'pt', 'pixel', 'reference frame']
+
+def normalize_image_shape(img):
+    assert img.ndim in (2, 3), (
+        'image must be 2D (Y, X) or 3D (C, Y, X)'
+    )
+    if img.ndim == 2:
+        img = img.reshape(1, *img.shape)
+    assert np.argmin(img.shape) == 0, (
+        '3D image must be in (C, Y, X) order'
+    )
+    return img
+
+def save_pyramid(
+    out_img, output_path,
+    pixel_sizes=(1, 1),
+    pixel_size_units=('µm', 'µm'),
+    channel_names=None,
+    software=None,
+    is_mask=False
+):
+    assert '.ome.tif' in str(output_path)
+    assert len(pixel_sizes) == len(pixel_size_units) == 2
+    assert out_img.ndim in (2, 3), (
+        'image must be either 2D (Y, X) or 3D (C, Y, X)'
+    )
+    
+    img_shape_ori = out_img.shape
+    out_img = normalize_image_shape(out_img)
+    img_shape = out_img.shape
+
+    size_x, size_y = np.array(pixel_sizes, dtype=float)
+    unit_x, unit_y = pixel_size_units
+
+    assert (unit_x in PHYSICAL_SIZE_UNIT) & (unit_y in PHYSICAL_SIZE_UNIT), (
+        f'pixel_size_units must be a tuple of the followings: '
+        f'{", ".join(PHYSICAL_SIZE_UNIT)}'
+    )
+
+    n_channels = img_shape[0]
+    if channel_names == None:
+        channel_names = [f'Channel {i}' for i in range(n_channels)]
+    else:
+        if type(channel_names) == str:
+            channel_names = [channel_names]
+        n_channel_names = len(channel_names)
+        assert n_channel_names == n_channels, (
+            f'number of channel_names ({n_channel_names}) must match '
+            f'number of channels ({n_channels})'
+        )
+    
+    if software == None:
+        software = ''
+        
+    metadata = {
+        'Creator': software,
+        'Pixels': {
+            'PhysicalSizeX': size_x,
+            'PhysicalSizeXUnit': unit_x,
+            'PhysicalSizeY': size_y,
+            'PhysicalSizeYUnit': unit_y,
+        },
+        'Channel': {'Name': channel_names},
+        
+    }
+
+    max_size = np.max(img_shape)
+    subifds = np.ceil(np.log2(max_size / 1024)).astype(int)
+    
+    # use optimal tile size for disk space
+    tile_size = 16*np.ceil(
+        np.array(img_shape[1:]) / (2**subifds) / 16
+    ).astype(int)
+    options = {
+        'tile': tuple(tile_size)
+    }
+
+    with tifffile.TiffWriter(output_path, bigtiff=True) as tiff_out:
+        tiff_out.write(
+            data=out_img,
+            metadata=metadata,
+            software=software,
+            subifds=subifds,
+            **options
+        )
+        for i in range(subifds):
+            if i == 0:
+                down_2x_img = downsize_img_channels(out_img, is_mask=is_mask)
+            else:
+                down_2x_img = downsize_img_channels(down_2x_img, is_mask=is_mask)
+            tiff_out.write(
+                data=down_2x_img,
+                subfiletype=1,
+                **options
+            )
+
+    out_img = out_img.reshape(img_shape_ori)
+    return
+
+def downsize_channel(img, is_mask):
+    if is_mask:
+        return img[::2, ::2]
+    else:
+        return skimage.transform.downscale_local_mean(img, (2, 2)).astype(img.dtype)
+
+def downsize_img_channels(img, is_mask):
+    return np.array([
+        downsize_channel(c, is_mask=is_mask)
+        for c in img
+    ])