6

When I run my code from Pyinstaller the tiff reader works fine. After freezing using Pyinstaller I get the following warning:

enter image description here

UserWarning: ImportError: No module named '_tifffile'. Loading of some compressed images will be very slow. Tifffile.c can be obtained at http://www.lfd.uci.edu/~gohlke

And sure enough, a tiff file that used to take seconds to load into a numpy array may now take minutes.

Here is a simplified form of my code to focus on the problem. If you load an example tiff like this one it should load fast without problems.

If you use C:\Python35\python.exe C:\Python35\Scripts\pyinstaller.exe --additional-hooks-dir=. --clean --win-private-assemblies tiffile_problems.py you should get a functional .exe with the above error message when you run it. When you try to load the same tiff it now takes much longer.

tiffile_problems.py

#!/usr/bin/env python3

import os
import sys
import traceback
import numpy as np
import matplotlib.pyplot as plt

from PyQt4.QtGui import *
from PyQt4.QtCore import *

sys.path.append('..')

from MBE_for_SO.util import fileloader, fileconverter

class NotConvertedError(Exception):
  pass

class FileAlreadyInProjectError(Exception):
  def __init__(self, filename):
    self.filename = filename

class Widget(QWidget):
  def __init__(self, project, parent=None):
    super(Widget, self).__init__(parent)

    if not project:
        self.setup_ui()
        return

  def setup_ui(self):
    vbox = QVBoxLayout()

    ## Related to importing Raws
    self.setWindowTitle('Import Raw File')

    #vbox.addWidget(QLabel('Set the size all data are to be rescaled to'))

    grid = QGridLayout()

    vbox.addLayout(grid)
    vbox.addStretch()

    self.setLayout(vbox)
    self.resize(400, 220)

    self.listview = QListView()
    self.listview.setStyleSheet('QListView::item { height: 26px; }')
    self.listview.setSelectionMode(QAbstractItemView.NoSelection)
    vbox.addWidget(self.listview)

    hbox = QVBoxLayout()
    pb = QPushButton('New Video')
    pb.clicked.connect(self.new_video)
    hbox.addWidget(pb)

    vbox.addLayout(hbox)
    vbox.addStretch()
    self.setLayout(vbox)


  def convert_tif(self, filename):
    path = os.path.splitext(os.path.basename(filename))[0] + '.npy'
    #path = os.path.join(self.project.path, path)

    progress = QProgressDialog('Converting tif to npy...', 'Abort', 0, 100, self)
    progress.setAutoClose(True)
    progress.setMinimumDuration(0)
    progress.setValue(0)

    def callback(value):
      progress.setValue(int(value * 100))
      QApplication.processEvents()

    try:
      fileconverter.tif2npy(filename, path, callback)
      print('Tifffile saved to wherever this script is')
    except:
      # qtutil.critical('Converting tiff to npy failed.')
      progress.close()
    return path

  def to_npy(self, filename):
    if filename.endswith('.raw'):
      print('No raws allowed')
      #filename = self.convert_raw(filename)
    elif filename.endswith('.tif'):
      filename = self.convert_tif(filename)
    else:
      raise fileloader.UnknownFileFormatError()
    return filename

  def import_file(self, filename):
    if not filename.endswith('.npy'):
      new_filename = self.to_npy(filename)
      if not new_filename:
        raise NotConvertedError()
      else:
        filename = new_filename

    return filename

  def import_files(self, filenames):
    for filename in filenames:
      try:
        filename = self.import_file(filename)
      except NotConvertedError:
        # qtutil.warning('Skipping file \'{}\' since not converted.'.format(filename))
        print('Skipping file \'{}\' since not converted.'.format(filename))
      except FileAlreadyInProjectError as e:
        # qtutil.warning('Skipping file \'{}\' since already in project.'.format(e.filename))
        print('Skipping file \'{}\' since already in project.'.format(e.filename))
      except:
        # qtutil.critical('Import of \'{}\' failed:\n'.format(filename) +\
        #   traceback.format_exc())
        print('Import of \'{}\' failed:\n'.format(filename) + traceback.format_exc())
      # else:
      #   self.listview.model().appendRow(QStandardItem(filename))

  def new_video(self):
    filenames = QFileDialog.getOpenFileNames(
      self, 'Load images', QSettings().value('last_load_data_path'),
      'Video files (*.npy *.tif *.raw)')
    if not filenames:
      return
    QSettings().setValue('last_load_data_path', os.path.dirname(filenames[0]))
    self.import_files(filenames)

