diff --git a/build/lib/Python/__init__.py b/build/lib/Python/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/build/lib/Python/_property.py b/build/lib/Python/_property.py new file mode 100644 index 0000000000000000000000000000000000000000..bf7e7a53dc3f4f56ac3481e22f69832acfa264f0 --- /dev/null +++ b/build/lib/Python/_property.py @@ -0,0 +1,16 @@ +import requests +import json + +# retrieves/sets viewParams, renderParams, uiParams, etc.... +def setProperty(self,propertyName,propertyStruct): + URL = self._leversc_url("/"+propertyName) + response = requests.post(URL,json=propertyStruct) + +def getProperty(self,propertyName): + URL = self._leversc_url("/"+propertyName) + response = requests.get(URL) + property = response.json() + if 'list'==type(property): + # view and render params come as 1 element list + property = property[0] + return property diff --git a/src/Python/_readImage.py b/build/lib/Python/_readImage.py similarity index 100% rename from src/Python/_readImage.py rename to build/lib/Python/_readImage.py diff --git a/build/lib/Python/leversc.py b/build/lib/Python/leversc.py new file mode 100644 index 0000000000000000000000000000000000000000..05ca899fd6b3b4112b74967f2498c62388292505 --- /dev/null +++ b/build/lib/Python/leversc.py @@ -0,0 +1,396 @@ +import math +import json +import struct +import requests +import numpy as np +import os +import sys +import time +import pkgutil +import subprocess +from imageio import imread + +class leversc: + """ + A class wrapper to interface with the leversc viewer. + + This class is built to function similarly to matplotlib and Matlab's figure() function, + the leversc object represents a single leversc viewer window with associated image metadata (imD) + and visualization parameters. Object methods allow changing rendering parameters programatically + and send new images to view (with the same metadata). + + Parameters + ---------- + im : numpy.ndarray, optional + 5-D image to send to viewer on startup, can also be sent using the show(im) method + imD : dict, optional + Leverjs image metadata, can be reset using the setImageData(imD) method + figNum : int, default=1 + Figure number of the leversc viewer (default to 1) + + Raises + ------ + TypeError + If im is not None and is not of type numpy.ndarray + + See Also + -------- + show: Show 5-D image in the leversc viewer + """ + def __init__(self, im=None, imD=None, figNumber=1,strDB=None): + self._host = "http://localhost" + self._base_port = 3000 + self._npack = 4 + + self._figNumber = figNumber + self.setImageData(imD) + if strDB is not None: + self.showLEVER(strDB) + elif im is not None: + self.show(im) + + def setImageData(self, imD=None): + """ + Set the image metadata associated with this viewer window. + + This function will set or reset (in the case of imD=None or no arguments) the leverjs metadata associated with + this leversc viewer window. + + Parameters + ---------- + imD : dict, optional + Leverjs image metadata + Important Keys: + "PhysicalPixelSize" - The resolution of each pixel dimension x,y,z (generally in um) + "ChannelNames" - The names of each channel in the image + """ + if imD is None: + self._CONSTANTS={} + self._imageData = {} + elif 'imageData' in imD.keys(): + # passed in a CONSTANTS struct - use CONSTANTS.imageData + self._CONSTANTS=imD + self._imageData = imD['imageData'] + else: + self._CONSTANTS={} + self._imageData = imD + + + def setFigureNumber(self, figNumber): + """ + Set the the figure number (leversc viewer) associated with this object + + Parameters + ---------- + figNumber : int + This is the figure number (leversc window) that the object will send images to + + Raises + ------ + ValueError + If figNumber is less than 1 + """ + if figNumber <= 0: + raise ValueError("Figure number must be greater than zero") + self._figNumber = figNumber + + def captureImage(self): + if ( not self._check_leversc() ): + return None + # make sure leversc is ready + while (not self.drawComplete()): + time.sleep(0.1) + # do the capture + URL = self._leversc_url('screenCap') + im=imread(URL) + return im + + def drawComplete(self): + URL=self._leversc_url('drawComplete') + response=requests.get(URL) + bComplete=response.json() + return bComplete + def showLEVER(self,strDB): + """ + Open a .LEVER file in the leversc viewer + + Parameters + ---------- + strDB : string + /path/to/myFile.LEVER + + """ + if ( not self._check_leversc() ): + if ( not self._init_leversc(strDB) ): + return + + def show(self, im): + """ + Show a 5-D image in the leversc viewer + + Sends a 5-D numpy array to the leversc viewer for display. Note: if the imageData has not been set + then this method will set default imageData based on the input image. + + Parameters + ---------- + im : numpy.ndarray + 5-D image to send to viewer + + Raises + ------ + TypeError + If im is not of type numpy.ndarray + """ + if type(im) is not np.ndarray: + raise TypeError("Image must be a numpy array") + + if len(im.shape) < 3: + print("ERROR: 2D Images are not supported!") + return + + # Launch leversc viewer if not alread up + if ( not self._check_leversc() ): + if ( not self._init_leversc() ): + return + + # Normalize image access by creating an f_contiguous image view + imfctg = leversc._get_fcontig_imview(im) + if len(imfctg.shape) == 3: + imfctg = np.expand_dims(imfctg, axis=3) + # Normalize dimensions (make sure there are at least 4 (x,y,z,c)) + def onepad_slice(tpl, size): return (tpl + (1,)*(size-len(tpl))) + dims = onepad_slice(imfctg.shape, 4) + + if ( len(dims) > 4 and dims[4] > 1 ): + print('WARNING: Multi-frame images are not supported using frame 1!') + imfctg = imfctg[:,:,:,:,0] + + # Convert im to uint8 + chmax = np.amax(np.amax(np.amax(imfctg, axis=0, keepdims=True), axis=1, keepdims=True), axis=2, keepdims=True) + chim = ((255.0 * imfctg) / chmax).astype("uint8") + + # Setup valid imageData + self._imageData = leversc._imd_from_im(imfctg,dims,self._imageData) + + header_json,count_packs = self._make_header(dims) + + # Multipart-post request send + multipart = [("header", (None, header_json, "application/json"))] + for i in range(count_packs): + multipart.append(("lbins",("lbin%d"%(i), self._im_to_lbin(chim,dims,i), "application/octet-stream"))) + + resp = requests.post(url=self._leversc_url("/loadfig"), files=multipart) + + # if we have a CONSTANTS and a renderParams, set it + if self._CONSTANTS!={} and 'renderParams' in self._CONSTANTS.keys(): + self.renderParams=self._CONSTANTS['renderParams'] + + + def _im_to_lbin(self,im,dims,pidx): + choffset = pidx * self._npack + chsize = min(dims[3]-choffset, self._npack) + + imsub = im[:,:,:,choffset:(choffset+chsize)] + lbin_size = 4*2 + np.prod(dims[:3])*chsize + + outbytes = bytearray(lbin_size) + struct.pack_into("!HHHH", outbytes, 0, chsize,dims[0],dims[1],dims[2]) + imout = np.frombuffer(memoryview(outbytes)[(4*2):], "uint8") + + imout[:] = np.reshape(np.transpose(imsub, (3,0,1,2)), -1, order='F') + return outbytes + + + def _make_header(self, dims): + count_packs = math.ceil(dims[3] / self._npack) + + # Make json metadata header + imdjson = json.dumps(self._imageData) + return imdjson,count_packs + + def _check_leversc(self): + try: + resp = requests.get(url=self._leversc_url("/info"), timeout=0.5) + except requests.exceptions.ConnectTimeout as e: + return False + except requests.exceptions.ConnectionError as e: + return False + except Exception as e: + print("Unable to contact leversc server: %s (%s)" % (self._leversc_url("/info"), e)) + return False + return True + + + def _leversc_url(self,reqpath): + if ('/' != reqpath[0]): + reqpath='/'+reqpath + return "%s:%s%s"%(self._host,self._base_port + self._figNumber, reqpath) + + + @staticmethod + def _get_fcontig_imview(im): + if im.flags.f_contiguous: + return im + else: + imflat = np.reshape(im, -1, order='A') + imfctg = np.reshape(imflat, im.shape[::-1], order='F') + return imfctg + + + @staticmethod + def _get_normalized_dims(im): + # Helper to pd im.shape out to required number of dimensions + def _opad_slice(tpl, size): return (tpl + (1,)*(size-len(tpl)))[:size] + + # Reverse the dimension order depending of f/c_contiguous + if im.flags.f_contiguous: + # dims = _opad_slice(im.shape, 4) + dims = _opad_slice(im.shape, 4) + else: + dims = _opad_slice(im.shape[::-1], 4) + return dims + + + @staticmethod + def _imd_from_im(im, dims, imD): + # Make sure every channel has a name ("Channel %i" by default) + chnames = imD["ChannelNames"] if "ChannelNames" in imD else [] + chnames = [chnames[x] if len(chnames) > x else "Channel %s"%(x+1) for x in range(dims[3])] + + # Set valid pixel size for each x,y,z dim + pxsize = imD["PixelPhysicalSize"] if "PixelPhysicalSize" in imD else [1,1,1] + pxsize = [x if x > 0 else 1 for x in pxsize] + + new_imD = {"Dimensions": dims[:3], + "NumberOfChannels": dims[3], + "ChannelNames": chnames, + "PixelPhysicalSize": pxsize, + "PixelFormat": "uint8"} + return new_imD + + + def _init_leversc(self,strDB=None): + leverpath = leversc._get_leverjs_path() + if ( not self._run_leversc(leverpath,strDB) ): + return False + + # Try to contact leversc after launch + for _ in range(10): + time.sleep(0.5) + if ( self._check_leversc() ): + return True + + print("Unable to contact Leversc app") + return False + + + def _run_leversc(self, leverpath,strDB): + runargs = None + if ( leverpath is None ): + # Try to run installed leversc from path + runargs = ["open","-a","leverjs.app","-n","--args"] if leversc._is_macos() else ["leverjs"] + else: + runargs = leversc._get_os_ljselectron_exec(leverpath) + + if ( runargs is None ): + print("Unable to start Leversc app") + return False + + if strDB is not None: + strDB=os.path.abspath(strDB) + runargs.append("--leverFile=%s" % strDB) + port = self._base_port + self._figNumber + runargs.append("--port=%s" % port) + runargs.append("--title=figure %s" % self._figNumber) + p = leversc._exec_bg_process(runargs) + if ( p is None): + return False + return True + + + @staticmethod + def _get_os_ljselectron_exec(leverpath): + runargs = None + if ( leversc._is_windows() ): + elec_path = os.path.join(leverpath,"node_modules","electron","dist","electron.exe") + mainjs_path = os.path.join(leverpath,"elever","main.js") + runargs = [elec_path, mainjs_path] + elif ( leversc._is_linux() or leversc._is_macos() ): + elec_path = os.path.join(leverpath,"node_modules",".bin","electron") + mainjs_path = os.path.join(leverpath,"elever","main.js") + runargs = [elec_path, mainjs_path] + + return runargs + + + @staticmethod + def _exec_bg_process(args): + try: + if ( leversc._is_windows() ): + p = subprocess.Popen(args, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) + elif ( leversc._is_linux() or leversc._is_macos() ): + p = subprocess.Popen(args, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True) + else: + return None + except Exception as e: + print("Unable to launch leversc: (%s)"%e) + return None + return p + + + @staticmethod + def _is_windows(): + return (sys.platform == "win32") + + @staticmethod + def _is_linux(): + return (sys.platform == "linux" or sys.platform == "cygwin") + + @staticmethod + def _is_macos(): + return (sys.platform == "darwin") + + + @staticmethod + def _get_leverjs_path(): + # Check if ljspath.py is on sys.path (e.g. PYTHONPATH) + chk_ldr = pkgutil.find_loader("ljspath") + if ( chk_ldr is None ): + return None + # Try to load the module + ljs_m = chk_ldr.load_module("ljspath") + if ( ljs_m is None ): + return None + return ljs_m.get_ljspath() + + from _readImage import readImage + from _property import setProperty,getProperty + @property + def viewParams(self): + return self.getProperty('viewParams') + @viewParams.setter + def viewParams(self,VP): + self.setProperty('viewParams',VP) + @property + def renderParams(self): + return self.getProperty('renderParams') + @renderParams.setter + def renderParams(self,RP): + self.setProperty('renderParams',RP) + @property + def uiParams(self): + return self.getProperty('uiParams') + @uiParams.setter + def uiParams(self,UIP): + self.setProperty('uiParams',UIP) + + + diff --git a/src/Python/test_cells3d.py b/build/lib/Python/test_cells3d.py similarity index 100% rename from src/Python/test_cells3d.py rename to build/lib/Python/test_cells3d.py diff --git a/src/Python/test_leversc.py b/build/lib/Python/test_leversc.py similarity index 100% rename from src/Python/test_leversc.py rename to build/lib/Python/test_leversc.py diff --git a/readme.md b/readme.md index d405e0b69cf2c935f34eda122e7b5accfc06cdf4..8103a0f7cca97a51850a17db88d7108322dc7d06 100644 --- a/readme.md +++ b/readme.md @@ -12,6 +12,8 @@ LEVERSC is a cross-platform multichannel 3-D visualization tool for volumetric f ### Windows App Install - Download and run the [Windows installer](https://leverjs.net/download/app/Windows/). +Note: The Windows install will generate a security warning because the App is not signed. App signing on Windows requires an annual subscription for the certificate authority and is not currently supported by LEVERSC. We provide md5 checksums for the App on all platforms. + ### Linux App Manual Install 1. Download the [Linux Appimage](https://leverjs.net/download/app/Posix/). 2. Symlink the appimage file to a folder in ```$PATH``` (e.g. ```~/.local/bin```). @@ -28,12 +30,12 @@ LEVERSC is a cross-platform multichannel 3-D visualization tool for volumetric f **Note:** If using Fiji (**highly recommended**) then you may wish to create the ```3D``` subfolder in ```plugins``` and place the jar file within, as it can make it easier to find. Alternatively, the jar file can be placed directly in the ```plugins``` folder and will be listed in the Fiji menu as ```Plugins->Leversc Viewer```. -### Python Module +### Python Module – requires >= Python 3.6 1. Download the [Python module](src/Python) directory (Select the download icon and choose ```Download this directory```). 2. Extract the downloaded Python folder to a convenient location. 3. Add the folder to your ```PYTHONPATH``` environment variable so that ```import leversc``` statements can automatically find the LEVERSC module -### MATLAB Class +### MATLAB Class – requires MATLAB 2019B or later 1. Download the [MATLAB class](src/MATLAB) directory (Select the download icon and choose ```Download this directory```). 2. Extract the downloaded MATLAB class and support folders to a convenient location. 3. Add the folder to your MATLAB path, for example by adding the statement ```addpath('path/to/extracted/folder')``` to your ```startup.m``` file. @@ -45,10 +47,26 @@ LEVERSC is a cross-platform multichannel 3-D visualization tool for volumetric f NOTE: Fiji/ImageJ will not be able to open the HDF5 format sample image that ships with LEVERSC. Be sure to use the OME TIFF. + + NOTE: There is currently no scripting support available from within Fiji/ImageJ. In order to control the viewer programmatically you need to use MATLAB or Python as described below. + 2. Choose ```Open...``` in Fiji and navigate to the downloaded image (or another dataset). 3. Once the image has loaded, select ```Plugins->3D->Leversc Viewer```, this should launch the LEVERSC program and load the image data for the selected frame. ### Python + +#### without source code +##### windows: +```pip install leversc``` +##### mac/linux: +```pip3 install leversc``` + +Verify install: +``` +>>> from leversc.test_random import test_random +>>> test_random() +``` +#### with source code 1. Download or clone the full [LEVERSC repository](https://git-bioimage.coe.drexel.edu/opensource/leversc/-/archive/master/leversc-master.zip) 2. Navigate to the LEVERSC ```src/Python``` folder. 3. Run: ```python test_leversc.py```. diff --git a/src/MATLAB/AddSQLiteToPath.m b/src/MATLAB/AddSQLiteToPath.m new file mode 100644 index 0000000000000000000000000000000000000000..d3c6c3a23251b89afffcca57ba90507ee72f0625 --- /dev/null +++ b/src/MATLAB/AddSQLiteToPath.m @@ -0,0 +1,26 @@ +function AddSQLiteToPath() + %CHECKJARPATH Summary of this function goes here + % Detailed explanation goes here + + if isdeployed() + return + end + %% ensure that the sqlite jar file is on the path + dynamicPaths = javaclasspath('-dynamic'); + bfIsLoaded = false; + if (~isempty(dynamicPaths)) + for i=1:length(dynamicPaths) + [~,name,~] = fileparts(dynamicPaths{i}); + if (strcmpi('sqlite-jdbc-3.21.0',name)) + bfIsLoaded = true; + break + end + end + end + + if (~bfIsLoaded) + curPath = mfilename('fullpath'); + [pathstr,~,~] = fileparts(curPath); + javaaddpath(fullfile(pathstr,'sqlite-jdbc-3.21.0.jar'),'-end'); + end +end \ No newline at end of file diff --git a/src/MATLAB/sampleReadVolume.m b/src/MATLAB/sampleReadVolume.m new file mode 100644 index 0000000000000000000000000000000000000000..039cd418b72a9a302536f8dd82169dbc1fb2fad9 --- /dev/null +++ b/src/MATLAB/sampleReadVolume.m @@ -0,0 +1,34 @@ +function [im,CONSTANTS] = sampleReadVolume() +% +% we can use the leverjs tools to read the sample movie. this is preferred +% if you have leverjs on your path (see https://leverjs.net/git) and the +% database toolbox installed. NOTE the .LEVER version of the sample image +% is stored in lscSampleImage.h5 and contains 2 channels (vs. the 1 channel +% ome sample tiff version) +% +% AddSQLiteToPath(); +% strDB='../../sampleImages/lscSampleImage.LEVER'; +% [im,CONSTANTS]=leversc.loadImage(strDB); + +% +% alternatively, we can read the sample image directly, and build our own +% metadata. +% +fnameTiff='../../sampleImages/lscSampleImage.ome.tiff'; +imageInfo=imfinfo(fnameTiff); +im=[]; +for z=1:length(imageInfo) + im(:,:,z)=imread(fnameTiff,z); +end +% set up our own metadata. leverjs stores metadata in the .LEVER file, but +% that is not required to use the viewer. +CONSTANTS=[]; +CONSTANTS.imageData=leversc.makeMetadata(im); +% pixelPhysicalSize is IMPORTANT! generally comes from microscope. you +% really want to set this value correctly. +CONSTANTS.imageData.PixelPhysicalSize=[0.37,0.37,0.6]; +CONSTANTS.imageData.DatasetName='lscSampleImage'; +CONSTANTS.imageData.ChannelNames={'H2B'}; % nuclear marker +CONSTANTS.imageData.ChannelColors=[1,0,0]; % r,g,b on 0,1 + + diff --git a/src/MATLAB/sampleVolumeMovie.m b/src/MATLAB/sampleVolumeMovie.m index 08b90c0d71f20fd2673b60eef7902f43156c1e1c..9889645fef63ac144fc2e21f6bd1985bcb41e714 100644 --- a/src/MATLAB/sampleVolumeMovie.m +++ b/src/MATLAB/sampleVolumeMovie.m @@ -1,9 +1,8 @@ % sample script to generate a rotating volume movie - +% this is the movie from supplementary figure 1 function sampleVolumeMovie(bPreview) %% Load image (in this case from a LEVER file) - strDB='../../sampleImages/lscSampleImage.LEVER'; - [im,CONSTANTS]=leversc.loadImage(strDB); + [im,CONSTANTS] = sampleReadVolume(); lsc=leversc(im, CONSTANTS.imageData); %% Set up pre-selected render properties diff --git a/src/MATLAB/sqlite-jdbc-3.21.0.jar b/src/MATLAB/sqlite-jdbc-3.21.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..72222c8e0d9783bfd9ca488947187f00bfaa9987 --- /dev/null +++ b/src/MATLAB/sqlite-jdbc-3.21.0.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e0951687e18faf405655ed37ce76c54b3b7ac9a5ab89f88c23b9150b01be396 +size 6672489 diff --git a/src/Python/build/lib/leversc/__init__.py b/src/Python/build/lib/leversc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b58b40a0d40f7abec4276bf55d75077f305af168 --- /dev/null +++ b/src/Python/build/lib/leversc/__init__.py @@ -0,0 +1 @@ +__all__ = ["leversc", "_readImage", "_property"] diff --git a/src/Python/leversc/__init__.py b/src/Python/leversc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b58b40a0d40f7abec4276bf55d75077f305af168 --- /dev/null +++ b/src/Python/leversc/__init__.py @@ -0,0 +1 @@ +__all__ = ["leversc", "_readImage", "_property"] diff --git a/src/Python/leversc/_property.py b/src/Python/leversc/_property.py new file mode 100644 index 0000000000000000000000000000000000000000..bf7e7a53dc3f4f56ac3481e22f69832acfa264f0 --- /dev/null +++ b/src/Python/leversc/_property.py @@ -0,0 +1,16 @@ +import requests +import json + +# retrieves/sets viewParams, renderParams, uiParams, etc.... +def setProperty(self,propertyName,propertyStruct): + URL = self._leversc_url("/"+propertyName) + response = requests.post(URL,json=propertyStruct) + +def getProperty(self,propertyName): + URL = self._leversc_url("/"+propertyName) + response = requests.get(URL) + property = response.json() + if 'list'==type(property): + # view and render params come as 1 element list + property = property[0] + return property diff --git a/src/Python/leversc/_readImage.py b/src/Python/leversc/_readImage.py new file mode 100644 index 0000000000000000000000000000000000000000..ed17d98dd913869a0667c3bbbf1b439771881170 --- /dev/null +++ b/src/Python/leversc/_readImage.py @@ -0,0 +1,55 @@ +import numpy as np +import h5py, sqlite3, os, sys, json + +def getConstants(strDB): + conn = sqlite3.connect(strDB) + c=conn.cursor() + c.execute('select jsConstants from tblConstants') + res=c.fetchone() + # r[0] is jsConstants + CONSTANTS=json.loads(res[0]) + conn.close() + return(CONSTANTS) + +def checkImageLocationTypes(targetFolder,strDB): + types=['.h5','.klb'] + basename=os.path.basename(strDB) + basename=os.path.splitext(basename) + basename=basename[0] + for t in types: + target=os.path.join(targetFolder,basename+t) + if os.path.exists(target): + return(target) + return False + +def getImageFileName(strDB,CONSTANTS): + # 1st check folder with .LEVER file + target=os.path.dirname(strDB) + leverFile=os.path.basename(strDB) + imageFile=checkImageLocationTypes(target,strDB) + if False!=imageFile: + return imageFile + # 2nd check CONSTANTS.imageDir + target=CONSTANTS['imageDir'] + imageFile=checkImageLocationTypes(target,strDB) + if False!=imageFile: + return imageFile + print(strDB+' :: image file not found',file=sys.stderr) + # finally, check lever.json override + # ACK todo +def readImage(strDB,time=0,channel=0): + """ readImage + strDB - full path to lever file + CONSTANTS - from leverjs.readConstants() + time - scalar + channel - list of channels + """ + CONSTANTS=getConstants(strDB) + imageFile=getImageFileName(strDB,CONSTANTS) + with h5py.File(imageFile, 'r') as f: + fTarget=f['Images']['Original'] + im=fTarget[time,channel,:,:,:] + # note t must be scalar, so axis order + # im is (c,z,y,x) - reverse axis order for (x,y,z,c) + im=np.transpose(im) + return (im,CONSTANTS) \ No newline at end of file diff --git a/src/Python/leversc/leversc.py b/src/Python/leversc/leversc.py new file mode 100644 index 0000000000000000000000000000000000000000..a643794c1ccb892ac18dd97d67b093c421724096 --- /dev/null +++ b/src/Python/leversc/leversc.py @@ -0,0 +1,397 @@ +import math +import json +import struct +import requests +import numpy as np +import os +import sys +import time +import pkgutil +import subprocess +from imageio import imread + +class leversc: + """ + A class wrapper to interface with the leversc viewer. + + This class is built to function similarly to matplotlib and Matlab's figure() function, + the leversc object represents a single leversc viewer window with associated image metadata (imD) + and visualization parameters. Object methods allow changing rendering parameters programatically + and send new images to view (with the same metadata). + + Parameters + ---------- + im : numpy.ndarray, optional + 5-D image to send to viewer on startup, can also be sent using the show(im) method + imD : dict, optional + Leverjs image metadata, can be reset using the setImageData(imD) method + figNum : int, default=1 + Figure number of the leversc viewer (default to 1) + + Raises + ------ + TypeError + If im is not None and is not of type numpy.ndarray + + See Also + -------- + show: Show 5-D image in the leversc viewer + """ + def __init__(self, im=None, imD=None, figNumber=1,strDB=None): + self._host = "http://localhost" + self._base_port = 3000 + self._npack = 4 + + self._figNumber = figNumber + self.setImageData(imD) + if strDB is not None: + self.showLEVER(strDB) + elif im is not None: + self.show(im) + + def setImageData(self, imD=None): + """ + Set the image metadata associated with this viewer window. + + This function will set or reset (in the case of imD=None or no arguments) the leverjs metadata associated with + this leversc viewer window. + + Parameters + ---------- + imD : dict, optional + Leverjs image metadata + Important Keys: + "PhysicalPixelSize" - The resolution of each pixel dimension x,y,z (generally in um) + "ChannelNames" - The names of each channel in the image + """ + if imD is None: + self._CONSTANTS={} + self._imageData = {} + elif 'imageData' in imD.keys(): + # passed in a CONSTANTS struct - use CONSTANTS.imageData + self._CONSTANTS=imD + self._imageData = imD['imageData'] + else: + self._CONSTANTS={} + self._imageData = imD + + + def setFigureNumber(self, figNumber): + """ + Set the the figure number (leversc viewer) associated with this object + + Parameters + ---------- + figNumber : int + This is the figure number (leversc window) that the object will send images to + + Raises + ------ + ValueError + If figNumber is less than 1 + """ + if figNumber <= 0: + raise ValueError("Figure number must be greater than zero") + self._figNumber = figNumber + + def captureImage(self): + if ( not self._check_leversc() ): + return None + # make sure leversc is ready + while (not self.drawComplete()): + time.sleep(0.1) + # do the capture + URL = self._leversc_url('screenCap') + im=imread(URL) + return im + + def drawComplete(self): + URL=self._leversc_url('drawComplete') + response=requests.get(URL) + bComplete=response.json() + return bComplete + def showLEVER(self,strDB): + """ + Open a .LEVER file in the leversc viewer + + Parameters + ---------- + strDB : string + /path/to/myFile.LEVER + + """ + if ( not self._check_leversc() ): + if ( not self._init_leversc(strDB) ): + return + + def show(self, im): + """ + Show a 5-D image in the leversc viewer + + Sends a 5-D numpy array to the leversc viewer for display. Note: if the imageData has not been set + then this method will set default imageData based on the input image. + + Parameters + ---------- + im : numpy.ndarray + 5-D image to send to viewer + + Raises + ------ + TypeError + If im is not of type numpy.ndarray + """ + if type(im) is not np.ndarray: + raise TypeError("Image must be a numpy array") + + if len(im.shape) < 3: + print("ERROR: 2D Images are not supported!") + return + + # Launch leversc viewer if not alread up + if ( not self._check_leversc() ): + if ( not self._init_leversc() ): + return + + # Normalize image access by creating an f_contiguous image view + imfctg = leversc._get_fcontig_imview(im) + if len(imfctg.shape) == 3: + imfctg = np.expand_dims(imfctg, axis=3) + # Normalize dimensions (make sure there are at least 4 (x,y,z,c)) + def onepad_slice(tpl, size): return (tpl + (1,)*(size-len(tpl))) + dims = onepad_slice(imfctg.shape, 4) + + if ( len(dims) > 4 and dims[4] > 1 ): + print('WARNING: Multi-frame images are not supported using frame 1!') + imfctg = imfctg[:,:,:,:,0] + + # Convert im to uint8 + chmax = np.amax(np.amax(np.amax(imfctg, axis=0, keepdims=True), axis=1, keepdims=True), axis=2, keepdims=True) + chim = ((255.0 * imfctg) / chmax).astype("uint8") + + # Setup valid imageData + self._imageData = leversc._imd_from_im(imfctg,dims,self._imageData) + + header_json,count_packs = self._make_header(dims) + + # Multipart-post request send + multipart = [("header", (None, header_json, "application/json"))] + for i in range(count_packs): + multipart.append(("lbins",("lbin%d"%(i), self._im_to_lbin(chim,dims,i), "application/octet-stream"))) + + resp = requests.post(url=self._leversc_url("/loadfig"), files=multipart) + + # if we have a CONSTANTS and a renderParams, set it + if self._CONSTANTS!={} and 'renderParams' in self._CONSTANTS.keys(): + self.renderParams=self._CONSTANTS['renderParams'] + + + def _im_to_lbin(self,im,dims,pidx): + choffset = pidx * self._npack + chsize = min(dims[3]-choffset, self._npack) + + imsub = im[:,:,:,choffset:(choffset+chsize)] + lbin_size = 4*2 + np.prod(dims[:3])*chsize + + outbytes = bytearray(lbin_size) + struct.pack_into("!HHHH", outbytes, 0, chsize,dims[0],dims[1],dims[2]) + imout = np.frombuffer(memoryview(outbytes)[(4*2):], "uint8") + + imout[:] = np.reshape(np.transpose(imsub, (3,0,1,2)), -1, order='F') + return outbytes + + + def _make_header(self, dims): + count_packs = math.ceil(dims[3] / self._npack) + + # Make json metadata header + imdjson = json.dumps(self._imageData) + return imdjson,count_packs + + def _check_leversc(self): + try: + resp = requests.get(url=self._leversc_url("/info"), timeout=0.5) + except requests.exceptions.ConnectTimeout as e: + return False + except requests.exceptions.ConnectionError as e: + return False + except Exception as e: + print("Unable to contact leversc server: %s (%s)" % (self._leversc_url("/info"), e)) + return False + return True + + + def _leversc_url(self,reqpath): + if ('/' != reqpath[0]): + reqpath='/'+reqpath + return "%s:%s%s"%(self._host,self._base_port + self._figNumber, reqpath) + + + @staticmethod + def _get_fcontig_imview(im): + if im.flags.f_contiguous: + return im + else: + imflat = np.reshape(im, -1, order='A') + imfctg = np.reshape(imflat, im.shape[::-1], order='F') + return imfctg + + + @staticmethod + def _get_normalized_dims(im): + # Helper to pd im.shape out to required number of dimensions + def _opad_slice(tpl, size): return (tpl + (1,)*(size-len(tpl)))[:size] + + # Reverse the dimension order depending of f/c_contiguous + if im.flags.f_contiguous: + # dims = _opad_slice(im.shape, 4) + dims = _opad_slice(im.shape, 4) + else: + dims = _opad_slice(im.shape[::-1], 4) + return dims + + + @staticmethod + def _imd_from_im(im, dims, imD): + # Make sure every channel has a name ("Channel %i" by default) + chnames = imD["ChannelNames"] if "ChannelNames" in imD else [] + chnames = [chnames[x] if len(chnames) > x else "Channel %s"%(x+1) for x in range(dims[3])] + + # Set valid pixel size for each x,y,z dim + pxsize = imD["PixelPhysicalSize"] if "PixelPhysicalSize" in imD else [1,1,1] + pxsize = [x if x > 0 else 1 for x in pxsize] + + new_imD = {"Dimensions": dims[:3], + "NumberOfChannels": dims[3], + "ChannelNames": chnames, + "PixelPhysicalSize": pxsize, + "PixelFormat": "uint8"} + return new_imD + + + def _init_leversc(self,strDB=None): + leverpath = leversc._get_leverjs_path() + if ( not self._run_leversc(leverpath,strDB) ): + return False + + # Try to contact leversc after launch + for _ in range(10): + time.sleep(0.5) + if ( self._check_leversc() ): + return True + + print("Unable to contact Leversc app") + return False + + + def _run_leversc(self, leverpath,strDB): + runargs = None + if ( leverpath is None ): + # Try to run installed leversc from path + runargs = ["open","-a","leverjs.app","-n","--args"] if leversc._is_macos() else ["leverjs"] + else: + runargs = leversc._get_os_ljselectron_exec(leverpath) + + if ( runargs is None ): + print("Unable to start Leversc app") + return False + + if strDB is not None: + strDB=os.path.abspath(strDB) + runargs.append("--leverFile=%s" % strDB) + port = self._base_port + self._figNumber + runargs.append("--port=%s" % port) + runargs.append("--title=figure %s" % self._figNumber) + p = leversc._exec_bg_process(runargs) + if ( p is None): + return False + return True + + + @staticmethod + def _get_os_ljselectron_exec(leverpath): + runargs = None + if ( leversc._is_windows() ): + elec_path = os.path.join(leverpath,"node_modules","electron","dist","electron.exe") + mainjs_path = os.path.join(leverpath,"elever","main.js") + runargs = [elec_path, mainjs_path] + elif ( leversc._is_linux() or leversc._is_macos() ): + elec_path = os.path.join(leverpath,"node_modules",".bin","electron") + mainjs_path = os.path.join(leverpath,"elever","main.js") + runargs = [elec_path, mainjs_path] + + return runargs + + + @staticmethod + def _exec_bg_process(args): + try: + if ( leversc._is_windows() ): + p = subprocess.Popen(args, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) + elif ( leversc._is_linux() or leversc._is_macos() ): + p = subprocess.Popen(args, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True) + else: + return None + except Exception as e: + print("Unable to launch leversc: (%s)"%e) + print("please check the app is properly installed. see https://leverjs.net/leversc for details.") + return None + return p + + + @staticmethod + def _is_windows(): + return (sys.platform == "win32") + + @staticmethod + def _is_linux(): + return (sys.platform == "linux" or sys.platform == "cygwin") + + @staticmethod + def _is_macos(): + return (sys.platform == "darwin") + + + @staticmethod + def _get_leverjs_path(): + # Check if ljspath.py is on sys.path (e.g. PYTHONPATH) + chk_ldr = pkgutil.find_loader("ljspath") + if ( chk_ldr is None ): + return None + # Try to load the module + ljs_m = chk_ldr.load_module("ljspath") + if ( ljs_m is None ): + return None + return ljs_m.get_ljspath() + + from leversc._readImage import readImage + from leversc._property import setProperty,getProperty + @property + def viewParams(self): + return self.getProperty('viewParams') + @viewParams.setter + def viewParams(self,VP): + self.setProperty('viewParams',VP) + @property + def renderParams(self): + return self.getProperty('renderParams') + @renderParams.setter + def renderParams(self,RP): + self.setProperty('renderParams',RP) + @property + def uiParams(self): + return self.getProperty('uiParams') + @uiParams.setter + def uiParams(self,UIP): + self.setProperty('uiParams',UIP) + + + diff --git a/src/Python/leversc/test.py b/src/Python/leversc/test.py new file mode 100644 index 0000000000000000000000000000000000000000..85e917e9a8baaf325feb75d7232e9d832480e4cc --- /dev/null +++ b/src/Python/leversc/test.py @@ -0,0 +1,3 @@ +import leversc as lsc + +pass \ No newline at end of file diff --git a/src/Python/leversc/test_cells3d.py b/src/Python/leversc/test_cells3d.py new file mode 100644 index 0000000000000000000000000000000000000000..1f42e53204f1a5e5c03ba70f97451075cdb66269 --- /dev/null +++ b/src/Python/leversc/test_cells3d.py @@ -0,0 +1,19 @@ +""" +This small example loads the cells3d dataset from scikit-image into LEVERSC for visualization +""" +import numpy as np +from skimage.data import cells3d + +import leversc + +# Load the cells3d dataset +cells = cells3d() +# Rearrange the dimensions to be in expected order (c,z,y,x) for row-major numpy array +cellsC = np.copy(np.transpose(cells,[1,0,2,3]), order='C') + +# Set up the image metadata: such as physical voxel size, and channel names +# See: https://scikit-image.org/docs/0.18.x/api/skimage.data.html?highlight=cells3d#skimage.data.cells3d +imD = {"PixelPhysicalSize": [0.29,0.26,0.26], + "ChannelNames": ["Membrane","Nuclei"]} +# Send data to the LEVERSC viewer +lsc = leversc.leversc(im=cellsC, imD=imD) diff --git a/src/Python/leversc/test_leversc.py b/src/Python/leversc/test_leversc.py new file mode 100644 index 0000000000000000000000000000000000000000..c8819dd4ac14ad9152cd8722a48353d2c57184f9 --- /dev/null +++ b/src/Python/leversc/test_leversc.py @@ -0,0 +1,51 @@ +""" Python sample for using the leversc multichannel 3-d viewer + mark winter and andrew r. cohen + + these routines show how to use the leversc class. +""" + + +import numpy as np, matplotlib.pyplot as plt +from leversc import leversc +import time +# if you have leverjs git repository on your path, add +# the environment variable PYTHONPATH=/path/to/leverjs.git/python +# + +def setMovieUI(lsc): + """ setMovieUI(lsc) - sets the ui elements for screen capture + lsc - leversc class instance + """ + # wait for leversc to finish any image render before we set ui + # so all pending ui updates are completed before our changes + while (not lsc1.drawComplete()): + time.sleep(0.1) + # pull ui dictionary, modify and then write it back + # this allows us to properly use the property setter, and also allows + # bulk property changes + ui=lsc.uiParams + ui['webToolbar']='none' + ui['clockButton']='none' + ui['sidebar']='none' + lsc.uiParams=ui + +# show a random image +# im = np.random.rand(1,50,512,1024) +# lsc1 = leversc(im=im) + +# show the .LEVER sample image +strDB='../../sampleImages/lscSampleImage.LEVER' +(im,CONSTANTS)=leversc.readImage(strDB) +lsc1 = leversc(im=im,imD=CONSTANTS) + +# capture a screen shot +# first, set ui. NOTE - read property, set it, write it back +# so setter properly updates it.' +setMovieUI(lsc1) +# now grab image +im=lsc1.captureImage() +# show image +plt.imshow(im) +plt.show() +# make movie, rotate, etc... +pass \ No newline at end of file diff --git a/src/Python/leversc/test_random.py b/src/Python/leversc/test_random.py new file mode 100644 index 0000000000000000000000000000000000000000..481d1c6beb65cef5279068af1a856ac26e16fd42 --- /dev/null +++ b/src/Python/leversc/test_random.py @@ -0,0 +1,14 @@ + +# from leversc.test_random import test_random +def test_random(): + import numpy as np + from leversc import leversc + + imRandom = np.random.random_sample((256,256,256)) + # Set up the image metadata: such as physical voxel size, and channel names + # See: https://scikit-image.org/docs/0.18.x/api/skimage.data.html?highlight=cells3d#skimage.data.cells3d + imD = {"PixelPhysicalSize": [0.29,0.26,0.26], + "ChannelNames": ["random_sample"]} + # Send data to the LEVERSC viewer + leversc.leversc(im=imRandom, imD=imD) + pass \ No newline at end of file diff --git a/src/Python/pyproject.toml b/src/Python/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..b5a3c468d9e85e7fa7469c3a90d47b48ab93e54a --- /dev/null +++ b/src/Python/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/src/Python/leversc pip requirements.txt b/src/Python/requirements.txt similarity index 64% rename from src/Python/leversc pip requirements.txt rename to src/Python/requirements.txt index 8ec573c14c3f800cdca1cf7c64c3bfbfd7196d53..473b914b54b643808684e012b5d8945edcabbf20 100644 --- a/src/Python/leversc pip requirements.txt +++ b/src/Python/requirements.txt @@ -1,3 +1 @@ -pip requirements - matplotlib h5py imageio requests \ No newline at end of file diff --git a/src/Python/setup.cfg b/src/Python/setup.cfg new file mode 100644 index 0000000000000000000000000000000000000000..1a83da5e535df04938ad03cf549584079dfdf97e --- /dev/null +++ b/src/Python/setup.cfg @@ -0,0 +1,23 @@ +[metadata] +name = leversc +version = 22.1.19 +author = Andrew R. Cohen & Mark Winter +author_email = andrew.r.cohen@drexel.edu +description = 4-D scriptable visualization +long_description = file: README.md +long_description_content_type = text/markdown +url = https://leverjs.net/leversc +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +packages = find: +python_requires = >=3.6 +install_requires = + matplotlib + h5py + imageio + requests + scikit-image \ No newline at end of file diff --git a/src/Python/setup.py b/src/Python/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..8ab824cc7c6794bf18e5d66aa39ad7d73fc731f7 --- /dev/null +++ b/src/Python/setup.py @@ -0,0 +1,2 @@ +from setuptools import setup +setup() \ No newline at end of file