diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/__init__.py b/Modules/Biophotonics/python/iMC/mc/__init__.py similarity index 100% copy from Modules/Biophotonics/python/inverseMonteCarlo/__init__.py copy to Modules/Biophotonics/python/iMC/mc/__init__.py diff --git a/Modules/Biophotonics/python/iMC/mc/batches.py b/Modules/Biophotonics/python/iMC/mc/batches.py new file mode 100644 index 0000000000..2706262dc6 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/batches.py @@ -0,0 +1,375 @@ +''' +Created on Oct 15, 2015 + +@author: wirkert +''' + +import numpy as np +from pandas import DataFrame +import pandas as pd + + +class AbstractBatch(object): + """summarizes a batch of simulated mc spectra""" + + def __init__(self): + self._nr_layers = 0 # internally keeps track of number of layers + my_index = pd.MultiIndex(levels=[[], []], + labels=[[], []]) + self.df = DataFrame(columns=my_index) + + def create_parameters(self, nr_samples): + """create the parameters for the batch, the simulation has + to create the resulting reflectances""" + pass + + def nr_elements(self): + return self.df.shape[0] + + +class GenericBatch(AbstractBatch): + """generic n-layer batch with each layer having the same oxygenation """ + + def __init__(self): + super(GenericBatch, self).__init__() + + def append_one_layer(self, saO2, nr_samples): + """helper function to create parameters for one layer""" + + # scales data to lie between maxi and mini instead of 0 and 1 + scale = lambda x, mini, maxi: x * (maxi - mini) + mini + # shortcut to random generator + gen = np.random.random_sample + gen_n = np.random.normal + + # create layer elements + self.df["layer" + str(self._nr_layers), "vhb"] = \ + scale(gen(nr_samples), 0, 1.) + self.df["layer" + str(self._nr_layers), "sao2"] = \ + saO2 + self.df["layer" + str(self._nr_layers), "a_mie"] = \ + np.clip(gen_n(loc=18.9, scale=10.2, size=nr_samples), + 0.1, np.inf) * 100 # to 1/m + self.df["layer" + str(self._nr_layers), "b_mie"] = \ + np.clip(gen_n(loc=1.286, scale=0.521, size=nr_samples), 0, np.inf) + self.df["layer" + str(self._nr_layers), "d"] = \ + scale(gen(nr_samples), 0, 1.) + self.df["layer" + str(self._nr_layers), "n"] = \ + scale(gen(nr_samples), 1.33, 1.54) + self.df["layer" + str(self._nr_layers), "g"] = \ + scale(gen(nr_samples), 0.8, 0.95) + self._nr_layers += 1 + + def create_parameters(self, nr_samples): + """Create generic three layer batch with a total diameter of 2mm. + saO2 is the same in all layers, but all other parameters vary randomly + within each layer""" + saO2 = np.random.random_sample(size=nr_samples) + + # create three layers with random samples + self.append_one_layer(saO2, nr_samples) + self.append_one_layer(saO2, nr_samples) + self.append_one_layer(saO2, nr_samples) + + # "normalize" d to 2mm + # first extract all layers from df + self.df + + layers = [l for l in self.df.columns.levels[0] if "layer" in l] + # summarize all ds + sum_d = 0 + for l in layers: + sum_d += self.df[l, "d"] + for l in layers: + self.df[l, "d"] = self.df[l, "d"] / sum_d * 2000. * 10 ** -6 + self.df[l, "d"] = np.clip(self.df[l, "d"], 25 * 10 ** -6, np.inf) + + return self.df + + +class GenericBatch(AbstractBatch): + """generic n-layer batch with each layer having the same oxygenation """ + + def __init__(self): + super(GenericBatch, self).__init__() + + def append_one_layer(self, saO2, nr_samples): + """helper function to create parameters for one layer""" + + # scales data to lie between maxi and mini instead of 0 and 1 + scale = lambda x, mini, maxi: x * (maxi - mini) + mini + # shortcut to random generator + gen = np.random.random_sample + gen_n = np.random.normal + + # create layer elements + self.df["layer" + str(self._nr_layers), "vhb"] = \ + scale(gen(nr_samples), 0, 1.) + self.df["layer" + str(self._nr_layers), "sao2"] = \ + saO2 + self.df["layer" + str(self._nr_layers), "a_mie"] = \ + np.clip(gen_n(loc=18.9, scale=10.2, size=nr_samples), + 0.1, np.inf) * 100 # to 1/m + self.df["layer" + str(self._nr_layers), "b_mie"] = \ + np.clip(gen_n(loc=1.286, scale=0.521, size=nr_samples), 0, np.inf) + self.df["layer" + str(self._nr_layers), "d"] = \ + scale(gen(nr_samples), 0, 1.) + self.df["layer" + str(self._nr_layers), "n"] = \ + scale(gen(nr_samples), 1.33, 1.54) + self.df["layer" + str(self._nr_layers), "g"] = \ + scale(gen(nr_samples), 0.8, 0.95) + self._nr_layers += 1 + + def create_parameters(self, nr_samples): + """Create generic three layer batch with a total diameter of 2mm. + saO2 is the same in all layers, but all other parameters vary randomly + within each layer""" + saO2 = np.random.random_sample(size=nr_samples) + + # create three layers with random samples + self.append_one_layer(saO2, nr_samples) + self.append_one_layer(saO2, nr_samples) + self.append_one_layer(saO2, nr_samples) + + # "normalize" d to 2mm + # first extract all layers from df + self.df + + layers = [l for l in self.df.columns.levels[0] if "layer" in l] + # summarize all ds + sum_d = 0 + for l in layers: + sum_d += self.df[l, "d"] + for l in layers: + self.df[l, "d"] = self.df[l, "d"] / sum_d * 2000. * 10 ** -6 + self.df[l, "d"] = np.clip(self.df[l, "d"], 25 * 10 ** -6, np.inf) + + return self.df + + +class LessGenericBatch(AbstractBatch): + """less generic three layer batch. This only varies blood volume fraction + w.r.t. the ColonMuscleBatch. Let's see if DA works in this case.""" + + def __init__(self): + super(LessGenericBatch, self).__init__() + + def append_one_layer(self, saO2, n, d_ranges, nr_samples): + """helper function to create parameters for one layer""" + + # scales data to lie between maxi and mini instead of 0 and 1 + scale = lambda x, mini, maxi: x * (maxi - mini) + mini + # shortcut to random generator + gen = np.random.random_sample + + # create as generic batch + super(LessGenericBatch, self).append_one_layer(saO2, nr_samples) + self._nr_layers -= 1 # we're not finished + + # but some changes in specific layer elements + # more specific layer thicknesses + self.df["layer" + str(self._nr_layers), "d"] = \ + scale(gen(nr_samples), d_ranges[0], d_ranges[1]) + # more specific n + self.df["layer" + str(self._nr_layers), "n"] = \ + n + + self._nr_layers += 1 + + def create_parameters(self, nr_samples): + """Create generic three layer batch with a total diameter of 2mm. + saO2 is the same in all layers, but all other parameters vary randomly + within each layer""" + saO2 = np.random.random_sample(size=nr_samples) + n = np.ones_like(saO2) + # create three layers with random samples + # muscle + self.append_one_layer(saO2, n * 1.36, (600.*10 ** -6, 1010.*10 ** -6), + nr_samples) + # submucosa + self.append_one_layer(saO2, n * 1.36, (415.*10 ** -6, 847.*10 ** -6), + nr_samples) + # mucosa + self.append_one_layer(saO2, n * 1.38, (395.*10 ** -6, 603.*10 ** -6), + nr_samples) + + return self.df + + +class ColonMuscleBatch(GenericBatch): + """three layer batch simulating colonic tissue""" + + def __init__(self): + super(ColonMuscleBatch, self).__init__() + + def append_one_layer(self, saO2, n, d_ranges, nr_samples): + """helper function to create parameters for one layer""" + + # scales data to lie between maxi and mini instead of 0 and 1 + scale = lambda x, mini, maxi: x * (maxi - mini) + mini + # shortcut to random generator + gen = np.random.random_sample + + # create as generic batch + super(ColonMuscleBatch, self).append_one_layer(saO2, nr_samples) + self._nr_layers -= 1 # we're not finished + + # but some changes in specific layer elements + # less blood + self.df["layer" + str(self._nr_layers), "vhb"] = \ + scale(gen(nr_samples), 0, 0.1) + # more specific layer thicknesses + self.df["layer" + str(self._nr_layers), "d"] = \ + scale(gen(nr_samples), d_ranges[0], d_ranges[1]) + # more specific n + self.df["layer" + str(self._nr_layers), "n"] = \ + n + + self._nr_layers += 1 + + def create_parameters(self, nr_samples): + """Create generic three layer batch with a total diameter of 2mm. + saO2 is the same in all layers, but all other parameters vary randomly + within each layer""" + saO2 = np.random.random_sample(size=nr_samples) + n = np.ones_like(saO2) + # create three layers with random samples + # muscle + self.append_one_layer(saO2, n * 1.36, (600.*10 ** -6, 1010.*10 ** -6), + nr_samples) + # submucosa + self.append_one_layer(saO2, n * 1.36, (415.*10 ** -6, 847.*10 ** -6), + nr_samples) + # mucosa + self.append_one_layer(saO2, n * 1.38, (395.*10 ** -6, 603.*10 ** -6), + nr_samples) + + return self.df + + +class GenericMeanScatteringBatch(GenericBatch): + """three layer batch simulating colonic tissue""" + + def __init__(self): + super(GenericMeanScatteringBatch, self).__init__() + + def append_one_layer(self, saO2, nr_samples): + """helper function to create parameters for one layer""" + + # create as generic batch + super(GenericMeanScatteringBatch, self).append_one_layer(saO2, + nr_samples) + self._nr_layers -= 1 # we're not finished + + # restrict exponential scattering to mean value for soft tissue. + self.df["layer" + str(self._nr_layers), "b_mie"] = 1.286 + + self._nr_layers += 1 + + +class ColonMuscleMeanScatteringBatch(ColonMuscleBatch): + """three layer batch simulating colonic tissue""" + + def __init__(self): + super(ColonMuscleMeanScatteringBatch, self).__init__() + + def append_one_layer(self, saO2, n, d_ranges, nr_samples): + """helper function to create parameters for one layer""" + + # create as generic batch + super(ColonMuscleMeanScatteringBatch, self).append_one_layer(saO2, + n, + d_ranges, + nr_samples) + self._nr_layers -= 1 # we're not finished + + # restrict exponential scattering to mean value for soft tissue. + self.df["layer" + str(self._nr_layers), "b_mie"] = 1.286 + + self._nr_layers += 1 + + +class VisualizationBatch(AbstractBatch): + """batch used for visualization of different spectra. Feel free to adapt + for your visualization purposes.""" + + def __init__(self): + super(VisualizationBatch, self).__init__() + + def append_one_layer(self, vhb, sao2, a_mie, b_mie, d, n, g, nr_samples): + """helper function to create parameters for one layer""" + + # create layer elements + self.df["layer" + str(self._nr_layers), "vhb"] = vhb + self.df["layer" + str(self._nr_layers), "sao2"] = sao2 + self.df["layer" + str(self._nr_layers), "a_mie"] = a_mie + self.df["layer" + str(self._nr_layers), "b_mie"] = b_mie + self.df["layer" + str(self._nr_layers), "d"] = d + self.df["layer" + str(self._nr_layers), "n"] = n + self.df["layer" + str(self._nr_layers), "g"] = g + self._nr_layers += 1 + + def create_parameters(self, nr_samples): + # bvf = np.linspace(0.0, .1, nr_samples) + # saO2 = np.linspace(0., 1., nr_samples) + # d = np.linspace(175, 735, nr_samples) * 10 ** -6 + # a_mie = np.linspace(5., 30., nr_samples) * 100 + # a_ray = np.linspace(0., 60., nr_samples) * 100 + # n = np.linspace(1.33, 1.54, nr_samples) + # g = np.linspace(0, 0.95, nr_samples) + # create three layers with random samples + self.append_one_layer([0.1, 0.02], [0.7, 0.1], 18.9*100., 1.286, + 500 * 10 ** -6, 1.38, 0.9, + nr_samples) + self.append_one_layer(0.04, 0.7, 18.9*100., 1.286, 500 * 10 ** -6, + 1.36, 0.9, + nr_samples) + self.append_one_layer(0.04, 0.7, 18.9*100., 1.286, 500 * 10 ** -6, + 1.36, 0.9, + nr_samples) + + return self.df + + +class IntralipidPhantomBatch(AbstractBatch): + """batch used for visualization of different spectra. Feel free to adapt + for your visualization purposes.""" + + def __init__(self): + super(IntralipidPhantomBatch, self).__init__() + + def append_one_layer(self, nr_samples): + """helper function to create parameters for one layer""" + + # scales data to lie between maxi and mini instead of 0 and 1 + scale = lambda x, mini, maxi: x * (maxi - mini) + mini + # shortcut to random generator + gen = np.random.random_sample + + # create layer elements + self.df["layer" + str(self._nr_layers), "vhb"] = \ + scale(gen(nr_samples), 0.001, 0.1) + self.df["layer" + str(self._nr_layers), "sao2"] = \ + scale(gen(nr_samples), 0., 1.) + self.df["layer" + str(self._nr_layers), "a_mie"] = \ + scale(gen(nr_samples), 5., 40.) * 100 # to 1/m + self.df["layer" + str(self._nr_layers), "b_mie"] = \ + scale(gen(nr_samples), 2.3, 2.4) + self.df["layer" + str(self._nr_layers), "d"] = \ + 2000.*10**-6 + self.df["layer" + str(self._nr_layers), "n"] = \ + scale(gen(nr_samples), 1.33, 1.54) + self.df["layer" + str(self._nr_layers), "g"] = \ + scale(gen(nr_samples), 0.8, 0.95) + self._nr_layers += 1 + + def create_parameters(self, nr_samples): + """Create intralipid batch with a total diameter of 2mm. + all other parameters vary randomly + within each layer to simulate the interlipid scattering/absorption + properties.""" + + # create three layers with random samples + self.append_one_layer(nr_samples) + + return self.df diff --git a/Modules/Biophotonics/python/iMC/mc/create_spectrum.py b/Modules/Biophotonics/python/iMC/mc/create_spectrum.py new file mode 100644 index 0000000000..8d42ea197c --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/create_spectrum.py @@ -0,0 +1,41 @@ + +import logging +import time +import os + +from mc.sim import get_diffuse_reflectance + + +def create_spectrum(tissue_model, sim_wrapper, wavelengths): + """ + Create a whole spectrum from one instance (dataframe_row) using our + tissue model at wavelength wavelength. + + Args: + tissue_model: the model which should be used to generate the + spectrum + sim_wrapper: the simulation which should be used to generate the + reflectances + wavelengths: the wavelengths which shall be simulated + + Returns: the simulated reflectances + """ + start = time.time() + # map the _wavelengths array to reflectance list + + def wavelength_to_reflectance(wavelength): + # helper function to determine the reflectance for a given + # wavelength using the current model and simulation + tissue_model.set_wavelength(wavelength) + tissue_model.create_mci_file() + sim_wrapper.run_simulation() + simulation_path = os.path.split(sim_wrapper.mcml_executable)[0] + return get_diffuse_reflectance(os.path.join(simulation_path, + tissue_model. + get_mco_filename())) + reflectances = map(wavelength_to_reflectance, wavelengths) + end = time.time() + # success! + logging.info("successfully ran simulation in " + + "{:.2f}".format(end - start) + " seconds") + return reflectances diff --git a/Modules/Biophotonics/python/iMC/mc/data/beta_carotin.txt b/Modules/Biophotonics/python/iMC/mc/data/beta_carotin.txt new file mode 100644 index 0000000000..902019d56c --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/data/beta_carotin.txt @@ -0,0 +1,1924 @@ +lambda beta carotin +##Wavelength (nm) Molar Extinction (cm-1/M) +219.74 860669 +219.99 866939 +220.24 873996 +220.49 887103 +220.74 901200 +220.99 914068 +221.24 931102 +221.49 952261 +221.74 984397 +221.99 1006831 +222.24 1032652 +222.49 1061135 +222.74 1081015 +222.99 1093144 +223.24 1101278 +223.49 1095590 +223.74 1087870 +223.99 1078610 +224.24 1055667 +224.49 1019649 +224.74 984279 +224.99 942523 +225.24 897948 +225.49 847468 +225.74 802920 +225.99 750521 +226.24 704257 +226.49 657528 +226.74 612102 +226.99 568332 +227.24 522203 +227.49 483550 +227.74 445839 +227.99 410272 +228.24 376479 +228.49 346892 +228.74 317864 +228.99 291931 +229.24 267330 +229.49 244432 +229.74 224462 +229.99 206460 +230.24 189372 +230.49 174797 +230.74 162312 +230.99 149431 +231.24 139576 +231.49 129411 +231.74 119268 +231.99 111778 +232.24 104297 +232.49 97028 +232.74 91749 +232.99 85840 +233.24 82466 +233.49 78359 +233.74 74179 +233.99 71819 +234.24 68531 +234.49 65721 +234.74 62361 +234.99 60073 +235.24 57992 +235.49 56889 +235.74 54353 +235.99 52961 +236.24 51795 +236.49 51443 +236.74 49052 +236.99 49363 +237.24 49200 +237.49 48552 +237.74 47755 +237.99 47755 +238.24 47872 +238.49 46494 +238.74 45877 +238.99 45503 +239.24 46845 +239.49 45570 +239.74 46489 +239.99 45822 +240.24 45151 +240.49 45629 +240.74 45232 +240.99 45426 +241.24 44949 +241.49 45723 +241.74 45863 +241.99 44908 +242.24 45165 +242.49 45219 +242.74 44710 +242.99 45597 +243.24 44814 +243.49 45192 +243.74 45250 +243.99 44863 +244.24 44926 +244.49 44841 +244.74 44660 +244.99 44359 +245.24 44796 +245.49 44260 +245.74 45746 +245.99 45404 +246.24 45917 +246.49 45759 +246.74 46012 +246.99 45386 +247.24 45372 +247.49 46291 +247.74 45656 +247.99 46417 +248.24 46989 +248.49 46750 +248.74 47615 +248.99 48034 +249.24 48313 +249.49 48295 +249.74 48777 +249.99 49601 +250.24 49817 +250.49 50290 +250.74 50169 +250.99 49971 +251.24 51047 +251.49 51475 +251.74 52313 +251.99 52551 +252.24 52416 +252.49 54020 +252.74 54168 +252.99 53988 +253.24 53583 +253.49 55038 +253.74 55213 +253.99 55308 +254.24 55155 +254.49 56299 +254.74 56546 +254.99 56470 +255.24 56546 +255.49 56929 +255.74 56641 +255.99 57726 +256.24 58510 +256.49 58433 +256.74 58609 +256.99 58830 +257.24 59411 +257.49 60199 +257.74 59785 +257.99 61920 +258.24 62595 +258.49 63109 +258.74 64541 +258.99 63901 +259.24 64662 +259.49 65284 +259.74 66779 +259.99 66365 +260.24 67946 +260.49 67734 +260.74 69689 +260.99 70594 +261.24 71004 +261.49 72387 +261.74 74004 +261.99 75368 +262.24 75283 +262.49 75864 +262.74 75738 +262.99 77503 +263.24 76265 +263.49 77116 +263.74 77976 +263.99 78476 +264.24 78827 +264.49 79215 +264.74 79841 +264.99 79246 +265.24 79341 +265.49 78886 +265.74 79652 +265.99 80958 +266.24 80696 +266.49 82030 +266.74 82214 +266.99 82201 +267.24 83273 +267.49 84101 +267.74 84790 +267.99 86502 +268.24 87196 +268.49 87479 +268.74 88029 +268.99 89853 +269.24 89641 +269.49 90925 +269.74 93334 +269.99 93983 +270.24 94415 +270.49 95136 +270.74 95951 +270.99 97046 +271.24 98482 +271.49 99924 +271.74 101248 +271.99 101460 +272.24 102617 +272.49 104198 +272.74 104572 +272.99 103621 +273.24 104112 +273.49 103928 +273.74 103185 +273.99 102324 +274.24 103414 +274.49 102270 +274.74 100802 +274.99 100099 +275.24 98856 +275.49 97528 +275.74 98050 +275.99 96253 +276.24 96469 +276.49 97104 +276.74 97212 +276.99 97663 +277.24 98329 +277.49 98235 +277.74 98523 +277.99 98942 +278.24 100099 +278.49 99757 +278.74 101550 +278.99 101649 +279.24 100933 +279.49 102689 +279.74 102865 +279.99 102743 +280.24 103266 +280.49 104220 +280.74 104018 +280.99 103748 +281.24 104797 +281.49 105247 +281.74 105878 +281.99 105954 +282.24 107324 +282.49 108333 +282.74 109679 +282.99 110400 +283.24 111179 +283.49 111332 +283.74 111499 +283.99 110323 +284.24 108968 +284.49 108432 +284.74 106148 +284.99 104653 +285.24 102905 +285.49 99717 +285.74 97735 +285.99 95681 +286.24 92771 +286.49 90096 +286.74 88344 +286.99 85795 +287.24 83975 +287.49 83147 +287.74 80728 +287.99 79206 +288.24 79071 +288.49 78386 +288.74 77503 +288.99 78206 +289.24 77125 +289.49 78053 +289.74 76715 +289.99 77215 +290.24 77035 +290.49 77913 +290.74 77071 +290.99 77715 +291.24 76454 +291.49 76783 +291.74 74440 +291.99 75175 +292.24 73260 +292.49 72198 +292.74 71828 +292.99 71576 +293.24 71725 +293.49 71535 +293.74 70918 +293.99 70319 +294.24 70306 +294.49 71035 +294.74 70603 +294.99 70896 +295.24 70797 +295.49 70783 +295.74 69067 +295.99 67986 +296.24 66658 +296.49 64874 +296.74 62600 +296.99 61176 +297.24 59316 +297.49 56191 +297.74 53763 +297.99 50912 +298.24 48412 +298.49 44381 +298.74 41994 +298.99 39296 +299.24 36734 +299.49 34342 +299.74 32923 +299.99 31887 +300.24 30667 +300.49 29343 +300.74 27352 +300.99 28383 +301.24 26834 +301.49 25902 +301.74 24852 +301.99 23951 +302.24 23591 +302.49 22627 +302.74 22717 +302.99 22136 +303.24 21889 +303.49 22100 +303.74 22961 +303.99 21911 +304.24 20664 +304.49 20551 +304.74 20934 +304.99 19150 +305.24 20736 +305.49 19983 +305.74 21087 +305.99 20245 +306.24 18448 +306.49 18254 +306.74 17547 +306.99 16808 +307.24 16777 +307.49 16434 +307.74 16232 +307.99 17605 +308.24 16551 +308.49 17214 +308.74 16664 +308.99 16218 +309.24 16601 +309.49 16939 +309.74 16205 +309.99 19488 +310.24 16412 +310.49 17713 +310.74 18520 +310.99 12223 +311.24 14417 +311.49 16669 +311.74 18119 +311.99 15633 +312.24 15871 +312.49 14016 +312.74 15961 +312.99 15457 +313.24 13570 +313.49 16808 +313.74 15669 +313.99 14421 +314.24 16033 +314.49 14642 +314.74 16385 +314.99 13471 +315.24 14871 +315.49 17218 +315.74 15943 +315.99 17722 +316.24 13056 +316.49 16376 +316.74 14218 +316.99 13444 +317.24 15043 +317.49 14808 +317.74 12493 +317.99 14142 +318.24 13034 +318.49 11939 +318.74 13980 +318.99 15398 +319.24 12804 +319.49 14115 +319.74 14277 +319.99 15561 +320.24 16313 +320.49 14525 +320.74 13642 +320.99 11989 +321.24 13939 +321.49 14367 +321.74 14759 +321.99 12133 +322.24 13624 +322.49 14430 +322.74 8715 +322.99 14687 +323.24 13741 +323.49 11989 +323.74 10408 +323.99 12457 +324.24 13241 +324.49 12881 +324.74 12376 +324.99 13309 +325.24 14390 +325.49 12120 +325.74 13155 +325.99 12120 +326.24 13552 +326.49 12597 +326.74 13300 +326.99 11822 +327.24 16074 +327.49 14097 +327.74 13498 +327.99 12624 +328.24 14426 +328.49 12291 +328.74 11822 +328.99 13106 +329.24 13863 +329.49 13056 +329.74 11061 +329.99 14421 +330.24 14417 +330.49 13083 +330.74 15079 +330.99 13691 +331.24 12201 +331.49 15858 +331.74 14128 +331.99 14903 +332.24 14727 +332.49 12435 +332.74 13052 +332.99 14660 +333.24 15223 +333.49 13520 +333.74 12002 +333.99 13655 +334.24 14056 +334.49 13705 +334.74 12313 +334.99 14272 +335.24 14295 +335.49 16786 +335.74 14813 +335.99 13565 +336.24 15128 +336.49 14200 +336.74 14241 +336.99 13975 +337.24 12165 +337.49 12854 +337.74 16070 +337.99 12939 +338.24 14484 +338.49 14727 +338.74 16151 +338.99 16245 +339.24 14529 +339.49 15313 +339.74 15655 +339.99 13791 +340.24 13822 +340.49 16466 +340.74 14390 +340.99 15889 +341.24 13714 +341.49 12903 +341.74 11899 +341.99 13854 +342.24 11629 +342.49 13813 +342.74 14853 +342.99 15475 +343.24 13944 +343.49 13637 +343.74 14529 +343.99 14056 +344.24 14466 +344.49 14957 +344.74 14079 +344.99 13953 +345.24 16088 +345.49 14678 +345.74 12854 +345.99 14944 +346.24 13219 +346.49 16070 +346.74 14556 +346.99 15281 +347.24 13998 +347.49 15043 +347.74 15038 +347.99 15565 +348.24 14723 +348.49 15164 +348.74 13660 +348.99 14002 +349.24 14480 +349.49 15579 +349.74 14678 +349.99 15786 +350.24 15880 +350.49 16146 +350.74 14390 +350.99 15020 +351.24 15007 +351.49 15502 +351.74 14209 +351.99 16236 +352.24 16880 +352.49 14142 +352.74 14709 +352.99 15254 +353.24 14678 +353.49 16052 +353.74 15038 +353.99 13822 +354.24 15281 +354.49 14601 +354.74 14588 +354.99 16583 +355.24 15187 +355.49 16678 +355.74 15232 +355.99 15380 +356.24 15466 +356.49 17695 +356.74 15534 +356.99 17934 +357.24 17358 +357.49 16344 +357.74 15511 +357.99 15781 +358.24 17137 +358.49 16813 +358.74 16587 +358.99 15664 +359.24 16673 +359.49 14529 +359.74 15412 +359.99 15412 +360.24 15714 +360.49 16862 +360.74 15723 +360.99 17907 +361.24 16574 +361.49 17502 +361.74 17295 +361.99 16673 +362.24 17141 +362.49 17795 +362.74 16178 +362.99 17646 +363.24 18047 +363.49 17975 +363.74 17389 +363.99 16786 +364.24 16745 +364.49 18614 +364.74 17331 +364.99 17686 +365.24 19191 +365.49 18857 +365.74 17056 +365.99 19529 +366.24 20862 +366.49 18862 +366.74 19281 +366.99 19776 +367.24 20452 +367.49 19727 +367.74 19623 +367.99 18574 +368.24 21010 +368.49 19911 +368.74 20812 +368.99 20028 +369.24 22623 +369.49 22425 +369.74 21695 +369.99 21609 +370.24 21308 +370.49 22456 +370.74 21591 +370.99 21555 +371.24 22578 +371.49 22956 +371.74 23033 +371.99 23641 +372.24 24204 +372.49 24150 +372.74 24123 +372.99 25019 +373.24 23528 +373.49 23469 +373.74 25523 +373.99 25361 +374.24 24298 +374.49 25068 +374.74 25244 +374.99 26555 +375.24 26762 +375.49 25352 +375.74 26244 +375.99 26451 +376.24 26654 +376.49 27352 +376.74 26591 +376.99 27451 +377.24 27424 +377.49 28518 +377.74 27892 +377.99 28577 +378.24 28338 +378.49 29059 +378.74 29298 +378.99 29928 +379.24 29253 +379.49 30090 +379.74 29847 +379.99 29757 +380.24 32072 +380.49 30329 +380.74 29325 +380.99 32432 +381.24 30365 +381.49 30424 +381.74 32013 +381.99 31068 +382.24 32824 +382.49 32959 +382.74 34991 +382.99 31968 +383.24 33045 +383.49 33599 +383.74 34049 +383.99 34923 +384.24 33482 +384.49 34774 +384.74 36216 +384.99 35004 +385.24 36008 +385.49 35981 +385.74 36450 +385.99 35626 +386.24 36828 +386.49 36954 +386.74 38242 +386.99 37833 +387.24 37616 +387.49 38977 +387.74 38472 +387.99 38454 +388.24 39576 +388.49 38796 +388.74 40656 +388.99 40003 +389.24 40733 +389.49 40107 +389.74 42683 +389.99 41850 +390.24 42098 +390.49 40931 +390.74 42791 +390.99 43976 +391.24 44219 +391.49 44246 +391.74 45836 +391.99 45904 +392.24 44570 +392.49 46881 +392.74 47948 +392.99 46849 +393.24 47448 +393.49 48187 +393.74 48863 +393.99 50925 +394.24 50723 +394.49 50916 +394.74 49691 +394.99 50151 +395.24 51164 +395.49 52340 +395.74 52096 +395.99 51624 +396.24 54218 +396.49 53326 +396.74 53754 +396.99 55420 +397.24 55465 +397.49 54529 +397.74 55632 +397.99 56407 +398.24 57528 +398.49 57091 +398.74 56961 +398.99 59213 +399.24 58726 +399.49 59208 +399.74 59109 +399.99 58312 +400.24 60258 +400.49 60744 +400.74 60505 +400.99 61185 +401.24 62154 +401.49 61483 +401.74 61023 +401.99 62302 +402.24 61348 +402.49 61676 +402.74 62717 +402.99 62919 +403.24 62879 +403.49 64248 +403.74 63455 +403.99 63627 +404.24 65315 +404.49 64874 +404.74 65644 +404.99 66126 +405.24 65203 +405.49 66360 +405.74 65117 +405.99 66631 +406.24 66333 +406.49 67063 +406.74 68004 +406.99 68117 +407.24 68063 +407.49 68108 +407.74 68747 +407.99 68189 +408.24 71153 +408.49 69270 +408.74 70999 +408.99 70135 +409.24 71612 +409.49 72414 +409.74 73071 +409.99 73202 +410.24 73179 +410.49 73873 +410.74 74963 +410.99 75463 +411.24 75206 +411.49 75805 +411.74 76260 +411.99 76634 +412.24 78287 +412.49 77485 +412.74 78656 +412.99 78692 +413.24 80233 +413.49 79859 +413.74 81250 +413.99 80426 +414.24 82710 +414.49 83723 +414.74 83723 +414.99 82223 +415.24 83354 +415.49 83633 +415.74 85885 +415.99 86389 +416.24 86750 +416.49 88132 +416.74 88398 +416.99 88259 +417.24 90065 +417.49 89979 +417.74 90416 +417.99 91672 +418.24 92141 +418.49 93294 +418.74 92389 +418.99 94789 +419.24 94744 +419.49 95321 +419.74 96519 +419.99 97374 +420.24 97712 +420.49 97825 +420.74 98014 +420.99 99496 +421.24 99347 +421.49 99527 +421.74 99527 +421.99 100442 +422.24 100955 +422.49 101784 +422.74 102153 +422.99 103176 +423.24 103428 +423.49 103626 +423.74 103468 +423.99 103338 +424.24 103757 +424.49 104738 +424.74 105297 +424.99 105923 +425.24 105756 +425.49 105819 +425.74 106126 +425.99 106094 +426.24 106166 +426.49 105085 +426.74 105711 +426.99 105774 +427.24 106162 +427.49 107071 +427.74 107080 +427.99 106824 +428.24 107193 +428.49 106770 +428.74 108269 +428.99 107216 +429.24 107855 +429.49 107589 +429.74 107445 +429.99 106824 +430.24 107324 +430.49 107711 +430.74 109035 +430.99 107571 +431.24 108634 +431.49 108661 +431.74 107621 +431.99 109040 +432.24 108008 +432.49 108801 +432.74 109179 +432.99 109801 +433.24 109459 +433.49 109341 +433.74 110170 +433.99 110003 +434.24 110116 +434.49 109801 +434.74 111197 +434.99 111436 +435.24 110805 +435.49 112053 +435.74 111643 +435.99 113197 +436.24 112742 +436.49 113008 +436.74 115215 +436.99 114129 +437.24 113994 +437.49 115435 +437.74 116111 +437.99 116147 +438.24 116593 +438.49 117701 +438.74 118205 +438.99 117944 +439.24 118453 +439.49 119908 +439.74 119529 +439.99 120966 +440.24 120948 +440.49 121295 +440.74 122556 +440.99 123367 +441.24 124087 +441.49 124380 +441.74 125105 +441.99 126781 +442.24 124200 +442.49 126961 +442.74 127037 +442.99 127835 +443.24 128785 +443.49 127609 +443.74 129172 +443.99 130510 +444.24 130758 +444.49 130731 +444.74 131604 +444.99 132086 +445.24 132609 +445.49 132721 +445.74 132582 +445.99 135095 +446.24 135649 +446.49 133969 +446.74 134676 +446.99 135383 +447.24 136428 +447.49 135964 +447.74 136752 +447.99 136270 +448.24 137604 +448.49 138288 +448.74 137329 +448.99 136698 +449.24 137464 +449.49 139171 +449.74 139342 +449.99 138730 +450.24 138243 +450.49 139356 +450.74 138590 +450.99 139500 +451.24 137541 +451.49 138509 +451.74 138342 +451.99 137401 +452.24 137887 +452.49 137820 +452.74 136518 +452.99 137572 +453.24 136653 +453.49 136000 +453.74 135915 +453.99 135550 +454.24 135437 +454.49 133577 +454.74 134036 +454.99 133122 +455.24 134000 +455.49 132424 +455.74 133253 +455.99 130983 +456.24 131816 +456.49 131920 +456.74 129028 +456.99 129478 +457.24 129087 +457.49 129015 +457.74 126893 +457.99 126808 +458.24 125736 +458.49 125065 +458.74 125335 +458.99 124367 +459.24 123227 +459.49 122187 +459.74 121966 +459.99 121173 +460.24 119818 +460.49 119651 +460.74 119435 +460.99 118336 +461.24 118070 +461.49 116665 +461.74 116818 +461.99 116273 +462.24 114805 +462.49 114057 +462.74 113998 +462.99 113535 +463.24 113904 +463.49 112882 +463.74 111368 +463.99 112332 +464.24 111233 +464.49 111436 +464.74 109504 +464.99 110148 +465.24 110386 +465.49 109990 +465.74 109431 +465.99 110161 +466.24 110139 +466.49 108369 +466.74 108981 +466.99 107995 +467.24 107450 +467.49 107445 +467.74 107661 +467.99 108432 +468.24 107621 +468.49 108796 +468.74 107517 +468.99 107607 +469.24 108378 +469.49 107977 +469.74 108251 +469.99 108427 +470.24 108792 +470.49 108900 +470.74 108157 +470.99 109283 +471.24 109350 +471.49 109805 +471.74 109094 +471.99 109197 +472.24 109319 +472.49 109058 +472.74 109940 +472.99 110148 +473.24 110472 +473.49 110071 +473.74 111864 +473.99 111409 +474.24 110747 +474.49 111733 +474.74 111134 +474.99 112395 +475.24 112972 +475.49 112566 +475.74 112521 +475.99 111765 +476.24 111747 +476.49 111792 +476.74 112778 +476.99 110404 +477.24 112314 +477.49 112476 +477.74 113111 +477.99 112368 +478.24 111850 +478.49 110445 +478.74 111517 +478.99 110711 +479.24 110828 +479.49 110382 +479.74 109873 +479.99 110769 +480.24 110166 +480.49 108936 +480.74 108427 +480.99 108409 +481.24 108837 +481.49 107270 +481.74 105716 +481.99 106765 +482.24 105491 +482.49 104711 +482.74 103712 +482.99 104356 +483.24 103198 +483.49 102941 +483.74 101518 +483.99 100162 +484.24 99802 +484.49 98491 +484.74 98960 +484.99 96757 +485.24 96248 +485.49 95622 +485.74 94663 +485.99 95114 +486.24 93803 +486.49 91772 +486.74 91551 +486.99 89497 +487.24 89529 +487.49 85826 +487.74 86646 +487.99 85047 +488.24 85115 +488.49 83304 +488.74 81021 +488.99 80611 +489.24 80399 +489.49 79075 +489.74 77318 +489.99 76107 +490.24 74945 +490.49 74323 +490.74 72553 +490.99 70999 +491.24 71265 +491.49 70590 +491.74 68027 +491.99 66887 +492.24 65180 +492.49 64514 +492.74 63618 +492.99 62996 +493.24 59983 +493.49 59379 +493.74 57789 +493.99 58560 +494.24 55605 +494.49 55024 +494.74 54200 +494.99 52736 +495.24 52376 +495.49 50371 +495.74 50218 +495.99 49926 +496.24 48696 +496.49 47142 +496.74 46525 +496.99 45332 +497.24 44390 +497.49 42030 +497.74 41454 +497.99 40458 +498.24 41260 +498.49 37598 +498.74 37044 +498.99 36337 +499.24 35968 +499.49 34621 +499.74 33729 +499.99 32662 +500.24 32135 +500.49 32657 +500.74 30180 +500.99 29933 +501.24 28100 +501.49 27982 +501.74 27437 +501.99 26257 +502.24 24960 +502.49 25203 +502.74 24942 +502.99 24686 +503.24 23118 +503.49 22375 +503.74 21353 +503.99 20673 +504.24 19871 +504.49 20407 +504.74 18155 +504.99 19186 +505.24 19596 +505.49 17912 +505.74 16619 +505.99 16867 +506.24 16880 +506.49 15628 +506.74 15205 +506.99 15434 +507.24 14736 +507.49 13628 +507.74 13016 +507.99 13286 +508.24 13223 +508.49 13417 +508.74 12363 +508.99 11259 +509.24 11800 +509.49 10606 +509.74 9760 +509.99 10985 +510.24 10440 +510.49 9751 +510.74 10277 +510.99 9890 +511.24 9287 +511.49 9683 +511.74 8845 +511.99 8039 +512.24 7296 +512.49 7192 +512.74 7215 +512.99 7994 +513.24 7449 +513.49 6850 +513.74 7003 +513.99 7147 +514.24 6652 +514.49 7490 +514.74 6341 +514.99 6539 +515.24 5129 +515.49 5084 +515.74 4206 +515.99 4963 +516.24 4859 +516.49 5557 +516.74 5490 +516.99 4585 +517.24 4828 +517.49 4278 +517.74 4301 +517.99 4350 +518.24 5098 +518.49 4562 +518.74 4062 +518.99 4589 +519.24 3778 +519.49 3220 +519.74 3639 +519.99 3386 +520.24 3319 +520.49 2850 +520.74 3823 +520.99 4305 +521.24 3666 +521.49 3400 +521.74 1900 +521.99 4188 +522.24 2418 +522.49 3350 +522.74 2116 +522.99 2670 +523.24 3585 +523.49 2535 +523.74 3355 +523.99 1621 +524.24 2661 +524.49 2306 +524.74 2441 +524.99 2026 +525.24 1990 +525.49 2706 +525.74 2319 +525.99 1806 +526.24 2026 +526.49 3058 +526.74 2233 +526.99 319 +527.24 1873 +527.49 2814 +527.74 2837 +527.99 1999 +528.24 1612 +528.49 2143 +528.74 1977 +528.99 3026 +529.24 1549 +529.49 1180 +529.74 1508 +529.99 2229 +530.24 2040 +530.49 824 +530.74 1607 +530.99 1562 +531.24 2260 +531.49 1157 +531.74 1193 +531.99 1810 +532.24 675 +532.49 1837 +532.74 801 +532.99 1801 +533.24 2405 +533.49 963 +533.74 743 +533.99 540 +534.24 1688 +534.49 1288 +534.74 1806 +534.99 454 +535.24 2053 +535.49 1603 +535.74 1896 +535.99 1184 +536.24 -67 +536.49 1423 +536.74 1697 +536.99 986 +537.24 653 +537.49 247 +537.74 157 +537.99 -405 +538.24 1022 +538.49 797 +538.74 1495 +538.99 2062 +539.24 999 +539.49 1634 +539.74 -229 +539.99 2143 +540.24 571 +540.49 1797 +540.74 1274 +540.99 909 +541.24 1355 +541.49 1391 +541.74 1567 +541.99 1157 +542.24 855 +542.49 571 +542.74 810 +542.99 1229 +543.24 1130 +543.49 1035 +543.74 1198 +543.99 571 +544.24 1279 +544.49 319 +544.74 1283 +544.99 702 +545.24 774 +545.49 0 +545.74 1265 +545.99 963 +546.24 -297 +546.49 819 +546.74 427 +546.99 1923 +547.24 495 +547.49 1238 +547.74 405 +547.99 1292 +548.24 882 +548.49 657 +548.74 567 +548.99 900 +549.24 1851 +549.49 1202 +549.74 1373 +549.99 1369 +550.24 1756 +550.49 1153 +550.74 851 +550.99 1675 +551.24 729 +551.49 1279 +551.74 806 +551.99 -495 +552.24 508 +552.49 544 +552.74 1013 +552.99 621 +553.24 229 +553.49 972 +553.74 1697 +553.99 234 +554.24 27 +554.49 1490 +554.74 369 +554.99 747 +555.24 1963 +555.49 585 +555.74 1756 +555.99 13 +556.24 1107 +556.49 -58 +556.74 783 +556.99 1490 +557.24 -337 +557.49 1860 +557.74 666 +557.99 459 +558.24 117 +558.49 977 +558.74 310 +558.99 1026 +559.24 -3 +559.49 1576 +559.74 900 +559.99 -1184 +560.24 774 +560.49 936 +560.74 360 +560.99 801 +561.24 -819 +561.49 1580 +561.74 63 +561.99 1112 +562.24 571 +562.49 833 +562.74 283 +562.99 -211 +563.24 1035 +563.49 252 +563.74 792 +563.99 -481 +564.24 288 +564.49 540 +564.74 31 +564.99 1585 +565.24 -94 +565.49 1427 +565.74 675 +565.99 1252 +566.24 1499 +566.49 -977 +566.74 648 +566.99 -198 +567.24 1549 +567.49 1089 +567.74 680 +567.99 -423 +568.24 198 +568.49 878 +568.74 189 +568.99 193 +569.24 576 +569.49 1022 +569.74 630 +569.99 653 +570.24 481 +570.49 270 +570.74 216 +570.99 1216 +571.24 873 +571.49 -756 +571.74 1589 +571.99 175 +572.24 909 +572.49 -499 +572.74 1134 +572.99 472 +573.24 1162 +573.49 1702 +573.74 36 +573.99 1026 +574.24 1130 +574.49 896 +574.74 1058 +574.99 972 +575.24 1107 +575.49 702 +575.74 1454 +575.99 1116 +576.24 1562 +576.49 707 +576.74 1130 +576.99 1576 +577.24 364 +577.49 18 +577.74 666 +577.99 175 +578.24 905 +578.49 364 +578.74 2161 +578.99 684 +579.24 1342 +579.49 130 +579.74 923 +579.99 833 +580.24 1116 +580.49 666 +580.74 27 +580.99 1189 +581.24 454 +581.49 139 +581.74 -414 +581.99 481 +582.24 2022 +582.49 743 +582.74 594 +582.99 657 +583.24 1026 +583.49 1630 +583.74 207 +583.99 319 +584.24 553 +584.49 1166 +584.74 716 +584.99 1107 +585.24 -72 +585.49 815 +585.74 1382 +585.99 1445 +586.24 860 +586.49 535 +586.74 819 +586.99 355 +587.24 400 +587.49 626 +587.74 -121 +587.99 1175 +588.24 1116 +588.49 788 +588.74 -198 +588.99 1490 +589.24 1279 +589.49 -499 +589.74 1049 +589.99 103 +590.24 481 +590.49 1153 +590.74 1396 +590.99 -193 +591.24 999 +591.49 364 +591.74 972 +591.99 891 +592.24 -31 +592.49 810 +592.74 1171 +592.99 626 +593.24 959 +593.49 1143 +593.74 297 +593.99 558 +594.24 -94 +594.49 918 +594.74 504 +594.99 1022 +595.24 626 +595.49 211 +595.74 490 +595.99 1391 +596.24 220 +596.49 617 +596.74 261 +596.99 346 +597.24 828 +597.49 162 +597.74 121 +597.99 36 +598.24 184 +598.49 1360 +598.74 1022 +598.99 630 +599.24 1116 +599.49 1819 +599.74 -45 +599.99 734 +600.24 522 +600.49 558 +600.74 887 +600.99 81 +601.24 1373 +601.49 297 +601.74 1427 +601.99 -603 +602.24 81 +602.49 1351 +602.74 891 +602.99 635 +603.24 963 +603.49 -247 +603.74 1058 +603.99 936 +604.24 1688 +604.49 -162 +604.74 1342 +604.99 680 +605.24 963 +605.49 567 +605.74 1171 +605.99 1238 +606.24 693 +606.49 1517 +606.74 526 +606.99 792 +607.24 436 +607.49 1508 +607.74 -328 +607.99 -144 +608.24 1567 +608.49 914 +608.74 702 +608.99 891 +609.24 535 +609.49 1261 +609.74 495 +609.99 1630 +610.24 486 +610.49 1116 +610.74 1274 +610.99 1130 +611.24 914 +611.49 1022 +611.74 1504 +611.99 441 +612.24 1216 +612.49 279 +612.74 729 +612.99 1693 +613.24 968 +613.49 1234 +613.74 851 +613.99 698 +614.24 873 +614.49 1040 +614.74 617 +614.99 684 +615.24 1472 +615.49 608 +615.74 779 +615.99 -139 +616.24 1851 +616.49 1391 +616.74 1540 +616.99 463 +617.24 1080 +617.49 450 +617.74 1058 +617.99 1067 +618.24 819 +618.49 783 +618.74 747 +618.99 603 +619.24 1454 +619.49 896 +619.74 1243 +619.99 1743 +620.24 535 +620.49 1607 +620.74 -198 +620.99 1026 +621.24 535 +621.49 54 +621.74 1121 +621.99 432 +622.24 1243 +622.49 846 +622.74 918 +622.99 662 +623.24 2058 +623.49 207 +623.74 508 +623.99 869 +624.24 -13 +624.49 1387 +624.74 950 +624.99 1779 +625.24 1017 +625.49 1369 +625.74 720 +625.99 346 +626.24 2076 +626.49 319 +626.74 1495 +626.99 1202 +627.24 1274 +627.49 2175 +627.74 743 +627.99 1977 +628.24 680 +628.49 1292 +628.74 1234 +628.99 1914 +629.24 626 +629.49 27 +629.74 1882 +629.99 535 +630.24 1495 +630.49 1171 +630.74 720 +630.99 571 +631.24 1049 +631.49 680 +631.74 788 +631.99 1981 +632.24 738 +632.49 801 +632.74 986 +632.99 837 +633.24 1265 +633.49 1481 +633.74 1896 +633.99 738 +634.24 990 +634.49 729 +634.74 968 +634.99 1684 +635.24 963 +635.49 2310 +635.74 743 +635.99 1071 +636.24 1247 +636.49 617 +636.74 2139 +636.99 1688 +637.24 1432 +637.49 1004 +637.74 788 +637.99 1234 +638.24 761 +638.49 806 +638.74 743 +638.99 1589 +639.24 1490 +639.49 2116 +639.74 1774 +639.99 936 +640.24 1612 +640.49 1193 +640.74 977 +640.99 720 +641.24 1238 +641.49 1657 +641.74 900 +641.99 1603 +642.24 761 +642.49 878 +642.74 599 +642.99 585 +643.24 1891 +643.49 1684 +643.74 2022 +643.99 1634 +644.24 873 +644.49 342 +644.74 1373 +644.99 346 +645.24 1774 +645.49 1107 +645.74 977 +645.99 1909 +646.24 468 +646.49 1171 +646.74 662 +646.99 1837 +647.24 274 +647.49 562 +647.74 932 +647.99 621 +648.24 648 +648.49 815 +648.74 1220 +648.99 1022 +649.24 1067 +649.49 828 +649.74 373 +649.99 553 +650.24 828 +650.49 1116 +650.74 1752 +650.99 297 +651.24 2085 +651.49 45 +651.74 1103 +651.99 562 +652.24 1234 +652.49 540 +652.74 581 +652.99 400 +653.24 1288 +653.49 644 +653.74 423 +653.99 1472 +654.24 490 +654.49 684 +654.74 1571 +654.99 1094 +655.24 535 +655.49 198 +655.74 454 +655.99 666 +656.24 445 +656.49 234 +656.74 544 +656.99 1594 +657.24 806 +657.49 941 +657.74 797 +657.99 914 +658.25 234 +658.5 819 +658.75 873 +659 36 +659.25 1481 +659.5 1306 +659.75 662 +660 454 +660.25 1013 +660.5 945 +660.75 -108 +661 1504 +661.25 472 +661.5 1004 +661.75 391 +662 -67 +662.25 648 +662.5 -139 +662.75 869 +663 306 +663.25 1400 +663.5 1116 +663.75 887 +664 1040 +664.25 391 +664.5 378 +664.75 -337 +665 666 +665.25 562 +665.5 112 +665.75 806 +666 544 +666.25 1508 +666.5 90 +666.75 211 +667 378 +667.25 -441 +667.5 540 +667.75 486 +668 562 +668.25 -36 +668.5 247 +668.75 423 +669 -486 +669.25 225 +669.5 918 +669.75 1049 +670 1409 +670.25 549 +670.5 477 +670.75 369 +671 283 +671.25 891 +671.5 171 +671.75 49 +672 387 +672.25 1008 +672.5 405 +672.75 67 +673 -175 +673.25 905 +673.5 -139 +673.75 558 +674 945 +674.25 959 +674.5 450 +674.75 508 +675 234 +675.25 297 +675.5 -13 +675.75 -49 +676 414 +676.25 382 +676.5 -103 +676.75 1013 +677 639 +677.25 513 +677.5 -58 +677.75 1661 +678 -153 +678.25 40 +678.5 130 +678.75 639 +679 941 +679.25 -477 +679.5 -108 +679.75 180 +680 472 +680.25 765 +680.5 -297 +680.75 1220 +681 684 +681.25 450 +681.5 234 +681.75 310 +682 -54 +682.25 279 +682.5 261 +682.75 499 +683 -135 +683.25 517 +683.5 -2 +683.75 94 +684 72 +684.25 680 +684.5 -108 +684.75 279 +685 513 +685.25 -328 +685.5 594 +685.75 126 +686 675 +686.25 103 +686.5 387 +686.75 927 +687 -310 +687.25 364 +687.5 306 +687.75 756 +688 810 +688.25 725 +688.5 54 +688.75 40 +689 720 +689.25 409 +689.5 657 +689.75 315 +690 599 +690.25 373 +690.5 -247 +690.75 490 +691 54 +691.25 540 +691.5 -126 +691.75 -549 +692 486 +692.25 788 +692.5 -234 +692.75 -162 +693 171 +693.25 409 +693.5 477 +693.75 -981 +694 157 +694.25 99 +694.5 1071 +694.75 -648 +695 -58 +695.25 477 +695.5 220 +695.75 211 +696 -171 +696.25 126 +696.5 666 +696.75 166 +697 743 +697.25 387 +697.5 -130 +697.75 716 +698 409 +698.25 4 +698.5 774 +698.75 1639 +699 252 +699.25 396 +699.5 -454 +699.75 -148 +700 1436 diff --git a/Modules/Biophotonics/python/iMC/mc/data/bilirubin.txt b/Modules/Biophotonics/python/iMC/mc/data/bilirubin.txt new file mode 100644 index 0000000000..3ca6e8e1c0 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/data/bilirubin.txt @@ -0,0 +1,1844 @@ +# taken from http://omlc.org/spectra/PhotochemCAD/data/119-abs.txt +##Wavelength (nm) Molar Extinction (cm-1/M) +239.75 17208 +240 16074 +240.25 18576 +240.5 16910 +240.75 17810 +241 17784 +241.25 17734 +241.5 18617 +241.75 17737 +242 17532 +242.25 17582 +242.5 17609 +242.75 17141 +243 17102 +243.25 16666 +243.5 16594 +243.75 16111 +244 16576 +244.25 15964 +244.5 15470 +244.75 15805 +245 15304 +245.25 15243 +245.5 15230 +245.75 14725 +246 14481 +246.25 14588 +246.5 14454 +246.75 14002 +247 14152 +247.25 13951 +247.5 13735 +247.75 13755 +248 13531 +248.25 13231 +248.5 13354 +248.75 13226 +249 12855 +249.25 13256 +249.5 12851 +249.75 12557 +250 12254 +250.25 12287 +250.5 12174 +250.75 12231 +251 11840 +251.25 11772 +251.5 11575 +251.75 11246 +252 11231 +252.25 11165 +252.5 11021 +252.75 11034 +253 11068 +253.25 10789 +253.5 10691 +253.75 10568 +254 10477 +254.25 10341 +254.5 10346 +254.75 10029 +255 10164 +255.25 10055 +255.5 9970 +255.75 9865 +256 9945 +256.25 9764 +256.5 9632 +256.75 9781 +257 9670 +257.25 9526 +257.5 9223 +257.75 9096 +258 9220 +258.25 9312 +258.5 9145 +258.75 9138 +259 8955 +259.25 8984 +259.5 8862 +259.75 8933 +260 8891 +260.25 8896 +260.5 8816 +260.75 8984 +261 8774 +261.25 8840 +261.5 8737 +261.75 8679 +262 8719 +262.25 8874 +262.5 8777 +262.75 8685 +263 8627 +263.25 8732 +263.5 8527 +263.75 8572 +264 8650 +264.25 8812 +264.5 8614 +264.75 8601 +265 8612 +265.25 8688 +265.5 8513 +265.75 8498 +266 8698 +266.25 8554 +266.5 8479 +266.75 8449 +267 8560 +267.25 8544 +267.5 8536 +267.75 8448 +268 8470 +268.25 8574 +268.5 8519 +268.75 8580 +269 8518 +269.25 8460 +269.5 8514 +269.75 8616 +270 8620 +270.25 8681 +270.5 8436 +270.75 8509 +271 8573 +271.25 8526 +271.5 8474 +271.75 8543 +272 8591 +272.25 8515 +272.5 8537 +272.75 8695 +273 8550 +273.25 8547 +273.5 8596 +273.75 8545 +274 8435 +274.25 8695 +274.5 8471 +274.75 8560 +275 8727 +275.25 8631 +275.5 8652 +275.75 8360 +276 8625 +276.25 8456 +276.5 8639 +276.75 8632 +277 8473 +277.25 8521 +277.5 8589 +277.75 8557 +278 8627 +278.25 8344 +278.5 8597 +278.75 8561 +279 8646 +279.25 8452 +279.5 8527 +279.75 8426 +280 8634 +280.25 8461 +280.5 8496 +280.75 8525 +281 8383 +281.25 8515 +281.5 8307 +281.75 8411 +282 8374 +282.25 8663 +282.5 8398 +282.75 8418 +283 8501 +283.25 8565 +283.5 8227 +283.75 8286 +284 8582 +284.25 8334 +284.5 8305 +284.75 8220 +285 8313 +285.25 8211 +285.5 8302 +285.75 8226 +286 8234 +286.25 8471 +286.5 8205 +286.75 8313 +287 8258 +287.25 8237 +287.5 8234 +287.75 8210 +288 8104 +288.25 8203 +288.5 8269 +288.75 8138 +289 8030 +289.25 8063 +289.5 8104 +289.75 8119 +290 7874 +290.25 7970 +290.5 8081 +290.75 8068 +291 8115 +291.25 7943 +291.5 7911 +291.75 7808 +292 7876 +292.25 7947 +292.5 7832 +292.75 8024 +293 7867 +293.25 7657 +293.5 7798 +293.75 7739 +294 7745 +294.25 7622 +294.5 7718 +294.75 7922 +295 7810 +295.25 7709 +295.5 7784 +295.75 7745 +296 7644 +296.25 7642 +296.5 7512 +296.75 7609 +297 7791 +297.25 7664 +297.5 7414 +297.75 7515 +298 7596 +298.25 7627 +298.5 7578 +298.75 7628 +299 7569 +299.25 7374 +299.5 7429 +299.75 7468 +300 7370 +300.25 7448 +300.5 7331 +300.75 7592 +301 7374 +301.25 7214 +301.5 7484 +301.75 7449 +302 7282 +302.25 7230 +302.5 7328 +302.75 7071 +303 7348 +303.25 7135 +303.5 7224 +303.75 7300 +304 7399 +304.25 7224 +304.5 7244 +304.75 7208 +305 7085 +305.25 7036 +305.5 7257 +305.75 7153 +306 6989 +306.25 6795 +306.5 7012 +306.75 6993 +307 7047 +307.25 7085 +307.5 6900 +307.75 6823 +308 6737 +308.25 6897 +308.5 6852 +308.75 7083 +309 6903 +309.25 6895 +309.5 6826 +309.75 6820 +310 6811 +310.25 6907 +310.5 6804 +310.75 6522 +311 6559 +311.25 6618 +311.5 6688 +311.75 6701 +312 6632 +312.25 6548 +312.5 6667 +312.75 6580 +313 6519 +313.25 6607 +313.5 6338 +313.75 6559 +314 6529 +314.25 6385 +314.5 6425 +314.75 6346 +315 6458 +315.25 6318 +315.5 6260 +315.75 6212 +316 6277 +316.25 6176 +316.5 6219 +316.75 6363 +317 6160 +317.25 6304 +317.5 6070 +317.75 6201 +318 6177 +318.25 6108 +318.5 6033 +318.75 5911 +319 5864 +319.25 5951 +319.5 5995 +319.75 5901 +320 6010 +320.25 5815 +320.5 5753 +320.75 5996 +321 5886 +321.25 5830 +321.5 5816 +321.75 5627 +322 5870 +322.25 5621 +322.5 5559 +322.75 5697 +323 5679 +323.25 5544 +323.5 5478 +323.75 5637 +324 5690 +324.25 5486 +324.5 5325 +324.75 5452 +325 5328 +325.25 5432 +325.5 5564 +325.75 5209 +326 5194 +326.25 5150 +326.5 5252 +326.75 5311 +327 5056 +327.25 5097 +327.5 5312 +327.75 5208 +328 5030 +328.25 4895 +328.5 5167 +328.75 5078 +329 4928 +329.25 5029 +329.5 4921 +329.75 4866 +330 4851 +330.25 4971 +330.5 4965 +330.75 4756 +331 4830 +331.25 4924 +331.5 4578 +331.75 4746 +332 4801 +332.25 4654 +332.5 4736 +332.75 4795 +333 4822 +333.25 4698 +333.5 4701 +333.75 4652 +334 4351 +334.25 4609 +334.5 4523 +334.75 4423 +335 4573 +335.25 4883 +335.5 4723 +335.75 4337 +336 4560 +336.25 4593 +336.5 4807 +336.75 4531 +337 4442 +337.25 4470 +337.5 4425 +337.75 4531 +338 4267 +338.25 4399 +338.5 4248 +338.75 4383 +339 4350 +339.25 4346 +339.5 4390 +339.75 4632 +340 4394 +340.25 4330 +340.5 4231 +340.75 4441 +341 4371 +341.25 4360 +341.5 4383 +341.75 4538 +342 4197 +342.25 4176 +342.5 4356 +342.75 4335 +343 4234 +343.25 4170 +343.5 4184 +343.75 4384 +344 4472 +344.25 4256 +344.5 4466 +344.75 4355 +345 4351 +345.25 4556 +345.5 4298 +345.75 4225 +346 4508 +346.25 4502 +346.5 4557 +346.75 4384 +347 4371 +347.25 4533 +347.5 4385 +347.75 4628 +348 4396 +348.25 4668 +348.5 4687 +348.75 4254 +349 4565 +349.25 4804 +349.5 4687 +349.75 4724 +350 4639 +350.25 4570 +350.5 4544 +350.75 4738 +351 4801 +351.25 4728 +351.5 4914 +351.75 4843 +352 4826 +352.25 4962 +352.5 5069 +352.75 4863 +353 4951 +353.25 5046 +353.5 5156 +353.75 5211 +354 5139 +354.25 4968 +354.5 5138 +354.75 5196 +355 5241 +355.25 5325 +355.5 5324 +355.75 5280 +356 5180 +356.25 5445 +356.5 5225 +356.75 5468 +357 5401 +357.25 5586 +357.5 5684 +357.75 5657 +358 5600 +358.25 5847 +358.5 5801 +358.75 5667 +359 5802 +359.25 5822 +359.5 5899 +359.75 6019 +360 5936 +360.25 5993 +360.5 6071 +360.75 6071 +361 6135 +361.25 6222 +361.5 6196 +361.75 6219 +362 6386 +362.25 6342 +362.5 6462 +362.75 6449 +363 6670 +363.25 6545 +363.5 6722 +363.75 6663 +364 6678 +364.25 6709 +364.5 6865 +364.75 6765 +365 6972 +365.25 6997 +365.5 6991 +365.75 7078 +366 7225 +366.25 7159 +366.5 7295 +366.75 7440 +367 7392 +367.25 7443 +367.5 7485 +367.75 7604 +368 7718 +368.25 7726 +368.5 7732 +368.75 7787 +369 8029 +369.25 7979 +369.5 8120 +369.75 8211 +370 8189 +370.25 8371 +370.5 8318 +370.75 8419 +371 8399 +371.25 8576 +371.5 8579 +371.75 8714 +372 8934 +372.25 8884 +372.5 8952 +372.75 8892 +373 9054 +373.25 9118 +373.5 9234 +373.75 9307 +374 9389 +374.25 9520 +374.5 9382 +374.75 9615 +375 9685 +375.25 9807 +375.5 9809 +375.75 10012 +376 9949 +376.25 10165 +376.5 10160 +376.75 10361 +377 10418 +377.25 10527 +377.5 10587 +377.75 10720 +378 10795 +378.25 10832 +378.5 10931 +378.75 11099 +379 11352 +379.25 11250 +379.5 11380 +379.75 11488 +380 11551 +380.25 11693 +380.5 11781 +380.75 12018 +381 12026 +381.25 12158 +381.5 12153 +381.75 12321 +382 12376 +382.25 12542 +382.5 12561 +382.75 12914 +383 12831 +383.25 13004 +383.5 13033 +383.75 13120 +384 13339 +384.25 13526 +384.5 13534 +384.75 13639 +385 13778 +385.25 13924 +385.5 14011 +385.75 14125 +386 14245 +386.25 14357 +386.5 14611 +386.75 14578 +387 14794 +387.25 14943 +387.5 15004 +387.75 15132 +388 15217 +388.25 15346 +388.5 15625 +388.75 15701 +389 15763 +389.25 15872 +389.5 16082 +389.75 16198 +390 16334 +390.25 16618 +390.5 16750 +390.75 16922 +391 16897 +391.25 17153 +391.5 17363 +391.75 17306 +392 17508 +392.25 17645 +392.5 17947 +392.75 18077 +393 18306 +393.25 18360 +393.5 18564 +393.75 18681 +394 18947 +394.25 19077 +394.5 19257 +394.75 19386 +395 19603 +395.25 19819 +395.5 19840 +395.75 20018 +396 20306 +396.25 20537 +396.5 20696 +396.75 20890 +397 21025 +397.25 21099 +397.5 21508 +397.75 21590 +398 21695 +398.25 22046 +398.5 22099 +398.75 22208 +399 22456 +399.25 22739 +399.5 22910 +399.75 23068 +400 23223 +400.25 23332 +400.5 23529 +400.75 23716 +401 23938 +401.25 24060 +401.5 24153 +401.75 24483 +402 24617 +402.25 24823 +402.5 24967 +402.75 25185 +403 25435 +403.25 25604 +403.5 25578 +403.75 25931 +404 26126 +404.25 26394 +404.5 26468 +404.75 26635 +405 26737 +405.25 27056 +405.5 27230 +405.75 27451 +406 27857 +406.25 27866 +406.5 28176 +406.75 28139 +407 28467 +407.25 28640 +407.5 28746 +407.75 29143 +408 29168 +408.25 29527 +408.5 29637 +408.75 29858 +409 30049 +409.25 30302 +409.5 30569 +409.75 30645 +410 30831 +410.25 30924 +410.5 31204 +410.75 31548 +411 31698 +411.25 31887 +411.5 32216 +411.75 32411 +412 32610 +412.25 32535 +412.5 32945 +412.75 32846 +413 33200 +413.25 33433 +413.5 33480 +413.75 33777 +414 34095 +414.25 34190 +414.5 34531 +414.75 34834 +415 34903 +415.25 35085 +415.5 35414 +415.75 35564 +416 35706 +416.25 35778 +416.5 36112 +416.75 36431 +417 36596 +417.25 36893 +417.5 37139 +417.75 37287 +418 37558 +418.25 37587 +418.5 37957 +418.75 38212 +419 38475 +419.25 38640 +419.5 38902 +419.75 39044 +420 39293 +420.25 39645 +420.5 39675 +420.75 39886 +421 40236 +421.25 40299 +421.5 40898 +421.75 40977 +422 41146 +422.25 41209 +422.5 41438 +422.75 41659 +423 42024 +423.25 41937 +423.5 42254 +423.75 42509 +424 42569 +424.25 42830 +424.5 43004 +424.75 43220 +425 43491 +425.25 43775 +425.5 43958 +425.75 44066 +426 44106 +426.25 44366 +426.5 44656 +426.75 44818 +427 44920 +427.25 45175 +427.5 45289 +427.75 45205 +428 45653 +428.25 45663 +428.5 46031 +428.75 45880 +429 46284 +429.25 46275 +429.5 46321 +429.75 46875 +430 46801 +430.25 47034 +430.5 47183 +430.75 47159 +431 47497 +431.25 47738 +431.5 47623 +431.75 47944 +432 48068 +432.25 48045 +432.5 48481 +432.75 48552 +433 48455 +433.25 48794 +433.5 48981 +433.75 48824 +434 49361 +434.25 49335 +434.5 49562 +434.75 49363 +435 49796 +435.25 49830 +435.5 50195 +435.75 50058 +436 50080 +436.25 50471 +436.5 50242 +436.75 50212 +437 50999 +437.25 50705 +437.5 50718 +437.75 51066 +438 51272 +438.25 51313 +438.5 51585 +438.75 51472 +439 51653 +439.25 51846 +439.5 51777 +439.75 51924 +440 52109 +440.25 52208 +440.5 52280 +440.75 52293 +441 52356 +441.25 52457 +441.5 52557 +441.75 52914 +442 53001 +442.25 52673 +442.5 52891 +442.75 52803 +443 53133 +443.25 52993 +443.5 53312 +443.75 53499 +444 53570 +444.25 53376 +444.5 53727 +444.75 53590 +445 53466 +445.25 53330 +445.5 53550 +445.75 53855 +446 53945 +446.25 54100 +446.5 53803 +446.75 54042 +447 53727 +447.25 54205 +447.5 54320 +447.75 54323 +448 54336 +448.25 54364 +448.5 54198 +448.75 54445 +449 54593 +449.25 54669 +449.5 54570 +449.75 54814 +450 54889 +450.25 55016 +450.5 54631 +450.75 55023 +451 54904 +451.25 55003 +451.5 54709 +451.75 54692 +452 54913 +452.25 54882 +452.5 54780 +452.75 54816 +453 54985 +453.25 54709 +453.5 54940 +453.75 54965 +454 54944 +454.25 54897 +454.5 54881 +454.75 54860 +455 54838 +455.25 54970 +455.5 54914 +455.75 54792 +456 54852 +456.25 54810 +456.5 54653 +456.75 54691 +457 54697 +457.25 54499 +457.5 54411 +457.75 54379 +458 54456 +458.25 54476 +458.5 54507 +458.75 54318 +459 54179 +459.25 54368 +459.5 53982 +459.75 53776 +460 53869 +460.25 53863 +460.5 53765 +460.75 53687 +461 53612 +461.25 53421 +461.5 53163 +461.75 53220 +462 53073 +462.25 52805 +462.5 52643 +462.75 52655 +463 52297 +463.25 52346 +463.5 52125 +463.75 52124 +464 51893 +464.25 51713 +464.5 51601 +464.75 51196 +465 51182 +465.25 50770 +465.5 50676 +465.75 50469 +466 50213 +466.25 50146 +466.5 49932 +466.75 49547 +467 49358 +467.25 49033 +467.5 48863 +467.75 48688 +468 48330 +468.25 48011 +468.5 47847 +468.75 47529 +469 47213 +469.25 47164 +469.5 46779 +469.75 46505 +470 46188 +470.25 45749 +470.5 45325 +470.75 45059 +471 44709 +471.25 44313 +471.5 44104 +471.75 43981 +472 43437 +472.25 43051 +472.5 42721 +472.75 42370 +473 41989 +473.25 41733 +473.5 41243 +473.75 40789 +474 40495 +474.25 39942 +474.5 39694 +474.75 39259 +475 38765 +475.25 38218 +475.5 37894 +475.75 37467 +476 36981 +476.25 36510 +476.5 36003 +476.75 35534 +477 35156 +477.25 34646 +477.5 34092 +477.75 33757 +478 33338 +478.25 32856 +478.5 32487 +478.75 31972 +479 31378 +479.25 30972 +479.5 30621 +479.75 30125 +480 29636 +480.25 29219 +480.5 28809 +480.75 28426 +481 27950 +481.25 27596 +481.5 27063 +481.75 26732 +482 26271 +482.25 25872 +482.5 25391 +482.75 24929 +483 24544 +483.25 24180 +483.5 23794 +483.75 23353 +484 22833 +484.25 22532 +484.5 22123 +484.75 21593 +485 21333 +485.25 20952 +485.5 20411 +485.75 20085 +486 19771 +486.25 19386 +486.5 18940 +486.75 18564 +487 18211 +487.25 17827 +487.5 17433 +487.75 17152 +488 16794 +488.25 16455 +488.5 16035 +488.75 15691 +489 15367 +489.25 14995 +489.5 14678 +489.75 14362 +490 14025 +490.25 13750 +490.5 13415 +490.75 13103 +491 12797 +491.25 12457 +491.5 12191 +491.75 11964 +492 11835 +492.25 11548 +492.5 11298 +492.75 11013 +493 10718 +493.25 10489 +493.5 10199 +493.75 9944 +494 9634 +494.25 9399 +494.5 9098 +494.75 8913 +495 8611 +495.25 8326 +495.5 8195 +495.75 7887 +496 7707 +496.25 7520 +496.5 7320 +496.75 7033 +497 6821 +497.25 6602 +497.5 6485 +497.75 6171 +498 6090 +498.25 5889 +498.5 5721 +498.75 5508 +499 5400 +499.25 5182 +499.5 5068 +499.75 4893 +500 4767 +500.25 4534 +500.5 4461 +500.75 4285 +501 4204 +501.25 4053 +501.5 3875 +501.75 3753 +502 3654 +502.25 3487 +502.5 3412 +502.75 3333 +503 3151 +503.25 3151 +503.5 3037 +503.75 2899 +504 2794 +504.25 2746 +504.5 2654 +504.75 2530 +505 2503 +505.25 2359 +505.5 2327 +505.75 2211 +506 2144 +506.25 2030 +506.5 2033 +506.75 2005 +507 1863 +507.25 1858 +507.5 1718 +507.75 1776 +508 1702 +508.25 1587 +508.5 1611 +508.75 1561 +509 1466 +509.25 1385 +509.5 1383 +509.75 1374 +510 1299 +510.25 1314 +510.5 1200 +510.75 1148 +511 1116 +511.25 1032 +511.5 1108 +511.75 995 +512 966 +512.25 941 +512.5 920 +512.75 908 +513 846 +513.25 848 +513.5 846 +513.75 737 +514 782 +514.25 724 +514.5 716 +514.75 704 +515 624 +515.25 616 +515.5 574 +515.75 602 +516 582 +516.25 547 +516.5 506 +516.75 525 +517 485 +517.25 488 +517.5 487 +517.75 431 +518 396 +518.25 411 +518.5 418 +518.75 365 +519 401 +519.25 361 +519.5 375 +519.75 346 +520 340 +520.25 326 +520.5 357 +520.75 292 +521 306 +521.25 270 +521.5 273 +521.75 251 +522 212 +522.25 253 +522.5 285 +522.75 233 +523 251 +523.25 247 +523.5 236 +523.75 235 +524 214 +524.25 188 +524.5 212 +524.75 201 +525 214 +525.25 169 +525.5 136 +525.75 182 +526 222 +526.25 147 +526.5 164 +526.75 133 +527 191 +527.25 176 +527.5 141 +527.75 156 +528 208 +528.25 179 +528.5 117 +528.75 116 +529 152 +529.25 121 +529.5 153 +529.75 178 +530 129 +530.25 102 +530.5 163 +530.75 118 +531 130 +531.25 82 +531.5 83 +531.75 99 +532 126 +532.25 153 +532.5 70 +532.75 109 +533 101 +533.25 72 +533.5 72 +533.75 114 +534 68 +534.25 71 +534.5 90 +534.75 177 +535 107 +535.25 107 +535.5 80 +535.75 92 +536 121 +536.25 37 +536.5 15 +536.75 54 +537 120 +537.25 104 +537.5 59 +537.75 40 +538 81 +538.25 126 +538.5 102 +538.75 80 +539 66 +539.25 65 +539.5 32 +539.75 83 +540 66 +540.25 64 +540.5 92 +540.75 84 +541 58 +541.25 72 +541.5 14 +541.75 78 +542 9 +542.25 71 +542.5 40 +542.75 118 +543 57 +543.25 100 +543.5 89 +543.75 64 +544 61 +544.25 86 +544.5 68 +544.75 76 +545 46 +545.25 85 +545.5 71 +545.75 61 +546 31 +546.25 68 +546.5 84 +546.75 68 +547 45 +547.25 94 +547.5 125 +547.75 33 +548 104 +548.25 70 +548.5 59 +548.75 33 +549 21 +549.25 1 +549.5 55 +549.75 41 +550 61 +550.25 75 +550.5 71 +550.75 81 +551 34 +551.25 63 +551.5 68 +551.75 56 +552 27 +552.25 61 +552.5 69 +552.75 45 +553 71 +553.25 90 +553.5 55 +553.75 105 +554 20 +554.25 47 +554.5 21 +554.75 10 +555 57 +555.25 77 +555.5 20 +555.75 56 +556 60 +556.25 105 +556.5 15 +556.75 8 +557 -8 +557.25 69 +557.5 18 +557.75 47 +558 6 +558.25 42 +558.5 27 +558.75 39 +559 12 +559.25 20 +559.5 41 +559.75 9 +560 52 +560.25 67 +560.5 39 +560.75 72 +561 40 +561.25 52 +561.5 73 +561.75 39 +562 -4 +562.25 72 +562.5 64 +562.75 52 +563 109 +563.25 12 +563.5 49 +563.75 -25 +564 9 +564.25 35 +564.5 59 +564.75 78 +565 15 +565.25 27 +565.5 52 +565.75 98 +566 10 +566.25 -3 +566.5 40 +566.75 -2 +567 16 +567.25 64 +567.5 82 +567.75 19 +568 28 +568.25 37 +568.5 6 +568.75 64 +569 35 +569.25 19 +569.5 18 +569.75 -33 +570 45 +570.25 -6 +570.5 19 +570.75 12 +571 9 +571.25 -6 +571.5 -1 +571.75 3 +572 47 +572.25 39 +572.5 16 +572.75 33 +573 10 +573.25 32 +573.5 18 +573.75 29 +574 68 +574.25 52 +574.5 9 +574.75 21 +575 34 +575.25 24 +575.5 7 +575.75 -63 +576 24 +576.25 32 +576.5 54 +576.75 19 +577 13 +577.25 11 +577.5 59 +577.75 31 +578 78 +578.25 -16 +578.5 39 +578.75 -12 +579 13 +579.25 21 +579.5 77 +579.75 25 +580 2 +580.25 -3 +580.5 -6 +580.75 0 +581 24 +581.25 -27 +581.5 29 +581.75 -11 +582 17 +582.25 -18 +582.5 -10 +582.75 51 +583 27 +583.25 -5 +583.5 17 +583.75 -66 +584 0 +584.25 19 +584.5 21 +584.75 20 +585 -21 +585.25 -8 +585.5 49 +585.75 -12 +586 4 +586.25 60 +586.5 29 +586.75 16 +587 54 +587.25 -10 +587.5 15 +587.75 -22 +588 -39 +588.25 15 +588.5 46 +588.75 29 +589 -31 +589.25 44 +589.5 -37 +589.75 12 +590 56 +590.25 51 +590.5 44 +590.75 22 +591 46 +591.25 -2 +591.5 59 +591.75 27 +592 77 +592.25 38 +592.5 74 +592.75 56 +593 -8 +593.25 0 +593.5 25 +593.75 54 +594 18 +594.25 110 +594.5 17 +594.75 -16 +595 10 +595.25 -71 +595.5 -25 +595.75 57 +596 5 +596.25 -29 +596.5 52 +596.75 37 +597 30 +597.25 24 +597.5 85 +597.75 28 +598 36 +598.25 2 +598.5 -48 +598.75 67 +599 -31 +599.25 8 +599.5 56 +599.75 71 +600 58 +600.25 40 +600.5 -8 +600.75 5 +601 17 +601.25 8 +601.5 73 +601.75 91 +602 42 +602.25 2 +602.5 64 +602.75 -16 +603 22 +603.25 55 +603.5 -33 +603.75 6 +604 -2 +604.25 -12 +604.5 71 +604.75 24 +605 53 +605.25 8 +605.5 28 +605.75 37 +606 29 +606.25 -16 +606.5 59 +606.75 11 +607 37 +607.25 8 +607.5 -6 +607.75 -12 +608 25 +608.25 69 +608.5 78 +608.75 64 +609 18 +609.25 7 +609.5 37 +609.75 81 +610 54 +610.25 2 +610.5 29 +610.75 2 +611 -1 +611.25 21 +611.5 48 +611.75 -5 +612 32 +612.25 2 +612.5 39 +612.75 12 +613 12 +613.25 -9 +613.5 50 +613.75 6 +614 0 +614.25 79 +614.5 19 +614.75 20 +615 85 +615.25 -10 +615.5 29 +615.75 36 +616 84 +616.25 31 +616.5 14 +616.75 4 +617 84 +617.25 -14 +617.5 46 +617.75 24 +618 -3 +618.25 -3 +618.5 0 +618.75 -23 +619 -20 +619.25 69 +619.5 30 +619.75 24 +620 -2 +620.25 89 +620.5 -31 +620.75 3 +621 62 +621.25 27 +621.5 29 +621.75 46 +622 54 +622.25 -19 +622.5 48 +622.75 4 +623 51 +623.25 -26 +623.5 69 +623.75 -97 +624 64 +624.25 -31 +624.5 78 +624.75 -17 +625 28 +625.25 -42 +625.5 77 +625.75 57 +626 15 +626.25 40 +626.5 -65 +626.75 -14 +627 77 +627.25 -7 +627.5 36 +627.75 2 +628 15 +628.25 -23 +628.5 13 +628.75 14 +629 47 +629.25 -70 +629.5 -54 +629.75 33 +630 28 +630.25 42 +630.5 -13 +630.75 -13 +631 29 +631.25 51 +631.5 59 +631.75 44 +632 -29 +632.25 51 +632.5 26 +632.75 71 +633 54 +633.25 60 +633.5 29 +633.75 90 +634 0 +634.25 11 +634.5 15 +634.75 13 +635 32 +635.25 75 +635.5 78 +635.75 -24 +636 13 +636.25 47 +636.5 -18 +636.75 -10 +637 0 +637.25 0 +637.5 29 +637.75 32 +638 -9 +638.25 -19 +638.5 5 +638.75 61 +639 36 +639.25 11 +639.5 27 +639.75 56 +640 4 +640.25 43 +640.5 15 +640.75 -59 +641 -19 +641.25 76 +641.5 32 +641.75 -21 +642 -35 +642.25 39 +642.5 20 +642.75 39 +643 -22 +643.25 -29 +643.5 -12 +643.75 17 +644 -3 +644.25 32 +644.5 54 +644.75 -25 +645 -26 +645.25 11 +645.5 91 +645.75 -3 +646 -24 +646.25 -56 +646.5 -49 +646.75 -47 +647 93 +647.25 36 +647.5 0 +647.75 86 +648 34 +648.25 34 +648.5 -23 +648.75 25 +649 -25 +649.25 -49 +649.5 -35 +649.75 78 +650 86 +650.25 -25 +650.5 -16 +650.75 77 +651 -16 +651.25 35 +651.5 3 +651.75 -19 +652 8 +652.25 19 +652.5 -30 +652.75 -23 +653 5 +653.25 23 +653.5 79 +653.75 6 +654 -46 +654.25 -15 +654.5 -17 +654.75 60 +655 78 +655.25 -25 +655.5 51 +655.75 -48 +656 53 +656.25 93 +656.5 54 +656.75 -45 +657 14 +657.25 98 +657.5 -2 +657.75 42 +658 -13 +658.25 52 +658.5 59 +658.75 74 +659 -3 +659.25 54 +659.5 60 +659.75 -27 +660 30 +660.25 25 +660.5 -6 +660.75 8 +661 24 +661.25 67 +661.5 5 +661.75 46 +662 24 +662.25 -33 +662.5 26 +662.75 106 +663 -18 +663.25 62 +663.5 37 +663.75 81 +664 37 +664.25 104 +664.5 17 +664.75 110 +665 -21 +665.25 74 +665.5 51 +665.75 35 +666 -78 +666.25 49 +666.5 43 +666.75 42 +667 55 +667.25 65 +667.5 48 +667.75 -49 +668 48 +668.25 86 +668.5 68 +668.75 2 +669 31 +669.25 -21 +669.5 16 +669.75 26 +670 87 +670.25 181 +670.5 87 +670.75 44 +671 107 +671.25 43 +671.5 88 +671.75 28 +672 13 +672.25 73 +672.5 7 +672.75 76 +673 44 +673.25 80 +673.5 14 +673.75 12 +674 32 +674.25 11 +674.5 5 +674.75 -23 +675 -18 +675.25 12 +675.5 -56 +675.75 -7 +676 47 +676.25 33 +676.5 0 +676.75 58 +677 158 +677.25 46 +677.5 64 +677.75 17 +678 30 +678.25 -80 +678.5 21 +678.75 -21 +679 -1 +679.25 5 +679.5 86 +679.75 36 +680 -20 +680.25 -34 +680.5 53 +680.75 6 +681 21 +681.25 41 +681.5 59 +681.75 39 +682 51 +682.25 8 +682.5 46 +682.75 43 +683 11 +683.25 11 +683.5 -19 +683.75 8 +684 15 +684.25 -16 +684.5 33 +684.75 32 +685 -36 +685.25 32 +685.5 11 +685.75 25 +686 49 +686.25 -53 +686.5 0 +686.75 -11 +687 -6 +687.25 -22 +687.5 8 +687.75 -18 +688 45 +688.25 -41 +688.5 17 +688.75 11 +689 32 +689.25 -40 +689.5 54 +689.75 34 +690 35 +690.25 50 +690.5 19 +690.75 -58 +691 -59 +691.25 70 +691.5 -55 +691.75 8 +692 21 +692.25 8 +692.5 40 +692.75 -27 +693 -41 +693.25 11 +693.5 21 +693.75 -24 +694 48 +694.25 13 +694.5 24 +694.75 -16 +695 -9 +695.25 0 +695.5 3 +695.75 16 +696 -3 +696.25 70 +696.5 31 +696.75 -48 +697 -6 +697.25 52 +697.5 12 +697.75 44 +698 50 +698.25 -7 +698.5 4 +698.75 -4 +699 8 +699.25 35 +699.5 -61 +699.75 -32 +700 103 diff --git a/Modules/Biophotonics/python/iMC/mc/data/colon_default.mci b/Modules/Biophotonics/python/iMC/mc/data/colon_default.mci new file mode 100644 index 0000000000..9017b775cb --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/data/colon_default.mci @@ -0,0 +1,14 @@ +1.0 # file version +1 # number of runs + +temp.mco A # output filename, ASCII/Binary +1000000 # No. of photons +0.002 2 # dz, dr +500 1 1 # No. of dz, dr & da. + +2 # No. of layers +# n mua mus g d # One line for each layer +1.0 # n for medium above. +1.380 1.78741 20.00000 0.000 0.050 +1.380 1.78741 20.00000 0.000 0.050 +1.0 # n for medium below. diff --git a/Modules/Biophotonics/python/iMC/mc/data/correct.mci b/Modules/Biophotonics/python/iMC/mc/data/correct.mci new file mode 100644 index 0000000000..3548fe2f84 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/data/correct.mci @@ -0,0 +1,15 @@ +1.0 # file version +1 # number of runs + +temp.mco A # output filename, ASCII/Binary +1000000 # No. of photons +0.002 2 # dz, dr +500 1 1 # No. of dz, dr & da. + +3 # No. of layers +# n mua mus g d # One line for each layer +1.0 # n for medium above. +1.000 0.02100 0.03200 4.300 540.000 +1.000 0.01000 0.01000 1.000 100.000 +100.100 1.01100 1.02100 103.100 10410.000 +1.0 # n for medium below. diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/data/haemoglobin.txt b/Modules/Biophotonics/python/iMC/mc/data/haemoglobin.txt similarity index 100% rename from Modules/Biophotonics/python/inverseMonteCarlo/data/haemoglobin.txt rename to Modules/Biophotonics/python/iMC/mc/data/haemoglobin.txt diff --git a/Modules/Biophotonics/python/iMC/mc/dfmanipulations.py b/Modules/Biophotonics/python/iMC/mc/dfmanipulations.py new file mode 100644 index 0000000000..6bb2c76258 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/dfmanipulations.py @@ -0,0 +1,45 @@ +''' +Created on Oct 19, 2015 + +@author: wirkert +''' + +from scipy.interpolate import interp1d +import pandas as pd + + +def fold_by_sliding_average(df, window_size): + """take a batch and apply a sliding average with given window size to + the reflectances. + window_size is elements to the left and to the right. + There will be some boundary effect on the edges.""" + # next line does the folding. + df.reflectances = pd.rolling_mean(df.reflectances.T, window_size, + center=True).T + # let's get rid of NaN columns which are created at the boundaries + df.dropna(axis="columns", inplace=True) + return df + + +def switch_reflectances(df, new_wavelengths, new_reflectances): + df.drop(df["reflectances"].columns, axis=1, level=1, inplace=True) + for i, nw in enumerate(new_wavelengths): + df["reflectances", nw] = new_reflectances[:, i] + return df + + +def interpolate_wavelengths(df, new_wavelengths): + """ interpolate image data to fit new_wavelengths. Current implementation + performs simple linear interpolation. Neither existing nor new _wavelengths + need to be sorted. """ + # build an interpolator using the inormation provided by the dataframes + # reflectance column + interpolator = interp1d(df.reflectances.columns.astype(float), + df.reflectances.as_matrix(), assume_sorted=False, + bounds_error=False) + # use this to create new reflectances + new_reflectances = interpolator(new_wavelengths) + # build a new dataframe out of this information and set the original df + # to the new information. This seems hacky, can't it be done easier? + switch_reflectances(df, new_wavelengths, new_reflectances) + return df diff --git a/Modules/Biophotonics/python/iMC/mc/factories.py b/Modules/Biophotonics/python/iMC/mc/factories.py new file mode 100644 index 0000000000..dd6c317f9c --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/factories.py @@ -0,0 +1,115 @@ +''' +Created on Oct 15, 2015 + +@author: wirkert +''' + +from mc.tissuemodels import AbstractTissue, GenericTissue, PhantomTissue +from mc.batches import AbstractBatch +from mc.batches import GenericBatch, LessGenericBatch, GenericMeanScatteringBatch +from mc.batches import ColonMuscleBatch, ColonMuscleMeanScatteringBatch +from mc.batches import VisualizationBatch, IntralipidPhantomBatch + + +class AbstractMcFactory(object): + ''' + Monte Carlo Factory. + Will create fitting models and batches, dependent on your task + ''' + + def create_tissue_model(self): + return AbstractTissue() + + def create_batch_to_simulate(self): + return AbstractBatch() + + def __init__(self): + ''' + Constructor + ''' + + +class GenericMcFactory(AbstractMcFactory): + + def create_tissue_model(self): + return GenericTissue() + + def create_batch_to_simulate(self): + return GenericBatch() + + def __init__(self): + ''' + Constructor + ''' + + +class LessGenericMcFactory(GenericMcFactory): + + def create_batch_to_simulate(self): + return LessGenericBatch() + + def __init__(self): + ''' + Constructor + ''' + + +class ColonMuscleMcFactory(GenericMcFactory): + + def create_batch_to_simulate(self): + return ColonMuscleBatch() + + def __init__(self): + ''' + Constructor + ''' + + +class GenericMeanScatteringFactory(GenericMcFactory): + + def create_batch_to_simulate(self): + return GenericMeanScatteringBatch() + + def __init__(self): + ''' + Constructor + ''' + + +class ColonMuscleMeanScatteringFactory(GenericMcFactory): + + def create_batch_to_simulate(self): + return ColonMuscleMeanScatteringBatch() + + def __init__(self): + ''' + Constructor + ''' + + +class VisualizationMcFactory(AbstractMcFactory): + + def create_tissue_model(self): + return GenericTissue() + + def create_batch_to_simulate(self): + return VisualizationBatch() + + def __init__(self): + ''' + Constructor + ''' + + +class PhantomFactory(AbstractMcFactory): + + def create_tissue_model(self): + return PhantomTissue() + + def create_batch_to_simulate(self): + return IntralipidPhantomBatch() + + def __init__(self): + ''' + Constructor + ''' diff --git a/Modules/Biophotonics/python/iMC/mc/plot.py b/Modules/Biophotonics/python/iMC/mc/plot.py new file mode 100644 index 0000000000..2706a15c2b --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/plot.py @@ -0,0 +1,43 @@ +''' +Created on Oct 16, 2015 + +@author: wirkert +''' + + +import numpy as np +import matplotlib.pyplot as plt + + +class PlotFunctor(object): + """helping functor necessary because we need to save color for plotting""" + + def __init__(self, axes, wavelengths, nr_plot_elements): + self.axes = axes + self.sortedIndices = sorted(range(len(wavelengths)), + key=lambda k: wavelengths[k]) + self.sortedWavelenghts = wavelengths[self.sortedIndices] + self.nr_plot_elements = nr_plot_elements + self.i = 0 + + + def __call__(self, r): + pass + # set color so it slowly moves from blue to red + plt_color = (1. / float(self.nr_plot_elements) * self.i, + 0., + 1. - (1. / float(self.nr_plot_elements) * self.i)) + self.axes.plot(self.sortedWavelenghts, r[self.sortedIndices], "-o", + color=plt_color) + self.i += 1 + return self.i + +def plot(batch, axes=None): + if axes is None: + axes = plt.gca() + + f = PlotFunctor(axes, batch._wavelengths, batch.reflectances.shape[0]) + + np.apply_along_axis(f, + axis=1, + arr=batch.reflectances) diff --git a/Modules/Biophotonics/python/iMC/mc/sim.py b/Modules/Biophotonics/python/iMC/mc/sim.py new file mode 100644 index 0000000000..f629a25d99 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/sim.py @@ -0,0 +1,211 @@ +''' +Created on Sep 8, 2015 + +This file contains methods which wrap the mcml simulation so it can be +conveniently called from python. One example for a mcml simulation would be +gpumcml: +https://code.google.com/p/gpumcml/ + +@author: wirkert +''' + +import os +import contextlib +import logging + +import subprocess32 + +""" helper method to change to the correct path and back again """ +@contextlib.contextmanager +def cd(newPath): + savedPath = os.getcwd() + os.chdir(newPath) + yield + os.chdir(savedPath) + + +class MciWrapper(object): + ''' + this class provides a wrapper to the mcml monte carlo file. + Its purpose is to create a .mci file which the mcml simulation can use to + create the simulation + ''' + + def set_mci_filename(self, mci_filename): + self.mci_filename = mci_filename + + def set_mco_filename(self, mco_filename): + """path of the mco file. + This can be either a path relative to the mcml executable + or an absolute path. + BUG: it seems that it can only be relative file name + """ + self.mco_filename = mco_filename + + def set_nr_photons(self, nr_photons): + self.nr_photons = nr_photons + + def add_layer(self, n=None, ua=None, us=None, g=None, d=None): + """adds a layer below the currently existing ones. + + Arguments: + n: Refraction index of medium + ua: absorption coefficient [1/m] + us: scattering coefficient [1/m] + g: anisotropy factor + d: thickness of layer [m] + """ + if n is None: + n = 1. + if ua is None: + ua = 0. + if us is None: + us = 0. + if g is None: + g = 1. + if d is None: + d = 500.*10 ** -6 + self.layers.append([n, ua, us, g, d]) + + def set_layer(self, layer_nr, n, ua, us, g, d): + """set a layer with a specific layer_nr (stariting with layer_nr 0). + Note that the layer must already exist, otherwise an error will occure + """ + self.layers[layer_nr] = [n, ua, us, g, d] + + def set_file_version(self, file_version): + self.file_version = file_version + + def set_nr_runs(self, nr_runs): + self.nr_runs = nr_runs + + def set_dz_dr(self, dz, dr): + self.dz = dz + self.dr = dr + + def set_nr_dz_dr_da(self, nr_dz, nr_dr, nr_da): + self.nr_dz = nr_dz + self.nr_dr = nr_dr + self.nr_da = nr_da + + def set_n_medium_above(self, n_above): + self.n_above = n_above + + def set_n_medium_below(self, n_below): + self.n_below = n_below + + def create_mci_file(self): + """this method creates the mci file at the location self.mci_filename""" + open(self.mci_filename, 'a').close() + f = open(self.mci_filename, 'w') + # write general information + f.write(str(self.file_version) + " # file version\n") + f.write(str(self.nr_runs) + " # number of runs\n\n") + # write the data for run + f.write(self.mco_filename + " A # output filename, ASCII/Binary\n") + f.write(str(self.nr_photons) + " # No. of photons\n") + f.write(repr(self.dz) + " " + repr(self.dr) + " # dz, dr\n") + f.write(repr(self.nr_dz) + " " + + repr(self.nr_dr) + " " + + repr(self.nr_da) + " # No. of dz, dr & da.\n\n") + # write layer information + f.write(str(len(self.layers)) + " # No. of layers\n") + f.write("# n mua mus g d # One line for each layer\n") + f.write(repr(self.n_above) + " # n for medium above.\n") + for layer in self.layers: + + # factors (/100.; *100.) to convert to mcml expected units: + f.write("%.3f" % layer[0] + " " + # n + "%.5f" % (layer[1] / 100.) + " " + # ua + "%.5f" % (layer[2] / 100.) + " " + # us + "%.3f" % layer[3] + " " + # g + "%.3f" % (layer[4] * 100.) + "\n") # d + f.write(repr(self.n_below) + " # n for medium below.\n") + f.close() + if not os.path.isfile(self.mci_filename): + raise IOError("input file for monte carlo simulation not " + + "succesfully created") + + def __init__(self): + # set standard parameters + self.file_version = 1.0 + self.nr_photons = 10**6 + self.nr_runs = 1 + self.dz = 0.002 + self.dr = 2 + self.nr_dz = 500 + self.nr_dr = 1 + self.nr_da = 1 + self.n_above = 1.0 + self.n_below = 1.0 + # initialize to 0 layers + self.layers = [] + + +class SimWrapper(object): + + def set_mci_filename(self, mci_filename): + """the full path to the input file. E.g. ./data/my.mci + """ + self.mci_filename = mci_filename + + def set_mcml_executable(self, mcml_executable): + """ the full path of the excutable. E.g. ./mcml/mcml.exe""" + self.mcml_executable = mcml_executable + + def run_simulation(self): + """this method runs a monte carlo simulation""" + mcml_path, mcml_file = os.path.split(self.mcml_executable) + abs_mci_filename = os.path.abspath(self.mci_filename) + # note: the -A option makes gpumcml much faster, but is not available + # in original mcml. Maybe a switch should be introduced here + args = ("./" + mcml_file, "-A", abs_mci_filename) + # switch to folder where mcml resides in and execute it. + with cd(mcml_path): + try: + popen = subprocess32.Popen(args, stdout=subprocess32.PIPE) + popen.wait(timeout=100) + except: + logging.error("couldn't run simulation") + # popen.kill() + + def __init__(self): + pass + + +def get_diffuse_reflectance(mco_filename): + """ + extract reflectance from mco file. + Attention: mco_filename specifies full path. + + Returns: the reflectance + """ + with open(mco_filename) as myFile: + for line in myFile: + if "Diffuse reflectance" in line: + return float(line.split(' ', 1)[0]) + + +def get_specular_reflectance(mco_filename): + """ + extract reflectance from mco file. + Attention: mco_filename specifies full path. + + Returns: the reflectance + """ + with open(mco_filename) as myFile: + for line in myFile: + if "Specular reflectance" in line: + return float(line.split(' ', 1)[0]) + + +def get_total_reflectance(mco_filename): + """ + extract reflectance from mco file. + Attention: mco_filename specifies full path. + + Returns: the reflectance + """ + return get_diffuse_reflectance(mco_filename) + \ + get_specular_reflectance(mco_filename) + diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/generate/__init__.py b/Modules/Biophotonics/python/iMC/mc/test/__init__.py similarity index 100% rename from Modules/Biophotonics/python/inverseMonteCarlo/generate/__init__.py rename to Modules/Biophotonics/python/iMC/mc/test/__init__.py diff --git a/Modules/Biophotonics/python/iMC/mc/test/test_dfmanipulations.py b/Modules/Biophotonics/python/iMC/mc/test/test_dfmanipulations.py new file mode 100644 index 0000000000..7e62886813 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/test/test_dfmanipulations.py @@ -0,0 +1,78 @@ +''' +Created on Oct 19, 2015 + +@author: wirkert +''' +import unittest + +import numpy as np +from pandas.util.testing import assert_frame_equal + +from mc.batches import ColonMuscleBatch +import mc.dfmanipulations as dfmani + +class Test(unittest.TestCase): + + def setUp(self): + # create a colon batch with 2 samples + self.test_batch = ColonMuscleBatch() + self.test_batch.create_parameters(2) + + # artificially add 10 fake "reflectances" to this batch + # at 10 fake "wavelengths" + WAVELENGHTS = np.linspace(450, 720, 10) + reflectance1 = np.arange(0, 30, 3) + reflectance2 = np.arange(30, 60, 3) + for w in WAVELENGHTS: + self.test_batch.df["reflectances", w] = np.NAN + for r1, r2, w in zip(reflectance1, reflectance2, WAVELENGHTS): + self.test_batch.df["reflectances", w][0] = r1 + self.test_batch.df["reflectances", w][1] = r2 + + # shortcut to dataframe that we are interested in: + self.df = self.test_batch.df + + def test_sliding_average(self): + # by test design folding should not alter elements (only at boundaries, + # which are excluded by array slicing: + expected_elements = self.df.reflectances.iloc[:, 1:-1].copy() + dfmani.fold_by_sliding_average(self.df, 3) + + assert_frame_equal(self.df.reflectances, expected_elements) + + def test_interpolation(self): + new_wavelengths = [465, 615, 555] + + dfmani.interpolate_wavelengths(self.df, new_wavelengths) + + expected = np.array([[1.5, 16.5, 10.5], [31.5, 46.5, 40.5]]) + np.testing.assert_almost_equal(self.df.reflectances.as_matrix(), + expected, + err_msg="test if interpolation " + + "works fine on batches") + + def test_select_n(self): + """ this is less a test and more a showing of how to select n elements + from a dataframe.""" + # draw one sample. Look into documentation for sample to see all the + # options. Sample is quite powerfull. + self.df = self.df.sample(1) + self.assertEqual(self.df.shape[0], 1, + "one sample selected") + + def test_sortout_bands(self): + """ this is less a test and more a showing of how to sortout specific + bands from a dataframe """ + # drop the 510 and 720 nm band + band_names_to_sortout = [510, 720] + self.df.drop(band_names_to_sortout, axis=1, level=1, inplace=True) + + df_r = self.df["reflectances"] + self.assertTrue(not (510 in df_r.columns)) + self.assertTrue(not 720 in df_r.columns) + self.assertTrue(690 in df_r.columns) + + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/Modules/Biophotonics/python/iMC/mc/test/test_sim.py b/Modules/Biophotonics/python/iMC/mc/test/test_sim.py new file mode 100644 index 0000000000..313341c081 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/test/test_sim.py @@ -0,0 +1,125 @@ +''' +Created on Sep 8, 2015 + +@author: wirkert +''' +import unittest +import filecmp +import os + +from mc.sim import MciWrapper, SimWrapper, \ + get_total_reflectance, get_diffuse_reflectance + + +path_to_gpumcml = "/home/wirkert/workspace/monteCarlo/gpumcml/" + \ + "fast-gpumcml/gpumcml.sm_20" +skip_gpu_tests = not os.path.exists(path_to_gpumcml) + + +class Test(unittest.TestCase): + + def setUp(self): + self.mci_filename = "temp.mci" + self.mco_filename = "temp.mco" + # create a mci_wrapper which shall create a mci file + self.mci_wrapper = MciWrapper() + self.mci_wrapper.set_mci_filename(self.mci_filename) + self.mci_wrapper.set_mco_filename(self.mco_filename) + self.mci_wrapper.set_nr_photons(10 ** 6) + self.mci_wrapper.add_layer(1.0, 2.1, 3.2, 4.3, 5.4) + self.mci_wrapper.add_layer(6.5, 7.8, 8.9, 9.10, 10.11) + self.mci_wrapper.add_layer(100.1001, 101.10001, 102.100001, + 103.1000001, 104.10000001) + self.mci_wrapper.set_layer(1, 1, 1, 1, 1, 1) + # expected mci file + self.correct_mci_filename = "./mc/data/correct.mci" + # path to the externaly installed mcml simulation. This is machine + # dependent. Thus tests depending on the execution of mcml will only + # be performed if this file exists. + # Should the file be located somewhere else on your computer, + # change this path to your actual location. + + def tearDown(self): + os.remove(self.mci_filename) + mcml_path, mcml_file = os.path.split(path_to_gpumcml) + created_mco_file = mcml_path + "/" + self.mco_filename + if os.path.isfile(created_mco_file): + os.remove(created_mco_file) + + def test_mci_wrapper(self): + self.mci_wrapper.create_mci_file() + self.assertTrue(os.path.isfile(self.mci_filename), + "mci file was created") + self.assertTrue(filecmp.cmp(self.mci_filename, + self.correct_mci_filename, shallow=False), + "the written mci file is the same as the stored " + + "reference file") + + @unittest.skipIf(skip_gpu_tests, "skip if gpumcml not installed") + def test_sim_wrapper(self): + mcml_path, mcml_file = os.path.split(path_to_gpumcml) + if os.path.isfile(path_to_gpumcml): + self.mci_wrapper.create_mci_file() + sim_wrapper = SimWrapper() + sim_wrapper.set_mci_filename(self.mci_filename) + sim_wrapper.set_mcml_executable(path_to_gpumcml) + sim_wrapper.run_simulation() + self.assertTrue(os.path.isfile(os.path.join(mcml_path, + self.mco_filename)), + "mco file was created") + + @unittest.skipIf(skip_gpu_tests, "skip if gpumcml not installed") + def test_mci_wrapper_book_example(self): + """see if our result matches the one from + Biomedical Optics + Principles and Imaging + page 55 (Table 3.1)""" + # create a book_p55_mci which shall create a mci file + book_p55_mci = MciWrapper() + book_p55_mci.set_mci_filename(self.mci_filename) + book_p55_mci.set_mco_filename(self.mco_filename) + book_p55_mci.set_nr_photons(10**6) + book_p55_mci.add_layer(1, 1000, 9000, 0.75, 0.0002) + + mcml_path, mcml_file = os.path.split(path_to_gpumcml) + if os.path.isfile(path_to_gpumcml): + book_p55_mci.create_mci_file() + sim_wrapper = SimWrapper() + sim_wrapper.set_mci_filename(self.mci_filename) + sim_wrapper.set_mcml_executable(path_to_gpumcml) + sim_wrapper.run_simulation() + self.assertTrue(os.path.isfile(mcml_path + "/" + self.mco_filename), + "mco file was created") + refl = get_diffuse_reflectance(os.path.join(mcml_path, + self.mco_filename)) + self.assertAlmostEqual(refl, 0.09734, 3, + "correct reflectance determined " + + "according to book table 3.1") + + @unittest.skipIf(skip_gpu_tests, "skip if gpumcml not installed") + def test_mci_wrapper_book_example_2(self): + """see if our result matches the one from + Biomedical Optics + Principles and Imaging + page 56 (Table 3.2)""" + # create a book_p56_mci which shall create a mci file + book_p56_mci = MciWrapper() + book_p56_mci.set_mci_filename(self.mci_filename) + book_p56_mci.set_mco_filename(self.mco_filename) + book_p56_mci.set_nr_photons(10**6) + book_p56_mci.add_layer(1.5, 1000, 9000, 0., 1) + + mcml_path, mcml_file = os.path.split(path_to_gpumcml) + if os.path.isfile(path_to_gpumcml): + book_p56_mci.create_mci_file() + sim_wrapper = SimWrapper() + sim_wrapper.set_mci_filename(self.mci_filename) + sim_wrapper.set_mcml_executable(path_to_gpumcml) + sim_wrapper.run_simulation() + self.assertTrue(os.path.isfile(mcml_path + "/" + self.mco_filename), + "mco file was created") + refl = get_total_reflectance(os.path.join(mcml_path, + self.mco_filename)) + self.assertAlmostEqual(refl, 0.26, delta=0.01, + msg="correct reflectance determined " + + "according to book table 3.2") diff --git a/Modules/Biophotonics/python/iMC/mc/test/test_tissuemodels.py b/Modules/Biophotonics/python/iMC/mc/test/test_tissuemodels.py new file mode 100644 index 0000000000..bd708c2fe7 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/test/test_tissuemodels.py @@ -0,0 +1,50 @@ +''' +Created on Sep 9, 2015 + +@author: wirkert +''' + +import unittest +import filecmp +import os + +from mc.tissuemodels import GenericTissue + +this_dir, this_filename = os.path.split(__file__) +DATA_PATH = os.path.join(this_dir, "..", "data") + + +class TestTissueModels(unittest.TestCase): + + def setUp(self): + self.mci_filename = "temp.mci" + self.mco_filename = "temp.mco" + # in this file we stored the expected result created from the + # "old" implementation of our algorithm: + self.correct_mci_filename = os.path.join(DATA_PATH, "colon_default.mci") + + def tearDown(self): + os.remove(self.mci_filename) + + def test_tissue_model(self): + # create nice colon model + tissue = GenericTissue(nr_layers=2) + tissue.set_mci_filename(self.mci_filename) + tissue.set_mco_filename(self.mco_filename) + tissue.wavelength = 500. * 10 ** -9 + # just use the default parameters for this test + # now create the simulation file + tissue.create_mci_file() + # and assert its correct + self.assertTrue(os.path.isfile(self.mci_filename), + "mci file was created") + self.assertTrue(filecmp.cmp(self.mci_filename, + self.correct_mci_filename, + shallow=False), + "the written mci file is the same as the stored " + + "reference file") + + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/Modules/Biophotonics/python/iMC/mc/test/test_ua.py b/Modules/Biophotonics/python/iMC/mc/test/test_ua.py new file mode 100644 index 0000000000..d29f2f5f4c --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/test/test_ua.py @@ -0,0 +1,40 @@ +''' +Created on Sep 8, 2015 + +@author: wirkert +''' + +import unittest + +from mc.usuag import Ua + +class test_ua(unittest.TestCase): + + def setUp(self): + self.ua_l2 = Ua() + self.ua532 = self.ua_l2(532.*10 ** -9) / 100. + self.ua800 = self.ua_l2(800.*10 ** -9) / 100. + + def test_uA532(self): + self.assertTrue(3. < self.ua532 < 4., "test if calculated ua_l2 takes " + + "reasonable values " + + "(according to \"Determination of optical" + + " properties of normal and adenomatous human colon " + + "tissues in vitro using integrating sphere " + + "techniques\")") + + def test_uA800(self): + self.assertTrue(0.05 < self.ua800 < 0.15, "test if calculated ua_l2 " + + "takes reasonable values " + + "(according to \"Differences in" + + " optical properties between healthy and " + + "pathological human colon tissues using a Ti:sapphire" + + " laser: an in vitro study using the Monte Carlo " + + "inversion technique\")") + + def test_saO2_makes_difference(self): + self.ua_l2.saO2 = 1.0 + self.assertNotAlmostEqual(self.ua532, + self.ua_l2(532.*10 ** -9) / 100., + msg="changing oxygenation changes result") + diff --git a/Modules/Biophotonics/python/iMC/mc/test/test_usg.py b/Modules/Biophotonics/python/iMC/mc/test/test_usg.py new file mode 100644 index 0000000000..2af36b493c --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/test/test_usg.py @@ -0,0 +1,26 @@ +''' +Created on Oct 23, 2015 + +@author: wirkert +''' +import unittest + +from mc.usuag import UsgJacques + + +class TestUs(unittest.TestCase): + + def setUp(self): + self.usg = UsgJacques() + + def test_no_rayleigh_high_wavelengths(self): + self.usg.a_ray = 2.*100 + self.usg.a_mie = 20.*100 + w = 500. * 10 ** -9 + print self.usg(w)[0] / 100. + # todo write test + + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/Modules/Biophotonics/python/iMC/mc/tissuemodels.py b/Modules/Biophotonics/python/iMC/mc/tissuemodels.py new file mode 100644 index 0000000000..f6a31821c1 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/tissuemodels.py @@ -0,0 +1,149 @@ +''' +Created on Sep 9, 2015 + +@author: wirkert +''' + +import numpy as np + +from mc.sim import MciWrapper +from mc.usuag import Ua, UsgJacques, UsgIntralipid + + +class AbstractTissue(object): + ''' + Initializes a abstract tissue model" + ''' + + def set_nr_photons(self, nr_photons): + self._mci_wrapper.set_nr_photons(nr_photons) + + def set_mci_filename(self, mci_filename): + self._mci_wrapper.set_mci_filename(mci_filename) + + def set_mco_filename(self, mco_filename): + self._mci_wrapper.set_mco_filename(mco_filename) + + def get_mco_filename(self): + return self._mci_wrapper.mco_filename + + def set_wavelength(self, wavelength): + self.wavelength = wavelength + + def create_mci_file(self): + # set layers + for i, ua in enumerate(self.uas): + self._mci_wrapper.set_layer(i, # layer nr + self.ns[i], # refraction index + self.uas[i](self.wavelength), # ua + self.usgs[i](self.wavelength)[0], # us + self.usgs[i](self.wavelength)[1], # g + self.ds[i]) # d + # now that the layers have been updated: create file + self._mci_wrapper.create_mci_file() + + def __str__(self): + """ Overwrite this method! + print the current model""" + model_string = "" + return model_string + + def __init__(self, ns, uas, usgs, ds): + self._mci_wrapper = MciWrapper() + + self.wavelength = 500.*10**9 # standard wavelength, should be set. + self.uas = uas + self.usgs = usgs + self.ds = ds + self.ns = ns + # initially create layers. these will be overwritten as soon + # as create_mci_file is called. + for i in enumerate(uas): + self._mci_wrapper.add_layer() + + +class GenericTissue(AbstractTissue): + ''' + Initializes a 3-layer generic tissue model + ''' + + def set_dataframe_row(self, df_row): + """take one example (one row) of a created batch and set the tissue to + resemble the structure specified by this row + + Args: + df_row: one row of a dataframe created by a batch.""" + layers = [l for l in df_row.index.levels[0] if "layer" in l] + for i, l in enumerate(layers): + self.set_layer(i, + df_row[l, "vhb"], + df_row[l, "sao2"], + df_row[l, "a_mie"], + df_row[l, "b_mie"], + df_row[l, "d"], + df_row[l, "n"], + df_row[l, "g"]) + + def set_layer(self, layer_nr=0, + bvf=None, saO2=None, a_mie=None, b_mie=None, d=None, + n=None, g=None): + """Helper function to set one layer.""" + if bvf is None: + bvf = 0.02 + if saO2 is None: + saO2 = 0.7 + if a_mie is None: + a_mie = 10. * 100 + if d is None: + d = 500. * 10 ** -6 + if b_mie is None: + b_mie = 1.286 + if n is None: + n = 1.38 + if g is None: + g = 0. + # build obejct for absorption coefficient determination + self.uas[layer_nr].bvf = bvf + self.uas[layer_nr].saO2 = saO2 + # and one for scattering coefficient + self.usgs[layer_nr].a_mie = a_mie + self.usgs[layer_nr].a_ray = 0. + self.usgs[layer_nr].b_mie = b_mie + self.usgs[layer_nr].g = g + self.ds[layer_nr] = d + self.ns[layer_nr] = n + + def __str__(self): + """print the current model""" + model_string = "" + for i, ua in enumerate(self.uas): + layer_string = "layer " + str(i) + \ + " - vhb: " + "%.1f" % (self.uas[i].bvf * 100.) + \ + "%; sao2: " + "%.1f" % (self.uas[i].saO2 * 100.) + \ + "%; a_mie: " + "%.2f" % (self.usgs[i].a_mie / 100.) + \ + "cm^-1; a_ray: " + "%.2f" % (self.usgs[i].a_ray / 100.) + \ + "cm^-1; b_mie: " + "%.3f" % self.usgs[i].b_mie + \ + "; d: " + "%.0f" % (self.ds[i] * 10 ** 6) + "um" + \ + "; n: " + "%.2f" % (self.ns[i]) + \ + "; g: " + "%.2f" % self.usgs[i].g + "\n" + model_string += layer_string + return model_string + + def __init__(self, nr_layers=3): + uas = [] + usgs = [] + for i in range(nr_layers): + uas.append(Ua()) + usgs.append(UsgJacques()) + ds = np.ones(nr_layers, dtype=float) * 500.*10 ** -6 + ns = np.ones(nr_layers, dtype=float) * 1.38 + super(GenericTissue, self).__init__(ns, uas, usgs, ds) + + +class PhantomTissue(GenericTissue): + + def __init__(self, nr_layers=1): + super(PhantomTissue, self).__init__(nr_layers=1) + self.usgs = [UsgIntralipid()] + + diff --git a/Modules/Biophotonics/python/iMC/mc/usuag.py b/Modules/Biophotonics/python/iMC/mc/usuag.py new file mode 100644 index 0000000000..d60d8173f6 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/mc/usuag.py @@ -0,0 +1,217 @@ +''' +Created on Sep 8, 2015 + +@author: wirkert +''' + +import math +import os + +import numpy as np +from scipy.interpolate import interp1d + + +this_dir, this_filename = os.path.split(__file__) +DATA_PATH = os.path.join(this_dir, "data") + +def get_haemoglobin_extinction_coefficients(reference_filename=None): + """ + helper method to get reference data for eHbO2 and eHb from Scott Prahls + reference file: + http://omlc.org/spectra/hemoglobin/summary.html + """ + if reference_filename is None: + reference_filename = os.path.join(DATA_PATH, "haemoglobin.txt") + # table with wavelength at 1st row, + # HbO2 molar extinction coefficient [cm**-1/(moles/l)] at 2nd row, + # Hb molar extinction coefficient [cm**-1/(moles/l)] at 3rd row + haemoLUT = np.loadtxt(reference_filename, skiprows=2) + # we calculate everything in [m] instead of [nm] and [1/cm] + haemoLUT[:, 0] = haemoLUT[:, 0] * 10 ** -9 + haemoLUT[:, 1:] = haemoLUT[:, 1:] * 10 ** 2 + # get the data into an interpolation map for oxy and deoxy haemoglobin + eHbO2 = interp1d(haemoLUT[:, 0], haemoLUT[:, 1]) + eHb = interp1d(haemoLUT[:, 0], haemoLUT[:, 2]) + return eHbO2, eHb + + +def get_beta_carotin_extinction_coefficients(reference_filename=None): + """ + Reference data taken from + http://omlc.org/spectra/PhotochemCAD/data/041-abs.txt + """ + if reference_filename is None: + reference_filename = os.path.join(DATA_PATH, "beta_carotin.txt") + # table with wavelength at 1st row, + # beta carotin molar extinction coefficient [cm**-1/(M)] + betaLUT = np.loadtxt(reference_filename, skiprows=2) + # we calculate everything in [m] instead of [nm] and [1/cm] + betaLUT[:, 0] = betaLUT[:, 0] * 10 ** -9 + betaLUT[:, 1:] = betaLUT[:, 1:] * 10 ** 2 + # get the data into an interpolation map + eBc = interp1d(betaLUT[:, 0], betaLUT[:, 1], bounds_error=False, + fill_value=0.) + return eBc + + +def get_bilirubin_extinction_coefficients(reference_filename=None): + """ + Reference data taken from + http://omlc.org/spectra/PhotochemCAD/data/041-abs.txt + """ + if reference_filename is None: + reference_filename = os.path.join(DATA_PATH, "bilirubin.txt") + # table with wavelength at 1st row, + # beta carotin molar extinction coefficient [cm**-1/(M)] + biliLUT = np.loadtxt(reference_filename, skiprows=2) + # we calculate everything in [m] instead of [nm] and [1/cm] + biliLUT[:, 0] = biliLUT[:, 0] * 10 ** -9 + biliLUT[:, 1:] = biliLUT[:, 1:] * 10 ** 2 + # get the data into an interpolation map + eBili = interp1d(biliLUT[:, 0], biliLUT[:, 1], bounds_error=False, + fill_value=0.) + return eBili + + +class Ua(object): + + def __init__(self): + self.bvf = 0.02 # % + self.cHb = 120. # g*Hb/l + self.saO2 = 0. # % + self.eHbO2, self.eHb = \ + get_haemoglobin_extinction_coefficients() + + self.cBetaCarotinUgProDl = 0. # 2000. + # g / l + self.cBili = 0. # 1.23 * 10 ** -2 + self.eBc = get_beta_carotin_extinction_coefficients() + self.eBili = get_bilirubin_extinction_coefficients() + + + def __call__(self, wavelength): + """ determine ua [1/m] as combination of + Haemoglobin extinction coefficients. + For more on this equation, please refer to + http://omlc.org/spectra/hemoglobin/ + """ + ua_haemoglobin = math.log(10) * self.cHb * \ + (self.saO2 * self.eHbO2(wavelength) + + (1 - self.saO2) * self.eHb(wavelength)) \ + / 64500. * self.bvf + ua_bilirubin = math.log(10) * self.cBili / 574.65 * \ + self.eBili(wavelength) + # second line is to convert from ug/dl to g/ mole + ua_beta_carotin = math.log(10) * self.cBetaCarotinUgProDl / \ + 536.8726 * 10 ** -5 * \ + self.eBc(wavelength) + + return ua_haemoglobin + ua_bilirubin + ua_beta_carotin + + +class UaMuscle(): + """helper class for setting ua in muscle layer. + for ua_sm in the muscle layer we don't use mie theory but the + approximation presented in + Rowe et al. + "Modelling and validation of spectral reflectance for the colon" + calculated to retrieve an absorption of 11.2 cm-1 at 515nm + """ + def __init__(self): + self.ua = Ua() + + def __call__(self, wavelength): + A = 1.7923385088285804 + self.ua.bvf = 0.1 * A + self.ua.saO2 = 0.7 + self.ua.cHb = 120. + return self.ua(wavelength) + + +class UsgJacques(object): + + def __init__(self): + """ + To be set externally: + + a': + """ + self.a_ray = 0. * 100. + self.a_mie = 20. * 100. + self.b_mie = 1.286 + self.g = 0. + + def __call__(self, wavelength): + """ + Calculate the scattering parameters relevant for monte carlo simulation. + + Uses equation (2) from: Optical properties of biological tissues: + a Review + + Args + ____ + wavelength: + wavelength of the incident light [m] + + Returns: + ____ + (us, g) + scattering coefficient us [1/m] and anisotropy factor g + """ + norm_wavelength = (wavelength / (500 * 10 ** -9)) + + us_ray = self.a_ray * norm_wavelength ** (-4) + us_mie = self.a_mie * norm_wavelength ** (-self.b_mie) + + us_prime = (us_ray + us_mie) # * 100. to convert to m^-1 + # actually we calculated the reduced scattering coefficent, so + # assume g is 0 + us = us_prime / (1 - self.g) + + return us, self.g + + +class UsGMuscle(object): + """helper object for setting us in muscle layer. + for us in the muscle layer we don't use mie theory but the + approximation presented in + Rowe et al. + "Modelling and validation of spectral reflectance for the colon" + """ + + def __init__(self): + pass + + + def __call__(self, wavelength): + us = 168.52 * (wavelength * 10 ** 9) ** -0.332 / (1. - 0.96) * 100. + g = 0.96 + return us, g + + +class UsgIntralipid(object): + """helper object for setting us and g in intralipid + We use the formulas from + http://omlc.org/spectra/intralipid/ to calculate + """ + + def __init__(self): + self.a_ray = 0. * 100. + self.a_mie = 20. * 100. + self.b_mie = 2.33 + self.g = 0.85 + + def __call__(self, wavelength): + + norm_wavelength = (wavelength / (500 * 10 ** -9)) + + us_ray = self.a_ray * norm_wavelength ** (-4) + us_mie = self.a_mie * norm_wavelength ** (-self.b_mie) + + us_prime = (us_ray + us_mie) # * 100. to convert to m^-1 + + g = 2.25 * (wavelength * 10**9)**-0.155 + + us = us_prime / (1 - g) + + return us, g diff --git a/Modules/Biophotonics/python/iMC/msi/__init__.py b/Modules/Biophotonics/python/iMC/msi/__init__.py new file mode 100644 index 0000000000..0e9a801a5f --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 14 15:54:35 2015 + +@author: wirkert +""" + diff --git a/Modules/Biophotonics/python/iMC/msi/data/Transmission_15-49-35-978_filter700nm.txt b/Modules/Biophotonics/python/iMC/msi/data/Transmission_15-49-35-978_filter700nm.txt new file mode 100755 index 0000000000..5f619b446a --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/data/Transmission_15-49-35-978_filter700nm.txt @@ -0,0 +1,2064 @@ +Data from Transmission_15-49-35-978.txt Node + +Date: Mon Jun 01 15:49:35 CEST 2015 +User: thomaskirchnerbackup +Spectrometer: HR+C3363 +Autoset integration time: false +Trigger mode: 0 +Integration Time (sec): 1.600000E-2 +Scans to average: 1 +Electric dark correction enabled: true +Nonlinearity correction enabled: false +Boxcar width: 0 +XAxis mode: Wavelengths +Stop averaging: false +Number of Pixels in Spectrum: 2048 +>>>>>Begin Spectral Data<<<<< +187.255 70.50 +187.731 70.51 +188.206 70.51 +188.682 -0 +189.158 303.75 +189.634 135.85 +190.109 109.35 +190.585 9.89 +191.061 5400 +191.536 -476.47 +192.012 17100 +192.488 -54 +192.963 46.61 +193.439 -211.22 +193.914 114.18 +194.389 225 +194.865 137.65 +195.34 270 +195.816 -8100 +196.291 407.81 +196.766 -276.92 +197.242 51.43 +197.717 591.43 +198.192 -140.63 +198.667 135 +199.142 161.8 +199.618 -990 +200.093 139.75 +200.568 2.79 +201.043 -794.12 +201.518 101.61 +201.993 -50.4 +202.468 -3.35 +202.943 90 +203.418 -252.34 +203.893 -42.35 +204.368 112.5 +204.842 100.41 +205.317 331.58 +205.792 -203.77 +206.267 -900 +206.741 43.55 +207.216 -488.57 +207.691 69.74 +208.165 33.51 +208.64 276.92 +209.115 135 +209.589 33.51 +210.064 100.49 +210.538 -10800 +211.013 -0 +211.487 -144 +211.962 0 +212.436 185.05 +212.91 172.6 +213.385 14.33 +213.859 103.85 +214.333 96.43 +214.808 -582.35 +215.282 12600 +215.756 39.01 +216.23 -24.11 +216.704 -380.77 +217.178 40.15 +217.653 264.71 +218.127 11700 +218.601 -93.6 +219.075 81.82 +219.549 -14400 +220.023 308.22 +220.496 22.84 +220.97 -29.14 +221.444 -121.35 +221.918 -38.63 +222.392 28.72 +222.866 1800 +223.339 102.27 +223.813 106.21 +224.287 63.22 +224.76 -203.77 +225.234 -1042.11 +225.708 -38.79 +226.181 5.92 +226.655 94.41 +227.128 -138.46 +227.602 -43.9 +228.075 -26.21 +228.549 623.08 +229.022 56.25 +229.496 -25.23 +229.969 46.61 +230.442 119.58 +230.916 270 +231.389 26.87 +231.862 -787.5 +232.335 -0 +232.809 431.51 +233.282 -203.23 +233.755 14.06 +234.228 134.16 +234.701 -101.12 +235.174 76.34 +235.647 70.39 +236.12 43.71 +236.593 -32.37 +237.066 190.68 +237.539 210.28 +238.012 210.94 +238.485 14.34 +238.958 57.94 +239.43 308.57 +239.903 -55.1 +240.376 168.75 +240.849 321.43 +241.321 360 +241.794 -7.09 +242.267 -110.2 +242.739 30.58 +243.212 -31.03 +243.684 5.29 +244.157 -39.13 +244.629 9.57 +245.102 -1136.84 +245.574 -152.83 +246.047 -18.48 +246.519 0 +246.992 27.95 +247.464 -38.03 +247.936 8.74 +248.408 -371.74 +248.881 90 +249.353 2.51 +249.825 72.69 +250.297 157.98 +250.769 340.54 +251.242 -18.37 +251.714 -25.1 +252.186 163.64 +252.658 -668.57 +253.13 43.09 +253.602 -105.88 +254.074 29.89 +254.546 78.88 +255.017 22.31 +255.489 43.69 +255.961 27.36 +256.433 23.68 +256.905 -87.38 +257.377 29.03 +257.848 101.61 +258.32 -232.26 +258.792 -23.14 +259.263 225 +259.735 67.16 +260.206 -562.5 +260.678 450 +261.15 -16200 +261.621 104.13 +262.093 -100.93 +262.564 33.75 +263.035 119.74 +263.507 128.57 +263.978 43.71 +264.449 810 +264.921 72.97 +265.392 63 +265.863 474.55 +266.335 65.77 +266.806 26.03 +267.277 56.25 +267.748 -3600 +268.219 43.49 +268.69 64.29 +269.161 -411.43 +269.632 -41.12 +270.103 50.28 +270.574 43.9 +271.045 -373.58 +271.516 -15.08 +271.987 64.75 +272.458 60.27 +272.929 59.21 +273.4 -63.38 +273.87 741.18 +274.341 6.92 +274.812 -112.5 +275.282 12.16 +275.753 38.3 +276.224 124.14 +276.694 -578.57 +277.165 -33.96 +277.636 234.78 +278.106 3.86 +278.577 -48.65 +279.047 29.61 +279.517 -22.84 +279.988 -87.31 +280.458 11.16 +280.929 -10800 +281.399 -265.91 +281.869 -55.9 +282.34 246.58 +282.81 -37.76 +283.28 -388.64 +283.75 60.45 +284.22 -14.36 +284.691 -257.14 +285.161 102.27 +285.631 20.07 +286.101 168.75 +286.571 116.52 +287.041 67.92 +287.511 19.42 +287.981 -173.08 +288.451 84.37 +288.921 69.23 +289.39 -3.72 +289.86 0 +290.33 7.17 +290.8 21.6 +291.27 31500 +291.739 -102.27 +292.209 52.94 +292.679 20.22 +293.148 112.5 +293.618 49.54 +294.088 87.91 +294.557 55.45 +295.027 114.89 +295.496 900 +295.966 12.05 +296.435 -59.21 +296.904 331.58 +297.374 178.02 +297.843 -220.41 +298.313 -246.77 +298.782 450 +299.251 128.57 +299.72 11.25 +300.19 23.14 +300.659 44.55 +301.128 69.23 +301.597 -58.24 +302.066 -25.35 +302.535 19.78 +303.004 -60.67 +303.473 86.3 +303.942 293.48 +304.411 1125 +304.88 12.23 +305.349 -21.18 +305.818 121.62 +306.287 215.22 +306.756 -11.84 +307.225 60.34 +307.693 29.51 +308.162 37.89 +308.631 -37.76 +309.1 17.82 +309.568 68.82 +310.037 59.58 +310.506 81.38 +310.974 21.18 +311.443 105.08 +311.911 -8.85 +312.38 -115.2 +312.848 37.67 +313.317 63.38 +313.785 109.35 +314.254 21.11 +314.722 -43.2 +315.19 245.45 +315.659 -81.82 +316.127 258.9 +316.595 -16.77 +317.063 476.47 +317.532 -33.75 +318 63.53 +318.468 11.16 +318.936 101.61 +319.404 48.91 +319.872 0 +320.34 44.72 +320.808 -26.87 +321.276 71.05 +321.744 -92.52 +322.212 53.31 +322.68 16200 +323.148 -715.91 +323.616 214.68 +324.083 107.52 +324.551 3.14 +325.019 15.95 +325.487 -209.48 +325.954 11.18 +326.422 -18.6 +326.89 -101.61 +327.357 288 +327.825 120.47 +328.292 85.04 +328.76 -65.67 +329.228 -45.69 +329.695 -35.53 +330.162 -303.75 +330.63 0 +331.097 0 +331.565 41.54 +332.032 0 +332.499 -86.17 +332.967 52.43 +333.434 41.54 +333.901 246.77 +334.368 12.05 +334.835 83.01 +335.303 101.41 +335.77 -25.9 +336.237 296.34 +336.704 19.1 +337.171 -47.37 +337.638 -80.9 +338.105 27.41 +338.572 62.23 +339.039 -50.7 +339.506 64.73 +339.973 0 +340.439 -172.8 +340.906 -100.49 +341.373 -26.39 +341.84 34.24 +342.307 1440 +342.773 41.31 +343.24 6.54 +343.707 48.18 +344.173 -21.69 +344.64 -52.65 +345.106 12.23 +345.573 68.3 +346.039 -12.16 +346.506 33.4 +346.972 -20.66 +347.439 60.7 +347.905 13.35 +348.372 -1.5 +348.838 22.62 +349.304 58.7 +349.77 -16.32 +350.237 -14.32 +350.703 -42.57 +351.169 16.71 +351.635 10.62 +352.102 5.62 +352.568 -12.53 +353.034 7.15 +353.5 -6.18 +353.966 -37.23 +354.432 1.27 +354.898 17.31 +355.364 -65.77 +355.83 -25.55 +356.296 -26.71 +356.762 -7.26 +357.227 2.74 +357.693 9.59 +358.159 3.23 +358.625 -9.59 +359.09 -10.49 +359.556 7.01 +360.022 9.83 +360.488 9.76 +360.953 -9.1 +361.419 -7.91 +361.884 4.83 +362.35 4.13 +362.815 -0.86 +363.281 10.41 +363.746 -1.98 +364.212 -5.27 +364.677 0 +365.143 12.01 +365.608 0 +366.073 -9.66 +366.539 -1.3 +367.004 6.13 +367.469 13.65 +367.934 -8.58 +368.399 4.52 +368.865 0 +369.33 -4.49 +369.795 -10.07 +370.26 3.41 +370.725 -1.21 +371.19 6.18 +371.655 13.9 +372.12 8.63 +372.585 -7.93 +373.05 -1.84 +373.515 -3.98 +373.979 5.92 +374.444 2.8 +374.909 -7.58 +375.374 -0.98 +375.839 10.89 +376.303 4.17 +376.768 8.22 +377.233 0.44 +377.697 15.92 +378.162 -15.93 +378.626 -3.97 +379.091 -1.29 +379.556 -0.49 +380.02 -6.48 +380.485 -7.32 +380.949 0 +381.413 -2.81 +381.878 2.27 +382.342 -0.38 +382.806 4 +383.271 0.75 +383.735 5.76 +384.199 8 +384.663 5.29 +385.128 2.28 +385.592 2.08 +386.056 -5.6 +386.52 5.33 +386.984 6.92 +387.448 -4.68 +387.912 -8.79 +388.376 -0.71 +388.84 0.93 +389.304 2.74 +389.768 -3.72 +390.232 0.93 +390.696 2.48 +391.16 0 +391.623 -0.89 +392.087 4.05 +392.551 0.51 +393.015 2.94 +393.478 5.03 +393.942 7.63 +394.406 0.73 +394.869 0.47 +395.333 0.52 +395.796 2.78 +396.26 0.51 +396.723 -4.75 +397.187 0 +397.65 0.25 +398.114 2.78 +398.577 -1.45 +399.041 -6.15 +399.504 0 +399.967 1.95 +400.43 2.06 +400.894 -2.95 +401.357 -1.1 +401.82 -1.52 +402.283 2.15 +402.746 -0.86 +403.21 0.95 +403.673 0.42 +404.136 -3.42 +404.599 -1.46 +405.062 -0.79 +405.525 0.59 +405.988 3.17 +406.451 1.14 +406.913 -0.74 +407.376 -2.42 +407.839 3.43 +408.302 2.36 +408.765 1.76 +409.228 0.53 +409.69 -2.5 +410.153 -0.49 +410.616 1.49 +411.078 4.88 +411.541 1.27 +412.004 1.97 +412.466 -1.29 +412.929 -2.17 +413.391 -0.76 +413.854 -1.06 +414.316 0 +414.778 0.3 +415.241 -1.03 +415.703 -0.43 +416.166 0.29 +416.628 -3.02 +417.09 0.71 +417.552 -1.7 +418.015 -0.28 +418.477 -1.05 +418.939 1.9 +419.401 -0.13 +419.863 -1.23 +420.325 1.69 +420.787 0.39 +421.25 0.26 +421.712 -2.89 +422.174 -0.94 +422.635 1.26 +423.097 2.28 +423.559 -0.13 +424.021 2.43 +424.483 -1.65 +424.945 -2.66 +425.407 1.11 +425.868 1.76 +426.33 0.91 +426.792 1.99 +427.254 -1.2 +427.715 -0.59 +428.177 0.68 +428.638 2.35 +429.1 -1.49 +429.562 -0.67 +430.023 -1.57 +430.485 -0.85 +430.946 -0.87 +431.407 2.02 +431.869 -0.51 +432.33 0.61 +432.792 1.65 +433.253 -0.81 +433.714 -3.13 +434.175 0 +434.637 -1.85 +435.098 1.75 +435.559 1.45 +436.02 0 +436.481 -1.51 +436.942 -0.09 +437.404 0.26 +437.865 0.94 +438.326 -0.89 +438.787 -0.44 +439.248 0.51 +439.709 1.08 +440.169 0.57 +440.63 0.17 +441.091 -1.89 +441.552 0.97 +442.013 -1.9 +442.474 -2.27 +442.934 1.11 +443.395 0.81 +443.856 -0.38 +444.316 0.23 +444.777 0.55 +445.238 0.52 +445.698 0.29 +446.159 -0.53 +446.619 0.9 +447.08 2.23 +447.54 -1.09 +448.001 0.89 +448.461 0.51 +448.921 0.66 +449.382 -0.15 +449.842 0 +450.302 -2.32 +450.763 -1.75 +451.223 0.14 +451.683 0.15 +452.143 -0.58 +452.604 0.76 +453.064 -0.49 +453.524 -2.23 +453.984 0 +454.444 -0.07 +454.904 0.47 +455.364 -1.87 +455.824 0.59 +456.284 0.74 +456.744 -0.19 +457.204 -2.07 +457.664 0 +458.123 0.12 +458.583 -0.62 +459.043 -0.74 +459.503 0.18 +459.963 -0.42 +460.422 0.23 +460.882 1.68 +461.342 -0.62 +461.801 -0.45 +462.261 -1.45 +462.72 -0.44 +463.18 0.75 +463.639 -0.21 +464.099 -1.98 +464.558 -0.4 +465.018 -0.1 +465.477 -1.16 +465.936 -0.53 +466.396 0.38 +466.855 -0.38 +467.314 -0.09 +467.774 0 +468.233 -1.23 +468.692 -0.66 +469.151 -1.29 +469.61 -0.79 +470.07 -0.16 +470.529 -0.16 +470.988 0.47 +471.447 0.63 +471.906 0.31 +472.365 -0.44 +472.824 -0.44 +473.283 0 +473.742 0.14 +474.2 -0.03 +474.659 0.03 +475.118 0.07 +475.577 -0.33 +476.036 0.35 +476.494 0.16 +476.953 -0.18 +477.412 0.59 +477.87 -0.55 +478.329 -0.61 +478.788 0.64 +479.246 0.22 +479.705 -0.19 +480.163 -0.21 +480.622 -0.52 +481.08 -0.95 +481.539 -0.32 +481.997 0.19 +482.455 -0.15 +482.914 -0.24 +483.372 -0.36 +483.83 0.05 +484.289 -0.07 +484.747 -0.42 +485.205 -0.18 +485.663 0.02 +486.122 -0.17 +486.58 -0.1 +487.038 -0.25 +487.496 -0.14 +487.954 0.02 +488.412 0.02 +488.87 0.33 +489.328 0.19 +489.786 -0.1 +490.244 0.08 +490.702 -0.21 +491.159 -0.11 +491.617 0.11 +492.075 0.13 +492.533 0.34 +492.991 0.18 +493.448 0.23 +493.906 0.23 +494.364 0.31 +494.821 -0.12 +495.279 0.32 +495.737 0.13 +496.194 0.02 +496.652 0.21 +497.109 0.08 +497.567 -0.23 +498.024 -0.14 +498.481 -0.08 +498.939 0.11 +499.396 -0.06 +499.854 -0.14 +500.311 0.26 +500.768 0.27 +501.225 0.42 +501.683 0.29 +502.14 -0.3 +502.597 -0.22 +503.054 -0.04 +503.511 0.3 +503.968 -0.63 +504.425 -0.24 +504.882 0.19 +505.339 -0.26 +505.796 0.13 +506.253 0 +506.71 0.18 +507.167 0.08 +507.624 0.14 +508.081 0.05 +508.538 0.05 +508.995 -0.15 +509.451 0.04 +509.908 0.2 +510.365 -0.07 +510.821 -0.07 +511.278 -0.16 +511.735 0.03 +512.191 -0.29 +512.648 -0.17 +513.104 0.23 +513.561 0 +514.017 -0.24 +514.474 -0.01 +514.93 0.31 +515.387 0.02 +515.843 0.16 +516.299 -0.07 +516.756 0.12 +517.212 -0.02 +517.668 0.13 +518.125 0.08 +518.581 0.18 +519.037 0.09 +519.493 -0.1 +519.949 0.16 +520.405 0.13 +520.861 0.13 +521.317 0.16 +521.773 -0.02 +522.229 -0.11 +522.685 0.09 +523.141 0.48 +523.597 0.28 +524.053 0.14 +524.509 0.29 +524.965 -0.08 +525.421 -0.02 +525.876 -0.17 +526.332 0.01 +526.788 0.03 +527.244 -0.04 +527.699 -0.1 +528.155 0.05 +528.61 0.03 +529.066 0 +529.522 -0.07 +529.977 -0.14 +530.433 -0.05 +530.888 0.03 +531.344 0.08 +531.799 0.29 +532.254 -0.03 +532.71 -0.08 +533.165 -0.16 +533.62 -0.26 +534.076 -0.3 +534.531 0.03 +534.986 0.1 +535.441 -0.14 +535.897 -0.03 +536.352 0.1 +536.807 0.02 +537.262 0.27 +537.717 -0.1 +538.172 0 +538.627 0.07 +539.082 0.38 +539.537 -0.27 +539.992 -0.1 +540.447 0.04 +540.902 -0.08 +541.356 -0.16 +541.811 0.04 +542.266 0 +542.721 0.05 +543.176 -0.01 +543.63 0.09 +544.085 0.17 +544.54 -0.04 +544.994 -0.05 +545.449 0.08 +545.903 0.01 +546.358 -0.03 +546.812 -0.12 +547.267 -0.12 +547.721 -0.13 +548.176 -0.04 +548.63 0.08 +549.085 -0.07 +549.539 -0.11 +549.993 0.11 +550.448 -0.08 +550.902 -0.01 +551.356 -0.01 +551.81 -0.08 +552.264 -0.14 +552.719 0.13 +553.173 0.1 +553.627 -0.07 +554.081 0.03 +554.535 0.11 +554.989 -0.04 +555.443 0.04 +555.897 0.08 +556.351 0.12 +556.805 -0.06 +557.259 -0.23 +557.712 -0.03 +558.166 0.05 +558.62 0.15 +559.074 -0.31 +559.528 -0.14 +559.981 0.09 +560.435 0.09 +560.889 0.09 +561.342 -0.06 +561.796 0.25 +562.25 -0.1 +562.703 0.15 +563.157 -0.09 +563.61 0.01 +564.064 -0.16 +564.517 -0.14 +564.97 -0.19 +565.424 -0.04 +565.877 -0.2 +566.331 -0.01 +566.784 0.13 +567.237 0.04 +567.69 0.14 +568.144 0.17 +568.597 -0.04 +569.05 -0.07 +569.503 -0.04 +569.956 -0.11 +570.409 0 +570.862 0.06 +571.315 -0.04 +571.768 0.21 +572.221 0.07 +572.674 -0.07 +573.127 0.2 +573.58 -0.02 +574.033 0.08 +574.486 0 +574.939 0.07 +575.392 -0.08 +575.844 0.04 +576.297 0.05 +576.75 -0.15 +577.202 -0.05 +577.655 0.08 +578.108 -0.12 +578.56 -0.1 +579.013 -0.11 +579.465 0.23 +579.918 0.02 +580.37 0.21 +580.823 0.34 +581.275 0.18 +581.728 0.02 +582.18 -0.06 +582.632 0.14 +583.085 -0.13 +583.537 0.06 +583.989 0.02 +584.442 -0.05 +584.894 -0.02 +585.346 -0.03 +585.798 0.08 +586.25 0.05 +586.702 -0.05 +587.155 0.05 +587.607 0.02 +588.059 0.17 +588.511 0.01 +588.963 0.08 +589.415 -0.03 +589.866 0.05 +590.318 0.03 +590.77 0.02 +591.222 -0.02 +591.674 0.06 +592.126 0 +592.577 0.09 +593.029 -0.1 +593.481 0.04 +593.932 0 +594.384 -0.02 +594.836 -0.05 +595.287 0.02 +595.739 -0.11 +596.19 -0.02 +596.642 0.11 +597.093 0.15 +597.545 0.07 +597.996 -0.06 +598.448 0.02 +598.899 -0.04 +599.35 -0.07 +599.802 -0.01 +600.253 0.02 +600.704 -0.06 +601.155 -0.15 +601.607 -0.12 +602.058 -0.11 +602.509 -0.01 +602.96 -0.06 +603.411 0.11 +603.862 0.08 +604.313 0.2 +604.764 -0.06 +605.215 0.02 +605.666 -0.07 +606.117 -0.06 +606.568 0.11 +607.019 -0.13 +607.47 -0.02 +607.921 -0.12 +608.371 -0.23 +608.822 0.02 +609.273 0.06 +609.724 0.13 +610.174 0.08 +610.625 0.11 +611.076 0.06 +611.526 -0.06 +611.977 -0.06 +612.427 -0.02 +612.878 0.08 +613.328 0.01 +613.779 0.04 +614.229 0.05 +614.68 -0.07 +615.13 -0.06 +615.58 -0.15 +616.031 0.01 +616.481 0 +616.931 -0.15 +617.382 -0.12 +617.832 0.01 +618.282 0.17 +618.732 -0.02 +619.182 -0.07 +619.632 -0.28 +620.083 -0.04 +620.533 -0.14 +620.983 0.08 +621.433 0.05 +621.883 0.09 +622.333 0.05 +622.783 -0.11 +623.232 -0.11 +623.682 0.13 +624.132 0.09 +624.582 -0.01 +625.032 -0.08 +625.481 0.11 +625.931 -0.02 +626.381 0.02 +626.831 0 +627.28 0.08 +627.73 0.15 +628.179 -0.16 +628.629 0.02 +629.079 0.14 +629.528 0.08 +629.978 -0.13 +630.427 -0.15 +630.876 0.16 +631.326 0.04 +631.775 0.04 +632.225 0.09 +632.674 -0.07 +633.123 -0.12 +633.573 -0.03 +634.022 -0.05 +634.471 0.13 +634.92 0.17 +635.369 0.01 +635.818 0.01 +636.268 -0.14 +636.717 0.02 +637.166 0.11 +637.615 0.08 +638.064 -0.04 +638.513 -0.13 +638.962 -0.01 +639.411 0.14 +639.859 0.1 +640.308 -0.04 +640.757 0.15 +641.206 0.04 +641.655 -0.14 +642.103 0.01 +642.552 0.04 +643.001 0.07 +643.45 -0.04 +643.898 0.11 +644.347 0 +644.795 0.17 +645.244 0 +645.692 -0.03 +646.141 -0.03 +646.589 -0.07 +647.038 0.18 +647.486 0.03 +647.935 0.08 +648.383 0.11 +648.831 -0.06 +649.28 -0.03 +649.728 0.03 +650.176 -0.05 +650.625 -0.01 +651.073 0.07 +651.521 -0.04 +651.969 0.07 +652.417 0 +652.865 0.04 +653.313 -0.09 +653.761 -0.01 +654.209 0.11 +654.657 0.12 +655.105 0.16 +655.553 -0.13 +656.001 -0.06 +656.449 -0.04 +656.897 -0.06 +657.345 0.12 +657.793 0.06 +658.24 0.03 +658.688 -0.02 +659.136 0.05 +659.584 -0.09 +660.031 -0.05 +660.479 0.11 +660.926 0.17 +661.374 -0.04 +661.822 0.11 +662.269 0.03 +662.717 -0.09 +663.164 0.02 +663.611 0.12 +664.059 0.11 +664.506 0.08 +664.954 -0.03 +665.401 0.05 +665.848 -0.04 +666.296 0.02 +666.743 0.02 +667.19 0.13 +667.637 -0.07 +668.084 -0.06 +668.532 -0.11 +668.979 -0.1 +669.426 -0.03 +669.873 -0.02 +670.32 0.25 +670.767 -0.01 +671.214 -0.02 +671.661 0.22 +672.108 0.21 +672.555 0.14 +673.002 0.23 +673.448 0.15 +673.895 0.16 +674.342 0.04 +674.789 0.17 +675.235 0.12 +675.682 0.19 +676.129 0.03 +676.576 0.37 +677.022 0.15 +677.469 0.16 +677.915 -0.05 +678.362 0.24 +678.808 0.33 +679.255 0.2 +679.701 0.22 +680.148 0.21 +680.594 0.07 +681.041 0.21 +681.487 0.21 +681.933 0.36 +682.38 0.29 +682.826 0.5 +683.272 0.48 +683.718 0.49 +684.165 0.79 +684.611 1.09 +685.057 1.31 +685.503 1.83 +685.949 3.15 +686.395 4.55 +686.841 7.27 +687.287 11.74 +687.733 19.13 +688.179 29.95 +688.625 44.29 +689.071 56.79 +689.517 66.46 +689.963 72.42 +690.408 73.42 +690.854 75.79 +691.3 76.21 +691.746 77.76 +692.191 77.99 +692.637 79.37 +693.083 80.64 +693.528 83.42 +693.974 85.5 +694.419 85.33 +694.865 87.07 +695.311 86.82 +695.756 85.95 +696.201 85.16 +696.647 82.87 +697.092 82.21 +697.538 82.19 +697.983 82.97 +698.428 82.35 +698.874 82.57 +699.319 83.05 +699.764 83.78 +700.209 83.92 +700.655 84.64 +701.1 84.21 +701.545 84.33 +701.99 84.34 +702.435 85.04 +702.88 84.41 +703.325 86.05 +703.77 87.18 +704.215 88.7 +704.66 88.74 +705.105 88.61 +705.55 85.02 +705.995 79.77 +706.44 69.94 +706.884 58.82 +707.329 46.78 +707.774 35.81 +708.219 27.19 +708.663 19.34 +709.108 14.29 +709.553 10.15 +709.997 7.17 +710.442 5.26 +710.886 3.41 +711.331 2.8 +711.775 2.18 +712.22 1.45 +712.664 1.32 +713.109 0.94 +713.553 0.72 +713.998 0.73 +714.442 0.43 +714.886 0.27 +715.331 0.25 +715.775 0.54 +716.219 0.14 +716.663 0.47 +717.107 0.19 +717.552 0.09 +717.996 0.24 +718.44 0.14 +718.884 0.23 +719.328 0.27 +719.772 0.13 +720.216 0.17 +720.66 0.2 +721.104 0.08 +721.548 -0.05 +721.992 0.1 +722.436 0.08 +722.879 0.07 +723.323 0.4 +723.767 0.21 +724.211 0.08 +724.654 0.21 +725.098 0.06 +725.542 -0.13 +725.985 -0.12 +726.429 -0.12 +726.873 0.03 +727.316 0.06 +727.76 0.11 +728.203 0.08 +728.647 -0.01 +729.09 0.05 +729.534 0.09 +729.977 -0.01 +730.42 0.11 +730.864 0.11 +731.307 -0.07 +731.75 -0.11 +732.194 0.02 +732.637 0.01 +733.08 0.28 +733.523 0.24 +733.966 0 +734.41 -0.16 +734.853 0.03 +735.296 -0.11 +735.739 -0.07 +736.182 0.16 +736.625 0.21 +737.068 0.3 +737.511 -0.04 +737.954 -0.09 +738.396 0.19 +738.839 -0.24 +739.282 -0.08 +739.725 -0.15 +740.168 0.16 +740.61 -0.02 +741.053 -0.16 +741.496 0.01 +741.939 0.01 +742.381 0.01 +742.824 -0.13 +743.266 0.1 +743.709 0.27 +744.151 0.05 +744.594 -0.23 +745.036 -0.19 +745.479 0.08 +745.921 -0.12 +746.364 -0.12 +746.806 -0.17 +747.248 0.23 +747.691 0.32 +748.133 -0.11 +748.575 0.12 +749.017 0.06 +749.46 -0.01 +749.902 -0.21 +750.344 0.2 +750.786 -0.33 +751.228 -0.01 +751.67 0.01 +752.112 0.06 +752.554 0.18 +752.996 -0.05 +753.438 0.16 +753.88 -0.27 +754.322 -0.16 +754.764 0.19 +755.206 0.18 +755.648 0.06 +756.09 0.2 +756.531 -0.06 +756.973 0.31 +757.415 0.35 +757.856 -0.08 +758.298 0.32 +758.74 -0.02 +759.181 0.24 +759.623 0.04 +760.065 0.02 +760.506 -0.07 +760.948 -0.06 +761.389 -0.15 +761.83 0.12 +762.272 0.23 +762.713 0.32 +763.155 -0.19 +763.596 -0.45 +764.037 -0.04 +764.479 0.09 +764.92 0.01 +765.361 0.1 +765.802 0.22 +766.243 -0.03 +766.685 0.18 +767.126 -0.05 +767.567 0.53 +768.008 0.1 +768.449 0 +768.89 -0.1 +769.331 0.01 +769.772 -0.26 +770.213 0.14 +770.654 0 +771.095 -0.24 +771.535 0.05 +771.976 0.05 +772.417 0.03 +772.858 0.29 +773.299 -0.08 +773.739 0.01 +774.18 0.25 +774.621 0.39 +775.061 -0.04 +775.502 0.4 +775.942 0.41 +776.383 0.15 +776.824 0 +777.264 0.43 +777.705 0.43 +778.145 -0.07 +778.585 -0.08 +779.026 -0.27 +779.466 -0.03 +779.906 0.4 +780.347 0.03 +780.787 0.01 +781.227 -0.01 +781.668 -0.1 +782.108 0.46 +782.548 0.21 +782.988 0.59 +783.428 0 +783.868 -0.2 +784.308 -0.07 +784.748 -0.01 +785.188 0 +785.628 -0.22 +786.068 0.48 +786.508 0.28 +786.948 0.18 +787.388 -0.16 +787.828 -0.18 +788.268 -0.03 +788.708 0.23 +789.147 0.19 +789.587 0.38 +790.027 0.25 +790.467 0 +790.906 -0.18 +791.346 -0.03 +791.785 0.06 +792.225 -0.57 +792.665 0.2 +793.104 -0.11 +793.544 -0.2 +793.983 0.11 +794.422 0.3 +794.862 -0.11 +795.301 0.38 +795.741 -0.38 +796.18 0.06 +796.619 -0.11 +797.059 -0.1 +797.498 0.11 +797.937 0.08 +798.376 0 +798.815 0.43 +799.255 0.38 +799.694 0.08 +800.133 -0.05 +800.572 -0.49 +801.011 -0.2 +801.45 -0.03 +801.889 -0.03 +802.328 0.24 +802.767 -0.09 +803.206 0.55 +803.645 -0.31 +804.083 0.14 +804.522 0.3 +804.961 0.12 +805.4 -0.36 +805.839 0.14 +806.277 0.14 +806.716 -0.04 +807.155 0.16 +807.593 -0.3 +808.032 0.12 +808.471 -0.18 +808.909 0.29 +809.348 0.45 +809.786 0.09 +810.225 -0.23 +810.663 0.18 +811.101 0.23 +811.54 0.51 +811.978 0 +812.417 0.04 +812.855 0.2 +813.293 0.17 +813.731 -0.09 +814.17 -0.19 +814.608 0.06 +815.046 0.21 +815.484 -0.04 +815.922 -0.04 +816.36 0.1 +816.798 0.02 +817.236 0.35 +817.674 -0.09 +818.112 0 +818.55 0.2 +818.988 -0.08 +819.426 -0.36 +819.864 0 +820.302 0.18 +820.74 -0.22 +821.178 0.12 +821.615 0.1 +822.053 -0.1 +822.491 -0.18 +822.929 0.18 +823.366 0.25 +823.804 0.17 +824.241 0.39 +824.679 0.19 +825.117 0 +825.554 -0.1 +825.992 0.29 +826.429 -0.33 +826.867 0.23 +827.304 0 +827.741 0.34 +828.179 0.58 +828.616 0.11 +829.053 -0.11 +829.491 0.31 +829.928 0.29 +830.365 -0.22 +830.802 -0.13 +831.24 -0.57 +831.677 0.15 +832.114 -0.37 +832.551 0.02 +832.988 0.22 +833.425 0 +833.862 -0.45 +834.299 0.04 +834.736 -0.14 +835.173 -0.09 +835.61 -0.33 +836.047 -0.07 +836.484 0.19 +836.92 0.12 +837.357 -0.09 +837.794 0.35 +838.231 -0.31 +838.668 0.32 +839.104 -0.34 +839.541 0.2 +839.978 0.19 +840.414 -0.25 +840.851 0.1 +841.287 -0.17 +841.724 0.27 +842.16 -0.37 +842.597 -0.27 +843.033 0.3 +843.47 0.55 +843.906 -0.48 +844.342 -0.36 +844.779 -0.38 +845.215 0.66 +845.651 -0.55 +846.088 -0.36 +846.524 -0.03 +846.96 0.34 +847.396 -0.05 +847.832 -0.34 +848.269 0.24 +848.705 -0.71 +849.141 -0.21 +849.577 -0.03 +850.013 0.21 +850.449 -0.03 +850.885 0.08 +851.321 0.64 +851.757 0.68 +852.192 -0.3 +852.628 0.58 +853.064 0.36 +853.5 0.22 +853.936 -0.14 +854.372 0.25 +854.807 0.42 +855.243 0.23 +855.679 -0.06 +856.114 -0.38 +856.55 0.15 +856.985 0.29 +857.421 0.43 +857.857 0.48 +858.292 0.36 +858.728 0.72 +859.163 -0.12 +859.598 0.39 +860.034 0.59 +860.469 0.12 +860.905 -0.24 +861.34 0.9 +861.775 -0.12 +862.21 -0.55 +862.646 -0.24 +863.081 0.09 +863.516 0.62 +863.951 -0.84 +864.386 -0.25 +864.821 0.54 +865.257 0.06 +865.692 -0.03 +866.127 0.06 +866.562 -0.1 +866.997 0.46 +867.432 -0.36 +867.866 -0.3 +868.301 0.27 +868.736 0.48 +869.171 0.07 +869.606 0.03 +870.041 0.77 +870.475 -0.03 +870.91 -0.17 +871.345 0.44 +871.779 0.17 +872.214 -0.53 +872.649 0.14 +873.083 0.11 +873.518 0.04 +873.952 0.07 +874.387 -0.53 +874.821 -0.32 +875.256 -0.14 +875.69 -0.48 +876.125 -0.75 +876.559 0.11 +876.993 1.02 +877.428 -0.11 +877.862 0.22 +878.296 0.68 +878.731 -0.51 +879.165 0.43 +879.599 -1.17 +880.033 -0.11 +880.467 0.07 +880.901 0.56 +881.336 0.19 +881.77 0.8 +882.204 1.04 +882.638 -0.27 +883.072 -0.44 +883.506 0.08 +883.939 0.27 +884.373 0.2 +884.807 -0.12 +885.241 0.6 +885.675 0.28 +886.109 0.19 +886.542 0.87 +886.976 1.75 +887.41 -0.41 +887.844 0.72 +888.277 -0.61 +888.711 0.72 +889.144 0.04 +889.578 0.29 +890.012 0 +890.445 0.21 +890.879 -0.79 +891.312 0.21 +891.745 0.39 +892.179 0.34 +892.612 -0.92 +893.046 0.26 +893.479 0.9 +893.912 -0.49 +894.345 -0.31 +894.779 -0.39 +895.212 0.17 +895.645 0.84 +896.078 -0.18 +896.511 0 +896.945 0 +897.378 -0.09 +897.811 -0.91 +898.244 0.49 +898.677 0.19 +899.11 -0.8 +899.543 -0.09 +899.976 0.65 +900.409 -0.29 +900.841 -0.67 +901.274 -0.24 +901.707 1.15 +902.14 -0.5 +902.573 0.34 +903.005 0.5 +903.438 0.86 +903.871 -0.5 +904.303 -0.1 +904.736 -0.36 +905.169 0.71 +905.601 -0.62 +906.034 0.52 +906.466 -0.11 +906.899 0.89 +907.331 -0.46 +907.764 1.2 +908.196 0.05 +908.629 1.16 +909.061 0.16 +909.493 1.59 +909.926 1.5 +910.358 1.49 +910.79 -0.11 +911.222 2.14 +911.655 0.61 +912.087 -0.18 +912.519 -0.17 +912.951 0.53 +913.383 0.35 +913.815 -0.53 +914.247 -0.24 +914.679 2 +915.111 0.9 +915.543 -1.49 +915.975 -1.89 +916.407 0.55 +916.839 1 +917.271 -0.81 +917.703 -0.7 +918.134 0.19 +918.566 -0.82 +918.998 -0.58 +919.43 0.45 +919.861 0.06 +920.293 -0.13 +920.725 -0.39 +921.156 -0.26 +921.588 0.2 +922.019 0.07 +922.451 0.2 +922.882 0.49 +923.314 0 +923.745 -0.49 +924.177 0.42 +924.608 0.14 +925.039 0.63 +925.471 -3.04 +925.902 0.79 +926.333 -0.29 +926.765 0.52 +927.196 1.22 +927.627 0.36 +928.058 1.38 +928.489 1.27 +928.921 2.33 +929.352 -2.11 +929.783 -1.35 +930.214 -2.41 +930.645 0 +931.076 -0.77 +931.507 1.16 +931.938 1.15 +932.369 0.23 +932.8 -0.39 +933.23 0.16 +933.661 0.88 +934.092 1.68 +934.523 0.16 +934.954 0.41 +935.384 1.2 +935.815 -0.33 +936.246 0.81 +936.676 1.8 +937.107 -1.17 +937.538 -1.4 +937.968 1.54 +938.399 1.7 +938.829 -2.14 +939.26 -0.78 +939.69 -0.53 +940.121 -1.11 +940.551 -2.13 +940.981 0.61 +941.412 -1.23 +941.842 -0.44 +942.272 -0.45 +942.703 -2.33 +943.133 0.18 +943.563 -1.75 +943.993 -2.54 +944.423 0.27 +944.854 -0.85 +945.284 -1.04 +945.714 -0.47 +946.144 0.55 +946.574 0.58 +947.004 0.57 +947.434 1.08 +947.864 1.43 +948.294 0.6 +948.724 -0.42 +949.153 1.04 +949.583 0.82 +950.013 0.11 +950.443 0.11 +950.873 0.92 +951.302 1.37 +951.732 0.43 +952.162 -0.94 +952.591 -1.46 +953.021 2.59 +953.451 -2.59 +953.88 -1.14 +954.31 -0.23 +954.739 -0.12 +955.169 -2.41 +955.598 -1.05 +956.028 0.81 +956.457 -1.19 +956.886 -1.07 +957.316 2.25 +957.745 1.26 +958.174 0.63 +958.604 1.33 +959.033 -1.53 +959.462 -1.52 +959.891 -3.42 +960.321 0 +960.75 2.76 +961.179 3.52 +961.608 -2.31 +962.037 -2.28 +962.466 0.57 +962.895 0.14 +963.324 -0.96 +963.753 1.14 +964.182 0 +964.611 -0.43 +965.04 -4.87 +965.469 2.85 +965.897 -0.29 +966.326 -0.46 +966.755 -0.46 +967.184 2.18 +967.612 1.86 +968.041 -3.15 +968.47 -5.56 +968.898 1.3 +969.327 1.63 +969.756 3.69 +970.184 -0.5 +970.613 -0.16 +971.041 -3.54 +971.47 0.7 +971.898 -0.52 +972.327 1.9 +972.755 0.69 +973.183 -1.22 +973.612 -3.38 +974.04 3.44 +974.468 -2.12 +974.897 -1.66 +975.325 3.9 +975.753 1.33 +976.181 3.11 +976.609 0 +977.037 -2.63 +977.466 -0.81 +977.894 0.81 +978.322 -1.76 +978.75 0.19 +979.178 -0.98 +979.606 -1.1 +980.034 -3.35 +980.462 -1.11 +980.889 6.78 +981.317 -3.81 +981.745 -6.38 +982.173 -0.44 +982.601 1.75 +983.028 3.07 +983.456 3.13 +983.884 -2.93 +984.312 5.23 +984.739 0.25 +985.167 -0.24 +985.594 3.59 +986.022 3.31 +986.45 -7.18 +986.877 0.76 +987.305 0.26 +987.732 -6.62 +988.159 -5.36 +988.587 3.67 +989.014 1.57 +989.442 -2.17 +989.869 -6.23 +990.296 -2.88 +990.724 0.96 +991.151 -1.39 +991.578 -2.61 +992.005 0 +992.432 3.35 +992.859 -9.57 +993.287 4.06 +993.714 -0.94 +994.141 9.31 +994.568 -5.5 +994.995 4.01 +995.422 6.11 +995.849 10.28 +996.276 -0.35 +996.703 4.97 +997.129 2.86 +997.556 5.1 +997.983 0.34 +998.41 4.91 +998.837 4.97 +999.263 -1.38 +999.69 -3.33 +1000.117 -6.12 +1000.543 3.92 +1000.97 -9.06 +1001.397 -1.9 +1001.823 1.28 +1002.25 -2.51 +1002.676 -6.06 +1003.103 -1.61 +1003.529 -6.3 +1003.956 -1.49 +1004.382 0 +1004.809 0.36 +1005.235 3.54 +1005.661 6.18 +1006.088 -5.62 +1006.514 -1.94 +1006.94 -11.56 +1007.366 -3.57 +1007.793 -0.37 +1008.219 -7.33 +1008.645 -5.68 +1009.071 0.42 +1009.497 -11.95 +1009.923 4.68 +1010.349 2.5 +1010.775 4.38 +1011.201 -0.37 +1011.627 0.83 +1012.053 1.3 +1012.479 -6.09 +1012.905 -8.7 +1013.331 4.39 +1013.757 -2.78 +1014.182 -4.7 +1014.608 -5.59 +1015.034 2.86 +1015.46 7.87 +1015.885 2.81 +1016.311 14.54 +1016.737 2.86 +1017.162 1.02 +1017.588 -6.08 +1018.013 -10.6 +1018.439 5.79 +1018.865 3.54 +1019.29 -6.29 +1019.715 -6.88 +1020.141 1.56 +1020.566 -0.58 +1020.992 0.55 +1021.417 2.12 +1021.842 14.08 +1022.268 8.29 +1022.693 0.55 +1023.118 -13.02 +1023.543 4.47 +1023.969 -0.69 +1024.394 0 +1024.819 3.49 +1025.244 -3.87 +1025.669 -20.01 +1026.094 -7.1 +1026.519 7.23 +1026.944 3.64 +1027.369 9.1 +1027.794 7.98 +1028.219 11.93 +1028.644 15.23 +1029.069 -9.57 +1029.494 2.11 +1029.918 9.82 +1030.343 2.82 +1030.768 3.45 +1031.193 -7.7 +1031.617 13.55 +1032.042 -12.28 +1032.467 -13.25 +1032.891 9.24 +1033.316 0.84 +1033.741 -7.04 +1034.165 20.32 +1034.59 31.57 +1035.014 3.97 +1035.439 -6.09 +1035.863 12.65 +1036.288 3.28 +1036.712 2.02 +1037.136 3 +1037.561 -2.94 +1037.985 -6.86 +1038.409 9.1 +1038.834 3.37 +1039.258 12.76 +1039.682 12.08 +1040.106 32.3 +1040.53 -1.87 +1040.954 -24.08 +1041.379 3.85 +1041.803 -1.93 +1042.227 -20.24 +1042.651 15.72 +1043.075 17.92 +1043.499 -5.06 +1043.923 -10.36 +1044.346 -12.13 +1044.77 24.82 +1045.194 -21.95 +1045.618 3.58 +1046.042 16.51 +1046.466 -6.67 +1046.889 20.48 +1047.313 -6.99 +1047.737 15.57 +1048.16 12.78 +1048.584 -26.61 +1049.008 13.94 +1049.431 -13.27 +1049.855 4.12 +1050.278 10.99 +1050.702 20.35 +1051.125 21.65 +1051.549 -1.59 +1051.972 8.34 +1052.396 8.35 +1052.819 2.71 +1053.242 5.37 +1053.666 -20.87 +1054.089 13.2 +1054.512 8.08 +1054.936 -42.45 +1055.359 -22.08 +1055.782 -20.04 +1056.205 6.86 +1056.628 -56.12 +1057.051 -5.14 +1057.475 -5.18 +1057.898 -29.62 +1058.321 -37.82 +1058.744 -18.49 +1059.167 -1.39 +1059.59 -22.43 +1060.012 12.52 +1060.435 -2 +1060.858 17.13 +1061.281 -54.48 +1061.704 -23.87 +1062.127 -2.33 +1062.549 -8.36 +1062.972 8.35 +1063.395 -8.79 +1063.818 0 +1064.24 7.29 +1064.663 -12.27 +1065.086 -17.55 +1065.508 20.67 +1065.931 28.65 +1066.353 -75.74 +1066.776 -62.38 +1067.198 29.03 +1067.621 10.9 +1068.043 -6.16 +1068.465 -7.71 +1068.888 66.86 +1069.31 26.9 +1069.732 12.16 +1070.155 3.14 +1070.577 31.19 +1070.999 -55.31 +1071.421 27.58 +1071.844 0 +1072.266 50.94 +1072.688 -51.92 +1073.11 -12.3 +1073.532 12.16 +1073.954 -56.25 +1074.376 -34.18 +1074.798 27.11 +1075.22 66.89 +1075.642 43.03 +1076.064 -56.57 +1076.486 -16.73 +1076.908 10.76 +1077.33 40.22 +1077.751 342 +1078.173 50.2 +1078.595 40.15 +1079.017 -112.5 +1079.438 -3.35 +1079.86 14.62 +1080.282 42.49 +1080.703 -85.34 +1081.125 -35.5 +1081.546 79.79 +1081.968 -6.92 +1082.39 -192.86 +1082.811 7.16 +1083.232 -13.38 +1083.654 -86.17 +1084.075 -115.2 +1084.497 39.13 +1084.918 51.51 +1085.339 116.38 +1085.761 38.85 +1086.182 -132.17 +1086.603 37.67 +1087.024 -67.16 +1087.446 -194.78 +1087.867 105.88 +1088.288 60.45 +1088.709 -93.1 +1089.13 40.91 +1089.551 5.59 +1089.972 165.79 +1090.393 194.59 +1090.814 9.64 +1091.235 -135.85 +1091.656 120.9 +1092.077 229.09 +1092.498 22.66 +1092.919 89.74 +1093.34 -90 +1093.76 -8.74 +1094.181 -50.94 +1094.602 113.59 +1095.023 151.4 +1095.443 -110.2 +1095.864 267.57 +1096.285 165.31 +1096.705 -109.35 +1097.126 -100.8 +1097.546 52.43 +1097.967 104.85 +1098.387 -152.11 +1098.808 229.09 +1099.228 169.41 +1099.649 68.13 +1100.069 8.6 +1100.49 -36 +1100.91 18.27 +1101.33 201.72 +1101.751 60.34 +1102.171 33.54 +1102.591 13.71 +1103.011 -67.29 +1103.431 3.72 +1103.852 68.13 diff --git a/Modules/Biophotonics/python/iMC/msi/data/testMsi.nrrd b/Modules/Biophotonics/python/iMC/msi/data/testMsi.nrrd new file mode 100644 index 0000000000..82708c6160 Binary files /dev/null and b/Modules/Biophotonics/python/iMC/msi/data/testMsi.nrrd differ diff --git a/Modules/Biophotonics/python/iMC/msi/imgmani.py b/Modules/Biophotonics/python/iMC/msi/imgmani.py new file mode 100644 index 0000000000..ceb06a581b --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/imgmani.py @@ -0,0 +1,66 @@ +''' +Created on Aug 28, 2015 + +@author: wirkert +''' + +import numpy as np + + +def collapse_image(img): + """ helper method which transorms the n x m x nrWavelengths image to a + (n*m) x nrWavelength image. + + note that this function doesn't take an object of class Msi but + msi.get_image() """ + return img.reshape(-1, img.shape[-1]) + + +def remove_masked_elements(img): + """ helper method which removes masked pixels. + Note that by applying this method, the img loses it's shape.""" + collapsed_image = collapse_image(img) + # if one reflectance is masked msis are defined to have all refl. + # masked. Thus we can just have a look at the first column + one_column = collapsed_image[:, 0] + if (isinstance(one_column, np.ma.masked_array)): + masked_elems = np.where(one_column.mask) + collapsed_image = np.delete(collapsed_image, masked_elems, 0) + return collapsed_image + + +def select_n_reflectances(img, n): + """ randomly select n reflectances from image. + The output is of shape n x nr_wavelengths """ + collapsed_image = collapse_image(img) + perms = np.random.permutation(collapsed_image.shape[0]) + first_n_perms = perms[0:n] + return collapsed_image[first_n_perms, :] + + +def get_bands(img, bands): + """get the bands bands (np.array) from the multispectral image. + Example: image is 2048x2048x8. get_bands(img, [0,3] will return + img[:,:,[0,3]]. The advantage of this function is that the image does not + need to be 2d + wavelength.""" + original_shape = img.shape + collapsed_image = collapse_image(img) + img_bands = collapsed_image[ :, bands] + new_nr_bands = 1 + if hasattr(bands, "__len__"): + new_nr_bands = len(bands) + new_shape = original_shape[:-1] + (new_nr_bands,) + return np.reshape(img_bands, new_shape) + + +def sortout_bands(img, bands): + """delete bands bands (np.array) from the multispectral image. + Example: image is 2048x2048x8. sortout_bands(img, [0,3] will return + img[:,:,[1,2,4,5,6,7]]. The advantage of this function is that the image does not + need to be 2d + wavelength. + + TODO SW: Test""" + all_bands = np.arange(img.shape[-1]) + bands_to_get = np.setdiff1d(all_bands, bands) + return get_bands(img, bands_to_get) + diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/helper/__init__.py b/Modules/Biophotonics/python/iMC/msi/io/__init__.py similarity index 100% rename from Modules/Biophotonics/python/inverseMonteCarlo/helper/__init__.py rename to Modules/Biophotonics/python/iMC/msi/io/__init__.py diff --git a/Modules/Biophotonics/python/iMC/msi/io/msireader.py b/Modules/Biophotonics/python/iMC/msi/io/msireader.py new file mode 100644 index 0000000000..6bb7413ad8 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/msireader.py @@ -0,0 +1,22 @@ +''' +Created on Aug 25, 2015 + +@author: wirkert +''' + +import pickle + +class MsiReader(object): + ''' + The MsiReader reads the Msi from the serialized python object. + This is the prefered way of reading an Msi. + ''' + + def __init__(self): + pass + + def read(self, filename_to_read): + msi_file = open(filename_to_read, 'r') + msi = pickle.load(msi_file) + msi_file.close() + return msi diff --git a/Modules/Biophotonics/python/iMC/msi/io/msiwriter.py b/Modules/Biophotonics/python/iMC/msi/io/msiwriter.py new file mode 100644 index 0000000000..815c465a54 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/msiwriter.py @@ -0,0 +1,25 @@ +''' +Created on Aug 25, 2015 + +@author: wirkert +''' + +import pickle + +from writer import Writer + +class MsiWriter(Writer): + ''' + The MsiReader writing the Msi as a serialized python object. + ''' + + def __init__(self, msiToWrite): + """ + initialize the write with a specific multi spectral image (class Msi) + """ + self.msiToWrite = msiToWrite + + def write (self, uri): + f = open(uri, 'w') + pickle.dump(self.msiToWrite, f) + f.close() diff --git a/Modules/Biophotonics/python/iMC/msi/io/nrrdreader.py b/Modules/Biophotonics/python/iMC/msi/io/nrrdreader.py new file mode 100644 index 0000000000..637ac7ab43 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/nrrdreader.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Aug 10 16:29:20 2015 + +@author: wirkert +""" + +import logging +import numpy as np + +import SimpleITK as sitk + +from msi.io.reader import Reader +from msi.msi import Msi + + +class NrrdReader(Reader): + + def __init__(self): + pass + + def read(self, fileToRead): + """ read the nrrd image. + TODO: properties are not correctly extracted from nrrd.""" + + image = None + + try: + reader = sitk.ImageFileReader() + reader.SetFileName(fileToRead) + + image = reader.Execute() + image = sitk.GetArrayFromImage(image) + + except RuntimeError as re: + # image could not be read + logging.warning("image " + fileToRead + + " could not be loaded: " + + str(re)) + # rethrow exception after logging + raise + + # if image is too low dimensional singleton dimensions + # are added when saving. Done because sitk can only handle dimensions + # 2,3,4. This removes these singleton dimensions again. + squeezed_image = np.squeeze(image) + msi = Msi(squeezed_image) + return msi diff --git a/Modules/Biophotonics/python/iMC/msi/io/nrrdwriter.py b/Modules/Biophotonics/python/iMC/msi/io/nrrdwriter.py new file mode 100644 index 0000000000..a07694a0e0 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/nrrdwriter.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 13 09:48:18 2015 + +@author: wirkert +""" + +import logging +import numpy as np + +import SimpleITK as sitk + +from msi.io.writer import Writer + + +class NrrdWriter(Writer): + + def __init__(self, msiToWrite): + """ + initialize the write with a specific multi spectral image (class Msi) + """ + self.msiToWrite = msiToWrite + + def write(self, uri): + """ + write the msi image to the specified uri + """ + img_to_write = self.msiToWrite.get_image() + + # sitk can only write images of dimension 2,3,4. This hack is + # to fake 1d images as being 2d. 1d images e.g. occure after taking + # the mean of an image. + if len(img_to_write.shape) == 1: + img_to_write = np.reshape(img_to_write, (1, 1, img_to_write.shape[0])) + + img = sitk.GetImageFromArray(img_to_write, isVector=True) + sitk.WriteImage(img, uri) + logging.info("written file " + uri + " to disk") + return None diff --git a/Modules/Biophotonics/python/iMC/msi/io/pngreader.py b/Modules/Biophotonics/python/iMC/msi/io/pngreader.py new file mode 100644 index 0000000000..48ee12e81e --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/pngreader.py @@ -0,0 +1,58 @@ +''' +Created on Sep 28, 2015 + +@author: wirkert +''' + + +import os +import itertools + +# PIL always rescales the image, thus PIL and skimage (which uses PIL) cannot +# be used +import png +import numpy as np + +from msi.io.reader import Reader +from msi.msi import Msi + + +class PngReader(Reader): + """ + Assumes bitdepth 16bit. + TODO SW: document and test""" + + def __init__(self): + pass + + + def read(self, fileToRead): + """ read the msi from pngs. + The fileToRead is a string prefix, all files starting with this + prefix will be summarized to one msi""" + + path, file_prefix = os.path.split(fileToRead) + files = os.listdir(path) + files_to_read = [os.path.join(path, f) for f in files + if f.startswith(file_prefix)] + files_to_read.sort() + image_array = [toImage(f) + for f in files_to_read] + image = reduce(lambda x, y: np.dstack((x, y)), image_array) + + msi = Msi(image) + return msi + + +def toImage(f): + reader = png.Reader(f) + column_count, row_count, pngdata, params = reader.asDirect() + plane_count = params['planes'] + image_2d = np.vstack(itertools.imap(np.uint16, pngdata)) + # this is needed for rgb images. probably better would be a mean in case + # we convert "real" rgb data. This is just for rgb images which + # contain the same values for r,g and b for every pixel. + image_3d = np.reshape(image_2d, + (row_count, column_count, plane_count)) + return image_3d[:, :, 0] + diff --git a/Modules/Biophotonics/python/iMC/msi/io/reader.py b/Modules/Biophotonics/python/iMC/msi/io/reader.py new file mode 100644 index 0000000000..99d6b678c3 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/reader.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 7 13:39:16 2015 + +@author: wirkert +""" + + +class Reader: + """ + Abstract reader base class + """ + + def __init__(self): + pass + + def read(self, fileToRead): + return None + diff --git a/Modules/Biophotonics/python/iMC/msi/io/spectrometerreader.py b/Modules/Biophotonics/python/iMC/msi/io/spectrometerreader.py new file mode 100644 index 0000000000..b182201d7a --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/spectrometerreader.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 7 12:04:18 2015 + +@author: wirkert +""" + +import numpy as np +from msi.io.reader import Reader +from msi.msi import Msi + + +class SpectrometerReader(Reader): + + def __init__(self): + pass + + def read(self, file_to_read): + # our spectrometer like to follow german standards in files, we need + # to switch to english ones + transformed="" + replacements = {',': '.', '\r\n': ''} + with open(file_to_read) as infile: + for line in infile: + for src, target in replacements.iteritems(): + line = line.replace(src, target) + transformed = "\n".join([transformed, line]) + + for num, line in enumerate(transformed.splitlines(), 1): + if ">>>>>Begin" in line: + break + + for num_end, line in enumerate(transformed.splitlines(), 1): + if ">>>>>End" in line: + num_end -= 1 + break + string_only_spectrum = "\n".join(transformed.splitlines()[num:num_end]) + data_vector = np.fromstring(string_only_spectrum, + sep="\t").reshape(-1, 2) + msi = Msi(data_vector[:, 1], + {'wavelengths': data_vector[:, 0] * 10 ** -9}) + return msi + diff --git a/Modules/Biophotonics/python/iMC/msi/io/tiffreader.py b/Modules/Biophotonics/python/iMC/msi/io/tiffreader.py new file mode 100644 index 0000000000..fdc7bae5fb --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/tiffreader.py @@ -0,0 +1,100 @@ +''' +Created on Sep 28, 2015 + +@author: wirkert +''' + + +import os + +import Image +import scipy +import numpy as np + +from msi.io.reader import Reader +from msi.msi import Msi + + +def sort_by_filter(s1, s2): + ''' + Sorting function which takes two strings and sorts them lexigraphically by + the last character before the file extension. + + say: + + blabla_w2_F0.tiff < blabla_w0_F1.tiff + + This is done to sort by the filter index, which is always written as the + last thing by the tiff writer. + ''' + s1_prefix = os.path.splitext(s1)[0] + s2_prefix = os.path.splitext(s2)[0] + + result = 0 + if s1_prefix < s2_prefix: + result = 1 + elif s1_prefix > s2_prefix: + result = -1 + + return result + + +class TiffReader(Reader): + """ + AAATTTEEENTION: Some big bug hiding somewhere here, ordering of files + is corrupted, no time now to fix. TODO SW + + Assumes bitdepth 16bit on a 12bit camera. + TODO SW: document and test""" + + # static member to globally set resizing for all images read by the reader + # 1.0 for no resizing + RESIZE_FACTOR = 1.0 + + def __init__(self, shift_bits=4): + self.shift_bits = shift_bits + + def read(self, file_to_read, resize_factor=None, + sorting_function=sort_by_filter): + """ read the msi from tiffs. + The fileToRead is a string prefix, all files starting with this + prefix will be summarized to one msi. they will be sorted as specified + in the sorting_function + + Args: + sorting_function: the function which defines the sorting of the + strings that match the prefix. Pass none if normal + lexicographical sorting is wished + file_to_read: the prefix of the tiff file which shall be read + """ + + if resize_factor is None: + resize_factor = TiffReader.RESIZE_FACTOR + + path, file_prefix = os.path.split(file_to_read) + files = os.listdir(path) + files_to_read = [os.path.join(path, f) for f in files + if file_prefix[2:] in f] + files_to_read.sort(cmp=sorting_function) + image_array = [self.to_image(f, resize_factor=resize_factor) + for f in files_to_read] + image = reduce(lambda x, y: np.dstack((x, y)), image_array) + + msi = Msi(image) + return msi + + def to_image(self, f, resize_factor): + im = Image.open(f) + im_array = np.array(im) + + im_array >>= self.shift_bits + + if do_resize(resize_factor): + im_array = scipy.misc.imresize(im_array, resize_factor, + interp="bilinear", mode="F") + + return im_array.astype('float') + + +def do_resize(resize_factor): + return not np.isclose(resize_factor, 1.0) and (resize_factor is not None) diff --git a/Modules/Biophotonics/python/iMC/msi/io/tiffringreader.py b/Modules/Biophotonics/python/iMC/msi/io/tiffringreader.py new file mode 100644 index 0000000000..b0bf6177b9 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/tiffringreader.py @@ -0,0 +1,100 @@ +''' +Created on Nov 2, 2015 + +@author: wirkert +''' + +import os +import logging + +import Image +import scipy +import numpy as np + +from msi.io.reader import Reader +from msi.msi import Msi + + +class TiffRingReader(Reader): + ''' + TODO SW: document and test + ''' + + # static member to globally set resizing for all images read by the reader + # 1.0 for no resizing + RESIZE_FACTOR = 1.0 + + def __init__(self, shift_bits=4): + self.shift_bits = shift_bits + + def read(self, fileToRead, n, resize_factor=None, + segmentation=None): + """ read the msi from tiffs. + The fileToRead is the first file to read, + then n files will be read to one msi from a + sorted file list + + segmentation: tiff filename of the segmentation. If none, it will be + tried to get a segmentation from npy files with filenames like the + tiff files + _seg.tiff. If this fails, no segmentation will be + assumed + """ + + if resize_factor is None: + resize_factor = TiffRingReader.RESIZE_FACTOR + + path, file_name = os.path.split(fileToRead) + files = os.listdir(path) + files_in_folder = [os.path.join(path, f) for f in files if + os.path.isfile(os.path.join(path, f)) and + f.endswith('.tiff')] + + files_in_folder.sort() + position = files_in_folder.index(fileToRead) + # take n images from found position + image_array = [self.to_image(f, resize_factor) + for f in files_in_folder[position:position + n]] + image = reduce(lambda x, y: np.dstack((x, y)), image_array) + + # in case of 1 dimensional image: add a fake last dimension, since + # we always assume the last dimension to be the wavelength domain. + # TODO SW: Test this and implement for other readers + if n is 1: + image = np.expand_dims(image, -1) + + msi = Msi(image) + + # we pass an explicic image as segmentation + if segmentation is not None: + segmentation = self.to_image(segmentation, resize_factor) + else: # otherwise: search for numpy segmentations + try: + segmentation_array = [to_segmentation(f) + for f in + files_in_folder[position:position + n]] + if do_resize(resize_factor): + segmentation = reduce(lambda x, y: x & y, segmentation_array) + segmentation = scipy.misc.imresize(segmentation, resize_factor, + interp="bilinear") + except: + logging.info("didn't find segmentation for all images") + return msi, segmentation + + def to_image(self, f, resize_factor): + im = Image.open(f) + im_array = np.array(im) + im_array >>= self.shift_bits + + if do_resize(resize_factor): + im_array= scipy.misc.imresize(im_array, resize_factor, + interp="bilinear", mode="F") + return im_array.astype('float') + + +def to_segmentation(f): + seg = np.load(f + ".seg.npy") + return seg + + +def do_resize(resize_factor): + return not np.isclose(resize_factor, 1.0) and (resize_factor is not None) diff --git a/Modules/Biophotonics/python/iMC/msi/io/tiffwriter.py b/Modules/Biophotonics/python/iMC/msi/io/tiffwriter.py new file mode 100644 index 0000000000..36506aa850 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/tiffwriter.py @@ -0,0 +1,72 @@ + + +import logging + +import numpy as np +from libtiff import TIFF + +from msi.io.writer import Writer + + +class TiffWriter(Writer): + """ + The tiff write will store nr_wavelength tiff files for one msi + """ + + def __init__(self, msi_to_write, convert_to_nm=True, scale_to_16_bit=False): + """ + initialize the write with a specific multi spectral image (class Msi) + """ + self.msi_to_write = msi_to_write + self.convert_to_nm = convert_to_nm + self.scale_to_16_bit = scale_to_16_bit + + def write(self, uri_prefix): + """ + write the msi image to the specified uri_prefix + + Args: + uri_prefix: the prefix off the uri. E.g. C:\example the image + write will automatically extend this prefix path to include the + wavelengths information and add a suffix. Your final + file may look similar to: C:\example_w_470nm_F0.tiff + convert_to_nm: if the wavelengths are saved in m they are hard to + write as string. Thus they can be automatically expanded to nm. + """ + img_to_write = self.msi_to_write.get_image() + + max_image_value = np.max(img_to_write) + + if self.scale_to_16_bit: + img_to_write *= 2**16 / max_image_value + + nr_wavelengths = self.msi_to_write.get_wavelengths().size + for wavelength_index in np.arange(nr_wavelengths): + full_uri = self._build_full_uri(uri_prefix, wavelength_index) + self._write_single_image(full_uri, + img_to_write[:, :, wavelength_index]) + + logging.info("written file " + full_uri + " to disk") + return None + + @staticmethod + def _write_single_image(full_uri, image_array): + """ + internally used method to write single tiff image + """ + tiff = TIFF.open(full_uri, mode='w') + tiff.write_image(image_array.astype('uint16'), write_rgb=False) + tiff.close() + + def _build_full_uri(self, uri_prefix, wavelength_index): + """ + Helper method to build full path of one image + Returns: full uri containing the desired properties. + """ + wavelength = self.msi_to_write.get_wavelengths()[wavelength_index] + if self.convert_to_nm: + wavelength *= 10**9 + full_uri = uri_prefix + "_w_" + str(wavelength) +\ + "_F" + str(wavelength_index) + ".tiff" + + return full_uri diff --git a/Modules/Biophotonics/python/iMC/msi/io/writer.py b/Modules/Biophotonics/python/iMC/msi/io/writer.py new file mode 100644 index 0000000000..9342ff00fe --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/io/writer.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 13 09:47:31 2015 + +@author: wirkert +""" + + +class Writer(): + """ + Abstract writer base class + """ + + def __init__(self): + pass + + def write(self, fileToWrite): + return None \ No newline at end of file diff --git a/Modules/Biophotonics/python/iMC/msi/msi.py b/Modules/Biophotonics/python/iMC/msi/msi.py new file mode 100644 index 0000000000..5638d44ad7 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/msi.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 6 18:21:36 2015 + +@author: wirkert +""" + +import numpy as np + + +class Msi(): + """ a multi spectral image stack consisting of: + + image: a rows x columns x nrWavelengths dimensional array + properties: additional, application dependent properties + """ + + def __init__(self, image=None, properties=None): + if image is None: + image = np.array([]) + if properties is None: + properties = {} + self._image = image + self._properties = properties + self._assure_basic_properties() + + self._test_image() + + def get_image(self): + return self._image + + def set_image(self, image, wavelengths=None): + """ + Put a new image into this msi + Args: + image: the rows x columns x nrWavelengths dimensional array + np.array. + wavelengths: a np.array of size nrWavelengths. If the number of + wavelengths hasn't change this is not needed. + """ + self._image = image + if wavelengths is not None: + self.set_wavelengths(wavelengths) + self._assure_basic_properties() + self._test_image() + + def get_wavelengths(self): + """ shortcut to get the wavelengths property + The wavelengths are given in [m] units and need not be sorted. """ + if 'wavelengths' not in self.get_properties(): + return None + return self._properties['wavelengths'] + + def set_wavelengths(self, wavelengths): + """ shortcut to set the wavelengths property """ + w_prop = {"wavelengths":wavelengths} + self.add_property(w_prop) + self._test_image() + + def get_properties(self): + return self._properties + + def add_property(self, newProperty): + """ add a new property(ies) to the existing properties """ + self._properties.update(newProperty) + self._test_image() + + def set_mask(self, mask): + """" applies a masked to the Msi. After this call, the image is of + type MaskedArray. If the image was already masked, the existing + masked will be "or ed" with the new mask. mask is a boolean array of + the same shape as self.get_image() + + Args: + mask: a mask of the same size as the image. 1s stand for pixels + masked out, 0s for pixels not masked.""" + if not isinstance(self.get_image(), np.ma.MaskedArray): + self.set_image(np.ma.masked_array(self.get_image(), mask, + fill_value=999999)) + else: + self.get_image()[mask] = np.ma.masked + + def __eq__(self, other): + """ + overrite the == operator + Two Msi s are the same if they contain the same image and properties. + Note: properties not implemented yet! + """ + if isinstance(other, Msi): + samesame = np.array_equal(other.get_image(), self.get_image()) + return samesame + return NotImplemented + + def __ne__(self, other): + """ != operator implemented by inverting to ==""" + result = self.__eq__(other) + if result is NotImplemented: + return result + return not result + + def _assure_basic_properties(self): + """ + helper method to automatically add the basic properties: + wavelength + to the msi if not added explicicly. basic wavelengths will just be + integers from 0 to 1 + """ + if self._image.size > 0 and ( + ("wavelengths" not in self._properties.keys() or + self._properties["wavelengths"].size == 0)): + self._properties["wavelengths"] = np.arange(self._image.shape[-1]) + if self._image.size == 0 and "wavelengths" not in self._properties.keys(): + self._properties["wavelengths"] = np.array([]) + + def _test_image(self): + """ + helper method which tests for the integrity of the msi. + E.g. the number of wavelengths must match the number of bands. + """ + # either both image and wavelength property are empty + if self._image.size == 0 and len(self._properties["wavelengths"]) != 0: + raise RuntimeError("dimension of image and wavelength mismatch: " + + "image size is zero, but wavelengths are set") + # or both are same + elif self._image.shape[-1] != len(self._properties["wavelengths"]): + raise RuntimeError("dimension of image and wavelength mismatch: " + + "image size and wavelenths do not match") + diff --git a/Modules/Biophotonics/python/iMC/msi/msimanipulations.py b/Modules/Biophotonics/python/iMC/msi/msimanipulations.py new file mode 100644 index 0000000000..9169fa35c1 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/msimanipulations.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 13 13:42:00 2015 + +@author: wirkert +""" + +import logging +import copy +import numpy as np + +from scipy.interpolate import interp1d + +from imgmani import collapse_image +import imgmani +from msi import Msi + + +''' +The msi manipulations module includes usefull convenience operations on msis. + +E.g. calculate_mean_spectrum to calculate the average spectrum on all the image +data or interpolate_wavelengths to change to a different wavelenght set by +simple interpolation. + +All methods take a msi and change it in place. They also return the same msi +object for convenience (can e.g. be used to chain msi manipulations). +''' + + +def apply_segmentation(msi, segmentation): + """ applies a segmentation to an msi. + + If the msi has imaging data of n x m x nr_wavelengths the segmentation + has to be a numpy array of n x m size. pixelvalues with values different + than zero will be included in the segmentation. + By applying the segmentation, not segmented elements will be np.ma.masked. + + Alternatively, one can input a msi with the mentioned n x m numpy array + in from of a msi as segmentation (for convenience to be able to use the + same reader for msis and segmentations) + """ + if (isinstance(segmentation, Msi)): + # expects just an image, but if a Msi is passed it's also ok + segmentation = segmentation.get_image() + segmentation = np.squeeze(segmentation) + mask = (0 == segmentation) + # mask needs to be expanded to cover all wavlengths + wholeMask = np.zeros_like(msi.get_image(), dtype="bool") + # doesn't seem elegant + for i in range(wholeMask.shape[-1]): + wholeMask[:, :, i] = mask + + msi.set_mask(wholeMask) + return msi + + +def calculate_mean_spectrum(msi): + """ reduce this image to only its mean spectrum. + If the msi.get_image() is a masked array these values will be ignored when + calculating the mean spectrum """ + # reshape to collapse all but last dimension (which contains reflectances) + collapsedImage = collapse_image(msi.get_image()) + msi.set_image(np.mean(collapsedImage, axis=0)) + # returns the same msi. + return msi + + +def interpolate_wavelengths(msi, newWavelengths): + """ interpolate image data to fit newWavelengths. Current implementation + performs simple linear interpolation. Neither existing nor new wavelengths + need to be sorted. """ + interpolator = interp1d(msi.get_wavelengths(), msi.get_image(), assume_sorted=False) + msi.set_image(interpolator(newWavelengths), wavelengths=newWavelengths) + return msi + + +def normalize_integration_times(msi): + """ divides by integration times """ + if ('integration times' not in msi.get_properties()): + logging.warn("warning: trying to normalize integration times for " + "image without the integration time property") + return msi + + original_shape = msi.get_image().shape + collapsed_image = collapse_image(msi.get_image()) + collapsed_image = collapsed_image / msi.get_properties()['integration times'] + msi.set_image(collapsed_image.reshape(original_shape)) + + msi.add_property({'integration times': + np.ones_like(msi.get_properties()['integration times'])}) + return msi + + +def dark_correction(msi, dark): + """" subtract dark current from multi spectral image. + + The dark msi should either be of the same shape + as msi or 1xnr_wavelengths (see tests).""" + msi.set_image(msi.get_image() - dark.get_image()) + return msi + + +def flatfield_correction(msi, flatfield): + """ divide by flatfield to remove dependencies on light source form and + imaging system. + + The flatfield msi should either be of the same shape + as msi or 1xnr_wavelengths (see tests).""" + # we copy the flatfield to ensure it is unchanged by the normalization + flatfield_copy = copy.copy(flatfield) + normalize_integration_times(flatfield_copy) + normalize_integration_times(msi) + + msi.set_image(msi.get_image() / flatfield_copy.get_image()) + return msi + + +def image_correction(msi, flatfield, dark): + """ do the usual image correction: + msi = ((msi - dark) / integration_time) / ((flatfield - dark) / integration_time) + this function changes only the msi, flatfield and dark image + are left unchanged. + """ + # we need a copy of flatfield since otherwise the dark correction + # changes the flatfield + dark_correction(msi, dark) + flatfield_copy = copy.copy(flatfield) + dark_correction(flatfield_copy, dark) + flatfield_correction(msi, flatfield_copy) + return msi + + +def get_bands(msi, bands): + """ + TODO SW: document and test + """ + msi.set_image(imgmani.get_bands(msi.get_image(), bands)) + if msi.get_wavelengths() is not None: + msi.set_wavelengths(msi.get_wavelengths()[bands]) + return msi + diff --git a/Modules/Biophotonics/python/iMC/msi/normalize.py b/Modules/Biophotonics/python/iMC/msi/normalize.py new file mode 100644 index 0000000000..880bbffed4 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/normalize.py @@ -0,0 +1,53 @@ + +import numpy as np +from sklearn.preprocessing import Normalizer + + +from imgmani import collapse_image + + +class Normalize(): + + def __init__(self): + pass + + def normalize(self, msi): + pass + + +class NormalizeIQ(Normalize): + """Normalize by image quotient""" + def __init__(self, iqBand=None): + if iqBand is None: + iqBand = 0 + self.iqBand = iqBand + + def normalize(self, msi): + # todo: guard if iqBand is outside of image dimension + original_shape = msi.get_image().shape + collapsed_image = collapse_image(msi.get_image()) + iqDimension = collapsed_image[ :, self.iqBand] + normalized_image = collapsed_image / iqDimension[:, None] + msi.set_image(np.reshape(normalized_image, original_shape)) + + +class NormalizeMean(Normalize): + """Normalize by image mean""" + def __init__(self): + pass + + def normalize(self, msi, norm="l1"): + original_shape = msi.get_image().shape + collapsed_image = collapse_image(msi.get_image()) + # temporarily save mask, since scipy normalizer removes mask + is_masked_array = isinstance(msi.get_image(), np.ma.MaskedArray) + if is_masked_array: + mask = msi.get_image().mask + normalizer = Normalizer(norm=norm) + normalized_image = normalizer.transform(collapsed_image) + if is_masked_array: + normalized_image = np.ma.MaskedArray(normalized_image, mask=mask) + msi.set_image(np.reshape(normalized_image, original_shape)) + + +standard_normalizer = NormalizeMean() diff --git a/Modules/Biophotonics/python/iMC/msi/plot.py b/Modules/Biophotonics/python/iMC/msi/plot.py new file mode 100644 index 0000000000..0ae653b2ce --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/plot.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 13 11:13:31 2015 + +@author: wirkert +""" + +import copy +import logging + +import numpy as np +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable + +import imgmani +import msimanipulations as msimani + +def plot(msi, axes=None, color=None): + """ + create a plot for the Msi with x axes being the wavelengths and + y-axes being the corresponding image values (e.g. reflectances, absorptions) + Takes image masks into account: + doesn't plot a spectrum containing masked elements + """ + if axes is None: + axes = plt.gca() + + sortedIndices = sorted(range(len(msi.get_wavelengths())), + key=lambda k: msi.get_wavelengths()[k]) + sortedWavelenghts = msi.get_wavelengths()[sortedIndices] + # reshape to collapse all but last dimension (which contains reflectances) + collapsedImage = imgmani.collapse_image(msi.get_image()) + # todo: simply use np.ma.compress_rows + + # print "filtered ", filteredImage.shape + i = 0 + for i in range(collapsedImage.shape[0]): + if (collapsedImage[i, 0] is not np.ma.masked): + axes.plot(sortedWavelenghts, + collapsedImage[i, :][sortedIndices], "-o", color=color) + + +def plot_images(msi): + """plot the images as a 2d image array, one image for + each wavelength.""" + nr_wavelengths = msi.get_image().shape[-1] + f, axarr = plt.subplots(1, nr_wavelengths) + for i, a in enumerate(axarr): + one_band_image = imgmani.get_bands(msi.get_image(), i) + im = a.imshow(np.squeeze(one_band_image)) + a.set_title("band nr " + str(i), fontsize=5) + divider_dsp = make_axes_locatable(a) + cax_dsp = divider_dsp.append_axes("right", size="10%", pad=0.1) + cbar = plt.colorbar(im, cax=cax_dsp) + cbar.ax.tick_params(labelsize=5) + a.xaxis.set_visible(False) + a.yaxis.set_visible(False) + + +def plotMeanError(msi, axes=None): + """ + create a plot for the Msi with x axes being the wavelengths and + y-axes being the corresponding mean image values + (e.g. reflectances, absorptions). Plots also standard deviation bands + Takes image masks into account: + doesn't plot a spectrum containing masked elements + """ + if axes is None: + axes = plt.gca() + # sort the wavelengths + sortedIndices = sorted(range(len(msi.get_wavelengths())), + key=lambda k: msi.get_wavelengths()[k]) + sortedWavelenghts = msi.get_wavelengths()[sortedIndices] + # copy the msi, since it will be altered (mean will be built) + msi_copy = copy.deepcopy(msi) + image = msi_copy.get_image() + image = imgmani.collapse_image(image) + std_curve = np.ma.std(image, axis=0) + msimani.calculate_mean_spectrum(msi_copy) + # calculate std + logging.info("percentual std: " + + str(std_curve / msi_copy.get_image() * 100.)) + # plot as errorbar + axes.errorbar(sortedWavelenghts, msi_copy.get_image()[sortedIndices], + yerr=std_curve, fmt='-o') diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/reader/__init__.py b/Modules/Biophotonics/python/iMC/msi/test/__init__.py similarity index 100% rename from Modules/Biophotonics/python/inverseMonteCarlo/reader/__init__.py rename to Modules/Biophotonics/python/iMC/msi/test/__init__.py diff --git a/Modules/Biophotonics/python/iMC/msi/test/helpers.py b/Modules/Biophotonics/python/iMC/msi/test/helpers.py new file mode 100644 index 0000000000..4565f7e63a --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/test/helpers.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 13 09:53:52 2015 + +@author: wirkert +""" + +import numpy as np +from msi.msi import Msi + + +def getFakeMsi(): + + # build a fake multispectral image with 5 dimensions. + image = np.concatenate((np.ones((5, 5, 1)), + np.ones((5, 5, 1)) * 2, + np.ones((5, 5, 1)) * 3, + np.ones((5, 5, 1)) * 4, + np.ones((5, 5, 1)) * 5), + axis=-1) + msi = Msi(image) + + msi.set_wavelengths(np.array([5, 4, 3, 2, 1])) + + return msi diff --git a/Modules/Biophotonics/python/iMC/msi/test/test_imgmani.py b/Modules/Biophotonics/python/iMC/msi/test/test_imgmani.py new file mode 100644 index 0000000000..c6960958ca --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/test/test_imgmani.py @@ -0,0 +1,96 @@ +''' +Created on Aug 28, 2015 + +@author: wirkert +''' +import unittest +import numpy as np + +import msi.msimanipulations as msimani +import msi.imgmani as imgmani +import msi.test.helpers as helpers +from msi.imgmani import remove_masked_elements, select_n_reflectances + + +class TestImgMani(unittest.TestCase): + + def setUp(self): + self.msi = helpers.getFakeMsi() + # set one pixel to special values + self.specialValue = np.arange(self.msi.get_image().shape[-1]) * 2 + self.msi.get_image()[2, 2, :] = self.specialValue + # create a segmentation which sets all elements to invalid but the + # one pixel with the special value + self.segmentation = np.zeros(self.msi.get_image().shape[0:-1]) + self.segmentation[2, 2] = 1 + # apply this segmentation to the msi + msimani.apply_segmentation(self.msi, self.segmentation) + self.image = self.msi.get_image() + + def tearDown(self): + self.msi = None + self.specialValue = None + self.segmentation = None + self.image = None + + def test_collapse_image(self): + image = self.image + newShapedImage = imgmani.collapse_image(image) + + self.assertEqual(newShapedImage.shape, + (image.shape[0] * image.shape[1], image.shape[2]), + "collapsed image has correct shape") + np.testing.assert_equal(newShapedImage[2 * 5 + 2, :], + self.msi.get_image()[2, 2, :], + "values have been correctly transformed") + + def test_collapse_image_retains_data(self): + newShapedImage = imgmani.collapse_image(self.image) + self.msi.get_image()[2, 2, 0] = 5000. + + self.assertEqual(newShapedImage[2 * 5 + 2, 0], 5000., + "collapse_image does not copy data") + + def test_remove_masked_elements(self): + value = self.msi.get_image()[2, 2, :] + image_without_masked = remove_masked_elements(self.image) + np.testing.assert_array_equal(image_without_masked[0, :], value, + "mask correctly removed") + self.assertEqual(image_without_masked.shape, + (1, self.image.shape[-1]), + "shape of image without mask correct") + + def test_select_n_reflectances_selects(self): + n = 10 + new_image = select_n_reflectances(self.image, n) + self.assertEqual(new_image.shape, (n, self.image.shape[-1]), + "correct shape after selection") + + def test_select_n_reflectances_permutes(self): + image_shape = self.image.shape + new_first_layer = np.random.random_sample(image_shape[0:-1]) + self.image[:, :, 0] = new_first_layer + shuffled_image = select_n_reflectances(self.image, + image_shape[0] * image_shape[1]) + # restore_shape + shuffled_image = np.reshape(shuffled_image, image_shape) + self.assertFalse(np.allclose(shuffled_image[:, :, 0], + new_first_layer), + "image has been shuffled") + + def test_get_bands_from_int(self): + new_image_bands = imgmani.get_bands(self.image, 2) + self.assertEqual(new_image_bands.shape, (5, 5, 1), + "new image has correct shape") + self.assertEqual(new_image_bands[2, 2, :], self.specialValue[2], + "new image has correct values") + + def test_get_bands_from_array(self): + new_image_bands = imgmani.get_bands(self.image, np.array([0, 1, 2])) + self.assertEqual(new_image_bands.shape, (5, 5, 3), + "new image has correct shape") + np.testing.assert_allclose(new_image_bands[2, 2, :], + self.specialValue[:3], + err_msg="new image has correct values") + + diff --git a/Modules/Biophotonics/python/iMC/msi/test/test_msi.py b/Modules/Biophotonics/python/iMC/msi/test/test_msi.py new file mode 100644 index 0000000000..128a0691df --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/test/test_msi.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Mar 27 19:12:40 2015 + +@author: wirkert +""" + +import unittest +import numpy as np + +from msi.msi import Msi +from msi.test import helpers + + +class TestMsi(unittest.TestCase): + + def setUp(self): + self.msi = helpers.getFakeMsi() + + def tearDown(self): + pass + + def test_create(self): + self.assertTrue(True, "Created msi during setup") + + def test_add_property(self): + self.msi.add_property({'test':np.array([1, 2, 3])}) + self.assertTrue(np.array_equal(self.msi.get_properties()['test'], + np.array([1, 2, 3])), "property successfully added to msi") + + def test_properties_not_shared(self): + msi1 = Msi() + msi2 = Msi() + msi1.add_property({"integration time": np.array([1, 2, 3])}) + + self.assertTrue('integration time' not in msi2.get_properties()) + + def test_add_dummy_wavelengths_automatically(self): + msi_no_wavelengths_set = Msi() + msi_no_wavelengths_set.set_image(self.msi.get_image()) + + nr_wavelengths = msi_no_wavelengths_set.get_image().shape[-1] + + np.testing.assert_equal(msi_no_wavelengths_set.get_wavelengths(), + np.arange(nr_wavelengths), + "correct dummy wavelength values set") + + + + diff --git a/Modules/Biophotonics/python/iMC/msi/test/test_msimanipulations.py b/Modules/Biophotonics/python/iMC/msi/test/test_msimanipulations.py new file mode 100644 index 0000000000..9b895a6cf4 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/test/test_msimanipulations.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 13 13:52:23 2015 + +@author: wirkert +""" + + +import unittest +import copy +import numpy as np + +from msi.test import helpers +import msi.msimanipulations as mani + + +class TestMsiManipulations(unittest.TestCase): + + def setUp(self): + self.msi = helpers.getFakeMsi() + self.specialmsi = helpers.getFakeMsi() + + # set one pixel to special values + self.specialValue = np.arange(self.specialmsi.get_image().shape[-1]) * 2 + self.specialmsi.get_image()[2, 2, :] = self.specialValue + + # create a segmentation which sets all elements to invalid but the + # one pixel with the special value + self.segmentation = np.zeros(self.specialmsi.get_image().shape[0:-1]) + self.segmentation[2, 2] = 1 + + def tearDown(self): + pass + + def test_apply_segmentation(self): + + mani.apply_segmentation(self.specialmsi, self.segmentation) + + validImageEntries = self.specialmsi.get_image() \ + [~self.specialmsi.get_image().mask] + + np.testing.assert_equal(validImageEntries, self.specialValue, + "image has been correctly segmented") + + def test_calculate_mean_spectrum(self): + + mani.calculate_mean_spectrum(self.specialmsi) + + np.testing.assert_equal(np.array([0.96, 2., 3.04, 4.08, 5.12]), + self.specialmsi.get_image(), + "mean spectrum is correctly calculated on image with " + + "no mask applied") + + def test_calculate_mean_spectrum_masked_image(self): + + mani.apply_segmentation(self.specialmsi, self.segmentation) + mani.calculate_mean_spectrum(self.specialmsi) + + np.testing.assert_equal(self.specialValue, self.specialmsi.get_image(), + "mean spectrum is correctly calculated on image with " + + "mask applied") + + def test_interpolate(self): + # create not sorted new wavelengths + newWavelengths = np.array([4.0, 2.5, 3.5, 1.5]) + mani.interpolate_wavelengths(self.msi, newWavelengths) + + np.testing.assert_equal(newWavelengths, self.msi.get_wavelengths(), + "wavelengths correctly updated") + # check if first image pixel was correctly calculated + # (hopefully true for all then) + np.testing.assert_equal(np.array([2.0, 3.5, 2.5, 4.5]), + self.msi.get_image()[0, 0, :], + "image elements correctly interpolated") + + def test_normalize_integration_times(self): + old_shape = self.msi.get_image().shape + integration_times = np.array([1., 2., 3., 4., 5.]) + self.msi.add_property({'integration times': integration_times}) + mani.normalize_integration_times(self.msi) + + np.testing.assert_equal(self.msi.get_image()[1, 3, :], + np.ones_like(integration_times), + "normalized integration times") + np.testing.assert_equal(self.msi.get_properties()['integration times'], + np.ones_like(integration_times), + "integration time property set to ones") + self.assertEqual(self.msi.get_image().shape, old_shape, + "shape did not change from normalizing") + + def test_normalize_integration_times_none_given(self): + msi_copy = copy.deepcopy(self.msi) + mani.normalize_integration_times(msi_copy) + np.testing.assert_equal(msi_copy.get_image(), self.msi.get_image(), + "nothing change by normalizing without" + \ + "integration times given") + + def test_dark_correction(self): + desired_image_data = copy.copy(self.msi.get_image()) + desired_image_data -= 1 + + dark = copy.copy(self.msi) + dark.set_image(np.ones_like(dark.get_image())) + + mani.dark_correction(self.msi, dark) + + np.testing.assert_equal(self.msi.get_image(), + desired_image_data, + "dark image correctly accounted for") + np.testing.assert_equal(dark.get_image(), + np.ones_like(dark.get_image()), + "dark image unchanged by dark correction") + + def test_dark_correction_with_single_value(self): + desired_image_data = copy.copy(self.specialmsi.get_image()) + desired_image_data -= 1 + + dark = copy.copy(self.specialmsi) + dark.set_image(np.ones_like(dark.get_image())) + mani.calculate_mean_spectrum(dark) + + mani.dark_correction(self.specialmsi, dark) + + np.testing.assert_equal(self.specialmsi.get_image(), + desired_image_data, + "dark image correctly accounted for from singular dark value") + np.testing.assert_equal(dark.get_image(), + np.ones_like(dark.get_image()), + "dark image unchanged by dark correction") + + def test_flatfield_correction(self): + desired_image_data = np.ones_like(self.specialmsi.get_image()) + desired_image_data[2, 2, 0] = np.nan + + mani.flatfield_correction(self.specialmsi, self.specialmsi) + + np.testing.assert_equal(self.specialmsi.get_image(), + desired_image_data, + "correct image by itself should lead to only 1s ") + + def test_flatfield_correction_differing_integration_times(self): + MSI_INTEGRATION_TIME = 3.0 + FLATFIELD_INTEGRATION_TIME = 2.0 + desired_image_data = np.ones_like(self.specialmsi.get_image()) * \ + FLATFIELD_INTEGRATION_TIME / MSI_INTEGRATION_TIME + desired_image_data[2, 2, 0] = np.nan + self.specialmsi.add_property({"integration times": + np.ones_like( + self.specialmsi.get_image()[0, 0, :]) + * MSI_INTEGRATION_TIME}) + flatfield = copy.deepcopy(self.specialmsi) + flatfield.add_property({"integration times": + np.ones_like( + flatfield.get_image()[0, 0, :]) + * FLATFIELD_INTEGRATION_TIME}) + # for testing if flatfield does not changed by correction we copy it + flatfield_copy = copy.deepcopy(flatfield) + + mani.flatfield_correction(self.specialmsi, flatfield_copy) + + np.testing.assert_almost_equal(self.specialmsi.get_image(), + desired_image_data, 15, + "corrected image is a division of integration times") + np.testing.assert_equal(flatfield.get_image(), + flatfield_copy.get_image(), + "flatfield doesn't change by correction") + + def test_flatfield_correction_with_single_value(self): + desired_image_data = np.ones_like(self.msi.get_image()) + flatfield = copy.copy(self.msi) + mani.calculate_mean_spectrum(flatfield) + unchanged_flatfield = copy.deepcopy(flatfield) + + mani.flatfield_correction(self.msi, flatfield) + + np.testing.assert_equal(self.msi.get_image(), + desired_image_data, + "flatfield correctly accounted for from singular reference value") + np.testing.assert_equal(flatfield, unchanged_flatfield, + "flatfield not changed by algorithm") + + def test_image_correction(self): + dark = copy.copy(self.msi) + dark.set_image(np.ones_like(dark.get_image()) * 0.5) + flatfield = copy.copy(self.msi) + flatfield_copy = copy.deepcopy(flatfield) + dark_copy = copy.deepcopy(dark) + + mani.image_correction(self.msi, flatfield, dark) + + np.testing.assert_equal(flatfield.get_image(), + flatfield_copy.get_image(), + "image correction didn't change flatfield") + np.testing.assert_equal(dark.get_image(), dark_copy.get_image(), + "image correction didn't change dark image") + np.testing.assert_almost_equal(self.msi.get_image(), + np.ones_like(self.msi.get_image()), + 15, "image correctly corrected :-)") + + + + + diff --git a/Modules/Biophotonics/python/iMC/msi/test/test_msireaderwriter.py b/Modules/Biophotonics/python/iMC/msi/test/test_msireaderwriter.py new file mode 100644 index 0000000000..a3bed221b0 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/test/test_msireaderwriter.py @@ -0,0 +1,40 @@ +''' +Created on Aug 25, 2015 + +@author: wirkert +''' +import unittest +import os +import numpy as np + +import msi.test.helpers as helpers +from msi.io.msiwriter import MsiWriter +from msi.io.msireader import MsiReader + + +class Test(unittest.TestCase): + + def setUp(self): + self.msi = helpers.getFakeMsi() + self.test_file_path = "test_msi.msi" + + def tearDown(self): + # remove the hopefully written file + os.remove(self.test_file_path) + + def test_read_and_write(self): + reader = MsiReader() + writer = MsiWriter(self.msi) + writer.write(self.test_file_path) + read_msi = reader.read(self.test_file_path) + + np.testing.assert_array_equal(self.msi.get_image(), + read_msi.get_image(), + "data array of msi stays same" + + "after read and write") + np.testing.assert_array_equal( + self.msi.get_properties()["wavelengths"], + read_msi.get_properties()["wavelengths"], + "properties of msi stay same after read and write") + + diff --git a/Modules/Biophotonics/python/iMC/msi/test/test_normalize.py b/Modules/Biophotonics/python/iMC/msi/test/test_normalize.py new file mode 100644 index 0000000000..72f2a844c6 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/test/test_normalize.py @@ -0,0 +1,74 @@ +''' +Created on Aug 20, 2015 + +@author: wirkert +''' +import unittest +import numpy as np + +import msi.normalize as norm +import msi.test.helpers as helpers + + +class TestNormalize(unittest.TestCase): + + def setUp(self): + self.specialmsi = helpers.getFakeMsi() + # set one pixel to special values + self.specialValue = np.arange(self.specialmsi.get_image().shape[-1]) * 2 + self.specialmsi.get_image()[2, 2, :] = self.specialValue + + def tearDown(self): + pass + + def test_normalizeIQ(self): + original_shape = self.specialmsi.get_image().shape # shape should stay + # value 4.0 is in band 3 + desired_matrix = self.specialmsi.get_image() / 4.0 + # except for special value, where it is 8 + desired_matrix[2, 2, :] = self.specialmsi.get_image()[2, 2, :] / 6.0 + # the same after normalization + iq_normalizer = norm.NormalizeIQ(3) + iq_normalizer.normalize(self.specialmsi) + + self.assertEqual(self.specialmsi.get_image().shape, original_shape, + "shape not changed by normalization") + np.testing.assert_equal(self.specialmsi.get_image(), + desired_matrix, + "msi correctly normalized by iq") + + def test_normalizeMean(self): + original_shape = self.specialmsi.get_image().shape # shape should stay + desired_matrix = self.specialmsi.get_image() / 15.0 + desired_matrix[2, 2, :] = self.specialmsi.get_image()[2, 2, :] / 20.0 + # the same after normalization + mean_normalizer = norm.NormalizeMean() + mean_normalizer.normalize(self.specialmsi) + + self.assertEqual(self.specialmsi.get_image().shape, original_shape, + "shape not changed by normalization") + np.testing.assert_equal(self.specialmsi.get_image(), + desired_matrix, + "msi correctly normalized by mean") + + def test_normalizeMean_with_masked_elemnets(self): + original_shape = self.specialmsi.get_image().shape # shape should stay + # set mask so it masks the special value + mask = np.zeros_like(self.specialmsi.get_image()) + mask [2, 2, :] = 1 + mask = mask.astype(bool) + masked_msi_image = np.ma.MaskedArray(self.specialmsi.get_image(), + mask=mask) + self.specialmsi.set_image(masked_msi_image) + desired_matrix = masked_msi_image / 15.0 + # the same after normalization + mean_normalizer = norm.NormalizeMean() + mean_normalizer.normalize(self.specialmsi) + + self.assertEqual(self.specialmsi.get_image().shape, original_shape, + "shape not changed by normalization") + np.testing.assert_equal(self.specialmsi.get_image(), + desired_matrix, + "msi correctly normalized by mean") + np.testing.assert_equal(mask, self.specialmsi.get_image().mask) + diff --git a/Modules/Biophotonics/python/iMC/msi/test/test_nrrdreader.py b/Modules/Biophotonics/python/iMC/msi/test/test_nrrdreader.py new file mode 100644 index 0000000000..da5083a125 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/test/test_nrrdreader.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Aug 10 16:43:31 2015 + +@author: wirkert +""" + +import unittest +from msi.io.nrrdreader import NrrdReader +import numpy as np + + +class TestNrrdReader(unittest.TestCase): + + def setUp(self): + self.nrrdReader = NrrdReader() + self.msi = self.nrrdReader.read('./msi/data/testMsi.nrrd') + + def test_read_does_not_crash(self): + # if we got this far, at least an image was read. + self.assertTrue(len(self.msi.get_image().shape) == 3, + "read image has correct basic shape dimensions") + self.assertTrue(self.msi.get_image().shape[-1] == 5, + "read image has correct number of image stacks") + self.assertTrue(np.array_equal(self.msi.get_image()[2, 2, :], + np.array([1, 2, 3, 4, 5])), + "read image contains correct data") + + def test_read_non_existing_image_returns_exception(self): + with self.assertRaises(RuntimeError): + self.nrrdReader.read("./msi/data/asdf.nrrd") diff --git a/Modules/Biophotonics/python/iMC/msi/test/test_nrrdwriter.py b/Modules/Biophotonics/python/iMC/msi/test/test_nrrdwriter.py new file mode 100644 index 0000000000..c442b76159 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/test/test_nrrdwriter.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 13 09:52:47 2015 + +@author: wirkert +""" + +import unittest +import os +import numpy as np + +import msi.msimanipulations as msimani +from msi.io.nrrdreader import NrrdReader +from msi.io.nrrdwriter import NrrdWriter +from msi.test import helpers + + +class TestNrrdWriter(unittest.TestCase): + + def setUp(self): + # setup file and the path where it shall be written to + self.msi = helpers.getFakeMsi() + self.fileUriToWrite = "testfile.nrrd" + + def tearDown(self): + # remove the hopefully written file + os.remove(self.fileUriToWrite) + + def test_imageWriterCreatesFile(self): + writer = NrrdWriter(self.msi) + writer.write(self.fileUriToWrite) + self.assertTrue(os.path.isfile(self.fileUriToWrite), + "file was written to disk") + + def test_imageWriterCreatesCorrectFile(self): + + writer = NrrdWriter(self.msi) + writer.write(self.fileUriToWrite) + + reader = NrrdReader() + msi = reader.read(self.fileUriToWrite) + self.assertTrue(msi == helpers.getFakeMsi(), + "image correctly written and read") + + def test_write_one_d_image_works(self): + writer = NrrdWriter(self.msi) + msimani.calculate_mean_spectrum(self.msi) + writer.write(self.fileUriToWrite) + + reader = NrrdReader() + msi = reader.read(self.fileUriToWrite) + np.testing.assert_array_equal(msi.get_image(), + np.array([1, 2, 3, 4, 5]), + "1d image correctly written and read") diff --git a/Modules/Biophotonics/python/iMC/msi/test/test_spectrometerreader.py b/Modules/Biophotonics/python/iMC/msi/test/test_spectrometerreader.py new file mode 100644 index 0000000000..b993a434bf --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/test/test_spectrometerreader.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 7 18:02:08 2015 + +@author: wirkert +""" + +import unittest + +from msi.io.spectrometerreader import SpectrometerReader + + +class TestSpectrometer(unittest.TestCase): + + def setUp(self): + self.exampleFileName = "./msi/data/Transmission_15-49-35-978_filter700nm.txt" + self.reader = SpectrometerReader() + + def tearDown(self): + pass + + def test_create(self): + self.assertTrue(True, "Created empty reader during setup") + + def test_read_spectrum(self): + msi = self.reader.read(self.exampleFileName) + + self.assertAlmostEqual(msi.get_image()[0], + 70.50, + msg="first spectral element is read correctly") + self.assertAlmostEqual(msi.get_image()[-1], + 68.13, + msg="last sprectral element is read correctly") + self.assertTrue(msi.get_image().size == 2048, + "correct number of elements read") + + def test_read_wavelengths(self): + msi = self.reader.read(self.exampleFileName) + + self.assertAlmostEqual(msi.get_wavelengths()[0], + 187.255 * 10 ** -9, + msg="first wavelength element is read correctly") + self.assertAlmostEqual(msi.get_wavelengths()[-1], + 1103.852 * 10 ** -9, + msg="last wavelength element is read correctly") + self.assertTrue(msi.get_wavelengths().size == 2048, + "correct number of elements read") diff --git a/Modules/Biophotonics/python/iMC/msi/test/test_tiffwriter.py b/Modules/Biophotonics/python/iMC/msi/test/test_tiffwriter.py new file mode 100644 index 0000000000..a2e880b80c --- /dev/null +++ b/Modules/Biophotonics/python/iMC/msi/test/test_tiffwriter.py @@ -0,0 +1,40 @@ + +import unittest +import os + +from msi.io.tiffwriter import TiffWriter +from msi.io.tiffreader import TiffReader +from msi.test import helpers + + +class TestTiffWriter(unittest.TestCase): + + def setUp(self): + # setup file and the path where it shall be written to + self.msi = helpers.getFakeMsi() + self.msi.set_image(self.msi.get_image()) + self.fileUriToWrite = os.path.join(os.getcwd(), "testfiles") + + def tearDown(self): + # remove the hopefully written files + folder, file_prefix = os.path.split(self.fileUriToWrite) + image_files = [f for f in os.listdir(folder) if + os.path.isfile(os.path.join(folder, f))] + image_files = [f for f in image_files if f.startswith(file_prefix)] + # expand to full path + image_files = [os.path.join(folder, f) for f in image_files] + for f in image_files: + os.remove(f) + + def test_imageWriterCreatesFile(self): + writer = TiffWriter(self.msi, convert_to_nm=False) + writer.write(self.fileUriToWrite) + + def test_imageWriterCreatesCorrectFile(self): + writer = TiffWriter(self.msi, convert_to_nm=False) + writer.write(self.fileUriToWrite) + + reader = TiffReader(shift_bits=0) + msi = reader.read(self.fileUriToWrite) + self.assertTrue(msi == helpers.getFakeMsi(), + "image correctly written and read") diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/setup/__init__.py b/Modules/Biophotonics/python/iMC/regression/__init__.py similarity index 100% rename from Modules/Biophotonics/python/inverseMonteCarlo/setup/__init__.py rename to Modules/Biophotonics/python/iMC/regression/__init__.py diff --git a/Modules/Biophotonics/python/iMC/regression/domain_adaptation.py b/Modules/Biophotonics/python/iMC/regression/domain_adaptation.py new file mode 100644 index 0000000000..316f378aa9 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/regression/domain_adaptation.py @@ -0,0 +1,88 @@ +''' +Created on Oct 20, 2015 + +@author: wirkert +''' + +import numpy as np +from sklearn.cross_validation import KFold +from sklearn.grid_search import GridSearchCV +from sklearn.linear_model.logistic import LogisticRegressionCV +from sklearn.ensemble.forest import RandomForestClassifier + + +def prepare_data_for_weights_estimation(X_s, X_t): + nr_s = X_s.shape[0] + nr_t = X_t.shape[0] + source_labels = np.zeros(nr_s) + target_labels = np.ones(nr_t) + X_all = np.concatenate((X_s, X_t)) + all_labels = np.concatenate((source_labels, target_labels)) + return X_all, all_labels + + +def estimate_weights_random_forests(X_s, X_t, X_w): + + X_all, all_labels = prepare_data_for_weights_estimation(X_s, X_t) + # train logistic regression + kf = KFold(X_all.shape[0], 10, shuffle=True) + param_grid_rf = [ + {"n_estimators": np.array([500]), + "max_depth": np.array([6]), + # "max_features": np.array([1, 2, 4, 8, 16]), + "min_samples_leaf": np.array([100])}] + rf = GridSearchCV(RandomForestClassifier(50, max_depth=10, + class_weight="auto", n_jobs=-1), + param_grid_rf, cv=kf, n_jobs=-1) + rf = RandomForestClassifier(100, max_depth=6, min_samples_leaf=200, + class_weight="auto", n_jobs=-1) + rf.fit(X_all, all_labels) + # print "best parameters for rf weights determination: ", rf.best_estimator_ + probas = rf.predict_proba(X_w) + weights = probas[:, 1] / probas[:, 0] + return weights + + +def estimate_weights_logistic_regresssion(X_s, X_t): + """ estimate a logistic regressor to predict the probability of a sample + to be generated by one class or the other. + If one class is over or under represented weights will be adapted. + + Parameters: + X_s: samples from the source domain + X_t: samples from the target domain + + Returns: + weigths for X_s """ + X_all, all_labels = prepare_data_for_weights_estimation(X_s, X_t) + + kf = KFold(X_all.shape[0], 10, shuffle=True) + best_lr = LogisticRegressionCV(class_weight="auto", + Cs=np.logspace(4, 8, 10), + fit_intercept=False) + best_lr.fit(X_all, all_labels) + + weights = X_s.shape[0] / X_t.shape[0] * np.exp(np.dot(X_s, best_lr.coef_.T) + + best_lr.intercept_) + return weights + + +def resample(X, y, w, nr_samples=None): + """bootstrapping: resample with replacement according to weights + + Returns: + (X_new, w_new): the chosen samples and the new weights. + by design these new weights are all equal to 1.""" + if (nr_samples is None): + nr_samples = X.shape[0] + w = w / np.sum(w) # normalize + # create index array with samples to draw: + total_nr_samples = X.shape[0] # nr total samples + chosen_samples = np.random.choice(total_nr_samples, + size=nr_samples, + replace=True, p=np.squeeze(w)) + if y.ndim == 1: + y_chosen = y[chosen_samples] + else: + y_chosen = y[chosen_samples, :] + return X[chosen_samples, :], y_chosen, np.ones(nr_samples) diff --git a/Modules/Biophotonics/python/iMC/regression/estimation.py b/Modules/Biophotonics/python/iMC/regression/estimation.py new file mode 100644 index 0000000000..b4c9dd1c59 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/regression/estimation.py @@ -0,0 +1,134 @@ + + +""" + +The MultiSpectral Imaging Toolkit (MSITK) + +Copyright (c) German Cancer Research Center, +Computer Assisted Interventions. +All rights reserved. + +This software is distributed WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. + +See LICENSE for details + +""" +''' +Created on Oct 21, 2015 + +@author: wirkert +''' + +import math +import logging +import time + +import tensorflow as tf +import numpy as np +import SimpleITK as sitk + +from regression.tensorflow_estimator import multilayer_perceptron, cnn +import msi.imgmani as imgmani + + +def SAMDistance(x, y): + return math.acos(np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y))) + + +def estimate_image(msi, regressor): + """given an Msi and an regressor estimate the parmaeters for this image + + Paramters: + msi: multi spectral image + regressor: regressor, must implement the predict method""" + + # estimate parameters + collapsed_msi = imgmani.collapse_image(msi.get_image()) + # in case of nan values: set to 0 + collapsed_msi[np.isnan(collapsed_msi)] = 0. + collapsed_msi[np.isinf(collapsed_msi)] = 0. + + start = time.time() + estimated_parameters = regressor.predict(collapsed_msi) + end = time.time() + estimation_time = end - start + logging.info("time necessary for estimating image parameters: " + + str(estimation_time) + "s") + # restore shape + feature_dimension = 1 + if len(estimated_parameters.shape) > 1: + feature_dimension = estimated_parameters.shape[-1] + + estimated_paramters_as_image = np.reshape( + estimated_parameters, (msi.get_image().shape[0], + msi.get_image().shape[1], + feature_dimension)) + # save as sitk nrrd. + sitk_img = sitk.GetImageFromArray(estimated_paramters_as_image, + isVector=True) + return sitk_img, estimation_time + + +def estimate_image_tensorflow(msi, model_checkpoint_dir): + # estimate parameters + collapsed_msi = imgmani.collapse_image(msi.get_image()) + # in case of nan values: set to 0 + collapsed_msi[np.isnan(collapsed_msi)] = 0. + collapsed_msi[np.isinf(collapsed_msi)] = 0. + + + tf.reset_default_graph() + + keep_prob = tf.placeholder("float") + nr_wavelengths = len(msi.get_wavelengths()) + x = tf.placeholder("float", [None, nr_wavelengths, 1, 1]) + + x_test_image = np.reshape(msi.get_image(), [-1, nr_wavelengths, 1, 1]) + + # Construct the desired model + # pred, regularizers = multilayer_perceptron(x, nr_wavelengths, 100, 1, + # keep_prob) + pred = cnn(x, 1, keep_prob) + + # Initializing the variables + init = tf.initialize_all_variables() + + saver = tf.train.Saver() + + with tf.Session() as sess: + sess.run(tf.initialize_all_variables()) + # restore model: + ckpt = tf.train.get_checkpoint_state(model_checkpoint_dir) + + if ckpt and ckpt.model_checkpoint_path: + saver.restore(sess, ckpt.model_checkpoint_path) + + start = time.time() + estimated_parameters = pred.eval({x: x_test_image, + keep_prob:1.0}) + end = time.time() + estimation_time = end - start + logging.info("time necessary for estimating image parameters: " + + str(estimation_time) + "s") + # restore shape + feature_dimension = 1 + if len(estimated_parameters.shape) > 1: + feature_dimension = estimated_parameters.shape[-1] + + estimated_paramters_as_image = np.reshape( + estimated_parameters, (msi.get_image().shape[0], + msi.get_image().shape[1], + feature_dimension)) + # save as sitk nrrd. + sitk_img = sitk.GetImageFromArray(estimated_paramters_as_image, + isVector=True) + + return sitk_img, estimation_time + + +def standard_score(estimator, X, y): + """our standard scoring method is the median absolute error""" + return np.median(np.abs(estimator.predict(X) - y)) + diff --git a/Modules/Biophotonics/python/iMC/regression/linear.py b/Modules/Biophotonics/python/iMC/regression/linear.py new file mode 100644 index 0000000000..dcd96977a7 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/regression/linear.py @@ -0,0 +1,95 @@ +''' +Created on Oct 19, 2015 + +@author: wirkert +''' + +import numpy as np +from scipy.interpolate import interp1d + +from mc.usuag import get_haemoglobin_extinction_coefficients + +class LinearSaO2Unmixing(object): + ''' + classdocs + ''' + + def __init__(self): + # oxygenated haemoglobin extinction coefficients + eHb02 = 0 + eHb = 0 + + # oxygenated haemoglobin extinction coefficients + eHbO2 = np.array([34772.8, + 27840.93333, + 23748.8 , + 21550.8 , + 21723.46667, + 28064.8 , + 39131.73333, + 45402.93333, + 42955.06667, + 40041.73333, + 42404.4 , + 36333.6 , + 22568.26667, + 6368.933333, + 1882.666667, + 1019.333333, + 664.6666667, + 473.3333333, + 376.5333333, + 327.2 , + 297.0666667],) + # deoxygenated haemoglobin extinction coefficients + eHb = [18031.73333 , + 15796.8 , + 17365.33333 , + 21106.53333 , + 26075.06667 , + 32133.2 , + 39072.66667 , + 46346.8 , + 51264 , + 50757.33333 , + 45293.33333 , + 36805.46667 , + 26673.86667 , + 17481.73333 , + 10210.13333 , + 7034 , + 5334.533333 , + 4414.706667 , + 3773.96 , + 3257.266667 , + 2809.866667] + nr_total_wavelengths = len(eHb) + # to account for scattering losses we allow a constant offset + scattering = np.ones(nr_total_wavelengths) + # put eHbO2, eHb and scattering term in one measurement matrix + self.H = np.vstack((eHbO2, eHb, scattering)).T + self.lsq_solution_matrix = np.dot(np.linalg.inv(np.dot(self.H.T, + self.H)), + self.H.T) + + + def fit(self, X, y, weights=None): + """only implemented to fit to the standard sklearn framework.""" + pass + + def predict(self, X): + """predict like in sklearn: + + Parameters: + X: nrsamples x nr_features matrix of samples to predict for + regression + + Returns: + y: array of shape [nr_samples] with values for predicted + oxygenation """ + # do least squares estimation + oxy_test, deoxy, s = np.dot(self.lsq_solution_matrix, X.T) + # calculate oxygenation = oxygenated blood / total blood + saO2 = oxy_test / (oxy_test + deoxy) + + return np.clip(saO2, 0., 1.) diff --git a/Modules/Biophotonics/python/iMC/regression/preprocessing.py b/Modules/Biophotonics/python/iMC/regression/preprocessing.py new file mode 100644 index 0000000000..494f7a82db --- /dev/null +++ b/Modules/Biophotonics/python/iMC/regression/preprocessing.py @@ -0,0 +1,94 @@ +''' +Created on Oct 26, 2015 + +@author: wirkert +''' + +import numpy as np +import pandas as pd +from sklearn.preprocessing import Normalizer + + +def preprocess2(df, nr_samples=None, snr=None, movement_noise_sigma=None, + magnification=None, bands_to_sortout=None): + + # first set 0 reflectances to nan + df["reflectances"] = df["reflectances"].replace(to_replace=0., + value=np.nan) + # remove nan + df.dropna(inplace=True) + + # extract nr_samples samples from data + if nr_samples is not None: + df = df.sample(nr_samples) + + # get reflectance and oxygenation + X = df.reflectances + if bands_to_sortout is not None and bands_to_sortout.size > 0: + X.drop(X.columns[bands_to_sortout], axis=1, inplace=True) + snr = np.delete(snr, bands_to_sortout) + X = X.values + y = df.layer0[["sao2", "vhb"]] + + # do data magnification + if magnification is not None: + X_temp = X + y_temp = y + for i in range(magnification - 1): + X = np.vstack((X, X_temp)) + y = pd.concat([y, y_temp]) + + # add noise to reflectances + camera_noise = 0. + if snr is not None: + sigmas = X / snr + noises = np.random.normal(loc=0., scale=1, size=X.shape) + camera_noise = sigmas*noises + + movement_noise = 0. + if movement_noise_sigma is not None: + nr_bands = X.shape[1] + nr_samples = X.shape[0] + # we assume no correlation between neighboring bands + CORRELATION_COEFFICIENT = 0.0 + movement_variance = movement_noise_sigma ** 2 + movement_variances = np.ones(nr_bands) * movement_variance + movement_covariances = np.ones(nr_bands-1) * CORRELATION_COEFFICIENT * \ + movement_variance + movement_covariance_matrix = np.diag(movement_variances) + \ + np.diag(movement_covariances, -1) + \ + np.diag(movement_covariances, 1) + # percentual sample errors + sample_errors_p = np.random.multivariate_normal(mean=np.zeros(nr_bands), + cov=movement_covariance_matrix, + size=nr_samples) + # errors w.r.t. the curve height. + movement_noise = X * sample_errors_p + + X += camera_noise + movement_noise + + X = np.clip(X, 0.00001, 1.) + # do normalizations + X = normalize(X) + return X, y + + +def preprocess(batch, nr_samples=None, snr=None, movement_noise_sigma=None, + magnification=None, bands_to_sortout=None): + X, y = preprocess2(batch, nr_samples, snr, movement_noise_sigma, + magnification, bands_to_sortout) + + return X, y["sao2"] + + +def normalize(X): + # normalize reflectances + normalizer = Normalizer(norm='l1') + X = normalizer.transform(X) + # reflectances to absorption + absorptions = -np.log(X) + X = absorptions + # get rid of sorted out bands + normalizer = Normalizer(norm='l2') + X = normalizer.transform(X) + return X diff --git a/Modules/Biophotonics/python/iMC/regression/tensorflow_dataset.py b/Modules/Biophotonics/python/iMC/regression/tensorflow_dataset.py new file mode 100644 index 0000000000..4bc2065aac --- /dev/null +++ b/Modules/Biophotonics/python/iMC/regression/tensorflow_dataset.py @@ -0,0 +1,83 @@ +"""Functions for downloading and reading ipcai data.""" +from __future__ import print_function + +import os + +import numpy +import pandas as pd + +from regression.preprocessing import preprocess + + +class DataSet(object): + def __init__(self, images, labels, fake_data=False): + if fake_data: + self._num_examples = 10000 + else: + assert images.shape[0] == labels.shape[0], ( + "images.shape: %s labels.shape: %s" % (images.shape, + labels.shape)) + self._num_examples = images.shape[0] + images = images.astype(numpy.float32) + self._images = images + self._labels = labels + if self._labels.ndim == 1: + self._labels = self._labels[:, numpy.newaxis] + self._epochs_completed = 0 + self._index_in_epoch = 0 + + @property + def images(self): + return self._images + + @property + def labels(self): + return self._labels + + @property + def num_examples(self): + return self._num_examples + + @property + def epochs_completed(self): + return self._epochs_completed + + def next_batch(self, batch_size, fake_data=False): + """Return the next `batch_size` examples from this data set.""" + if fake_data: + fake_image = [1.0 for _ in xrange(784)] + fake_label = 0 + return [fake_image for _ in xrange(batch_size)], [ + fake_label for _ in xrange(batch_size)] + start = self._index_in_epoch + self._index_in_epoch += batch_size + if self._index_in_epoch > self._num_examples: + # Finished epoch + self._epochs_completed += 1 + # Shuffle the data + perm = numpy.arange(self._num_examples) + numpy.random.shuffle(perm) + self._images = self._images[perm] + self._labels = self._labels[perm] + + # Start next epoch + start = 0 + self._index_in_epoch = batch_size + assert batch_size <= self._num_examples + end = self._index_in_epoch + return self._images[start:end], self._labels[start:end] + + +def read_data_set(dataframe_filename, fake_data=False): + + if fake_data: + data_set = DataSet([], [], fake_data=True) + return data_set + + df_data_set = pd.read_csv(os.path.join(dir, dataframe_filename), + header=[0, 1]) + + data_set_images, data_set_labels = preprocess(df_data_set, snr=10.) + data_set_labels = data_set_labels.values + data_set = DataSet(data_set_images, data_set_labels) + return data_set diff --git a/Modules/Biophotonics/python/iMC/regression/tensorflow_estimator.py b/Modules/Biophotonics/python/iMC/regression/tensorflow_estimator.py new file mode 100644 index 0000000000..b5025563ec --- /dev/null +++ b/Modules/Biophotonics/python/iMC/regression/tensorflow_estimator.py @@ -0,0 +1,88 @@ + + +import tensorflow as tf + + +def weight_variable(shape): + initial = tf.truncated_normal(shape, stddev=0.1) + return tf.Variable(initial) + + +def bias_variable(shape): + initial = tf.constant(0.1, shape=shape) + return tf.Variable(initial) + + +def conv2d(x, W, padding='SAME'): + return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding=padding) + + +def max_pool1d(x, poolsize=2): + return tf.nn.max_pool(x, ksize=[1, poolsize, 1, 1], + strides=[1, poolsize, 1, 1], padding='SAME') + + +def add_cnn_layer(input, n_inputs, n_outputs, kernel_size, padding='SAME'): + #w = weight_variable([n_inputs, n_outputs]) + #b = bias_variable([n_outputs]) + W = weight_variable([kernel_size, 1, n_inputs, n_outputs]) + b = bias_variable([n_outputs]) + # Hidden layer with RELU activation + #new_layer = tf.nn.relu(tf.add(tf.matmul(input, w), b)) + h_conv = tf.nn.relu(conv2d(input, W, padding=padding) + b) + # Add dropout regularization + #new_layer_with_dropout = tf.nn.dropout(new_layer, keep_prob) + h_pool = max_pool1d(h_conv) + return h_pool, W + + +def add_fully_connected_layer(_X, n_inputs, n_outputs, keep_prob): + W = weight_variable([n_inputs, n_outputs]) + b = bias_variable([n_outputs]) + # Hidden layer with RELU activation + new_layer = tf.nn.relu(tf.add(tf.matmul(_X, W), b)) + # Add dropout regularization + new_layer_with_dropout = tf.nn.dropout(new_layer, keep_prob) + + return new_layer_with_dropout, W + + +# this is my exemplary convolutional network +def cnn(_X, n_classes, keep_prob): + # two convolutional layers + layer_1, _ = add_cnn_layer(_X, 1, 32, 3, padding='VALID') + layer_2, _ = add_cnn_layer(layer_1, 32, 32, 2, padding='VALID') + # flatten last one to be able to apply it to fully connected layer + final_number_of_dimensions = 1*32 + layer_2_flat = tf.reshape(layer_2, [-1, final_number_of_dimensions]) + + # fully connected layer to bring information together + fc_dim = 5 + h_fc1_drop, _ = add_fully_connected_layer(layer_2_flat, + final_number_of_dimensions, + fc_dim, keep_prob) + + # return linear output layer + W_fc2 = weight_variable([fc_dim, n_classes]) + b_fc2 = bias_variable([n_classes]) + return tf.matmul(h_fc1_drop, W_fc2) + b_fc2 + + +# and this is the simpler multilayer perceptron +def multilayer_perceptron(x, n_bands, n_hidden, n_classes, keep_prob): + flattend_input = tf.reshape(x, [-1, n_bands]) + layer_1, W_1 = add_fully_connected_layer(flattend_input, n_bands, n_hidden, + keep_prob) + layer_2, W_2 = add_fully_connected_layer(layer_1, n_hidden, n_hidden, + keep_prob) + last_hidden_layer, W_3 = add_fully_connected_layer(layer_2, n_hidden, n_hidden, + keep_prob) + + W_out = weight_variable([n_hidden, n_classes]) + b_out = bias_variable([n_classes]) + + regularizers = (tf.nn.l2_loss(W_1) + tf.nn.l2_loss(W_2) + + tf.nn.l2_loss(W_3) + tf.nn.l2_loss(W_out)) + + return tf.matmul(last_hidden_layer, W_out) + b_out, regularizers + diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/tests/__init__.py b/Modules/Biophotonics/python/iMC/scripts/basic_checks/__init__.py similarity index 100% rename from Modules/Biophotonics/python/inverseMonteCarlo/tests/__init__.py rename to Modules/Biophotonics/python/iMC/scripts/basic_checks/__init__.py diff --git a/Modules/Biophotonics/python/iMC/scripts/basic_checks/script_evaluate_color_checkerboard.py b/Modules/Biophotonics/python/iMC/scripts/basic_checks/script_evaluate_color_checkerboard.py new file mode 100644 index 0000000000..fcf5bea72e --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/basic_checks/script_evaluate_color_checkerboard.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 14 11:09:18 2015 + +@author: wirkert +""" + + +import Image +import ImageEnhance +import logging +import datetime +import copy + +from scipy.interpolate import interp1d +from sklearn.preprocessing import normalize +from sklearn.metrics import r2_score + +from msi.io.nrrdreader import NrrdReader +import msi.normalize as norm +from ipcai2016.tasks_common import * +import commons +from ipcai2016 import tasks_mc +import msi.plot as msiplot +from msi.io.spectrometerreader import SpectrometerReader +from msi.normalize import standard_normalizer +from msi.io.tiffringreader import TiffRingReader + +TiffRingReader.RESIZE_FACTOR = 0.5 + +sc = commons.ScriptCommons() + +sc.add_dir("COLORCHECKER_DATA", + os.path.join(sc.get_dir("DATA_FOLDER"), "colorchecker_laparoscope")) + +sc.add_dir("COLORCHECKER_RESULTS", + os.path.join(sc.get_dir("RESULTS_FOLDER"), + "colorchecker_laparoscope")) + +sc.add_dir("FLAT_FOLDER", + os.path.join(sc.get_dir("DATA_FOLDER"), + "colorchecker_laparoscope_flatfield")) + +sc.add_dir("SPECTROMETER_REFERENCE_DATA", + os.path.join(sc.get_dir("DATA_FOLDER"), + "spectrometer_reflectance_new")) + +sc.add_dir("FILTER_TRANSMISSIONS", + os.path.join(sc.get_dir("DATA_FOLDER"), + "filter_transmissions")) + + +class CheckColorCheckerBoards(luigi.Task): + image_name = luigi.Parameter() + + def requires(self): + return Flatfield(flatfield_folder=sc.get_full_dir("FLAT_FOLDER")), \ + SingleMultispectralImage(image=self.image_name), \ + Dark(dark_folder=sc.get_full_dir("DARK_FOLDER")), \ + SpectrometerToSpectrocam(spectrometer_measurement= + os.path.join(sc.get_full_dir("SPECTROMETER_REFERENCE_DATA"), + os.path.split(self.image_name)[1][0:8] + ".txt")) + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("COLORCHECKER_RESULTS"), + os.path.split(self.image_name)[1] + + "_" + "_color_check" + ".png")) + + def run(self): + + print "... read data" + + segmentation_file = os.path.join( + sc.get_full_dir("COLORCHECKER_DATA"), "seg.tiff") + segmentation_file2 = os.path.join( + sc.get_full_dir("COLORCHECKER_DATA"), "seg2.tiff") + + nrrd_reader = NrrdReader() + tiff_ring_reader = TiffRingReader() + # read the flatfield + flat = nrrd_reader.read(self.input()[0].path) + dark = nrrd_reader.read(self.input()[2].path) + # read the msi + nr_filters = len(sc.other["RECORDED_WAVELENGTHS"]) + msi, segmentation = tiff_ring_reader.read(self.input()[1].path, + nr_filters, + segmentation=segmentation_file) + msi_copy = copy.deepcopy(msi) # copy to be able to apply both + # segmentations + msimani.apply_segmentation(msi, segmentation) + msi2, segmentation2 = tiff_ring_reader.read(self.input()[1].path, + nr_filters, + segmentation=segmentation_file2) + msimani.apply_segmentation(msi2, segmentation2) + + msimani.apply_segmentation(msi_copy, segmentation + segmentation2) + + # read the spectrometer measurement + msi_spectrometer = nrrd_reader.read(self.input()[3].path) + + # correct by flatfield and dark image + #msimani.image_correction(msi, flat, dark) + #msimani.image_correction(msi2, flat, dark) + #msimani.image_correction(msi_copy, flat, dark) + msimani.dark_correction(msi, dark) + msimani.dark_correction(msi2, dark) + msimani.dark_correction(msi_copy, dark) + + # create artificial rgb + rgb_image = msi_copy.get_image()[:, :, [2, 3, 1]] + rgb_image /= np.max(rgb_image) + rgb_image *= 255. + + # preprocess the image + # sortout unwanted bands + print "... apply normalizations" + # normalize to get rid of lighting intensity + norm.standard_normalizer.normalize(msi) + norm.standard_normalizer.normalize(msi2) + + print "... plot" + + # plot of the rgb image + rgb_image = rgb_image.astype(np.uint8) + im = Image.fromarray(rgb_image, 'RGB') + enh_brightness = ImageEnhance.Brightness(im) + im = enh_brightness.enhance(2.) + plotted_image = np.array(im) + + plt.figure() + f, (ax_rgb, ax_spectra) = plt.subplots(1, 2) + plot_image(plotted_image, ax_rgb, title="false rgb") + + msiplot.plotMeanError(msi, ax_spectra) + msiplot.plotMeanError(msi2, ax_spectra) + + standard_normalizer.normalize(msi_spectrometer) + msiplot.plot(msi_spectrometer, ax_spectra) + + mean_spectrometer = msi_spectrometer.get_image() + mean_msi = msimani.calculate_mean_spectrum(msi).get_image() + mean_msi2 = msimani.calculate_mean_spectrum(msi2).get_image() + r2_msi = r2_score(mean_spectrometer, mean_msi) + r2_msi2 = r2_score(mean_spectrometer, mean_msi2) + + ax_spectra.legend(["spectrocam 1 r2: " + str(r2_msi), + "spectrocam 2 r2: " + str(r2_msi2), "spectrometer"], + bbox_to_anchor=(0., 1.02, 1., .102), loc=3, + ncol=2, mode="expand", borderaxespad=0., + fontsize=5) + + plt.savefig(self.output().path, + dpi=250, bbox_inches='tight') + + +class CameraQEFile(luigi.Task): + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("DATA_FOLDER"), + "camera_quantum_efficiency.csv")) + + +class SpectrometerToSpectrocam(luigi.Task): + + spectrometer_measurement = luigi.Parameter() + + def requires(self): + # all wavelengths must have been measured for transmission and stored in + # wavelength.txt files (e.g. 470.txt) + filenames = ((sc.other["RECORDED_WAVELENGTHS"] * 10**9).astype(int)).astype(str) + filenames = map(lambda name: tasks_mc.FilterTransmission(os.path.join(sc.get_full_dir("FILTER_TRANSMISSIONS"), + name) + ".txt"), + filenames) + + return tasks_mc.SpectrometerFile(self.spectrometer_measurement), \ + filenames, CameraQEFile() + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("INTERMEDIATES_FOLDER"), + os.path.split(self.spectrometer_measurement)[1] + + "_spectrocam.nrrd")) + + def run(self): + # load spectrometer measurement + spectrometer_reader = SpectrometerReader() + spectrometer_msi = spectrometer_reader.read(self.input()[0].path) + + # the wavelengths recorded by the spectrometer + spectrometer_wavelengths = spectrometer_msi.get_wavelengths() + + spectrometer_white = spectrometer_reader.read(os.path.join( + sc.get_full_dir("DATA_FOLDER"), "spectrometer_whitebalance", + "white_IL_1_OO_20ms.txt")) + spectrometer_dark = spectrometer_reader.read(os.path.join( + sc.get_full_dir("DATA_FOLDER"), "spectrometer_whitebalance", + "dark_1_OO_20ms.txt")) + msimani.dark_correction(spectrometer_white, spectrometer_dark) + white_interpolator = interp1d(spectrometer_white.get_wavelengths(), + spectrometer_white.get_image(), + bounds_error=False, fill_value=0.) + white_interpolated = white_interpolator(spectrometer_wavelengths) + + camera_qe = pd.read_csv(self.input()[2].path) + camera_qe_interpolator = interp1d(camera_qe["wavelengths"] * 10**-9, + camera_qe["quantum efficiency"], + bounds_error=False, + fill_value=0.) + camera_qe_interpolated = \ + camera_qe_interpolator(spectrometer_wavelengths) + + # camera batch creation: + new_reflectances = [] + for band in self.input()[1]: + df_filter = pd.read_csv(band.path) + interpolator = interp1d(df_filter["wavelengths"], + df_filter["reflectances"], + assume_sorted=False, bounds_error=False) + # use this to create new reflectances + interpolated_filter = interpolator(spectrometer_wavelengths) + # if a wavelength cannot be interpolated, set it to 0 + interpolated_filter = np.nan_to_num(interpolated_filter) + # account for cameras quantum efficiency + interpolated_filter *= camera_qe_interpolated * white_interpolated + # normalize band response + #normalize(interpolated_filter.reshape(1, -1), norm='l1', copy=False) + folded_reflectance = np.dot(spectrometer_msi.get_image(), + interpolated_filter) + new_reflectances.append(folded_reflectance) + plt.plot(interpolated_filter) + new_reflectances = np.array(new_reflectances).T + spectrometer_msi.set_image(new_reflectances, + wavelengths=sc.other["RECORDED_WAVELENGTHS"]) + + # write it + nrrd_writer = NrrdWriter(spectrometer_msi) + nrrd_writer.write(self.output().path) + +if __name__ == '__main__': + + # create a folder for the results if necessary + sc.set_root("/media/wirkert/data/Data/2020_Current_Works/") + sc.create_folders() + + # root folder there the data lies + logging.basicConfig(filename=os.path.join(sc.get_full_dir("LOG_FOLDER"), + "color_checker" + + str(datetime.datetime.now()) + + '.log'), level=logging.INFO) + luigi.interface.setup_interface_logging() + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + logger = logging.getLogger() + logger.addHandler(ch) + + sch = luigi.scheduler.CentralPlannerScheduler() + w = luigi.worker.Worker(scheduler=sch) + + files = get_image_files_from_folder(sc.get_full_dir("COLORCHECKER_DATA"), + suffix="F0.tiff", fullpath=True) + + for f in files: + main_task = CheckColorCheckerBoards(image_name=f) + w.add(main_task) + + w.run() + diff --git a/Modules/Biophotonics/python/iMC/scripts/commons.py b/Modules/Biophotonics/python/iMC/scripts/commons.py new file mode 100644 index 0000000000..48e060132c --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/commons.py @@ -0,0 +1,97 @@ +""" +This file contains a singleton class which manages the paths set for evaluating +the scripts. + +Also it contains some utility methods. +""" + +import os + +import numpy as np + + +class ScriptCommons(object): + """ + The commonly shared paths to the data/log/results... folders. + + additional commonly available data as recorded wavelengths + this is a singleton pattern copied from + http://python-3-patterns-idioms-test.readthedocs.org/en/latest/Singleton.html + """ + + class __ScriptCommons_Singleton: + + def __init__(self): + + self.root = os.path.join("..", "..") + + self.dirs = {"LOG_FOLDER": "log", + "DATA_FOLDER": "data", + "RESULTS_FOLDER": "results"} + + self.dirs["FLAT_FOLDER"] = os.path.join(self.dirs["DATA_FOLDER"], + "flatfields") + self.dirs["DARK_FOLDER"] = os.path.join(self.dirs["DATA_FOLDER"], + "dark") + + self.dirs["INTERMEDIATES_FOLDER"] = os.path.join( + self.dirs["RESULTS_FOLDER"], "intermediate") + + self.dirs["MC_DATA_FOLDER"] = os.path.join( + self.dirs["INTERMEDIATES_FOLDER"], "mc_data") + + self.other = {"RECORDED_WAVELENGTHS": np.array([580, 470, + 660, 560, + 480, 511, + 600, 700]) + * 10 ** -9} + + def create_folders(self): + """ + Create all folders listed in self.folders if not existing + """ + for f in self.dirs: + create_folder_if_necessary(self.get_full_dir(f)) + + def set_root(self, root): + self.root = root + + def get_root(self): + return self.root + + def add_dir(self, key, new_dir): + """ + Add/replace a directory to the singletons list. + Directories can be returned with get_dir and with their full path by + calling get_full_dir + + :param key: the key under which it shall be retrievable + :param new_dir: the directory to add to the list + """ + self.dirs[key] = new_dir + + def get_dir(self, key): + return self.dirs[key] + + def get_full_dir(self, key): + return os.path.join(self.get_root(), self.get_dir(key)) + + instance = None + + def __new__(cls): # __new__ always a classmethod + if not ScriptCommons.instance: + ScriptCommons.instance = ScriptCommons.__ScriptCommons_Singleton() + return ScriptCommons.instance + + def __getattr__(self, name): + return getattr(self.instance, name) + + def __setattr__(self, name): + return setattr(self.instance, name) + + +def create_folder_if_necessary(folder): + """ + :param folder: create the folder folder if necessary (not already existing) + """ + if not os.path.exists(folder): + os.makedirs(folder) diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/__init__.py b/Modules/Biophotonics/python/iMC/scripts/domain_adaptation/__init__.py similarity index 100% copy from Modules/Biophotonics/python/inverseMonteCarlo/__init__.py copy to Modules/Biophotonics/python/iMC/scripts/domain_adaptation/__init__.py diff --git a/Modules/Biophotonics/python/iMC/scripts/domain_adaptation/domain_adaptation_paths.py b/Modules/Biophotonics/python/iMC/scripts/domain_adaptation/domain_adaptation_paths.py new file mode 100644 index 0000000000..23660eb1f1 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/domain_adaptation/domain_adaptation_paths.py @@ -0,0 +1,18 @@ + + +import os + +ROOT_FOLDER = "/media/wirkert/data/Data/2016_03_Domain_Adaptation" +LOG_FOLDER = os.path.join(ROOT_FOLDER, "log") +DATA_FOLDER = os.path.join(ROOT_FOLDER, "data") +RESULTS_FOLDER = os.path.join(ROOT_FOLDER, "results") +INTERMEDIATES_FOLDER = os.path.join(RESULTS_FOLDER, "intermediate") +MC_DATA_FOLDER = os.path.join(INTERMEDIATES_FOLDER, "mc_data") + + +def create_folder_if_necessary(folder): + if not os.path.exists(folder): + os.makedirs(folder) + +create_folder_if_necessary(INTERMEDIATES_FOLDER) +create_folder_if_necessary(LOG_FOLDER) diff --git a/Modules/Biophotonics/python/iMC/scripts/domain_adaptation/script_analyze_da_in_silico.py b/Modules/Biophotonics/python/iMC/scripts/domain_adaptation/script_analyze_da_in_silico.py new file mode 100644 index 0000000000..f8d486e90a --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/domain_adaptation/script_analyze_da_in_silico.py @@ -0,0 +1,364 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 14 11:09:18 2015 + +@author: wirkert +""" + +import os +import logging +import datetime + +import numpy as np +import pandas as pd +import luigi +import matplotlib.pyplot as plt +import seaborn as sns + +from regression.preprocessing import preprocess +import domain_adaptation_paths as sp +from ipcai2016 import tasks_mc +from ipcai2016.script_analyze_ipcai_in_silico import w_standard, noise_levels, \ + evaluate_data, standard_plotting, NoisePlot +# additionally we need the weights estimation functionality +from regression.domain_adaptation import estimate_weights_random_forests + + +class WeightedBatch(luigi.Task): + which_source = luigi.Parameter() + which_target = luigi.Parameter() + noise = luigi.FloatParameter() + + def requires(self): + return tasks_mc.CameraBatch(self.which_source), \ + tasks_mc.CameraBatch(self.which_target) + + def output(self): + return luigi.LocalTarget(os.path.join(sp.ROOT_FOLDER, + sp.RESULTS_FOLDER, + "adapted_" + + self.which_source + + "_with_" + self.which_target + + "_noise_" + str(self.noise) + + ".txt")) + + def run(self): + # get data + df_source = pd.read_csv(self.input()[0].path, header=[0, 1]) + df_target = pd.read_csv(self.input()[1].path, header=[0, 1]) + + # first extract X_source and X_target, preprocessed at standard noise + # level + X_source, y_source = preprocess(df_source, w_percent=w_standard) + X_target, y_target = preprocess(df_target, w_percent=w_standard) + + # train a classifier to determine probability for specific class + weights = estimate_weights_random_forests(X_source, X_target, X_source) + # add weight to dataframe + df_source["weights"] = weights + + # finally save the dataframe with the added weights + df_source.to_csv(self.output().path, index=False) + + +class DaNoisePlot(luigi.Task): + """ + Very similar to NoisePlot in IPCAI in silico evaluation but with + weighted data coming in. + """ + which_train = luigi.Parameter() + which_test = luigi.Parameter() + + def requires(self): + # for each noise level we need to create the weights + NecessaryBatches = map(lambda noise: WeightedBatch(self.which_train, + self.which_test, + noise), + noise_levels) + return NecessaryBatches(self.which_train, self.which_test), \ + tasks_mc.CameraBatch(self.which_test) + + def output(self): + return luigi.LocalTarget(os.path.join(sp.ROOT_FOLDER, + sp.RESULTS_FOLDER, + sp.FINALS_FOLDER, + "noise_da_plot_train_" + + self.which_train + + "_test_" + self.which_test + + ".png")) + + def run(self): + # get data + df_train = pd.read_csv(self.input()[0].path, header=[0, 1]) + df_test = pd.read_csv(self.input()[1].path, header=[0, 1]) + + df = evaluate_data(df_train, noise_levels, df_test, noise_levels) + standard_plotting(df) + + # finally save the figure + plt.savefig(self.output().path, dpi=500, + bbox_inches='tight') + + +class GeneratingDistributionPlot(luigi.Task): + which_source = luigi.Parameter() + which_target = luigi.Parameter() + + def requires(self): + return WeightedBatch(self.which_source, self.which_target), \ + tasks_mc.CameraBatch(self.which_target) + + def output(self): + return luigi.LocalTarget(os.path.join(sp.ROOT_FOLDER, + sp.RESULTS_FOLDER, + sp.FINALS_FOLDER, + "generating_distribution_" + + self.which_source + + "_adapted_to_" + + self.which_target + + ".png")) + + def run(self): + # get data + df_source = pd.read_csv(self.input()[0].path, header=[0, 1]) + df_target = pd.read_csv(self.input()[1].path, header=[0, 1]) + + # create dataframe suited for plotting + # we do a weighted sampling with replacement to be able to create some + # plots there the data distribution is visible. + nr_samples = 100 + # first data weighted by domain adaptation + df_source_adapted = df_source["layer0"].copy() + df_source_adapted["weights"] = df_source["weights"] + df_source_adapted["data"] = "adapted" + df_source_adapted = df_source_adapted.sample(n=nr_samples, replace=True, + weights="weights") + # now original source data + df_source = df_source["layer0"].copy() + df_source["weights"] = 1 # we set the weights here to 1 + df_source["data"] = "source" + df_source = df_source.sample(n=nr_samples, replace=True, + weights="weights") + # now the target data + df_target = df_target["layer0"] + df_target["weights"] = 1 + df_target["data"] = "target" + df_target = df_target.sample(n=nr_samples, replace=True, + weights="weights") + # now merge all three dataframes to the dataframe used for the plotting + df = pd.concat([df_source, df_source_adapted, df_target]) + # since we already sampled we can get rid of weights + df.drop("weights", axis=1, inplace=True) + # d to um + df["d"] *= 10**6 + # vhb and sao2 to % + df["vhb"] *= 100 + df["sao2"] *= 100 + + # do some serious plotting + g = sns.pairplot(df, vars=["vhb", "sao2", "d"], + hue="data", markers=["o", "s", "D"]) + + # tidy up plot + g.add_legend() + + # finally save the figure + plt.savefig(self.output().path, dpi=500, + bbox_inches='tight') + + +class WeightDistributionPlot(luigi.Task): + which_source = luigi.Parameter() + which_target = luigi.Parameter() + + def requires(self): + return WeightedBatch(self.which_source, self.which_target) + + def output(self): + return luigi.LocalTarget(os.path.join(sp.ROOT_FOLDER, + sp.RESULTS_FOLDER, + sp.FINALS_FOLDER, + "weight_distribution_" + + self.which_source + + "_adapted_to_" + + self.which_target + + ".png")) + + def run(self): + # get data + df_source = pd.read_csv(self.input().path, header=[0, 1]) + + df_source["weights"].plot.hist(bins=100) + plt.axvline(x=1, ymin=0, ymax=df_source.shape[0]) + # TODO: add cumsum on top + + # finally save the figure + plt.savefig(self.output().path, dpi=500, + bbox_inches='tight') + + +class FeatureDistributionPlot(luigi.Task): + which_source = luigi.Parameter() + which_target = luigi.Parameter() + + def requires(self): + return WeightedBatch(self.which_source, self.which_target), \ + tasks_mc.CameraBatch(self.which_target) + + def output(self): + return luigi.LocalTarget(os.path.join(sp.ROOT_FOLDER, + sp.RESULTS_FOLDER, + sp.FINALS_FOLDER, + "feature_distribution_" + + self.which_source + + "_adapted_to_" + + self.which_target + + ".png")) + + def run(self): + # get data + df_source = pd.read_csv(self.input()[0].path, header=[0, 1]) + df_target = pd.read_csv(self.input()[1].path, header=[0, 1]) + + df_f_source = format_dataframe_for_distribution_plotting(df_source) + df_f_target = format_dataframe_for_distribution_plotting(df_target) + df_f_adapted = format_dataframe_for_distribution_plotting(df_source, + weights=df_source["weights"].values.squeeze()) + + # build a combined df + df_f_source["data"] = "source" + df_f_target["data"] = "target" + df_f_adapted["data"] = "adapted" + df = pd.concat([df_f_source, df_f_target, df_f_adapted]) + + # do the plotting + grid = sns.FacetGrid(df, col="w", hue="data", col_wrap=3, size=1.5) + grid.map(plt.plot, "bins", "frequency") + + # tidy up plot + grid.fig.tight_layout(w_pad=1) + grid.add_legend() + grid.set(xticks=(0, 1)) + + # finally save the figure + plt.savefig(self.output().path, dpi=500) + + +class DAvNormalPlot(luigi.Task): + which_train = luigi.Parameter() + which_test = luigi.Parameter() + which_train_no_covariance_shift = luigi.Parameter() + + def requires(self): + return WeightedBatch(self.which_train, self.which_test), \ + tasks_mc.CameraBatch(self.which_test), \ + tasks_mc.CameraBatch(self.which_train_no_covariance_shift) + + def output(self): + return luigi.LocalTarget(os.path.join(sp.ROOT_FOLDER, + sp.RESULTS_FOLDER, + sp.FINALS_FOLDER, + "da_v_normal_train_" + + self.which_train + + "_test_" + self.which_test + + ".png")) + + def run(self): + # get data + df_train = pd.read_csv(self.input()[0].path, header=[0, 1]) + df_test = pd.read_csv(self.input()[1].path, header=[0, 1]) + df_train_no_covariance_shift = pd.read_csv(self.input()[2].path, + header=[0, 1]) + + evaluation_setups = [EvaluationStruct("Proposed", rf)] + # evaluate the different methods + df_adapted = evaluate_data(df_train, noise_levels, + df_test, noise_levels, + evaluation_setups=evaluation_setups) + df_adapted["data"] = "adapted" + df_no_adaptation = evaluate_data( + df_train.drop("weights", axis=1), noise_levels, + df_test, noise_levels, + evaluation_setups=evaluation_setups) + df_no_adaptation["data"] = "source" + df_no_covariance_shift = evaluate_data( + df_train_no_covariance_shift, noise_levels, + df_test, noise_levels, + evaluation_setups=evaluation_setups) + df_no_covariance_shift["data"] = "target" + df = pd.concat([df_adapted, df_no_adaptation, df_no_covariance_shift]) + + # plot it + sns.boxplot(data=df, x="noise added [sigma %]", y="Errors", hue="data", + hue_order=["source", "adapted", "target"], fliersize=0) + # tidy up plot + plt.ylim((0, 40)) + plt.legend(loc='upper left') + + # finally save the figure + plt.savefig(self.output().path, dpi=500) + + +def format_dataframe_for_distribution_plotting(df, weights=None): + if weights is None: + weights = np.ones(df.shape[0]) + + bins = np.arange(0, 1, 0.01) + + # we're only interested in reflectance information + df_formatted = df.loc[:, "reflectances"] + # to [nm] for plotting + df_formatted.rename(columns=lambda x: float(x)*10**9, inplace=True) + + # transform data to a histogram + df_formatted = df_formatted.apply(lambda x: + pd.Series(np.histogram(x, bins=bins, + weights=weights, + normed=True)[0]), + axis=0) + # convert to long form using bins as identifier + df_formatted["bins"] = bins[1:] + df_formatted = pd.melt(df_formatted, id_vars=["bins"], + var_name="w", value_name="frequency") + + return df_formatted + + +if __name__ == '__main__': + logging.basicConfig(filename=os.path.join(sp.LOG_FOLDER, + "da_in_silico_plots" + + str(datetime.datetime.now()) + + '.log'), + level=logging.INFO) + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + logger = logging.getLogger() + logger.addHandler(ch) + luigi.interface.setup_interface_logging() + + source_domain = "ipcai_revision_generic" + target_domain = "ipcai_revision_colon_test" + + sch = luigi.scheduler.CentralPlannerScheduler() + w = luigi.worker.Worker(scheduler=sch) + # check how the graph looks with same domains for training and testing + w.add(DaNoisePlot(which_train="ipcai_revision_colon_train", + which_test="ipcai_revision_colon_test")) + # check how the graph looks without domain adaptation + w.add(NoisePlot(which_train=source_domain, which_test=target_domain)) + # Set a different testing domain to evaluate domain sensitivity + w.add(DaNoisePlot(which_train=source_domain, which_test=target_domain)) + + w.add(WeightDistributionPlot(which_source=source_domain, + which_target=target_domain)) + w.add(FeatureDistributionPlot(which_source=source_domain, + which_target=target_domain)) + # also plot distributions for equal domains to check for errors in data + w.add(FeatureDistributionPlot(which_source="ipcai_revision_colon_mean_scattering_train", + which_target="ipcai_revision_colon_mean_scattering_test")) + # plot how the generating model data (e.g. sao2 and vhb) is distributed + w.add(GeneratingDistributionPlot(which_source=source_domain, + which_target=target_domain)) + w.add(DAvNormalPlot(which_train=source_domain, which_test=target_domain, + which_train_no_covariance_shift="ipcai_revision_colon_train")) + w.run() diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/__init__.py b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/__init__.py similarity index 100% copy from Modules/Biophotonics/python/inverseMonteCarlo/__init__.py copy to Modules/Biophotonics/python/iMC/scripts/ipcai2016/__init__.py diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_analyze_ipcai_in_silico.py b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_analyze_ipcai_in_silico.py new file mode 100644 index 0000000000..0db37b69f9 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_analyze_ipcai_in_silico.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 14 11:09:18 2015 + +@author: wirkert +""" + +import os +import logging +import datetime +from collections import namedtuple + +import matplotlib +import numpy as np +import pandas as pd +from pandas import DataFrame +import luigi +import matplotlib.pyplot as plt +from sklearn.ensemble.forest import RandomForestRegressor + +import tasks_mc +from regression.preprocessing import preprocess, preprocess2 +from regression.linear import LinearSaO2Unmixing + +import commons + +sc = commons.ScriptCommons() + +sc.add_dir("IN_SILICO_RESULTS_PATH", os.path.join(sc.get_dir("RESULTS_FOLDER"), + "in_silico")) + +sc.other["RECORDED_WAVELENGTHS"] = np.arange(470, 680, 10) * 10 ** -9 + +w_standard = 10. # for this evaluation we add 10% noise + +font = {'family' : 'normal', + 'size' : 20} + +matplotlib.rc('font', **font) + + +# setup standard random forest +rf = RandomForestRegressor(10, min_samples_leaf=10, max_depth=9, n_jobs=-1) +EvaluationStruct = namedtuple("EvaluationStruct", + "name regressor") +# standard evaluation setup +standard_evaluation_setups = [EvaluationStruct("Linear Beer-Lambert", + LinearSaO2Unmixing()) + , EvaluationStruct("Proposed", rf)] + +# color palette +my_colors = ["red", "green"] + +# standard noise levels +noise_levels = np.array([1,2,3,4,5,6,7,8,9,10, + 15,20,30,40,50,100,150,200]).astype("float") + + +class TrainingSamplePlot(luigi.Task): + which_train = luigi.Parameter() + which_test = luigi.Parameter() + + def requires(self): + return tasks_mc.CameraBatch(self.which_train), \ + tasks_mc.CameraBatch(self.which_test) + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("IN_SILICO_RESULTS_PATH"), + "sample_plot_train_" + + self.which_train + + "_test_" + self.which_test + + ".pdf")) + + def run(self): + # get data + df_train = pd.read_csv(self.input()[0].path, header=[0, 1]) + df_test = pd.read_csv(self.input()[1].path, header=[0, 1]) + + # for this plot we write a custom evaluation function as it is built + # a little different + + # create a new dataframe which will hold all the generated errors + df = pd.DataFrame() + + nr_training_samples = np.arange(10, 15010, 50).astype(int) + # not very pythonic, don't care + for n in nr_training_samples: + X_test, y_test = preprocess(df_test, snr=w_standard) + # only take n samples for training + X_train, y_train = preprocess(df_train, nr_samples=n, + snr=w_standard) + + regressor = rf + regressor.fit(X_train, y_train) + y_pred = regressor.predict(X_test) + # save results to a dataframe + errors = np.abs(y_pred - y_test) + errors = errors.reshape(len(errors), 1) + current_df = DataFrame(errors * 100, + columns=["Errors"]) + current_df["Method"] = "Proposed" + current_df["Number Samples"] = n / 10**3. + df = pd.concat([df, current_df], ignore_index=True) + logging.info( + "Finished training classifier with {0} samples".format( + str(n))) + + df = df.groupby("Number Samples").describe() + # get the error description in the rows: + df = df.unstack(-1) + # get rid of multiindex by dropping "Error" level + df.columns = df.columns.droplevel(0) + + plt.figure() + plt.plot(df.index, df["50%"], color="green") + + # tidy up the plot + plt.xlabel("number of training samples / 1000") + plt.ylabel("absolute error [%]") + plt.ylim((0, 20)) + plt.xlim((0, 15)) + plt.grid() + + # finally save the figure + plt.savefig(self.output().path, mode="pdf", dpi=500, + bbox_inches='tight') + + +class VhbPlot(luigi.Task): + which_train = luigi.Parameter() + which_test = luigi.Parameter() + + def requires(self): + return tasks_mc.CameraBatch(self.which_train), \ + tasks_mc.CameraBatch(self.which_test) + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("IN_SILICO_RESULTS_PATH"), + "vhb_noise_plot_train_" + + self.which_train + + "_test_" + self.which_test + + ".pdf")) + + @staticmethod + def preprocess_vhb(batch, nr_samples=None, snr=None, + magnification=None, bands_to_sortout=None): + """ For evaluating vhb we extract labels for vhb instead of sao2""" + X, y = preprocess2(batch, nr_samples, snr, + magnification, bands_to_sortout) + + return X, y["vhb"].values + + def run(self): + # get data + df_train = pd.read_csv(self.input()[0].path, header=[0, 1]) + df_test = pd.read_csv(self.input()[1].path, header=[0, 1]) + + # for vhb we only evaluate the proposed method since the linear + # beer-lambert is not applicable + evaluation_setups = [EvaluationStruct("Proposed", rf)] + df = evaluate_data(df_train, noise_levels, df_test, noise_levels, + evaluation_setups=evaluation_setups, + preprocessing=self.preprocess_vhb) + standard_plotting(df, color_palette=["green"], + xytext_position=(2, 3)) + plt.ylim((0, 4)) + + # finally save the figure + plt.savefig(self.output().path, dpi=500, + bbox_inches='tight') + + +class NoisePlot(luigi.Task): + which_train = luigi.Parameter() + which_test = luigi.Parameter() + + def requires(self): + return tasks_mc.CameraBatch(self.which_train), \ + tasks_mc.CameraBatch(self.which_test) + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("IN_SILICO_RESULTS_PATH"), + "noise_plot_train_" + + self.which_train + + "_test_" + self.which_test + + ".pdf")) + + def run(self): + # get data + df_train = pd.read_csv(self.input()[0].path, header=[0, 1]) + df_test = pd.read_csv(self.input()[1].path, header=[0, 1]) + + df = evaluate_data(df_train, noise_levels, df_test, noise_levels) + standard_plotting(df) + + # finally save the figure + plt.savefig(self.output().path, mode="pdf", dpi=500, + bbox_inches='tight') + + +class WrongNoisePlot(luigi.Task): + which_train = luigi.Parameter() + which_test = luigi.Parameter() + train_snr = luigi.FloatParameter() + + def requires(self): + return tasks_mc.CameraBatch(self.which_train), \ + tasks_mc.CameraBatch(self.which_test) + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("IN_SILICO_RESULTS_PATH"), + str(self.train_snr) + + "_wrong_noise_plot_train_" + + self.which_train + + "_test_" + self.which_test + + ".pdf")) + + def run(self): + # get data + df_train = pd.read_csv(self.input()[0].path, header=[0, 1]) + df_test = pd.read_csv(self.input()[1].path, header=[0, 1]) + + # do same as in NoisePlot but with standard noise input + df = evaluate_data(df_train, + np.ones_like(noise_levels) * self.train_snr, + df_test, noise_levels) + standard_plotting(df) + + # finally save the figure + plt.savefig(self.output().path, mode="pdf", dpi=500, + bbox_inches='tight') + + +def evaluate_data(df_train, w_train, df_test, w_test, + evaluation_setups=None, preprocessing=None): + """ Our standard method to evaluate the data. It will fill a DataFrame df + which saves the errors for each evaluated setup""" + if evaluation_setups is None: + evaluation_setups = standard_evaluation_setups + if preprocessing is None: + preprocessing = preprocess + if ("weights" in df_train) and df_train["weights"].size > 0: + weights = df_train["weights"].as_matrix().squeeze() + else: + weights = np.ones(df_train.shape[0]) + + # create a new dataframe which will hold all the generated errors + df = pd.DataFrame() + for one_w_train, one_w_test in zip(w_train, w_test): + # setup testing function + X_test, y_test = preprocessing(df_test, snr=one_w_test) + # extract noisy data + X_train, y_train = preprocessing(df_train, snr=one_w_train) + for e in evaluation_setups: + regressor = e.regressor + regressor.fit(X_train, y_train, weights) + y_pred = regressor.predict(X_test) + # save results to a dataframe + errors = np.abs(y_pred - y_test) + errors = errors.reshape(len(errors), 1) + current_df = DataFrame(errors * 100, + columns=["Errors"]) + current_df["Method"] = e.name + current_df["SNR"] = int(one_w_test) + df = pd.concat([df, current_df], ignore_index=True) + + return df + + +def standard_plotting(df, color_palette=None, xytext_position=None): + if color_palette is None: + color_palette = my_colors + if xytext_position is None: + xytext_position = (2, 15) + + plt.figure() + + # group it by method and noise level and get description on the errors + df_statistics = df.groupby(['Method', 'SNR']).describe() + # get the error description in the rows: + df_statistics = df_statistics.unstack(-1) + # get rid of multiindex by dropping "Error" level + df_statistics.columns = df_statistics.columns.droplevel(0) + + # iterate over methods to plot linegraphs with error tube + # probably this can be done nicer, but no idea how exactly + + for color, method in zip( + color_palette, df_statistics.index.get_level_values("Method").unique()): + df_method = df_statistics.loc[method] + plt.plot(df_method.index, df_method["50%"], + color=color, label=method) + plt.fill_between(df_method.index, df_method["25%"], df_method["75%"], + facecolor=color, edgecolor=color, + alpha=0.5) + # tidy up the plot + plt.ylim((0, 40)) + plt.gca().set_xticks(np.arange(0, 200, 10), minor=True) + plt.xlabel("SNR") + plt.ylabel("absolute error [%]") + plt.grid() + plt.legend() + + +if __name__ == '__main__': + + sc.set_root("/media/wirkert/data/Data/2016_02_02_IPCAI/") + sc.create_folders() + + logging.basicConfig(filename=os.path.join(sc.get_full_dir("LOG_FOLDER"), + "in_silico_plots_" + + str(datetime.datetime.now()) + + '.log'), + level=logging.INFO) + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + logger = logging.getLogger() + logger.addHandler(ch) + luigi.interface.setup_interface_logging() + + train = "ipcai_revision_colon_mean_scattering_train" + test = "ipcai_revision_colon_mean_scattering_test" + + sch = luigi.scheduler.CentralPlannerScheduler() + w = luigi.worker.Worker(scheduler=sch) + w.add(TrainingSamplePlot(which_train=train, which_test=test)) + w.add(NoisePlot(which_train=train, which_test=test)) + w.add(WrongNoisePlot(which_train=train, which_test=test, train_snr=10.)) + w.add(WrongNoisePlot(which_train=train, which_test=test, train_snr=50.)) + w.add(WrongNoisePlot(which_train=train, which_test=test, train_snr=200.)) + # Set a different testing domain to evaluate domain sensitivity + w.add(NoisePlot(which_train=train, + which_test="ipcai_revision_generic_mean_scattering_test")) + w.add(VhbPlot(which_train=train, which_test=test)) + w.run() diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_analyze_ipcai_in_vivo_liver.py b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_analyze_ipcai_in_vivo_liver.py new file mode 100644 index 0000000000..2f547afe3f --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_analyze_ipcai_in_vivo_liver.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 14 11:09:18 2015 + +@author: wirkert +""" + + +import Image +import ImageEnhance +import logging +import datetime + +import SimpleITK as sitk +import matplotlib + +from msi.io.nrrdreader import NrrdReader +import msi.normalize as norm +from regression.estimation import estimate_image +from tasks_common import * +import commons +from msi.io.tiffringreader import TiffRingReader + +TiffRingReader.RESIZE_FACTOR = 0.5 + +sc = commons.ScriptCommons() + +sc.add_dir("LIVER_DATA", + os.path.join(sc.get_dir("DATA_FOLDER"), "liver_images")) +sc.add_dir("LIVER_RESULTS", os.path.join(sc.get_dir("RESULTS_FOLDER"), "liver")) + +sc.add_dir("FILTER_TRANSMISSIONS", + os.path.join(sc.get_dir("DATA_FOLDER"), + "filter_transmissions")) + +font = {'family' : 'normal', + 'size' : 30} +matplotlib.rc('font', **font) + + +class ResultsFile(luigi.Task): + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("LIVER_RESULTS"), + "results.csv")) + + +class OxyAndVhbOverTimeTask(luigi.Task): + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("LIVER_RESULTS"), + "liver_oxy_over_time.pdf")) + + def requires(self): + return ResultsFile() + + def run(self): + df = pd.read_csv(self.input().path, index_col=0) + # determine times from start: + image_name_strings = df["image name"].values + time_strings = map(lambda s: s[ + s.find("2014-08-03_")+11:s.find("2014-08-03_")+19], + image_name_strings) + time_in_s = map(lambda s: int(s[0:2]) * 3600 + + int(s[3:5]) * 60 + + int(s[6:]), time_strings) + df["time since drug delivery [s]"] = np.array(time_in_s) - time_in_s[0] + + # print oxy over time as scatterplot. + ax = df.plot.scatter(x="time since drug delivery [s]", + y="oxygenation mean [%]", + s=100, alpha=0.5, + fontsize=30) + ax.set_xlim((-1, 70)) + + plt.axvline(x=0, ymin=0, ymax=1, linewidth=2) + plt.axvline(x=56, ymin=0, ymax=1, linewidth=2) + ax.annotate('drug delivery', xy=(0, ax.get_ylim()[1]), + xycoords='data', xytext=(0, 0), + fontsize=30, + textcoords='offset points') + ax.annotate('porcine death', xy=(56, ax.get_ylim()[1]), + xycoords='data', xytext=(-100, 0), + fontsize=30, + textcoords='offset points') + ax.yaxis.label.set_size(30) + ax.xaxis.label.set_size(30) + plt.grid() + + df.to_csv(self.input().path) + + # create and save vhb plot + plt.savefig(self.output().path, + dpi=250, bbox_inches='tight', mode="pdf") + + # print vhb over time as scatterplot. + ax = df.plot.scatter(x="time since drug delivery [s]", + y="blood volume fraction mean [%]", + s=100, alpha=0.5, + fontsize=30) + ax.set_xlim((-1, 70)) + + plt.axvline(x=0, ymin=0, ymax=1, linewidth=2) + plt.axvline(x=56, ymin=0, ymax=1, linewidth=2) + ax.annotate('drug delivery', xy=(0, ax.get_ylim()[1]), + xycoords='data', xytext=(0, 0), + fontsize=30, + textcoords='offset points') + ax.annotate('porcine death', xy=(56, ax.get_ylim()[1]), + xycoords='data', xytext=(-100, 0), + fontsize=30, + textcoords='offset points') + ax.yaxis.label.set_size(30) + ax.xaxis.label.set_size(30) + plt.grid() + + plt.savefig(self.output().path + "_vhb_mean.pdf", + dpi=250, bbox_inches='tight', mode="pdf") + + +class IPCAICreateOxyImageTask(luigi.Task): + image_name = luigi.Parameter() + df_prefix = luigi.Parameter() + + def requires(self): + return IPCAITrainRegressor(df_prefix=self.df_prefix), \ + Flatfield(flatfield_folder=sc.get_full_dir("FLAT_FOLDER")), \ + SingleMultispectralImage(image=os.path.join( + sc.get_full_dir("LIVER_DATA"), self.image_name)), \ + Dark(dark_folder=sc.get_full_dir("DARK_FOLDER")) + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("LIVER_RESULTS"), + self.image_name + "_" + + self.df_prefix + + "_oxy_summary" + ".png")) + + def run(self): + nrrd_reader = NrrdReader() + tiff_ring_reader = TiffRingReader() + # read the flatfield + flat = nrrd_reader.read(self.input()[1].path) + dark = nrrd_reader.read(self.input()[3].path) + # read the msi + nr_filters = len(sc.other["RECORDED_WAVELENGTHS"]) + msi, segmentation = tiff_ring_reader.read(self.input()[2].path, + nr_filters) + + # only take into account not saturated pixels. + segmentation = np.max(msi.get_image(), axis=-1) < 2000. + + # read the regressor + e_file = open(self.input()[0].path, 'r') + e = pickle.load(e_file) + + # correct image setup + position_filter_nr_in_string = self.image_name.find(" 2014") - 1 + filter_nr = int(self.image_name[ + position_filter_nr_in_string:position_filter_nr_in_string+1]) + original_order = np.arange(nr_filters) + new_image_order = np.concatenate(( + original_order[nr_filters - filter_nr:], + original_order[:nr_filters - filter_nr])) + # resort msi to restore original order + msimani.get_bands(msi, new_image_order) + # correct by flatfield + msimani.image_correction(msi, flat, dark) + + # create artificial rgb + rgb_image = msi.get_image()[:, :, [2, 3, 1]] + rgb_image /= np.max(rgb_image) + rgb_image *= 255. + + # preprocess the image + # sortout unwanted bands + print "1" + # zero values would lead to infinity logarithm, thus clip. + msi.set_image(np.clip(msi.get_image(), 0.00001, 2. ** 64)) + # normalize to get rid of lighting intensity + norm.standard_normalizer.normalize(msi) + # transform to absorption + msi.set_image(-np.log(msi.get_image())) + # normalize by l2 for stability + norm.standard_normalizer.normalize(msi, "l2") + print "2" + # estimate + sitk_image, time = estimate_image(msi, e) + image = sitk.GetArrayFromImage(sitk_image) + + plt.figure() + print "3" + rgb_image = rgb_image.astype(np.uint8) + im = Image.fromarray(rgb_image, 'RGB') + enh_brightness = ImageEnhance.Brightness(im) + im = enh_brightness.enhance(5.) + plotted_image = np.array(im) + top_left_axis = plt.gca() + top_left_axis.imshow(plotted_image, interpolation='nearest') + top_left_axis.xaxis.set_visible(False) + top_left_axis.yaxis.set_visible(False) + + plt.set_cmap("jet") + print "4" + # plot parametric maps + # first oxygenation + plt.figure() + oxy_image = image[:, :, 0] + segmentation[0, 0] = 1 + segmentation[0, 1] = 1 + oxy_image = np.ma.masked_array(oxy_image, ~segmentation) + oxy_image[np.isnan(oxy_image)] = 0. + oxy_image[np.isinf(oxy_image)] = 0. + oxy_mean = np.mean(oxy_image) + oxy_image[0, 0] = 0.0 + oxy_image[0, 1] = 1. + plot_image(oxy_image[:, :], plt.gca()) + plt.savefig(self.output().path, + dpi=250, bbox_inches='tight') + # second blood volume fraction + plt.figure() + vhb_image = image[:, :, 1] + vhb_image = np.ma.masked_array(vhb_image, ~segmentation) + vhb_image[np.isnan(vhb_image)] = 0. + vhb_image[np.isinf(vhb_image)] = 0. + vhb_image[0, 0] = 0.0 + vhb_image[0, 1] = 0.1 + vhb_image = np.clip(vhb_image, 0.0, 0.1) + vhb_mean = np.mean(vhb_image) + plot_image(vhb_image, plt.gca()) + plt.savefig(self.output().path + "vhb.png", + dpi=250, bbox_inches='tight') + + # store results summary in dataframe + df_image_results = pd.DataFrame(data=np.expand_dims([self.image_name, + oxy_mean * 100., + vhb_mean * 100., + time], 0), + columns=["image name", + "oxygenation mean [%]", + "blood volume fraction mean [%]", + "time to estimate"]) + + results_file = os.path.join(sc.get_full_dir("LIVER_RESULTS"), + "results.csv") + if os.path.isfile(results_file): + df_results = pd.read_csv(results_file, index_col=0) + df_results = pd.concat((df_results, df_image_results)).reset_index( + drop=True + ) + else: + df_results = df_image_results + + df_results.to_csv(results_file) + + print "5" + plt.close("all") + + +if __name__ == '__main__': + + # create a folder for the results if necessary + sc.set_root("/media/wirkert/data/Data/2016_02_02_IPCAI/") + sc.create_folders() + + # root folder there the data lies + logging.basicConfig(filename=os.path.join(sc.get_full_dir("LOG_FOLDER"), + "liver" + + str(datetime.datetime.now()) + + '.log'), level=logging.INFO) + luigi.interface.setup_interface_logging() + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + logger = logging.getLogger() + logger.addHandler(ch) + + sch = luigi.scheduler.CentralPlannerScheduler() + w = luigi.worker.Worker(scheduler=sch) + + onlyfiles = get_image_files_from_folder(sc.get_full_dir("LIVER_DATA")) + + first_invivo_image_files = filter(lambda image_name: "0 2014" in image_name, + onlyfiles) + + for f in first_invivo_image_files: + main_task = IPCAICreateOxyImageTask(image_name=f, + df_prefix="ipcai_revision_colon_mean_scattering_train") + w.add(main_task) + + w.run() + + oxygenation_over_time_task = OxyAndVhbOverTimeTask() + w.add(oxygenation_over_time_task) + w.run() + diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_analyze_ipcai_in_vivo_small_bowel.py b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_analyze_ipcai_in_vivo_small_bowel.py new file mode 100644 index 0000000000..6ce3da2895 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_analyze_ipcai_in_vivo_small_bowel.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 14 11:09:18 2015 + +@author: wirkert +""" + +import Image +import ImageEnhance +import logging +import datetime + +import SimpleITK as sitk +import matplotlib + +from msi.io.nrrdreader import NrrdReader +import msi.normalize as norm +from regression.estimation import estimate_image +from tasks_common import * +import commons +from msi.io.tiffringreader import TiffRingReader + +TiffRingReader.RESIZE_FACTOR = 0.5 + +sc = commons.ScriptCommons() + + +sc.add_dir("SMALL_BOWEL_DATA", + os.path.join(sc.get_dir("DATA_FOLDER"), "small_bowel_images")) + +sc.add_dir("SMALL_BOWEL_RESULT", os.path.join(sc.get_dir("RESULTS_FOLDER"), + "small_bowel")) + +sc.add_dir("FILTER_TRANSMISSIONS", + os.path.join(sc.get_dir("DATA_FOLDER"), + "filter_transmissions")) + +font = {'family' : 'normal', + 'size' : 25} +matplotlib.rc('font', **font) + + +class ResultsFile(luigi.Task): + + def output(self): + return luigi.LocalTarget(os.path.join( + sc.get_full_dir("SMALL_BOWEL_RESULT"), "results.csv")) + + +class OxyOverTimeTask(luigi.Task): + + def output(self): + return luigi.LocalTarget(os.path.join( + sc.get_full_dir("SMALL_BOWEL_RESULT"), + "colon_oxy_over_time.pdf")) + + def requires(self): + return ResultsFile() + + def run(self): + df = pd.read_csv(self.input().path, index_col=0) + + # determine times from start: + image_name_strings = df["image name"].values + time_strings = map(lambda s: s[ + s.find("2015-10-08_")+11:s.find("2015-10-08_")+19], + image_name_strings) + time_in_s = map(lambda s: int(s[0:2]) * 3600 + + int(s[3:5]) * 60 + + int(s[6:]), time_strings) + df["time since first frame [s]"] = np.array(time_in_s) - time_in_s[0] + df["time since applying first clip [s]"] = df["time since first frame [s]"] - 4 + # print oxy over time as scatterplot. + plt.figure() + ax = df.plot.scatter(x="time since applying first clip [s]", + y="oxygenation mean [%]", fontsize=30, + s=50, alpha=0.5) + ax.set_xlim((-5, 600)) + + plt.axvline(x=0, ymin=0, ymax=1, linewidth=2) + plt.axvline(x=39, ymin=0, ymax=1, linewidth=2) + plt.axvline(x=100, ymin=0, ymax=1, linewidth=2) + plt.axvline(x=124, ymin=0, ymax=1, linewidth=2) + ax.annotate('1', xy=(0, ax.get_ylim()[1]), + fontsize=18, + color="blue", + xycoords='data', xytext=(-5, 0), + textcoords='offset points') + ax.annotate('2', xy=(39, ax.get_ylim()[1]), + fontsize=18, + color="blue", + xycoords='data', xytext=(-5, 0), + textcoords='offset points') + ax.annotate('3', xy=(100, ax.get_ylim()[1]), + fontsize=18, + color="blue", + xycoords='data', xytext=(-5, 0), + textcoords='offset points') + ax.annotate('4', xy=(124, ax.get_ylim()[1]), + fontsize=18, + color="blue", + xycoords='data', xytext=(-5, 0), + textcoords='offset points') + + plt.grid() + + df.to_csv(self.input().path) + + # save + plt.savefig(self.output().path, + dpi=250, bbox_inches='tight', mode="pdf") + + +def plot_image(image, axis): + im2 = axis.imshow(image, interpolation='nearest', alpha=1.0) + # axis.set_title(title, fontsize=5) + # divider = make_axes_locatable(axis) + # cax = divider.append_axes("right", size="10%", pad=0.05) + # cbar = plt.colorbar(im2, cax=cax) + # cbar.ax.tick_params(labelsize=5) + axis.xaxis.set_visible(False) + + +class IPCAICreateOxyImageTask(luigi.Task): + image_name = luigi.Parameter() + df_prefix = luigi.Parameter() + + def requires(self): + return IPCAITrainRegressor(df_prefix=self.df_prefix), \ + Flatfield(flatfield_folder=sc.get_full_dir("FLAT_FOLDER")), \ + SingleMultispectralImage(image=self.image_name), \ + Dark(dark_folder=sc.get_full_dir("DARK_FOLDER")) + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("SMALL_BOWEL_RESULT"), + os.path.split(self.image_name)[1] + + "_" + self.df_prefix + + "_summary" + ".png")) + + def run(self): + nrrd_reader = NrrdReader() + tiff_ring_reader = TiffRingReader() + # read the flatfield + flat = nrrd_reader.read(self.input()[1].path) + dark = nrrd_reader.read(self.input()[3].path) + # read the msi + nr_filters = len(sc.other["RECORDED_WAVELENGTHS"]) + msi, segmentation = tiff_ring_reader.read(self.input()[2].path, + nr_filters) + # only take into account not saturated pixels. + segmentation = np.logical_and(segmentation, + (np.max(msi.get_image(), + axis=-1) < 1000.)) + + # read the regressor + e_file = open(self.input()[0].path, 'r') + e = pickle.load(e_file) + + # correct image setup + filter_nr = int(self.image_name[-6:-5]) + original_order = np.arange(nr_filters) + new_image_order = np.concatenate(( + original_order[nr_filters - filter_nr:], + original_order[:nr_filters - filter_nr])) + # resort msi to restore original order + msimani.get_bands(msi, new_image_order) + # correct by flatfield + msimani.image_correction(msi, flat, dark) + + # create artificial rgb + rgb_image = msi.get_image()[:, :, [2, 3, 1]] + rgb_image /= np.max(rgb_image) + rgb_image *= 255. + + # preprocess the image + # sortout unwanted bands + print "1" + # zero values would lead to infinity logarithm, thus clip. + msi.set_image(np.clip(msi.get_image(), 0.00001, 2. ** 64)) + # normalize to get rid of lighting intensity + norm.standard_normalizer.normalize(msi) + # transform to absorption + msi.set_image(-np.log(msi.get_image())) + # normalize by l2 for stability + norm.standard_normalizer.normalize(msi, "l2") + print "2" + # estimate + sitk_image, time = estimate_image(msi, e) + image = sitk.GetArrayFromImage(sitk_image) + + plt.figure() + print "3" + rgb_image = rgb_image.astype(np.uint8) + im = Image.fromarray(rgb_image, 'RGB') + enh_brightness = ImageEnhance.Brightness(im) + im = enh_brightness.enhance(10.) + plotted_image = np.array(im) + top_left_axis = plt.gca() + top_left_axis.imshow(plotted_image, interpolation='nearest') + top_left_axis.xaxis.set_visible(False) + top_left_axis.yaxis.set_visible(False) + + plt.set_cmap("jet") + print "4" + # plot parametric maps + segmentation[0, 0] = 1 + segmentation[0, 1] = 1 + oxy_image = np.ma.masked_array(image[:, :, 0], ~segmentation) + oxy_image[np.isnan(oxy_image)] = 0. + oxy_image[np.isinf(oxy_image)] = 0. + oxy_mean = np.mean(oxy_image) + oxy_image[0, 0] = 0.0 + oxy_image[0, 1] = 1. + + plot_image(oxy_image[:, :], plt.gca()) + + df_image_results = pd.DataFrame(data=np.expand_dims([self.image_name, + oxy_mean * 100., + time], 0), + columns=["image name", + "oxygenation mean [%]", + "time to estimate"]) + + results_file = os.path.join(sc.get_full_dir("SMALL_BOWEL_RESULT"), + "results.csv") + if os.path.isfile(results_file): + df_results = pd.read_csv(results_file, index_col=0) + df_results = pd.concat((df_results, df_image_results)).reset_index( + drop=True + ) + else: + df_results = df_image_results + + df_results.to_csv(results_file) + + plt.savefig(self.output().path, + dpi=250, bbox_inches='tight') + plt.close("all") + + +if __name__ == '__main__': + + # create a folder for the results if necessary + sc.set_root("/media/wirkert/data/Data/2016_02_02_IPCAI/") + sc.create_folders() + + # root folder there the data lies + logging.basicConfig(filename=os.path.join(sc.get_full_dir("LOG_FOLDER"), + "small_bowel_images" + + str(datetime.datetime.now()) + + '.log'), + level=logging.INFO) + luigi.interface.setup_interface_logging() + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + logger = logging.getLogger() + logger.addHandler(ch) + + sch = luigi.scheduler.CentralPlannerScheduler() + w = luigi.worker.Worker(scheduler=sch) + + + # determine files to process + files = get_image_files_from_folder(sc.get_full_dir("SMALL_BOWEL_DATA"), + suffix="F0.tiff", fullpath=True) + + for f in files: + main_task = IPCAICreateOxyImageTask(image_name=f, + df_prefix="ipcai_revision_colon_mean_scattering_te") + w.add(main_task) + w.run() + + oxygenation_over_time_task = OxyOverTimeTask() + w.add(oxygenation_over_time_task) + w.run() + diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_calculate_spectra.py b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_calculate_spectra.py new file mode 100644 index 0000000000..5ceb764c33 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/script_calculate_spectra.py @@ -0,0 +1,123 @@ +''' +Created on Sep 9, 2015 + +@author: wirkert +''' + + +import logging +import datetime +import os +import time + +import numpy as np +import luigi + +import commons +import mc.factories as mcfac +from mc.sim import SimWrapper +from mc.create_spectrum import create_spectrum + +# parameter setting +NR_BATCHES = 100 +NR_ELEMENTS_IN_BATCH = 1000 +# the wavelengths to be simulated +WAVELENGHTS = np.arange(450, 720, 2) * 10 ** -9 +NR_PHOTONS = 10 ** 6 + +# experiment configuration +MCI_FILENAME = "./temp.mci" +MCO_FILENAME = "temp.mco" +# this path definitly needs to be adapted by you +PATH_TO_MCML = "/home/wirkert/workspace/monteCarlo/gpumcml/fast-gpumcml/" +EXEC_MCML = "gpumcml.sm_20" + + +sc = commons.ScriptCommons() + + +class CreateSpectraTask(luigi.Task): + df_prefix = luigi.Parameter() + batch_nr = luigi.IntParameter() + nr_samples = luigi.IntParameter() + factory = luigi.Parameter() + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("MC_DATA_FOLDER"), + self.df_prefix + "_" + + str(self.batch_nr) + ".txt")) + + def run(self): + start = time.time() + # setup simulation wrapper + sim_wrapper = SimWrapper() + sim_wrapper.set_mci_filename(MCI_FILENAME) + sim_wrapper.set_mcml_executable(os.path.join(PATH_TO_MCML, EXEC_MCML)) + # setup model + tissue_model = self.factory.create_tissue_model() + tissue_model.set_mci_filename(sim_wrapper.mci_filename) + tissue_model.set_mco_filename(MCO_FILENAME) + tissue_model.set_nr_photons(NR_PHOTONS) + # setup array in which data shall be stored + batch = self.factory.create_batch_to_simulate() + batch.create_parameters(self.nr_samples) + # dataframe created by batch: + df = batch.df + # add reflectance column to dataframe + for w in WAVELENGHTS: + df["reflectances", w] = np.NAN + + # for each instance of our tissue model + for i in range(df.shape[0]): + # set the desired element in the dataframe to be simulated + tissue_model.set_dataframe_row(df.loc[i, :]) + logging.info("running simulation " + str(i) + " for\n" + + str(tissue_model)) + reflectances = create_spectrum(tissue_model, sim_wrapper, + WAVELENGHTS) + # store in dataframe + for r, w in zip(reflectances, WAVELENGHTS): + df["reflectances", w][i] = r + + # clean up temporarily created files + os.remove(MCI_FILENAME) + created_mco_file = os.path.join(PATH_TO_MCML, MCO_FILENAME) + if os.path.isfile(created_mco_file): + os.remove(created_mco_file) + # save the created output + f = open(self.output().path, 'w') + df.to_csv(f) + + end = time.time() + logging.info("time for creating batch of mc data: %.f s" % + (end - start)) + + +if __name__ == '__main__': + + # create a folder for the results if necessary + sc.set_root("/media/wirkert/data/Data/2016_02_02_IPCAI/") + sc.create_folders() + + logging.basicConfig(filename=os.path.join(sc.get_full_dir("LOG_FOLDER"), + "calculate_spectra" + + str(datetime.datetime.now()) + + '.log'), + level=logging.INFO) + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + logger = logging.getLogger() + logger.addHandler(ch) + luigi.interface.setup_interface_logging() + + sch = luigi.scheduler.CentralPlannerScheduler() + w = luigi.worker.Worker(scheduler=sch) + BATCH_NUMBERS = np.arange(0, NR_BATCHES, 1) + for i in BATCH_NUMBERS: + colon_task = CreateSpectraTask("ipcai_revision_generic_mean_scattering", + i, + NR_ELEMENTS_IN_BATCH, + mcfac.GenericMeanScatteringFactory()) + w.add(colon_task) + w.run() + diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai2016/tasks_common.py b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/tasks_common.py new file mode 100644 index 0000000000..7df698175c --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/tasks_common.py @@ -0,0 +1,166 @@ + +import os +import pickle + +import numpy as np +import pandas as pd +import luigi +from sklearn.ensemble.forest import RandomForestRegressor +import matplotlib.pylab as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable + +import tasks_mc +import commons +from msi.msi import Msi +from msi.io.nrrdwriter import NrrdWriter +import msi.msimanipulations as msimani +from regression.preprocessing import preprocess2 +from msi.io.tiffringreader import TiffRingReader + +sc = commons.ScriptCommons() + +""" +Collection of functions and luigi.Task s which are used by more than one script +""" + + +def get_image_files_from_folder(folder, + prefix="", suffix=".tiff", fullpath=False): + # small helper function to get all the image files in a folder + # it will only return files which end with suffix. + # if fullpath==True it will return the full path of the file, otherwise + # only the filename + # get all filenames + image_files = [f for f in os.listdir(folder) if + os.path.isfile(os.path.join(folder, f))] + image_files.sort() + image_files = [f for f in image_files if f.endswith(suffix)] + image_files = [f for f in image_files if f.startswith(prefix)] + if fullpath: # expand to full path if wished + image_files = [os.path.join(folder, f) for f in image_files] + return image_files + + +def plot_image(image, axis=None, title=None, cmap=None): + if axis is None: + axis = plt.gca() + if cmap is None: + im = axis.imshow(image, interpolation='nearest', alpha=1.0) + else: + im = axis.imshow(image, interpolation='nearest', alpha=1.0, + cmap=cmap) + divider = make_axes_locatable(axis) + cax = divider.append_axes("right", size="20%", pad=0.05) + cbar = plt.colorbar(im, cax=cax) + + axis.xaxis.set_visible(False) + axis.yaxis.set_visible(False) + if title is not None: + axis.set_title(title) + + +class IPCAITrainRegressor(luigi.Task): + df_prefix = luigi.Parameter() + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("INTERMEDIATES_FOLDER"), + "reg_small_bowel_" + + self.df_prefix)) + + def requires(self): + return tasks_mc.SpectroCamBatch(self.df_prefix) + + def run(self): + # extract data from the batch + df_train = pd.read_csv(self.input().path, header=[0, 1]) + + X, y = preprocess2(df_train, snr=10.) + # train regressor + reg = RandomForestRegressor(10, min_samples_leaf=10, max_depth=9, + n_jobs=-1) + # reg = KNeighborsRegressor(algorithm="auto") + # reg = LinearRegression() + # reg = sklearn.svm.SVR(kernel="rbf", degree=3, C=100., gamma=10.) + # reg = LinearSaO2Unmixing() + reg.fit(X, y.values) + # reg = LinearSaO2Unmixing() + # save regressor + regressor_file = self.output().open('w') + pickle.dump(reg, regressor_file) + regressor_file.close() + + +class SingleMultispectralImage(luigi.Task): + + image = luigi.Parameter() + + def output(self): + return luigi.LocalTarget(self.image) + + +class Flatfield(luigi.Task): + + flatfield_folder = luigi.Parameter() + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("INTERMEDIATES_FOLDER"), + "flatfield.nrrd")) + + def run(self): + tiff_ring_reader = TiffRingReader() + nr_filters = len(sc.other["RECORDED_WAVELENGTHS"]) + + # analyze all the first image files + image_files = get_image_files_from_folder(self.flatfield_folder) + image_files = filter(lambda image_name: "F0" in image_name, image_files) + + # helper function to take maximum of two images + def maximum_of_two_images(image_1, image_name_2): + image_2 = tiff_ring_reader.read(os.path.join(self.flatfield_folder, + image_name_2), + nr_filters)[0].get_image() + return np.maximum(image_1, image_2) + + # now reduce to maximum of all the single images + flat_maximum = reduce(lambda x, y: maximum_of_two_images(x, y), + image_files, 0) + msi = Msi(image=flat_maximum) + msi.set_wavelengths(sc.other["RECORDED_WAVELENGTHS"]) + + # write flatfield as nrrd + writer = NrrdWriter(msi) + writer.write(self.output().path) + + +class Dark(luigi.Task): + dark_folder = luigi.Parameter() + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("INTERMEDIATES_FOLDER"), + "dark" + + ".nrrd")) + + def run(self): + tiff_ring_reader = TiffRingReader() + nr_filters = len(sc.other["RECORDED_WAVELENGTHS"]) + + # analyze all the first image files + image_files = get_image_files_from_folder(self.dark_folder, + suffix="F0.tiff") + + # returns the mean dark image vector of all inputted dark image + # overly complicated TODO SW: make this simple code readable. + dark_means = map(lambda image_name: + msimani.calculate_mean_spectrum( + tiff_ring_reader.read(os.path.join(self.dark_folder, image_name), + nr_filters)[0]), + image_files) + dark_means_sum = reduce(lambda x, y: x+y.get_image(), dark_means, 0) + final_dark_mean = dark_means_sum / len(dark_means) + + msi = Msi(image=final_dark_mean) + msi.set_wavelengths(sc.other["RECORDED_WAVELENGTHS"]) + + # write flatfield as nrrd + writer = NrrdWriter(msi) + writer.write(self.output().path) diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai2016/tasks_mc.py b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/tasks_mc.py new file mode 100644 index 0000000000..c3771dfd9d --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai2016/tasks_mc.py @@ -0,0 +1,155 @@ +''' +Created on Sep 10, 2015 + +@author: wirkert +''' + + + +import os + +import pandas as pd +import numpy as np +import luigi +from scipy.interpolate import interp1d +from sklearn.preprocessing import normalize + +import commons +import mc.dfmanipulations as dfmani +from msi.io.spectrometerreader import SpectrometerReader + +sc = commons.ScriptCommons() + + +class SpectrometerFile(luigi.Task): + input_file = luigi.Parameter() + + def output(self): + return luigi.LocalTarget(self.input_file) + + +class FilterTransmission(luigi.Task): + input_file = luigi.Parameter() + + def requires(self): + return SpectrometerFile(self.input_file) + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("INTERMEDIATES_FOLDER"), + "processed_transmission" + os.path.split(self.input_file)[1])) + + def run(self): + reader = SpectrometerReader() + filter_transmission = reader.read(self.input().path) + # filter high and low _wavelengths + wavelengths = filter_transmission.get_wavelengths() + fi_image = filter_transmission.get_image() + fi_image[wavelengths < 450 * 10 ** -9] = 0.0 + fi_image[wavelengths > 720 * 10 ** -9] = 0.0 + # filter elements farther away than +- 30nm + file_name = os.path.split(self.input_file)[1] + name_to_float = float(os.path.splitext(file_name)[0]) + fi_image[wavelengths < (name_to_float - 30) * 10 ** -9] = 0.0 + fi_image[wavelengths > (name_to_float + 30) * 10 ** -9] = 0.0 + # elements < 0 are set to 0. + fi_image[fi_image < 0.0] = 0.0 + + # write it to a dataframe + df = pd.DataFrame() + df["wavelengths"] = wavelengths + df["reflectances"] = fi_image + df.to_csv(self.output().path, index=False) + + +class JoinBatches(luigi.Task): + df_prefix = luigi.Parameter() + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("INTERMEDIATES_FOLDER"), + self.df_prefix + "_" + + "all" + ".txt")) + + def run(self): + # get all files in the search path + files = [f for f in os.listdir(sc.get_full_dir("MC_DATA_FOLDER")) + if os.path.isfile(os.path.join(sc.get_full_dir("MC_DATA_FOLDER"), f))] + # from these get only those who start with correct batch prefix + df_file_names = [os.path.join(sc.get_full_dir("MC_DATA_FOLDER"), f) + for f in files if f.startswith(self.df_prefix)] + # load these files + dfs = [pd.read_csv(f, header=[0, 1]) for f in df_file_names] + # now join them to one batch + joined_df = pd.concat(dfs, ignore_index=True) + # write it + joined_df.to_csv(self.output().path, index=False) + + +class CameraBatch(luigi.Task): + """takes a batch of reference data and converts it to the spectra + processed by a camera with the specified wavelengths assuming a 10nm FWHM""" + df_prefix = luigi.Parameter() + + def requires(self): + return JoinBatches(self.df_prefix) + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("INTERMEDIATES_FOLDER"), + self.df_prefix + + "_all_virtual_camera.txt")) + + def run(self): + # load dataframe + df = pd.read_csv(self.input().path, header=[0, 1]) + # camera batch creation: + dfmani.fold_by_sliding_average(df, 6) + dfmani.interpolate_wavelengths(df, sc.other["RECORDED_WAVELENGTHS"]) + # write it + df.to_csv(self.output().path, index=False) + + +class SpectroCamBatch(luigi.Task): + """ + Same as CameraBatch in purpose but adapts the batch to our very specific + SpectroCam set-up. + """ + df_prefix = luigi.Parameter() + + def requires(self): + # all wavelengths must have been measured for transmission and stored in + # wavelength.txt files (e.g. 470.txt) + filenames = ((sc.other["RECORDED_WAVELENGTHS"] * 10**9).astype(int)).astype(str) + filenames = map(lambda name: FilterTransmission(os.path.join(sc.get_full_dir("FILTER_TRANSMISSIONS"), + name + + ".txt")), + filenames) + + return JoinBatches(self.df_prefix), filenames + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("INTERMEDIATES_FOLDER"), + self.df_prefix + + "_all_spectrocam.txt")) + + def run(self): + # load dataframe + df = pd.read_csv(self.input()[0].path, header=[0, 1]) + # camera batch creation: + new_reflectances = [] + for band in self.input()[1]: + df_filter = pd.read_csv(band.path) + interpolator = interp1d(df_filter["wavelengths"], + df_filter["reflectances"], + assume_sorted=False, bounds_error=False) + # use this to create new reflectances + interpolated_filter = interpolator(df["reflectances"]. + columns.astype(float)) + # normalize band response + normalize(interpolated_filter.reshape(1, -1), norm='l1', copy=False) + folded_reflectance = np.dot(df["reflectances"].values, + interpolated_filter) + new_reflectances.append(folded_reflectance) + new_reflectances = np.array(new_reflectances).T + dfmani.switch_reflectances(df, sc.other["RECORDED_WAVELENGTHS"], + new_reflectances) + # write it + df.to_csv(self.output().path, index=False) diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/__init__.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/__init__.py similarity index 100% copy from Modules/Biophotonics/python/inverseMonteCarlo/__init__.py copy to Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/__init__.py diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_image_hdf5.h5_list.txt b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_image_hdf5.h5_list.txt new file mode 100644 index 0000000000..1a0f78a1b1 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_image_hdf5.h5_list.txt @@ -0,0 +1 @@ +ipcai_image_hdf5.h5 \ No newline at end of file diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_solver.prototxt b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_solver.prototxt new file mode 100644 index 0000000000..d76ba8c7f1 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_solver.prototxt @@ -0,0 +1,24 @@ +# The train/test net protocol buffer definition +train_net: "ipcai_train.prototxt" +test_net: "ipcai_test.prototxt" +# test_iter specifies how many forward passes the test should carry out. +# In the case of MNIST, we have test batch size 100 and 100 test iterations, +# covering the full 10,000 testing images. +test_iter: 100 +# Carry out testing every 500 training iterations. +test_interval: 500 +# The base learning rate, momentum and the weight decay of the network. +base_lr: 0.01 +momentum: 0.0 +weight_decay: 0.0005 +# The learning rate policy +lr_policy: "inv" +gamma: 0.0001 +power: 0.75 +# Display every 100 iterations +display: 1000 +# The maximum number of iterations +max_iter: 10000 +# snapshot intermediate results +snapshot: 5000 +snapshot_prefix: "snapshot" diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_test.prototxt b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_test.prototxt new file mode 100644 index 0000000000..72534d20a0 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_test.prototxt @@ -0,0 +1,77 @@ +layer { + name: "data" + type: "HDF5Data" + top: "data" + top: "label" + hdf5_data_param { + source: "ipcai_test_hdf5.h5_list.txt" + batch_size: 50 + } +} +layer { + name: "fc1" + type: "InnerProduct" + bottom: "data" + top: "fc1" + inner_product_param { + num_output: 25 + weight_filler { + type: "xavier" + } + bias_filler { + type: "constant" + value: 0.1 + } + } +} +layer { + name: "relu1" + type: "ReLU" + bottom: "fc1" + top: "fc1" +} +layer { + name: "fc2" + type: "InnerProduct" + bottom: "fc1" + top: "fc2" + inner_product_param { + num_output: 25 + weight_filler { + type: "xavier" + } + bias_filler { + type: "constant" + value: 0.1 + } + } +} +layer { + name: "relu2" + type: "ReLU" + bottom: "fc2" + top: "fc2" +} +layer { + name: "score" + type: "InnerProduct" + bottom: "fc2" + top: "score" + inner_product_param { + num_output: 1 + weight_filler { + type: "xavier" + } + bias_filler { + type: "constant" + value: 0.1 + } + } +} +layer { + name: "loss" + type: "EuclideanLoss" + bottom: "score" + bottom: "label" + top: "loss" +} diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_test_hdf5.h5_list.txt b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_test_hdf5.h5_list.txt new file mode 100644 index 0000000000..691abad4da --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_test_hdf5.h5_list.txt @@ -0,0 +1 @@ +ipcai_test_hdf5.h5 \ No newline at end of file diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_test_image.prototxt b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_test_image.prototxt new file mode 100644 index 0000000000..6bc1620c4f --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_test_image.prototxt @@ -0,0 +1,70 @@ + +layer { + name: "data" + type: "HDF5Data" + top: "data" + hdf5_data_param { + source: "ipcai_image_hdf5.h5_list.txt" + batch_size: 1263612 + } +} +layer { + name: "fc1" + type: "InnerProduct" + bottom: "data" + top: "fc1" + inner_product_param { + num_output: 25 + weight_filler { + type: "xavier" + } + bias_filler { + type: "constant" + value: 0.1 + } + } +} +layer { + name: "relu1" + type: "ReLU" + bottom: "fc1" + top: "fc1" +} +layer { + name: "fc2" + type: "InnerProduct" + bottom: "fc1" + top: "fc2" + inner_product_param { + num_output: 25 + weight_filler { + type: "xavier" + } + bias_filler { + type: "constant" + value: 0.1 + } + } +} +layer { + name: "relu2" + type: "ReLU" + bottom: "fc2" + top: "fc2" +} +layer { + name: "score" + type: "InnerProduct" + bottom: "fc2" + top: "score" + inner_product_param { + num_output: 1 + weight_filler { + type: "xavier" + } + bias_filler { + type: "constant" + value: 0.1 + } + } +} diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_train.prototxt b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_train.prototxt new file mode 100644 index 0000000000..d79b65aa90 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_train.prototxt @@ -0,0 +1,77 @@ +layer { + name: "data" + type: "HDF5Data" + top: "data" + top: "label" + hdf5_data_param { + source: "ipcai_train_hdf5.h5_list.txt" + batch_size: 100 + } +} +layer { + name: "fc1" + type: "InnerProduct" + bottom: "data" + top: "fc1" + inner_product_param { + num_output: 25 + weight_filler { + type: "xavier" + } + bias_filler { + type: "constant" + value: 0.1 + } + } +} +layer { + name: "relu1" + type: "ReLU" + bottom: "fc1" + top: "fc1" +} +layer { + name: "fc2" + type: "InnerProduct" + bottom: "fc1" + top: "fc2" + inner_product_param { + num_output: 25 + weight_filler { + type: "xavier" + } + bias_filler { + type: "constant" + value: 0.1 + } + } +} +layer { + name: "relu2" + type: "ReLU" + bottom: "fc2" + top: "fc2" +} +layer { + name: "score" + type: "InnerProduct" + bottom: "fc2" + top: "score" + inner_product_param { + num_output: 1 + weight_filler { + type: "xavier" + } + bias_filler { + type: "constant" + value: 0.1 + } + } +} +layer { + name: "loss" + type: "EuclideanLoss" + bottom: "score" + bottom: "label" + top: "loss" +} diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_train_hdf5.h5_list.txt b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_train_hdf5.h5_list.txt new file mode 100644 index 0000000000..d4b8316827 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/ipcai_train_hdf5.h5_list.txt @@ -0,0 +1 @@ +ipcai_train_hdf5.h5 \ No newline at end of file diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_create_hdf5_database.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_create_hdf5_database.py new file mode 100644 index 0000000000..3c416e3446 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_create_hdf5_database.py @@ -0,0 +1,29 @@ +import h5py, os +import pandas as pd + +from regression.preprocessing import preprocess + + +def create_hdf5(path_to_simulation_results, hdf5_name): + + df = pd.read_csv(path_to_simulation_results, header=[0, 1]) + + X, y = preprocess(df, snr=10.) + y = y.values + + with h5py.File(hdf5_name,'w') as H: + H.create_dataset('data', data=X ) # note the name X given to the dataset! + H.create_dataset('label', data=y ) # note the name y given to the dataset! + with open(hdf5_name + '_list.txt','w') as L: + L.write(hdf5_name) # list all h5 files you are going to use + + +data_root = "/media/wirkert/data/Data/2016_02_02_IPCAI/results/intermediate" + +TRAIN_IMAGES = os.path.join(data_root, + "ipcai_revision_colon_mean_scattering_train_all_spectrocam.txt") +TEST_IMAGES = os.path.join(data_root, + "ipcai_revision_colon_mean_scattering_test_all_spectrocam.txt") + +create_hdf5(TRAIN_IMAGES, "ipcai_train_hdf5.h5") +create_hdf5(TEST_IMAGES, "ipcai_test_hdf5.h5") diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_create_lmdb_database.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_create_lmdb_database.py new file mode 100644 index 0000000000..ad0160c8ce --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_create_lmdb_database.py @@ -0,0 +1,52 @@ + +import os + +import pandas as pd +import lmdb +import caffe + +from regression.preprocessing import preprocess + + +def create_lmdb(path_to_simulation_results, lmdb_name): + + df = pd.read_csv(path_to_simulation_results, header=[0, 1]) + + X, y = preprocess(df, snr=10.) + y = y.values * 1000 + + # We need to prepare the database for the size. We'll set it 10 times + # greater than what we theoretically need. There is little drawback to + # setting this too big. If you still run into problem after raising + # this, you might want to try saving fewer entries in a single + # transaction. + map_size = X.nbytes * 10 + + env = lmdb.open(lmdb_name, map_size=map_size) + + with env.begin(write=True) as txn: + # txn is a Transaction object + for i in range(X.shape[0]): + datum = caffe.proto.caffe_pb2.Datum() + datum.channels = X.shape[1] + datum.height = 1 + datum.width = 1 + datum.data = X[i].tobytes() # or .tostring() if numpy < 1.9 + datum.label = int(y[i]) + str_id = '{:08}'.format(i) + + # The encode is only essential in Python 3 + txn.put(str_id.encode('ascii'), datum.SerializeToString()) + + +data_root = "/media/wirkert/data/Data/2016_02_02_IPCAI/results/intermediate" + +TRAIN_IMAGES = os.path.join(data_root, + "ipcai_revision_colon_mean_scattering_train_all_spectrocam.txt") +TEST_IMAGES = os.path.join(data_root, + "ipcai_revision_colon_mean_scattering_test_all_spectrocam.txt") + +create_lmdb(TRAIN_IMAGES, "ipcai_train_lmdb") +create_lmdb(TEST_IMAGES, "ipcai_test_lmdb") + + diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_test_pretrained_model.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_test_pretrained_model.py new file mode 100644 index 0000000000..688ad933ec --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_test_pretrained_model.py @@ -0,0 +1,26 @@ + +import time + +import caffe + +from ipcai2016.tasks_common import plot_image + +model_def = 'ipcai_test_image.prototxt' +model_weights = 'snapshot_iter_100000.caffemodel' + +net = caffe.Net(model_def, # defines the structure of the model + model_weights, # contains the trained weights + caffe.TEST) # use test mode (e.g., don't perform dropout) + +### perform classification + +start = time.time() +output = net.forward() +end = time.time() +estimation_time = end - start + +print "time necessary for estimating image parameters: " + str(estimation_time) + "s" + +image = output['score'].reshape(1029,1228) + +plot_image(image) \ No newline at end of file diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_train_caffe.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_train_caffe.py new file mode 100644 index 0000000000..c269272502 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_caffe/script_train_caffe.py @@ -0,0 +1,83 @@ +from pylab import * + +import caffe +from caffe import layers as L, params as P + + +def ipcai(database, batch_size): + # our version of LeNet: a series of linear and simple nonlinear transformations + n = caffe.NetSpec() + + n.data, n.label = L.HDF5Data(batch_size=batch_size, source=database, ntop=2) + + n.fc1 = L.InnerProduct(n.data, num_output=25, weight_filler=dict(type='xavier'), + bias_filler=dict(type='constant', value=0.1)) + n.relu1 = L.ReLU(n.fc1, in_place=True) + n.fc2 = L.InnerProduct(n.relu1, num_output=25, weight_filler=dict(type='xavier'), + bias_filler=dict(type='constant', value=0.1)) + n.relu2 = L.ReLU(n.fc2, in_place=True) + n.score = L.InnerProduct(n.relu2, num_output=1, weight_filler=dict(type='xavier'), + bias_filler=dict(type='constant', value=0.1)) + n.loss = L.EuclideanLoss(n.score, n.label) + + return n.to_proto() + +with open('ipcai_train.prototxt', 'w') as f: + f.write(str(ipcai('ipcai_train_hdf5.h5_list.txt', 100))) + +with open('ipcai_test.prototxt', 'w') as f: + f.write(str(ipcai('ipcai_test_hdf5.h5_list.txt', 50))) + +caffe.set_device(0) +caffe.set_mode_gpu() + +### load the solver and create train and test nets +solver = None # ignore this workaround for lmdb data (can't instantiate two solvers on the same data) +solver = caffe.SGDSolver('ipcai_solver.prototxt') + +# each output is (batch size, feature dim, spatial dim) +print [(k, v.data.shape) for k, v in solver.net.blobs.items()] + +# just print the weight sizes (we'll omit the biases) +print [(k, v[0].data.shape) for k, v in solver.net.params.items()] + +solver.net.forward() # train net +print solver.test_nets[0].forward() # test net (there can be more than one) + +niter = 100000 +test_interval = 1000 +# losses will also be stored in the log +train_loss = zeros(niter) +test_acc = zeros(int(np.ceil(niter / test_interval))) +output = zeros((niter, 8, 10)) + +# the main solver loop +for it in range(niter): + solver.step(1) # SGD by Caffe + + # store the train loss + train_loss[it] = solver.net.blobs['loss'].data + + # store the output on the first test batch + # (start the forward pass at fc1 to avoid loading new data) + solver.test_nets[0].forward(start='fc1') + output[it] = solver.test_nets[0].blobs['score'].data[:8] + + # run a full test every so often + # (Caffe can also do this for us and write to a log, but we show here + # how to do it directly in Python, where more complicated things are easier.) + if it % test_interval == 0: + print 'Iteration', it, 'testing...' + mean = 0. + for i in range(100): + solver.test_nets[0].forward() + mean += np.sum(np.abs(np.squeeze(solver.test_nets[0].blobs['score'].data) + - solver.test_nets[0].blobs['label'].data)) + mean = mean / 5000 + test_acc[it // test_interval] = mean * 100. # % + +print "final testing accuracy: ", test_acc[-1] + + +print solver.test_nets[0].blobs['score'].data +print solver.test_nets[0].blobs['label'].data \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/__init__.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/__init__.py similarity index 100% copy from Modules/Biophotonics/python/inverseMonteCarlo/__init__.py copy to Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/__init__.py diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/input_data.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/input_data.py new file mode 100644 index 0000000000..d1d0d28e2b --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/input_data.py @@ -0,0 +1,144 @@ +"""Functions for downloading and reading MNIST data.""" +from __future__ import print_function +import gzip +import os +import urllib +import numpy +SOURCE_URL = 'http://yann.lecun.com/exdb/mnist/' +def maybe_download(filename, work_directory): + """Download the data from Yann's website, unless it's already here.""" + if not os.path.exists(work_directory): + os.mkdir(work_directory) + filepath = os.path.join(work_directory, filename) + if not os.path.exists(filepath): + filepath, _ = urllib.urlretrieve(SOURCE_URL + filename, filepath) + statinfo = os.stat(filepath) + print('Succesfully downloaded', filename, statinfo.st_size, 'bytes.') + return filepath +def _read32(bytestream): + dt = numpy.dtype(numpy.uint32).newbyteorder('>') + return numpy.frombuffer(bytestream.read(4), dtype=dt) +def extract_images(filename): + """Extract the images into a 4D uint8 numpy array [index, y, x, depth].""" + print('Extracting', filename) + with gzip.open(filename) as bytestream: + magic = _read32(bytestream) + if magic != 2051: + raise ValueError( + 'Invalid magic number %d in MNIST image file: %s' % + (magic, filename)) + num_images = _read32(bytestream) + rows = _read32(bytestream) + cols = _read32(bytestream) + buf = bytestream.read(rows * cols * num_images) + data = numpy.frombuffer(buf, dtype=numpy.uint8) + data = data.reshape(num_images, rows, cols, 1) + return data +def dense_to_one_hot(labels_dense, num_classes=10): + """Convert class labels from scalars to one-hot vectors.""" + num_labels = labels_dense.shape[0] + index_offset = numpy.arange(num_labels) * num_classes + labels_one_hot = numpy.zeros((num_labels, num_classes)) + labels_one_hot.flat[index_offset + labels_dense.ravel()] = 1 + return labels_one_hot +def extract_labels(filename, one_hot=False): + """Extract the labels into a 1D uint8 numpy array [index].""" + print('Extracting', filename) + with gzip.open(filename) as bytestream: + magic = _read32(bytestream) + if magic != 2049: + raise ValueError( + 'Invalid magic number %d in MNIST label file: %s' % + (magic, filename)) + num_items = _read32(bytestream) + buf = bytestream.read(num_items) + labels = numpy.frombuffer(buf, dtype=numpy.uint8) + if one_hot: + return dense_to_one_hot(labels) + return labels +class DataSet(object): + def __init__(self, images, labels, fake_data=False): + if fake_data: + self._num_examples = 10000 + else: + assert images.shape[0] == labels.shape[0], ( + "images.shape: %s labels.shape: %s" % (images.shape, + labels.shape)) + self._num_examples = images.shape[0] + # Convert shape from [num examples, rows, columns, depth] + # to [num examples, rows*columns] (assuming depth == 1) + assert images.shape[3] == 1 + images = images.reshape(images.shape[0], + images.shape[1] * images.shape[2]) + # Convert from [0, 255] -> [0.0, 1.0]. + images = images.astype(numpy.float32) + images = numpy.multiply(images, 1.0 / 255.0) + self._images = images + self._labels = labels + self._epochs_completed = 0 + self._index_in_epoch = 0 + @property + def images(self): + return self._images + @property + def labels(self): + return self._labels + @property + def num_examples(self): + return self._num_examples + @property + def epochs_completed(self): + return self._epochs_completed + def next_batch(self, batch_size, fake_data=False): + """Return the next `batch_size` examples from this data set.""" + if fake_data: + fake_image = [1.0 for _ in xrange(784)] + fake_label = 0 + return [fake_image for _ in xrange(batch_size)], [ + fake_label for _ in xrange(batch_size)] + start = self._index_in_epoch + self._index_in_epoch += batch_size + if self._index_in_epoch > self._num_examples: + # Finished epoch + self._epochs_completed += 1 + # Shuffle the data + perm = numpy.arange(self._num_examples) + numpy.random.shuffle(perm) + self._images = self._images[perm] + self._labels = self._labels[perm] + # Start next epoch + start = 0 + self._index_in_epoch = batch_size + assert batch_size <= self._num_examples + end = self._index_in_epoch + return self._images[start:end], self._labels[start:end] +def read_data_sets(train_dir, fake_data=False, one_hot=False): + class DataSets(object): + pass + data_sets = DataSets() + if fake_data: + data_sets.train = DataSet([], [], fake_data=True) + data_sets.validation = DataSet([], [], fake_data=True) + data_sets.test = DataSet([], [], fake_data=True) + return data_sets + TRAIN_IMAGES = 'train-images-idx3-ubyte.gz' + TRAIN_LABELS = 'train-labels-idx1-ubyte.gz' + TEST_IMAGES = 't10k-images-idx3-ubyte.gz' + TEST_LABELS = 't10k-labels-idx1-ubyte.gz' + VALIDATION_SIZE = 5000 + local_file = maybe_download(TRAIN_IMAGES, train_dir) + train_images = extract_images(local_file) + local_file = maybe_download(TRAIN_LABELS, train_dir) + train_labels = extract_labels(local_file, one_hot=one_hot) + local_file = maybe_download(TEST_IMAGES, train_dir) + test_images = extract_images(local_file) + local_file = maybe_download(TEST_LABELS, train_dir) + test_labels = extract_labels(local_file, one_hot=one_hot) + validation_images = train_images[:VALIDATION_SIZE] + validation_labels = train_labels[:VALIDATION_SIZE] + train_images = train_images[VALIDATION_SIZE:] + train_labels = train_labels[VALIDATION_SIZE:] + data_sets.train = DataSet(train_images, train_labels) + data_sets.validation = DataSet(validation_images, validation_labels) + data_sets.test = DataSet(test_images, test_labels) + return data_sets \ No newline at end of file diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/input_ipcai_data.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/input_ipcai_data.py new file mode 100644 index 0000000000..65429d72ad --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/input_ipcai_data.py @@ -0,0 +1,103 @@ +"""Functions for downloading and reading ipcai data.""" +from __future__ import print_function + +import os + +import numpy +import pandas as pd + +from regression.preprocessing import preprocess + + +class DataSet(object): + def __init__(self, images, labels, fake_data=False): + if fake_data: + self._num_examples = 10000 + else: + assert images.shape[0] == labels.shape[0], ( + "images.shape: %s labels.shape: %s" % (images.shape, + labels.shape)) + self._num_examples = images.shape[0] + images = images.astype(numpy.float32) + self._images = images + self._labels = labels + if self._labels.ndim == 1: + self._labels = self._labels[:, numpy.newaxis] + self._epochs_completed = 0 + self._index_in_epoch = 0 + + @property + def images(self): + return self._images + + @property + def labels(self): + return self._labels + + @property + def num_examples(self): + return self._num_examples + + @property + def epochs_completed(self): + return self._epochs_completed + + def next_batch(self, batch_size, fake_data=False): + """Return the next `batch_size` examples from this data set.""" + if fake_data: + fake_image = [1.0 for _ in xrange(784)] + fake_label = 0 + return [fake_image for _ in xrange(batch_size)], [ + fake_label for _ in xrange(batch_size)] + start = self._index_in_epoch + self._index_in_epoch += batch_size + if self._index_in_epoch > self._num_examples: + # Finished epoch + self._epochs_completed += 1 + # Shuffle the data + perm = numpy.arange(self._num_examples) + numpy.random.shuffle(perm) + self._images = self._images[perm] + self._labels = self._labels[perm] + + # Start next epoch + start = 0 + self._index_in_epoch = batch_size + assert batch_size <= self._num_examples + end = self._index_in_epoch + return self._images[start:end], self._labels[start:end] + + +def read_data_sets(dir, fake_data=False): + + class DataSets(object): + pass + data_sets = DataSets() + if fake_data: + data_sets.train = DataSet([], [], fake_data=True) + data_sets.validation = DataSet([], [], fake_data=True) + data_sets.test = DataSet([], [], fake_data=True) + return data_sets + + TRAIN_IMAGES = "ipcai_revision_colon_mean_scattering_train_all_spectrocam.txt" + TEST_IMAGES = "ipcai_revision_colon_mean_scattering_test_all_spectrocam.txt" + + df_train = pd.read_csv(os.path.join(dir, TRAIN_IMAGES), header=[0, 1]) + df_test = pd.read_csv(os.path.join(dir, TEST_IMAGES), header=[0, 1]) + + train_images, train_labels = preprocess(df_train, snr=10.) + test_images, test_labels = preprocess(df_test, snr=10.) + + train_labels = train_labels.values + test_labels = test_labels.values + + VALIDATION_SIZE = 1 + + validation_images = train_images[:VALIDATION_SIZE] + validation_labels = train_labels[:VALIDATION_SIZE] + train_images = train_images[VALIDATION_SIZE:] + train_labels = train_labels[VALIDATION_SIZE:] + data_sets.train = DataSet(train_images, train_labels) + data_sets.validation = DataSet(validation_images, validation_labels) + data_sets.test = DataSet(test_images, test_labels) + return data_sets \ No newline at end of file diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/script_train_tensorflow_model.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/script_train_tensorflow_model.py new file mode 100644 index 0000000000..ee85401f73 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_tensorflow/script_train_tensorflow_model.py @@ -0,0 +1,270 @@ + +import Image +import ImageEnhance +import logging +import datetime + +import SimpleITK as sitk +import tensorflow as tf + +from regression.tensorflow_estimator import multilayer_perceptron, cnn +from regression.tensorflow_dataset import read_data_set +from ipcai2016.tasks_common import * +import commons +from msi.io.nrrdreader import NrrdReader +import msi.normalize as norm +from regression.estimation import estimate_image_tensorflow + +sc = commons.ScriptCommons() +sc.set_root("/media/wirkert/data/Data/2016_02_02_IPCAI/") +sc.create_folders() + +ipcai_dir = sc.get_full_dir("INTERMEDIATES_FOLDER") + +sc.add_dir("SMALL_BOWEL_DATA", + os.path.join(sc.get_dir("DATA_FOLDER"), "small_bowel_images")) + +sc.add_dir("TENSORFLOW_DATA", + os.path.join(sc.get_dir("INTERMEDIATES_FOLDER"), "TensorflowModels")) + +sc.add_dir("SMALL_BOWEL_RESULT", os.path.join(sc.get_dir("RESULTS_FOLDER"), + "small_bowel_tensorflow")) + +sc.add_dir("FILTER_TRANSMISSIONS", + os.path.join(sc.get_dir("DATA_FOLDER"), + "filter_transmissions")) + + + +def plot_image(image, axis): + im2 = axis.imshow(image, interpolation='nearest', alpha=1.0) + axis.xaxis.set_visible(False) + + +class TensorFlowCreateOxyImageTask(luigi.Task): + image_name = luigi.Parameter() + df_prefix = luigi.Parameter() + df_test_prefix = luigi.Parameter() + + def requires(self): + return TensorflowTrainRegressor(df_prefix=self.df_prefix, + df_test_prefix=self.df_test_prefix), \ + Flatfield(flatfield_folder=sc.get_full_dir("FLAT_FOLDER")), \ + SingleMultispectralImage(image=self.image_name), \ + Dark(dark_folder=sc.get_full_dir("DARK_FOLDER")) + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("SMALL_BOWEL_RESULT"), + os.path.split(self.image_name)[1] + + "_" + self.df_prefix + + "_summary" + ".png")) + + + def run(self): + nrrd_reader = NrrdReader() + tiff_ring_reader = TiffRingReader() + # read the flatfield + flat = nrrd_reader.read(self.input()[1].path) + dark = nrrd_reader.read(self.input()[3].path) + # read the msi + nr_filters = len(sc.other["RECORDED_WAVELENGTHS"]) + msi, segmentation = tiff_ring_reader.read(self.input()[2].path, + nr_filters, resize_factor=0.5) + # only take into account not saturated pixels. + segmentation = np.logical_and(segmentation, + (np.max(msi.get_image(), + axis=-1) < 1000.)) + + # correct image setup + filter_nr = int(self.image_name[-6:-5]) + original_order = np.arange(nr_filters) + new_image_order = np.concatenate(( + original_order[nr_filters - filter_nr:], + original_order[:nr_filters - filter_nr])) + # resort msi to restore original order + msimani.get_bands(msi, new_image_order) + # correct by flatfield + msimani.image_correction(msi, flat, dark) + + # create artificial rgb + rgb_image = msi.get_image()[:, :, [2, 3, 1]] + rgb_image /= np.max(rgb_image) + rgb_image *= 255. + + # preprocess the image + # sortout unwanted bands + print "1" + # zero values would lead to infinity logarithm, thus clip. + msi.set_image(np.clip(msi.get_image(), 0.00001, 2. ** 64)) + # normalize to get rid of lighting intensity + norm.standard_normalizer.normalize(msi) + # transform to absorption + msi.set_image(-np.log(msi.get_image())) + # normalize by l2 for stability + norm.standard_normalizer.normalize(msi, "l2") + print "2" + # estimate + path = sc.get_full_dir("TENSORFLOW_DATA") + sitk_image, time = estimate_image_tensorflow(msi, path) + image = sitk.GetArrayFromImage(sitk_image) + + plt.figure() + print "3" + rgb_image = rgb_image.astype(np.uint8) + im = Image.fromarray(rgb_image, 'RGB') + enh_brightness = ImageEnhance.Brightness(im) + im = enh_brightness.enhance(10.) + plotted_image = np.array(im) + top_left_axis = plt.gca() + top_left_axis.imshow(plotted_image, interpolation='nearest') + top_left_axis.xaxis.set_visible(False) + top_left_axis.yaxis.set_visible(False) + + plt.set_cmap("jet") + print "4" + # plot parametric maps + segmentation[0, 0] = 1 + segmentation[0, 1] = 1 + oxy_image = np.ma.masked_array(image[:, :], ~segmentation) + oxy_image[np.isnan(oxy_image)] = 0. + oxy_image[np.isinf(oxy_image)] = 0. + oxy_mean = np.mean(oxy_image) + oxy_image[0, 0] = 0.0 + oxy_image[0, 1] = 1. + + plot_image(oxy_image[:, :], plt.gca()) + + df_image_results = pd.DataFrame(data=np.expand_dims([self.image_name, + oxy_mean * 100., + time], 0), + columns=["image name", + "oxygenation mean [%]", + "time to estimate"]) + + results_file = os.path.join(sc.get_full_dir("SMALL_BOWEL_RESULT"), + "results.csv") + if os.path.isfile(results_file): + df_results = pd.read_csv(results_file, index_col=0) + df_results = pd.concat((df_results, df_image_results)).reset_index( + drop=True + ) + else: + df_results = df_image_results + + df_results.to_csv(results_file) + + plt.savefig(self.output().path, + dpi=250, bbox_inches='tight') + plt.close("all") + + +class TensorflowTrainRegressor(luigi.Task): + df_prefix = luigi.Parameter() + df_test_prefix = luigi.Parameter() + + def output(self): + return luigi.LocalTarget(os.path.join(sc.get_full_dir("TENSORFLOW_DATA"), + "model.ckpt")) + + def requires(self): + return tasks_mc.SpectroCamBatch(self.df_prefix), \ + tasks_mc.SpectroCamBatch(self.df_test_prefix) + + def run(self): + # extract data from the batch + tensorflow_dataset = read_data_set(self.input()[0].path) + test_dataset = read_data_set(self.input()[1].path) + + # train regressor + # Construct the desired model + + # Network Parameters + nr_filters = len(sc.other["RECORDED_WAVELENGTHS"]) + x = tf.placeholder("float", [None, nr_filters, 1, 1]) + # Construct the desired model + keep_prob = tf.placeholder("float") + # pred, regularizers = multilayer_perceptron(x, nr_filters, 100, 1, + # keep_prob) + pred = cnn(x, 1, keep_prob) + # define parameters + learning_rate = 0.0001 + training_epochs = 300 + batch_size = 100 + display_step = 1 + + # Define loss and optimizer + + y = tf.placeholder("float", [None, 1]) + cost = tf.reduce_mean(tf.square(pred - y)) + optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost) + + # Initializing the variables + init = tf.initialize_all_variables() + + saver = tf.train.Saver() # defaults to saving all variables + + # Launch the graph + with tf.Session() as sess: + sess.run(init) + + # Training cycle + for epoch in range(training_epochs): + avg_cost = 0. + total_batch = int(tensorflow_dataset.num_examples/batch_size) + # Loop over all batches + for i in range(total_batch): + batch_xs, batch_ys = tensorflow_dataset.next_batch(batch_size) + # Fit training using batch data + x_image = np.reshape(batch_xs, [-1, nr_filters, 1, 1]) + sess.run(optimizer, feed_dict={x: x_image, y: batch_ys, + keep_prob: 0.75}) + # Compute average loss + avg_cost += sess.run(cost, feed_dict={x: x_image, y: batch_ys, + keep_prob: 1.0})/total_batch + # Display logs per epoch step + if epoch % display_step == 0: + print "Epoch:", '%04d' % (epoch+1), "cost=", "{:.9f}".format(avg_cost) + + # Test model + accuracy = tf.reduce_mean(tf.cast(tf.abs(pred-y), "float")) + x_test_image = np.reshape(test_dataset.images, [-1, nr_filters, 1, 1]) + print "Mean testing error:", accuracy.eval({x: x_test_image, + y: test_dataset.labels, + keep_prob:1.0}) + + print "Optimization Finished!" + saver.save(sess, self.output().path) + + +if __name__ == '__main__': + + # create a folder for the results if necessary + sc.set_root("/media/wirkert/data/Data/2016_02_02_IPCAI/") + sc.create_folders() + + # root folder there the data lies + logging.basicConfig(filename=os.path.join(sc.get_full_dir("LOG_FOLDER"), + "small_bowel_images" + + str(datetime.datetime.now()) + + '.log'), + level=logging.INFO) + luigi.interface.setup_interface_logging() + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + logger = logging.getLogger() + logger.addHandler(ch) + + sch = luigi.scheduler.CentralPlannerScheduler() + w = luigi.worker.Worker(scheduler=sch) + + # determine files to process + files = get_image_files_from_folder(sc.get_full_dir("SMALL_BOWEL_DATA"), + suffix="F0.tiff", fullpath=True) + + for f in files: + main_task = TensorFlowCreateOxyImageTask(image_name=f, + df_prefix="ipcai_revision_colon_mean_scattering_train", + df_test_prefix="ipcai_revision_colon_mean_scattering_test") + w.add(main_task) + w.run() + diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/__init__.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/__init__.py similarity index 100% copy from Modules/Biophotonics/python/inverseMonteCarlo/__init__.py copy to Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/__init__.py diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/input_icai_data.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/input_icai_data.py new file mode 100644 index 0000000000..4aaf742da7 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/input_icai_data.py @@ -0,0 +1,110 @@ +""" +This tutorial introduces logistic regression using Theano and stochastic +gradient descent. + +Logistic regression is a probabilistic, linear classifier. It is parametrized +by a weight matrix :math:`W` and a bias vector :math:`b`. Classification is +done by projecting data points onto a set of hyperplanes, the distance to +which is used to determine a class membership probability. + +Mathematically, this can be written as: + +.. math:: + P(Y=i|x, W,b) &= softmax_i(W x + b) \\ + &= \frac {e^{W_i x + b_i}} {\sum_j e^{W_j x + b_j}} + + +The output of the model or prediction is then done by taking the argmax of +the vector whose i'th element is P(Y=i|x). + +.. math:: + + y_{pred} = argmax_i P(Y=i|x,W,b) + + +This tutorial presents a stochastic gradient descent optimization method +suitable for large datasets. + + +References: + + - textbooks: "Pattern Recognition and Machine Learning" - + Christopher M. Bishop, section 4.3.2 + +""" + +from __future__ import print_function + +import os + +import numpy +import pandas as pd +import numpy as np + +import theano +import theano.tensor as T + +from regression.preprocessing import preprocess + +__docformat__ = 'restructedtext en' + + +def create_dataset(path_to_simulation_results): + + df = pd.read_csv(path_to_simulation_results, header=[0, 1]) + + X, y = preprocess(df, snr=10.) + y = y.values + return X, y + + + +def load_data(data_root): + ''' Loads the dataset + + :type dataset: string + :param dataset: the path to the dataset (here MNIST) + ''' + + TRAIN_IMAGES = os.path.join(data_root, + "ipcai_revision_colon_mean_scattering_train_all_spectrocam.txt") + TEST_IMAGES = os.path.join(data_root, + "ipcai_revision_colon_mean_scattering_test_all_spectrocam.txt") + + train_set = create_dataset(TRAIN_IMAGES) + valid_set = create_dataset(TEST_IMAGES) + test_set = (np.load("sample_image.npy"), np.array([0])) + + def shared_dataset(data_xy, borrow=True): + """ Function that loads the dataset into shared variables + + The reason we store our dataset in shared variables is to allow + Theano to copy it into the GPU memory (when code is run on GPU). + Since copying data into the GPU is slow, copying a minibatch everytime + is needed (the default behaviour if the data is not in a shared + variable) would lead to a large decrease in performance. + """ + data_x, data_y = data_xy + shared_x = theano.shared(numpy.asarray(data_x, + dtype=theano.config.floatX), + borrow=borrow) + shared_y = theano.shared(numpy.asarray(data_y, + dtype=theano.config.floatX), + borrow=borrow) + # When storing data on the GPU it has to be stored as floats + # therefore we will store the labels as ``floatX`` as well + # (``shared_y`` does exactly that). But during our computations + # we need them as ints (we use labels as index, and if they are + # floats it doesn't make sense) therefore instead of returning + # ``shared_y`` we will have to cast it to int. This little hack + # lets ous get around this issue + return shared_x, shared_y + + test_set_x, test_set_y = shared_dataset(test_set, 0) + valid_set_x, valid_set_y = shared_dataset(valid_set) + train_set_x, train_set_y = shared_dataset(train_set) + + rval = [(train_set_x, train_set_y), (valid_set_x, valid_set_y), + (test_set_x, test_set_y)] + return rval + diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/logistic_sgd.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/logistic_sgd.py new file mode 100644 index 0000000000..6ef9f732b3 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/logistic_sgd.py @@ -0,0 +1,176 @@ +""" +This tutorial introduces logistic regression using Theano and stochastic +gradient descent. + +Logistic regression is a probabilistic, linear classifier. It is parametrized +by a weight matrix :math:`W` and a bias vector :math:`b`. Classification is +done by projecting data points onto a set of hyperplanes, the distance to +which is used to determine a class membership probability. + +Mathematically, this can be written as: + +.. math:: + P(Y=i|x, W,b) &= softmax_i(W x + b) \\ + &= \frac {e^{W_i x + b_i}} {\sum_j e^{W_j x + b_j}} + + +The output of the model or prediction is then done by taking the argmax of +the vector whose i'th element is P(Y=i|x). + +.. math:: + + y_{pred} = argmax_i P(Y=i|x,W,b) + + +This tutorial presents a stochastic gradient descent optimization method +suitable for large datasets. + + +References: + + - textbooks: "Pattern Recognition and Machine Learning" - + Christopher M. Bishop, section 4.3.2 + +""" + +from __future__ import print_function + +import numpy + +import theano +import theano.tensor as T + + +__docformat__ = 'restructedtext en' + + +class LogisticRegression(object): + """Multi-class Logistic Regression Class + + The logistic regression is fully described by a weight matrix :math:`W` + and bias vector :math:`b`. Classification is done by projecting data + points onto a set of hyperplanes, the distance to which is used to + determine a class membership probability. + """ + + def __init__(self, input, n_in, n_out): + """ Initialize the parameters of the logistic regression + + :type input: theano.tensor.TensorType + :param input: symbolic variable that describes the input of the + architecture (one minibatch) + + :type n_in: int + :param n_in: number of input units, the dimension of the space in + which the datapoints lie + + :type n_out: int + :param n_out: number of output units, the dimension of the space in + which the labels lie + + """ + # start-snippet-1 + # initialize with 0 the weights W as a matrix of shape (n_in, n_out) + self.W = theano.shared( + value=numpy.zeros( + (n_in, n_out), + dtype=theano.config.floatX + ), + name='W', + borrow=True + ) + # initialize the biases b as a vector of n_out 0s + self.b = theano.shared( + value=numpy.zeros( + (n_out,), + dtype=theano.config.floatX + ), + name='b', + borrow=True + ) + + # symbolic expression for computing the matrix of class-membership + # probabilities + # Where: + # W is a matrix where column-k represent the separation hyperplane for + # class-k + # x is a matrix where row-j represents input training sample-j + # b is a vector where element-k represent the free parameter of + # hyperplane-k + #self.p_y_given_x = T.nnet.softmax(T.dot(input, self.W) + self.b) + self.p_y_given_x = T.max(T.dot(input, self.W) + self.b, axis = 1) + + + # symbolic description of how to compute prediction as class whose + # probability is maximal + self.y_pred = self.p_y_given_x + #self.y_pred = self.p_y_given_x + # end-snippet-1 + + # parameters of the model + self.params = [self.W, self.b] + + # keep track of model input + self.input = input + + def negative_log_likelihood(self, y): + """Return the mean of the negative log-likelihood of the prediction + of this model under a given target distribution. + + .. math:: + + \frac{1}{|\mathcal{D}|} \mathcal{L} (\theta=\{W,b\}, \mathcal{D}) = + \frac{1}{|\mathcal{D}|} \sum_{i=0}^{|\mathcal{D}|} + \log(P(Y=y^{(i)}|x^{(i)}, W,b)) \\ + \ell (\theta=\{W,b\}, \mathcal{D}) + + :type y: theano.tensor.TensorType + :param y: corresponds to a vector that gives for each example the + correct label + + Note: we use the mean instead of the sum so that + the learning rate is less dependent on the batch size + """ + # start-snippet-2 + # y.shape[0] is (symbolically) the number of rows in y, i.e., + # number of examples (call it n) in the minibatch + # T.arange(y.shape[0]) is a symbolic vector which will contain + # [0,1,2,... n-1] T.log(self.p_y_given_x) is a matrix of + # Log-Probabilities (call it LP) with one row per example and + # one column per class LP[T.arange(y.shape[0]),y] is a vector + # v containing [LP[0,y[0]], LP[1,y[1]], LP[2,y[2]], ..., + # LP[n-1,y[n-1]]] and T.mean(LP[T.arange(y.shape[0]),y]) is + # the mean (across minibatch examples) of the elements in v, + # i.e., the mean log-likelihood across the minibatch. + theano.printing.Print('p_y_given_x')(self.p_y_given_x) + theano.printing.Print('y')(y) + return T.mean(T.square(self.p_y_given_x - y)) + #return -T.mean(T.log(self.p_y_given_x)[T.arange(y.shape[0]), y]) + # end-snippet-2 + + def errors(self, y): + """Return a float representing the number of errors in the minibatch + over the total number of examples of the minibatch ; zero one + loss over the size of the minibatch + + :type y: theano.tensor.TensorType + :param y: corresponds to a vector that gives for each example the + correct label + """ + + # check if y has same dimension of y_pred + if y.ndim != self.y_pred.ndim: + raise TypeError( + 'y should have the same shape as self.y_pred', + ('y', y.type, 'y_pred', self.y_pred.type) + ) + # check if y is of the correct datatype + if y.dtype.startswith('float'): + # the T.neq operator returns a vector of 0s and 1s, where 1 + # represents a mistake in prediction + return T.mean(T.abs_(self.y_pred - y)) + else: + raise NotImplementedError() + + + diff --git a/Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/script_train_theano_model.py b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/script_train_theano_model.py new file mode 100644 index 0000000000..32c2a7045e --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/ipcai_to_theano/script_train_theano_model.py @@ -0,0 +1,445 @@ + + +""" +This tutorial introduces the multilayer perceptron using Theano. + + A multilayer perceptron is a logistic regressor where +instead of feeding the input to the logistic regression you insert a +intermediate layer, called the hidden layer, that has a nonlinear +activation function (usually tanh or sigmoid) . One can use many such +hidden layers making the architecture deep. The tutorial will also tackle +the problem of MNIST digit classification. + +.. math:: + + f(x) = G( b^{(2)} + W^{(2)}( s( b^{(1)} + W^{(1)} x))), + +References: + + - textbooks: "Pattern Recognition and Machine Learning" - + Christopher M. Bishop, section 5 + +""" + +from __future__ import print_function + +import os +import sys +import timeit +import time + +import numpy +import numpy as np +import matplotlib.pylab as plt +import theano +import theano.tensor as T + + +from logistic_sgd import LogisticRegression +from input_icai_data import load_data +from ipcai2016.tasks_common import plot_image + +#theano.config.compute_test_value = 'warn' +#theano.config.mode = "FAST_COMPILE" +#theano.config.exception_verbosity='high' + +__docformat__ = 'restructedtext en' + + +# start-snippet-1 +class HiddenLayer(object): + def __init__(self, rng, input, n_in, n_out, W=None, b=None, + activation=T.tanh): + """ + Typical hidden layer of a MLP: units are fully-connected and have + sigmoidal activation function. Weight matrix W is of shape (n_in,n_out) + and the bias vector b is of shape (n_out,). + + NOTE : The nonlinearity used here is tanh + + Hidden unit activation is given by: tanh(dot(input,W) + b) + + :type rng: numpy.random.RandomState + :param rng: a random number generator used to initialize weights + + :type input: theano.tensor.dmatrix + :param input: a symbolic tensor of shape (n_examples, n_in) + + :type n_in: int + :param n_in: dimensionality of input + + :type n_out: int + :param n_out: number of hidden units + + :type activation: theano.Op or function + :param activation: Non linearity to be applied in the hidden + layer + """ + self.input = input + # end-snippet-1 + + # `W` is initialized with `W_values` which is uniformely sampled + # from sqrt(-6./(n_in+n_hidden)) and sqrt(6./(n_in+n_hidden)) + # for tanh activation function + # the output of uniform if converted using asarray to dtype + # theano.config.floatX so that the code is runable on GPU + # Note : optimal initialization of weights is dependent on the + # activation function used (among other things). + # For example, results presented in [Xavier10] suggest that you + # should use 4 times larger initial weights for sigmoid + # compared to tanh + # We have no info for other function, so we use the same as + # tanh. + if W is None: + W_values = numpy.asarray( + rng.uniform( + low=-numpy.sqrt(6. / (n_in + n_out)), + high=numpy.sqrt(6. / (n_in + n_out)), + size=(n_in, n_out) + ), + dtype=theano.config.floatX + ) + if activation == theano.tensor.nnet.sigmoid: + W_values *= 4 + + W = theano.shared(value=W_values, name='W', borrow=True) + + if b is None: + b_values = 0.1+numpy.zeros((n_out,), dtype=theano.config.floatX) + b = theano.shared(value=b_values, name='b', borrow=True) + + self.W = W + self.b = b + + lin_output = T.dot(input, self.W) + self.b + self.output = ( + lin_output if activation is None + else activation(lin_output) + ) + # parameters of the model + self.params = [self.W, self.b] + + +# start-snippet-2 +class MLP(object): + """Multi-Layer Perceptron Class + + A multilayer perceptron is a feedforward artificial neural network model + that has one layer or more of hidden units and nonlinear activations. + Intermediate layers usually have as activation function tanh or the + sigmoid function (defined here by a ``HiddenLayer`` class) while the + top layer is a softmax layer (defined here by a ``LogisticRegression`` + class). + """ + + def __init__(self, rng, input, n_in, n_hidden, n_out): + """Initialize the parameters for the multilayer perceptron + + :type rng: numpy.random.RandomState + :param rng: a random number generator used to initialize weights + + :type input: theano.tensor.TensorType + :param input: symbolic variable that describes the input of the + architecture (one minibatch) + + :type n_in: int + :param n_in: number of input units, the dimension of the space in + which the datapoints lie + + :type n_hidden: int + :param n_hidden: number of hidden units + + :type n_out: int + :param n_out: number of output units, the dimension of the space in + which the labels lie + + """ + + # Since we are dealing with a one hidden layer MLP, this will translate + # into a HiddenLayer with a tanh activation function connected to the + # LogisticRegression layer; the activation function can be replaced by + # sigmoid or any other nonlinear function + self.hiddenLayer1 = HiddenLayer( + rng=rng, + input=input, + n_in=n_in, + n_out=n_hidden, + activation=T.nnet.relu + ) + + self.hiddenLayer2 = HiddenLayer( + rng=rng, + input=self.hiddenLayer1.output, + n_in=n_hidden, + n_out=n_hidden, + activation=T.nnet.relu + ) + + # The logistic regression layer gets as input the hidden units + # of the hidden layer + self.logRegressionLayer = LogisticRegression( + input=self.hiddenLayer2.output, + n_in=n_hidden, + n_out=n_out + ) + # end-snippet-2 start-snippet-3 + # L1 norm ; one regularization option is to enforce L1 norm to + # be small + self.L1 = ( + abs(self.hiddenLayer1.W).sum() + + abs(self.hiddenLayer2.W).sum() + + abs(self.logRegressionLayer.W).sum() + ) + + # square of L2 norm ; one regularization option is to enforce + # square of L2 norm to be small + self.L2_sqr = ( + (self.hiddenLayer1.W ** 2).sum() + + (self.hiddenLayer2.W ** 2).sum() + + (self.logRegressionLayer.W ** 2).sum() + ) + + # negative log likelihood of the MLP is given by the negative + # log likelihood of the output of the model, computed in the + # logistic regression layer + self.negative_log_likelihood = ( + self.logRegressionLayer.negative_log_likelihood + ) + # same holds for the function computing the number of errors + self.errors = self.logRegressionLayer.errors + + self.y_pred = self.logRegressionLayer.y_pred + + # the parameters of the model are the parameters of the two layer it is + # made out of + self.params = self.hiddenLayer1.params +\ + self.hiddenLayer2.params +\ + self.logRegressionLayer.params + # end-snippet-3 + + # keep track of model input + self.input = input + + +def test_mlp(learning_rate=0.001, L1_reg=0.0001, L2_reg=0.0001, n_epochs=1000, + dataset="/media/wirkert/data/Data/2016_02_02_IPCAI/results/intermediate", + batch_size=20, n_hidden=25, + do_timing_test=False): + """ + Demonstrate stochastic gradient descent optimization for a multilayer + perceptron + + This is demonstrated on MNIST. + + :type learning_rate: float + :param learning_rate: learning rate used (factor for the stochastic + gradient + + :type L1_reg: float + :param L1_reg: L1-norm's weight when added to the cost (see + regularization) + + :type L2_reg: float + :param L2_reg: L2-norm's weight when added to the cost (see + regularization) + + :type n_epochs: int + :param n_epochs: maximal number of epochs to run the optimizer + + :type dataset: string + :param dataset: the path of the MNIST dataset file from + http://www.iro.umontreal.ca/~lisa/deep/data/mnist/mnist.pkl.gz + + + """ + + datasets = load_data(dataset) + + train_set_x, train_set_y = datasets[0] + valid_set_x, valid_set_y = datasets[1] + test_set_x, test_set_y = datasets[2] + + # compute number of minibatches for training and validation + n_train_batches = train_set_x.get_value(borrow=True).shape[0] // batch_size + n_valid_batches = valid_set_x.get_value(borrow=True).shape[0] // batch_size + + ###################### + # BUILD ACTUAL MODEL # + ###################### + print('... building the model') + + # allocate symbolic variables for the data + index = T.lscalar() # index to a [mini]batch + x = T.matrix('x') # the data is presented as rasterized images + x.tag.test_value = np.zeros((5000, 8)).astype('float32') + y = T.vector('y') # the labels are presented as 1D vector of + # [int] labels + y.tag.test_value = np.ones(5000).astype('float32') + + rng = numpy.random.RandomState(1234) + + # construct the MLP class + classifier = MLP( + rng=rng, + input=x, + n_in=8, + n_hidden=n_hidden, + n_out=1 + ) + + # start-snippet-4 + # the cost we minimize during training is the negative log likelihood of + # the model plus the regularization terms (L1 and L2); cost is expressed + # here symbolically + cost = ( + classifier.negative_log_likelihood(y) + + L1_reg * classifier.L1 + + L2_reg * classifier.L2_sqr + ) + # end-snippet-4 + + # compiling a Theano function that computes the mistakes that are made + # by the model on a minibatch + test_model = theano.function( + inputs=[], + outputs=classifier.y_pred, + givens={ + x: test_set_x + } + ) + + validate_model = theano.function( + inputs=[index], + outputs=classifier.errors(y), + givens={ + x: valid_set_x[index * batch_size:(index + 1) * batch_size], + y: valid_set_y[index * batch_size:(index + 1) * batch_size] + } + ) + + # start-snippet-5 + # compute the gradient of cost with respect to theta (sorted in params) + # the resulting gradients will be stored in a list gparams + gparams = [T.grad(cost, param) for param in classifier.params] + + # specify how to update the parameters of the model as a list of + # (variable, update expression) pairs + + # given two lists of the same length, A = [a1, a2, a3, a4] and + # B = [b1, b2, b3, b4], zip generates a list C of same size, where each + # element is a pair formed from the two lists : + # C = [(a1, b1), (a2, b2), (a3, b3), (a4, b4)] + updates = [ + (param, param - learning_rate * gparam) + for param, gparam in zip(classifier.params, gparams) + ] + + # compiling a Theano function `train_model` that returns the cost, but + # in the same time updates the parameter of the model based on the rules + # defined in `updates` + train_model = theano.function( + inputs=[index], + outputs=cost, + updates=updates, + givens={ + x: train_set_x[index * batch_size: (index + 1) * batch_size], + y: train_set_y[index * batch_size: (index + 1) * batch_size] + } + ) + # end-snippet-5 + + ############### + # TRAIN MODEL # + ############### + print('... training') + + # early-stopping parameters + patience = 10000 # look as this many examples regardless + patience_increase = 2 # wait this much longer when a new best is + # found + improvement_threshold = 0.995 # a relative improvement of this much is + # considered significant + validation_frequency = min(n_train_batches, patience // 2) + # go through this many + # minibatche before checking the network + # on the validation set; in this case we + # check every epoch + + best_validation_loss = numpy.inf + best_iter = 0 + test_score = 0. + start_time = timeit.default_timer() + + epoch = 0 + done_looping = False + + while (epoch < n_epochs):# and (not done_looping): + epoch = epoch + 1 + for minibatch_index in range(n_train_batches): + + minibatch_avg_cost = train_model(minibatch_index) + # iteration number + iter = (epoch - 1) * n_train_batches + minibatch_index + + if (iter + 1) % validation_frequency == 0: + # compute zero-one loss on validation set + validation_losses = [validate_model(i) for i + in range(n_valid_batches)] + this_validation_loss = numpy.mean(validation_losses) + + print( + 'epoch %i, minibatch %i/%i, validation error %f %%' % + ( + epoch, + minibatch_index + 1, + n_train_batches, + this_validation_loss * 100. + ) + ) + + # if we got the best validation score until now + if this_validation_loss < best_validation_loss: + #improve patience if loss improvement is good enough + if ( + this_validation_loss < best_validation_loss * + improvement_threshold + ): + patience = max(patience, iter * patience_increase) + + best_validation_loss = this_validation_loss + best_iter = iter + + if do_timing_test: + # test it on the test set + start = time.time() + test_predictions = test_model() + end = time.time() + estimation_time = end - start + print("time necessary for estimating image parameters: " + + str(estimation_time) + "s") + print(test_predictions.shape) + + if patience <= iter: + done_looping = True + #break + + end_time = timeit.default_timer() + print(('Optimization complete. Best validation score of %f %% ' + 'obtained at iteration %i, with test performance %f %%') % + (best_validation_loss * 100., best_iter + 1, test_score * 100.)) + print(('The code for file ' + + os.path.split(__file__)[1] + + ' ran for %.2fm' % ((end_time - start_time) / 60.)), file=sys.stderr) + + test_predictions = test_model() + image = test_predictions.reshape(1029,1228) + image[0, 0] = 0.0 + image[0, 1] = 1. + image = np.clip(image, 0., 1.) + plot_image(image) + plt.savefig("sample_image.png", + dpi=250, bbox_inches='tight') + +if __name__ == '__main__': + test_mlp() + diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/__init__.py b/Modules/Biophotonics/python/iMC/scripts/spielewiese/__init__.py similarity index 100% rename from Modules/Biophotonics/python/inverseMonteCarlo/__init__.py rename to Modules/Biophotonics/python/iMC/scripts/spielewiese/__init__.py diff --git a/Modules/Biophotonics/python/iMC/scripts/spielewiese/monte_carlo_single_photon.py b/Modules/Biophotonics/python/iMC/scripts/spielewiese/monte_carlo_single_photon.py new file mode 100644 index 0000000000..05f0afc7dd --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/spielewiese/monte_carlo_single_photon.py @@ -0,0 +1,203 @@ +from collections import OrderedDict +import numpy as np + +import theano +from theano.ifelse import ifelse +import theano.tensor as T +import theano.tensor.inplace as inplace +#from theano.sandbox.rng_mrg import MRG_RandomStreams as RandomStreams +from theano.tensor.shared_randomstreams import RandomStreams +import time + + +#theano.config.compute_test_value = 'warn' +#theano.config.exception_verbosity='high' +#theano.config.profile=True +#theano.config.mode = "FAST_RUN" +#theano.config.mode = "FAST_COMPILE" +#theano.config.scan.allow_gc =True +#theano.config.scan.allow_output_prealloc =False +#theano.optimizer_excluding="more_mem" + +# initializing +rng = RandomStreams(seed=234) +photons = 10**6 +SHELL_MAX = 101 + + +mu_a = T.scalar('mu_a') +# provide Theano with a default test-value +mu_a.tag.test_value = 2. +mu_s = T.scalar('mu_s') +mu_s.tag.test_value = 20. +microns_per_shell = T.scalar('microns_per_shell') +microns_per_shell.tag.test_value = 50. + +albedo = mu_s / (mu_s + mu_a) +shells_per_mfp = 1e4/microns_per_shell/(mu_a+mu_s) + +heat = theano.shared(np.zeros(SHELL_MAX, dtype=theano.config.floatX)) + +x = T.scalar('x') +x.tag.test_value = 0 + +y = T.scalar('y') +y.tag.test_value = 0 + +z = T.scalar('z') +z.tag.test_value = 0 + +u = T.scalar('u') +u.tag.test_value = 0 + +v = T.scalar('v') +v.tag.test_value = 0 + +w = T.scalar('w') +w.tag.test_value = 1 + +weight = T.scalar('weight') +weight.tag.test_value = 1 + + +def one_run(my_x, my_y, my_z, + my_u, my_v, my_w, + my_weight, + my_heat, my_albedo, my_microns_per_shell): + + # move + random = rng.uniform(low=0.00003, high=1.) + t = -T.log(random) + + x_moved = my_x + my_u*t + y_moved = my_y + my_v*t + z_moved = my_z + my_w*t + + # absorb + shell = T.cast(T.sqrt(T.sqr(x_moved) + T.sqr(y_moved) + T.sqr(z_moved)) + * my_microns_per_shell, 'int32') + shell = T.clip(shell, 0, SHELL_MAX-1) + + new_weight = my_weight * my_albedo + + # new direction + xi1 = rng.uniform(low=-1., high=1.) + xi2 = rng.uniform(low=-1., high=1.) + xi_norm = T.sqrt(T.sqr(xi1) + T.sqr(xi2)) + + t_xi = rng.uniform(low=0.000000001, high=1.) + + # rescale xi12 to fit t_xi as norm + xi1 = xi1/xi_norm * T.sqr(t_xi) + xi2 = xi2/xi_norm * T.sqr(t_xi) + + u_new_direction = 2. * t_xi - 1. + v_new_direction = xi1 * T.sqrt((1. - T.sqr(u_new_direction)) / t_xi) + w_new_direction = xi2 * T.sqrt((1. - T.sqr(u_new_direction)) / t_xi) + + # roulette + weight_for_starting_roulette = 0.001 + CHANCE = 0.1 + partakes_roulette = T.switch(T.lt(new_weight, weight_for_starting_roulette), + 1, + 0) + roulette = rng.uniform(low=0., high=1.) + loses_roulette = T.gt(roulette, CHANCE) + # if roulette decides to terminate the photon: set weight to 0 + weight_after_roulette = ifelse(T.and_(partakes_roulette, loses_roulette), + 0., + new_weight) + # if partakes in roulette but does not get terminated + weight_after_roulette = ifelse(T.and_(partakes_roulette, T.invert(loses_roulette)), + weight_after_roulette / CHANCE, + weight_after_roulette) + + new_heat = (1.0 - my_albedo) * my_weight + heat_i = my_heat[shell] + + return (x_moved, y_moved, z_moved,\ + u_new_direction, v_new_direction, w_new_direction,\ + weight_after_roulette),\ + OrderedDict({my_heat: T.inc_subtensor(heat_i, new_heat)}) + + +# one_photon_results, one_photon_updates = theano.scan(fn=one_run, +# outputs_info=[T.zeros_like(x), +# T.zeros_like(y), +# T.zeros_like(z), +# T.zeros_like(u), +# T.zeros_like(v), +# T.ones_like(w), +# T.ones_like(weight)], +# non_sequences=[heat, +# albedo, +# microns_per_shell], +# n_steps=100, +# strict=True) +# +# final_one_photon_heat_result = one_photon_updates[heat] + +# heat_for_one_photon = theano.function(inputs=[x, y, z, +# u, v, w, +# weight, +# mu_a, mu_s, microns_per_shell], +# outputs=final_one_photon_heat_result, +# updates=one_photon_updates) + + +def all_runs(my_x, my_y, my_z, + my_u, my_v, my_w, + my_weight, + my_heat, + my_albedo, my_microns_per_shell): + my_one_photon_results, my_one_photon_updates = theano.scan(fn=one_run, + outputs_info=[T.zeros_like(my_x), + T.zeros_like(my_y), + T.zeros_like(my_z), + T.zeros_like(my_u), + T.zeros_like(my_v), + T.ones_like(my_w), + T.ones_like(my_weight)], + non_sequences=[my_heat, + my_albedo, + my_microns_per_shell], + n_steps=10, + strict=True) + return {my_heat: my_one_photon_updates[my_heat]} + + +all_photon_results, all_photon_updates = theano.scan(fn=all_runs, + outputs_info=None, + non_sequences=[x, y, z, + u, v, w, + weight, + heat, + albedo, microns_per_shell], + n_steps=10, + strict=True) + + +heat_for_all_photons = theano.function(inputs=[x, y, z, + u, v, w, + weight, + mu_a, mu_s, microns_per_shell], + outputs=all_photon_updates[heat], + updates=all_photon_updates) + + + + + +start = time.time() + +print("start simulation") + +print(heat_for_all_photons(0., 0., 0., + 0., 0., 1., + 1., + 2., 20., 50.)) + +end = time.time() +print("end simulation after: " + str(end - start) + " seconds") + + diff --git a/Modules/Biophotonics/python/iMC/scripts/spielewiese/spielewiese.py b/Modules/Biophotonics/python/iMC/scripts/spielewiese/spielewiese.py new file mode 100644 index 0000000000..871864547f --- /dev/null +++ b/Modules/Biophotonics/python/iMC/scripts/spielewiese/spielewiese.py @@ -0,0 +1,130 @@ +from collections import OrderedDict +import numpy as np + +import theano +import theano.tensor as T +from theano.sandbox.rng_mrg import MRG_RandomStreams as RandomStreams +#from theano.tensor.shared_randomstreams import RandomStreams +import time + +#theano.config.compute_test_value = 'warn' +#theano.config.exception_verbosity='high' +# initializing +rng = RandomStreams(seed=234) +photons = 10**6 +SHELL_MAX = 101 + +mu_a = T.scalar('mu_a') +# provide Theano with a default test-value +mu_a.tag.test_value = 2. +mu_s = T.scalar('mu_s') +mu_s.tag.test_value = 20. +microns_per_shell = T.scalar('microns_per_shell') +microns_per_shell.tag.test_value = 50. + +albedo = mu_s / (mu_s + mu_a) +shells_per_mfp = 1e4/microns_per_shell/(mu_a+mu_s) + + + + +finished = theano.shared(np.array(0, dtype='int8')) + +xyz = theano.shared(np.zeros((photons,3), dtype=theano.config.floatX)) + +uvw_np = np.zeros((photons,3), dtype=theano.config.floatX) +uvw_np[:,2] = 1. # w = 1 +uvw = theano.shared(uvw_np) + +#weights_np = np.ones((photons,1), dtype=theano.config.floatX) +weight = theano.shared(np.ones((photons,1), dtype=theano.config.floatX)) + +heat_np = np.zeros((SHELL_MAX,1), dtype=theano.config.floatX) +heat = theano.shared(heat_np) + + + + + + + + +# while sum alive > 0 + +def l2_norm_along_columns(A): + A_normed = T.sqrt(T.sum(T.sqr(A), axis=1)) + A_normed = A_normed.reshape((photons, 1)) + return A_normed + +# move +random = rng.uniform((photons,1), low=0.00003, high=1.) +t = -T.log(random) +t = T.addbroadcast(t, 1) + +xyz_moved = xyz + uvw*t +#theano.printing.Print('xyz_moved')(xyz_moved) + +# absorb +shells = T.cast(l2_norm_along_columns(xyz_moved) * shells_per_mfp, + 'int32') +shells = T.clip(shells, 0, SHELL_MAX-1) +new_heats = (1.0 - albedo) * weight +new_weight = weight * albedo +theano.printing.Print('shells')(shells) + +# new direction +xi12 = rng.uniform((photons,2), low=-1., high=1.) +xi_norm = l2_norm_along_columns(xi12) + +t_xi = rng.uniform((photons,1), low=0.000000001, high=1.) +t_xi = T.addbroadcast(t_xi, 1) + +# rescale xi12 to fit t_xi as norm +xi12 = xi12/xi_norm * T.sqr(t_xi) + +u_new_direction = 2. * t_xi - 1. +vw_new_direction = xi12 * T.sqrt((1. - T.sqr(u_new_direction)) / t_xi) +uvw_new_direction = T.concatenate([u_new_direction, vw_new_direction], axis=1) + +#theano.printing.Print('t_xi')(t_xi) +#theano.printing.Print('vw')(vw_new_direction) +#theano.printing.Print('uvw')(uvw_new_direction) +# roulette +weight_for_starting_roulette = 0.001 +CHANCE = 0.1 +partakes_roulette = T.switch(T.lt(new_weight, weight_for_starting_roulette), + 1, + 0) +roulette = rng.uniform((photons,1), low=0., high=1.) +loses_roulette = T.gt(roulette, CHANCE) +# if roulette decides to ter+minate the photon: set weight to 0 +weight_after_roulette = T.switch(T.and_(partakes_roulette, loses_roulette), + 0., + new_weight) +# if partakes in roulette but does not get terminated +weight_after_roulette = T.switch(T.and_(partakes_roulette, T.invert(loses_roulette)), + weight_after_roulette / CHANCE, + weight_after_roulette) +#theano.printing.Print('new weight')(new_weight) +#theano.printing.Print('partakes_roulette')(partakes_roulette) +#theano.printing.Print('loses_roulette')(loses_roulette) +#theano.printing.Print('weight_after_roulette')(weight_after_roulette) + + +one_cycle = theano.function(inputs=[mu_a, mu_s, microns_per_shell], + outputs=[shells, new_heats], + updates=OrderedDict({xyz: xyz_moved, uvw: uvw_new_direction, + weight: weight_after_roulette, + finished: T.allclose(weight, 0.)})) + + +start = time.time() +print("start simulation") + +while not finished.get_value(): + new_shells, new_heats = one_cycle(2, 20, 50) + +end = time.time() +print("end simulation after: " + str(end - start) + " seconds") + + diff --git a/Modules/Biophotonics/python/iMC/setup.py b/Modules/Biophotonics/python/iMC/setup.py new file mode 100644 index 0000000000..9e04fc9ca8 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/setup.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 7 18:41:50 2015 + +@author: wirkert +""" + +from setuptools import setup, find_packages + +setup(name='MITK-MSI', + version='0.1', + description='Multi spectral imaging (MSI) utilities', + author='Sebastian Wirkert', + author_email='s.wirkert@dkfz-heidelberg.de', + license='BSD', + packages=find_packages(exclude=['test*']), + package_dir={}, + package_data={'data': ['*.txt', '*.mci', '*.nrrd']}, + install_requires=['numpy>=1.10.2', 'scipy', 'scikit-learn>=0.17', + 'SimpleITK>=0.9.0', 'subprocess32', + 'pypng', 'pandas>0.17', 'libtiff'], + entry_points={} # for scripts, add later + ) diff --git a/Modules/Biophotonics/python/iMC/tox.ini b/Modules/Biophotonics/python/iMC/tox.ini new file mode 100644 index 0000000000..51e0c9f594 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/tox.ini @@ -0,0 +1,9 @@ +# content of: tox.ini , put in same dir as setup.py +[tox] +envlist = py27 +[testenv] +deps=discover # install pytest in the venvs +install_command=pip install -f http://www.simpleitk.org/SimpleITK/resources/software.html --trusted-host www.simpleitk.org {opts} {packages} +#changedir=tests +commands=discover + diff --git a/Modules/Biophotonics/python/iMC/tutorials/Monte Carlo Spectra Generation - Basic tutorial.ipynb b/Modules/Biophotonics/python/iMC/tutorials/Monte Carlo Spectra Generation - Basic tutorial.ipynb new file mode 100644 index 0000000000..e5ea2b6e52 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/tutorials/Monte Carlo Spectra Generation - Basic tutorial.ipynb @@ -0,0 +1,1018 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating and manipulating monte carlo spectra using MITK-MSI\n", + "\n", + "In this tutorial we will learn how to\n", + "1. create reflectance spectra from examplary tissues\n", + "2. how to analyse and visualize the created spectra\n", + "3. how to manipulate them\n", + "\n", + "The MITK-MSI software provides a wrapper to the popular MCML approach to simulate how light travels through tissue. This wrapper can be found in mc/sim.py.\n", + "In this tutorial we will utilize our tissue model which uses this wrapper to create the reflectance spectra.\n", + "\n", + "As a prerequisit, you need a MCML monte carlo simulation which uses the format specified [here](http://omlc.org/software/mc/).\n", + "I tested this software with the GPU accelerated version which can be found [here](https://code.google.com/archive/p/gpumcml/)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# 1.1 create spectra - setup simulation environment\n", + "\n", + "# some necessary imports\n", + "import logging\n", + "import numpy as np\n", + "import os\n", + "# everything related to the simulation wrapper\n", + "from mc import sim\n", + "# the factories create batches (tissue samples) and suited tissue models\n", + "from mc import factories\n", + "# function which runs simulations for each wavelength\n", + "from mc.create_spectrum import create_spectrum\n", + "\n", + "# Where does your monte carlo simulation executable resides in?\n", + "MCML_EXECUTABLE = \"/home/wirkert/workspace/monteCarlo/gpumcml/fast-gpumcml/gpumcml.sm_20\"\n", + "# The MCML needs a simulation input file, where shall it be created?\n", + "MCI_FILENAME = \"./temp.mci\"\n", + "# filename of the file with the simulation results. Due to a bug in GPUMCML will always\n", + "# be created in the same folder as the MCML executable\n", + "MCO_FILENAME = \"temp.mco\"\n", + "# The wavelengths for which we want to run our simulation\n", + "WAVELENGTHS = np.arange(450, 720, 2) * 10 ** -9\n", + "\n", + "# we want to create standard colonic tissue as specified in the IPCAI 2016 publication\n", + "# \"Robust Near Real-Time Estimation of Physiological Parameters from Megapixel\n", + "# Multispectral Images with Inverse Monte Carlo and Random Forest Regression\"\n", + "factory = factories.ColonMuscleMeanScatteringFactory()\n", + "# if you want to create data from the generic tissue mentioned in the paper, choose:\n", + "#factory = factories.GenericMeanScatteringFactory()\n", + "\n", + "# create a simulation wrapper\n", + "# the simulation wrapper wraps the mcml executable in python code\n", + "sim_wrapper = sim.SimWrapper()\n", + "# our simulation needs to know where the input file for the simulation\n", + "# shall resign (will be automatically created)\n", + "sim_wrapper.set_mci_filename(MCI_FILENAME)\n", + "# also it needs to know where the simulation executable shall lie in\n", + "sim_wrapper.set_mcml_executable(MCML_EXECUTABLE)\n", + "\n", + "# create the tissue model\n", + "# it is responsible for writing the simulation input file\n", + "tissue_model = factory.create_tissue_model()\n", + "# tell it where the input file shall lie in\n", + "tissue_model.set_mci_filename(sim_wrapper.mci_filename)\n", + "# also set the output filename\n", + "tissue_model.set_mco_filename(MCO_FILENAME)\n", + "# tell it how much photons shall be simulated. Will be set to 10**6 by standard,\n", + "# this is just an example\n", + "tissue_model.set_nr_photons(10**6)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
layer0layer1layer2
vhbsao2a_mieb_miedngvhbsao2a_mie...dngvhbsao2a_mieb_miedng
00.0037800.4475832425.9008821.2860.0009071.360.8726260.0689850.447583912.591151...0.0005031.360.9487000.0344270.4475833150.4940321.2860.0004541.380.804610
10.0211980.1844103022.1165321.2860.0007821.360.9302680.0638040.18441010.000000...0.0007351.360.8306010.0282470.1844103101.2614991.2860.0004301.380.813056
20.0520960.3074451171.3227581.2860.0008971.360.9249330.0210530.3074451433.788625...0.0007381.360.8073480.0066140.3074451002.5369011.2860.0004851.380.943837
30.0767990.4713041599.0386711.2860.0008781.360.9006370.0845040.4713043228.771326...0.0006051.360.9052220.0436740.4713041810.1143031.2860.0005041.380.868953
40.0531230.1081373524.4808851.2860.0010011.360.8334340.0953820.1081371988.134950...0.0006431.360.9209400.0312010.1081371731.7512831.2860.0005421.380.906191
50.0091470.866703372.7547991.2860.0009431.360.8412740.0949150.8667031660.695761...0.0005051.360.8569690.0092740.8667031991.3905631.2860.0004391.380.813544
60.0829030.2060322314.4154111.2860.0006631.360.8844920.0386080.206032459.458466...0.0005731.360.8505740.0637460.2060322728.8206781.2860.0004941.380.801766
70.0407190.2009561858.9101211.2860.0007721.360.8845160.0039300.2009563564.807750...0.0007661.360.9037640.0472570.2009561429.5744071.2860.0004681.380.849055
80.0288100.7296361751.8182481.2860.0008861.360.8133910.0074770.7296361388.286116...0.0008131.360.9141250.0929930.7296364001.4466131.2860.0005041.380.844552
90.0897580.6653973768.7091881.2860.0008001.360.8367110.0156480.6653971298.629577...0.0006201.360.9279040.0480230.6653973015.2893861.2860.0004271.380.893833
\n", + "

10 rows × 21 columns

\n", + "
" + ], + "text/plain": [ + " layer0 layer1 \\\n", + " vhb sao2 a_mie b_mie d n g vhb \n", + "0 0.003780 0.447583 2425.900882 1.286 0.000907 1.36 0.872626 0.068985 \n", + "1 0.021198 0.184410 3022.116532 1.286 0.000782 1.36 0.930268 0.063804 \n", + "2 0.052096 0.307445 1171.322758 1.286 0.000897 1.36 0.924933 0.021053 \n", + "3 0.076799 0.471304 1599.038671 1.286 0.000878 1.36 0.900637 0.084504 \n", + "4 0.053123 0.108137 3524.480885 1.286 0.001001 1.36 0.833434 0.095382 \n", + "5 0.009147 0.866703 372.754799 1.286 0.000943 1.36 0.841274 0.094915 \n", + "6 0.082903 0.206032 2314.415411 1.286 0.000663 1.36 0.884492 0.038608 \n", + "7 0.040719 0.200956 1858.910121 1.286 0.000772 1.36 0.884516 0.003930 \n", + "8 0.028810 0.729636 1751.818248 1.286 0.000886 1.36 0.813391 0.007477 \n", + "9 0.089758 0.665397 3768.709188 1.286 0.000800 1.36 0.836711 0.015648 \n", + "\n", + " ... layer2 \\\n", + " sao2 a_mie ... d n g vhb \n", + "0 0.447583 912.591151 ... 0.000503 1.36 0.948700 0.034427 \n", + "1 0.184410 10.000000 ... 0.000735 1.36 0.830601 0.028247 \n", + "2 0.307445 1433.788625 ... 0.000738 1.36 0.807348 0.006614 \n", + "3 0.471304 3228.771326 ... 0.000605 1.36 0.905222 0.043674 \n", + "4 0.108137 1988.134950 ... 0.000643 1.36 0.920940 0.031201 \n", + "5 0.866703 1660.695761 ... 0.000505 1.36 0.856969 0.009274 \n", + "6 0.206032 459.458466 ... 0.000573 1.36 0.850574 0.063746 \n", + "7 0.200956 3564.807750 ... 0.000766 1.36 0.903764 0.047257 \n", + "8 0.729636 1388.286116 ... 0.000813 1.36 0.914125 0.092993 \n", + "9 0.665397 1298.629577 ... 0.000620 1.36 0.927904 0.048023 \n", + "\n", + " \n", + " sao2 a_mie b_mie d n g \n", + "0 0.447583 3150.494032 1.286 0.000454 1.38 0.804610 \n", + "1 0.184410 3101.261499 1.286 0.000430 1.38 0.813056 \n", + "2 0.307445 1002.536901 1.286 0.000485 1.38 0.943837 \n", + "3 0.471304 1810.114303 1.286 0.000504 1.38 0.868953 \n", + "4 0.108137 1731.751283 1.286 0.000542 1.38 0.906191 \n", + "5 0.866703 1991.390563 1.286 0.000439 1.38 0.813544 \n", + "6 0.206032 2728.820678 1.286 0.000494 1.38 0.801766 \n", + "7 0.200956 1429.574407 1.286 0.000468 1.38 0.849055 \n", + "8 0.729636 4001.446613 1.286 0.000504 1.38 0.844552 \n", + "9 0.665397 3015.289386 1.286 0.000427 1.38 0.893833 \n", + "\n", + "[10 rows x 21 columns]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 1.2 create spectra - create tissue samples for simulation\n", + "\n", + "# setup batch with tissue instances which should be simulated\n", + "batch = factory.create_batch_to_simulate()\n", + "# we want to simulate ten tissue instances in this example\n", + "nr_samples = 10\n", + "df = batch.create_parameters(10)\n", + "\n", + "# lets have a look at the dataframe. Each row corresponds to one tissue instance,\n", + "# each tissue instance is defined by various layers, which all have certain parameters\n", + "# like e.g. oxygenation (here sao2)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
4.5e-074.52e-074.54e-074.56e-074.58e-074.6e-074.62e-074.64e-074.66e-074.68e-07...7e-077.02e-077.04e-077.06e-077.08e-077.1e-077.12e-077.14e-077.16e-077.18e-07
00.3186160.3469430.3712540.3798780.3855570.3890870.3948770.3977620.4010030.404130...0.4947310.4951620.4950680.4954260.4954470.4950080.4950810.4950930.4946230.494477
10.1402660.1896400.2435540.2597920.2755930.2848330.2963580.3023110.3088770.314986...0.4464260.4471380.4486750.4494690.4502970.4514850.4518860.4517670.4520540.452514
20.0217470.0351540.0562930.0651130.0736100.0807250.0889400.0945650.1008130.107133...0.3781550.3791120.3799530.3810480.3822710.3826990.3841090.3847810.3858160.386681
30.0219310.0319950.0459550.0517710.0569900.0616940.0676900.0713280.0760240.080335...0.4224780.4247930.4259650.4280110.4294140.4303710.4321060.4335270.4337730.435063
40.0692870.1071740.1597940.1774210.1961230.2080050.2219370.2313520.2392310.247642...0.4771400.4783930.4811330.4831720.4851440.4867610.4882540.4902850.4917000.492445
50.0456060.0513120.0585810.0627280.0653590.0689120.0734840.0759510.0801960.084404...0.3826210.3819220.3814710.3814510.3804810.3805680.3798410.3793170.3787140.378075
60.0273930.0446680.0708600.0810270.0912480.0985350.1074260.1125430.1184450.123929...0.3713010.3731900.3750970.3773240.3791270.3805240.3824810.3836580.3854440.386918
70.0489080.0788570.1249280.1422370.1596210.1722800.1868360.1968700.2060290.216111...0.4945830.4963940.4973510.4982170.4990100.4994520.5014810.5016000.5023150.503171
80.0867910.1051410.1258770.1359230.1430730.1511150.1602390.1656310.1738780.180887...0.5160760.5163510.5173030.5170440.5167270.5166640.5154350.5152280.5153230.514933
90.0563380.0714790.0890410.0970410.1033080.1094470.1168740.1209010.1278040.133711...0.5066700.5068890.5078580.5081430.5083510.5082180.5090400.5095760.5085060.508549
\n", + "

10 rows × 135 columns

\n", + "
" + ], + "text/plain": [ + " 4.500000e-07 4.520000e-07 4.540000e-07 4.560000e-07 4.580000e-07 \\\n", + "0 0.318616 0.346943 0.371254 0.379878 0.385557 \n", + "1 0.140266 0.189640 0.243554 0.259792 0.275593 \n", + "2 0.021747 0.035154 0.056293 0.065113 0.073610 \n", + "3 0.021931 0.031995 0.045955 0.051771 0.056990 \n", + "4 0.069287 0.107174 0.159794 0.177421 0.196123 \n", + "5 0.045606 0.051312 0.058581 0.062728 0.065359 \n", + "6 0.027393 0.044668 0.070860 0.081027 0.091248 \n", + "7 0.048908 0.078857 0.124928 0.142237 0.159621 \n", + "8 0.086791 0.105141 0.125877 0.135923 0.143073 \n", + "9 0.056338 0.071479 0.089041 0.097041 0.103308 \n", + "\n", + " 4.600000e-07 4.620000e-07 4.640000e-07 4.660000e-07 4.680000e-07 \\\n", + "0 0.389087 0.394877 0.397762 0.401003 0.404130 \n", + "1 0.284833 0.296358 0.302311 0.308877 0.314986 \n", + "2 0.080725 0.088940 0.094565 0.100813 0.107133 \n", + "3 0.061694 0.067690 0.071328 0.076024 0.080335 \n", + "4 0.208005 0.221937 0.231352 0.239231 0.247642 \n", + "5 0.068912 0.073484 0.075951 0.080196 0.084404 \n", + "6 0.098535 0.107426 0.112543 0.118445 0.123929 \n", + "7 0.172280 0.186836 0.196870 0.206029 0.216111 \n", + "8 0.151115 0.160239 0.165631 0.173878 0.180887 \n", + "9 0.109447 0.116874 0.120901 0.127804 0.133711 \n", + "\n", + " ... 7.000000e-07 7.020000e-07 7.040000e-07 7.060000e-07 \\\n", + "0 ... 0.494731 0.495162 0.495068 0.495426 \n", + "1 ... 0.446426 0.447138 0.448675 0.449469 \n", + "2 ... 0.378155 0.379112 0.379953 0.381048 \n", + "3 ... 0.422478 0.424793 0.425965 0.428011 \n", + "4 ... 0.477140 0.478393 0.481133 0.483172 \n", + "5 ... 0.382621 0.381922 0.381471 0.381451 \n", + "6 ... 0.371301 0.373190 0.375097 0.377324 \n", + "7 ... 0.494583 0.496394 0.497351 0.498217 \n", + "8 ... 0.516076 0.516351 0.517303 0.517044 \n", + "9 ... 0.506670 0.506889 0.507858 0.508143 \n", + "\n", + " 7.080000e-07 7.100000e-07 7.120000e-07 7.140000e-07 7.160000e-07 \\\n", + "0 0.495447 0.495008 0.495081 0.495093 0.494623 \n", + "1 0.450297 0.451485 0.451886 0.451767 0.452054 \n", + "2 0.382271 0.382699 0.384109 0.384781 0.385816 \n", + "3 0.429414 0.430371 0.432106 0.433527 0.433773 \n", + "4 0.485144 0.486761 0.488254 0.490285 0.491700 \n", + "5 0.380481 0.380568 0.379841 0.379317 0.378714 \n", + "6 0.379127 0.380524 0.382481 0.383658 0.385444 \n", + "7 0.499010 0.499452 0.501481 0.501600 0.502315 \n", + "8 0.516727 0.516664 0.515435 0.515228 0.515323 \n", + "9 0.508351 0.508218 0.509040 0.509576 0.508506 \n", + "\n", + " 7.180000e-07 \n", + "0 0.494477 \n", + "1 0.452514 \n", + "2 0.386681 \n", + "3 0.435063 \n", + "4 0.492445 \n", + "5 0.378075 \n", + "6 0.386918 \n", + "7 0.503171 \n", + "8 0.514933 \n", + "9 0.508549 \n", + "\n", + "[10 rows x 135 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 1.3 create spectra - run simulation\n", + "\n", + "# add reflectance column to dataframe\n", + "for w in WAVELENGTHS:\n", + " df[\"reflectances\", w] = np.NAN # the reflectances have not been calculated yet, thus set no nan\n", + "\n", + "# for each instance in our batch\n", + "for i in range(df.shape[0]):\n", + " # set the desired element in the dataframe to be simulated\n", + " tissue_model.set_dataframe_row(df.loc[i, :])\n", + " logging.info(\"running simulation \" + str(i) + \" for\\n\" +\n", + " str(tissue_model))\n", + " reflectances = create_spectrum(tissue_model, sim_wrapper, WAVELENGTHS)\n", + " # store in dataframe\n", + " for r, w in zip(reflectances, WAVELENGTHS):\n", + " df[\"reflectances\", w][i] = r\n", + " \n", + "# clean up temporarily created files\n", + "os.remove(MCI_FILENAME)\n", + "created_mco_file = os.path.join(os.path.split(MCML_EXECUTABLE)[0], MCO_FILENAME)\n", + "if os.path.isfile(created_mco_file):\n", + " os.remove(created_mco_file)\n", + "\n", + "# Hooray, finished,\n", + "# now our dataframe also contains reflectances for each tissue instance:\n", + "df[\"reflectances\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "image/png": "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\nwooZFMEAAAAASUVORK5CYII=\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# 2.1 analyse spectra - plot reflectances\n", + "\n", + "# the usual settings for plotting in ipython notebooks\n", + "import matplotlib.pylab as plt\n", + "%matplotlib inline\n", + "\n", + "# let's have a look at our reflectances\n", + "df[\"reflectances\"].T.plot(kind=\"line\")\n", + "plt.ylabel(\"reflectance\")\n", + "plt.xlabel(\"wavelengths [m]\")\n", + "# put legend outside of plot\n", + "plt.gca().legend(loc='center left', bbox_to_anchor=(1, 0.5))\n", + "plt.grid()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# 2.1 analyse spectra - show distribution of blood volume fraction (vhb) and sao2\n", + "\n", + "# now we need some special pandas functions\n", + "import pandas as pd\n", + "\n", + "# we're interested in the distribution of vhb and sao2 in the first layer (layer0)\n", + "df_vhb_sao2 = df[\"layer0\"][[\"vhb\", \"sao2\"]]\n", + "# plot a scatter matrix showing the distribution of vhb and sao2.\n", + "# of course, with this little data this does not really make sense,\n", + "# however it is a useful tool for analysis if much data is available\n", + "pd.tools.plotting.scatter_matrix(df_vhb_sao2, alpha=0.75, figsize=(6, 6))\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "image/png": "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\nYII=\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# 3.1 manipulate spectra - apply sliding average\n", + "# in 3.1 and 3.2 we will adapt the generated spectra to an imaginary imaging system\n", + "# This system has filters with 20nm bandwith (taken care of in 3.1)\n", + "# and takes multispectral images in 10nm steps (taken care of in 3.2)\n", + "\n", + "# the module mc.dfmanipulations was written to provide some basic,\n", + "# often needed manipulations of the calculated spectra\n", + "# all dmfmanipulations are performed inplace, however, the df is also returned.\n", + "import mc.dfmanipulations as dfmani\n", + "\n", + "# first copy to not lose our original data\n", + "df2 = df.copy()\n", + "# We apply a sliding average to our data. This is usefull if \n", + "# we want to see e.g. how the reflectance was recorded by bands with a certain width\n", + "# a sliding average of 11 will take the five left and five right of the current reflectance\n", + "# and average. Because we take 2nm steps of reflectance in our simulation this means\n", + "# a 20nm window.\n", + "dfmani.fold_by_sliding_average(df2, 11)\n", + "\n", + "# lets again plot the reflectances\n", + "df2[\"reflectances\"].T.plot(kind=\"line\")\n", + "plt.ylabel(\"reflectance\")\n", + "plt.xlabel(\"wavelengths [m]\")\n", + "# put legend outside of plot\n", + "plt.gca().legend(loc='center left', bbox_to_anchor=(1, 0.5))\n", + "plt.grid()\n", + "# we can see that the bump at 560nm is \"smoother\"" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# 3.2 manipulate spectra - select certain wavelenghts\n", + "\n", + "# our imaginary imaging system takes images in 10nm steps from 470 to 660nm\n", + "imaging_system_wavelengths = np.arange(470, 670, 10) * 10**-9\n", + "\n", + "df3 = df2.copy()\n", + "dfmani.interpolate_wavelengths(df3, imaging_system_wavelengths)\n", + "\n", + "# let's look at the newly created reflectances\n", + "df3[\"reflectances\"].T.plot(kind=\"line\", marker='o')\n", + "plt.ylabel(\"reflectance\")\n", + "plt.xlabel(\"wavelengths [m]\")\n", + "# put legend outside of plot\n", + "plt.gca().legend(loc='center left', bbox_to_anchor=(1, 0.5))\n", + "plt.grid()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# that's it, folks. If you want, you can save the created dataframe easily to csv:\n", + "df.to_csv(\"results.csv\", index=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/Modules/Biophotonics/python/iMC/tutorials/README.txt b/Modules/Biophotonics/python/iMC/tutorials/README.txt new file mode 100644 index 0000000000..f6fb2aa8d9 --- /dev/null +++ b/Modules/Biophotonics/python/iMC/tutorials/README.txt @@ -0,0 +1,6 @@ +The tutorials are not fletched out, since I want to discuss the general direction first. +I propose IPhython notebooks, as e.g. Google does for the Caffe framework: +http://nbviewer.jupyter.org/github/BVLC/caffe/blob/master/examples/00-classification.ipynb + +I added a tutorial for the monte carlo spectra generation to show how this could look like. + diff --git a/Modules/Biophotonics/python/iMC_ipcai_jcars_save/iMC/tox.ini b/Modules/Biophotonics/python/iMC_ipcai_jcars_save/iMC/tox.ini new file mode 100644 index 0000000000..51e0c9f594 --- /dev/null +++ b/Modules/Biophotonics/python/iMC_ipcai_jcars_save/iMC/tox.ini @@ -0,0 +1,9 @@ +# content of: tox.ini , put in same dir as setup.py +[tox] +envlist = py27 +[testenv] +deps=discover # install pytest in the venvs +install_command=pip install -f http://www.simpleitk.org/SimpleITK/resources/software.html --trusted-host www.simpleitk.org {opts} {packages} +#changedir=tests +commands=discover + diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/README.txt b/Modules/Biophotonics/python/inverseMonteCarlo/README.txt deleted file mode 100644 index e919c111d4..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/README.txt +++ /dev/null @@ -1,19 +0,0 @@ -This folder contains the python scripts for the inverse monte carlo tissue estimation using -regression forests and domain adaption. - -You can find the needed software to run this code in TechtreePython.txt - -Most important scripts are: - -To generate data use the generateAllSpectra.py script and activate what is needed. -You will need a working gpumcml on your computer to execute this code. -The general functions to generate Monte Carlo simulation can be found in the generate/ subfolder. -perfectGrid.py - generates a grid of reflectance spectra without any noise -randomUniform.py - generates randomly drawn reflectance spectra without any noise -randomNonUniform.py - generates randomly drawn reflectance spectra with noise - -optimization.py - to test tissue parameter estimation using optimization algorithms - -randomForest.py - to test tissue parameter estimation using random forests (domain adaption is not implemented yet) - - diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/TechtreePython.txt b/Modules/Biophotonics/python/inverseMonteCarlo/TechtreePython.txt deleted file mode 100644 index 3e26e1925b..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/TechtreePython.txt +++ /dev/null @@ -1,16 +0,0 @@ -To execute the python scripts in this folder you will need: - -A working executable to gpumcml (e.g. gpumcml.sm_20) somewhere on your computer. You can find gpumcml in -https://code.google.com/p/gpumcml/ -Apply patch gpumcml.pathch to gpumcml. This will change the output made by gpumcml slightly to only output things -needed for our simulations. - -Python libraries: -pymiecoated -numpy -scipy 0.15 -sklearn - -Other: -graphviz - diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/calculateWeightsForDomainAdaption.py b/Modules/Biophotonics/python/inverseMonteCarlo/calculateWeightsForDomainAdaption.py deleted file mode 100644 index dcde96873a..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/calculateWeightsForDomainAdaption.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Feb 17 16:14:48 2015 - -estimate the weighting function for performing the domain adaption. - -@author: wirkert -""" - - -import time - -import numpy as np - -from sklearn.grid_search import GridSearchCV -from sklearn.cross_validation import KFold -from sklearn.linear_model import LogisticRegression - - -def calculateWeights(sourceReflectances, targetReflectances): - - labelsSource = np.zeros(sourceReflectances.shape[0]) - labelsTarget = np.ones(targetReflectances.shape[0]) - - allReflectances = np.concatenate((sourceReflectances, targetReflectances)) - allLabels = np.concatenate((labelsSource, labelsTarget)) - - # train logistic regression - - start = time.time() - - print "now starting domain adaptation" - - kf = KFold(allReflectances.shape[0], 5, shuffle=True) - # todo include intercept scaling paramter - param_grid = [ - {'C': np.logspace(-3,6,1000), 'fit_intercept':['True', 'False']} ] - - best_lr = GridSearchCV(LogisticRegression(), param_grid, cv=kf, n_jobs=11) - - best_lr.fit(allReflectances, allLabels) - - end = time.time() - print "time necessary to train the logistic regression [s]: " + str((end - start)) - - sourceProbabilities = best_lr.predict_proba(sourceReflectances) - - return sourceProbabilities[:,1] / sourceProbabilities[:,0] - - #%% test - - - #print "score for uniform test:", best_lr.score(testReflectancesUniform, labelsTestUniform) - #print "score for gauss test :", best_lr.score(testReflectancesGauss, labelsTestGauss) - diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/estimateParametersRealImage.py b/Modules/Biophotonics/python/inverseMonteCarlo/estimateParametersRealImage.py deleted file mode 100644 index 8635ce03a1..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/estimateParametersRealImage.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Feb 19 10:14:34 2015 - -@author: wirkert -""" - - -from randomForest import randomForest - -from calculateWeightsForDomainAdaption import calculateWeights - -import random -import numpy as np -import time - -import matplotlib.pyplot as plt - - - - - -def estimateParametersRealImage(trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation, activateDA): - - sourceReflectancesDA = image[np.nonzero(trainsegmentation)[0], :] - # choose m reflectances for training DA - m = trainingReflectances.shape[0] - sourceReflectancesDA = np.matrix(random.sample(sourceReflectancesDA, m)) - - #%% 2. determine domain adaptation weights - - trainingWeights = np.ones(trainingReflectances.shape[0]) - - if (activateDA): - trainingWeights = calculateWeights(trainingReflectances, sourceReflectancesDA) - - #%% 3. train forest - - rf = randomForest(trainingParameters, trainingReflectances, trainingWeights) - - #%% 4. estimate the parameters for the image - - print "starting to estimate the tissue parameters" - start = time.time() - - estimatedParameters = rf.predict(image) - - # set to zero if not in segmentation mask - #estimatedParameters[np.where(0 == testsegmentation), :] = 0 - - end = time.time() - print "time necessary to estimate parameters for image [s]: " + str((end - start)) - - - #%% save the parametric images TODO delete after everything works -# import Image -# -# for i in np.arange(0,estimatedParameters.shape[1]): -# parameterImage_i = np.reshape(estimatedParameters[:,i], shape) -# im = Image.fromarray(parameterImage_i) -# im.save("data/output/" + "parameterImage_" + str(i) + ".tiff") - - - #%% 6. evaluate data - - # for this, create monte carlo simulation for each - # parameter estimate. The resulted reflectance estimate can then be compared to - # the measured reflectance. - - from setup import systemPaths - from setup import simulation - - import helper.monteCarloHelper as mch - - - - infileString, outfolderMC, outfolderRS, gpumcmlDirectory, gpumcmlExecutable = systemPaths.initPaths() - infile = open(infileString) - - BVFs, Vss, ds, SaO2s, rs, nrSamples, photons, wavelengths, FWHM, eHbO2, eHb, nrSimulations = simulation.noisy() - - # the estimated parameters within the segmentation - estimatedParametersOnlySegmented = estimatedParameters[np.nonzero(testsegmentation)[0], :] - # the image reflectances from which these parameters where estimated - inputReflectancesOnlySegmented = image[np.nonzero(testsegmentation)[0], :] - - # index vector for selecting n samples from this data - indices = np.arange(0, estimatedParametersOnlySegmented.shape[0], 1) - # choose n - n = 20 - nSamples = random.sample(indices, n) - estimatedParametersOnlySegmented = estimatedParametersOnlySegmented[nSamples] - inputReflectancesOnlySegmented = inputReflectancesOnlySegmented[nSamples] - - # placeholder for the reflectance computed from MC with the estimated parameters - reflectancesFromEstimatedParameters = np.zeros((inputReflectancesOnlySegmented.shape[0], inputReflectancesOnlySegmented.shape[1]+1)) - - #wavelengths = np.delete(wavelengths, [2, 7]) - - for i, (BVF, Vs, d) in enumerate(estimatedParametersOnlySegmented): - - - print('starting simulation ' + str(i) + ' of ' + str(estimatedParametersOnlySegmented.shape[0])) - - for j, wavelength in enumerate(wavelengths): - - reflectanceValue = mch.runOneSimulation( - wavelength, eHbO2, eHb, - infile, outfolderMC, gpumcmlDirectory, gpumcmlExecutable, - BVF, Vs, d, - # np.mean(rs), SaO2, - # submucosa_BVF=sm_BVF, submucosa_Vs=sm_Vs, submucosa_SaO2=SaO2, - Fwhm = FWHM, nrPhotons=photons) - - - #print((BVF, Vs, d, wavelength)) - reflectancesFromEstimatedParameters[i, j] = reflectanceValue - - - # correct these reflectances by image quotient - reflectancesFromEstimatedParameters = mch.normalizeImageQuotient(reflectancesFromEstimatedParameters) - - wavelengths = mch.removeIqWavelength(wavelengths) - #%% plot data for nicer inspection - - from sklearn.metrics import r2_score - - r2Score = r2_score(reflectancesFromEstimatedParameters.T, inputReflectancesOnlySegmented.T) - - - print("r2Score for random forest estimatation of:", str(r2Score)) - - - #%% sort by wavelength: - - for plot_i in range(n): - - sortedIndices = sorted(range(len(wavelengths)), key=lambda k: wavelengths[k]) - - plt.figure() - plt.plot(wavelengths[sortedIndices], reflectancesFromEstimatedParameters[plot_i,sortedIndices], 'g-o') - plt.plot(wavelengths[sortedIndices], inputReflectancesOnlySegmented[plot_i,sortedIndices], 'b-o') - print(str(r2_score(reflectancesFromEstimatedParameters[plot_i, :], inputReflectancesOnlySegmented[plot_i, :]))) - plt.legend(["estimated", "measurement"]) - plt.xlabel("wavelength [m]") - plt.ylabel("normalized reflectance") - plt.savefig("data/output/example_fit_" + str(plot_i) + '.png') - - return estimatedParameters, r2Score, reflectancesFromEstimatedParameters, inputReflectancesOnlySegmented \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/evaluateImages.py b/Modules/Biophotonics/python/inverseMonteCarlo/evaluateImages.py deleted file mode 100644 index 76a0b50d2c..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/evaluateImages.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Feb 26 17:53:53 2015 - -@author: wirkert -""" - -import sys - -sys.path.append('/home/wirkert/workspace/promotion/MITK/Modules/Biophotonics/python/inverseMonteCarlo') - -import os -from setup import data -from setup import systemPaths -import Image -import numpy as np -import csv - -from estimateParametersRealImage import estimateParametersRealImage - - - - - -#%% little helper function -def saveResults(result, shape, r2Score, dataFolder, imageName): - for i in np.arange(0,result.shape[1]): - parameterImage_i = np.reshape(result[:,i], shape) - im = Image.fromarray(parameterImage_i) - im.save(dataFolder + imageName + "_parameterImage_" + str(i) + ".tiff") - - - with open(dataFolder + "result_" + imageName + '.csv', 'wb') as csvfile: - spamwriter = csv.writer(csvfile, delimiter=';', - quotechar='|', quoting=csv.QUOTE_MINIMAL) - spamwriter.writerow([imageName]) - spamwriter.writerow(['r2Score', str(r2Score)]) - - - - -# the folder with the reflectance spectra -dataFolder = systemPaths.getOutputFolder() - - - -##%% Evaluate CC2 Mitte links data -#imageToLoad = "CC2_links" -# -# -###%% get data -#trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation = \ -# data.realImage(dataFolder, imageToLoad) -# -####%% do evaluation with out domain adaptation -#resName = imageToLoad + "_NODA" -#resultCC2linksNODA, r2ScoreCC2linksNODA, backProjectedReflectancesCC2linksNODA, realReflectanceCC2linksNODA = estimateParametersRealImage(trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation, False) -#saveResults(resultCC2linksNODA, shape, r2ScoreCC2linksNODA, dataFolder, resName) - - -#%% Evaluate CC2 Mitte rechts data -imageToLoad = "CC2_rechts" - - -##%% get data -trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation = \ - data.realImage(dataFolder, imageToLoad) - -####%% do evaluation with out domain adaptation -#resName = imageToLoad + "_NODA" -#resultCC2rechtsNODA, r2ScoreCC2rechtsNODA, backProjectedReflectancesCC2rechtsNODA, realReflectanceCC2rechtsNODA = estimateParametersRealImage(trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation, False) -#saveResults(resultCC2rechtsNODA, shape, r2ScoreCC2rechtsNODA, dataFolder, resName) - -###%% do evaluation with domain adaptation -resName = imageToLoad + "_DA" -resultCC2rechtsDA, r2ScoreCC2rechtsDA, backProjectedReflectancesCC2rechtsDA, realReflectanceCC2rechtsDA = estimateParametersRealImage(trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation, True) -saveResults(resultCC2rechtsDA, shape, r2ScoreCC2rechtsDA, dataFolder, resName) - - - -##%% Evaluate CC1 unten data -#imageToLoad = "CC1" -# -# -###%% get data -#trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation = \ -# data.realImage(dataFolder, imageToLoad) -# -####%% do evaluation with domain adaptation -##resName = imageToLoad + "_DA" -##resultCC1DA, r2ScoreCC1DA, backProjectedReflectancesCC1DA, realReflectancesCC1DA = estimateParametersRealImage(trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation, True) -##saveResults(resultCC1DA, shape, r2ScoreCC1DA, dataFolder, resName) -# -# -##%% do evaluation without domain adaptation -#resName = imageToLoad + "_NODA" -#resultCC1NODA, r2ScoreCC1NODA, backProjectedReflectancesCC1NODA, realReflectancesCC1NODA = estimateParametersRealImage(trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation, False) -#saveResults(resultCC1NODA, shape, r2ScoreCC1NODA, dataFolder, resName) -# -# -###%% Evaluate CC1 middle data -#imageToLoad = "CC1_2" -# -# -##%% get data -#trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation = \ -# data.realImage(dataFolder, imageToLoad) -# -###%% do evaluation with domain adaptation -##resultCC2DA, r2ScoreCC2DA, backProjectedReflectancesCC2DA, realReflectancesCC2DA = estimateParametersRealImage(trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation, True) -##saveResults(resultCC2DA, shape, r2ScoreCC2DA, dataFolder, imageToLoad + "_DA") -# -##%% do evaluation without domain adaptation -#resultCC2NODA, r2ScoreCC2NODA, backProjectedReflectancesCC2NODA, realReflectancesCC2NODA = estimateParametersRealImage(trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation, False) -#saveResults(resultCC2NODA, shape, backProjectedReflectancesCC2NODA, dataFolder, imageToLoad + "_NODA") \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/evaluateOptimization.py b/Modules/Biophotonics/python/inverseMonteCarlo/evaluateOptimization.py deleted file mode 100644 index 6ef02f3780..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/evaluateOptimization.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Feb 24 09:38:09 2015 - -@author: wirkert -""" - -import numpy as np -import matplotlib.pyplot as plt -from setup import data -from optimization import optimization - -#%% load data - -dataFolder = "data/output/" - -trainingParameters, trainingReflectances, testParameters, testReflectances = \ - data.perfect(dataFolder) - -dummy1, dummy2, testParameters, testReflectances = \ - data.noisy(dataFolder) - -testingErrors, r2Score = optimization(trainingParameters, trainingReflectances, testParameters, testReflectances) - -#%% test - -print("error distribution BVF, Volume fraction") -print("median: " + str(np.median(testingErrors, axis=0))) -print("lower quartile: " + str(np.percentile(testingErrors, 25, axis=0))) -print("higher quartile: " + str(np.percentile(testingErrors, 75, axis=0))) -print("r2Score", str(r2Score)) \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/evaluateRandomForest.py b/Modules/Biophotonics/python/inverseMonteCarlo/evaluateRandomForest.py deleted file mode 100644 index 65c2b03db0..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/evaluateRandomForest.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Feb 24 09:45:05 2015 - -@author: wirkert -""" - -import numpy as np - -from setup import data -from randomForest import randomForest - - -#%% load data -# the folder with the reflectance spectra -dataFolder = 'data/output/' - -trainingParameters, trainingReflectances, testParameters, testReflectances = \ - data.noisy(dataFolder) - - -trainingWeights = np.ones((trainingParameters.shape[0],)) - -#%% train forest - -rf = randomForest(trainingParameters, trainingReflectances, trainingWeights) - -#%% test - -#with open("iris.dot", 'w') as f: -# f = tree.export_graphviz(rf, out_file=f) - -# predict test reflectances and get absolute errors. - -absErrors = np.abs(rf.predict(testReflectances) - testParameters) - -r2Score = rf.score(testReflectances, testParameters) - -#import matplotlib.pyplot as plt - -print("absolute error distribution BVF, Volume fraction") -print("median: " + str(np.median(absErrors, axis=0))) -print("lower quartile: " + str(np.percentile(absErrors, 25, axis=0))) -print("higher quartile: " + str(np.percentile(absErrors, 75, axis=0))) -print("r2Score", str(r2Score)) diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/generate/noisyRandom.py b/Modules/Biophotonics/python/inverseMonteCarlo/generate/noisyRandom.py deleted file mode 100644 index 33f8d1f691..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/generate/noisyRandom.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Feb 6 16:14:57 2015 - -@author: wirkert -""" - -import time -import datetime -import random - -import numpy as np - -print __name__ - -from helper import monteCarloHelper as mch -from setup import simulation -from setup import systemPaths - - -def noisyRandom(generatedFilename): - - - infileString, outfolderMC, outfolderRS, gpumcmlDirectory, gpumcmlExecutable = systemPaths.initPaths() - infile = open(infileString) - - BVFs, Vss, ds, SaO2s, rs, nrSamples, photons, wavelengths, FWHM, eHbO2, eHb, nrSimulations = simulation.noisy() - - - reflectances = np.zeros((nrSimulations, len(wavelengths))) - parameters = np.zeros((nrSimulations, 7)) - - print('start simulations...') - - #%% start program logic - start = time.time() - - - - for i in range(nrSimulations): - - print('starting simulation ' + str(i) + ' of ' + str(nrSimulations)) - - BVF = random.uniform(min(BVFs), max(BVFs)) - Vs = random.uniform(min(Vss), max(Vss)) - d = random.uniform(min(ds), max(ds)) - SaO2= random.uniform(min(SaO2s), max(SaO2s)) - r = random.uniform(min(rs), max(rs)) - - min_sm_BVF = max(min(BVFs), 0.03) - sm_BVF = random.uniform(min_sm_BVF, max(BVFs)) - sm_Vs = random.uniform(min(Vss), max(Vss)) - - parameters[i,:] = np.array([BVF, Vs, d, SaO2, r, sm_BVF, sm_Vs]) - - - for j, wavelength in enumerate(wavelengths): - - reflectanceValue = mch.runOneSimulation( - wavelength, eHbO2, eHb, - infile, outfolderMC, gpumcmlDirectory, gpumcmlExecutable, - BVF, Vs, d, - r, SaO2, - submucosa_BVF=sm_BVF, submucosa_Vs=sm_Vs, submucosa_SaO2=SaO2, - Fwhm = FWHM, nrPhotons=photons) - - - print((BVF, Vs, d, SaO2, r, wavelength, sm_BVF, sm_Vs)) - - # here, summarize result from wavelength in reflectance spectrum - reflectances[i, j] = reflectanceValue - - - - - infile.close() - - # save the reflectance results! - now = datetime.datetime.now().strftime("%Y%B%d%I:%M%p") - np.save(outfolderRS + now + generatedFilename + "reflectances" + str(photons) + "photons", reflectances) - np.save(outfolderRS + now + generatedFilename + str(nrSimulations) + "parameters", parameters) - - end = time.time() - print "total time to generate noisy random data: " + str((end - start)) \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/generate/perfectGrid.py b/Modules/Biophotonics/python/inverseMonteCarlo/generate/perfectGrid.py deleted file mode 100644 index dd7b736675..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/generate/perfectGrid.py +++ /dev/null @@ -1,77 +0,0 @@ -import time -import itertools -import datetime - -import numpy as np - -from helper import monteCarloHelper as mch -from setup import simulation -from setup import systemPaths - -""" -generating: -_____ -1. - All the monte carlo simulations -2. - Reflectance over wavelengths for the given parameter ranges (assuming a - infinitly wide, uniform light source) - -TODO: -_____ - -""" - -def perfectGrid(generatedFilename): - - infileString, outfolderMC, outfolderRS, gpumcmlDirectory, gpumcmlExecutable = systemPaths.initPaths() - infile = open(infileString) - - BVFs, Vss, ds, SaO2s, rs, nrSamples, photons, wavelengths, FWHM, eHbO2, eHb, nrSimulations = simulation.perfect() - - reflectances = np.zeros((nrSamples, len(wavelengths))) - - print('start simulations...') - - - #%% start program logic - start = time.time() - - currentSimulation = 0 - - params = itertools.product(BVFs, Vss, ds, SaO2s, rs) - paramsList = list(itertools.product(BVFs, Vss, ds, SaO2s, rs)) - - for BVF, Vs, d, SaO2, r in params: - - print('starting simulation ' + str(currentSimulation) + ' of ' + str(nrSamples)) - for idx, wavelength in enumerate(wavelengths): - - # here the magic happens: run monte carlo simulation for tissue parameters - # and specific wavelength - reflectanceValue = mch.runOneSimulation( - wavelength, eHbO2, eHb, - infile, outfolderMC, gpumcmlDirectory, gpumcmlExecutable, - BVF, Vs, d, - r, SaO2, - Fwhm = FWHM, nrPhotons=photons) - - print((BVF, Vs, d, SaO2, r, wavelength)) - - # here, summarize result from wavelength in reflectance spectrum - reflectances[currentSimulation, idx] = reflectanceValue - - currentSimulation += 1 - - - - - infile.close() - - # save the reflectance results! - now = datetime.datetime.now().strftime("%Y%B%d%I:%M%p") - np.save(outfolderRS + now + generatedFilename + "reflectances" + str(photons) + "photons", reflectances) - np.save(outfolderRS + now + generatedFilename + str(nrSamples) + "parameters", paramsList) - - end = time.time() - print "total time for generating perfect data on grid: " + str((end - start)) \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/generate/randomNonUniform.py b/Modules/Biophotonics/python/inverseMonteCarlo/generate/randomNonUniform.py deleted file mode 100644 index 2d3292c4df..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/generate/randomNonUniform.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Feb 17 15:15:23 2015 - -@author: wirkert -""" - -# -*- coding: utf-8 -*- -""" -Created on Fri Feb 6 16:14:57 2015 - -@author: wirkert -""" - -import time -import datetime -import random - -import numpy as np - -from helper import monteCarloHelper as mch -from setup import simulation -from setup import systemPaths - -def drawFromNormal(parameterVector): - """ - helper function to draw a sample from a normal distribution clipped by - the boarders of the parameter range - """ - # draw sample from some normal distribution - sample = random.gauss(np.mean(parameterVector), (max(parameterVector) - min (parameterVector)) / 10.0) - # clip it to be in the allowed range - sample = np.clip(sample, min(parameterVector), max(parameterVector)) - return sample - - - -def randomNonUniform(generatedFilename): - - - infileString, outfolderMC, outfolderRS, gpumcmlDirectory, gpumcmlExecutable = systemPaths.initPaths() - infile = open(infileString) - - - BVFs, Vss, ds, SaO2s, rs, nrSamples, photons, wavelengths, FWHM, eHbO2, eHb, nrSimulations = simulation.noisy() - - reflectances = np.zeros((nrSimulations, len(wavelengths))) - parameters = np.zeros((nrSimulations, 8)) - - print('start simulations...') - - #%% start program logic - start = time.time() - - - for i in range(nrSimulations): - - print('starting simulation ' + str(i) + ' of ' + str(nrSimulations)) - - BVF = drawFromNormal(BVFs) - Vs = drawFromNormal(Vss) - d = drawFromNormal(ds) - SaO2= drawFromNormal(SaO2s) - r = drawFromNormal(rs) - - min_sm_BVF = max(min(BVFs), 0.03) - sm_BVF = drawFromNormal([min_sm_BVF, max(BVFs)]) - sm_Vs = drawFromNormal(Vss) - sm_SaO2= drawFromNormal(SaO2s) - - parameters[i,:] = np.array([BVF, Vs, d, SaO2, r, sm_BVF, sm_Vs, sm_SaO2]) - - - for j, wavelength in enumerate(wavelengths): - - - reflectanceValue = mch.runOneSimulation( - wavelength, eHbO2, eHb, - infile, outfolderMC, gpumcmlDirectory, gpumcmlExecutable, - BVF, Vs, d, - r, SaO2, - Fwhm = FWHM, nrPhotons=photons) - - - print((BVF, Vs, d, SaO2, r, wavelength)) - - # here, summarize result from wavelength in reflectance spectrum - reflectances[i, j] = reflectanceValue - - - - - infile.close() - - # save the reflectance results! - now = datetime.datetime.now().strftime("%Y%B%d%I:%M%p") - np.save(outfolderRS + now + generatedFilename + "reflectances" + str(photons) + "photons", reflectances) - np.save(outfolderRS + now + generatedFilename + str(nrSimulations) + "parameters", parameters) - - end = time.time() - print "total time for generating random non uniform data: " + str((end - start)) \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/generate/randomUniform.py b/Modules/Biophotonics/python/inverseMonteCarlo/generate/randomUniform.py deleted file mode 100644 index 862b8249c9..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/generate/randomUniform.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Feb 6 16:14:57 2015 - -@author: wirkert -""" - -import time -import datetime -import random - -import numpy as np - -from helper import monteCarloHelper as mch -from setup import simulation -from setup import systemPaths - -def randomUniform(generatedFilename): - - infileString, outfolderMC, outfolderRS, gpumcmlDirectory, gpumcmlExecutable = systemPaths.initPaths() - infile = open(infileString) - - - BVFs, Vss, ds, SaO2s, rs, nrSamples, photons, wavelengths, FWHM, eHbO2, eHb, nrSimulations = simulation.perfect() - - reflectances = np.zeros((nrSimulations, len(wavelengths))) - parameters = np.zeros((nrSimulations, 4)) - - print('start simulations...') - - #%% start program logic - start = time.time() - - - - for i in range(nrSimulations): - - print('starting simulation ' + str(i) + ' of ' + str(nrSimulations)) - - BVF = random.uniform(min(BVFs), max(BVFs)) - Vs = random.uniform(min(Vss), max(Vss)) - d = random.uniform(min(ds), max(ds)) - r = random.uniform(min(rs), max(rs)) - SaO2= random.uniform(min(SaO2s), max(SaO2s)) - - parameters[i,:] = np.array([BVF, Vs, d, SaO2]) - - - for j, wavelength in enumerate(wavelengths): - - - reflectanceValue = mch.runOneSimulation( - wavelength, eHbO2, eHb, - infile, outfolderMC, gpumcmlDirectory, gpumcmlExecutable, - BVF, Vs, d, - r, SaO2, - Fwhm = FWHM, nrPhotons=photons) - - - print((BVF, Vs, d, SaO2, r, wavelength)) - - # here, summarize result from wavelength in reflectance spectrum - reflectances[i, j] = reflectanceValue - - - - - infile.close() - - # save the reflectance results! - now = datetime.datetime.now().strftime("%Y%B%d%I:%M%p") - np.save(outfolderRS + now + generatedFilename + "reflectances" + str(photons) + "photons", reflectances) - np.save(outfolderRS + now + generatedFilename + str(nrSimulations) + "parameters", parameters) - - end = time.time() - print "total time to generate random uniform data: " + str((end - start)) \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/generateAllSpectra.py b/Modules/Biophotonics/python/inverseMonteCarlo/generateAllSpectra.py deleted file mode 100644 index b540e91f1f..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/generateAllSpectra.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Feb 17 18:13:13 2015 - -this script will call the other generate scripts to generate all the data neccesary -for our experiments - -@author: wirkert -""" - - -from generate.noisyRandom import noisyRandom -from generate.randomUniform import randomUniform -from generate.perfectGrid import perfectGrid - -# training noisy spectra -noisyRandom("NoisyRandomTraining") - -# testing noisy spectra -noisyRandom("NoisyRandomTesting") - -# random uniform non-noisy spectra -randomUniform("UniformRandom") - -# training perfect data on grid -perfectGrid("PerfectGridTraining") - diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/gpumcml.patch b/Modules/Biophotonics/python/inverseMonteCarlo/gpumcml.patch deleted file mode 100644 index a63a6dd906..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/gpumcml.patch +++ /dev/null @@ -1,122 +0,0 @@ ---- /home/wirkert/workspace/monteCarlo/gpumcml_unpatched/fast-gpumcml/gpumcml_io.c 2010-06-11 12:45:12.000000000 +0200 -+++ /home/wirkert/workspace/monteCarlo/gpumcml/fast-gpumcml/gpumcml_io.c 2015-02-02 14:10:09.421555000 +0100 -@@ -204,39 +204,6 @@ - fprintf(pFile_outp,"%G \t\t #Transmittance [-]\n",(double)T/scale1); - - -- // Calculate and write A_l -- fprintf(pFile_outp,"\nA_l #Absorption as a function of layer. [-]\n"); -- z=0; -- for(l=1;l<=sim->n_layers;l++) -- { -- temp=0; -- while(((double)z+0.5)*dz<=sim->layers[l].z_max) -- { --#if 0 -- for(r=0;rA_rz[z*nr+r]; --#else -- for (r = 0; r < nr; ++r) temp += HostMem->A_rz[r*nz + z]; --#endif -- z++; -- if(z==nz)break; -- } -- fprintf(pFile_outp,"%G\n",(double)temp/scale1); -- } -- -- // Calculate and write A_z -- scale2=scale1*dz; -- fprintf(pFile_outp,"\nA_z #A[0], [1],..A[nz-1]. [1/cm]\n"); -- for(z=0;zA_rz[z*nr+r]; --#else -- for (r = 0; r < nr; ++r) temp += HostMem->A_rz[r*nz + z]; --#endif -- fprintf(pFile_outp,"%E\n",(double)temp/scale2); -- } -- - // Calculate and write Rd_r - fprintf(pFile_outp,"\nRd_r #Rd[0], [1],..Rd[nr-1]. [1/cm2]\n"); - for(r=0;rRd_ra[a*nr+r]; -- scale2=scale1*4*PI_const*sin((a+0.5)*da)*sin(da/2); -- fprintf(pFile_outp,"%E\n",(double)temp/scale2); -- } -- -- // Calculate and write Tt_r -- fprintf(pFile_outp,"\nTt_r #Tt[0], [1],..Tt[nr-1]. [1/cm2]\n"); -- for(r=0;rTt_ra[a*nr+r]; -- scale2=scale1*2*PI_const*(r+0.5)*dr*dr; -- fprintf(pFile_outp,"%E\n",(double)temp/scale2); -- } -- -- // Calculate and write Tt_a -- fprintf(pFile_outp,"\nTt_a #Tt[0], [1],..Tt[na-1]. [sr-1]\n"); -- for(a=0;aTt_ra[a*nr+r]; -- scale2=scale1*4*PI_const*sin((a+0.5)*da)*sin(da/2); -- fprintf(pFile_outp,"%E\n",(double)temp/scale2); -- } -- -- -- // Scale and write A_rz -- i=0; -- fprintf(pFile_outp,"\n# A[r][z]. [1/cm3]\n# A[0][0], [0][1],..[0][nz-1]\n# A[1][0], [1][1],..[1][nz-1]\n# ...\n# A[nr-1][0], [nr-1][1],..[nr-1][nz-1]\nA_rz\n"); -- for(r=0;rA_rz[z*nr+r]/scale2); --#else -- fprintf(pFile_outp, " %E ", (double)HostMem->A_rz[r*nz + z] / scale2); --#endif -- if((i++)==4){i=0;fprintf(pFile_outp,"\n");} -- } -- } -- -- // Scale and write Rd_ra -- i=0; -- fprintf(pFile_outp,"\n\n# Rd[r][angle]. [1/(cm2sr)].\n# Rd[0][0], [0][1],..[0][na-1]\n# Rd[1][0], [1][1],..[1][na-1]\n# ...\n# Rd[nr-1][0], [nr-1][1],..[nr-1][na-1]\nRd_ra\n"); -- for(r=0;rRd_ra[a*nr+r]/scale2); -- if((i++)==4){i=0;fprintf(pFile_outp,"\n");} -- } -- } -- -- // Scale and write Tt_ra -- i=0; -- fprintf(pFile_outp,"\n\n# Tt[r][angle]. [1/(cm2sr)].\n# Tt[0][0], [0][1],..[0][na-1]\n# Tt[1][0], [1][1],..[1][na-1]\n# ...\n# Tt[nr-1][0], [nr-1][1],..[nr-1][na-1]\nTt_ra\n"); -- for(r=0;rTt_ra[a*nr+r]/scale2); -- if((i++)==4){i=0;fprintf(pFile_outp,"\n");} -- } -- } - - fclose(pFile_outp); - return 0; diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/helper/mieMonteCarlo.py b/Modules/Biophotonics/python/inverseMonteCarlo/helper/mieMonteCarlo.py deleted file mode 100644 index a1ff0cd246..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/helper/mieMonteCarlo.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- - - -import math -from pymiecoated import Mie - -#%% setup and create derived paramters - -def mieMonteCarlo(wavelength = 450*10**-9, r = 0.4*10**-6, Vs = 0.1, nParticle = 1.46, nMedium = 1.36): - """ - Calculate the scattering parameters relevant for monte carlo simulation. - - Needs pymiecoated: https://code.google.com/p/pymiecoated/ - These are calculate scattering coefficient [1/cm] and anisotropy factor for given: - - Args - ____ - wavelength: - wavelength of the incident light [m] - r: - radius of the particle [m] - Vs: - volume fraction of scattering particles - nParticle: - refractive index of the particle that the light wave is scattered on - (default value is the refractive index of collagen) - nMedium: - refractive index of the surronding medium (default is that of colonic - mucosal tissue) - - Returns: - ____ - {'us', 'g'}: - scattering coefficient us [1/m] and anisotropy factor g - - TODO: - _____ - Additional input parameter specifying a FWHM for the wavelength to simulate the - scattering for a broad filter - """ - - # create derived parameters - sizeParamter = 2 * math.pi * r / wavelength - nRelative = nParticle / nMedium - - #%% execute mie and create derived parameters - - mie = Mie(x=sizeParamter, m=complex(nRelative,0.0)) # 0.0 complex for no attenuation - - A = math.pi * r**2 # geometrical cross sectional area - cs = mie.qsca() * A # scattering cross section - - us = Vs / (4/3 * r**3 * math.pi) * cs # scattering coefficient [m⁻1] - g = 0.77+0.18*(1.-math.exp(-(wavelength*10**9-378.7)/111.1)) - return {'us': us, 'g': g} - -def mieMonteCarlo_FWHM(wavelength = 450*10**-9, r = 0.4*10**-6, Vs = 0.1, nParticle = 1.46, nMedium = 1.36, FWHM = 0): - vl = mieMonteCarlo(wavelength - FWHM / 2, r, Vs, nParticle, nMedium) - vr = mieMonteCarlo(wavelength + FWHM / 2, r, Vs, nParticle, nMedium) - vc = mieMonteCarlo(wavelength, r, Vs, nParticle, nMedium) - - us = (0.5 * vl['us'] + vc['us'] + 0.5 * vr['us']) / 2 - g = (0.5 * vl['g'] + vc['g'] + 0.5 * vr['g']) / 2 - return {'us': us, 'g': g} - -if __name__ == "__main__": - - usg = mieMonteCarlo(wavelength = 450*10**-9, r = 300*10**-9, Vs = 0.1, nParticle = 1.46, nMedium = 1.36) - - - #%% print results - print("Scattering coefficient in [1/cm]: " + str(usg['us'] * 10**-2) + "; expected: 589.36") - print("Anisotropy factor: " + str(usg['g']) + "; expected: 0.88") diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/helper/monteCarloHelper.py b/Modules/Biophotonics/python/inverseMonteCarlo/helper/monteCarloHelper.py deleted file mode 100644 index 120d2fcf65..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/helper/monteCarloHelper.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sun Feb 01 13:52:23 2015 -Some helper methods - -@author: Seb -""" - -import math -import numpy as np -import mieMonteCarlo -import subprocess -import os -from setup import systemPaths - -import contextlib - -@contextlib.contextmanager -def cd(newPath): - savedPath = os.getcwd() - os.chdir(newPath) - yield - os.chdir(savedPath) - - -def createSimulationFile(infile, replacements, simulationFileName): - infile.seek(0); - simulationFile = open(simulationFileName, 'w') - - for line in infile: - for src, target in replacements.iteritems(): - line = line.replace(src, target) - simulationFile.write(line) - - simulationFile.close() - -def calcUa(BVF, cHb, SaO2, eHbO2, eHb, wavelength): - # determine ua [1/m] as combination of extinction coefficients. - # For more on this equation, please refer to - # http://omlc.org/spectra/hemoglobin/ - return math.log(10) * cHb * \ - (SaO2 * eHbO2(wavelength) + (1-SaO2) * eHb(wavelength)) \ - / 64500 * BVF - -def calcUsMuscle(wavelength): - return 168.52 * (wavelength * 10**9)**-0.332 / (1 - 0.96) * 100 - -def getReflectanceFromFile(fileName): - ''' - extract reflectance from a specific monte carlo output file. - - Attention: this is kept very simple and assumes that the reflectance output - starts at a specific line and keeps till eof. This might be subject to change, - e.g., when you alter the numbers of layers the line number there reflectance - values start will change. - Error handling etc. needed. - ''' - - with open(fileName) as myFile: - for num, line in enumerate(myFile, 1): - if "Rd_r #Rd[0], [1],..Rd[nr-1]. [1/cm2]" in line: - break - - #print 'found at line:', num - - reflectances = np.loadtxt(fileName, skiprows=num) - - return sum(reflectances) - - -from setup import simulation - -def normalizeIntegral(data): - # normalize data - # first sort by wavelength: - ourWavelengths = simulation.wavelengths - #ourWavelengths = np.delete(simulation.getWavelengths(), [2,7]) - - sortedIndices = sorted(range(len(ourWavelengths)), key=lambda k: ourWavelengths[k]) - - norms = np.trapz(data[:,sortedIndices], ourWavelengths[sortedIndices], axis=1) - return data / norms[:,None] - -def normalizeSum(data): - return data/data.sum(axis=1)[:,None] - -def normalizeMean(data): - return data - data.mean(axis=1)[:,None] - -iqBand = 2 - -def normalizeImageQuotient(data): - - - # use line iqBand as image quotient - normData = data / data[:,iqBand][:,None] - # discard it - normData = np.delete(normData, iqBand, axis=1) - - #normData = normalizeMean(normData) - - return normData - #return normalizeIntegral(data) - - #return normalizeMean(firstNorm) - -def removeIqWavelength(wavelenghts): - #return wavelenghts - return np.delete(wavelenghts, iqBand) - -def runOneSimulation(wavelength, eHbO2, eHb, - infile, simulationOutputFolderName, gpumcmlDirectory, gpumcmlExecutable, - BVF=0.1, Vs=0.4, d=500 * 10**-6, - r=0.4 * 10**-6, SaO2=0.7, cHb=120, - submucosa_BVF=0.1, submucosa_Vs=0.3, submucosa_r = 2*10**-6, submucosa_SaO2=0.7, - Fwhm = 20 * 10**-9, nrPhotons = 10**6, - ): - """ - Args: - _____ - cHb: - concentration of haemoglobin per unit volume of blood in colon [g/L], - taken from: "Modelling and validation of spectral reflectance for the colon" - """ - outputFilename = \ - simulationOutputFolderName + \ - "BVF" + repr(BVF) + \ - "Vs" + repr(Vs) + \ - "d" + repr(d) + \ - "SaO2" + repr(SaO2) + \ - "r" + repr(r) + \ - "photons" + repr(nrPhotons) + \ - "wavelength" + repr(wavelength) + \ - ".mco" - - usg = mieMonteCarlo.mieMonteCarlo_FWHM(wavelength, r, Vs, FWHM = Fwhm) - - ua = calcUa(BVF, cHb, SaO2, eHbO2, eHb, wavelength) - # calculate ua, us for submucosa, values taken from - # "Model based inversion for deriving maps of - # histological parameters characteristic of cancer - # from ex-vivo multispectral images of the colon" - ua_sm = calcUa(submucosa_BVF, cHb, submucosa_SaO2, eHbO2, eHb, wavelength) - usg_sm = mieMonteCarlo.mieMonteCarlo_FWHM(wavelength,submucosa_r, submucosa_Vs, FWHM = Fwhm) - - # now create us and ua for muscle layer according to - # "Modelling and validation of spectral reflectance for the colon" - us_mus = calcUsMuscle(wavelength) - A = 1.7923385088285804 # calculated to retrieve an absorption of 11.2 cm-1 - # at 515nm - ua_mus = calcUa(A*0.1, 120, 0.7, eHbO2, eHb, wavelength) - - - # factors are due to the program expecting [cm] instead of [m] - replacements = {'muc_c_ua':str(ua / 100), 'muc_c_us':str(usg['us'] / 100), - 'muc_c_g':str(usg['g']), 'muc_c_d':str(d * 100), - 'sm_c_ua':str(ua_sm / 100), 'sm_c_us':str(usg_sm['us'] / 100), - 'sm_c_g':str(usg_sm['g']), - 'mus_c_ua':str(ua_mus / 100), 'mus_c_us':str(us_mus / 100), - 'outputFilename':outputFilename, - 'c_ph': str(nrPhotons)} - - createSimulationFile(infile, replacements, - systemPaths.getOutputFolder() + "simulationFile.mci") - - args = ("./" + gpumcmlExecutable, "-A", systemPaths.getOutputFolder() + "simulationFile.mci") - - with cd(gpumcmlDirectory): - popen = subprocess.Popen(args, stdout=subprocess.PIPE) - popen.wait() - - # outside the context manager we are back wherever we started. - - - - # this function is error prone, please refer to documentation to see - # if problematic for you - return getReflectanceFromFile(gpumcmlDirectory + outputFilename) - - -if __name__ == "__main__": - - us515 = calcUsMuscle(515 * 10**-9) - print ("calculated us [cm-1] for muscle layer at 515nm: " + str(us515 / 100) + " expected ca. 530") - us1064 = calcUsMuscle(1064 * 10**-9) - print ("calculated us [cm-1] for muscle layer at 1064nm: " + str(us1064 / 100)) \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/lenstest.py b/Modules/Biophotonics/python/inverseMonteCarlo/lenstest.py deleted file mode 100644 index ff84d6b2e8..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/lenstest.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sun Mar 1 15:24:48 2015 - -@author: wirkert -""" - -import numpy as np -import matplotlib.pyplot as plt - -import helper.mieMonteCarlo as mmc -import helper.monteCarloHelper as mch -from setup import simulation -from csvImageReader import csvImageReader - -plt.close('all') - -def plotOne(liver, noobj, flatf, testsegmentation, title): - - si = sorted(range(len(simulation.wavelengths)), key=lambda k: simulation.wavelengths[k]) - - #%% - liverMean = np.mean(liver[np.nonzero(testsegmentation)[0], :], axis = 0) - noobjMean = np.mean(noobj[np.nonzero(testsegmentation)[0], :], axis = 0) - flatfMean = np.mean(flatf[np.nonzero(testsegmentation)[0], :], axis = 0) - - plt.figure() - plt.plot(simulation.wavelengths[si], liverMean[si], 'r-o') - plt.plot(simulation.wavelengths[si], noobjMean[si], 'g-o') - plt.plot(simulation.wavelengths[si], flatfMean[si], 'b-o') - - plt.title(title) - plt.legend(["liver", "no object", "flatfield"]) - plt.xlabel('wavelength') - plt.ylabel('reflectance') - - - -shape, testsegmentation = csvImageReader("data/output/liver_28_testsegmentation") - - -#%% -shape, liver28 = csvImageReader("data/output/liver_28") -shape, noobj28 = csvImageReader("data/output/NoObject_28") -shape, flatf28 = csvImageReader("data/output/flatfield_28") - - -plotOne(liver28, noobj28, flatf28, testsegmentation, "f2.8") - - -#%% -shape, liver22 = csvImageReader("data/output/liver_22") -shape, noobj22 = csvImageReader("data/output/NoObject_22") -shape, flatf22 = csvImageReader("data/output/flatfield_22") - - - -plotOne(liver22, noobj22, flatf22, testsegmentation, "f22") - - - -#%% - -shape, liver8 = csvImageReader("data/output/liver_8") -shape, noobj8 = csvImageReader("data/output/NoObject_8") -shape, flatf8 = csvImageReader("data/output/flatfield_8") - -#%% - -plotOne(liver8, noobj8, flatf8, testsegmentation, "f8") \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/lighttest.py b/Modules/Biophotonics/python/inverseMonteCarlo/lighttest.py deleted file mode 100644 index 04b1f1e7c6..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/lighttest.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Mar 4 15:44:11 2015 - -@author: wirkert -""" - -from csvImageReader import csvImageReader -import numpy as np -import matplotlib.pyplot as plt -from setup import simulation - -#%% load - -shape, links = csvImageReader("data/output/CC2_links") -shape, flatfseglinks = csvImageReader("data/output/CC2_links_flatfieldsegmentation") - -shape, rechts= csvImageReader("data/output/CC2_rechts") -shape, flatfsegrechts = csvImageReader("data/output/CC2_rechts_flatfieldsegmentation") - -#%% process - -wav = simulation.wavelengths -si = sorted(range(len(simulation.wavelengths)), key=lambda k: simulation.wavelengths[k]) - - - -linksMean = np.mean(links[np.nonzero(flatfseglinks)[0], :], axis = 0) -rechtsMean = np.mean(rechts[np.nonzero(flatfsegrechts)[0], :], axis = 0) - - -plt.close('all') - -plt.figure() -plt.plot(wav[si], linksMean[si]) -plt.plot(wav[si], rechtsMean[si]*1.905) -plt.legend(["light source left", "light source right"]) - - - -#%% load - -shape, flatfield = csvImageReader("data/output/ff") -shape, segLight = csvImageReader("data/output/ff_testsegmentation") -shape, segDark = csvImageReader("data/output/ff_trainsegmentation") - -#%% process - -segLightMean = np.mean(flatfield[np.nonzero(segLight)[0], :], axis = 0) -segDarkMean = np.mean(flatfield[np.nonzero(segDark)[0], :], axis = 0) - - - -plt.close('all') - -plt.figure() -plt.plot(wav[si], segLightMean[si]) -plt.plot(wav[si], segDarkMean[si]*1.53) -plt.legend(["light source light", "light source dark"]) diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/optimization.py b/Modules/Biophotonics/python/inverseMonteCarlo/optimization.py deleted file mode 100644 index 68e510d1f2..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/optimization.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Feb 07 23:42:41 2015 - -@author: Seb -""" - -import numpy as np -from scipy.optimize import minimize -from reflectanceError import ReflectanceError -from sklearn.metrics import r2_score - - - - -def optimization(trainingParameters, trainingReflectances, testParameters, testReflectances): - - BVFs = np.unique(trainingParameters[:,0]) - Vss = np.unique(trainingParameters[:,1]) - - - #%% build optimization function from rectangular reflectance grid - - reflectanceGrid3D = np.reshape(trainingReflectances, (len(BVFs), len(Vss), trainingReflectances.shape[1])) - functionToMinimize = ReflectanceError(BVFs, Vss, reflectanceGrid3D) - - - #%% do optimization - - estimatedParameters = np.zeros_like(testParameters) - absErrors = np.zeros_like(testParameters) - - - for idx, (testParameter, testReflectance) in enumerate(zip(testParameters, testReflectances)): - functionToMinimize.setReflectanceToMatch(testReflectance) - minimization = minimize(functionToMinimize.f, [np.median(BVFs), np.median(Vss)], method="Nelder-Mead") - # interpolation extrapolates with constant values. We just crop it to the bounds - clippedX= np.clip(minimization.x, [min(BVFs), min(Vss)], [max(BVFs), max(Vss)]) - - estimatedParameters[idx, :] = clippedX - absErrors[idx,:] = np.abs(clippedX - testParameter) - - r2Score = r2_score(testParameters.T, estimatedParameters.T) - - return absErrors, r2Score - diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/randomForest.py b/Modules/Biophotonics/python/inverseMonteCarlo/randomForest.py deleted file mode 100644 index 4a8f77cb59..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/randomForest.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Feb 6 10:49:45 2015 - -This is basically a wrapper for the python random forests. -It is necessary to do our preprocessing and KFold cross validation + grid search. - -@author: wirkert -""" - - -import numpy as np -import time -from sklearn.ensemble import RandomForestRegressor -from sklearn.grid_search import GridSearchCV -from sklearn.cross_validation import KFold - - - -# additional things that could be checked in this study: -# 1. band selection results -# 2. effect of normalizations -# 3. parameter study -# 4. optimal image quotient - -def randomForest(trainingParameters, trainingReflectances, trainingWeights): - #%% train forest - - start = time.time() - - # get best forest using k-fold cross validation and grid search - # outcommented for now because it takes to long for quick tests - - print "starting forest training now." - -# kf = KFold(trainingReflectances.shape[0], 5, shuffle=True) -# param_grid = [ -# {'max_depth': np.arange(2,40,1), 'max_features': np.arange(1,trainingReflectances.shape[1],1)}] -# -# rf = GridSearchCV(RandomForestRegressor(500, max_depth=8), param_grid, cv=kf, n_jobs=11) - #print("best random forest parameters: " + str(rf.best_estimator_)) - rf = RandomForestRegressor(500, max_depth=24) - rf.fit(trainingReflectances, trainingParameters, trainingWeights) - - - end = time.time() - print "time necessary to train the forest [s]: " + str((end - start)) - - - return rf - - diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/reader/csvImageReader.py b/Modules/Biophotonics/python/inverseMonteCarlo/reader/csvImageReader.py deleted file mode 100644 index 2db47eca4a..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/reader/csvImageReader.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Feb 17 17:48:05 2015 - -Reads the csv images provided by our MITK miniapp. -The image is supposed to be structured as follows: - -1. line: arbitrary data -2. line: image shape;1024;768 -3. line - 1024x768 + 2 line: multispectral spectra. - -e.g.: 4000;8144;3776;4880;9408;6048;3808;4704 -for an image with 8 spectral bands. This is one pixel. - -image shape;1024;768 is of course only an example for an image resolution of 1024x768 - -@author: wirkert -""" - -import csv -import numpy as np -import Image - - -def csvImageReader(csvFilename): - """ - read in image from csv format. - attention: enter name without suffix (.csv will be added automatically) - """ - csvFilename = csvFilename + ".csv" - - print "loading image", csvFilename - - - with open(csvFilename, 'r') as myFile: - reader=csv.reader(myFile, delimiter=";") - for i, row in enumerate(reader): - if i == 1: - shapeLine = row - break - - # for some reason, shapes are flipped from itk to csv - resultShape = (int(shapeLine[2]), int(shapeLine[1])) - - print 'determined shape to be: ', resultShape - - reflectances = np.loadtxt(csvFilename, skiprows=2, delimiter=";") - - return (resultShape, reflectances) - - -def csvMultiSpectralImageReader2(csvFilename): - shape, reflectances = csvImageReader(csvFilename) - dummy, flatfieldsegmentation = csvImageReader(csvFilename + "_flatfieldsegmentation") - dummy, trainsegmentation = csvImageReader(csvFilename + "_trainsegmentation") - dummy, testsegmentation =csvImageReader(csvFilename + "_testsegmentation") - - flatfield = reflectances[np.nonzero(flatfieldsegmentation)[0], :] - flatfield = np.mean(flatfield, axis = 0) - - correctedReflectances = reflectances / flatfield - - return (shape, correctedReflectances, trainsegmentation, testsegmentation) - - -def csvMultiSpectralImageReader(csvFilename): - """ - read an image and correct it by its dark and flatfield image. - dark and flatfield image are expected to have the same name - as the original image with suffixes - _dark - _flatfield - Also returns the segmentation of the multispectral image, expecting - the suffixes: - _trainsegmentation - _testsegmentation - Segmentation will have value > 0 if pixel is segmented, 0 otherwise. - """ - - shape, reflectances = csvImageReader(csvFilename) - dummy, dark = csvImageReader(csvFilename + "_dark") - dummy, flatfield = csvImageReader(csvFilename + "_flatfield") - dummy, trainsegmentation = csvImageReader(csvFilename + "_trainsegmentation") - dummy, testsegmentation =csvImageReader(csvFilename + "_testsegmentation") - - correctedReflectances = (reflectances - dark) / (flatfield - dark) - - return (shape, correctedReflectances, trainsegmentation, testsegmentation) - - -if __name__ == "__main__": - - #%% load images - shape, reflectances = csvImageReader("data/output/sample_0") - shape, correctedReflectances, trainseg, testseg = csvMultiSpectralImageReader("data/output/sample_0") - - #%% save again - - # save one wavelength of the uncorrected image to see if reading worked - uncorrectedImage_0 = np.reshape(reflectances[:,0], shape) - - #Rescale to 0-255 and convert to uint8 - rescaled = (255.0 / uncorrectedImage_0.max() * (uncorrectedImage_0 - uncorrectedImage_0.min())).astype(np.uint8) - - im = Image.fromarray(rescaled) - im.save("data/output/" + "testUncorrectedImage_0.tiff") - - # save the corrected images to see if reading and correction worked. - for i in np.arange(0,correctedReflectances.shape[1]): - correctedImage_i = np.reshape(correctedReflectances[:,i], shape) - im = Image.fromarray(correctedImage_i) - im.save("data/output/" + "testCorrectedImage_" + str(i) + ".tiff") diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/reader/sitkImageReader.py b/Modules/Biophotonics/python/inverseMonteCarlo/reader/sitkImageReader.py deleted file mode 100644 index bb22114dc0..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/reader/sitkImageReader.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Apr 17 16:21:07 2015 - -@author: wirkert -""" - - -import SimpleITK as sitk - -def read (fileName): - """ - load a simpleitk image and put its data array in a numpy array - - Params: - ____ - fileName - the filename _without_ extension. .nrrd will be expected. - - Returns: - ____ - the data of the image as numpy array. all other information in the SimpleITK - image is ignored - """ - image = None - - try: - reader = sitk.ImageFileReader() - reader.SetFileName(fileName + ".nrrd") - - image = reader.Execute() - image = sitk.GetArrayFromImage(image) - - except RuntimeError: - print "image " + fileName + ".nrrd not found and thus not loaded." - - return image - -def readMS (fileName): - """ - reads the information necessary for multispectral image processing. - - Returns: - ____ - (image, trainSeg, testSeg) - the image is the multispectral (ms) image corrected by an - (optional) white balance image. If available, train- and test segmentations are - retured. They are of the same 2D size as the ms image and have as values either 1 (inclueded in segmentation) - or 0 (not included in segmentation) - """ - - rawMsImage = read(fileName) - whitebalanceImage = read(fileName + "_flatfield") - trainSegmentation = read(fileName + "_trainSeg") - testSegmentation = read(fileName + "_testSeg") - - msImage = rawMsImage / whitebalanceImage - - return msImage, trainSegmentation, testSegmentation \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/reflectanceError.py b/Modules/Biophotonics/python/inverseMonteCarlo/reflectanceError.py deleted file mode 100644 index 8028aac1bd..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/reflectanceError.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Feb 10 15:50:13 2015 - -@author: wirkert -""" - -from scipy.interpolate import RectBivariateSpline -import numpy as np - -class ReflectanceError: - """ - calculate the fit error of a given reflectance and examples of reflectances. - """ - - def __init__(self, BVFs, Vss, reflectances): - """ - intialize the reflectance error calculation. - - Arguments - _________ - BVFs: - array storing all the differnet values for BVF (Blood Volume Fraction) - Vss: - array storing all the different values Vs (Volume fraction of scattering particles) - reflectances: - grid of dimension BVFs.shape[0] x Vss.shape[0] x wavelenghts with reflectance values - """ - nr_wavelengths = reflectances.shape[2] - - # create array of bivariate splines, one for each wavelength - self.RBSs = np.empty(nr_wavelengths, dtype=object) - - for i in np.arange(nr_wavelengths): - self.RBSs[i] = RectBivariateSpline(BVFs, - Vss, - reflectances[:,:,i]) - - def setReflectanceToMatch(self, reflectance): - """ - set the baseline reflectance. the l2 distance to this reflectance will be output - """ - self.reflectance = reflectance - - def reflectanceForX(self, xy): - result = np.zeros(len(self.reflectance)) - for idx, rbsi in enumerate(self.RBSs): - result[idx] = rbsi(xy[0], xy[1]) - return result - - def f(self, xy): - """ - return l2 norm of reflectance implied by x and self.reflectances - """ - quadraticError = 0 - # build quadratic error of differences between reflectances and - for refli, rbsi in zip(self.reflectance, self.RBSs): - quadraticError += (refli - rbsi(xy[0], xy[1], grid = False))**2 - - return quadraticError - - def df(self, xy): - """ - return derivate of l2 norm of reflectance implied by x and self.reflectances - """ - quadraticErrorDerivate = [0,0] - for refli, rbsi in zip(self.reflectance, self.RBSs): - u = 2*(rbsi(xy[0], xy[1], grid = False) - refli) - # dx - quadraticErrorDerivate[0] += \ - u*rbsi(xy[0], xy[1], dx=1, grid = False) - #dy - quadraticErrorDerivate[1] += \ - u*rbsi(xy[0], xy[1], dy=1, grid = False) - return quadraticErrorDerivate diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/setup/data.py b/Modules/Biophotonics/python/inverseMonteCarlo/setup/data.py deleted file mode 100644 index 71a7fc9af1..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/setup/data.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Feb 13 16:13:11 2015 - -functions storing training / testdata combinations to ensure consistent usage -of datasets (same combinations / normalizations / ...) - -@author: wirkert -""" - -import numpy as np -import helper.monteCarloHelper as mch - - -import reader.csvImageReader as csvR - - -def perfect(dataFolder): - trainingParameters = np.load(dataFolder + "2015February2401:43AMPerfectGridTraining10000parameters.npy") - trainingParameters = trainingParameters[:,0:2] # only BVF and Vs for perfect data simulations - trainingReflectances = np.load(dataFolder + "2015February2401:43AMPerfectGridTrainingreflectances1000000photons.npy") - trainingReflectances = mch.normalizeImageQuotient(trainingReflectances) - - testParameters = np.load(dataFolder + "2015February2306:52PMUniformRandom10000parameters.npy") - testParameters = testParameters[:,0:2] # only BVF and Vs for perfect data simulations - testReflectances = np.load(dataFolder + "2015February2306:52PMUniformRandomreflectances1000000photons.npy") - testReflectances = mch.normalizeImageQuotient(testReflectances) - - return trainingParameters, trainingReflectances, testParameters, testReflectances - - -def noisy(dataFolder): - trainingParameters = np.load(dataFolder + "2015February2208:16PMNoisyRandomTraining10000parameters.npy") - trainingParameters = trainingParameters[:,0:2] - trainingReflectances = np.load(dataFolder + "2015February2208:16PMNoisyRandomTrainingreflectances1000000photons.npy") - trainingReflectances = mch.normalizeImageQuotient(trainingReflectances) - - testParameters = np.load(dataFolder + "2015February2301:23AMNoisyRandomTesting10000parameters.npy") - testParameters = testParameters[:,0:2] - testReflectances = np.load(dataFolder + "2015February2301:23AMNoisyRandomTestingreflectances1000000photons.npy") - testReflectances = mch.normalizeImageQuotient(testReflectances) - - return trainingParameters, trainingReflectances, testParameters, testReflectances - - -def realImage(dataFolder, imageToLoad): - trainingParameters = np.load(dataFolder + "2015February2807:13PMNoisyRandomTraining10000parameters.npy") - # estimate: BVF, Vs, d, SaO2: - trainingParameters = trainingParameters[:, 0:3] - trainingReflectances = np.load(dataFolder + "2015February2807:13PMNoisyRandomTrainingreflectances1000000photons.npy") - - #trainingReflectances = np.delete(trainingReflectances, [2, 7], axis=1) - trainingReflectances = mch.normalizeImageQuotient(trainingReflectances) - - shape, image, trainsegmentation, testsegmentation = csvR.csvMultiSpectralImageReader2(dataFolder + imageToLoad) - - #image = np.delete(image, [2, 7], axis=1) - image = mch.normalizeImageQuotient(image) - - return trainingParameters, trainingReflectances, shape, image, trainsegmentation, testsegmentation \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/setup/simulation.py b/Modules/Biophotonics/python/inverseMonteCarlo/setup/simulation.py deleted file mode 100644 index 5af6148196..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/setup/simulation.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Feb 6 15:32:26 2015 - -@author: wirkert -""" - - - -import numpy as np -from scipy.interpolate import interp1d -from scipy.stats import norm - - -nrSimulations = 10000 - -# number of photons for one MC simulation run -photons = 1 * 10**6 - -# the wavelengths [m] for which the reflectance spectrum shall be evaluated -#wavelengths = np.linspace(470,700,23) * 10**-9 -wavelengths = np.array([580, 470, 660, 560, 480, 511, 600, 700]) * 10**-9 - -# The full width at half maximum [m] of the used imaging systems filters -FWHM = 20 * 10**-9 - -print('create reference data...') -#reference data - -# table with wavelength at 1st row, -# HbO2 molar extinction coefficient [cm**-1/(moles/l)] at 2nd row, -# Hb molar extinction coefficient [cm**-1/(moles/l)] at 3rd row -haemoLUT = np.loadtxt("data/haemoglobin.txt", skiprows=2) -# we calculate everything in [m] instead of [nm] and [1/cm] -haemoLUT[:,0] = haemoLUT[:,0] * 10**-9 -haemoLUT[:,1:] = haemoLUT[:,1:] * 10**2 -# to account for the FWHM of the used filters, compute convolution -# see http://en.wikipedia.org/wiki/Full_width_at_half_maximum -filterResponse = norm(loc = 0, scale = FWHM / 2.355) -# parse at 20 locations -x = np.linspace(filterResponse.ppf(0.01), - filterResponse.ppf(0.99), 20) -filterResponse_table = filterResponse.pdf(x) -# TODO verify if this normalization is correct! -filterResponse_table = filterResponse_table / sum(filterResponse_table) -haemoLUT[:, 1] = np.convolve(haemoLUT[:, 1], filterResponse_table, 'same') -haemoLUT[:, 2] = np.convolve(haemoLUT[:, 2], filterResponse_table, 'same') - -eHbO2 = interp1d(haemoLUT[:,0], haemoLUT[:,1]) -eHb = interp1d(haemoLUT[:,0], haemoLUT[:,2]) - -def getWavelengths(): - return wavelengths - -def noisy(): - #%% intialization - print('initialize...') - - # evaluated parameters for MC - - # blood volume fraction range - samplesBVF = 100 - BVFs = np.linspace(0.001, 0.15, samplesBVF) - # scattering particles volume fraction range - samplesVs = 100 - Vss = np.linspace(0.01, 0.60, samplesVs) - # thickness of mucosa [m] range - samplesD = 5 - ds = np.linspace(250 * 10**-6, 735 * 10**-6, samplesD) - #ds = np.linspace(500 * 10**-6, 500 * 10**-6, samplesD) - # haemoglobin saturation - samplesSaO2 = 6 - SaO2s= np.linspace(0.0, 1.0, samplesSaO2) - # radius of scattering particles [m] - samplesR = 1 - rs = np.linspace(0.4 * 10**-6, 0.4 * 10**-6, samplesR) - - - - nrSamples = samplesBVF * samplesD * samplesR * samplesSaO2 * samplesVs - - - return BVFs, Vss, ds, SaO2s, rs, nrSamples, photons, wavelengths, FWHM, eHbO2, eHb, nrSimulations - - - -def perfect(): - #%% intialization - print('initialize...') - - # evaluated parameters for MC - - # blood volume fraction range - samplesBVF = 100 - BVFs = np.linspace(0.001, 0.15, samplesBVF) - # scattering particles volume fraction range - samplesVs = 100 - Vss = np.linspace(0.01, 0.60, samplesVs) - # thickness of mucosa [m] range - samplesD = 1 - #ds = np.linspace(250 * 10**-6, 735 * 10**-6, samplesD) - ds = np.linspace(500 * 10**-6, 500 * 10**-6, samplesD) - # haemoglobin saturation - samplesSaO2 = 1 - SaO2s= np.linspace(0.7, 0.7, samplesSaO2) - # radius of scattering particles [m] - samplesR = 1 - rs = np.linspace(0.4 * 10**-6, 0.4 * 10**-6, samplesR) - - - nrSamples = samplesBVF * samplesD * samplesR * samplesSaO2 * samplesVs - - return BVFs, Vss, ds, SaO2s, rs, nrSamples, photons, wavelengths, FWHM, eHbO2, eHb, nrSimulations - - -if __name__ == "__main__": - - import matplotlib.pyplot as plt - - # currently will not work due to path of haemoblobin.txt - # change this when moving to unit tests. - - #%% do plotting - - BVFs, Vss, ds, SaO2s, rs, nrSamples, photons, wavelengths, FWHM, eHbO2, eHb, nrSimulations = perfect() - - sortedWavelengths = np.sort(wavelengths) - - plt.figure() - plt.plot(sortedWavelengths, eHbO2(sortedWavelengths), 'bo-') - plt.plot(sortedWavelengths, eHb(sortedWavelengths), 'go-') \ No newline at end of file diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/setup/systemPaths.py b/Modules/Biophotonics/python/inverseMonteCarlo/setup/systemPaths.py deleted file mode 100644 index 65576cac01..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/setup/systemPaths.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Feb 18 09:49:56 2015 - -@author: wirkert -""" - -import os - -def getModuleRootFolder(): - - mycwd = os.getcwd() - os.chdir(os.path.dirname(__file__)) - os.chdir('..') - - moduleRootFolder = os.getcwd() + "/" - - os.chdir(mycwd) - - return moduleRootFolder - - - -def getOutputFolder() : - """ - standard folder to put output of the inverse monte carlo script - """ - - outputFolder = getModuleRootFolder() + 'data/output/' - - return outputFolder - - -def initPaths(): - """ - modify these paths so they concide with the ones on your system. - """ - - # the input file without the run specific parameters for ua, us and d: - infileString = getModuleRootFolder() + 'data/colonTemplate.mci' - # the output folder for the mc simulations - # attention: this is relative to your gpumcml path! - outfolderMC = 'outputMC/' - # the output folder for the reflectance spectra - outfolderRS = getOutputFolder() - gpumcmlDirectory = '/home/wirkert/workspace/monteCarlo/gpumcml/fast-gpumcml/' - gpumcmlExecutable = 'gpumcml.sm_20' - - return infileString, outfolderMC, outfolderRS, gpumcmlDirectory, gpumcmlExecutable - -if __name__ == "__main__": - - i, o, o2, g, g2 = initPaths() diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/tests/test_mie.py b/Modules/Biophotonics/python/inverseMonteCarlo/tests/test_mie.py deleted file mode 100644 index c9c1b3f1e3..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/tests/test_mie.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Mar 27 19:12:40 2015 - -@author: wirkert -""" - -import unittest -import helper.mieMonteCarlo as miemc - - -class test_mie(unittest.TestCase): - - def setUp(self): - self.usg470 = miemc.mieMonteCarlo(wavelength = 450*10**-9, Vs = 0.04) - self.usg700 = miemc.mieMonteCarlo(wavelength = 700*10**-9, Vs = 0.04) - - def test_us470nm(self): - self.assertTrue(self.usg470['us'] * 10**-2 < 400) - self.assertTrue(self.usg470['us'] * 10**-2 > 300) - - def test_us700nm(self): - self.assertTrue(self.usg470['us'] * 10**-2 < 300) - self.assertTrue(self.usg470['us'] * 10**-2 > 200) - - def test_g700nm(self): - self.assertTrue(self.usg470['g'] > 0.9) - - - - diff --git a/Modules/Biophotonics/python/inverseMonteCarlo/tests/test_sitkImageReader.py b/Modules/Biophotonics/python/inverseMonteCarlo/tests/test_sitkImageReader.py deleted file mode 100644 index 6e870e0934..0000000000 --- a/Modules/Biophotonics/python/inverseMonteCarlo/tests/test_sitkImageReader.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Apr 17 16:22:34 2015 - -@author: wirkert -""" - -import unittest -import reader.sitkImageReader as reader - -class test_sitkImageReader(unittest.TestCase): - - def setUp(self): - self.image = reader.read('data/allBands') - self.notExistingImage = reader.read('data/asdf') - # note: this test has to be changed to an image lying in mitk testdata - - def test_readDoesNotCrash(self): - # if we got this far, at least an image was read. - self.assertTrue(len(self.image.shape) == 3) - - def test_readNonExistingImageReturnsNone(self): - self.assertTrue(self.notExistingImage is None) \ No newline at end of file