├── java-archive.tgz ├── dm3_lib ├── __init__.py ├── demo │ ├── utilities.py │ └── demo.py └── _dm3_lib.py ├── LICENSE.txt ├── setup.py ├── TODO.txt └── README.rst /java-archive.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piraynal/pyDM3reader/HEAD/java-archive.tgz -------------------------------------------------------------------------------- /dm3_lib/__init__.py: -------------------------------------------------------------------------------- 1 | from ._dm3_lib import VERSION 2 | from ._dm3_lib import DM3 3 | from ._dm3_lib import SUPPORTED_DATA_TYPES 4 | -------------------------------------------------------------------------------- /dm3_lib/demo/utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import numpy as np 4 | 5 | # histogram, re-compute cuts 6 | 7 | def calcHistogram(imdata, bins_=256): 8 | '''Compute image histogram.''' 9 | im_values = np.ravel(imdata) 10 | hh, bins_ = np.histogram( im_values, bins=bins_ ) 11 | return hh, bins_ 12 | 13 | def calcDisplayRange(imdata, cutoff=.1, bins_=512): 14 | '''Compute display range, i.e., cuts. 15 | (ignore the 'cutoff'% lowest/highest value pixels)''' 16 | # compute image histogram 17 | hh, bins_ = calcHistogram(imdata, bins_) 18 | # check histogram format 19 | if len(bins_)==len(hh): 20 | bb = bins_ 21 | else: 22 | bb = bins_[:-1] # 'bins' == bin_edges 23 | # number of pixels 24 | Npx = np.sum(hh) 25 | # calc. lower limit : 26 | i = 1 27 | while np.sum( hh[:i] ) < Npx*cutoff/100.: 28 | i += 1 29 | cut0 = round( bb[i] ) 30 | # calc. higher limit 31 | j = 1 32 | while np.sum( hh[-j:] ) < Npx*cutoff/100.: 33 | j += 1 34 | cut1 = round( bb[-j] ) 35 | return cut0,cut1 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013- Pierre-Ivan Raynal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | setup( 3 | name = "dm3_lib", 4 | version = "1.5", 5 | packages = ['dm3_lib'], 6 | 7 | install_requires = ['pillow>=2.3.1', 'numpy'], 8 | 9 | package_data = { 10 | # If any package contains *.txt or *.rst files, include them: 11 | '': ['*.txt', '*.rst'], 12 | 'dm3_lib': ['demo/demo.py', 'demo/utilities.py'], 13 | }, 14 | 15 | # metadata for upload to PyPI 16 | author = "Pierre-Ivan Raynal", 17 | author_email = "raynal@univ-tours.fr", 18 | description = "Python module for parsing GATAN DM3|DM4 (DigitalMicrograph) files", 19 | license = "MIT", 20 | keywords = "GATAN DigitalMicrograph DM3 DM4 Transmission Electron Microscopy", 21 | url = "https://microscopies.med.univ-tours.fr/pydm3reader/", # project home page, if any 22 | classifiers=[ 23 | 'Development Status :: 4 - Beta', 24 | 'Environment :: Console', 25 | 'Intended Audience :: Science/Research', 26 | 'License :: OSI Approved :: MIT License (MIT)' 27 | 'Natural Language :: English', 28 | 'Programming Language :: Python :: 2.7', 29 | 'Programming Language :: Python :: 3.4', 30 | 'Topic :: Scientific/Engineering :: Visualization'], 31 | # List of classifiers: 32 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 33 | # could also include long_description, download_url, classifiers, etc. 34 | ) 35 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | TODO file for pyDM3lib 2 | ====================== 3 | 4 | - support of additional image data types... 5 | -- 0: 'NULL_DATA', 6 | ++ 1: 'SIGNED_INT16_DATA', 7 | ++ 2: 'REAL4_DATA', 8 | -- 3: 'COMPLEX8_DATA', 9 | -- 4: 'OBSELETE_DATA', 10 | -- 5: 'PACKED_DATA', 11 | ++ 6: 'UNSIGNED_INT8_DATA', 12 | ++ 7: 'SIGNED_INT32_DATA', 13 | -- 8: 'RGB_DATA', 14 | ++ 9: 'SIGNED_INT8_DATA', 15 | ++ 10: 'UNSIGNED_INT16_DATA', 16 | ++ 11: 'UNSIGNED_INT32_DATA', 17 | -- 12: 'REAL8_DATA', 18 | -- 13: 'COMPLEX16_DATA', 19 | ++ 14: 'BINARY_DATA', 20 | -- 15: 'RGB_UINT8_0_DATA', 21 | -- 16: 'RGB_UINT8_1_DATA', 22 | -- 17: 'RGB_UINT16_DATA', 23 | -- 18: 'RGB_FLOAT32_DATA', 24 | -- 19: 'RGB_FLOAT64_DATA', 25 | -- 20: 'RGBA_UINT8_0_DATA', 26 | -- 21: 'RGBA_UINT8_1_DATA', 27 | -- 22: 'RGBA_UINT8_2_DATA', 28 | -- 23: 'RGBA_UINT8_3_DATA', 29 | -- 24: 'RGBA_UINT16_DATA', 30 | -- 25: 'RGBA_FLOAT32_DATA', 31 | -- 26: 'RGBA_FLOAT64_DATA', 32 | -- 27: 'POINT2_SINT16_0_DATA', 33 | -- 28: 'POINT2_SINT16_1_DATA', 34 | -- 29: 'POINT2_SINT32_0_DATA', 35 | -- 30: 'POINT2_FLOAT32_0_DATA', 36 | -- 31: 'RECT_SINT16_1_DATA', 37 | -- 32: 'RECT_SINT32_1_DATA', 38 | -- 33: 'RECT_FLOAT32_1_DATA', 39 | -- 34: 'RECT_FLOAT32_0_DATA', 40 | -- 35: 'SIGNED_INT64_DATA', 41 | -- 36: 'UNSIGNED_INT64_DATA', 42 | -- 37: 'LAST_DATA', 43 | 44 | - check against later IJ DM3_Reader versions 45 | 46 | - drop Python 2 support eventually 47 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Python DM3 Reader 3 | ================= 4 | 5 | 6 | Author: 7 | `Pierre-Ivan Raynal `_ 8 | (`https://microscopies.med.univ-tours.fr/ 9 | `_) 10 | 11 | Contributions by 12 | `Philippe Mallet-Ladeira `_ 13 | and 14 | `Jordan Chess `_ 15 | 16 | This Python module is an adaptation and extension of the `DM3_Reader` 17 | `ImageJ plug-in `_ by 18 | `Greg Jefferis `_. 19 | 20 | It allows to extract image data, metadata (specimen, operator, HV, MAG, etc.) 21 | and thumbnail from `GATAN DigitalMicrograph 3` and `4` files. In particular, 22 | it can dump all metadata (“Tags”), pass thumbnail and image data as PIL Images 23 | or Numpy arrays, as well as save the included thumbnail in a PNG file. 24 | (It was initially meant to be called by a script indexing electron microscope 25 | images in a database.) 26 | 27 | 28 | Dependencies 29 | ============ 30 | 31 | - Numpy 32 | - Pillow (fork of Python Imaging Library) 33 | 34 | Optional Dependencies 35 | --------------------- 36 | 37 | - Matplotlib 38 | - SciPy 39 | - IPython IDE 40 | 41 | 42 | Usage 43 | ===== 44 | 45 | Use in your own script would typically require lines such as:: 46 | 47 | import dm3_lib as dm3 48 | import matplotlib.pyplot as plt 49 | # parse DM3 file 50 | dm3f = dm3.DM3("sample.dm3") 51 | # print some useful image information 52 | print dm3f.info 53 | print "pixel size = %s %s"%dm3f.pxsize 54 | # display image 55 | plt.ion() 56 | plt.matshow(dm3f.imagedata, vmin=dm3f.cuts[0], vmax=dm3f.cuts[1]) 57 | plt.colorbar(shrink=.8) 58 | 59 | A more detailed example is located in the ``site-packages/dm3_lib/demo`` directory 60 | under the name ``demo.py``. 61 | 62 | Known Issues 63 | ============ 64 | 65 | Not all data types are implemented yet. 66 | -------------------------------------------------------------------------------- /dm3_lib/demo/demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import print_function, division 4 | 5 | import os.path 6 | import argparse 7 | 8 | import numpy as np 9 | import matplotlib.pyplot as plt 10 | 11 | from PIL import Image 12 | 13 | import dm3_lib as dm3 14 | 15 | from utilities import calcHistogram, calcDisplayRange 16 | 17 | # CONSTANTS 18 | 19 | savedir = os.path.expanduser("~/Desktop") 20 | debug = 0 21 | 22 | # define command line arguments 23 | parser = argparse.ArgumentParser() 24 | 25 | parser.add_argument("file", help="path to DM3 file to parse") 26 | parser.add_argument("-v", "--verbose", help="increase output verbosity", 27 | action="store_true") 28 | parser.add_argument("--dump", help="dump DM3 tags in text file", 29 | action="store_true") 30 | parser.add_argument("--convert", help="save image in various formats", 31 | action="store_true") 32 | 33 | # parse command line arguments 34 | args = parser.parse_args() 35 | if args.verbose: 36 | debug = 1 37 | filepath = args.file 38 | 39 | # get filename 40 | filename = os.path.split(filepath)[1] 41 | fileref = os.path.splitext(filename)[0] 42 | 43 | # pyplot interactive mode 44 | plt.ion() 45 | plt.close('all') 46 | 47 | # parse DM3 file 48 | dm3f = dm3.DM3(filepath, debug=debug) 49 | 50 | # get some useful tag data and print 51 | print("file:", dm3f.filename) 52 | print("file info.:") 53 | print(dm3f.info) 54 | print("scale: %.3g %s/px"%dm3f.pxsize) 55 | cuts = dm3f.cuts 56 | print("cuts:",cuts) 57 | 58 | # dump image Tags in txt file 59 | if args.dump: 60 | dm3f.dumpTags(savedir) 61 | 62 | # get image data 63 | aa = dm3f.imagedata 64 | 65 | # display image 66 | # - w/o cuts 67 | if args.verbose: 68 | plt.matshow(aa, cmap=plt.cm.pink) 69 | plt.title("%s (w/o cuts)"%filename) 70 | plt.colorbar(shrink=.8) 71 | # - w/ cuts (if cut values different) 72 | if cuts[0] != cuts[1]: 73 | plt.matshow(aa, cmap=plt.cm.pink, vmin=cuts[0], vmax=cuts[1]) 74 | plt.title("%s"%filename) 75 | plt.colorbar(shrink=.8) 76 | 77 | # - display image histogram 78 | if args.verbose: 79 | hh,bb = calcHistogram(aa) 80 | plt.figure('Image histogram') 81 | plt.plot(bb[:-1],hh,drawstyle='steps') 82 | plt.xlim(bb[0],bb[-1]) 83 | plt.xlabel('Intensity') 84 | plt.ylabel('Number') 85 | 86 | # convert image to various formats 87 | if args.convert: 88 | # save image as TIFF 89 | tif_file = os.path.join(savedir,fileref+'.tif') 90 | im = Image.fromarray(aa) 91 | im.save(tif_file) 92 | # check TIFF dynamic range 93 | tim = Image.open(tif_file) 94 | if tim.mode == 'L': 95 | tif_range = "8-bit" 96 | else: 97 | tif_range = "32-bit" 98 | print("Image saved as %s TIFF."%tif_range) 99 | 100 | # save image as PNG and JPG files 101 | # - normalize image for conversion to 8-bit 102 | aa_norm = aa.copy() 103 | # -- apply cuts (optional) 104 | if cuts[0] != cuts[1]: 105 | aa_norm[ (aa <= min(cuts)) ] = float(min(cuts)) 106 | aa_norm[ (aa >= max(cuts)) ] = float(max(cuts)) 107 | # -- normalize 108 | aa_norm = (aa_norm - np.min(aa_norm)) / (np.max(aa_norm) - np.min(aa_norm)) 109 | # -- scale to 0--255, convert to (8-bit) integer 110 | aa_norm = np.uint8(np.round( aa_norm * 255 )) 111 | 112 | if args.verbose: 113 | # - display normalized image 114 | plt.matshow(aa_norm, cmap=plt.cm.Greys_r) 115 | plt.title("%s [8-bit display]"%filename) 116 | plt.colorbar(shrink=.8) 117 | 118 | # - save as PNG and JPG 119 | im_dsp = Image.fromarray(aa_norm) 120 | im_dsp.save(os.path.join(savedir,fileref+'.png')) 121 | im_dsp.save(os.path.join(savedir,fileref+'.jpg')) 122 | 123 | -------------------------------------------------------------------------------- /dm3_lib/_dm3_lib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Python module for parsing GATAN DM3 and DM4 files""" 3 | 4 | ################################################################################ 5 | ## Python script for parsing GATAN DM3/DM4 (DigitalMicrograph) files 6 | ## -- 7 | ## based on the DM3_Reader plug-in (v1.3.4) for ImageJ 8 | ## by Greg Jefferis 9 | ## https://imagej.nih.gov/ij/plugins/DM3_Reader.html 10 | ## -- 11 | ## Python adaptation: Pierre-Ivan Raynal 12 | ## https://microscopies.med.univ-tours.fr/ 13 | ################################################################################ 14 | 15 | from __future__ import print_function 16 | 17 | import sys 18 | import os.path 19 | import struct 20 | import numpy 21 | from PIL import Image 22 | 23 | __all__ = ["DM3", "VERSION", "SUPPORTED_DATA_TYPES"] 24 | 25 | VERSION = '1.5' 26 | 27 | debugLevel = 0 # 0=none, 1-3=basic, 4-5=simple, 6-10 verbose 28 | 29 | ## check for Python version 30 | PY3 = (sys.version_info[0] == 3) 31 | 32 | ## - adjust for Python3 33 | if PY3: 34 | # unicode() deprecated in Python 3 35 | unicode_str = str 36 | else: 37 | unicode_str = unicode 38 | 39 | ### utility fuctions ### 40 | 41 | ### binary data reading functions ### 42 | 43 | def readByte(f): 44 | """Read 1 byte as integer in file f""" 45 | read_bytes = f.read(1) 46 | return struct.unpack('>b', read_bytes)[0] 47 | 48 | def readShort(f): 49 | """Read 2 bytes as BE integer in file f""" 50 | read_bytes = f.read(2) 51 | return struct.unpack('>h', read_bytes)[0] 52 | 53 | def readLong(f): 54 | """Read 4 bytes as BE integer in file f""" 55 | read_bytes = f.read(4) 56 | return struct.unpack('>l', read_bytes)[0] 57 | 58 | def readLongLong(f): 59 | """Read 8 bytes as BE integer in file f""" 60 | read_bytes = f.read(8) 61 | return struct.unpack('>q', read_bytes)[0] 62 | 63 | def readBool(f): 64 | """Read 1 byte as boolean in file f""" 65 | read_val = readByte(f) 66 | return (read_val!=0) 67 | 68 | def readChar(f): 69 | """Read 1 byte as char in file f""" 70 | read_bytes = f.read(1) 71 | return struct.unpack('c', read_bytes)[0] 72 | 73 | def readString(f, len_=1): 74 | """Read len_ bytes as a string in file f""" 75 | read_bytes = f.read(len_) 76 | str_fmt = '>'+str(len_)+'s' 77 | return struct.unpack( str_fmt, read_bytes )[0] 78 | 79 | def readLEShort(f): 80 | """Read 2 bytes as *little endian* integer in file f""" 81 | read_bytes = f.read(2) 82 | return struct.unpack(' reading function 137 | readFunc = { 138 | SHORT: readLEShort, 139 | LONG: readLELong, 140 | LONGLONG: readLELongLong, # DM4 141 | BELONGLONG: readLongLong, # DM4 142 | USHORT: readLEUShort, 143 | ULONG: readLEULong, 144 | FLOAT: readLEFloat, 145 | DOUBLE: readLEDouble, 146 | BOOLEAN: readBool, 147 | CHAR: readChar, 148 | OCTET: readChar, # difference with char??? 149 | } 150 | 151 | ## list of image DataTypes ## 152 | dataTypes = { 153 | 0: 'NULL_DATA', 154 | 1: 'SIGNED_INT16_DATA', 155 | 2: 'REAL4_DATA', 156 | 3: 'COMPLEX8_DATA', 157 | 4: 'OBSELETE_DATA', 158 | 5: 'PACKED_DATA', 159 | 6: 'UNSIGNED_INT8_DATA', 160 | 7: 'SIGNED_INT32_DATA', 161 | 8: 'RGB_DATA', 162 | 9: 'SIGNED_INT8_DATA', 163 | 10: 'UNSIGNED_INT16_DATA', 164 | 11: 'UNSIGNED_INT32_DATA', 165 | 12: 'REAL8_DATA', 166 | 13: 'COMPLEX16_DATA', 167 | 14: 'BINARY_DATA', 168 | 15: 'RGB_UINT8_0_DATA', 169 | 16: 'RGB_UINT8_1_DATA', 170 | 17: 'RGB_UINT16_DATA', 171 | 18: 'RGB_FLOAT32_DATA', 172 | 19: 'RGB_FLOAT64_DATA', 173 | 20: 'RGBA_UINT8_0_DATA', 174 | 21: 'RGBA_UINT8_1_DATA', 175 | 22: 'RGBA_UINT8_2_DATA', 176 | 23: 'RGBA_UINT8_3_DATA', 177 | 24: 'RGBA_UINT16_DATA', 178 | 25: 'RGBA_FLOAT32_DATA', 179 | 26: 'RGBA_FLOAT64_DATA', 180 | 27: 'POINT2_SINT16_0_DATA', 181 | 28: 'POINT2_SINT16_1_DATA', 182 | 29: 'POINT2_SINT32_0_DATA', 183 | 30: 'POINT2_FLOAT32_0_DATA', 184 | 31: 'RECT_SINT16_1_DATA', 185 | 32: 'RECT_SINT32_1_DATA', 186 | 33: 'RECT_FLOAT32_1_DATA', 187 | 34: 'RECT_FLOAT32_0_DATA', 188 | 35: 'SIGNED_INT64_DATA', 189 | 36: 'UNSIGNED_INT64_DATA', 190 | 37: 'LAST_DATA', 191 | } 192 | 193 | ## supported Data Types 194 | dT_supported = [1, 2, 6, 7, 9, 10, 11, 14] 195 | SUPPORTED_DATA_TYPES = {i: dataTypes[i] for i in dT_supported} 196 | 197 | ## other constants ## 198 | IMGLIST = "root.ImageList." 199 | OBJLIST = "root.DocumentObjectList." 200 | MAXDEPTH = 64 201 | 202 | DEFAULTCHARSET = 'utf-8' 203 | ## END constants ## 204 | 205 | 206 | class DM3(object): 207 | """DM3 object. """ 208 | 209 | ## utility functions 210 | def _makeGroupString(self): 211 | tString = str(self._curGroupAtLevelX[0]) 212 | for i in range( 1, self._curGroupLevel+1 ): 213 | tString += '.{}'.format(self._curGroupAtLevelX[i]) 214 | return tString 215 | 216 | def _makeGroupNameString(self): 217 | tString = self._curGroupNameAtLevelX[0] 218 | for i in range( 1, self._curGroupLevel+1 ): 219 | tString += '.' + str( self._curGroupNameAtLevelX[i] ) 220 | return tString 221 | 222 | def _readIntValue(self): 223 | if (self._fileVersion == 4): 224 | Val = readLongLong(self._f) 225 | else: 226 | Val = readLong(self._f) 227 | return Val 228 | 229 | def _readTagGroup(self): 230 | # go down a level 231 | self._curGroupLevel += 1 232 | # increment group counter 233 | self._curGroupAtLevelX[self._curGroupLevel] += 1 234 | # set number of current tag to -1 235 | # --- readTagEntry() pre-increments => first gets 0 236 | self._curTagAtLevelX[self._curGroupLevel] = -1 237 | if ( debugLevel > 5): 238 | print("rTG: Current Group Level:", self._curGroupLevel) 239 | # is the group sorted? 240 | sorted_ = readByte(self._f) 241 | isSorted = (sorted_ == 1) 242 | # is the group open? 243 | opened = readByte(self._f) 244 | isOpen = (opened == 1) 245 | # number of Tags 246 | nTags = self._readIntValue() 247 | if ( debugLevel > 5): 248 | print("rTG: Iterating over the", nTags, "tag entries in this group") 249 | # read Tags 250 | for i in range( nTags ): 251 | self._readTagEntry() 252 | # go back up one level as reading group is finished 253 | self._curGroupLevel += -1 254 | return 1 255 | 256 | def _readTagEntry(self): 257 | # is data or a new group? 258 | data = readByte(self._f) 259 | isData = (data == 21) 260 | self._curTagAtLevelX[self._curGroupLevel] += 1 261 | # get tag label if exists 262 | lenTagLabel = readShort(self._f) 263 | if ( lenTagLabel != 0 ): 264 | tagLabel = readString(self._f, lenTagLabel).decode('latin-1') 265 | else: 266 | tagLabel = str( self._curTagAtLevelX[self._curGroupLevel] ) 267 | if ( debugLevel > 5): 268 | print("{}|{}:".format(self._curGroupLevel, self._makeGroupString()), 269 | end=' ') 270 | print("Tag label = "+tagLabel) 271 | elif ( debugLevel > 1 ): 272 | print(str(self._curGroupLevel)+": Tag label = "+tagLabel) 273 | # if DM4 file, get tag data size 274 | if (self._fileVersion == 4): 275 | lenTagData = readLongLong(self._f) 276 | if ( debugLevel > 1 ): 277 | print(str(self._curGroupLevel)+": Tag data size = "+str(lenTagData)+" bytes") 278 | if isData: 279 | # give it a name 280 | self._curTagName = self._makeGroupNameString()+"."+tagLabel 281 | # read it 282 | self._readTagType() 283 | else: 284 | # it is a tag group 285 | self._curGroupNameAtLevelX[self._curGroupLevel+1] = tagLabel 286 | self._readTagGroup() # increments curGroupLevel 287 | return 1 288 | 289 | def _readTagType(self): 290 | delim = readString(self._f, 4).decode('latin-1') 291 | if ( delim != '%%%%' ): 292 | raise Exception(hex( self._f.tell() ) 293 | + ": Tag Type delimiter not %%%%") 294 | nInTag = self._readIntValue() 295 | self._readAnyData() 296 | return 1 297 | 298 | def _encodedTypeSize(self, eT): 299 | # returns the size in bytes of the data type 300 | if eT == 0: 301 | width = 0 302 | elif eT in (BOOLEAN, CHAR, OCTET): 303 | width = 1 304 | elif eT in (SHORT, USHORT): 305 | width = 2 306 | elif eT in (LONG, ULONG, FLOAT): 307 | width = 4 308 | elif eT in (DOUBLE, LONGLONG, BELONGLONG): 309 | width = 8 310 | else: 311 | # returns -1 for unrecognised types 312 | width = -1 313 | return width 314 | 315 | def _readAnyData(self): 316 | ## higher level function dispatching to handling data types 317 | ## to other functions 318 | # - get Type category (short, long, array...) 319 | encodedType = self._readIntValue() 320 | # - calc size of encodedType 321 | etSize = self._encodedTypeSize(encodedType) 322 | if ( debugLevel > 5): 323 | print("rAnD, " + hex( self._f.tell() ) + ":", end=' ') 324 | print("Tag Type = " + str(encodedType) + ",", end=' ') 325 | print("Tag Size = " + str(etSize)) 326 | if ( etSize > 0 ): 327 | self._storeTag( self._curTagName, 328 | self._readNativeData(encodedType, etSize) ) 329 | elif ( encodedType == STRING ): 330 | stringSize = self._readIntValue() 331 | self._readStringData(stringSize) 332 | elif ( encodedType == STRUCT ): 333 | # does not store tags yet 334 | structTypes = self._readStructTypes() 335 | self._readStructData(structTypes) 336 | elif ( encodedType == ARRAY ): 337 | # does not store tags yet 338 | # indicates size of skipped data blocks 339 | arrayTypes = self._readArrayTypes() 340 | self._readArrayData(arrayTypes) 341 | else: 342 | raise Exception("rAnD, " + hex(self._f.tell()) 343 | + ": Can't understand encoded type") 344 | return 1 345 | 346 | def _readNativeData(self, encodedType, etSize): 347 | # reads ordinary data types 348 | if encodedType in readFunc: 349 | val = readFunc[encodedType](self._f) 350 | else: 351 | raise Exception("rND, " + hex(self._f.tell()) 352 | + ": Unknown data type " + str(encodedType)) 353 | if ( debugLevel > 3 ): 354 | print("rND, " + hex(self._f.tell()) + ": " + str(val)) 355 | elif ( debugLevel > 1 ): 356 | print(val) 357 | return val 358 | 359 | def _readStringData(self, stringSize): 360 | # reads string data 361 | if ( stringSize <= 0 ): 362 | rString = "" 363 | else: 364 | if ( debugLevel > 3 ): 365 | print("rSD @ " + str(self._f.tell()) + "/" + hex(self._f.tell()) +" :", end=' ') 366 | rString = readString(self._f, stringSize) 367 | # /!\ UTF-16 unicode string => convert to Python unicode str 368 | rString = rString.decode('utf-16-le') 369 | if ( debugLevel > 3 ): 370 | print(rString + " <" + repr( rString ) + ">") 371 | if ( debugLevel > 1 ): 372 | print("StringVal:", rString) 373 | self._storeTag( self._curTagName, rString ) 374 | return rString 375 | 376 | def _readArrayTypes(self): 377 | # determines the data types in an array data type 378 | arrayType = self._readIntValue() 379 | itemTypes = [] 380 | if ( arrayType == STRUCT ): 381 | itemTypes = self._readStructTypes() 382 | elif ( arrayType == ARRAY ): 383 | itemTypes = self._readArrayTypes() 384 | else: 385 | itemTypes.append( arrayType ) 386 | return itemTypes 387 | 388 | def _readArrayData(self, arrayTypes): 389 | # reads array data 390 | arraySize = self._readIntValue() 391 | 392 | if ( debugLevel > 3 ): 393 | print("rArD, " + hex( self._f.tell() ) + ":", end=' ') 394 | print("Reading array of size = " + str(arraySize)) 395 | 396 | itemSize = 0 397 | encodedType = 0 398 | 399 | for i in range( len(arrayTypes) ): 400 | encodedType = int( arrayTypes[i] ) 401 | etSize = self._encodedTypeSize(encodedType) 402 | itemSize += etSize 403 | if ( debugLevel > 5 ): 404 | print("rArD: Tag Type = " + str(encodedType) + ",", end=' ') 405 | print("Tag Size = " + str(etSize)) 406 | ##! readNativeData( encodedType, etSize ) !## 407 | 408 | if ( debugLevel > 5 ): 409 | print("rArD: Array Item Size = " + str(itemSize)) 410 | 411 | bufSize = arraySize * itemSize 412 | 413 | if ( (not self._curTagName.endswith("ImageData.Data")) 414 | and ( len(arrayTypes) == 1 ) 415 | and ( encodedType == USHORT ) 416 | and ( arraySize < 256 ) ): 417 | # treat as string 418 | val = self._readStringData( bufSize ) 419 | else: 420 | # treat as binary data 421 | # - store data size and offset as tags 422 | self._storeTag( self._curTagName + ".Size", bufSize ) 423 | self._storeTag( self._curTagName + ".Offset", self._f.tell() ) 424 | # - skip data w/o reading 425 | self._f.seek( self._f.tell() + bufSize ) 426 | 427 | return 1 428 | 429 | def _readStructTypes(self): 430 | # analyses data types in a struct 431 | 432 | if ( debugLevel > 3 ): 433 | print("Reading Struct Types at Pos = " + hex(self._f.tell())) 434 | 435 | structNameLength = self._readIntValue() 436 | nFields = self._readIntValue() 437 | 438 | if ( debugLevel > 5 ): 439 | print("nFields = ", nFields) 440 | 441 | if ( nFields > 100 ): 442 | raise Exception(hex(self._f.tell())+": Too many fields") 443 | 444 | fieldTypes = [] 445 | nameLength = 0 446 | for i in range( nFields ): 447 | nameLength = self._readIntValue() 448 | if ( debugLevel > 9 ): 449 | print("{}th nameLength = {}".format(i, nameLength)) 450 | fieldType = self._readIntValue() 451 | fieldTypes.append( fieldType ) 452 | 453 | return fieldTypes 454 | 455 | def _readStructData(self, structTypes): 456 | # reads struct data based on type info in structType 457 | for i in range( len(structTypes) ): 458 | encodedType = structTypes[i] 459 | etSize = self._encodedTypeSize(encodedType) 460 | 461 | if ( debugLevel > 5 ): 462 | print("Tag Type = " + str(encodedType) + ",", end=' ') 463 | print("Tag Size = " + str(etSize)) 464 | 465 | # get data 466 | self._readNativeData(encodedType, etSize) 467 | 468 | return 1 469 | 470 | def _storeTag(self, tagName, tagValue): 471 | # store Tags as list and dict 472 | # NB: all tag values (and names) stored as unicode objects; 473 | # => can then be easily converted to any encoding 474 | if ( debugLevel == 1 ): 475 | print(" - storing Tag:") 476 | print(" -- name: ", tagName) 477 | print(" -- value: ", tagValue, type(tagValue)) 478 | # - convert tag value to unicode if not already unicode object 479 | self._storedTags.append( tagName + " = " + unicode_str(tagValue) ) 480 | self._tagDict[tagName] = unicode_str(tagValue) 481 | 482 | ### END utility functions ### 483 | 484 | def __init__(self, filename, debug=0): 485 | """DM3 object: parses DM3 file.""" 486 | 487 | ## initialize variables ## 488 | self._debug = debug 489 | self._outputcharset = DEFAULTCHARSET 490 | self._filename = filename 491 | self._chosenImage = 1 492 | # - track currently read group 493 | self._curGroupLevel = -1 494 | self._curGroupAtLevelX = [ 0 for x in range(MAXDEPTH) ] 495 | self._curGroupNameAtLevelX = [ '' for x in range(MAXDEPTH) ] 496 | # - track current tag 497 | self._curTagAtLevelX = [ '' for x in range(MAXDEPTH) ] 498 | self._curTagName = '' 499 | # - open file for reading 500 | self._f = open( self._filename, 'rb' ) 501 | # - create Tags repositories 502 | self._storedTags = [] 503 | self._tagDict = {} 504 | 505 | ## parse header 506 | isDM3,isDM4 = (False, False) 507 | # get version 508 | fileVersion = readLong(self._f) 509 | if (fileVersion == 3): 510 | isDM3 = True 511 | elif (fileVersion == 4): 512 | isDM4 = True 513 | # get size of root tag directory, check consistency 514 | fileSize = os.path.getsize(self._filename) 515 | sizeOK = True 516 | if isDM3: 517 | rootLen = readLong(self._f) 518 | if (rootLen != fileSize - 16): 519 | sizeOK = False 520 | elif isDM4: 521 | rootLen = readLongLong(self._f) 522 | if (rootLen != fileSize - 24): 523 | sizeOK = False 524 | # get byte-ordering 525 | lE = readLong(self._f) 526 | littleEndian = (lE == 1) 527 | if not littleEndian: 528 | isDM3,isDM4 = (False, False) 529 | 530 | # raise Exception if not DM3 or DM4 531 | if not (isDM3 or isDM4): 532 | raise Exception("'%s' does not appear to be a DM3/DM4 file." 533 | % os.path.split(self._filename)[1]) 534 | elif self._debug > 0: 535 | print("'%s' appears to be a DM%s file" % (self._filename, fileVersion)) 536 | 537 | if ( debugLevel > 5 or self._debug > 1): 538 | print("Header info. found:") 539 | print("- file version:", fileVersion) 540 | print("- byte order:", lE) 541 | print("- root tag dir. size:", rootLen, "bytes") 542 | print("- file size:", fileSize, "bytes") 543 | if not sizeOK: 544 | msg = "Warning: file size and root tag dir. size inconsistent" 545 | print("+ %s"%msg) 546 | 547 | self._fileVersion = fileVersion 548 | 549 | # set name of root group (contains all data)... 550 | self._curGroupNameAtLevelX[0] = "root" 551 | # ... then read it 552 | self._readTagGroup() 553 | if self._debug > 0: 554 | print("-- %s Tags read --" % len(self._storedTags)) 555 | 556 | # fetch image characteristics 557 | tag_root = 'root.ImageList.1' 558 | self._data_type = int( self.tags["%s.ImageData.DataType" % tag_root] ) 559 | self._im_width = int( self.tags["%s.ImageData.Dimensions.0" % tag_root] ) 560 | self._im_height = int( self.tags["%s.ImageData.Dimensions.1" % tag_root] ) 561 | try: 562 | self._im_depth = int( self.tags['root.ImageList.1.ImageData.Dimensions.2'] ) 563 | except KeyError: 564 | self._im_depth = 1 565 | 566 | if self._debug > 0: 567 | print("Notice: image size: %sx%s px" % (self._im_width, self._im_height)) 568 | if self._im_depth>1: 569 | print("Notice: %s image stack" % (self._im_depth)) 570 | 571 | @property 572 | def file_version(self): 573 | """Returns file format version (i.e., 3 or 4).""" 574 | return self._fileVersion 575 | 576 | @property 577 | def data_type(self): 578 | """Returns image DataType.""" 579 | return self._data_type 580 | 581 | @property 582 | def data_type_str(self): 583 | """Returns image DataType string.""" 584 | return dataTypes[self._data_type] 585 | 586 | @property 587 | def width(self): 588 | """Returns image width (px).""" 589 | return self._im_width 590 | 591 | @property 592 | def height(self): 593 | """Returns image height (px).""" 594 | return self._im_height 595 | 596 | @property 597 | def depth(self): 598 | """Returns image depth (i.e. number of images in stack).""" 599 | return self._im_depth 600 | 601 | @property 602 | def size(self): 603 | """Returns image size (width,height[,depth]).""" 604 | if self._im_depth > 1: 605 | return (self._im_width, self._im_height, self._im_depth) 606 | else: 607 | return (self._im_width, self._im_height) 608 | 609 | @property 610 | def outputcharset(self): 611 | """Returns Tag dump/output charset.""" 612 | return self._outputcharset 613 | 614 | @outputcharset.setter 615 | def outputcharset(self, value): 616 | """Set Tag dump/output charset.""" 617 | self._outputcharset = value 618 | 619 | @property 620 | def filename(self): 621 | """Returns full file path.""" 622 | return self._filename 623 | 624 | @property 625 | def tags(self): 626 | """Returns all image Tags.""" 627 | return self._tagDict 628 | 629 | def dumpTags(self, dump_dir='/tmp'): 630 | """Dumps image Tags in a txt file.""" 631 | dump_file = os.path.join(dump_dir, 632 | os.path.split(self._filename)[1] 633 | + ".tagdump.txt") 634 | try: 635 | dumpf = open( dump_file, 'w' ) 636 | except: 637 | print("Warning: cannot generate dump file.") 638 | else: 639 | for tag in self._storedTags: 640 | dumpf.write( "{}\n".format(tag.encode(self._outputcharset))) 641 | dumpf.close 642 | 643 | @property 644 | def info(self): 645 | """Extracts useful experiment info from DM3 file.""" 646 | # define useful information 647 | tag_root = 'root.ImageList.1.ImageTags' 648 | info_ = { 649 | 'gms_v': "GMS Version.Created", 650 | 'gms_v_': "GMS Version.Saved", 651 | 'device': "Acquisition.Device.Name", 652 | 'acq_date': "DataBar.Acquisition Date", 653 | 'acq_time': "DataBar.Acquisition Time", 654 | 'binning': "DataBar.Binning", 655 | 'hv': "Microscope Info.Voltage", 656 | 'hv_f': "Microscope Info.Formatted Voltage", 657 | 'mag': "Microscope Info.Indicated Magnification", 658 | 'mag_f': "Microscope Info.Formatted Indicated Mag", 659 | 'mode': "Microscope Info.Operation Mode", 660 | 'micro': "Session Info.Microscope", 661 | 'operator': "Session Info.Operator", 662 | 'specimen': "Session Info.Specimen", 663 | 'name_old': "Microscope Info.Name", 664 | 'micro_old': "Microscope Info.Microscope", 665 | 'operator_old': "Microscope Info.Operator", 666 | 'specimen_old': "Microscope Info.Specimen", 667 | # 'image_notes': "root.DocumentObjectList.10.Text' # = Image Notes 668 | } 669 | # get experiment information 670 | infoDict = {} 671 | for key in info_.keys(): 672 | tag_name = "%s.%s" % (tag_root, info_[key]) 673 | if tag_name in self.tags: 674 | # tags supplied as Python unicode str; convert to chosen charset 675 | # (typically latin-1 or utf-8) 676 | infoDict[key] = self.tags[tag_name].encode(self._outputcharset) 677 | # return experiment information 678 | return infoDict 679 | 680 | @property 681 | def imagedata(self): 682 | """Extracts image data as numpy.array""" 683 | 684 | # numpy dtype strings associated to the various image dataTypes 685 | dT_str = { 686 | 1: ' 0: 706 | print("Notice: image data in %s starts at %s" % ( 707 | os.path.split(self._filename)[1], hex(data_offset) 708 | )) 709 | 710 | # check if image DataType is implemented, then read 711 | if data_type in dT_str: 712 | np_dt = numpy.dtype( dT_str[data_type] ) 713 | if self._debug > 0: 714 | print("Notice: image data type: %s ('%s'), read as %s" % ( 715 | data_type, dataTypes[data_type], np_dt 716 | )) 717 | self._f.seek( data_offset ) 718 | # - fetch image data 719 | rawdata = self._f.read(data_size) 720 | # - convert raw to numpy array w/ correct dtype 721 | ima = numpy.fromstring(rawdata, dtype=np_dt) 722 | # - reshape to matrix or stack 723 | if im_depth > 1: 724 | ima = ima.reshape(im_depth, im_height, im_width) 725 | else: 726 | ima = ima.reshape(im_height, im_width) 727 | else: 728 | raise Exception( 729 | "Cannot extract image data from %s: unimplemented DataType (%s:%s)." % 730 | (os.path.split(self._filename)[1], data_type, dataTypes[data_type]) 731 | ) 732 | 733 | # if image dataType is BINARY, binarize image 734 | # (i.e., px_value>0 is True) 735 | if data_type == 14: 736 | ima[ima>0] = 1 737 | 738 | return ima 739 | 740 | 741 | @property 742 | def Image(self): 743 | """Returns image data as PIL Image""" 744 | 745 | # define PIL Image mode for the various (supported) image dataTypes, 746 | # among: 747 | # - '1': 1-bit pixels, black and white, stored as 8-bit pixels 748 | # - 'L': 8-bit pixels, gray levels 749 | # - 'I': 32-bit integer pixels 750 | # - 'F': 32-bit floating point pixels 751 | dT_modes = { 752 | 1: 'I', # 16-bit LE signed integer 753 | 2: 'F', # 32-bit LE floating point 754 | 6: 'L', # 8-bit unsigned integer 755 | 7: 'I', # 32-bit LE signed integer 756 | 9: 'I', # 8-bit signed integer 757 | 10: 'I', # 16-bit LE unsigned integer 758 | 11: 'I', # 32-bit LE unsigned integer 759 | 14: 'L', # "binary" 760 | } 761 | 762 | # define loaded array dtype if has to be fixed to match Image mode 763 | dT_newdtypes = { 764 | 1: 'int32', # 16-bit LE integer to 32-bit int 765 | 2: 'float32', # 32-bit LE float to 32-bit float 766 | 9: 'int32', # 8-bit signed integer to 32-bit int 767 | 10: 'int32', # 16-bit LE u. integer to 32-bit int 768 | } 769 | 770 | # get relevant Tags 771 | data_type = self._data_type 772 | im_width = self._im_width 773 | im_height = self._im_height 774 | im_depth = self._im_depth 775 | 776 | # fetch image data array 777 | ima = self.imagedata 778 | # assign Image mode 779 | mode_ = dT_modes[data_type] 780 | 781 | # reshape array if image stack 782 | if im_depth > 1: 783 | ima = ima.reshape(im_height*im_depth, im_width) 784 | 785 | # load image data array into Image object (recast array if necessary) 786 | if data_type in dT_newdtypes: 787 | im = Image.fromarray(ima.astype(dT_newdtypes[data_type]),mode_) 788 | else: 789 | im = Image.fromarray(ima,mode_) 790 | 791 | return im 792 | 793 | 794 | @property 795 | def contrastlimits(self): 796 | """Returns display range (cuts).""" 797 | tag_root = 'root.DocumentObjectList.0' 798 | low = int(float(self.tags["%s.ImageDisplayInfo.LowLimit" % tag_root])) 799 | high = int(float(self.tags["%s.ImageDisplayInfo.HighLimit" % tag_root])) 800 | cuts = (low, high) 801 | return cuts 802 | 803 | @property 804 | def cuts(self): 805 | """Returns display range (cuts).""" 806 | return self.contrastlimits 807 | 808 | @property 809 | def pxsize(self): 810 | """Returns pixel size and unit.""" 811 | tag_root = 'root.ImageList.1' 812 | pixel_size = float( 813 | self.tags["%s.ImageData.Calibrations.Dimension.0.Scale" % tag_root]) 814 | unit = self.tags["%s.ImageData.Calibrations.Dimension.0.Units" % 815 | tag_root] 816 | if unit == u'\xb5m': 817 | unit = 'micron' 818 | else: 819 | unit = unit.encode('ascii') 820 | if self._debug > 0: 821 | print("pixel size = %s %s" % (pixel_size, unit)) 822 | return (pixel_size, unit) 823 | 824 | 825 | @property 826 | def tnImage(self): 827 | """Returns thumbnail as PIL Image.""" 828 | # get thumbnail 829 | tag_root = 'root.ImageList.0' 830 | tn_size = int( self.tags["%s.ImageData.Data.Size" % tag_root] ) 831 | tn_offset = int( self.tags["%s.ImageData.Data.Offset" % tag_root] ) 832 | tn_width = int( self.tags["%s.ImageData.Dimensions.0" % tag_root] ) 833 | tn_height = int( self.tags["%s.ImageData.Dimensions.1" % tag_root] ) 834 | 835 | if self._debug > 0: 836 | print("Notice: tn data in %s starts at %s" % ( 837 | os.path.split(self._filename)[1], hex(tn_offset) 838 | )) 839 | print("Notice: tn size: %sx%s px" % (tn_width, tn_height)) 840 | 841 | if (tn_width*tn_height*4) != tn_size: 842 | raise Exception("Cannot extract thumbnail from %s" 843 | % os.path.split(self._filename)[1]) 844 | else: 845 | self._f.seek( tn_offset ) 846 | rawdata = self._f.read(tn_size) 847 | # - read as 32-bit LE unsigned integer 848 | tn = Image.frombytes( 'F', (tn_width, tn_height), rawdata, 849 | 'raw', 'F;32' ) 850 | # - rescale and convert px data 851 | tn = tn.point(lambda x: x * (1./65536) + 0) 852 | tn = tn.convert('L') 853 | # - return image 854 | return tn 855 | 856 | @property 857 | def thumbnaildata(self): 858 | """Fetch thumbnail image data as numpy.array""" 859 | 860 | # get useful thumbnail Tags 861 | tag_root = 'root.ImageList.0' 862 | tn_size = int( self.tags["%s.ImageData.Data.Size" % tag_root] ) 863 | tn_offset = int( self.tags["%s.ImageData.Data.Offset" % tag_root] ) 864 | tn_width = int( self.tags["%s.ImageData.Dimensions.0" % tag_root] ) 865 | tn_height = int( self.tags["%s.ImageData.Dimensions.1" % tag_root] ) 866 | 867 | if self._debug > 0: 868 | print("Notice: tn data in %s starts at %s" % ( 869 | os.path.split(self._filename)[1], hex(tn_offset) 870 | )) 871 | print("Notice: tn size: %sx%s px" % (tn_width, tn_height)) 872 | 873 | # get thumbnail data 874 | if (tn_width*tn_height*4) == tn_size: 875 | self._f.seek(tn_offset) 876 | rawtndata = self._f.read(tn_size) 877 | print('## rawdata:', len(rawtndata)) 878 | # - read as 32-bit LE unsigned integer 879 | np_dt_tn = numpy.dtype(' 0: 907 | print("Thumbnail saved as '%s'." % tn_path) 908 | except: 909 | print("Warning: could not save thumbnail.") 910 | 911 | 912 | ## MAIN ## 913 | if __name__ == '__main__': 914 | print("dm3_lib %s" % VERSION) 915 | 916 | --------------------------------------------------------------------------------