Skip to content

Commit d653f17

Browse files
committed
Merge pull request #304 from NaturalHistoryMuseum/feature/303-delay-load-binaries
Feature/303 delay load binaries
2 parents b936ddd + 3203474 commit d653f17

13 files changed

Lines changed: 140 additions & 71 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ This is an overview of major changes. Refer to the git repository for a full log
22

33
Version 0.1.30
44
-------------
5+
- Fixed #303 - Improve startup time
56

67
Version 0.1.29
78
-------------

build.sh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,29 @@ VERSION=`python inselect.py --version 2>&1 | sed 's/inselect.py //g'`
77
echo Building Inselect $VERSION
88

99
echo Clean
10+
rm -rf cover build dist
1011
find . -name "*pyc" -print0 | xargs -0 rm -rf
1112
find . -name __pycache__ -print0 | xargs -0 rm -rf
12-
rm -rf cover
1313

1414
echo Tests
1515
nosetests --with-coverage --cover-html --cover-inclusive --cover-erase --cover-tests --cover-package=inselect inselect
1616

17+
echo Report startup time and check for non-essential binary imports
18+
mkdir build
19+
time python -v inselect.py --quit &> build/startup_log
20+
for module in cv2 numpy pydmtx scipy sklearn zbar; do
21+
if grep -q $module build/startup_log; then
22+
echo Non-essential binary $module imported on startup
23+
exit 1
24+
fi
25+
done
26+
1727
echo Source build
18-
rm -rf dist
1928
./setup.py sdist
2029
mv dist/inselect-$VERSION.tar.gz .
2130

2231
if [[ "$OSTYPE" == "darwin"* ]]; then
2332
# Clean existing build files
24-
rm -rf build dist
2533
pyinstaller --clean inselect.spec
2634

2735
for script in export_metadata ingest read_barcodes save_crops; do

inselect/app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ def main(args):
2727
help='Use LOCALE; intended for testing purposes only')
2828
parser.add_argument('-v', '--version', action='version',
2929
version='%(prog)s ' + inselect.__version__)
30+
parser.add_argument('-q', '--quit', action='store_true',
31+
help='Exit immediately after showing the main window')
3032
parsed = parser.parse_args(args[1:])
3133

3234
# TODO LH A command-line switch to clear all QSettings
@@ -63,4 +65,7 @@ def main(args):
6365
if parsed.file:
6466
window.open_file(parsed.file)
6567

66-
sys.exit(app.exec_())
68+
if parsed.quit:
69+
sys.exit(0)
70+
else:
71+
sys.exit(app.exec_())

