From b0c3d782b20ccd47a8b521f90b922ee0f5c336fb Mon Sep 17 00:00:00 2001 From: Lawrence Hudson Date: Thu, 14 Apr 2016 14:50:53 +0100 Subject: [PATCH] [#213] Padding around objects --- CHANGELOG.md | 1 + inselect/gui/model.py | 9 +- inselect/gui/plugins/subsegment.py | 20 ++++- inselect/lib/image.py | 7 +- inselect/lib/rect.py | 26 +++++- inselect/lib/segment.py | 12 ++- inselect/tests/lib/test_document_export.py | 10 +-- inselect/tests/lib/test_image.py | 20 +++-- inselect/tests/lib/test_rect.py | 9 ++ inselect/tests/lib/test_segment.py | 4 + .../tests/test_data/test_segment.inselect | 83 ++++++++++--------- 11 files changed, 131 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c44472e..ebae7f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Version 0.1.26 - Fixed #256 - Disable template 'Reload' command when default template selected - Fixed #254 - An Error Occurred: {'Choices with data ... - Fixed #218 - N boxes / N selected widget to a status bar +- Fixed #213 - Increase default border around "objects" post segmentation Version 0.1.25 ------------- diff --git a/inselect/gui/model.py b/inselect/gui/model.py index 54a3ddf..d9d6655 100644 --- a/inselect/gui/model.py +++ b/inselect/gui/model.py @@ -77,11 +77,12 @@ def _boxes_from_items(self, items, image_width=None, image_height=None): data = [None] * len(items) for index, item in enumerate(items): + # Convert normalised coords to pixel coords for pixmap rect = item['rect'] - rect = QRect(rect[0]*image_width, - rect[1]*image_height, - rect[2]*image_width, - rect[3]*image_height) + rect = QRect(int(round(rect[0] * image_width)), + int(round(rect[1] * image_height)), + int(round(rect[2] * image_width)), + int(round(rect[3] * image_height))) data[index] = { "fields": item.get('fields', {}), "rect": rect, diff --git a/inselect/gui/plugins/subsegment.py b/inselect/gui/plugins/subsegment.py index b20ddb4..c09fa89 100644 --- a/inselect/gui/plugins/subsegment.py +++ b/inselect/gui/plugins/subsegment.py @@ -3,6 +3,7 @@ from PySide.QtGui import QIcon, QMessageBox from inselect.lib.segment import segment_grabcut +from inselect.lib.rect import Rect from inselect.lib.utils import debug_print from .plugin import Plugin @@ -61,9 +62,18 @@ def __call__(self, progress): rects, display = segment_grabcut(image.array, window, seeds) - # Replace the item - rects = [{'rect': r} for r in image.to_normalised(rects)] - items[row:(1+row)] = rects + # 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) + + # Replace the existing item + items[row:(1+row)] = [{'rect': r} for r in rects] # Segmentation image h, w = image.array.shape[:2] @@ -74,4 +84,6 @@ def __call__(self, progress): self.items, self.display = items, display_image - debug_print('SegmentPlugin.__call__ exiting. Found [{0}] boxes'.format(len(rects))) + debug_print( + 'SegmentPlugin.__call__ exiting. Found [{0}] boxes'.format(len(rects)) + ) diff --git a/inselect/lib/image.py b/inselect/lib/image.py index 75cfebc..7b950e7 100644 --- a/inselect/lib/image.py +++ b/inselect/lib/image.py @@ -58,15 +58,16 @@ def from_normalised(self, boxes): """ w, h = self.dimensions for left, top, width, height in boxes: - yield Rect(int(w*left), int(h*top), int(w*width), int(h*height)) + yield Rect(int(round(w * left)), int(round(h * top)), + int(round(w * width)), int(round(h * height))) def to_normalised(self, boxes): """Generator function that yields instances of Rect """ w, h = self.dimensions + w, h = float(w), float(h) for left, top, width, height in boxes: - yield Rect(float(left)/w, float(top)/h, float(width)/w, - float(height)/h) + yield Rect(left / w, top / h, width / w, height / h) def crops(self, normalised, rotation=None): """Generator function that yields cropped images diff --git a/inselect/lib/rect.py b/inselect/lib/rect.py index c6d0baf..abcb03c 100644 --- a/inselect/lib/rect.py +++ b/inselect/lib/rect.py @@ -10,13 +10,13 @@ class Rect(collections.namedtuple('Rect', ['left', 'top', 'width', 'height'])): @property def area(self): "The product of width and height" - return self.width*self.height + return self.width * self.height @property def coordinates(self): "Coordinates(left, top, right, bottom)" - return Coordinates(self.left, self.top, self.left+self.width, - self.top+self.height) + return Coordinates(self.left, self.top, self.left + self.width, + self.top + self.height) @property def topleft(self): @@ -33,6 +33,26 @@ def centre(self): "Point(x, y)" return Point(self.left + self.width / 2, self.top + self.height / 2) + def padded(self, percent): + "Returns self with percentage padding applied" + x_offset = self.width * float(percent) / 100.0 + y_offset = self.height * float(percent) / 100.0 + return Rect(self.left - x_offset, self.top - y_offset, + self.width + 2 * x_offset, self.height + 2 * y_offset) + + def intersect(self, other): + "Returns self intersected to be within other" + if isinstance(other, Rect): + left, top, right, bottom = self.coordinates + other_left, other_top, other_right, other_bottom = other.coordinates + left = max(other_left, left) + top = max(other_top, top) + width = min(other_right, right) - left + height = min(other_bottom, bottom) - top + return Rect(left, top, width, height) + else: + raise NotImplementedError() + def __eq__(self, other): if isinstance(other, Rect): return (self.left == other.left and diff --git a/inselect/lib/segment.py b/inselect/lib/segment.py index 0e9fc3f..4248468 100644 --- a/inselect/lib/segment.py +++ b/inselect/lib/segment.py @@ -44,10 +44,16 @@ def segment_document(doc, resize=None, *args, **kwargs): rects, display_image = segment_edges(img.array, resize=resize, *args, **kwargs) - # TODO LH Apply padding here? - - rects = map(lambda r: Rect(r[0], r[1], r[2], r[3]), rects) + # 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) diff --git a/inselect/tests/lib/test_document_export.py b/inselect/tests/lib/test_document_export.py index 9befe20..2574c94 100644 --- a/inselect/tests/lib/test_document_export.py +++ b/inselect/tests/lib/test_document_export.py @@ -125,31 +125,31 @@ def test_csv_export(self): metadata_cols = itemgetter(0, 1, 10, 11, 12, 13, 14, 15, 16) self.assertEqual( (u'01_1.png', u'1', - u'0', u'0', u'187', u'187', + u'0', u'0', u'189', u'189', u'1', u'A', u'1'), metadata_cols(reader.next()) ) self.assertEqual( (u'02_2.png', u'2', - u'272', u'0', u'458', u'187', + u'271', u'0', u'459', u'189', u'2', u'B', u'2'), metadata_cols(reader.next()) ) self.assertEqual( (u'03_10.png', u'3', - u'195', u'196', u'257', u'231', + u'194', u'196', u'257', u'232', u'3', u'インセクト', u'10'), metadata_cols(reader.next()) ) self.assertEqual( (u'04_3.png', u'4', - u'0', u'250', u'187', u'436', + u'0', u'248', u'189', u'437', u'', u'Elsinoë', u'3'), metadata_cols(reader.next()) ) self.assertEqual( (u'05_4.png', u'5', - u'272', u'250', u'458', u'436', + u'271', u'248', u'459', u'437', u'', u'D', u'4'), metadata_cols(reader.next()) ) diff --git a/inselect/tests/lib/test_image.py b/inselect/tests/lib/test_image.py index 78059a6..48a3a3e 100644 --- a/inselect/tests/lib/test_image.py +++ b/inselect/tests/lib/test_image.py @@ -64,7 +64,7 @@ def test_from_normalised(self): i = InselectImage(TESTDATA / 'test_segment.png') h, w = i.array.shape[:2] boxes = [Rect(0, 0, 1, 1), Rect(0, 0.2, 0.1, 0.8)] - self.assertEqual([Rect(0, 0, 459, 437), Rect(0, 87, 45, 349)], + self.assertEqual([Rect(0, 0, 459, 437), Rect(0, 87, 46, 350)], list(i.from_normalised(boxes))) def test_not_normalised(self): @@ -113,10 +113,10 @@ def test_save_crop_partial(self): crop = InselectImage(p).array # Crop should have this shape - self.assertEqual((131, 183, 3), crop.shape) + self.assertEqual((131, 184, 3), crop.shape) # Crop should have these pixels - expected = i.array[87:218, 45:228] + expected = i.array[87:218, 46:230] self.assertTrue(np.all(expected == InselectImage(p).array)) finally: shutil.rmtree(temp) @@ -128,21 +128,23 @@ def test_save_crop_overlapping(self): try: # A crop that is partially overlapping the image p = Path(temp) / 'overlapping.png' + i.save_crops([Rect(-0.1, -0.1, 0.4, 0.3)], [p]) crop = InselectImage(p).array # Crop should have this shape - self.assertEqual((131, 183, 3), crop.shape) + self.assertEqual((131, 184, 3), crop.shape) # Non-intersecting regions should be all zeroes - self.assertTrue(np.all(0 == crop[0:43, 0:45])) - self.assertTrue(np.all(0 == crop[0:43, ])) - self.assertTrue(np.all(0 == crop[:, 0:45])) + self.assertTrue(np.all(0 == crop[0:44, 0:46])) + self.assertTrue(np.all(0 == crop[0:44, ])) + self.assertTrue(np.all(0 == crop[:, 0:46])) + coords = list(i.from_normalised([Rect(-0.1, -0.1, 0.4, 0.3)])) - expected = i.array[0:88, 0:138, ] + expected = i.array[0:87, 0:138, ] - self.assertTrue(np.all(expected == crop[43:, 45:, ])) + self.assertTrue(np.all(expected == crop[44:, 46:, ])) finally: shutil.rmtree(temp) diff --git a/inselect/tests/lib/test_rect.py b/inselect/tests/lib/test_rect.py index c98454c..8e184f2 100644 --- a/inselect/tests/lib/test_rect.py +++ b/inselect/tests/lib/test_rect.py @@ -43,6 +43,15 @@ def test_bottomright(self): def test_centre(self): self.assertEqual(Point(1, 2), self.R.centre) + def test_padded(self): + r = Rect(0, 0, 100, 100) + self.assertEqual(Rect(-10, -10, 120, 120), r.padded(10.0)) + + def test_intersect(self): + a = Rect(-10, -10, 110, 110) + b = Rect(0, 0, 100, 100) + self.assertEqual(Rect(0, 0, 100, 100), a.intersect(b)) + def test_comparison(self): a = Rect(0, 1, 2, 3) self.assertEqual(a, self.R) diff --git a/inselect/tests/lib/test_segment.py b/inselect/tests/lib/test_segment.py index 2171fa8..9cbf827 100644 --- a/inselect/tests/lib/test_segment.py +++ b/inselect/tests/lib/test_segment.py @@ -1,3 +1,4 @@ +import json import unittest from pathlib import Path @@ -16,12 +17,15 @@ def test_segment_document(self): # Compare the rects in pixels expected = doc.scanned.from_normalised([i['rect'] for i in doc.items]) + # from pprint import pprint + # pprint([i['rect'] for i in doc.items]) doc.set_items([]) self.assertEqual(0, len(doc.items)) doc, display_image = segment_document(doc) actual = doc.scanned.from_normalised([i['rect'] for i in doc.items]) + # pprint([i['rect'] for i in doc.items]) self.assertEqual(list(expected), list(actual)) diff --git a/inselect/tests/test_data/test_segment.inselect b/inselect/tests/test_data/test_segment.inselect index dc4041a..725f2b4 100644 --- a/inselect/tests/test_data/test_segment.inselect +++ b/inselect/tests/test_data/test_segment.inselect @@ -1,5 +1,5 @@ { - "inselect version": 1, + "inselect version": 2, "items": [ { "fields": { @@ -7,64 +7,69 @@ "scientificName": "A" }, "rect": [ - 0.0002, - 0.0002, - 0.40820000000000006, - 0.42900000000000005 - ] - }, + 0.0, + 0.0, + 0.4117647058823529, + 0.43249427917620137 + ], + "rotation": 0 + }, { - "fields": { + "fields": { "catalogNumber": "2", "scientificName": "B" }, - "rect": [ - 0.5936, - 0.0002, - 0.40620000000000006, - 0.42900000000000005 - ] - }, + "rect": [ + 0.5904139433551199, + 0.0, + 0.4095860566448802, + 0.43249427917620137 + ], + "rotation": 0 + }, { "fields": { "catalogNumber": "3", "scientificName": "インセクト" }, "rect": [ - 0.42560000000000003, - 0.4494, - 0.1356, - 0.08060000000000003 - ] - }, + 0.4226579520697168, + 0.448512585812357, + 0.13725490196078433, + 0.08237986270022883 + ], + "rotation": 0 + }, { "fields": { "scientificName": "Elsinoë" }, "rect": [ - 0.0002, - 0.5730000000000001, - 0.40820000000000006, - 0.42679999999999996 - ] - }, + 0.0, + 0.5675057208237986, + 0.4117647058823529, + 0.43249427917620137 + ], + "rotation": 0 + }, { "fields": { "scientificName": "D" }, "rect": [ - 0.5936, - 0.5730000000000001, - 0.40620000000000006, - 0.42679999999999996 - ] + 0.5904139433551199, + 0.5675057208237986, + 0.4095860566448802, + 0.43249427917620137 + ], + "rotation": 0 } - ], + ], "properties": { - "Created by": "Lawrence Hudson", - "Created on": "2015-03-14T09:19:47Z", - "Saved by": "Lawrence Hudson", - "Saved on": "2015-03-14T09:19:47Z" - }, + "Created by": "Lawrence Hudson", + "Created on": "2015-03-14T09:19:47Z", + "Saved by": "Lawrence Hudson", + "Saved on": "2015-03-14T09:19:47Z" + }, "scanned extension": ".png" -} +} \ No newline at end of file