|
| 1 | +# -------- Activate virtual environment ------------------------- |
| 2 | +import os |
| 3 | +import ctypes |
| 4 | +import sys |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | +def search_activation_path(): |
| 8 | + for i in range(5): |
| 9 | + final_path = str(Path(__file__).parents[i]) + '\\env\\Scripts\\activate_this.py' |
| 10 | + if os.path.exists(final_path): |
| 11 | + return final_path |
| 12 | + return '' |
| 13 | + |
| 14 | +activate_path = search_activation_path() |
| 15 | +if os.path.exists(activate_path): |
| 16 | + exec(open(activate_path).read(), {'__file__': activate_path}) |
| 17 | + print(f'Aivia virtual environment activated\nUsing python: {activate_path}') |
| 18 | +else: |
| 19 | + error_mess = f'Error: {activate_path} was not found.\n\nPlease check that:\n' \ |
| 20 | + f' 1/ The \'FirstTimeSetup.py\' script was already run in Aivia,\n' \ |
| 21 | + f' 2/ The current python recipe is in one of the "\\PythonEnvForAivia\\" subfolders.' |
| 22 | + ctypes.windll.user32.MessageBoxW(0, error_mess, 'Error', 0) |
| 23 | + sys.exit(error_mess) |
| 24 | +# --------------------------------------------------------------- |
| 25 | + |
| 26 | +import shlex |
| 27 | +import subprocess |
| 28 | +import imagecodecs |
| 29 | +import numpy as np |
| 30 | +from skimage import transform, img_as_uint, img_as_ubyte |
| 31 | +from tifffile import imread, imwrite |
| 32 | +from os.path import dirname as up |
| 33 | +from magicgui import magicgui |
| 34 | + |
| 35 | + |
| 36 | +""" |
| 37 | +Scales the input channel up or down (isotropic factor) and rotates the volume 90 degrees around one axis (not centered). |
| 38 | +Works only for 3D (not timelapses) and for single channels. |
| 39 | +
|
| 40 | +Requirements |
| 41 | +------------ |
| 42 | +numpy |
| 43 | +scikit-image |
| 44 | +imagecodecs |
| 45 | +tifffile |
| 46 | +
|
| 47 | +Parameters |
| 48 | +---------- |
| 49 | +Input channel: |
| 50 | + Input channel to be scaled. |
| 51 | +
|
| 52 | +Returns |
| 53 | +------- |
| 54 | +New channel in original image: |
| 55 | + Returns an empty channel. |
| 56 | +
|
| 57 | +New image: |
| 58 | + Opens Aivia to display the new scaled image. |
| 59 | +
|
| 60 | +""" |
| 61 | + |
| 62 | +axis_rot_options = {'ClockWise': {'X': (1, 0), 'Y': (0, 2), 'Z': (2, 1)}, |
| 63 | + 'CounterClockWise': {'X': (0, 1), 'Y': (2, 0), 'Z': (1, 2)}} |
| 64 | +interpolation_mode = 1 # 0: Nearest-neighbor, 1: Bi-linear , 2: Bi-quadratic, 3: Bi-cubic, 4: Bi-quartic, 5: Bi-quintic |
| 65 | + |
| 66 | + |
| 67 | +# Get path to the Aivia executable |
| 68 | +def getParentDir(curr_dir, level=1): |
| 69 | + for i in range(level): |
| 70 | + parent_dir = up(curr_dir) |
| 71 | + curr_dir = parent_dir |
| 72 | + return curr_dir |
| 73 | + |
| 74 | + |
| 75 | +exeDir = sys.executable |
| 76 | +parentDir = getParentDir(exeDir, level=2) |
| 77 | +aivia_path = parentDir + '\\Aivia.exe' |
| 78 | + |
| 79 | + |
| 80 | +# [INPUT Name:inputImagePath Type:string DisplayName:'Input Channel'] |
| 81 | +# [OUTPUT Name:resultPath Type:string DisplayName:'Duplicate of input'] |
| 82 | +def run(params): |
| 83 | + global axis_rot_options, interpolation_mode |
| 84 | + # image_org = params['EntryPoint'] |
| 85 | + image_location = params['inputImagePath'] |
| 86 | + result_location = params['resultPath'] |
| 87 | + zCount = int(params['ZCount']) |
| 88 | + tCount = int(params['TCount']) |
| 89 | + pixel_cal_tmp = params['Calibration'] |
| 90 | + pixel_cal = pixel_cal_tmp[6:].split(', ') # Expects calibration with 'XYZT: ' in front |
| 91 | + |
| 92 | + # Getting XY and Z calibration values # Expecting only 'Micrometers' in this code |
| 93 | + XY_cal = float(pixel_cal[0].split(' ')[0]) |
| 94 | + Z_cal = float(pixel_cal[2].split(' ')[0]) |
| 95 | + Z_ratio = float(Z_cal) / float(XY_cal) |
| 96 | + |
| 97 | + if not os.path.exists(image_location): |
| 98 | + print(f"Error: {image_location} does not exist") |
| 99 | + return |
| 100 | + |
| 101 | + if not os.path.exists(aivia_path): |
| 102 | + print(f"Error: {aivia_path} does not exist") |
| 103 | + return |
| 104 | + |
| 105 | + raw_data = imread(image_location) |
| 106 | + dims = raw_data.shape |
| 107 | + print('-- Input dimensions (expected (Z), Y, X): ', np.asarray(dims), ' --') |
| 108 | + |
| 109 | + # Checking image is not 2D+t or 3D+t |
| 110 | + if len(dims) != 3 or (len(dims) == 3 and tCount > 1): |
| 111 | + print('Error: Image should be XYZ only.') |
| 112 | + return |
| 113 | + |
| 114 | + # Scale image to be isotropic |
| 115 | + final_cal = XY_cal |
| 116 | + scale_factor_xy = 1 |
| 117 | + scale_factor_z = 1 |
| 118 | + if Z_ratio > 1: |
| 119 | + scale_factor_z = Z_ratio |
| 120 | + elif Z_ratio < 1: |
| 121 | + scale_factor_xy = 1 / Z_ratio |
| 122 | + final_cal = Z_cal |
| 123 | + |
| 124 | + if abs(Z_ratio - 1) > 0.001: |
| 125 | + print('-- Rescaling image as XY and Z calibration are different') |
| 126 | + final_scale = (scale_factor_z, scale_factor_xy, scale_factor_xy) |
| 127 | + iso_data = transform.rescale(raw_data, final_scale, interpolation_mode) |
| 128 | + else: |
| 129 | + iso_data = raw_data |
| 130 | + |
| 131 | + # GUI to choose rotation axis |
| 132 | + swap_axes_options = axis_rot_options['ClockWise'] |
| 133 | + @magicgui(axis={"label": "Select the rotation axis:", "widget_type": "RadioButtons", |
| 134 | + 'choices': swap_axes_options.keys()}, |
| 135 | + direction={"label": "Select the rotation direction:", "widget_type": "RadioButtons", |
| 136 | + 'choices': axis_rot_options.keys()}, |
| 137 | + call_button="Run") |
| 138 | + def get_rot_param(axis=list(swap_axes_options.keys())[0], direction=list(axis_rot_options.keys())[0]): |
| 139 | + pass |
| 140 | + |
| 141 | + @get_rot_param.called.connect |
| 142 | + def close_GUI_callback(): |
| 143 | + get_rot_param.close() |
| 144 | + |
| 145 | + get_rot_param.show(run=True) |
| 146 | + rot_axis = get_rot_param.axis.value |
| 147 | + rot_dir = get_rot_param.direction.value |
| 148 | + |
| 149 | + # Rotation |
| 150 | + rot_axes = axis_rot_options[rot_dir][rot_axis] |
| 151 | + processed_data = np.rot90(iso_data, axes=rot_axes) |
| 152 | + |
| 153 | + # Formatting result array |
| 154 | + if raw_data.dtype is np.dtype('u2'): |
| 155 | + out_data = img_as_uint(processed_data) |
| 156 | + else: |
| 157 | + out_data = img_as_ubyte(processed_data) |
| 158 | + |
| 159 | + # Defining axes for output metadata and scale factor variable |
| 160 | + axes = 'ZYX' |
| 161 | + meta_info = {'axes': axes, 'spacing': str(final_cal), 'unit': 'um'} |
| 162 | + |
| 163 | + # Formatting voxel calibration values |
| 164 | + inverted_XY_cal = 1 / final_cal |
| 165 | + |
| 166 | + tmp_path = result_location.replace('.tif', '-rotated.tif') |
| 167 | + print('Saving image in temp location:\n', tmp_path) |
| 168 | + imwrite(tmp_path, out_data, imagej=True, photometric='minisblack', metadata=meta_info, |
| 169 | + resolution=(inverted_XY_cal, inverted_XY_cal)) |
| 170 | + |
| 171 | + # Dummy save |
| 172 | + # dummy_data = np.zeros(image_data.shape, dtype=image_data.dtype) |
| 173 | + # imwrite(result_location, dummy_data) |
| 174 | + |
| 175 | + # Run external program |
| 176 | + cmdLine = 'start \"\" \"' + aivia_path + '\" \"' + tmp_path + '\"' |
| 177 | + |
| 178 | + args = shlex.split(cmdLine) |
| 179 | + subprocess.run(args, shell=True) |
| 180 | + |
| 181 | + |
| 182 | +if __name__ == '__main__': |
| 183 | + params = {'inputImagePath': r'D:\PythonCode\_tests\3D_Embryo_Fluo-IsotropicCube-8b.aivia.tif', |
| 184 | + 'resultPath': r'D:\PythonCode\_tests\output.tif', |
| 185 | + 'TCount': 1, |
| 186 | + 'ZCount': 192, |
| 187 | + 'Calibration': 'XYZT: 0.46 micrometers, 0.46 micrometers, 0.46 micrometers, 1 Default'} |
| 188 | + run(params) |
| 189 | + |
| 190 | +# CHANGELOG |
| 191 | +# v1_00: - Including isotropic scaling and proper export to Aivia |
| 192 | +# v1_10: - Adding a GUI to choose rotation axis + virtual environment activation |
| 193 | +# v1_11: - New virtual env code for auto-activation |
0 commit comments