added a function that creates an image containing a circle

this function will be used for to complete #3
This commit is contained in:
Guillaume Raffy 2020-04-02 15:52:43 +02:00
parent 0d19ff936b
commit e8b1e331fe
5 changed files with 223 additions and 1 deletions

View File

@ -96,6 +96,17 @@ test0001: install
echo "test's return code : $$ERROR_CODE" ; \ echo "test's return code : $$ERROR_CODE" ; \
exit $$ERROR_CODE exit $$ERROR_CODE
.PHONY: test0001
imgproc_tests: install
echo 2 > '/tmp/test_result.txt' ; \
$(FIJI_EXE_PATH) --ij2 --headless --run './tests/improc_tests.py' ; \
ERROR_CODE=$$? ; \
echo "Fiji 's return code : $$ERROR_CODE" ; \
ERROR_CODE=$$(cat '/tmp/test_result.txt') ; \
echo "test's return code : $$ERROR_CODE" ; \
exit $$ERROR_CODE
.PHONY: test_globules_area .PHONY: test_globules_area
test_globules_area: install test_globules_area: install
# on macosx : /Applications/Fiji.app/Contents/MacOS/ImageJ-macosx --ij2 --headless --run './test0001.py' # on macosx : /Applications/Fiji.app/Contents/MacOS/ImageJ-macosx --ij2 --headless --run './test0001.py'
@ -109,7 +120,7 @@ test_globules_area: install
exit $$ERROR_CODE exit $$ERROR_CODE
.PHONY: test .PHONY: test
test: test0001 test: test0001 imgproc_tests
.PHONY: clean .PHONY: clean
clean: clean_doc clean: clean_doc

View File

@ -57,6 +57,10 @@ class IImage(ABC):
def get_height(self): def get_height(self):
pass pass
@abc.abstractmethod
def set_pixel(self, x, y, value):
pass
@abc.abstractmethod @abc.abstractmethod
def get_subimage(self, aabb): def get_subimage(self, aabb):
""" """

View File

@ -68,6 +68,9 @@ class IJImage(IImage):
ij_pixel_type = self.ij_image.getType() ij_pixel_type = self.ij_image.getType()
return IJ_PIXELTYPE_TO_PIXEL_TYPE[ij_pixel_type] return IJ_PIXELTYPE_TO_PIXEL_TYPE[ij_pixel_type]
def set_pixel(self, x, y, value):
self.ij_image.getProcessor().putPixelValue(x, y, value)
def get_subimage(self, aabb): def get_subimage(self, aabb):
self.ij_image.saveRoi() self.ij_image.saveRoi()
self.ij_image.setRoi(aabb.x_min, aabb.y_min, aabb.width, aabb.height) self.ij_image.setRoi(aabb.x_min, aabb.y_min, aabb.width, aabb.height)

View File