class MyPlugin:
  def __init__(self, project):
    self.name = 'Import video files'
    self.widget = Widget(project)

  def run(self):
    pass

if __name__ == '__main__':
  app = QApplication(sys.argv)
  app.aboutToQuit.connect(app.deleteLater)
  w = QMainWindow()
  w.setCentralWidget(Widget(None))
  w.show()
  app.exec_()
  sys.exit()

fileconverter.py

#!/usr/bin/env python3

import os
import numpy as np

import tifffile as tiff

class ConvertError(Exception):
  pass

def tif2npy(filename_from, filename_to, progress_callback):
  with tiff.TiffFile(filename_from) as tif:
    w, h = tif[0].shape
    shape = len(tif), w, h
    np.save(filename_to, np.empty(shape, tif[0].dtype))
    fp = np.load(filename_to, mmap_mode='r+')
    for i, page in enumerate(tif):
      progress_callback(i / float(shape[0]-1))
      fp[i] = page.asarray()

def raw2npy(filename_from, filename_to, dtype, width, height,
  num_channels, channel, progress_callback):
    fp = np.memmap(filename_from, dtype, 'r')
    frame_size = width * height * num_channels
    if len(fp) % frame_size:
      raise ConvertError()
    num_frames = len(fp) / frame_size
    fp = np.memmap(filename_from, dtype, 'r',
      shape=(num_frames, width, height, num_channels))
    np.save(filename_to, np.empty((num_frames, width, height), dtype))
    fp_to = np.load(filename_to, mmap_mode='r+')
    for i, frame in enumerate(fp):
      progress_callback(i / float(len(fp)-1))
      fp_to[i] = frame[:,:,channel-1]

fileloader.py

#!/usr/bin/env python3

import numpy as np

class UnknownFileFormatError(Exception):
  pass

def load_npy(filename):
  frames = np.load(filename)
  # frames[np.isnan(frames)] = 0
  return frames

def load_file(filename):
  if filename.endswith('.npy'):
    frames = load_npy(filename)
  else:
    raise UnknownFileFormatError()
  return frames

def load_reference_frame_npy(filename, offset):
  frames_mmap = np.load(filename, mmap_mode='c')
  if frames_mmap is None:
    return None
  frame = np.array(frames_mmap[offset])
  frame[np.isnan(frame)] = 0
  frame = frame.swapaxes(0, 1)
  if frame.ndim == 2:
    frame = frame[:, ::-1]
  elif frame.ndim == 3:
    frame = frame[:, ::-1, :]
  return frame

def load_reference_frame(filename, offset=0):
  if filename.endswith('.npy'):
    frame = load_reference_frame_npy(filename, offset)
  else:
    raise UnknownFileFormatError()
  return frame

Why? And how do I fix this? I've located tifffile.py, tifffile.cpython-35.pyc, tifffile.c and placed them all in the same directory as the .exe. No effect. _tifffile.cp35-win_amd64.pyd is created by pyinstaller and placed in the same dir as the .exe. I don't know what other options are available to me.

tifffile_problems.spec

# -*- mode: python -*-

block_cipher = None


a = Analysis(['tiffile_problems.py'],
             pathex=['C:\\Users\\Cornelis\\PycharmProjects\\tester\\MBE_for_SO'],
             binaries=None,
             datas=None,
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=True,
             cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=True,
          name='tiffile_problems',
          debug=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               name='tiffile_problems')

tiffile.spec when using C:\Python35\python.exe C:\Python35\Scripts\pyinstaller.exe --additional-hooks-dir=. --clean --win-private-assemblies --onefile tiffile_problems.py

# -*- mode: python -*-

block_cipher = None


