Skip to content

Commit

Permalink
[#213] Padding around objects
Browse files Browse the repository at this point in the history
  • Loading branch information
quicklizard99 committed Apr 14, 2016
1 parent d8c82d8 commit b0c3d78
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 70 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------
Expand Down
9 changes: 5 additions & 4 deletions inselect/gui/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 16 additions & 4 deletions inselect/gui/plugins/subsegment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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))
)
7 changes: 4 additions & 3 deletions inselect/lib/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 23 additions & 3 deletions inselect/lib/rect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
12 changes: 9 additions & 3 deletions inselect/lib/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions inselect/tests/lib/test_document_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
)
Expand Down
20 changes: 11 additions & 9 deletions inselect/tests/lib/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down
9 changes: 9 additions & 0 deletions inselect/tests/lib/test_rect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions inselect/tests/lib/test_segment.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import unittest
from pathlib import Path

Expand All @@ -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))


Expand Down
83 changes: 44 additions & 39 deletions inselect/tests/test_data/test_segment.inselect
Original file line number Diff line number Diff line change
@@ -1,70 +1,75 @@
{
"inselect version": 1,
"inselect version": 2,
"items": [
{
"fields": {
"catalogNumber": "1",
"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"
}
}

0 comments on commit b0c3d78

Please sign in to comment.