From 7eec2a7ddc5f512f4eda0251cf95b5ffbe1ad974 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Tue, 26 Apr 2016 22:11:43 +0100 Subject: [PATCH 01/22] [#212] loads of work on box sorting --- CHANGELOG.md | 1 + DevelopingOnMacOSX.md | 2 +- DevelopingOnWindows.md | 4 +- inselect/gui/main_window.py | 31 +++++ inselect/gui/plugins/segment.py | 13 +- inselect/gui/plugins/subsegment.py | 53 ++------ inselect/gui/sort_document_items.py | 37 ++++++ inselect/lib/segment.py | 50 -------- inselect/lib/segment_document.py | 115 ++++++++++++++++++ inselect/lib/sort_document_items.py | 47 +++++++ inselect/scripts/segment.py | 18 ++- inselect/tests/gui/test_action_state.py | 4 + inselect/tests/lib/test_document_export.py | 4 +- inselect/tests/lib/test_segment.py | 30 +++-- inselect/tests/lib/test_sort_boxes.py | 31 +++++ .../tests/test_data/test_segment.inselect | 2 + 16 files changed, 325 insertions(+), 117 deletions(-) create mode 100644 inselect/gui/sort_document_items.py create mode 100644 inselect/lib/segment_document.py create mode 100644 inselect/lib/sort_document_items.py create mode 100644 inselect/tests/lib/test_sort_boxes.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d95c5f..0f090cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Version 0.1.28 - Fixed #286 - Disable default template command when the default template selected - Fixed #284 - Document info view to contain links - Fixed #281 - Don't make images read-only +- Fixed #212 - Customized bounding box ordering - Fixed #204 - Improvements to subsegment dots - Fixed #203 - Resize handles should have constant size regardless of zoom diff --git a/DevelopingOnMacOSX.md b/DevelopingOnMacOSX.md index aea171a..da899d3 100644 --- a/DevelopingOnMacOSX.md +++ b/DevelopingOnMacOSX.md @@ -24,7 +24,7 @@ conda update --yes conda # Inselect env ``` -conda create --yes --name inselect pillow pyside +conda create --yes --name inselect pillow pyside scikit-learn source activate inselect ``` diff --git a/DevelopingOnWindows.md b/DevelopingOnWindows.md index 47c0d1f..49be4d1 100644 --- a/DevelopingOnWindows.md +++ b/DevelopingOnWindows.md @@ -25,7 +25,7 @@ follows: ``` conda update --yes conda -conda create --yes --name inselect pillow pyside pywin32 numpy +conda create --yes --name inselect pillow pyside pywin32 numpy scikit-learn activate inselect python -m pip install --upgrade pip pip install -r requirements.txt @@ -92,7 +92,7 @@ follows: ``` conda update --yes conda -conda create --yes --name inselect pillow pyside pywin32 numpy +conda create --yes --name inselect pillow pyside pywin32 numpy scikit-learn activate inselect python -m pip install --upgrade pip pip install -r requirements.txt diff --git a/inselect/gui/main_window.py b/inselect/gui/main_window.py index 4e0af48..a9e4421 100644 --- a/inselect/gui/main_window.py +++ b/inselect/gui/main_window.py @@ -32,6 +32,7 @@ from .plugins.subsegment import SubsegmentPlugin from .recent_documents import RecentDocuments from .roles import RotationRole +from .sort_document_items import sort_items_choice from .user_template_choice import user_template_choice from .utils import contiguous, report_to_user, qimage_of_bgr from .views.boxes import BoxesView, GraphicsItemView @@ -992,6 +993,15 @@ def _create_menu_actions(self): triggered=partial(self.rotate90, clockwise=False) ) + self.sort_by_rows_action = QAction( + "Sort by rows", self, checkable=True, + triggered=partial(self.sort_boxes, by_columns=False) + ) + self.sort_by_columns_action = QAction( + "Sort by columns", self, checkable=True, + triggered=partial(self.sort_boxes, by_columns=True) + ) + # Plugins # Plugin shortcuts start at F5 shortcut_offset = 5 @@ -1184,6 +1194,9 @@ def _create_menus(self): self._edit_menu.addAction(self.rotate_clockwise_action) self._edit_menu.addAction(self.rotate_counter_clockwise_action) self._edit_menu.addSeparator() + self._edit_menu.addAction(self.sort_by_rows_action) + self._edit_menu.addAction(self.sort_by_columns_action) + self._edit_menu.addSeparator() user_template_popup = self._edit_menu.addMenu('Metadata template') self.view_metadata.popup_button.inject_actions(user_template_popup) self._edit_menu.addSeparator() @@ -1345,6 +1358,22 @@ def copy_to_new_document(self): else: self.new_document(path, default_metadata_items=items) + @report_to_user + def sort_boxes(self, by_columns): + """Sorts boxes either by columns or by rows. + """ + if self.document: + # Sort boxes + self.model.to_document(self.document) + items = sort_items_choice().sort_items( + self.document.items, by_columns + ) + self.model.set_new_boxes(items) + else: + # Record the user's choice + sort_items_choice().sort_items([], by_columns) + self.sync_ui() + def _accept_drag_drop(self, event): """If event refers to a single file that can opened, returns the path. Returns None otherwise. @@ -1428,6 +1457,8 @@ def sync_ui(self): self.delete_action.setEnabled(has_selection) self.rotate_clockwise_action.setEnabled(has_selection) self.rotate_counter_clockwise_action.setEnabled(has_selection) + self.sort_by_rows_action.setChecked(not sort_items_choice().by_columns) + self.sort_by_columns_action.setChecked(sort_items_choice().by_columns) self.cookie_cutter_widget.sync_actions(document, has_rows) for action in self.plugin_actions: action.setEnabled(document) diff --git a/inselect/gui/plugins/segment.py b/inselect/gui/plugins/segment.py index 9564259..84a9714 100644 --- a/inselect/gui/plugins/segment.py +++ b/inselect/gui/plugins/segment.py @@ -1,8 +1,10 @@ from PySide.QtGui import QIcon, QMessageBox -from inselect.lib.segment import segment_document +from inselect.lib.segment_document import SegmentDocument from inselect.lib.utils import debug_print +from inselect.gui.sort_document_items import sort_items_choice + from .plugin import Plugin @@ -16,6 +18,7 @@ def __init__(self, document, parent): self.rects = self.display = None self.document = document self.parent = parent + self.sort_choice = sort_items_choice().by_columns @classmethod def icon(cls): @@ -34,8 +37,12 @@ def can_be_run(self): def __call__(self, progress): debug_print('SegmentPlugin.__call__') - doc, display = segment_document(self.document, callback=progress) + doc, display = SegmentDocument(self.sort_choice).segment( + self.document, callback=progress + ) self.items, self.display = doc.items, display - debug_print('SegmentPlugin.__call__ exiting. Found [{0}] boxes'.format(len(self.items))) + debug_print('SegmentPlugin.__call__ exiting. Found [{0}] boxes'.format( + len(self.items)) + ) diff --git a/inselect/gui/plugins/subsegment.py b/inselect/gui/plugins/subsegment.py index e8074b8..80e3f58 100644 --- a/inselect/gui/plugins/subsegment.py +++ b/inselect/gui/plugins/subsegment.py @@ -1,11 +1,10 @@ -import numpy as np - from PySide.QtGui import QIcon, QMessageBox -from inselect.lib.segment import segment_grabcut -from inselect.lib.rect import Rect +from inselect.lib.segment_document import SegmentDocument from inselect.lib.utils import debug_print +from inselect.gui.sort_document_items import sort_items_choice + from .plugin import Plugin @@ -19,13 +18,13 @@ def __init__(self, document, parent): self.rects = self.display = None self.document = document self.parent = parent + self.sort_choice = sort_items_choice().by_columns @classmethod def icon(cls): return QIcon(':/data/subsegment_icon.png') def can_be_run(self): - # TODO LH Fix this horrible, horrible, horrible, horrible, horrible hack selected = self.parent.view_object.selectedIndexes() items_of_indexes = self.parent.view_graphics_item.items_of_indexes item = items_of_indexes(selected).next() if 1 == len(selected) else None @@ -44,52 +43,16 @@ def can_be_run(self): def __call__(self, progress): debug_print('SubsegmentPlugin.__call__') - if self.document.thumbnail: - debug_print('Subsegment will work on thumbnail') - image = self.document.thumbnail - else: - debug_print('Segment will work on full-res scan') - image = self.document.scanned - - # Perform the subsegmentation - items = self.document.items - row = self.row - window = image.from_normalised([items[row]['rect']]).next() - # Points as a list of tuples, with coordinates relative to # the top-left of the sub-segmentation window seeds = [(p.x(), p.y()) for p in self.seeds] - rects, display = segment_grabcut(image.array, window, seeds) - - # Normalised Rects - rects = list(Rect(*map(lambda v: int(round(v)), rect[:4])) for rect in rects) - rects = image.to_normalised(rects) - - # Padding of one percent of height and width - rects = (r.padded(percent=1) for r in rects) - - # Constrain rects to be within image - rects = list(r.intersect(Rect(0.0, 0.0, 1.0, 1.0)) for r in rects) - - # Copy any existing metadata, rotation etc to the new items, update with - # new rects and replace the existing item - existing = items[row] - new_items = [None] * len(rects) - for index, rect in enumerate(rects): - new_items[index] = existing.copy() - new_items[index]['rect'] = rect - items[row:(1+row)] = new_items - - # Segmentation image - h, w = image.array.shape[:2] - display_image = np.zeros((h, w, 3), dtype=np.uint8) - - x, y, w, h = window - display_image[y:y+h, x:x+w] = display + items, display_image = SegmentDocument(self.sort_choice).subsegment( + self.document, self.row, seeds, callback=progress + ) self.items, self.display = items, display_image debug_print( - 'SegmentPlugin.__call__ exiting. Found [{0}] boxes'.format(len(rects)) + 'SegmentPlugin.__call__ exiting. Found [{0}] boxes'.format(len(items)) ) diff --git a/inselect/gui/sort_document_items.py b/inselect/gui/sort_document_items.py new file mode 100644 index 0000000..ab9b02b --- /dev/null +++ b/inselect/gui/sort_document_items.py @@ -0,0 +1,37 @@ +from PySide.QtCore import QSettings + +from inselect.lib.sort_document_items import sort_document_items + +# QSettings path +_PATH = 'sort_by_columns' + +# Global - set to instance of CookieCutterChoice in cookie_cutter_boxes +_SORT_DOCUMENT = None + + +def sort_items_choice(): + "Returns an instance of SortDocumentItems" + global _SORT_DOCUMENT + if not _SORT_DOCUMENT: + _SORT_DOCUMENT = SortDocumentItems() + return _SORT_DOCUMENT + + +class SortDocumentItems(object): + def __init__(self): + self._by_columns = QSettings().value(_PATH, False) + + @property + def by_columns(self): + """The user's preference for ordering by columns (True) or by rows + (False) + """ + return self._by_columns + + def sort_items(self, items, by_columns): + """Returns items sorted by columns (True) or by rows (False) or by the + user's most recent preference (None). + """ + self._by_columns = by_columns + QSettings().setValue(_PATH, by_columns) + return sort_document_items(items, by_columns) diff --git a/inselect/lib/segment.py b/inselect/lib/segment.py index 4248468..7c2d7e3 100644 --- a/inselect/lib/segment.py +++ b/inselect/lib/segment.py @@ -4,62 +4,12 @@ import numpy as np -from .rect import Rect from .utils import debug_print # Breaks pyinstaller build # from skimage.morphology import watershed USE_OPENCV_WATERSHED = True -SEGMENTATION_PREFERRED_WIDTH = 4096 - - -def segment_document(doc, resize=None, *args, **kwargs): - """Returns doc with items replaced by the result of calling segment_edges(). - The caller is responsible for saving doc. - """ - debug_print('Segmenting [{0}]'.format(doc)) - - if doc.thumbnail: - img = doc.thumbnail - debug_print('Will segment using thumbnail [{0}]'.format(img)) - else: - img = doc.scanned - debug_print('Will segment using full-res scan [{0}]'.format(img)) - - # Make smaller images larger - height, width = img.array.shape[:2] - if resize is None and width != SEGMENTATION_PREFERRED_WIDTH: - # Resize, maintaining aspect ratio - # segment_edges() expects a tuple (height, width) - factor = float(SEGMENTATION_PREFERRED_WIDTH) / width - resize = (int(height * factor), SEGMENTATION_PREFERRED_WIDTH) - - msg = 'Resizing [{0}] from [{1}] to preferred size of [{2}]' - debug_print(msg.format(doc, (height, width), resize)) - else: - # Images of the preferred size or larger do not need resizing - debug_print('Image is of the preferred size or larger') - resize = False - - rects, display_image = segment_edges(img.array, resize=resize, *args, **kwargs) - - # Normalised Rects - rects = list(Rect(*map(lambda v: int(round(v)), rect[:4])) for rect in rects) - rects = img.to_normalised(rects) - - # Padding of one percent of height and width - rects = (r.padded(percent=1) for r in rects) - - # Constrain rects to be within image - rects = (r.intersect(Rect(0.0, 0.0, 1.0, 1.0)) for r in rects) - - items = [{"fields": {}, 'rect': r, 'rotation': 0} for r in rects] - doc = doc.copy() # Deep copy to avoid altering argument - doc.set_items(items) - debug_print('Segmented [{0}]'.format(doc)) - return doc, display_image - def _right_sized(contour, image, container_filter=True, size_filter=True): """Checks if contour size and shape is that of an object of interest. diff --git a/inselect/lib/segment_document.py b/inselect/lib/segment_document.py new file mode 100644 index 0000000..d693ddd --- /dev/null +++ b/inselect/lib/segment_document.py @@ -0,0 +1,115 @@ +import numpy as np + +from .rect import Rect +from .segment import segment_edges, segment_grabcut +from .sort_document_items import sort_document_items +from .utils import debug_print + +SEGMENTATION_PREFERRED_WIDTH = 4096 + + +class SegmentDocument(object): + """Segments and sub-segments documents, applies padding to rects + and orders rects. + """ + def __init__(self, sort_by_columns=False): + self.sort_by_columns = sort_by_columns + + def segment(self, doc, resize=None, *args, **kwargs): + """Returns doc with items replaced by the result of calling segment_edges(). + The caller is responsible for saving doc. + """ + debug_print('Segmenting [{0}]'.format(doc)) + + if doc.thumbnail: + img = doc.thumbnail + debug_print('Will segment using thumbnail [{0}]'.format(img)) + else: + img = doc.scanned + debug_print('Will segment using full-res scan [{0}]'.format(img)) + + # Make smaller images larger + height, width = img.array.shape[:2] + if resize is None and width != SEGMENTATION_PREFERRED_WIDTH: + # Resize, maintaining aspect ratio + # segment_edges() expects a tuple (height, width) + factor = float(SEGMENTATION_PREFERRED_WIDTH) / width + resize = (int(height * factor), SEGMENTATION_PREFERRED_WIDTH) + + msg = 'Resizing [{0}] from [{1}] to preferred size of [{2}]' + debug_print(msg.format(doc, (height, width), resize)) + else: + # Images of the preferred size or larger do not need resizing + debug_print('Image is of the preferred size or larger') + resize = False + + rects, display_image = segment_edges( + img.array, resize=resize, *args, **kwargs + ) + + rects = self._post_process_rects(img, rects) + + # Create item dicts + items = [{"fields": {}, 'rect': r, 'rotation': 0} for r in rects] + + # Sort items by user's most recent preference + items = sort_document_items(items, self.sort_by_columns) + + doc = doc.copy() # Deep copy to avoid altering argument + doc.set_items(items) + + debug_print('Segmented [{0}]'.format(doc)) + + return doc, display_image + + def subsegment(self, doc, row, seeds, *args, **kwargs): + """seeds - a list of tuples (x, y) with coordinates relative to + the top-left of the sub-segmentation window + """ + if doc.thumbnail: + debug_print('Subsegment will work on thumbnail') + img = doc.thumbnail + else: + debug_print('Segment will work on full-res scan') + img = doc.scanned + + items = doc.items + window = next(img.from_normalised([items[row]['rect']])) + rects, display = segment_grabcut(img.array, window, seeds) + + rects = list(self._post_process_rects(img, rects)) + + # Copy any existing metadata, rotation etc to the new items, update with + # new rects and replace the existing item + existing = items[row] + new_items = [None] * len(rects) + for index, rect in enumerate(rects): + new_items[index] = existing.copy() + new_items[index]['rect'] = rect + + # Sort items by user's most recent preference + new_items = sort_document_items(new_items, self.sort_by_columns) + + items[row:(1+row)] = new_items + + # Segmentation image + h, w = img.array.shape[:2] + display_image = np.zeros((h, w, 3), dtype=np.uint8) + + x, y, w, h = window + display_image[y:y+h, x:x+w] = display + + return items, display_image + + def _post_process_rects(self, img, rects): + """Generator of instances of normalised Rect with padding applied + """ + # Normalised coords and construct instances of Rect + rects = list(Rect(*map(lambda v: int(round(v)), rect[:4])) for rect in rects) + rects = img.to_normalised(rects) + + # Apply padding of one percent of height and width + rects = (r.padded(percent=1) for r in rects) + + # Constrain rects to be within image + return (r.intersect(Rect(0.0, 0.0, 1.0, 1.0)) for r in rects) diff --git a/inselect/lib/sort_document_items.py b/inselect/lib/sort_document_items.py new file mode 100644 index 0000000..c58c4ff --- /dev/null +++ b/inselect/lib/sort_document_items.py @@ -0,0 +1,47 @@ +from itertools import izip +from operator import itemgetter + +import numpy as np + +from scipy.signal import argrelextrema +from sklearn.neighbors import KernelDensity + + +def _do_kde(values): + """Uses kernel denstity estimation to assign values to clusters using + minima. Returns a generator of ints that are bin numbers. + """ + # http://stackoverflow.com/a/35151947 + RESCALE = 100 + values = np.array([int(v * RESCALE) for v in values]).reshape(-1, 1) + kde = KernelDensity().fit(values) + + # Identify minima and use as break points + samples = np.linspace(0, RESCALE) + evaluations = kde.score_samples(samples.reshape(-1, 1)) + minima = argrelextrema(evaluations, np.less)[0] + + # The right-hand edges of bins + bins = np.append(samples[minima], RESCALE) + + # Cut data + return (v[0] for v in np.digitize(values, bins, right=True).tolist()) + + +def sort_document_items(items, by_columns): + """Returns items sorted either by columns or by rows + """ + if not items: + # Algorithm is not tolerant of empty values + return [] + else: + rects = [i['rect'] for i in items] + x_bins = _do_kde([r.centre.x for r in rects]) + y_bins = _do_kde([r.centre.y for r in rects]) + + if by_columns: + keys = izip(x_bins, y_bins, (r.left for r in rects)) + else: + keys = izip(y_bins, x_bins, (r.left for r in rects)) + items_and_keys = sorted(izip(items, keys), key=itemgetter(1)) + return [v[0] for v in items_and_keys] diff --git a/inselect/scripts/segment.py b/inselect/scripts/segment.py index 1b34022..4589bf2 100755 --- a/inselect/scripts/segment.py +++ b/inselect/scripts/segment.py @@ -16,22 +16,23 @@ import inselect.lib.utils from inselect.lib.document import InselectDocument -from inselect.lib.segment import segment_document +from inselect.lib.segment_document import SegmentDocument from inselect.lib.utils import debug_print # TODO Recursive option # TODO Option to resegment documents with existing boxes -def segment(dir): +def segment(dir, sort_by_columns): dir = Path(dir) + segment_doc = SegmentDocument(sort_by_columns) for p in dir.glob('*' + InselectDocument.EXTENSION): doc = InselectDocument.load(p) if not doc.items: print(u'Segmenting [{0}]'.format(p)) try: debug_print(u'Will segment [{0}]'.format(p)) - doc, display_image = segment_document(doc) + doc, display_image = segment_doc.segment(doc) del display_image # We don't use this doc.save() except KeyboardInterrupt: @@ -49,13 +50,18 @@ def main(args): parser = argparse.ArgumentParser(description='Segments Inselect documents') parser.add_argument("dir", help='Directory containing Inselect documents') parser.add_argument('--debug', action='store_true') - parser.add_argument('-v', '--version', action='version', - version='%(prog)s ' + inselect.__version__) + parser.add_argument( + '--sort-by-columns', action='store_true', default=False, + help='Sort boxes columns; default is to sort boxes by rows') + parser.add_argument( + '-v', '--version', action='version', + version='%(prog)s ' + inselect.__version__ + ) args = parser.parse_args(args) inselect.lib.utils.DEBUG_PRINT = args.debug - segment(args.dir) + segment(args.dir, args.sort_by_columns) if __name__ == '__main__': diff --git a/inselect/tests/gui/test_action_state.py b/inselect/tests/gui/test_action_state.py index afa01fc..1072ecf 100644 --- a/inselect/tests/gui/test_action_state.py +++ b/inselect/tests/gui/test_action_state.py @@ -29,6 +29,8 @@ def _test_no_document(self): self.assertFalse(w.previous_box_action.isEnabled()) self.assertFalse(w.rotate_clockwise_action.isEnabled()) self.assertFalse(w.rotate_counter_clockwise_action.isEnabled()) + self.assertTrue(w.sort_by_rows_action.isEnabled()) + self.assertTrue(w.sort_by_columns_action.isEnabled()) self.assertFalse(w.plugin_actions[0].isEnabled()) # View @@ -57,6 +59,8 @@ def _test_document_open(self): self.assertTrue(w.previous_box_action.isEnabled()) self.assertFalse(w.rotate_clockwise_action.isEnabled()) self.assertFalse(w.rotate_counter_clockwise_action.isEnabled()) + self.assertTrue(w.sort_by_rows_action.isEnabled()) + self.assertTrue(w.sort_by_columns_action.isEnabled()) self.assertTrue(w.plugin_actions[0].isEnabled()) # View diff --git a/inselect/tests/lib/test_document_export.py b/inselect/tests/lib/test_document_export.py index 2574c94..87c6867 100644 --- a/inselect/tests/lib/test_document_export.py +++ b/inselect/tests/lib/test_document_export.py @@ -144,13 +144,13 @@ def test_csv_export(self): self.assertEqual( (u'04_3.png', u'4', u'0', u'248', u'189', u'437', - u'', u'Elsinoë', u'3'), + u'4', u'Elsinoë', u'3'), metadata_cols(reader.next()) ) self.assertEqual( (u'05_4.png', u'5', u'271', u'248', u'459', u'437', - u'', u'D', u'4'), + u'5', u'D', u'4'), metadata_cols(reader.next()) ) self.assertIsNone(next(reader, None)) diff --git a/inselect/tests/lib/test_segment.py b/inselect/tests/lib/test_segment.py index 2171fa8..65d5287 100644 --- a/inselect/tests/lib/test_segment.py +++ b/inselect/tests/lib/test_segment.py @@ -2,28 +2,42 @@ from pathlib import Path from inselect.lib.document import InselectDocument -from inselect.lib.segment import segment_document +from inselect.lib.segment_document import SegmentDocument TESTDATA = Path(__file__).parent.parent / 'test_data' class TestSegment(unittest.TestCase): - def test_segment_document(self): - doc = InselectDocument.load(TESTDATA / 'test_segment.inselect') - + def _segment(self, doc, sort_by_columns, expected): self.assertEqual(5, len(doc.items)) - - # Compare the rects in pixels - expected = doc.scanned.from_normalised([i['rect'] for i in doc.items]) doc.set_items([]) self.assertEqual(0, len(doc.items)) - doc, display_image = segment_document(doc) + segment_doc = SegmentDocument(sort_by_columns=sort_by_columns) + doc, display_image = segment_doc.segment(doc) + # Compare the rects in pixels actual = doc.scanned.from_normalised([i['rect'] for i in doc.items]) self.assertEqual(list(expected), list(actual)) + def test_segment_document_sort_by_rows(self): + "Segment the document with boxes sorted by rows" + doc = InselectDocument.load(TESTDATA / 'test_segment.inselect') + expected = doc.scanned.from_normalised( + [i['rect'] for i in doc.items] + ) + self._segment(doc, False, expected) + + def test_segment_document_sort_by_columns(self): + "Segment the document with boxes sorted by columns" + doc = InselectDocument.load(TESTDATA / 'test_segment.inselect') + items = doc.items + expected = doc.scanned.from_normalised( + [items[index]['rect'] for index in (0, 3, 2, 1, 4)] + ) + self._segment(doc, True, expected) + if __name__ == '__main__': unittest.main() diff --git a/inselect/tests/lib/test_sort_boxes.py b/inselect/tests/lib/test_sort_boxes.py new file mode 100644 index 0000000..66a0cba --- /dev/null +++ b/inselect/tests/lib/test_sort_boxes.py @@ -0,0 +1,31 @@ +import unittest +from pathlib import Path + +from inselect.lib.document import InselectDocument +from inselect.lib.sort_document_items import sort_document_items + + +TESTDATA = Path(__file__).parent.parent / 'test_data' + + +class TestSortBoxes(unittest.TestCase): + def test_order_by_rows(self): + doc = InselectDocument.load(TESTDATA / 'test_segment.inselect') + + items = sort_document_items(doc.items, by_columns=False) + self.assertEqual( + ['1', '2', '3', '4', '5'], + [item['fields']['catalogNumber'] for item in items] + ) + + def test_order_by_columns(self): + doc = InselectDocument.load(TESTDATA / 'test_segment.inselect') + items = sort_document_items(doc.items, by_columns=True) + self.assertEqual( + ['1', '4', '3', '2', '5'], + [item['fields']['catalogNumber'] for item in items] + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/inselect/tests/test_data/test_segment.inselect b/inselect/tests/test_data/test_segment.inselect index 725f2b4..b2b0996 100644 --- a/inselect/tests/test_data/test_segment.inselect +++ b/inselect/tests/test_data/test_segment.inselect @@ -42,6 +42,7 @@ }, { "fields": { + "catalogNumber": "4", "scientificName": "Elsinoë" }, "rect": [ @@ -54,6 +55,7 @@ }, { "fields": { + "catalogNumber": "5", "scientificName": "D" }, "rect": [ From 5897965d8b79526acd4386b004be2c1089909c51 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Tue, 26 Apr 2016 22:26:29 +0100 Subject: [PATCH 02/22] Attempt to fix travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b642b91..57dfd67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ before_install: install: - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then - sudo apt-get install --fix-missing python-opencv python-numpy python-pyside libtiff4-dev libjpeg8-dev zlib1g-dev libzbar-dev; + sudo apt-get install --fix-missing python-opencv python-numpy python-sklearn python-pyside libtiff4-dev libjpeg8-dev zlib1g-dev libzbar-dev; fi - pip install -r requirements.txt - "export DISPLAY=:99.0" From 09150b0c0e73c8359edb772f7f37a7231a94d3af Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Tue, 26 Apr 2016 23:31:28 +0100 Subject: [PATCH 03/22] Another attempt to fix travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 57dfd67..f62ee10 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ before_install: install: - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then - sudo apt-get install --fix-missing python-opencv python-numpy python-sklearn python-pyside libtiff4-dev libjpeg8-dev zlib1g-dev libzbar-dev; + sudo apt-get install --fix-missing python-opencv python-numpy python-scipy python-sklearn python-pyside libtiff4-dev libjpeg8-dev zlib1g-dev libzbar-dev; fi - pip install -r requirements.txt - "export DISPLAY=:99.0" From 3307f740774908a8de528a4934eb9ff194c0f5e5 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 08:18:59 +0100 Subject: [PATCH 04/22] argrelmin in preference to argrelextrema --- inselect/lib/sort_document_items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inselect/lib/sort_document_items.py b/inselect/lib/sort_document_items.py index c58c4ff..13d5867 100644 --- a/inselect/lib/sort_document_items.py +++ b/inselect/lib/sort_document_items.py @@ -3,7 +3,7 @@ import numpy as np -from scipy.signal import argrelextrema +from scipy.signal import argrelmin from sklearn.neighbors import KernelDensity @@ -19,7 +19,7 @@ def _do_kde(values): # Identify minima and use as break points samples = np.linspace(0, RESCALE) evaluations = kde.score_samples(samples.reshape(-1, 1)) - minima = argrelextrema(evaluations, np.less)[0] + minima = argrelmin(evaluations) # The right-hand edges of bins bins = np.append(samples[minima], RESCALE) From 28565f69763cda4cde284d524526bb2512c6efa5 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 08:37:22 +0100 Subject: [PATCH 05/22] Another attempt to fix travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f62ee10..fc3f2a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +dist: trusty python: - "2.7" From 5d9edfc20c4cb56ddc323105e6c1568e9f85f7a8 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 08:53:20 +0100 Subject: [PATCH 06/22] Another attempt to fix travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fc3f2a1..38b6c3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ before_install: install: - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then - sudo apt-get install --fix-missing python-opencv python-numpy python-scipy python-sklearn python-pyside libtiff4-dev libjpeg8-dev zlib1g-dev libzbar-dev; + sudo apt-get install -y --fix-missing python-opencv python-numpy python-scipy python-sklearn python-pyside libtiff4-dev libjpeg8-dev zlib1g-dev libzbar-dev; fi - pip install -r requirements.txt - "export DISPLAY=:99.0" From 94e41cd01fa332582f99ff99a472023358e714aa Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 09:15:02 +0100 Subject: [PATCH 07/22] Fix Windows build --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index a43a3fe..be4c36f 100755 --- a/setup.py +++ b/setup.py @@ -49,6 +49,8 @@ ], 'include_files': [ ('{site_packages}/numpy', 'numpy'), + ('{site_packages}/scipy', 'scipy'), + ('{site_packages}/sklearn', 'sklearn'), ], 'extra_packages': ['win32com.gen_py'], 'excludes': [ From a3ad91928f7d4753204a2443237c7f720fed37f1 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 09:16:13 +0100 Subject: [PATCH 08/22] Fix persistence of 'sort by' preference on Windows --- inselect/gui/sort_document_items.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/inselect/gui/sort_document_items.py b/inselect/gui/sort_document_items.py index ab9b02b..1dc860f 100644 --- a/inselect/gui/sort_document_items.py +++ b/inselect/gui/sort_document_items.py @@ -19,7 +19,8 @@ def sort_items_choice(): class SortDocumentItems(object): def __init__(self): - self._by_columns = QSettings().value(_PATH, False) + # Key holds an integer + self._by_columns = 1 == QSettings().value(_PATH, False) @property def by_columns(self): @@ -33,5 +34,7 @@ def sort_items(self, items, by_columns): user's most recent preference (None). """ self._by_columns = by_columns - QSettings().setValue(_PATH, by_columns) + # Pass integer to setValue - calling setValue with a bool with result + # in a string being written to the QSettings store. + QSettings().setValue(_PATH, 1 if by_columns else 0) return sort_document_items(items, by_columns) From df5affbedbbf616a7679c6cffd82c9b6ec97a4ed Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 10:42:43 +0100 Subject: [PATCH 09/22] Another attempt to fix travis --- .travis.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 38b6c3d..4103c3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: python -dist: trusty python: - "2.7" @@ -9,11 +8,30 @@ virtualenv: before_install: - sudo apt-get update - -install: - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then - sudo apt-get install -y --fix-missing python-opencv python-numpy python-scipy python-sklearn python-pyside libtiff4-dev libjpeg8-dev zlib1g-dev libzbar-dev; + sudo apt-get install -y --fix-missing libzbar-dev; + fi + + # We do this conditionally because it saves us some downloading if the + # version is the same. + - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then + wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh; + else + wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; fi + - bash miniconda.sh -b -p $HOME/miniconda + - export PATH="$HOME/miniconda/bin:$PATH" + - hash -r + - conda config --set always_yes yes --set changeps1 no + - conda update --yes conda + # Useful for debugging any issues with conda + - conda info -a + + - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION inselect pillow pyside numpy scikit-learn opencv + - source activate test-environment + - python setup.py install + +install: - pip install -r requirements.txt - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" From 15c13d0ee82c70548a0bfd7b11f385af3c99e3b4 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 10:47:36 +0100 Subject: [PATCH 10/22] Another attempt to fix travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4103c3d..5d33dac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ before_install: # Useful for debugging any issues with conda - conda info -a - - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION inselect pillow pyside numpy scikit-learn opencv + - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION pillow pyside numpy scikit-learn opencv - source activate test-environment - python setup.py install From 9ea1183fc6d2016ca141387c350b3c79e581fd8e Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 13:02:32 +0100 Subject: [PATCH 11/22] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f090cf..a1fe86e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ This is an overview of major changes. Refer to the git repository for a full log Version 0.1.28 ------------- +- Fixed #291 - Alter travis to use Anaconda / Miniconda - Fixed #288 - Minimap navigator - Fixed #286 - Disable default template command when the default template selected - Fixed #284 - Document info view to contain links From 9b39da26c4e90e34cdb36413cddbc545830276b8 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 13:02:49 +0100 Subject: [PATCH 12/22] Updated instructions for sklearn and related --- DevelopingOnMacOSX.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/DevelopingOnMacOSX.md b/DevelopingOnMacOSX.md index da899d3..036dd96 100644 --- a/DevelopingOnMacOSX.md +++ b/DevelopingOnMacOSX.md @@ -24,7 +24,7 @@ conda update --yes conda # Inselect env ``` -conda create --yes --name inselect pillow pyside scikit-learn +conda create --yes --name inselect pillow pyside source activate inselect ``` @@ -35,19 +35,24 @@ cd ~/projects/inselect pip install -r requirements.txt ``` -# OpenCV -`numpy` is pinned by opencv installation +## OpenCV +Version of `numpy` is pinned by opencv installation but we want latest version - +I have not encountered any problems by installing the latest version. +`jjhelmus` provides versions after `2.4.10` but these make the Mac build +extremely problematic by introducing many `dylib` dependencies that are +troublesome to freeze. ``` -conda install --yes -c https://conda.binstar.org/jjhelmus opencv +conda install --yes -c https://conda.binstar.org/jjhelmus opencv=2.4.10 +conda install --yes numpy ``` -# setuptools +## setuptools A [bug in PyInstaller 3.1.1](https://github.com/pyinstaller/pyinstaller/issues/1773) means that we need to use setupools 19.2: ``` -conda install setuptools=19.2 +conda install --yes setuptools=19.2 ``` ## LibDMTX barcode reading library From dcf8cf63f38bb82867e5e0862fc83a6e76ac2a62 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 13:04:24 +0100 Subject: [PATCH 13/22] Fix warning --- inselect/gui/plugins/subsegment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inselect/gui/plugins/subsegment.py b/inselect/gui/plugins/subsegment.py index 80e3f58..cfafed6 100644 --- a/inselect/gui/plugins/subsegment.py +++ b/inselect/gui/plugins/subsegment.py @@ -45,7 +45,7 @@ def __call__(self, progress): # Points as a list of tuples, with coordinates relative to # the top-left of the sub-segmentation window - seeds = [(p.x(), p.y()) for p in self.seeds] + seeds = [(int(p.x()), int(p.y())) for p in self.seeds] items, display_image = SegmentDocument(self.sort_choice).subsegment( self.document, self.row, seeds, callback=progress From 6c322b96ab310dbfbec909c280b95fb45fbfcdda Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 13:04:47 +0100 Subject: [PATCH 14/22] Fix tests --- inselect/tests/gui/test_segment.py | 6 ++++-- inselect/tests/gui/test_subsegment.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/inselect/tests/gui/test_segment.py b/inselect/tests/gui/test_segment.py index 14ea1e6..be42af4 100644 --- a/inselect/tests/gui/test_segment.py +++ b/inselect/tests/gui/test_segment.py @@ -9,6 +9,7 @@ from gui_test import MainWindowTest from inselect.gui.roles import RectRole +from inselect.gui.sort_document_items import SortDocumentItems TESTDATA = Path(__file__).parent.parent / 'test_data' @@ -34,8 +35,9 @@ def test_segment(self, mock_question): indexes = [w.model.index(r, 0) for r in xrange(0, w.model.rowCount())] expected = [w.model.data(i, RectRole) for i in indexes] - # Segment - self.run_async_operation(partial(w.run_plugin, 0)) + # Segment, sorting by rows + with patch.object(SortDocumentItems, 'by_columns', False): + self.run_async_operation(partial(w.run_plugin, 0)) # Get the rects of the new boxes self.assertEqual(5, w.model.rowCount()) diff --git a/inselect/tests/gui/test_subsegment.py b/inselect/tests/gui/test_subsegment.py index d2edd97..c885727 100644 --- a/inselect/tests/gui/test_subsegment.py +++ b/inselect/tests/gui/test_subsegment.py @@ -8,6 +8,7 @@ from PySide.QtGui import QMessageBox from inselect.gui.roles import MetadataRole, RectRole +from inselect.gui.sort_document_items import SortDocumentItems from gui_test import MainWindowTest @@ -37,8 +38,9 @@ def test_subsegment(self): for pos in seeds: box.append_point_of_interest(pos) - # Sub-segment - self.run_async_operation(partial(w.run_plugin, 1)) + # Sub-segment, sorting by rows + with patch.object(SortDocumentItems, 'by_columns', False): + self.run_async_operation(partial(w.run_plugin, 1)) # Should have three boxes with the same metadata self.assertEqual(3, w.model.rowCount()) From ab8157ceb47505ae67067ecde391987d8a7419e6 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 16:18:55 +0100 Subject: [PATCH 15/22] Additional dependencies for Windows --- setup.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index be4c36f..c451faa 100755 --- a/setup.py +++ b/setup.py @@ -51,6 +51,9 @@ ('{site_packages}/numpy', 'numpy'), ('{site_packages}/scipy', 'scipy'), ('{site_packages}/sklearn', 'sklearn'), + ('{environment_root}/Library/bin/mkl_core.dll', 'mkl_core.dll'), + ('{environment_root}/Library/bin/msvcr90.dll', 'msvcr90.dll'), + ('{environment_root}/Library/bin/libiomp5md.dll', 'libiomp5md.dll'), ], 'extra_packages': ['win32com.gen_py'], 'excludes': [ @@ -80,13 +83,17 @@ def cx_setup(): """cx_Freeze setup. Used for building Windows installers""" from cx_Freeze import setup, Executable from distutils.sysconfig import get_python_lib + from pathlib import Path - # Set path to include files - site_packages = get_python_lib() + # Set paths to include files + format_strings = { + 'site_packages': get_python_lib(), + 'environment_root': Path(sys.executable).parent, + } include_files = [] for i in setup_data['win32']['include_files']: include_files.append(( - i[0].format(site_packages=site_packages), + i[0].format(**format_strings), i[1] )) From 02bc001b75342e1c193ab0ebdddb7abd67541dde Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 19:23:20 +0100 Subject: [PATCH 16/22] Fixes Mac build --- inselect.spec | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/inselect.spec b/inselect.spec index 34c8815..e1f77b9 100644 --- a/inselect.spec +++ b/inselect.spec @@ -1,6 +1,7 @@ # For PyInstaller build on Mac import sys + from pathlib import Path block_cipher = None @@ -9,7 +10,7 @@ a = Analysis(['inselect.py'], pathex=[str(Path('.').absolute())], binaries=None, datas=None, - hiddenimports=[], + hiddenimports=['sklearn.neighbors.typedefs'], hookspath=[], runtime_hooks=[], excludes=['Tkinter'], @@ -18,23 +19,25 @@ a = Analysis(['inselect.py'], cipher=block_cipher) -# PyInstaller does not detect some dylibs, I think because they are symlinked. +# PyInstaller does not detect some dylibs, I think in some cases because they +# are symlinked. # See Stack Overflow post http://stackoverflow.com/a/17595149 for example # of manipulating Analysis.binaries. -# Tuples (name, source) MISSING_DYLIBS = ( - ('libQtCore.4.dylib', 'libQtCore.4.8.7.dylib'), - ('libQtGui.4.dylib', 'libQtGui.4.8.7.dylib'), - ('libpng16.16.dylib', 'libpng16.16.dylib'), - ('libz.1.dylib', 'libz.1.dylib'), + 'libQtCore.4.dylib', + 'libQtGui.4.dylib', + 'libpng16.16.dylib', + 'libz.1.dylib', ) # The lib directory associated with this environment LIB = Path(sys.argv[0]).parent.parent.joinpath('lib') -a.binaries += TOC([(name, str(LIB.joinpath(source)), 'BINARY') for name, source in MISSING_DYLIBS]) - +# Find the source for each library and add it to the list of binaries +a.binaries += TOC([ + (lib, str(LIB.joinpath(lib).resolve()), 'BINARY') for lib in MISSING_DYLIBS +]) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) # Prefer to freeze to a folder rather than a single file. The choice makes no @@ -51,7 +54,7 @@ if SINGLE_FILE: name='inselect', debug=False, strip=False, - upx=True, + upx=False, console=False, icon='data/inselect.icns') else: @@ -62,7 +65,7 @@ else: name='inselect', debug=False, strip=False, - upx=True, + upx=False, console=False, icon='data/inselect.icns') @@ -71,7 +74,7 @@ else: a.zipfiles, a.datas, strip=False, - upx=True, + upx=False, name='inselect') From c2403445ca9604120b7c984dabea5724fd97ab1e Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 19:36:24 +0100 Subject: [PATCH 17/22] OS X hidden imports --- build.sh | 8 ++++++-- inselect/scripts/export_metadata.py | 4 ---- inselect/scripts/ingest.py | 4 ---- inselect/scripts/read_barcodes.py | 4 ---- inselect/scripts/save_crops.py | 4 ---- inselect/scripts/segment.py | 4 ---- 6 files changed, 6 insertions(+), 22 deletions(-) diff --git a/build.sh b/build.sh index decfd34..0655735 100755 --- a/build.sh +++ b/build.sh @@ -24,10 +24,14 @@ if [[ "$OSTYPE" == "darwin"* ]]; then rm -rf build dist pyinstaller --clean inselect.spec - for script in export_metadata ingest read_barcodes save_crops segment; do + for script in export_metadata ingest read_barcodes save_crops; do rm -rf $script.spec - pyinstaller --onefile --icon=data/inselect.icns inselect/scripts/$script.py + pyinstaller --onefile --hidden-import numpy inselect/scripts/$script.py done + # segment has an additional hidden import + rm -rf segment.spec + pyinstaller --onefile --hidden-import numpy \ + --hidden-import sklearn.neighbors.typedefs inselect/scripts/segment.py # Add a few items to the PropertyList file generated by PyInstaller python -m bin.plist dist/inselect.app/Contents/Info.plist diff --git a/inselect/scripts/export_metadata.py b/inselect/scripts/export_metadata.py index 0dfd88c..3683daf 100755 --- a/inselect/scripts/export_metadata.py +++ b/inselect/scripts/export_metadata.py @@ -9,10 +9,6 @@ from pathlib import Path -# Import numpy here to prevent PyInstaller build from breaking -# TODO LH find a better solution -import numpy # noqa - import inselect import inselect.lib.utils diff --git a/inselect/scripts/ingest.py b/inselect/scripts/ingest.py index e670eb4..34436d7 100755 --- a/inselect/scripts/ingest.py +++ b/inselect/scripts/ingest.py @@ -9,10 +9,6 @@ from pathlib import Path -# Import numpy here to prevent PyInstaller build from breaking -# TODO LH find a better solution -import numpy # noqa - import inselect import inselect.lib.utils diff --git a/inselect/scripts/read_barcodes.py b/inselect/scripts/read_barcodes.py index 470ab77..f3e5446 100755 --- a/inselect/scripts/read_barcodes.py +++ b/inselect/scripts/read_barcodes.py @@ -10,10 +10,6 @@ from itertools import count, izip from pathlib import Path -# Import numpy here to prevent PyInstaller build from breaking -# TODO LH find a better solution -import numpy # noqa - import inselect.lib.utils from inselect.lib.utils import debug_print diff --git a/inselect/scripts/save_crops.py b/inselect/scripts/save_crops.py index 7c45cd1..f633999 100755 --- a/inselect/scripts/save_crops.py +++ b/inselect/scripts/save_crops.py @@ -9,10 +9,6 @@ from pathlib import Path -# Import numpy here to prevent PyInstaller build from breaking -# TODO LH find a better solution -import numpy # noqa - import inselect import inselect.lib.utils diff --git a/inselect/scripts/segment.py b/inselect/scripts/segment.py index 4589bf2..d85ce63 100755 --- a/inselect/scripts/segment.py +++ b/inselect/scripts/segment.py @@ -9,10 +9,6 @@ from pathlib import Path -# Import numpy here to prevent PyInstaller build from breaking -# TODO LH find a better solution -import numpy # noqa - import inselect.lib.utils from inselect.lib.document import InselectDocument From e9dbe381352c9f141372dd96dc34e0d4b4fd7ca9 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Wed, 27 Apr 2016 19:36:39 +0100 Subject: [PATCH 18/22] Fix command-line help --- inselect/scripts/segment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inselect/scripts/segment.py b/inselect/scripts/segment.py index d85ce63..e908c47 100755 --- a/inselect/scripts/segment.py +++ b/inselect/scripts/segment.py @@ -48,7 +48,7 @@ def main(args): parser.add_argument('--debug', action='store_true') parser.add_argument( '--sort-by-columns', action='store_true', default=False, - help='Sort boxes columns; default is to sort boxes by rows') + help='Sort boxes by columns; default is to sort boxes by rows') parser.add_argument( '-v', '--version', action='version', version='%(prog)s ' + inselect.__version__ From 671c1809a1b20a4a0c463b3188ce6f434ae14254 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Thu, 28 Apr 2016 11:16:02 +0100 Subject: [PATCH 19/22] Remove unused reference to Conda / Python 3 --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5d33dac..095230c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,6 @@ before_install: # version is the same. - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh; - else - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; fi - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" From 75c49f9180534ddcc9c5efcf8743de26abd3b367 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Thu, 28 Apr 2016 11:34:01 +0100 Subject: [PATCH 20/22] Avoid unnecessary creation of lists when sorting --- inselect/lib/sort_document_items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inselect/lib/sort_document_items.py b/inselect/lib/sort_document_items.py index 13d5867..1d3cbbb 100644 --- a/inselect/lib/sort_document_items.py +++ b/inselect/lib/sort_document_items.py @@ -36,8 +36,8 @@ def sort_document_items(items, by_columns): return [] else: rects = [i['rect'] for i in items] - x_bins = _do_kde([r.centre.x for r in rects]) - y_bins = _do_kde([r.centre.y for r in rects]) + x_bins = _do_kde(r.centre.x for r in rects) + y_bins = _do_kde(r.centre.y for r in rects) if by_columns: keys = izip(x_bins, y_bins, (r.left for r in rects)) From 1cada97abec35588d3e78be7ce1ae3440d3706e4 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Thu, 28 Apr 2016 12:10:05 +0100 Subject: [PATCH 21/22] Menu keyboard shortcuts --- inselect/gui/main_window.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/inselect/gui/main_window.py b/inselect/gui/main_window.py index a9e4421..691e753 100644 --- a/inselect/gui/main_window.py +++ b/inselect/gui/main_window.py @@ -964,11 +964,11 @@ def _create_menu_actions(self): triggered=partial(self.select_next_prev, next=False) ) self.select_by_size_larger_action = QAction( - "Select increasing size", self, shortcut="ctrl+>", + "Select &increasing size", self, shortcut="ctrl+>", triggered=partial(self.select_by_size_step, larger=True) ) self.select_by_size_smaller_action = QAction( - "Select decreasing size", self, shortcut="ctrl+<", + "Select d&ecreasing size", self, shortcut="ctrl+<", triggered=partial(self.select_by_size_step, larger=False) ) @@ -985,20 +985,20 @@ def _create_menu_actions(self): self.delete_action.shortcut()]) self.rotate_clockwise_action = QAction( - "Rotate clockwise", self, shortcut="ctrl+R", + "Rotate c&lockwise", self, shortcut="ctrl+R", triggered=partial(self.rotate90, clockwise=True) ) self.rotate_counter_clockwise_action = QAction( - "Rotate counter-clockwise", self, shortcut="ctrl+L", + "Rotate c&ounter-clockwise", self, shortcut="ctrl+L", triggered=partial(self.rotate90, clockwise=False) ) self.sort_by_rows_action = QAction( - "Sort by rows", self, checkable=True, + "Sort by &rows", self, checkable=True, triggered=partial(self.sort_boxes, by_columns=False) ) self.sort_by_columns_action = QAction( - "Sort by columns", self, checkable=True, + "Sort by &columns", self, checkable=True, triggered=partial(self.sort_boxes, by_columns=True) ) @@ -1064,7 +1064,7 @@ def _create_menu_actions(self): icon=self.style().standardIcon(QtGui.QStyle.SP_ArrowDown) ) self.zoom_home_action = QAction( - "Whole image", self, + "&Whole image", self, shortcut=QtGui.QKeySequence.MoveToStartOfDocument, triggered=self.zoom_home, checkable=True ) @@ -1081,10 +1081,10 @@ def _create_menu_actions(self): ) self.show_object_grid_action = QAction( - 'Show grid', self, shortcut='ctrl+G', triggered=self.show_grid + 'Show &grid', self, shortcut='ctrl+G', triggered=self.show_grid ) self.show_object_expanded_action = QAction( - 'Show expanded', self, + 'Show &expanded', self, shortcut='ctrl+E', triggered=self.show_expanded ) @@ -1223,7 +1223,7 @@ def _create_menus(self): self._view_menu.addAction(self.show_object_grid_action) self._view_menu.addAction(self.show_object_expanded_action) self._view_menu.addSeparator() - colours_popup = self._view_menu.addMenu('Colour scheme') + colours_popup = self._view_menu.addMenu('&Colour scheme') for action in self.colour_scheme_actions: colours_popup.addAction(action) From 12ccb3d404bfab49b0e2d8473a733e088d818168 Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Thu, 28 Apr 2016 13:56:06 +0100 Subject: [PATCH 22/22] Tweaks to Windows build --- setup.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index c451faa..cc7d90a 100755 --- a/setup.py +++ b/setup.py @@ -47,18 +47,19 @@ } for script in SCRIPTS ], + # Strings in braces within 'include_files' tuples expanded in cx_setup 'include_files': [ ('{site_packages}/numpy', 'numpy'), ('{site_packages}/scipy', 'scipy'), ('{site_packages}/sklearn', 'sklearn'), ('{environment_root}/Library/bin/mkl_core.dll', 'mkl_core.dll'), - ('{environment_root}/Library/bin/msvcr90.dll', 'msvcr90.dll'), ('{environment_root}/Library/bin/libiomp5md.dll', 'libiomp5md.dll'), ], 'extra_packages': ['win32com.gen_py'], 'excludes': [ - 'Tkinter', 'ttk', 'Tkconstants', 'tcl', - 'future.moves' # Errors from urllib otherwise + 'Tkinter', 'ttk', 'Tkconstants', 'tcl', '_ssl', + 'future.moves', # Errors from urllib otherwise + 'PySide.QtNetwork', ] } } @@ -106,6 +107,8 @@ def cx_setup(): 'packages': setup_data['packages'] + setup_data['win32']['extra_packages'], 'excludes': setup_data['win32']['excludes'], 'include_files': include_files, + 'include_msvcr': True, + 'optimize': 2, }, 'bdist_msi': { 'upgrade_code': '{fe2ed61d-cd5e-45bb-9d16-146f725e522f}'