@ -0,0 +1,143 @@
"""
an image processing technique to produce a 1D signal for each pixel : this 1D signal is obtained by projecting the neighborhood of this pixel on a set of bases (each base is used as a convilution kernel)
https://subversion.ipr.univ-rennes1.fr/repos/main/projects/antipode/src/python/antipode/texori.py
"""
from imageengine import IImageEngine, PixelType
class IProjectorBase(object):
"""
a set of projectors
a projector is a grey level image, in which each pixel value is acting as a weight
"""
def __init__(self):
pass
def get_num_projectors(self):
pass
def get_projector_kernel(self, projector_index):
pass
def create_circle_image(image_size, circle_radius, circle_pos, circle_thickness, background_value=0.0, circle_value=1.0):
"""
:param dict image_size:
"""
ie = IImageEngine.get_instance()
width = image_size['width']
height = image_size['height']
circle_pos_x = float(circle_pos['x'])
circle_pos_y = float(circle_pos['y'])
r_min = circle_radius - circle_thickness * 0.5
assert r_min >= 0.0
r_max = circle_radius + circle_thickness * 0.5
r_min_square = r_min * r_min
r_max_square = r_max * r_max
image = ie.create_image(width=width, height=height, pixel_type=PixelType.F32)
for y in range(height):
for x in range(width):
dx = x - circle_pos_x
dy = y - circle_pos_y
r_square = (dx * dx + dy * dy)
if r_min_square < r_square < r_max_square:
pixel_value = circle_value
else:
pixel_value = background_value
image.set_pixel(x, y, pixel_value)
return image
class CircularSymetryProjectorBase(IProjectorBase):
"""
generates a base of circles
"""
def __init__(self, max_radius, oversampling_scale=2):
"""
:param int max_radius: the biggest circle radius in the set of projectors
:param int oversampling_scale: oversampling is used to generate antialased circles. The higher this value, the better the quality of antialiasing is
"""
super().__init__()
assert max_radius > 0
assert oversampling_scale > 0
self.max_radius = max_radius
self.oversampling_scale = oversampling_scale
self.num_projectors = max_radius + 1
def get_num_projectors(self):
return self.num_projectors
def get_projector_kernel(self, projector_index):
assert projector_index < self.get_num_projectors()
radius = projector_index
image_size = self.max_radius * 2 + 1
circle_pos = {'x': self.max_radius, 'y': self.max_radius}
oversampled_circle = create_circle_image(
image_size={'width':image_size * self.oversampling_scale, 'height':image_size*self.oversampling_scale},
circle_radius=radius * self.oversampling_scale,
circle_pos={'x': self.max_radius* self.oversampling_scale, 'y': self.max_radius* self.oversampling_scale},
circle_thickness=1 * self.oversampling_scale)
circle_image = oversampled_circle.resample(width=image_size, height=image_size)
anchor_point = circle_pos
return circle_image, anchor_point
# class LocalProjector:
# """ This image processor computes the probability for each pixel to have a material oriented in the given direction
# The technique is based on the notion of a projector. The role of the projector is to project (or cumulate) the 2D neighborhood of each pixel on a projection axis that passes through the pixel and that has an orientation chosen by the user.
# """
# def __init__(self, searched_orientation, image_process_listener=imageprocessing.NullImageProcessListener()):
# imageprocessing.IImageProcessor.__init__(self, image_process_listener)
# self.searched_orientation = searched_orientation
# self.orientation_amount_image = None
# @staticmethod
# def compute_noise_in_perpendicular_orientation(src_image, orientation, half_window_width=5, image_process_listener=imageprocessing.NullImageProcessListener()):
# """ Computes an image that gives for each pixel, the amount of noise in a direction perpendicular to the given orientation
# Because the projection has to be performed for each pixel, we encode the projector operator in the form of a set of num_projectors 2D kernels that are used as a filter. Each kernel cumulates the image signal (therefore computes the sum of the image signal) along a line perpendicular to the projection axis, but each at a different position (offset) on this projection axis.
# :param src_image: the input image, the one we want to process
# :type src_image: numpy.array
# :param orientation: the orientation along which the noise needs to be computed
# :type orientation: float (in radians)
# :param half_window_width: half size of the neighborhood window, in pixels (the heighborood window is a window of size (2*half_window_width+1, 2*half_window_width+1) centered on each pixel). The bigger, the more precise the measurement but with less locality.
# """
# projector_base = OrientationProbabilityEstimator.ProjectorBase2(orientation, searched_segment_size=10.0, cut_blur_stddev=3.0, image_process_listener=image_process_listener)
# projections = numpy.empty((src_image.shape[0], src_image.shape[1], projector_base.get_num_projectors()), dtype=numpy.float32)
# # print('projections.shape=%s' % str(projections.shape))
# for projector_index in range(projector_base.get_num_projectors()):
# projector, center_of_filter = projector_base.get_projector_kernel(projector_index)
# projection = cv2.filter2D(src_image.astype(numpy.float32), ddepth=-1, kernel=projector, anchor=tuple(center_of_filter))
# image_process_listener.onImage(projection, 'projection_%f_%d' % (orientation, projector_index))
# projections[:, :, projector_index] = projection
# # for each pixel, compute the gradient along its projection axis
# # swap axes because I don't think the filter2D only works along fist 2 axes
# projections = numpy.swapaxes(projections, 1, 2)
# grad_kernel = numpy.array((-0.5, 0.5), dtype=numpy.float32)
# gradients = cv2.filter2D(projections, ddepth=-1, kernel=grad_kernel, anchor=(0, 0))
# if image_process_listener is not None:
# # grab a slice of the gradient image for debugging purpose
# cut = gradients[:, :, gradients.shape[2] / 2]
# image_process_listener.onImage(cut, 'cut_%f' % (orientation))
# gradients = numpy.swapaxes(gradients, 1, 2)
# gradients *= gradients
# texture_amount = numpy.sum(gradients, axis=2)
# image_process_listener.onImage(texture_amount, 'texture_amount_%f' % (orientation))
# return texture_amount
# def processImage(self, image):
# self.orientation_amount_image = OrientationProbabilityEstimator.compute_noise_in_perpendicular_orientation(image, self.searched_orientation, self.get_image_process_listener())
# def get_result(self):
# return self.orientation_amount_image

