-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Line detection PR #2 follow-up. #8
Changes from all commits
a02de88
c58fae0
7b2fabb
576d7df
adddc42
57e5e1a
a95dfab
214a437
e12ff39
8b09121
145d064
4d6b5df
9b20d9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Dmitry Chaplinsky <[email protected]> | ||
Fedir Nepyivoda <[email protected]> | ||
Ievgen Varavva <[email protected]> | ||
Konst Kolesnichenko <[email protected]> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
from .geometry import * | ||
from .colours import * | ||
from .geometry import * | ||
from .lines import * |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
import math | ||
import cmath | ||
import cv2 | ||
import numpy | ||
|
||
from unshred import threshold | ||
from unshred.features import AbstractShredFeature | ||
|
||
|
||
DEBUG = False | ||
|
||
|
||
MEAN, MEDIAN = range(2) | ||
|
||
|
||
def _get_dominant_angle(lines, domination_type=MEDIAN): | ||
"""Picks dominant angle of a set of lines. | ||
|
||
Args: | ||
lines: iterable of (x1, y1, x2, y2) tuples that define lines. | ||
domination_type: either MEDIAN or MEAN. | ||
|
||
Returns: | ||
Dominant angle value in radians. | ||
|
||
Raises: | ||
ValueError: on unknown domination_type. | ||
""" | ||
if domination_type == MEDIAN: | ||
return _get_median_angle(lines) | ||
elif domination_type == MEAN: | ||
return _get_mean_angle(lines) | ||
else: | ||
raise ValueError('Unknown domination type provided: %s' % ( | ||
domination_type)) | ||
|
||
|
||
def _normalize_angle(angle, range, step): | ||
"""Finds an angle that matches the given one modulo step. | ||
|
||
Increments and decrements the given value with a given step. | ||
|
||
Args: | ||
range: a 2-tuple of min and max target values. | ||
step: tuning step. | ||
|
||
Returns: | ||
Normalized value within a given range. | ||
""" | ||
while angle <= range[0]: | ||
angle += step | ||
while angle >= range[1]: | ||
angle -= step | ||
return angle | ||
|
||
|
||
def _get_mean_angle(lines): | ||
unit_vectors = [] | ||
for x1, y1, x2, y2 in lines: | ||
c = complex(x2, -y2) - complex(x1, -y1) | ||
unit = c / abs(c) | ||
unit_vectors.append(unit) | ||
|
||
avg_angle = cmath.phase(numpy.average(unit_vectors)) | ||
|
||
return _normalize_angle(avg_angle, [-math.pi / 2, math.pi / 2], math.pi) | ||
|
||
|
||
def _get_median_angle(lines): | ||
angles = [] | ||
for x1, y1, x2, y2 in lines: | ||
c = complex(x2, -y2) - complex(x1, -y1) | ||
angle = cmath.phase(c) | ||
angles.append(angle) | ||
|
||
# Not np.median to avoid averaging middle elements. | ||
median_angle = numpy.percentile(angles, .5) | ||
|
||
return _normalize_angle(median_angle, [-math.pi / 2, math.pi / 2], math.pi) | ||
|
||
|
||
class LinesFeatures(AbstractShredFeature): | ||
"""Feature detector that recognizes lines. | ||
|
||
If the lines are detected, tag "Has Lines" is set and "lines_angle" feature | ||
is set to the value of best guess of lines angle in radians in range of | ||
[-pi/2; pi/2]. | ||
""" | ||
TAG_HAS_LINES_FEATURE = "Has Lines" | ||
|
||
def get_info(self, shred, contour, name): | ||
tags = [] | ||
params = {} | ||
|
||
_, _, _, mask = cv2.split(shred) | ||
# | ||
# # expanding mask for future removal of a border | ||
kernel = numpy.ones((5, 5), numpy.uint8) | ||
if DEBUG: | ||
cv2.imwrite('../debug/%s_mask_0.png' % name, mask) | ||
mask = cv2.morphologyEx(mask, cv2.MORPH_ERODE, kernel, iterations=2) | ||
if DEBUG: | ||
cv2.imwrite('../debug/%s_mask_1.png' % name, mask) | ||
_, mask = cv2.threshold(mask, 240, 0, cv2.THRESH_TOZERO) | ||
if DEBUG: | ||
cv2.imwrite('../debug/%s_mask_2.png' % name, mask) | ||
|
||
# TODO: move thresholding to Shred class, to allow reusing from other | ||
# feature detectors. | ||
edges = 255 - threshold.threshold( | ||
shred, min(shred.shape[:2])).astype(numpy.uint8) | ||
edges = edges & mask | ||
|
||
if DEBUG: | ||
cv2.imwrite('../debug/%s_asrc.png' % name, shred) | ||
cv2.imwrite('../debug/%s_edges.png' % name, edges) | ||
cv2.imwrite('../debug/%s_mask.png' % name, mask) | ||
|
||
_, _, r_w, r_h = cv2.boundingRect(contour) | ||
|
||
# Line len should be at least 30% of shred's width, gap - 20% | ||
lines = cv2.HoughLinesP(edges, rho=10, theta=numpy.pi / 180 * 2, | ||
threshold=30, maxLineGap=r_w * 0.2, | ||
minLineLength=max([r_h, r_w]) * 0.3 | ||
) | ||
|
||
if lines is not None: | ||
lines = lines[0] | ||
tags.append(self.TAG_HAS_LINES_FEATURE) | ||
|
||
dominant_angle = _get_dominant_angle(lines) | ||
|
||
if DEBUG: | ||
dbg = cv2.cvtColor(edges, cv2.cv.CV_GRAY2BGRA) | ||
# Draw detected lines in green. | ||
for x1, y1, x2, y2 in lines: | ||
cv2.line(dbg, (x1, y1), (x2, y2), (0, 255, 0, 255), 1) | ||
|
||
approaches = [ | ||
((0, 0, 255, 255), _get_dominant_angle(lines, MEAN)), | ||
((255, 0, 0, 255), _get_dominant_angle(lines, MEDIAN)), | ||
] | ||
|
||
print [a[1] for a in approaches] | ||
|
||
# Draws lines originating from the middle of left border with | ||
# computed slopes: MEAN in red, MEDIAN in blue. | ||
for color, angle in approaches: | ||
def y(x0, x): | ||
return max(-2**15, | ||
min(2**15, | ||
int(x0 - math.tan(angle) * x))) | ||
|
||
x0 = shred.shape[0]/2 | ||
x1 = 0 | ||
y1 = y(x0, x1) | ||
x2 = shred.shape[1] | ||
y2 = y(x0, x2) | ||
cv2.line(dbg, (x1, y1), (x2, y2), color, 1) | ||
|
||
dbg = numpy.concatenate([shred, dbg], 1) | ||
cv2.imwrite('../debug/%s_houghlines.png' % name, dbg) | ||
params['lines_angle'] = dominant_angle | ||
|
||
return params, tags | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that we should try kind of adaptive filtration to filter out false positives. For example (very stupid one, just out of the top of my head), we might use only lines that longer than 50% of a longest line found. Another fruitful idea here is to build histogram for angles (say, with bucket of 3-5 degrees), find the biggest peak and filter out everything except lines of this angle it and their neighbors + the same angle + 90 degrees There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Many false positives come from "wide" lines on the image. This gives a number of related detected lines, all within a marginal slope and intercept. Histogram of angles would likely work nice. Another approach might be using algorithm similar to RANSAC, to come up with a consensus on the lines angle and pick only matching ones (+-90º) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
"""Fancy adaptive threshlding. | ||
|
||
Code adapted from | ||
http://stackoverflow.com/questions/22122309/opencv-adaptive-threshold-ocr. | ||
|
||
As I understand it: | ||
1. Reduces an image to a smaller one, where each | ||
DEFAULT_BLOCKSIZExDEFAULT_BLOCKSIZE block -> one pixel. | ||
2. Creates a mask of small_image size, where pixels corresponding to | ||
high-variance (non-background) blocks get value > 0, others (background | ||
blocks) get 0. | ||
3. Small image is inpainted using mask from step 2. So non-bg blocks are | ||
inpainted by surrounding bg blocks. | ||
4. Image is resize back to original size, resulting in what looks like just bg | ||
from original image. | ||
5. Bg image is subtracted from original and result thresholded. | ||
|
||
The algorith assumes dark foreground on light background. | ||
|
||
""" | ||
import cv2 | ||
import numpy as np | ||
|
||
DEFAULT_BLOCKSIZE = 40 | ||
|
||
# Blocks with variance over this value are assumed to contain foreground. | ||
MEAN_VARIANCE_THRESHOLD = 0.01 | ||
|
||
|
||
def _calc_block_mean_variance(image, mask, blocksize): | ||
"""Adaptively determines image background. | ||
|
||
Args: | ||
image: image converted 1-channel image. | ||
mask: 1-channel mask, same size as image. | ||
blocksize: adaptive algorithm parameter. | ||
|
||
Returns: | ||
image of same size as input with foreground inpainted with background. | ||
""" | ||
I = image.copy() | ||
I_f = I.astype(np.float32) / 255. # Used for mean and std. | ||
|
||
result = np.zeros( | ||
(image.shape[0] / blocksize, image.shape[1] / blocksize), | ||
dtype=np.float32) | ||
|
||
for i in xrange(0, image.shape[0] - blocksize, blocksize): | ||
for j in xrange(0, image.shape[1] - blocksize, blocksize): | ||
|
||
patch = I_f[i:i+blocksize+1, j:j+blocksize+1] | ||
mask_patch = mask[i:i+blocksize+1, j:j+blocksize+1] | ||
|
||
tmp1 = np.zeros((blocksize, blocksize)) | ||
tmp2 = np.zeros((blocksize, blocksize)) | ||
mean, std_dev = cv2.meanStdDev(patch, tmp1, tmp2, mask_patch) | ||
|
||
value = 0 | ||
if std_dev[0][0] > MEAN_VARIANCE_THRESHOLD: | ||
value = mean[0][0] | ||
|
||
result[i/blocksize, j/blocksize] = value | ||
|
||
small_image = cv2.resize(I, (image.shape[1] / blocksize, | ||
image.shape[0] / blocksize)) | ||
|
||
res, inpaintmask = cv2.threshold(result, 0.02, 1, cv2.THRESH_BINARY) | ||
|
||
inpainted = cv2.inpaint(small_image, inpaintmask.astype(np.uint8), 5, | ||
cv2.INPAINT_TELEA) | ||
|
||
res = cv2.resize(inpainted, (image.shape[1], image.shape[0])) | ||
|
||
return res | ||
|
||
|
||
def threshold(image, block_size=DEFAULT_BLOCKSIZE, mask=None): | ||
"""Applies adaptive thresholding to the given image. | ||
|
||
Args: | ||
image: BGRA image. | ||
block_size: optional int block_size to use for adaptive thresholding. | ||
mask: optional mask. | ||
Returns: | ||
Thresholded image. | ||
""" | ||
if mask is None: | ||
mask = np.zeros(image.shape[:2], dtype=np.uint8) | ||
mask[:] = 255 | ||
|
||
if len(image.shape) > 2 and image.shape[2] == 4: | ||
image = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY) | ||
res = _calc_block_mean_variance(image, mask, block_size) | ||
res = image.astype(np.float32) - res.astype(np.float32) + 255 | ||
_, res = cv2.threshold(res, 215, 255, cv2.THRESH_BINARY) | ||
return res | ||
|
||
|
||
if __name__ == '__main__': | ||
import argparse | ||
|
||
parser = argparse.ArgumentParser() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whatta hell is going on here? We are using both argparse and sys.argv? In any case, I think import argparse and import sys should go under ifmain. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Merge fail. |
||
parser.add_argument('input', type=str, help='Input file name.', | ||
nargs='?', default="11.jpg") | ||
parser.add_argument('output', type=str, help='Output file name.', | ||
nargs='?', default="out.png") | ||
|
||
args = parser.parse_args() | ||
|
||
fname = args.input | ||
outfile = args.output | ||
|
||
image = cv2.imread(fname, cv2.CV_LOAD_IMAGE_UNCHANGED) | ||
result = threshold(image) | ||
cv2.imwrite(outfile, result * 255) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that it's better to apply this smart-ass thresholding to each shred in split.py and then pass it to feature detectors as we'll reuse it for sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some feature detectors might prefer non-thresholded images.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but I'm just saying that we should provide both to the feature detector.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like a method to memoize in Shred() class. Adding a TODO.