# HG changeset patch # User perssond # Date 1615508026 0 # Node ID fd8dfd64f25e50566c10b07255323199af8a5aa5 "planemo upload for repository https://github.com/ohsu-comp-bio/basic-illumination commit a8d2367c8c66eecfc2586a593acc8547a7f8611c-dirty" diff -r 000000000000 -r fd8dfd64f25e basic_illumination.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/basic_illumination.xml Fri Mar 12 00:13:46 2021 +0000 @@ -0,0 +1,50 @@ + + ImageJ BaSiC shading correction for use with Ashlar + + macros.xml + + + + @VERSION_CMD@ + + + + + + + + + + + + + + + diff -r 000000000000 -r fd8dfd64f25e imagej_basic_ashlar.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imagej_basic_ashlar.py Fri Mar 12 00:13:46 2021 +0000 @@ -0,0 +1,135 @@ +# @File(label="Select a slide to process") filename +# @File(label="Select the output location", style="directory") output_dir +# @String(label="Experiment name (base name for output files)") experiment_name +# @Float(label="Flat field smoothing parameter (0 for automatic)", value=0.1) lambda_flat +# @Float(label="Dark field smoothing parameter (0 for automatic)", value=0.01) lambda_dark + +# Takes a slide (or other multi-series BioFormats-compatible file set) and +# generates flat- and dark-field correction profile images with BaSiC. The +# output format is two multi-series TIFF files (one for flat and one for dark) +# which is the input format used by Ashlar. + +# Invocation for running from the commandline: +# +# ImageJ --ij2 --headless --run imagej_basic_ashlar.py "filename='input.ext',output_dir='output',experiment_name='my_experiment'" + +import sys +from ij import IJ, WindowManager, Prefs +from ij.macro import Interpreter +from loci.plugins import BF +from loci.plugins.in import ImporterOptions +from loci.formats import ImageReader +from loci.formats.in import DynamicMetadataOptions +import BaSiC_ as Basic + +import pdb + + +def main(): + + Interpreter.batchMode = True + + if (lambda_flat == 0) ^ (lambda_dark == 0): + print ("ERROR: Both of lambda_flat and lambda_dark must be zero," + " or both non-zero.") + return + lambda_estimate = "Automatic" if lambda_flat == 0 else "Manual" + + print "Loading images..." + + # For multi-scene .CZI files, we need raw tiles instead of the + # auto-stitched mosaic and we don't want labels or overview images. This + # only affects BF.openImagePlus, not direct use of the BioFormats reader + # classes which we also do (see below) + Prefs.set("bioformats.zeissczi.allow.autostitch", "false") + Prefs.set("bioformats.zeissczi.include.attachments", "false") + + # Use BioFormats reader directly to determine dataset dimensions without + # reading every single image. The series count (num_images) is the one value + # we can't easily get any other way, but we might as well grab the others + # while we have the reader available. + dyn_options = DynamicMetadataOptions() + # Directly calling a BioFormats reader will not use the IJ Prefs settings + # so we need to pass these options explicitly. + dyn_options.setBoolean("zeissczi.autostitch", False) + dyn_options.setBoolean("zeissczi.attachments", False) + bfreader = ImageReader() + bfreader.setMetadataOptions(dyn_options) + bfreader.id = str(filename) + num_images = bfreader.seriesCount + num_channels = bfreader.sizeC + width = bfreader.sizeX + height = bfreader.sizeY + bfreader.close() + + # The internal initialization of the BaSiC code fails when we invoke it via + # scripting, unless we explicitly set a the private 'noOfSlices' field. + # Since it's private, we need to use Java reflection to access it. + Basic_noOfSlices = Basic.getDeclaredField('noOfSlices') + Basic_noOfSlices.setAccessible(True) + basic = Basic() + Basic_noOfSlices.setInt(basic, num_images) + + # Pre-allocate the output profile images, since we have all the dimensions. + ff_image = IJ.createImage("Flat-field", width, height, num_channels, 32); + df_image = IJ.createImage("Dark-field", width, height, num_channels, 32); + + print("\n\n") + + # BaSiC works on one channel at a time, so we only read the images from one + # channel at a time to limit memory usage. + for channel in range(num_channels): + print "Processing channel %d/%d..." % (channel + 1, num_channels) + print "===========================" + + options = ImporterOptions() + options.id = str(filename) + options.setOpenAllSeries(True) + # concatenate=True gives us a single stack rather than a list of + # separate images. + options.setConcatenate(True) + # Limit the reader to the channel we're currently working on. This loop + # is mainly why we need to know num_images before opening anything. + for i in range(num_images): + options.setCBegin(i, channel) + options.setCEnd(i, channel) + # openImagePlus returns a list of images, but we expect just one (a + # stack). + input_image = BF.openImagePlus(options)[0] + + # BaSiC seems to require the input image is actually the ImageJ + # "current" image, otherwise it prints an error and aborts. + WindowManager.setTempCurrentImage(input_image) + basic.exec( + input_image, None, None, + "Estimate shading profiles", "Estimate both flat-field and dark-field", + lambda_estimate, lambda_flat, lambda_dark, + "Ignore", "Compute shading only" + ) + input_image.close() + + # Copy the pixels from the BaSiC-generated profile images to the + # corresponding channel of our output images. + ff_channel = WindowManager.getImage("Flat-field:%s" % input_image.title) + ff_image.slice = channel + 1 + ff_image.getProcessor().insert(ff_channel.getProcessor(), 0, 0) + ff_channel.close() + df_channel = WindowManager.getImage("Dark-field:%s" % input_image.title) + df_image.slice = channel + 1 + df_image.getProcessor().insert(df_channel.getProcessor(), 0, 0) + df_channel.close() + + print("\n\n") + + template = '%s/%s-%%s.tif' % (output_dir, experiment_name) + ff_filename = template % 'ffp' + IJ.saveAsTiff(ff_image, ff_filename) + ff_image.close() + df_filename = template % 'dfp' + IJ.saveAsTiff(df_image, df_filename) + df_image.close() + + print "Done!" + + +main() diff -r 000000000000 -r fd8dfd64f25e imagej_basic_ashlar_filepattern.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imagej_basic_ashlar_filepattern.py Fri Mar 12 00:13:46 2021 +0000 @@ -0,0 +1,186 @@ +# @String(label="Enter a filename pattern describing the TIFFs to process") pattern +# @File(label="Select the output location", style="directory") output_dir +# @String(label="Experiment name (base name for output files)") experiment_name +# @Float(label="Flat field smoothing parameter (0 for automatic)", value=0.1) lambda_flat +# @Float(label="Dark field smoothing parameter (0 for automatic)", value=0.01) lambda_dark + +# Takes a filename pattern describing a list of image files and generates flat- +# and dark-field correction profile images with BaSiC. The pattern must contain +# a "*" wildcard to indicate the part of the filename that varies with the image +# series number. If the images are stored with one channel per file then the +# pattern must also contain the placeholder {channel} in place of the channel +# name or number. If the image files are multi-channel then the {channel} +# placeholder must be omitted. The output format is two multi-channel TIFF files +# (one for flat and one for dark) which is the input format used by Ashlar. + +# Invocation for running from the commandline: +# (to match files like "s001_c1.tif", "s001_c2.tif", "s002_c1.tif", etc.) +# +# ImageJ --ij2 --headless --run imagej_basic_ashlar_filepattern.py "pattern='input/s*_c{channel}.tif',output_dir='output',experiment_name='my_experiment'" + +import sys +import os +import re +import collections +from ij import IJ, WindowManager, ImagePlus, ImageStack +from ij.io import Opener +from ij.macro import Interpreter +import BaSiC_ as Basic + + +def enumerate_filenames(pattern): + """Return filenames matching pattern (a glob pattern containing an optional + {channel} placeholder). + + Returns a list of lists, where the top level is indexed by sorted channel + name/number and the bottom level is filenames for that channel. + + """ + (base, pattern) = os.path.split(pattern) + regex = re.sub(r'{([^:}]+)(?:[^}]*)}', r'(?P<\1>.*?)', + pattern.replace('.', '\.').replace('*', '.*?')) + channels = set() + num_images = 0 + # Dict[Union[int, str, None], List[str]] + filenames = collections.defaultdict(list) + for f in os.listdir(base): + match = re.match(regex, f) + if match: + gd = match.groupdict() + channel = gd.get('channel', None) + try: + channel = int(channel) + except (ValueError, TypeError): + pass + channels.add(channel) + filenames[channel].append(os.path.join(base, f)) + num_images += 1 + if num_images % len(channels) != 0: + print ( + "ERROR: Some image files seem to be missing --" + " image count (%d) is not a multiple of channel count (%d)" + % (num_images, len(channels)) + ) + return [] + channels = sorted(channels) + if len(channels) > 1: + print("Detected the following channel names/numbers from filenames:") + for channel in channels: + print(" %s" % channel) + filenames = [filenames[channel] for channel in channels] + return filenames + + +def main(): + + Interpreter.batchMode = True + + if (lambda_flat == 0) ^ (lambda_dark == 0): + print ("ERROR: Both of lambda_flat and lambda_dark must be zero," + " or both non-zero.") + return + lambda_estimate = "Automatic" if lambda_flat == 0 else "Manual" + + print "Loading images..." + filenames = enumerate_filenames(pattern) + if len(filenames) == 0: + return + # This is the number of channels inferred from the filenames. The number + # of channels in an individual image file will be determined below. + num_channels = len(filenames) + num_images = len(filenames[0]) + image = Opener().openImage(filenames[0][0]) + if image.getNDimensions() > 3: + print "ERROR: Can't handle images with more than 3 dimensions." + (width, height, channels, slices, frames) = image.getDimensions() + # The third dimension could be any of these three, but the other two are + # guaranteed to be equal to 1 since we know NDimensions is <= 3. + image_channels = max((channels, slices, frames)) + image.close() + if num_channels > 1 and image_channels > 1: + print ( + "ERROR: Can only handle single-channel images with {channel} in" + " the pattern, or multi-channel images without {channel}. The" + " filename patterns imply %d channels and the images themselves" + " have %d channels." % (num_channels, image_channels) + ) + return + if image_channels == 1: + multi_channel = False + else: + print ( + "Detected multi-channel image files with %d channels" + % image_channels + ) + multi_channel = True + num_channels = image_channels + # Clone the filename list across all channels. We will handle reading + # the individual image planes for each channel below. + filenames = filenames * num_channels + + # The internal initialization of the BaSiC code fails when we invoke it via + # scripting, unless we explicitly set a the private 'noOfSlices' field. + # Since it's private, we need to use Java reflection to access it. + Basic_noOfSlices = Basic.getDeclaredField('noOfSlices') + Basic_noOfSlices.setAccessible(True) + basic = Basic() + Basic_noOfSlices.setInt(basic, num_images) + + # Pre-allocate the output profile images, since we have all the dimensions. + ff_image = IJ.createImage("Flat-field", width, height, num_channels, 32); + df_image = IJ.createImage("Dark-field", width, height, num_channels, 32); + + print("\n\n") + + # BaSiC works on one channel at a time, so we only read the images from one + # channel at a time to limit memory usage. + for channel in range(num_channels): + print "Processing channel %d/%d..." % (channel + 1, num_channels) + print "===========================" + + stack = ImageStack(width, height, num_images) + opener = Opener() + for i, filename in enumerate(filenames[channel]): + print "Loading image %d/%d" % (i + 1, num_images) + # For multi-channel images the channel determines the plane to read. + args = [channel + 1] if multi_channel else [] + image = opener.openImage(filename, *args) + stack.setProcessor(image.getProcessor(), i + 1) + input_image = ImagePlus("input", stack) + + # BaSiC seems to require the input image is actually the ImageJ + # "current" image, otherwise it prints an error and aborts. + WindowManager.setTempCurrentImage(input_image) + basic.exec( + input_image, None, None, + "Estimate shading profiles", "Estimate both flat-field and dark-field", + lambda_estimate, lambda_flat, lambda_dark, + "Ignore", "Compute shading only" + ) + input_image.close() + + # Copy the pixels from the BaSiC-generated profile images to the + # corresponding channel of our output images. + ff_channel = WindowManager.getImage("Flat-field:%s" % input_image.title) + ff_image.slice = channel + 1 + ff_image.getProcessor().insert(ff_channel.getProcessor(), 0, 0) + ff_channel.close() + df_channel = WindowManager.getImage("Dark-field:%s" % input_image.title) + df_image.slice = channel + 1 + df_image.getProcessor().insert(df_channel.getProcessor(), 0, 0) + df_channel.close() + + print("\n\n") + + template = '%s/%s-%%s.tif' % (output_dir, experiment_name) + ff_filename = template % 'ffp' + IJ.saveAsTiff(ff_image, ff_filename) + ff_image.close() + df_filename = template % 'dfp' + IJ.saveAsTiff(df_image, df_filename) + df_image.close() + + print "Done!" + + +main() diff -r 000000000000 -r fd8dfd64f25e macros.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/macros.xml Fri Mar 12 00:13:46 2021 +0000 @@ -0,0 +1,20 @@ + + + + + python + basic-illumination + + + + + echo @VERSION@ + + + + + + + 1.0.2 + ImageJ --ij2 --headless --run ${__tool_directory__}/imagej_basic_ashlar.py +