61
tests/improc_tests.py Normal file
View File

@ -0,0 +1,61 @@
"""This script is supposed to be launched from fiji's jython interpreter
"""
# # note: fiji's jython doesn't support encoding keyword
# https://imagej.net/Scripting_Headless
import unittest # unittest2 doesn't exist in fiji
import sys
from lipase.imageengine import IImageEngine, PixelType, Aabb, NullDebugger, FileBasedDebugger
from lipase.imagej.ijimageengine import IJImageEngine, IJImage
from lipase.localprojector import create_circle_image
class ImProcTester(unittest.TestCase):
# we need to know if the test succeeded or not https://stackoverflow.com/questions/4414234/getting-pythons-unittest-results-in-a-teardown-method
# CURRENT_RESULT = None # holds last result object passed to run method
def setUp(self):
print("initializing ImProcTester instance")
IImageEngine.set_instance(IJImageEngine(debugger=FileBasedDebugger('./debug-images')))
def tearDown(self):
print("uninitializing ImProcTester instance")
def test_create_circle_image(self):
print("executing test_create_circle_image")
image_width = 21
image_height = 17
circle_image = create_circle_image(
image_size={'width': 21, 'height': 17},
circle_radius=7.0,
circle_pos={'x': 5.0, 'y': 13.0},
circle_thickness=1.0)
ie = IImageEngine.get_instance()
ie.debugger.on_image(circle_image, 'circle')
measured_mean_value = circle_image.get_mean_value()
expected_number_of_circle_pixels = 19
expected_mean_value = float(expected_number_of_circle_pixels) / (image_width * image_height)
print("expected_mean_value: %f" % expected_mean_value)
print("measured_mean_value: %f" % measured_mean_value)
self.assertAlmostEqual(measured_mean_value, expected_mean_value, delta=0.01)
def run_script():
print("executing run_script")
# unittest.main() # this would result in : ImportError: No module named __main__
# solution from : https://discourse.mcneel.com/t/using-unittest-in-rhino-python-not-possible/15364
suite = unittest.TestLoader().loadTestsFromTestCase(ImProcTester)
stream = sys.stdout # by default it's sys.stderr, which doesn't appear in imagej's output
test_result = unittest.TextTestRunner(stream=stream, verbosity=2).run(suite)
print('test_result : %s' % test_result)
# store summary of the result in a file so that the caller of imagej can detect that this python script failed (imagej seems to always return error code 0, regardless the error returned by the python script it executes : even sys.exit(1) doesn't change this)
with open('/tmp/test_result.txt', 'w') as f:
f.write('%d' % {True: 0, False: 1}[test_result.wasSuccessful()])
print('end of run_script')
# note : when launched from fiji, __name__ doesn't have the value "__main__", as when launched from python
run_script()