added a debugging mechansism so that debug images can now be turned on or off

This commit is contained in:
Guillaume Raffy 2020-02-14 18:28:34 +01:00
parent 68e2c82eae
commit ded2856619
6 changed files with 128 additions and 99 deletions

View File

@ -3,7 +3,10 @@
Using these abstract classes allows the user to write image processing code that can run with various different image engines (eg imagej or opencv).
"""
import abc
import errno
import os
ABC = abc.ABCMeta('ABC', (object,), {})
@ -215,7 +218,7 @@ class StackImageFeeder(IImageFeeder):
return self
def next(self):
print("channel %d/%d frame %d/%d slice %d/%d" % (self.next_channel_index, self.hyperstack.num_channels(), self.next_frame_index, self.hyperstack.num_frames(), self.next_slice_index, self.hyperstack.num_slices()))
# print("channel %d/%d frame %d/%d slice %d/%d" % (self.next_channel_index, self.hyperstack.num_channels(), self.next_frame_index, self.hyperstack.num_frames(), self.next_slice_index, self.hyperstack.num_slices()))
if self.end_is_reached:
raise StopIteration
else:
@ -245,13 +248,58 @@ class StackImageFeeder(IImageFeeder):
# self.next_channel_index += 1
# else:
# self.end_is_reached = True
print("after : channel %d/%d frame %d/%d slice %d/%d" % (self.next_channel_index, self.hyperstack.num_channels(), self.next_frame_index, self.hyperstack.num_frames(), self.next_slice_index, self.hyperstack.num_slices()))
# print("after : channel %d/%d frame %d/%d slice %d/%d" % (self.next_channel_index, self.hyperstack.num_channels(), self.next_frame_index, self.hyperstack.num_frames(), self.next_slice_index, self.hyperstack.num_slices()))
return image
def get_num_images(self):
return self.hyperstack.num_frames()
class IImageProcessingDebugger(ABC):
def __init__(self):
pass
@abc.abstractmethod
def on_image(self, image, image_id):
'''
:param IImage image:
:param str image_id:
'''
class NullDebugger(IImageProcessingDebugger):
def __init__(self):
IImageProcessingDebugger.__init__(self)
def on_image(self, image, image_id):
pass # do nothing
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc: # Python >= 2.5
already_exists_error_code = 20000 # normally it's errno.EEXIST (17) but for some reason it's 20000 in jython
if exc.errno == already_exists_error_code and os.path.isdir(path):
pass
else:
raise
class FileBasedDebugger(IImageProcessingDebugger):
def __init__(self, debug_images_root_path='/tmp'):
IImageProcessingDebugger.__init__(self)
self.debug_images_root_path = debug_images_root_path
mkdir_p(self.debug_images_root_path)
def on_image(self, image, image_id):
print('FileBasedDebugger.on_image : image_id = %s' % image_id)
ie = IImageEngine.get_instance()
ie.save_as_tiff(image, '%s/%s.tiff' % (self.debug_images_root_path, image_id))
class IImageEngine(ABC):
"""
The active image processor.
@ -279,6 +327,12 @@ class IImageEngine(ABC):
assert IImageEngine.__instance is not None
return IImageEngine.__instance
def __init__(self, debugger=NullDebugger()):
"""
:param IImageProcessingDebugger debugger:
"""
self.debugger = debugger
@abc.abstractmethod
def create_hyperstack(self, width, height, num_channels, num_slices, num_frames, pixel_type):
"""

View File

@ -1,7 +1,8 @@
"""Image processing using imagej.
"""
from ..imageengine import IImage, IHyperStack, IImageEngine, PixelType
import os.path
from ..imageengine import IImage, IHyperStack, IImageEngine, PixelType, IImageProcessingDebugger, NullDebugger
from ..maxima_finder import Match
from ij import IJ, ImagePlus # pylint: disable=import-error
from ij.process import FloodFiller # pylint: disable=import-error
@ -227,9 +228,20 @@ def perform_gray_morphology_with_ijopencv(image, operator, structuring_element_s
mat2imp = MatImagePlusConverter()
image.ij_image.setProcessor(mat2imp.toImageProcessor(cv_dst_image))
class ImageLogger(IImageProcessingDebugger):
def __init__(self):
IImageProcessingDebugger.__init__(self)
def on_image(self, image, image_id):
# todo : WindowManager
image.show()
class IJImageEngine(IImageEngine):
def __init__(self, debugger=NullDebugger()):
IImageEngine.__init__(self, debugger)
def missing(self):
pass
@ -245,6 +257,9 @@ class IJImageEngine(IImageEngine):
def save_as_tiff(self, image, out_file_path):
IJ.saveAsTiff(image.ij_image, out_file_path)
# IJ.saveAsTiff catches io exception in case the file couldn't be saved. So we have to check if the operation succeeded in another way
if not os.path.isfile(out_file_path):
assert False, 'failed to create %s' % out_file_path
def divide(self, numerator_image, denominator_image):
ic = ImageCalculator()

View File

@ -13,58 +13,9 @@ from ij.plugin import ZProjector # pylint: disable=import-error
from ij import WindowManager # pylint: disable=import-error
from catalog import Sequence, ImageCatalog
import telemos
from imageengine import IImageProcessingDebugger, NullDebugger
# greeting = "Hello, " + name + "!"
# image prefix :
# AF
# blé : coupes de blé
# CA : coupe d'amande
# FE : feuille d'épinard
# GGH : globule gras humain
# CRF chloroplastes de feuille d'épinard
# OL : oléosome
# DARK : dark
# white :
#
# - cin1 : cinétique 1
# phiG_40x_1 : cinétique avant et après injection enzyme gastrique
# phiG_40x_Zstack20um_1 : stack
# 0mn : on commence à enregistrer et on attend 10mn (pour le bleaching) -> phiG_40x_1
# 10mn : debut injection phase gastrique (poussée)
# 13mn : la phase gastrique (le petit tuyau contient 20ul) arrive dans la cellule d'un coup (1 nanol)
# 15mn : on arrête l'injection
# 50mn : on fait un stack -> phiG_40x_Zstack20um_1
# 51mn : début d'injection phase intestinale (poussée) -> phiG_I_40x_1
# x mn : on arrête l'injection
# 90mn : on fait un stack -> phiG_I_40x_Zstack20um_1
# - cin2 : autre échantillon similaire à cin1
# - cond[5678] : condition non réalistes
class IImageProcessingDebugger(object):
def __init__(self):
pass
def on_image(self, image, image_id):
'''
:param ImagePlus image:
:param str image_id:
'''
pass
class NullDebugger(IImageProcessingDebugger):
def __init__(self):
IImageProcessingDebugger.__init__(self)
def on_image(self, image, image_id):
pass
class ImageLogger(IImageProcessingDebugger):
def __init__(self):
@ -212,6 +163,8 @@ def test_find_white():
def run_script():
with open('/tmp/test_result.txt', 'w') as f:
f.write('%d' % {True: 0, False: 1}[False])
test_find_white()
if False:
@ -226,6 +179,8 @@ def run_script():
# dark_image = IJ.openImage(raw_images_root + '/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos0/img_000000000_DM300_327-353_fluo_000.tif')
# src_image.show()
# assert src_image
with open('/tmp/test_result.txt', 'w') as f:
f.write('%d' % {True: 0, False: 1}[True])
# If a Jython script is run, the variable __name__ contains the string '__main__'.

View File

@ -21,7 +21,7 @@ class TemplateMatcher(object):
correlation_image = ie.match_template(image, pattern)
ie.save_as_tiff(correlation_image, './similarity.tiff')
ie.debugger.on_image(correlation_image, 'similarity')
matches = self.maxima_finder.find_maxima(correlation_image)
return matches

View File

@ -22,7 +22,7 @@ class TrapBinarizer(object):
# Process/Find edges
# https://imagej.nih.gov/ij/developer/api/ij/process/ImageProcessor.html#findEdges--
edge_image = ie.compute_edge_transform(clean_trap_image)
ie.save_as_tiff(edge_image, './clean_trap_edge_image.tiff')
ie.debugger.on_image(edge_image, 'clean_trap_edge_image')
# IJ.run(imp, "Find Edges", "");
# after find edges, the walls are dark lines surrounded by 2 lines of clear pixels.
# Image/Adjust/Threshold, choose auto, then click apply
@ -33,22 +33,22 @@ class TrapBinarizer(object):
threshold_value = 0.20 # TODO: remove hardcoded value
is_edge = ie.threshold( edge_image, max_value * threshold_value)
# is_edge = ie.auto_threshold( edge_image)
ie.save_as_tiff(is_edge, './is_trap_edge.tiff')
ie.debugger.on_image(is_edge, 'is_trap_edge')
# convert image to mask
# at this point, the walls are black lines surrounded by 2 thick lines of white pixels
background_seed_pos = (70,34)
outer_edge_merged = ie.flood_fill(is_edge, background_seed_pos)
ie.save_as_tiff(outer_edge_merged, './outer_edge_merged.tiff')
ie.debugger.on_image(outer_edge_merged, 'outer_edge_merged')
# floodFill(70,34);
# ij.process.FloodFiller
# the start point of the flood fill is expected to be outside the trap so that the flood will merge with the outer white line, and stop at the wall position.
# IJ.run(imp, "Invert");
is_trap_outer_limit = ie.invert(outer_edge_merged)
ie.save_as_tiff(is_trap_outer_limit, './is_trap_outer_limit.tiff')
ie.debugger.on_image(is_trap_outer_limit, 'is_trap_outer_limit')
# the wall is now white and surrounded wy black. Inside the walls we have the inner black line, plus som more white pixels in the trap's flat areas. Filling holes will then perge together what's not the outside; after this, all trap pixels are white.
# IJ.run(imp, "Fill Holes", "");
is_trap = ie.fill_holes(is_trap_outer_limit)
ie.save_as_tiff(is_trap, './is_trap.tiff')
ie.debugger.on_image(is_trap, 'is_trap')
return is_trap
class TrapsDetector(object):
@ -80,7 +80,7 @@ class TrapsDetector(object):
template_trap_image = first_image.get_subimage(template_trap_aabb)
ie = IImageEngine.get_instance()
ie.save_as_tiff(template_trap_image, './template_trap_image.tiff')
ie.debugger.on_image(template_trap_image, 'template_trap_image')
matches = self.template_matcher.match_template(first_image, template_trap_image)
num_traps_per_frame = len(matches)
@ -102,7 +102,7 @@ class TrapsDetector(object):
image_feeder = StackImageFeeder(traps_stack)
clean_trap_image = ie.compute_median(image_feeder)
ie.save_as_tiff(clean_trap_image, './clean_trap_image.tiff')
ie.debugger.on_image(clean_trap_image, 'clean_trap_image')
binarizer = TrapBinarizer((70,34))
trap_mask = binarizer.binarize_trap(clean_trap_image)
@ -111,10 +111,10 @@ class TrapsDetector(object):
for frame_trap_index in range(num_traps_per_frame):
match = matches[frame_trap_index]
traps_mask.set_subimage(trap_mask, (match.x, match.y))
ie.save_as_tiff(traps_mask, './traps_mask.tiff')
ie.debugger.on_image(traps_mask, 'traps_mask')
#non_uniform_stack = sequence.as_stack()
#uniform_stack = IImageEngine.get_instance().divide(non_uniform_stack, white_estimate)
# IImageEngine.get_instance().save_as_tiff(white_estimate, './white_estimate.tiff')
# IImageEngine.get_instance().debugger.on_image(white_estimate, 'white_estimate')
return traps_mask

View File

@ -7,13 +7,14 @@
import unittest # unittest2 doesn't exist in fiji
import sys
from lipase.imageengine import IImageEngine, PixelType, Aabb # pylint: disable=import-error
from lipase.imageengine import IImageEngine, PixelType, Aabb, NullDebugger, FileBasedDebugger # pylint: disable=import-error
from lipase.imagej.ijimageengine import IJImageEngine, IJImage # pylint: disable=import-error
from lipase.telemos import WhiteEstimator, correct_non_uniform_lighting # pylint: disable=import-error
from lipase.maxima_finder import MaximaFinder # pylint: disable=import-error
from lipase.template_matcher import TemplateMatcher # pylint: disable=import-error
from lipase.traps_detector import TrapsDetector # pylint: disable=import-error
from lipase.catalog import ImageCatalog, Sequence # pylint: disable=import-error
from lipase.lipase import Lipase, ImageLogger # pylint: disable=import-error
class TestLipase(unittest.TestCase):
@ -24,49 +25,49 @@ class TestLipase(unittest.TestCase):
def setUp(self):
print("initializing TestLipase instance")
IImageEngine.set_instance(IJImageEngine())
IImageEngine.set_instance(IJImageEngine(debugger=FileBasedDebugger('./debug-images')))
self.catalog = ImageCatalog(self.RAW_IMAGES_ROOT_PATH)
def tearDown(self):
print("uninitializing TestLipase instance")
self.catalog = None
def test_estimate_white(self):
sequence = self.catalog.sequences['res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos2']
white_estimator = WhiteEstimator(open_size=75, close_size=75, average_size=75)
white_estimate = white_estimator.estimate_white([sequence], ['DM300_327-353_fluo'])
# find_white_reference_image(white_estimate, sequence.get_white())
print(white_estimate)
IImageEngine.get_instance().save_as_tiff(white_estimate, './white_estimate.tiff')
# assert False, "hellooooo"
print('end of test_estimate_white')
# def test_estimate_white(self):
# sequence = self.catalog.sequences['res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos2']
# white_estimator = WhiteEstimator(open_size=75, close_size=75, average_size=75)
# white_estimate = white_estimator.estimate_white([sequence], ['DM300_327-353_fluo'])
# # find_white_reference_image(white_estimate, sequence.get_white())
# print(white_estimate)
# IImageEngine.get_instance().debugger.on_image(white_estimate, 'white_estimate')
# # assert False, "hellooooo"
# print('end of test_estimate_white')
def test_uniform_lighting_correction(self):
non_uniform_sequence = self.catalog.sequences['res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos0']
uniform_sequence = correct_non_uniform_lighting(non_uniform_sequence, 'DM300_nofilter_vis', white_estimator=WhiteEstimator(open_size=75, close_size=75, average_size=75))
# def test_uniform_lighting_correction(self):
# non_uniform_sequence = self.catalog.sequences['res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos0']
# uniform_sequence = correct_non_uniform_lighting(non_uniform_sequence, 'DM300_nofilter_vis', white_estimator=WhiteEstimator(open_size=75, close_size=75, average_size=75))
def test_template_matcher(self):
sequence = self.catalog.sequences['res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos0']
stack = sequence.as_hyperstack(['DM300_nofilter_vis'], selected_frames=[0])
first_image = stack.get_image(frame_index=0)
x_min = 423
x_max = 553
y_min = 419
y_max = 533
template_trap_aabb = Aabb(x_min, y_min, x_max, y_max)
template_trap_image = first_image.get_subimage(template_trap_aabb)
for image in [first_image, template_trap_image]:
print(image.get_pixel_type(), image.get_width(), image.get_height())
# the typical value of peaks is -2.e10 and the value between peaks is below -8.0e10
threshold = -3.0e10
tolerance = 1.0e10
maxima_finder = MaximaFinder(threshold, tolerance)
template_matcher = TemplateMatcher(maxima_finder)
matches = template_matcher.match_template(first_image, template_trap_image)
num_traps = len(matches)
print("number of traps found : %d" % num_traps)
num_expected_traps = 13 # 13 traps are completely visible in the first image
self.assertAlmostEqual(len(matches), num_expected_traps, delta=1.0)
# def test_template_matcher(self):
# sequence = self.catalog.sequences['res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos0']
# stack = sequence.as_hyperstack(['DM300_nofilter_vis'], selected_frames=[0])
# first_image = stack.get_image(frame_index=0)
# x_min = 423
# x_max = 553
# y_min = 419
# y_max = 533
# template_trap_aabb = Aabb(x_min, y_min, x_max, y_max)
# template_trap_image = first_image.get_subimage(template_trap_aabb)
# for image in [first_image, template_trap_image]:
# print(image.get_pixel_type(), image.get_width(), image.get_height())
# # the typical value of peaks is -2.e10 and the value between peaks is below -8.0e10
# threshold = -3.0e10
# tolerance = 1.0e10
# maxima_finder = MaximaFinder(threshold, tolerance)
# template_matcher = TemplateMatcher(maxima_finder)
# matches = template_matcher.match_template(first_image, template_trap_image)
# num_traps = len(matches)
# print("number of traps found : %d" % num_traps)
# num_expected_traps = 13 # 13 traps are completely visible in the first image
# self.assertAlmostEqual(len(matches), num_expected_traps, delta=1.0)
def test_traps_detector(self):
@ -91,6 +92,10 @@ class TestLipase(unittest.TestCase):
print("measured_mean_value: %f" % measured_mean_value)
self.assertAlmostEqual(measured_mean_value, expected_mean_value, delta=0.01)
# def test_lipase_process(self):
# lipase = Lipase(self.catalog, debugger=NullDebugger())
def run_script():
# unittest.main() # this would result in : ImportError: No module named __main__