a = Analysis(['tiffile_problems.py'],
             pathex=['C:\\Users\\Cornelis\\PycharmProjects\\tester\\MBE_for_SO'],
             binaries=None,
             datas=None,
             hiddenimports=[],
             hookspath=['.'],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=True,
             cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name='tiffile_problems',
          debug=False,
          strip=False,
          upx=True,
          console=True )
Frikster
  • 2,755
  • 5
  • 37
  • 71

4 Answers4

1

I actually seen this via upwork whilst I was just browsing around the net and decided to have a play around.

kazemakase was on the right track at the very start, when you run normally, the __package__ is None, and when its packaged __package__ is set to tifffile and the first condition is executed, and it becomes relative to the module tifffile.

if __package__:
    from . import _tifffile
else:
    import _tifffile

I just converted tifffile to a module manually, by creating in site-packages a tifffile folder, creating an empty __init__.py file in the new folder, placing tifffile.py, _tifffile.pyd from site-packages into the tifffile folder and changing the import statement, admittedly in a simple skeleton.

import tifffile.tifffile as tiff

If this helps across your whole project I don't know. And should note I used the wheel from http://www.lfd.uci.edu/~gohlke/pythonlibs/ to install originally to save the compilation step so your mileage may vary. I did the above initially on 2.7 but it also appears to work fine on 3.5 from some testing I was able to do. And didn't need to put anything in .spec files when I tested from the one originally generated.

muggy
  • 11
  • 3
  • So I do want to verify your answer. However I tried aconz2's simple solution first and it worked (also uninstalled and reinstalled tifffile from the gohike wheel). Now, when I try to revert back to the previous pyinstaller version (3.2) I cannot replicate the error! It is just fixed and is staying fixed! Very weird. This means I cannot test that your answer works as I'd rather not dig into whether I made some other incidental change since I'm just glad it works now. Either way, I'll give you a +1 since you were likely more right in explaining where the error came from than anyone else. – Frikster Jan 24 '17 at 20:39
  • No probs.. I suspect you can't retest anyway, as you have upgraded setuptools/packaging and thats where the underlying fix was. – muggy Jan 25 '17 at 23:46
1

I think muggy is right about he weirdness with __package__ causing the issue here. I haven't tracked down the exact reason for the fix, but this seems to be resolved using the latest update to pyinstaller. Check your version with:

→ pyinstaller --version 3.2.1

and upgrade with

→ pip3 install --upgrade pyinstaller

The update was only made on January 15, 2017 so this wouldn't have helped when you originally asked, but it sure helps now.

aconz2
  • 316
  • 3
  • 7
0

From inspecting the code, tifffile.py seems to be looking for a module called _tifffile, which presumably is the expected name of the compiled C extension:

try:
    if __package__:
        from . import _tifffile
    else:
        import _tifffile
except ImportError:
    warnings.warn(
        "ImportError: No module named '_tifffile'. "
        "Loading of some compressed images will be very slow. "
        "Tifffile.c can be obtained at http://www.lfd.uci.edu/~gohlke/")

The tifffile.cpython-35.pyc is just the bytecode generated from tiffile.py. You don't need to bother with that one.

The .c file alone won't help you, either. It needs to be compiled to to create a usable Python extension, which needs to be named something like _tiffile.cp35-win_amd64.pyd (can vary depending on your system and python version/installation) so it can be used by import _tifffile.

Compiling can be a daunting task if you have not done this before. If you feel you are up to it, the Python documentation can help you get started. You will need to have the same Compiler and settings that your Python version was compiled with.

However, there may be a simpler solution. If your code works fine before freezing, chances are you have the compiled extension correctly installed on your system. Pyinstaller probably misses it because it can find tifffile.py and is satisfied. Look for the correct .pyd file in your Python directories and see if you can modify the .spec file Pyinstaller created for your project, where you specify to include the .pyd file.