inselect/gui/about.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,24 @@
22
"""
33
import platform
44

5-
import cv2
65
import humanize
7-
import numpy as np
86
import psutil
97
import PySide
108
import PySide.QtCore
119

1210
from PySide import QtGui
1311
from PySide.QtGui import QMessageBox
1412

13+
# Warning: lazy load of cv2 and numpy via local imports
14+
1515

1616
def _environment():
1717
"""Returns a formatted string containing version numbers of important
1818
dependencies
1919
"""
20+
import cv2
21+
import numpy as np
22+
2023
# Bit depth of interpreter
2124
python_bit_depth = platform.architecture()[0]
2225
if '32bit' == python_bit_depth:

inselect/gui/plugins/barcode.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@
99
from .barcode_dialog import BarcodeDialog
1010
from .barcode_settings import load_engine
1111

12-
try:
13-
from gouda.strategies.roi.roi import roi
14-
from gouda.strategies.resize import resize
12+
import inselect.lib.utils
1513

16-
import inselect.lib.utils
17-
import gouda.util
18-
except ImportError:
19-
roi = resize = None
14+
# Warning: lazy load of gouda via local imports
2015

2116

2217
class BarcodePlugin(Plugin):
@@ -27,7 +22,10 @@ class BarcodePlugin(Plugin):
2722

2823
def __init__(self, document, parent):
2924
super(BarcodePlugin, self).__init__()
30-
if not roi and not resize:
25+
try:
26+
import gouda.strategies.roi.roi # noqa
27+
import gouda.strategies.resize # noqa
28+
except ImportError:
3129
raise InselectError('Barcode decoding is not available')
3230
else:
3331
self.document = document
@@ -52,6 +50,7 @@ def __call__(self, progress):
5250

5351
engine = load_engine()
5452

53+
import gouda.util
5554
gouda.util.DEBUG_PRINT = inselect.lib.utils.DEBUG_PRINT
5655

5756
progress('Loading full-res image')
@@ -75,6 +74,8 @@ def __call__(self, progress):
7574
debug_print('BarcodePlugin.__call__ exiting. [{0}] boxes'.format(len(items)))
7675

7776
def _decode_barcodes(self, engine, crop, progress):
77+
from gouda.strategies.roi.roi import roi
78+
from gouda.strategies.resize import resize
7879
for strategy in (resize, roi):
7980
# TODO LH Must be able to cancel within call to strategy
8081
progress()

inselect/gui/plugins/barcode_settings.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,37 @@
33
from inselect.lib.inselect_error import InselectError
44
from inselect.lib.utils import debug_print
55

6-
try:
7-
import gouda
8-
from gouda.engines import InliteEngine, LibDMTXEngine, ZbarEngine
9-
from gouda.strategies.roi.roi import roi
10-
from gouda.strategies.resize import resize
11-
except ImportError:
12-
gouda = InliteEngine = LibDMTXEngine = ZbarEngine = roi = resize = None
6+
# Warning: lazy load of gouda via local imports
137

148

159
def inlite_available():
1610
"Returns True if the Inlite engine is available"
17-
return InliteEngine is not None and InliteEngine.available()
11+
try:
12+
from gouda.engines import InliteEngine
13+
except ImportError:
14+
return False
15+
else:
16+
return InliteEngine.available()
1817

1918

2019
def libdmtx_available():
2120
"Returns True if the libdmtx engine is available"
22-
return LibDMTXEngine is not None and LibDMTXEngine.available()
21+
try:
22+
from gouda.engines import LibDMTXEngine
23+
except ImportError:
24+
return False
25+
else:
26+
return LibDMTXEngine.available()
2327

2428

2529
def zbar_available():
2630
"Returns True if the zbar engine is available"
27-
return ZbarEngine is not None and ZbarEngine.available()
31+
try:
32+
from gouda.engines import ZbarEngine
33+
except ImportError:
34+
return False
35+
else:
36+
return ZbarEngine.available()
2837

2938

3039
def current_settings():
@@ -57,7 +66,11 @@ def update_settings(new_settings):
5766
def load_engine():
5867
"""Returns an instance of the user's choice of barcode reading engine
5968
"""
60-
if gouda:
69+
try:
70+
from gouda.engines import InliteEngine, LibDMTXEngine, ZbarEngine
71+
except ImportError:
72+
raise InselectError('Barcode decoding is not available')
73+
else:
6174
settings = current_settings()
6275
engine = settings['engine']
6376
if 'libdmtx' == engine:
@@ -68,5 +81,3 @@ def load_engine():
6881
return InliteEngine(settings['inlite-format'])
6982
else:
7083
raise ValueError('Unrecognised barcode reader [{0}]'.format(engine))
71-
else:
72-
raise InselectError('Barcode decoding is not available')

inselect/gui/utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@
55
from io import BytesIO
66
from itertools import groupby
77

8-
import cv2
9-
import numpy as np
10-
118
from PySide.QtGui import (QImage, QItemSelection, QItemSelectionModel,
129
QMessageBox, QWidget)
1310

1411
from copy_box import copy_details_box
1512

13+
# Warning: lazy load of cv2 and numpy via local imports
14+
1615

1716
def qimage_of_bgr(bgr):
1817
""" A QImage representation of a BGR numpy array
1918
"""
19+
import cv2
20+
import numpy as np
21+
2022
bgr = cv2.cvtColor(bgr.astype('uint8'), cv2.COLOR_BGR2RGB)
2123
bgr = np.ascontiguousarray(bgr)
2224
qt_image = QImage(bgr.data,

inselect/lib/document.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from itertools import chain
88
from pathlib import Path
99

10-
import cv2
11-
1210
from .image import InselectImage
1311
from .inselect_error import InselectError
1412
from .utils import debug_print, user_name
1513
from .rect import Rect
1614

15+
# Warning: lazy load of cv2 via local imports
16+
1717

1818
class InselectDocument(object):
1919
"""An Inselect document.
@@ -309,6 +309,9 @@ def save_crops_from_image(self, image, crop_paths, progress=None):
309309

310310
def _create_and_load_thumbnail(self, width):
311311
"Create thumbnail image"
312+
# TODO LH Delegate job of creating thumbnail to InselectImage class
313+
import cv2
314+
312315
p = self.thumbnail_path_of_scanned(self._scanned.path)
313316

314317
msg = u'Creating [{0}] with width of [{1}] pixels'

inselect/lib/image.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
from itertools import izip, count, chain, repeat
44
from pathlib import Path
55

6-
import cv2
7-
import numpy as np
8-
96
from PIL import Image
107

118
from inselect.lib.inselect_error import InselectError
129
from inselect.lib.utils import debug_print
1310
from inselect.lib.rect import Rect
1411

12+
# Warning: lazy load of cv2 and numpy via local imports
13+
1514

