diff --git a/Makefile b/Makefile
index 887053e..3b78b6a 100644
--- a/Makefile
+++ b/Makefile
@@ -96,6 +96,17 @@ test0001: install
 		echo "test's return code : $$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
 test_globules_area: install
 	# 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
 
 .PHONY: test
-test: test0001
+test: test0001 imgproc_tests
 
 .PHONY: clean
 clean: clean_doc
diff --git a/src/lipase/imageengine.py b/src/lipase/imageengine.py
index 701df66..83f814b 100644
--- a/src/lipase/imageengine.py
+++ b/src/lipase/imageengine.py
@@ -57,6 +57,10 @@ class IImage(ABC):
     def get_height(self):
         pass
 
+    @abc.abstractmethod
+    def set_pixel(self, x, y, value):
+        pass
+
     @abc.abstractmethod
     def get_subimage(self, aabb):
         """
diff --git a/src/lipase/imagej/ijimageengine.py b/src/lipase/imagej/ijimageengine.py
index 2313ca6..eb58a50 100644
--- a/src/lipase/imagej/ijimageengine.py
+++ b/src/lipase/imagej/ijimageengine.py
@@ -68,6 +68,9 @@ class IJImage(IImage):
         ij_pixel_type = self.ij_image.getType()
         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):
         self.ij_image.saveRoi()
         self.ij_image.setRoi(aabb.x_min, aabb.y_min, aabb.width, aabb.height)
diff --git a/src/lipase/localprojector.py b/src/lipase/localprojector.py
new file mode 100644
index 0000000..90eb4da
--- /dev/null
+++ b/src/lipase/localprojector.py
@@ -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
+    
\ No newline at end of file
diff --git a/tests/improc_tests.py b/tests/improc_tests.py
new file mode 100644
index 0000000..f86ede8
--- /dev/null
+++ b/tests/improc_tests.py
@@ -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()