MB-F
  • 22,770
  • 4
  • 61
  • 116
  • I have no issues with my code before freezing. Tiff files load nice and fast. _tifffile.cp35-win_amd64.pyd is created by pyinstaller and I can find it in the directory that contains the .exe. I have read through your link but I'm not sure how to include _tifffile.cp35-win_amd64.pyd to the .spec. I have added my .spec to the question – Frikster Jan 19 '17 at 02:35
  • I did try to put datas= [ ('_tifffile.cp35-win_amd64.pyd' ) ] as a field in the spec in Analysis. But when I run pyinstaler I notice the spec reverts back to how it was. – Frikster Jan 19 '17 at 03:10
  • ok nvm I did manage to modify the spec to include the .pyd file. But it makes no difference as I should expect since the file is already placed in the dir that contains the .exe by default. This naturally makes me wonder what is going on? If the .c file is already compiled and it is right there why isn't it being imported with import _tifffile? – Frikster Jan 19 '17 at 04:49
  • I'm a bit guessing in the dark here: maybe pyinstaller does not get the module search path right or the working directory is not set correctly when starting the resulting `.exe`. Something else, I noticed `exclude_binaries=True` in your .spec. I don't know what this option is supposed to do, but it sounds suspicious. Try setting it to `False`, please. Something else you can try if that fails is to run pyinstaller with `--onefile` to bundle everything into the .exe. – MB-F Jan 19 '17 at 08:20
  • When I do that I get a new .exe that seems to be a copy in dist (one level above dist/tiffile_problems where the original is placed. That's not all. The .exe doesn't work now. It just closes. When I try to open it with cmd I get "Error loading Python DLL: C:\Users\Cornelis\AppData\Local\Temp\_MEI108522\python35.dll (error code 126)" Not sure why it has to look there for the dll given that it is right there in the same directory as the .exe. – Frikster Jan 19 '17 at 08:52
  • Also both .exe's that are made have the same problem – Frikster Jan 19 '17 at 08:52
  • Just to get that straight: `exclude_binaries=False` causes the resulting .exe to look for the python35.dll in that temporary directory? Have you tried `--onefile` with the original setting `exclude_binaries=True`? – MB-F Jan 19 '17 at 08:57
  • Yes on the "exclude_binaries=False" part. Or at least as far as I can tell. I've just tried --onefile. I get one .exe and the familiar tiffile problem. And interesting. The exclude_binaries=False field is gone so. I let it build a new .spec so cant be sure it was false. – Frikster Jan 19 '17 at 09:08
  • I'm going to copy-paste the .spec file before and after using --onefile and add it to the question. Just have to refreeze. Edit: ok, done. Looks like there are a bunch of differences that arise when all I do is add --onefile. Not sure if this teaches us anything of significance though – Frikster Jan 19 '17 at 09:10
  • Maybe the .spec thing is a bit misleading, don't worry too mach about it if it did not solve your problem. Another option you can try is `--hidden-import=_tifffile`. Now I have run out of ideas :) – MB-F Jan 19 '17 at 09:15
  • Well, there is another thing we can go on if it is out of place. _tifffile.py is in Users\Cornelis\.Pycharm2016.3\system\python_stubs\-1313743207\_tifffile.py. Also, I'm not sure the --hidden-import field did anything I'm afraid. – Frikster Jan 19 '17 at 09:25
  • I'm afraid the file in Pycharm's python_stubs directory is [unrelated](http://stackoverflow.com/a/24269651/3005167) to your problem. – MB-F Jan 19 '17 at 09:38
  • Do you think the problem may be in pyinstaller or tifffile? If so which one? Because pyinstaller des ultimately build a functional .exe. Just the tiffile thing doesn't work. But if I run my code then tifffile seems to work as intended so neither is to blame? Either way, thanx for the attempt – Frikster Jan 19 '17 at 09:52
  • There are two versions of tiffile: the slow python implementation and a fast C implementation (compiled as an extension module called `_tiffile`). The python implementation tries to be smart and use the extenension module if available. This works fine in your python installation. It seems pyinstaller fails to correctly bundle the extension module and so the slow python implementation is used. – MB-F Jan 19 '17 at 10:28
0

Installation:

->If using conda, conda install tifffile -c conda-forge

-> Otherwise, pip install tifffile