1615
class InselectImage(object):
1716
"""Simple representation of an inselect image
@@ -54,6 +53,7 @@ def array(self):
5453
"""Lazy-load np.array of the colour image array, with channels stored in
5554
order B G R
5655
"""
56+
import cv2
5757
if self._array is None:
5858
self.assert_is_file()
5959
p = str(self._path)
@@ -86,6 +86,8 @@ def crops(self, normalised, rotation=None):
8686
Rotation should be None, an int or an iterable. If not None, crops will
8787
be rotated by that many clockwise degrees.
8888
"""
89+
import cv2
90+
import numpy as np
8991

9092
if not rotation:
9193
rotation = repeat(0)
@@ -139,6 +141,7 @@ def save_crops(self, normalised, paths, rotation=None, progress=None):
139141
"""
140142
# TODO Copy EXIF tags?
141143
# TODO Make read-only?
144+
import cv2
142145
self.assert_is_file()
143146
for index, crop, path in izip(count(), self.crops(normalised, rotation), paths):
144147
if progress:

inselect/lib/segment.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from random import randint
22

3-
import cv2
4-
5-
import numpy as np
6-
73
from .utils import debug_print
84

5+
# Warning: lazy load of cv2 and numpy via local imports
6+
7+
# TODO LH Decide on use of skimage's watershed
98
# Breaks pyinstaller build
109
# from skimage.morphology import watershed
1110
USE_OPENCV_WATERSHED = True
@@ -31,6 +30,8 @@ def _right_sized(contour, image, container_filter=True, size_filter=True):
3130
result : boolean
3231
Object is of correct sizing.
3332
"""
33+
import cv2
34+
3435
image_size = image.shape
3536
x, y, w, h = cv2.boundingRect(contour)
3637
area = image_size[0] * image_size[1]
@@ -80,6 +81,8 @@ def _process_contours(image, contours, hierarchy, callback, index=0,
8081
size_filter : boolean
8182
Filters large objects.
8283
"""
84+
import cv2
85+
8386
result = []
8487
while index >= 0:
8588
callback()
@@ -101,6 +104,8 @@ def _process_contours(image, contours, hierarchy, callback, index=0,
101104
# alternate process, may be useful if we abandon hierarchical contours later
102105
def _process_contours_iterate(image, contours, hierarchy, index=0,
103106
size_filter=True):
107+
import cv2
108+
104109
result = []
105110
for contour in contours:
106111
if right_sized(contour, image.shape, size_filter=size_filter):
@@ -124,6 +129,9 @@ def remove_lines(image):
124129
mask : (M, N) array
125130
Mask of image without lines.
126131
"""
132+
import cv2
133+
import numpy as np
134+
127135
gray = cv2.cvtColor(image, cv2.cv.CV_BGR2GRAY)
128136
v_edges = cv2.Sobel(gray, cv2.CV_32F, 1, 0, None, 1)
129137
h_edges = cv2.Sobel(gray, cv2.CV_32F, 0, 1, None, 1)
@@ -184,6 +192,9 @@ def segment_edges(image, window=None, threshold=12, lab_based=True,
184192
(rects, display) : list, (M, N, 3) array
185193
Region results and visualization image.
186194
"""
195+
import cv2
196+
import numpy as np
197+
187198
if not callback:
188199
def swallow(*args, **kwargs):
189200
pass
@@ -291,6 +302,9 @@ def swallow(*args, **kwargs):
291302

292303

293304
def segment_intensity(image, window=None):
305+
import cv2
306+
import numpy as np
307+
294308
if window:
295309
subimage = np.array(image)
296310
x, y, w, h = window
@@ -334,6 +348,9 @@ def segment_grabcut(image, window=None, seeds=[]):
334348
(rects, display) : list, (M, N, 3) array
335349
Region results and visualization image.
336350
"""
351+
import cv2
352+
import numpy as np
353+
337354
if window:
338355
subimage = np.array(image)
339356
x, y, w, h = window
@@ -419,6 +436,9 @@ def segment_watershed(image, window=None):
419436
(rects, display) : list, (M, N, 3) array
420437
Region results and visualization image.
421438
"""
439+
import cv2
440+
import numpy as np
441+
422442
if window:
423443
subimage = np.array(image)
424444
x, y, w, h = window
@@ -458,6 +478,9 @@ def segment_watershed(image, window=None):
458478

459479

460480
if __name__ == "__main__":
481+
import cv2
482+
import numpy as np
483+
461484
image = cv2.imread("../../data/drawer.jpg")
462485
scaled = 1.0
463486
image = cv2.resize(image, (int(image.shape[1] * scaled),

0 commit comments

Comments
 (0)