Skip to content

Instantly share code, notes, and snippets.

@jmuhlich
Last active August 4, 2021 00:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jmuhlich/96417108a2cf7d95ba9258a96904ba5a to your computer and use it in GitHub Desktop.
Save jmuhlich/96417108a2cf7d95ba9258a96904ba5a to your computer and use it in GitHub Desktop.
ImageJ BaSiC shading correction script for use with Ashlar
# @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
from ij.macro import Interpreter
from loci.plugins import BF
from loci.plugins.in import ImporterOptions
from loci.formats import ImageReader
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..."
# 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.
bfreader = ImageReader()
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()
# @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
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 str.format pattern containing
{channel} and {tile} placeholders).
Returns a list of lists, where the top level is indexed by channel number
and the bottom level is sorted filenames for that channel.
"""
(base, pattern) = os.path.split(pattern)
regex = re.sub(r'{([^:}]+)(?:[^}]*)}', r'(?P<\1>.*?)',
pattern.replace('.', '\.'))
tiles = set()
channels = set()
num_images = 0
# Dict[channel: int, List[filename: str]]
filenames = collections.defaultdict(list)
for f in os.listdir(base):
match = re.match(regex, f)
if match:
gd = match.groupdict()
tile = int(gd['tile'])
channel = int(gd['channel'])
tiles.add(tile)
channels.add(channel)
filenames[channel].append(os.path.join(base, f))
num_images += 1
if len(tiles) * len(channels) != num_images:
raise Exception("Missing some image files")
filenames = [
sorted(filenames[channel])
for channel in sorted(filenames.keys())
]
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"
#import pdb; pdb.set_trace()
print "Loading images..."
filenames = enumerate_filenames(pattern)
num_channels = len(filenames)
num_images = len(filenames[0])
image = Opener().openImage(filenames[0][0])
width = image.width
height = image.height
image.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 "==========================="
stack = ImageStack(width, height, num_images)
opener = Opener()
for i, filename in enumerate(filenames[channel]):
print "Loading image %d/%d" % (i + 1, num_images)
image = opener.openImage(filename)
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()
@jcasado
Copy link

jcasado commented Oct 4, 2018

When running on less powerful computers ImageJ was leaking memory on each iteration. Adding WindowManager.setTempCurrentImage(None) after imp.close() (line 80) solved it for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment