Major decoding algorithm alteration, working for all e2e tests
Find bounds using angles rather than skew. Get data along linear axis rather than looking for dot starts. Fixed unit tests, added testing all around.
This commit is contained in:
parent
4f8b9e1f94
commit
1d09fac9ff
|
@ -21,7 +21,8 @@ def encode(args):
|
|||
args.printerDpi,
|
||||
args.outPath,
|
||||
args.saveImages,
|
||||
args.noPdf)
|
||||
args.noPdf
|
||||
)
|
||||
|
||||
|
||||
def decode(args):
|
||||
|
@ -29,7 +30,9 @@ def decode(args):
|
|||
args.filenames,
|
||||
args.colorDepth,
|
||||
args.outfile,
|
||||
args.metadataFile)
|
||||
args.metadataFile,
|
||||
"." if args.debug else None # Save in current directory if debug option is specified
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -88,6 +91,8 @@ def main():
|
|||
help='Output filename of data file')
|
||||
decoder_parser.add_argument('--metadataFile', type=str, default="",
|
||||
help='Metadata filename')
|
||||
decoder_parser.add_argument('--debug', action='store_true', default=False,
|
||||
help='Debug output')
|
||||
|
||||
decoder_parser.set_defaults(func=decode)
|
||||
|
||||
|
|
|
@ -34,4 +34,4 @@ TotalPagesMaxBytes = 8 # 8 bytes per page maximum for the total-pages field
|
|||
MaxSkew = 5
|
||||
MaxSkewPerc = 0.002
|
||||
|
||||
DefaultThresholdWeight = 0.5
|
||||
DefaultThresholdWeight = 0.5
|
|
@ -4,7 +4,7 @@ import os
|
|||
from PIL import ImageDraw, Image
|
||||
|
||||
|
||||
def drawPage(page, tmpdir, filename, pixels=None, lines=None, color=(0, 0, 0)):
|
||||
def draw_page(page, tmpdir, filename, pixels=None, lines=None, color=(255, 0, 0)):
|
||||
"""
|
||||
Draw page with additional pixels and lines for debugging purposes
|
||||
:param page: InputPage type
|
||||
|
@ -20,14 +20,17 @@ def drawPage(page, tmpdir, filename, pixels=None, lines=None, color=(0, 0, 0)):
|
|||
# Draw image
|
||||
for y in range(page.height):
|
||||
for x in range(page.width):
|
||||
pixel = page.getPixel(int(y), int(x))
|
||||
pixel = page.get_pixel(int(y), int(x))
|
||||
pixel = tuple(map(operator.mul, pixel, (255,) * 3))
|
||||
pixel = tuple(map(int, pixel))
|
||||
image_pixels[int(x), int(y)] = pixel
|
||||
|
||||
if pixels:
|
||||
for y, x in pixels:
|
||||
image_pixels[x, y] = color
|
||||
for pixel in pixels:
|
||||
if pixel:
|
||||
y, x = pixel
|
||||
if page.width > x >= 0 and page.height > y >= 0:
|
||||
image_pixels[x, y] = color
|
||||
|
||||
if lines:
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
|
|
@ -1,30 +1,8 @@
|
|||
from colorsafe.csdatastructures import constants, DotByte, DotRow, Sector, Page, ColorSafeFile, MetadataSector, ColorChannels
|
||||
from colorsafe.csdatastructures import constants, DotByte, DotRow, Sector, Page, ColorSafeFile, MetadataSector
|
||||
from colorsafe.csdatastructures import Dot
|
||||
from colorsafe.utils import floatToBinaryList, intToBinaryList
|
||||
|
||||
|
||||
class InputPages:
|
||||
def __init__(self, totalPages, height, width):
|
||||
self.totalPages = totalPages
|
||||
self.height = height
|
||||
self.width = width
|
||||
|
||||
def getPagePixel(self, page, y, x):
|
||||
"""For caller to implement"""
|
||||
pass
|
||||
|
||||
|
||||
class InputPage:
|
||||
def __init__(self, pages, pageNum):
|
||||
self.pages = pages
|
||||
self.height = pages.height
|
||||
self.width = pages.width
|
||||
self.pageNum = pageNum
|
||||
|
||||
def getPixel(self, y, x):
|
||||
return self.pages.getPagePixel(self.pageNum, y, x)
|
||||
|
||||
|
||||
class DotDecoder(Dot):
|
||||
"""A group of channels representing a group of colorDepth bits.
|
||||
|
||||
|
@ -33,7 +11,7 @@ class DotDecoder(Dot):
|
|||
TODO: Extend to multiple shades
|
||||
"""
|
||||
|
||||
def __init__(self, channels, colorDepth, thresholdWeight=0.0):
|
||||
def __init__(self, channels, colorDepth, thresholdWeight):
|
||||
"""Takes in a list of channels, returns a list of bytes
|
||||
"""
|
||||
channelNum = self.getChannelNum(colorDepth)
|
||||
|
@ -68,9 +46,6 @@ class DotDecoder(Dot):
|
|||
val = channels.getAverageShade()
|
||||
|
||||
bitList = [int(val > thresholdWeight)]
|
||||
|
||||
# val = max(0.0, val - thresholdWeight)
|
||||
# bitList = floatToBinaryList(val, colorDepth)
|
||||
return bitList
|
||||
|
||||
def decodeSecondaryMode(self, channels, colorDepth):
|
||||
|
@ -110,7 +85,7 @@ class DotByteDecoder(DotByte):
|
|||
"""A group of 8 Dots, representing colorDepth bytes of data.
|
||||
"""
|
||||
|
||||
def __init__(self, channelsList, colorDepth, thresholdWeight=0.0):
|
||||
def __init__(self, channelsList, colorDepth, thresholdWeight):
|
||||
"""Takes in a list of exactly ByteSize (8) channels, returns a list of decoded bytes.
|
||||
|
||||
Sets each dot's decoded data into colorDepth bytes.
|
||||
|
@ -142,7 +117,7 @@ class DotRowDecoder(DotRow):
|
|||
colorDepth,
|
||||
width,
|
||||
rowNumber,
|
||||
thresholdWeight=0.0,
|
||||
thresholdWeight,
|
||||
xorRow=True):
|
||||
"""Takes in a list of width channels, returns a list of decoded bytes
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,610 @@
|
|||
import itertools
|
||||
import operator
|
||||
import os
|
||||
import sys
|
||||
from copy import copy
|
||||
|
||||
from colorsafe.debugutils import draw_page
|
||||
|
||||
from colorsafe import utils, defaults, exceptions
|
||||
|
||||
|
||||
def get_data_bounds(page, sector_height, sector_width, gap_size, tmpdir):
|
||||
if tmpdir:
|
||||
tmpdir_bounds = os.path.join(str(tmpdir), "bounds")
|
||||
os.mkdir(tmpdir_bounds)
|
||||
tmpdir = tmpdir_bounds
|
||||
|
||||
bounds = get_bounds(page, tmpdir)
|
||||
|
||||
data_bounds = list()
|
||||
debug_data_bounds = list()
|
||||
|
||||
for top_temp, bottom_temp, left_temp, right_temp in bounds:
|
||||
|
||||
height_per_dot = float(bottom_temp - top_temp + 1) / (sector_height + 2 * gap_size)
|
||||
width_per_dot = float(right_temp - left_temp + 1) / (sector_width + 2 * gap_size)
|
||||
|
||||
if height_per_dot < 1.0 or width_per_dot < 1.0:
|
||||
raise exceptions.DecodingError("Image has less than 1.0x resolution, cannot get all dots.")
|
||||
|
||||
top, bottom, left, right = get_real_sector_data_boundaries(page,
|
||||
height_per_dot,
|
||||
width_per_dot,
|
||||
top_temp,
|
||||
bottom_temp,
|
||||
left_temp,
|
||||
right_temp)
|
||||
|
||||
data_bounds.append((top, bottom, left, right))
|
||||
|
||||
if tmpdir:
|
||||
debug_data_bounds.extend([(top, left), (top, right), (bottom, left), (bottom, right)])
|
||||
|
||||
if tmpdir:
|
||||
draw_page(page, tmpdir, "data_bounds", tuple(debug_data_bounds), None)
|
||||
|
||||
return data_bounds
|
||||
|
||||
|
||||
def get_bounds(page, tmpdir):
|
||||
# Calculate vertical bounds first - more accurate since data typically extends the entire width of a page
|
||||
page_y_begin = find_beginning_or_ending(page, True, False)
|
||||
page_y_end = find_beginning_or_ending(page, True, True)
|
||||
page_x_begin = find_beginning_or_ending(page, False, False, page_y_begin, page_y_end)
|
||||
page_x_end = find_beginning_or_ending(page, False, True, page_y_begin, page_y_end)
|
||||
|
||||
page_y_length = page_y_end - page_y_begin
|
||||
page_x_length = page_x_end - page_x_begin
|
||||
|
||||
# TODO: Come up a better heuristic
|
||||
vertical_page_subdivisions = 4 + max(page_y_length - 128, 0) / 128
|
||||
horizontal_page_subdivisions = 4 + max(page_x_length - 128, 0) / 128
|
||||
|
||||
horizontal_sub_borders_all = list()
|
||||
for y_sub in range(vertical_page_subdivisions):
|
||||
y_min = int(page_y_length * float(y_sub) / vertical_page_subdivisions) + page_y_begin
|
||||
y_max = int(page_y_length * float(y_sub + 1) / vertical_page_subdivisions) + page_y_begin
|
||||
x_min = page_x_begin
|
||||
x_max = page_x_end
|
||||
subset_page_bounds = (x_min, x_max, y_min, y_max)
|
||||
horizontal_sub_borders = find_border_points_subset_page(page, subset_page_bounds, False)
|
||||
horizontal_sub_borders_all.append(horizontal_sub_borders)
|
||||
|
||||
vertical_sub_borders_all = list()
|
||||
for x_sub in range(horizontal_page_subdivisions):
|
||||
x_min = int(page_x_length * float(x_sub) / horizontal_page_subdivisions) + page_x_begin
|
||||
x_max = int(page_x_length * float(x_sub + 1) / horizontal_page_subdivisions) + page_x_begin
|
||||
y_min = page_y_begin
|
||||
y_max = page_y_end
|
||||
subset_page_bounds = (y_min, y_max, x_min, x_max)
|
||||
vertical_sub_borders = find_border_points_subset_page(page, subset_page_bounds, True)
|
||||
vertical_sub_borders_all.append(vertical_sub_borders)
|
||||
|
||||
clean_vertical_borders = clean_and_infer_borders(vertical_sub_borders_all, False)
|
||||
clean_horizontal_borders = clean_and_infer_borders(horizontal_sub_borders_all, True)
|
||||
|
||||
# Transpose lists so each borders points are within 1 list, not spread across all lists
|
||||
# NOTE: Transposing turns the vertical sub-borders into a list of horizontal lines, and vice-versa
|
||||
# TODO: Need to either tranpose by matching like values, or else work on inferring missing borders above
|
||||
# TODO: Left off here
|
||||
|
||||
horizontal_borders = transpose_and_infer(clean_vertical_borders, True)
|
||||
vertical_borders = transpose_and_infer(clean_horizontal_borders, False)
|
||||
|
||||
horizontal_border_angles_lines = infer_border_angled_lines(horizontal_borders, False)
|
||||
vertical_border_angled_lines = infer_border_angled_lines(vertical_borders, True)
|
||||
|
||||
intersections = get_intersections(vertical_border_angled_lines, horizontal_border_angles_lines)
|
||||
|
||||
bounds = list()
|
||||
|
||||
for top_left, top_right, bottom_left, bottom_right in intersections:
|
||||
# TODO: These seem mixed up, but it works...
|
||||
left = utils.average([top_left[0], top_right[0]])
|
||||
right = utils.average([bottom_left[0], bottom_right[0]])
|
||||
top = utils.average([top_left[1], bottom_left[1]])
|
||||
bottom = utils.average([top_right[1], bottom_right[1]])
|
||||
bounds.append((top, bottom, left, right))
|
||||
|
||||
if tmpdir:
|
||||
dots = list()
|
||||
for h in horizontal_borders:
|
||||
dots.extend(h)
|
||||
draw_page(page, tmpdir, "horizontal_borders", tuple(dots), None)
|
||||
|
||||
dots = list()
|
||||
for v in vertical_borders:
|
||||
dots.extend(v)
|
||||
draw_page(page, tmpdir, "vertical_borders", tuple(dots), None)
|
||||
|
||||
f = open(os.path.join(tmpdir, "get_bounds_data.txt"), "w")
|
||||
|
||||
f.write("page_y_begin " + str(page_y_begin))
|
||||
f.write("\rpage_y_end " + str(page_y_end))
|
||||
f.write("\rpage_x_begin " + str(page_x_begin))
|
||||
f.write("\rpage_x_end " + str(page_x_end))
|
||||
|
||||
f.write("\r\rhorizontal_sub_borders_all " + str(horizontal_sub_borders_all))
|
||||
f.write("\rvertical_sub_borders_all " + str(vertical_sub_borders_all))
|
||||
|
||||
f.write("\r\rclean_vertical_borders " + str(clean_vertical_borders))
|
||||
f.write("\rclean_horizontal_borders " + str(clean_horizontal_borders))
|
||||
|
||||
f.write("\r\rvertical_borders " + str(vertical_borders))
|
||||
f.write("\rhorizontal_borders " + str(horizontal_borders))
|
||||
|
||||
f.write("\r\rhorizontal_border_angles_lines " + str(horizontal_border_angles_lines))
|
||||
f.write("\rvertical_border_angled_lines " + str(vertical_border_angled_lines))
|
||||
|
||||
f.write("\r\rintersections " + str(intersections))
|
||||
|
||||
f.write("\r\rbounds " + str(bounds))
|
||||
|
||||
f.close()
|
||||
|
||||
border_coordinates = list()
|
||||
for slope, intercept in horizontal_border_angles_lines:
|
||||
y1 = intercept
|
||||
y2 = intercept + slope*page.width
|
||||
border_coordinates.append((y1, 0, y2, page.width-1))
|
||||
|
||||
for slope, intercept in vertical_border_angled_lines:
|
||||
x1 = intercept
|
||||
x2 = intercept + slope*page.height
|
||||
border_coordinates.append((0, x1, page.height-1, x2))
|
||||
|
||||
draw_page(page, tmpdir, "border_lines", None, tuple(border_coordinates))
|
||||
|
||||
if tmpdir:
|
||||
converted_bounds = list()
|
||||
for y1, y2, x1, x2 in bounds:
|
||||
converted_bounds.extend([(y1, x1), (y1, x2), (y2, x1), (y2, x2)])
|
||||
draw_page(page, tmpdir, "bounds", tuple(converted_bounds), None)
|
||||
|
||||
return bounds
|
||||
|
||||
|
||||
def find_beginning_or_ending(page, vertical, reverse, perp_min_override=None, perp_max_override=None):
|
||||
"""
|
||||
:param page: Page to find beginning (or ending) of
|
||||
:param vertical: True if vertical beginning (or ending), False if horizontal
|
||||
:param reverse: True if beginning, False if ending
|
||||
:return: Coordinate of beginning (or ending)
|
||||
"""
|
||||
low_border_threshold = 0.80
|
||||
max_skew = 3
|
||||
|
||||
along_min = 0
|
||||
perp_min = perp_min_override if perp_min_override else 0
|
||||
|
||||
along_max = page.height if vertical else page.width
|
||||
perp_max = perp_max_override if perp_max_override else page.width if vertical else page.height
|
||||
|
||||
# TODO: Replace the first part with page.get_perpendicular_shade_averages()
|
||||
along_range = range(along_min, along_max)
|
||||
if reverse:
|
||||
along_range = along_range[::-1]
|
||||
|
||||
for along_iter in along_range:
|
||||
perp_sum = 0
|
||||
for perp_iter in range(perp_min, perp_max):
|
||||
y = along_iter if vertical else perp_iter
|
||||
x = perp_iter if vertical else along_iter
|
||||
|
||||
shade = utils.average(page.get_pixel(y, x))
|
||||
perp_sum += shade
|
||||
perp_avg = perp_sum / (perp_max - perp_min)
|
||||
|
||||
if perp_avg < low_border_threshold:
|
||||
# Add a buffer equal to the max skew.
|
||||
along_border = along_iter + (1 if reverse else -1) * max_skew
|
||||
|
||||
# Don't exceed the page boundaries.
|
||||
along_border = max(min(along_border, (page.height if vertical else page.width) - 1), 0)
|
||||
|
||||
return along_border
|
||||
|
||||
# TODO: Throw decoding error
|
||||
return -1
|
||||
|
||||
|
||||
def find_border_points_subset_page(page, sub_bounds, vertical):
|
||||
low_border_threshold = 0.20
|
||||
min_border_difference = 32
|
||||
|
||||
borders = list()
|
||||
|
||||
along_min, along_max, perp_min, perp_max = sub_bounds
|
||||
|
||||
border_started = False
|
||||
last_perp_avg = -1
|
||||
|
||||
# TODO: Replace the first part with page.get_perpendicular_shade_averages()
|
||||
along_iter = along_min
|
||||
while along_iter <= along_max:
|
||||
perp_sum = 0
|
||||
for perp_iter in range(perp_min, perp_max + 1):
|
||||
y = along_iter if vertical else perp_iter
|
||||
x = perp_iter if vertical else along_iter
|
||||
|
||||
shade = utils.average(page.get_pixel(y, x))
|
||||
perp_sum += shade
|
||||
perp_avg = perp_sum / (perp_max - perp_min + 1)
|
||||
|
||||
# If the border has started, find a local darkest point in the border
|
||||
if border_started and perp_avg > last_perp_avg:
|
||||
border_started = False
|
||||
|
||||
along_coordinate = along_iter - 1
|
||||
perp_coordinate = (perp_max + perp_min) / 2
|
||||
|
||||
y = along_coordinate if vertical else perp_coordinate
|
||||
x = perp_coordinate if vertical else along_coordinate
|
||||
borders.append((y, x))
|
||||
|
||||
along_iter += min_border_difference - 1
|
||||
continue
|
||||
|
||||
# Check darkness to start the border (or if we're at the end of the range, end on a border)
|
||||
if perp_avg <= low_border_threshold:
|
||||
if along_iter == along_max:
|
||||
along_coordinate = along_iter
|
||||
perp_coordinate = (perp_max + perp_min) / 2
|
||||
|
||||
y = along_coordinate if vertical else perp_coordinate
|
||||
x = perp_coordinate if vertical else along_coordinate
|
||||
borders.append((y, x))
|
||||
|
||||
break
|
||||
|
||||
border_started = True
|
||||
last_perp_avg = perp_avg
|
||||
|
||||
along_iter += 1
|
||||
|
||||
return borders
|
||||
|
||||
|
||||
def clean_and_infer_borders(sub_borders_all, vertical):
|
||||
# Transpose lists so each borders points are within 1 list, not spread across all lists
|
||||
# TODO: Clean up and infer sub borders before transposing - unequal lists are clipped.
|
||||
|
||||
initial_points = list()
|
||||
perp_points = list()
|
||||
new_sub_borders_all = list()
|
||||
along_diffs_all = list()
|
||||
for sub_borders in sub_borders_all:
|
||||
if not sub_borders or not len(sub_borders):
|
||||
continue
|
||||
|
||||
initial_points.append(sub_borders[0][1] if vertical else sub_borders[0][0])
|
||||
perp_points.append(sub_borders[0][0] if vertical else sub_borders[0][1])
|
||||
|
||||
along_diffs = list()
|
||||
along_points = list()
|
||||
for y, x in sub_borders:
|
||||
along = x if vertical else y
|
||||
|
||||
if len(along_points):
|
||||
along_diffs.append(along - along_points[-1])
|
||||
|
||||
along_points.append(along)
|
||||
|
||||
along_diffs_all.append(along_diffs)
|
||||
|
||||
along_diffs_flat = reduce(operator.concat, along_diffs_all)
|
||||
if len(along_diffs_flat) < 3:
|
||||
return map(list, zip(*sub_borders_all))
|
||||
|
||||
along_diffs_flat_std = utils.standard_deviation_squared(along_diffs_flat)
|
||||
|
||||
# Begin clean-up - mark boundaries that can be merged
|
||||
# TODO: This needs to account for multiple merged diffs
|
||||
# e.g. differentiate [25,75,27,75,100,100,100] and [25,25,25,25,100,100,100]
|
||||
for all_iter, along_diffs in enumerate(along_diffs_all):
|
||||
along_diffs_others = reduce(operator.concat, utils.remove_index(along_diffs_all, all_iter))
|
||||
|
||||
if len(along_diffs) < 2:
|
||||
new_sub_borders_all.append(sub_borders_all[all_iter])
|
||||
continue
|
||||
|
||||
# Combine two sequential along-diff values if they reduce the standard deviation of the list
|
||||
merge_indexes = list()
|
||||
|
||||
for along_iter in range(1, len(along_diffs)):
|
||||
diff = along_diffs[along_iter]
|
||||
diff_previous = along_diffs[along_iter - 1]
|
||||
|
||||
# Merge diff1 and diff2 in the list
|
||||
lrem = utils.remove_index(along_diffs, along_iter - 1)
|
||||
lrem[along_iter - 1] = diff_previous + diff
|
||||
|
||||
# If merging those diffs would lower the overall standard deviation, then mark it for merge
|
||||
# TODO: Refine tolerance factor
|
||||
tolerance = 0.9
|
||||
if utils.standard_deviation_squared(lrem + along_diffs_others) < along_diffs_flat_std * tolerance:
|
||||
if not (len(merge_indexes) and merge_indexes[-1] == along_iter - 1):
|
||||
merge_indexes.append(along_iter)
|
||||
|
||||
along_diffs_clean = copy(along_diffs)
|
||||
|
||||
# Merge diffs
|
||||
# TODO: Could probably just use along_points
|
||||
i = 0
|
||||
while i < len(merge_indexes):
|
||||
index = merge_indexes[i]
|
||||
add = along_diffs_clean.pop(index-1)
|
||||
along_diffs_clean[index-1] += add
|
||||
merge_indexes = map(lambda x: x - 1, merge_indexes)
|
||||
|
||||
i += 1
|
||||
|
||||
# Turn diffs to new along points
|
||||
new_along_points = [initial_points[all_iter]]
|
||||
for i in range(len(along_diffs_clean)):
|
||||
new_along_points.append(sum(along_diffs_clean[:i+1]) + initial_points[all_iter])
|
||||
|
||||
if vertical:
|
||||
new_sub_borders = map(lambda x: (perp_points[all_iter], x), new_along_points)
|
||||
else:
|
||||
new_sub_borders = map(lambda y: (y, perp_points[all_iter]), new_along_points)
|
||||
|
||||
new_sub_borders_all.append(new_sub_borders)
|
||||
|
||||
return new_sub_borders_all
|
||||
|
||||
|
||||
def transpose_and_infer(borders_all, vertical):
|
||||
max_val = 0
|
||||
min_val = sys.maxint
|
||||
for borders in borders_all:
|
||||
for y, x in borders:
|
||||
max_val = max(y if vertical else x, max_val)
|
||||
min_val = min(y if vertical else x, min_val)
|
||||
|
||||
approximate_max_sectors = 15.0
|
||||
|
||||
max_diff = (max_val - min_val) / (2.0 * approximate_max_sectors)
|
||||
|
||||
transposed_list = list()
|
||||
for borders in borders_all:
|
||||
for y, x in borders:
|
||||
coord = (y, x)
|
||||
val = y if vertical else x
|
||||
|
||||
inserted = False
|
||||
for inserted_borders_index in range(len(transposed_list)):
|
||||
inserted_borders = transposed_list[inserted_borders_index]
|
||||
for y_ins, x_ins in inserted_borders:
|
||||
ins_val = y_ins if vertical else x_ins
|
||||
if ins_val - max_diff < val < ins_val + max_diff:
|
||||
transposed_list[inserted_borders_index].append(coord)
|
||||
|
||||
inserted = True
|
||||
break
|
||||
if inserted:
|
||||
break
|
||||
|
||||
if not inserted:
|
||||
transposed_list.append([coord])
|
||||
|
||||
# Sort first by second value, then by first
|
||||
sorted_transposed_list = sorted(transposed_list, key=lambda val: val[0][0 if vertical else 1])
|
||||
for i in range(len(sorted_transposed_list)):
|
||||
sorted_transposed_list[i] = sorted(sorted_transposed_list[i], key=lambda val: val[1 if vertical else 0])
|
||||
|
||||
return sorted_transposed_list
|
||||
|
||||
|
||||
def remove_outliers(l):
|
||||
"""
|
||||
Remove all values in l whose presence increases the overall standard deviation of the list
|
||||
:param l: Input list
|
||||
:return: Corrected list
|
||||
"""
|
||||
MIN_LIST_LENGTH = 2
|
||||
|
||||
if len(l) <= MIN_LIST_LENGTH:
|
||||
return l
|
||||
|
||||
tolerance = 0.95 # TODO: Determine this heuristically
|
||||
|
||||
# Sort by increasing distance from mean, since only removing the largest outliers in a list affects its variance.
|
||||
l_med = utils.median(l)
|
||||
l = sorted(l, key=lambda x: abs(x - l_med))
|
||||
|
||||
std = utils.standard_deviation_squared(l)
|
||||
for i in range(0, len(l))[::-1]:
|
||||
newstd = utils.standard_deviation_squared(utils.remove_index(l, i))
|
||||
if newstd < std * tolerance:
|
||||
l.pop(i)
|
||||
|
||||
if len(l) <= MIN_LIST_LENGTH:
|
||||
break
|
||||
|
||||
std = utils.standard_deviation_squared(l)
|
||||
continue
|
||||
|
||||
return l
|
||||
|
||||
|
||||
def infer_border_angled_lines(border_points_list, vertical):
|
||||
"""
|
||||
Get slope and x-intercept if vertical border points, or slope and y-intercept if horizontal.
|
||||
|
||||
This method currently uses a simple average.
|
||||
|
||||
TODO: A least squares fit would be more accurate (removing outlier influence, for example)
|
||||
|
||||
:param border_points_list: List of border points, e.g. [[(0,10),(0,20),(0,30)],[(101,10),(102,20),(103,30)]]
|
||||
:return: List of inferred angled lines, params slope and intercept, e.g. [(0,0), (0.1,100)]
|
||||
"""
|
||||
border_angled_lines = list()
|
||||
for border_points in border_points_list:
|
||||
while None in border_points:
|
||||
border_points.remove(None)
|
||||
|
||||
if not border_points or not len(border_points) > 1:
|
||||
continue
|
||||
|
||||
slope_list = list()
|
||||
intercept_list = list()
|
||||
|
||||
# Don't just look at adjacent pairs, which could have small along-differences that amplify the slope
|
||||
# Look at all n*(n-1) combinations of points, which are more likely to be far apart with smaller slopes
|
||||
for i in range(len(border_points) - 1):
|
||||
for j in range(i+1, len(border_points)):
|
||||
y1, x1 = border_points[i]
|
||||
y2, x2 = border_points[j]
|
||||
if vertical:
|
||||
if y1 != y2:
|
||||
slope = float(x2 - x1) / float(y2 - y1)
|
||||
slope_list.append(slope)
|
||||
intercept_list.append(x1 - slope * y1)
|
||||
else:
|
||||
if x1 != x2:
|
||||
slope = float(y2 - y1) / float(x2 - x1)
|
||||
slope_list.append(slope)
|
||||
intercept_list.append(y1 - slope * x1)
|
||||
|
||||
corrected_slope_list = remove_outliers(slope_list) if len(slope_list) > 2 else slope_list
|
||||
corrected_intercept_list = remove_outliers(intercept_list) if len(intercept_list) > 2 else intercept_list
|
||||
|
||||
slope = utils.average(corrected_slope_list)
|
||||
intercept = utils.average(corrected_intercept_list)
|
||||
|
||||
border_angled_lines.append((slope, intercept))
|
||||
|
||||
return border_angled_lines
|
||||
|
||||
|
||||
def get_coordinates(v_value, h_value):
|
||||
vertical_slope, vertical_intercept = v_value
|
||||
horizontal_slope, horizontal_intercept = h_value
|
||||
|
||||
y = vertical_slope * horizontal_intercept + vertical_intercept / (
|
||||
1 - vertical_slope * horizontal_slope)
|
||||
x = horizontal_slope * vertical_intercept + horizontal_intercept / (
|
||||
1 - vertical_slope * horizontal_slope)
|
||||
|
||||
return int(y), int(x)
|
||||
|
||||
|
||||
def get_intersections(vertical_border_angled_lines, horizontal_border_angles_lines):
|
||||
"""
|
||||
y = vertical_slope * x + vertical_intercept
|
||||
x = horizontal_slope * y + horizontal_intercept
|
||||
|
||||
Solving for these gives the intersection points:
|
||||
y = vertical_slope * horizontal_intercept + vertical_intercept / (1 - vertical_slope * horizontal_slope)
|
||||
x = horizontal_slope * vertical_intercept + horizontal_intercept / (1 - vertical_slope * horizontal_slope)
|
||||
|
||||
:param vertical_border_angled_lines: List of vertical slopes and intercepts
|
||||
:param horizontal_border_angles_lines: List of horizontal slopes and intercepts
|
||||
:return: List of intersection points
|
||||
"""
|
||||
|
||||
intersections = list()
|
||||
for h_index, h_value in enumerate(horizontal_border_angles_lines[1:], 1):
|
||||
for v_index, v_value in enumerate(vertical_border_angled_lines[1:], 1):
|
||||
v_value_prev = vertical_border_angled_lines[v_index - 1]
|
||||
h_value_prev = horizontal_border_angles_lines[h_index - 1]
|
||||
|
||||
top_left = get_coordinates(v_value_prev, h_value_prev)
|
||||
top_right = get_coordinates(v_value_prev, h_value)
|
||||
bottom_left = get_coordinates(v_value, h_value_prev)
|
||||
bottom_right = get_coordinates(v_value, h_value)
|
||||
|
||||
intersections.append((top_left, top_right, bottom_left, bottom_right))
|
||||
|
||||
return intersections
|
||||
|
||||
|
||||
def get_real_sector_data_boundary(page, leastAlong, mostAlong, leastPerp, mostPerp, vertical, reverse):
|
||||
"""Search within given rough sector bounds and return the true coordinate of the gap
|
||||
E.g. if looking for the real top gap coordinate, along is y and perp is x. Return y.
|
||||
"""
|
||||
perp_shades = page.get_perpendicular_shade_averages(leastAlong, mostAlong, leastPerp, mostPerp, vertical, reverse)
|
||||
|
||||
dataIndex = 0
|
||||
|
||||
brightestShade = max(perp_shades)
|
||||
firstGapIndex = perp_shades.index(brightestShade)
|
||||
darkestShadeAfterGap = min(perp_shades[firstGapIndex:])
|
||||
|
||||
if brightestShade == darkestShadeAfterGap:
|
||||
return dataIndex
|
||||
|
||||
# TODO: Improve this value
|
||||
gapToDataTolerance = 0.25
|
||||
|
||||
# Get the closest value that has a sizeable drop from the max of all previous shades
|
||||
# This only works if the initial shade is assumed to be the darkest part of the border
|
||||
for perpIndex, perpShade in enumerate(perp_shades[firstGapIndex:], firstGapIndex):
|
||||
thresholdedPerpShade = utils.threshold(perpShade, brightestShade, darkestShadeAfterGap)
|
||||
if thresholdedPerpShade < 1 - gapToDataTolerance:
|
||||
dataIndex = perpIndex
|
||||
break
|
||||
|
||||
dataBound = dataIndex + leastAlong if not reverse else mostAlong - dataIndex
|
||||
|
||||
return dataBound
|
||||
|
||||
|
||||
def get_real_sector_data_boundaries(page, heightPerDot, widthPerDot, topmost, bottommost, leftmost, rightmost):
|
||||
"""Find real data boundaries.
|
||||
Look within two-dot unit of pixels away, only looking inwards since input bounds will be within a border
|
||||
"""
|
||||
|
||||
# Search just past the boundary and gaps
|
||||
max_dots_away = defaults.borderSize + defaults.gapSize + 2
|
||||
|
||||
bottommostTop = topmost + int(round(max_dots_away * heightPerDot))
|
||||
topmostBottom = bottommost - int(round(max_dots_away * heightPerDot))
|
||||
rightmostLeft = leftmost + int(round(max_dots_away * widthPerDot))
|
||||
leftmostRight = rightmost - int(round(max_dots_away * widthPerDot))
|
||||
|
||||
# TODO: First find gap, then find data (or else a blurry line could count as data)
|
||||
top = get_real_sector_data_boundary(
|
||||
page,
|
||||
topmost,
|
||||
bottommostTop,
|
||||
rightmostLeft,
|
||||
leftmostRight,
|
||||
True,
|
||||
False)
|
||||
|
||||
bottom = get_real_sector_data_boundary(
|
||||
page,
|
||||
topmostBottom,
|
||||
bottommost,
|
||||
rightmostLeft,
|
||||
leftmostRight,
|
||||
True,
|
||||
True)
|
||||
|
||||
left = get_real_sector_data_boundary(
|
||||
page,
|
||||
leftmost,
|
||||
rightmostLeft,
|
||||
bottommostTop,
|
||||
topmostBottom,
|
||||
False,
|
||||
False)
|
||||
|
||||
right = get_real_sector_data_boundary(
|
||||
page,
|
||||
leftmostRight,
|
||||
rightmost,
|
||||
bottommostTop,
|
||||
topmostBottom,
|
||||
False,
|
||||
True)
|
||||
|
||||
top = top if top else topmost
|
||||
bottom = bottom if bottom else bottommost
|
||||
left = left if left else leftmost
|
||||
right = right if right else rightmost
|
||||
|
||||
return top, bottom, left, right
|
|
@ -0,0 +1,148 @@
|
|||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
from colorsafe.debugutils import draw_page
|
||||
|
||||
from colorsafe import exceptions, utils
|
||||
from colorsafe.csdatastructures import ColorChannels
|
||||
|
||||
|
||||
def get_normalized_channels_list(page, data_bounds, sector_height, sector_width, sectorNum, tmpdir):
|
||||
if tmpdir:
|
||||
tmpdir_bounds = os.path.join(str(tmpdir), "channels")
|
||||
try:
|
||||
os.mkdir(tmpdir_bounds)
|
||||
except OSError:
|
||||
pass
|
||||
tmpdir = tmpdir_bounds
|
||||
|
||||
top, bottom, left, right = data_bounds
|
||||
|
||||
channels_list = get_channels_list(page,
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
sector_height,
|
||||
sector_width,
|
||||
sectorNum,
|
||||
tmpdir)
|
||||
|
||||
normalized_channels_list = normalizeChannelsList(channels_list)
|
||||
|
||||
if (tmpdir):
|
||||
f = open(os.path.join(tmpdir, "normalized_channels_" + str(sectorNum) + ".txt"), "w")
|
||||
for i in channels_list:
|
||||
f.write(str(i.getChannels()) + "\r")
|
||||
f.close()
|
||||
|
||||
return normalized_channels_list
|
||||
|
||||
|
||||
def get_channels_list(page, top, bottom, left, right, sector_height, sector_width, sector_num, tmpdir):
|
||||
# TODO: Use bilinear interpolation to get pixel values instead
|
||||
total_pixels_height = bottom - top + 1
|
||||
total_pixels_width = right - left + 1
|
||||
|
||||
pixels_per_dot_width = float(total_pixels_height) / float(sector_height)
|
||||
pixels_per_dot_height = float(total_pixels_width) / float(sector_width)
|
||||
|
||||
if tmpdir:
|
||||
all_pixels_and_weight = list()
|
||||
|
||||
# For each dot in the sector
|
||||
channels_list = list()
|
||||
for y in range(sector_height):
|
||||
for x in range(sector_width):
|
||||
# Center halfway through the dot, y + 0.5 and x + 0.5
|
||||
y_center = pixels_per_dot_height * (y + 0.5) + top
|
||||
x_center = pixels_per_dot_width * (x + 0.5) + left
|
||||
|
||||
y_min = y_center - pixels_per_dot_height / 2
|
||||
y_max = y_center + pixels_per_dot_height / 2
|
||||
x_min = x_center - pixels_per_dot_width / 2
|
||||
x_max = x_center + pixels_per_dot_width / 2
|
||||
|
||||
pixels_and_weight = list()
|
||||
weight_sum = 0.0
|
||||
|
||||
y_pixel_min = int(math.floor(y_min))
|
||||
y_pixel_max = int(math.floor(y_max))
|
||||
x_pixel_min = int(math.floor(x_min))
|
||||
x_pixel_max = int(math.floor(x_max))
|
||||
for y_pixel in range(y_pixel_min, y_pixel_max + 1):
|
||||
for x_pixel in range(x_pixel_min, x_pixel_max + 1):
|
||||
pixel = page.get_pixel(y_pixel, x_pixel)
|
||||
|
||||
weight = 1.0
|
||||
|
||||
if y_pixel > y_max - 1:
|
||||
weight *= (y_max % 1)
|
||||
|
||||
if y_pixel < y_min:
|
||||
weight *= ((1 - y_min) % 1)
|
||||
|
||||
if x_pixel > x_max - 1:
|
||||
weight *= (x_max % 1)
|
||||
|
||||
if x_pixel < x_min:
|
||||
weight *= ((1 - x_min) % 1)
|
||||
|
||||
weight_sum += weight
|
||||
|
||||
pixels_and_weight.append((pixel, weight, y_pixel, x_pixel))
|
||||
|
||||
if tmpdir:
|
||||
all_pixels_and_weight.append((y, x, pixels_and_weight))
|
||||
|
||||
number_of_channels = len(page.get_pixel(0, 0))
|
||||
channels_sum = [0] * number_of_channels
|
||||
|
||||
for pixel, weight, y_pixel, x_pixel in pixels_and_weight:
|
||||
for i in range(number_of_channels):
|
||||
channels_sum[i] += pixel[i] * weight
|
||||
|
||||
channels_avg = map(lambda i: i / weight_sum, channels_sum)
|
||||
channels_list.append(channels_avg)
|
||||
|
||||
if tmpdir:
|
||||
f = open(os.path.join(tmpdir, "all_pixels_and_weight_" + str(sector_num) + ".txt"), "w")
|
||||
for y, x, pixels_and_weight in all_pixels_and_weight:
|
||||
f.write(str(y) + "," + str(x) + ":\r")
|
||||
for i in pixels_and_weight:
|
||||
f.write(" " + str(i) + "\r")
|
||||
f.close()
|
||||
|
||||
color_channels_list = map(lambda i: ColorChannels(*i), channels_list)
|
||||
|
||||
return color_channels_list
|
||||
|
||||
|
||||
def normalizeChannelsList(channelsList):
|
||||
minVals = [1.0, 1.0, 1.0]
|
||||
maxVals = [0.0, 0.0, 0.0]
|
||||
|
||||
# Get min and max vals for normalization
|
||||
for c in channelsList:
|
||||
vals = c.getChannels()
|
||||
for i, val in enumerate(vals):
|
||||
if val < minVals[i]:
|
||||
minVals[i] = val
|
||||
if val > maxVals[i]:
|
||||
maxVals[i] = val
|
||||
|
||||
normalizedChannelsList = list()
|
||||
for i, channels in enumerate(channelsList):
|
||||
minVal = sum(minVals) / len(minVals)
|
||||
maxVal = sum(maxVals) / len(maxVals)
|
||||
|
||||
if minVal == maxVal:
|
||||
raise exceptions.DecodingError("No variance detected in the data. All channels are the same color.")
|
||||
|
||||
channels.subtractShade(minVal)
|
||||
channels.multiplyShade([1.0 / (maxVal - minVal)])
|
||||
|
||||
normalizedChannelsList.append(channels)
|
||||
|
||||
return normalizedChannelsList
|
|
@ -1,7 +1,7 @@
|
|||
from PIL import Image
|
||||
import re
|
||||
|
||||
from colorsafe.decoder.csdecoder import InputPages
|
||||
from colorsafe.decoder.csinput_page import InputPages
|
||||
from colorsafe.decoder.csimages_decoder import ColorSafeImagesDecoder
|
||||
|
||||
MaxColorVal = 255
|
||||
|
|
|
@ -2,10 +2,12 @@ import os
|
|||
|
||||
from unireedsolomon import RSCoder, RSCodecError
|
||||
|
||||
from colorsafe import constants, defaults, exceptions, utils
|
||||
from colorsafe.csdatastructures import ColorSafeImages, ColorChannels, Sector, DotRow
|
||||
from colorsafe.debugutils import drawPage
|
||||
from colorsafe.decoder.csdecoder import SectorDecoder, InputPage
|
||||
from colorsafe import constants, defaults, utils
|
||||
from colorsafe.csdatastructures import ColorSafeImages, Sector, DotRow
|
||||
from colorsafe.decoder.csdecoder import SectorDecoder
|
||||
from colorsafe.decoder.csdecoder_getbounds import get_data_bounds
|
||||
from colorsafe.decoder.csdecoder_getchannels import get_normalized_channels_list
|
||||
from colorsafe.decoder.csinput_page import InputPage
|
||||
|
||||
|
||||
class ColorSafeImagesDecoder(ColorSafeImages):
|
||||
|
@ -30,48 +32,6 @@ class ColorSafeImagesDecoder(ColorSafeImages):
|
|||
for pageNum in range(pages.totalPages):
|
||||
page = InputPage(pages, pageNum)
|
||||
|
||||
# TODO: Use angles, not skew, for a more granular accuracy.
|
||||
|
||||
# Get skews
|
||||
verticalSkew = self.findSkew(page, True)
|
||||
horizontalSkew = self.findSkew(page, False)
|
||||
|
||||
# Vertical bounds are more accurate since the data will extend to the horizontal end of the page.
|
||||
# Retrieve them first and use them to clip the horizontal bound region to not check an empty page bottom.
|
||||
# TODO: Clip at the outer bounds of the image, for best accuracy. Scan could have whitespace to either side.
|
||||
verticalChannelShadeAvg = self.getChannelShadeAvg(page, verticalSkew, True)
|
||||
verticalBounds = self.findBounds(verticalChannelShadeAvg)
|
||||
|
||||
if not len(verticalBounds):
|
||||
raise exceptions.DecodingError("No vertical bounds detected.")
|
||||
|
||||
# TODO: Get the real bottom of the image. Currently this is simply the lowest inner sector bound.
|
||||
verticalEnd = verticalBounds[-1][-1]
|
||||
|
||||
horizontalChannelShadeAvg = self.getChannelShadeAvg(page, horizontalSkew, False, verticalEnd)
|
||||
horizontalBounds = self.findBounds(horizontalChannelShadeAvg)
|
||||
|
||||
if not len(horizontalBounds):
|
||||
raise exceptions.DecodingError("No horizontal bounds detected")
|
||||
|
||||
if (tmpdir):
|
||||
f = open(os.path.join(tmpdir, "skewAndBounds.txt"), "w")
|
||||
f.write("verticalSkew " + str(verticalSkew))
|
||||
f.write("\rhorizontalSkew " + str(horizontalSkew))
|
||||
f.write("\rverticalBounds " + str(verticalBounds))
|
||||
f.write("\rhorizontalBounds " + str(horizontalBounds))
|
||||
f.write("\rverticalChannelShadeAvg " + str(verticalChannelShadeAvg))
|
||||
f.write("\rhorizontalChannelShadeAvg " + str(horizontalChannelShadeAvg))
|
||||
f.close()
|
||||
|
||||
bounds = self.getSkewSectorBounds(verticalBounds, horizontalBounds, verticalSkew, horizontalSkew)
|
||||
|
||||
if (tmpdir):
|
||||
converted_bounds = list()
|
||||
for y1, y2, x1, x2 in bounds:
|
||||
converted_bounds.extend([(y1, x1), (y1, x2), (y2, x1), (y2, x2)])
|
||||
drawPage(page, tmpdir, "bounds", tuple(converted_bounds), None, (255, 0, 0))
|
||||
|
||||
# TODO: Calculate dynamically
|
||||
# TODO: Override by command-line argument
|
||||
sectorHeight = defaults.sectorHeight
|
||||
|
@ -79,66 +39,22 @@ class ColorSafeImagesDecoder(ColorSafeImages):
|
|||
gapSize = defaults.gapSize
|
||||
eccRate = defaults.eccRate
|
||||
|
||||
sectorNum = -1
|
||||
bounds = get_data_bounds(page, sectorHeight, sectorWidth, gapSize, tmpdir)
|
||||
|
||||
if (tmpdir):
|
||||
debug_dots = list()
|
||||
sectorNum = -1
|
||||
|
||||
# For each sector, beginning and ending at its gaps
|
||||
sectorDamage = list()
|
||||
for topTemp, bottomTemp, leftTemp, rightTemp in bounds:
|
||||
for each_bounds in bounds:
|
||||
sectorNum += 1
|
||||
# perc = str(int(100.0 * sectorNum / (sectorsHorizontal*sectorsVertical))) + "%"
|
||||
|
||||
# Use page-average to calculate height/width, works better for small sector sizes
|
||||
# Rotation should even out on average
|
||||
heightPerDot = float(bottomTemp - topTemp + 1) / (sectorHeight + 2 * gapSize)
|
||||
widthPerDot = float(rightTemp - leftTemp + 1) / (sectorWidth + 2 * gapSize)
|
||||
|
||||
if widthPerDot < 1.0: # Less than 1.0x resolution, cannot get all dots
|
||||
raise exceptions.DecodingError
|
||||
|
||||
top, bottom, left, right = self.getRealGaps(page,
|
||||
heightPerDot,
|
||||
widthPerDot,
|
||||
topTemp,
|
||||
bottomTemp,
|
||||
leftTemp,
|
||||
rightTemp)
|
||||
|
||||
rowsBoundaryChanges = self.getBoundaryChanges(page, left, right, top, bottom)
|
||||
columnsBoundaryChanges = self.getBoundaryChanges(page, top, bottom, left, right, False)
|
||||
|
||||
rowDotStartLocations = self.dotStartLocations(widthPerDot,
|
||||
rowsBoundaryChanges,
|
||||
sectorWidth)
|
||||
columnDotStartLocations = self.dotStartLocations(widthPerDot,
|
||||
columnsBoundaryChanges,
|
||||
sectorHeight)
|
||||
|
||||
if (tmpdir):
|
||||
for x in rowDotStartLocations:
|
||||
for y in columnDotStartLocations:
|
||||
debug_dots.append((top + x, left + y))
|
||||
channelsList = get_normalized_channels_list(page, each_bounds, sectorHeight, sectorWidth,
|
||||
sectorNum, tmpdir)
|
||||
|
||||
# TODO: Calculate dynamically
|
||||
bucketNum = 40
|
||||
|
||||
channelsList = self.getChannelsList(page,
|
||||
columnDotStartLocations,
|
||||
rowDotStartLocations,
|
||||
top,
|
||||
left,
|
||||
sectorHeight,
|
||||
sectorWidth)
|
||||
channelsList = self.normalizeChannelsList(channelsList)
|
||||
|
||||
if (tmpdir):
|
||||
f = open(os.path.join(tmpdir, "normalizedChannels" + str(sectorNum) + ".txt"), "w")
|
||||
for i in channelsList:
|
||||
f.write(str(i.getChannels()) + "\r")
|
||||
f.close()
|
||||
|
||||
thresholdWeight = self.getThresholdWeight(channelsList, bucketNum)
|
||||
|
||||
dataRows = Sector.getDataRowCount(sectorHeight, eccRate)
|
||||
|
@ -170,9 +86,6 @@ class ColorSafeImagesDecoder(ColorSafeImages):
|
|||
# TODO: Use ColorSafeFileDecoder to organize and parse this
|
||||
metadataStr += str(sectorNum) + "\n" + outData + "\n\n"
|
||||
|
||||
if tmpdir:
|
||||
drawPage(page, tmpdir, "dots", tuple(debug_dots), None, (255, 0, 0))
|
||||
|
||||
if len(sectorDamage):
|
||||
self.sectorDamageAvg = sum(sectorDamage) / len(sectorDamage)
|
||||
else:
|
||||
|
@ -185,515 +98,18 @@ class ColorSafeImagesDecoder(ColorSafeImages):
|
|||
self.dataStr = dataStr
|
||||
self.metadataStr = metadataStr
|
||||
|
||||
@staticmethod
|
||||
def normalize(val, minVal, maxVal):
|
||||
if maxVal == minVal:
|
||||
raise exceptions.DecodingError
|
||||
|
||||
return (val - minVal) / (maxVal - minVal)
|
||||
|
||||
@staticmethod
|
||||
def getSectorBounds(
|
||||
page,
|
||||
leastAlong,
|
||||
mostAlong,
|
||||
leastPerp,
|
||||
mostPerp,
|
||||
gapThreshold,
|
||||
vertical=True,
|
||||
reverse=False):
|
||||
"""Search within given rough sector bounds and return the true coordinate of the gap
|
||||
E.g. if looking for the real top gap coordinate, along is y and perp is x. Return y.
|
||||
"""
|
||||
alongRange = range(leastAlong + 1, mostAlong + 1)
|
||||
if reverse:
|
||||
alongRange = alongRange[::-1]
|
||||
|
||||
for along in alongRange:
|
||||
perpShadeSum = 0.0
|
||||
for perp in range(leastPerp, mostPerp + 1):
|
||||
y = along if vertical else perp
|
||||
x = perp if vertical else along
|
||||
|
||||
if y < 0 or y >= page.height or x < 0 or x >= page.width:
|
||||
break
|
||||
|
||||
perpShadeSum += utils.average(page.getPixel(y, x))
|
||||
perpShadeSum /= (mostPerp - leastPerp)
|
||||
if perpShadeSum > gapThreshold:
|
||||
return along
|
||||
|
||||
@staticmethod
|
||||
def findSkew(page, vertical=True, reverse=False):
|
||||
"""Find the a grid image's skew, based on the vertical (or horizontal) boolean
|
||||
|
||||
Vertical skew is the number of pixels skewed right on the bottom side (negative if left) if the top is constant
|
||||
Horizontal skew is the number of pixels skewed down on the right side (negative if up) if the left is constant
|
||||
"""
|
||||
# TODO: Binary search through perp to find first boundary. This will improve speed.
|
||||
# TODO: Search for two or more skew lines to improve accuracy? Scale
|
||||
# isn't known yet, so this is tricky
|
||||
|
||||
DEFAULT_BEST_SKEW = 0
|
||||
MAX_SHADE_VARIANCE = 0.1
|
||||
|
||||
dataDensity = 0.5
|
||||
alongStep = 10 # TODO: Refine this
|
||||
|
||||
# Get length of column (or row) and the length of the axis
|
||||
# perpendicular to it, row (or column)
|
||||
# Along the axis specified by the vertical bool
|
||||
alongLength = page.height if vertical else page.width
|
||||
# Perpendicular to the axis specified by the vertical bool
|
||||
perpLength = page.width if vertical else page.height
|
||||
|
||||
maxSkew = max(int(constants.MaxSkewPerc * perpLength),
|
||||
constants.MaxSkew)
|
||||
|
||||
# For each angle, find the minimum shade in the first border
|
||||
minShade = 1.0
|
||||
minShadeIter = perpLength if not reverse else 0
|
||||
bestSkewShadeList = []
|
||||
for skew in range(-maxSkew, maxSkew + 1):
|
||||
slope = float(skew) / alongLength
|
||||
|
||||
# Choose perpendicular range bounds such that sloped line will not
|
||||
# run off the page
|
||||
perpBounds = range(min(0, skew), perpLength - max(0, skew))
|
||||
if reverse:
|
||||
perpBounds = perpBounds[::-1]
|
||||
|
||||
# TODO: Consider stepping perp/along 5-10 at a time for speedup
|
||||
for perpIter in perpBounds:
|
||||
# Don't check far past the first border coordinate, in order to
|
||||
# speed up execution
|
||||
if not reverse:
|
||||
breakCond = (perpIter > minShadeIter + 2 * maxSkew)
|
||||
else:
|
||||
breakCond = (perpIter < minShadeIter - 2 * maxSkew)
|
||||
|
||||
if breakCond:
|
||||
break
|
||||
|
||||
skewLine = list()
|
||||
for alongIter in range(0, alongLength, alongStep):
|
||||
perpValue = int(round(alongIter * slope)) + perpIter
|
||||
|
||||
if perpValue < 0 or perpValue >= perpLength:
|
||||
skewLine = list()
|
||||
break
|
||||
|
||||
x = perpValue if vertical else alongIter
|
||||
y = alongIter if vertical else perpValue
|
||||
|
||||
pixelShade = utils.average(page.getPixel(y, x))
|
||||
skewLine.append(pixelShade)
|
||||
|
||||
if not len(skewLine):
|
||||
continue
|
||||
|
||||
# TODO: Normalize
|
||||
# Add all skews and shades to a list if they're smaller than the smallest shade (plus some variance)
|
||||
avgShade = sum(skewLine) / len(skewLine)
|
||||
if avgShade < minShade * (1 + MAX_SHADE_VARIANCE) and avgShade < dataDensity:
|
||||
minShade = avgShade
|
||||
bestSkewShadeList.append((skew, minShade))
|
||||
minShadeIter = perpIter
|
||||
|
||||
# The bestSkewShadeList will have many shades/skews not near the bottom, since above we started from 1.0 and
|
||||
# included all smaller shades along the way. We did that to reduce the list size here, where we take the average
|
||||
# of shades within the variance of the global minimum.
|
||||
globalMinShade = 1.0
|
||||
for skew, minShade in bestSkewShadeList:
|
||||
globalMinShade = min(globalMinShade, minShade)
|
||||
|
||||
bestSkews = []
|
||||
for skew, minShade in bestSkewShadeList:
|
||||
if minShade < globalMinShade * (1 + MAX_SHADE_VARIANCE):
|
||||
bestSkews.append(skew)
|
||||
|
||||
bestSkew = utils.average(bestSkews) if len(bestSkews) else DEFAULT_BEST_SKEW
|
||||
return bestSkew
|
||||
|
||||
@staticmethod
|
||||
def getChannelShadeAvg(page, skew, vertical=True, perpEnd = None):
|
||||
"""Get bounds including skew
|
||||
"""
|
||||
perpStep = 3 # TODO: Refine this
|
||||
|
||||
# Get length of column (or row) and the length of the axis
|
||||
# perpendicular to it, row (or column)
|
||||
# Along the axis specified by the vertical bool
|
||||
alongLength = page.height if vertical else page.width
|
||||
# Perpendicular, e.g. in the direction of the bounds. PerpEnd can replace this is supplied.
|
||||
perpLength = perpEnd if perpEnd else (page.width if vertical else page.height)
|
||||
|
||||
slope = float(skew) / perpLength
|
||||
|
||||
# Get all skew line shade sums
|
||||
channelShadeAvg = list()
|
||||
for alongIter in range(0, alongLength):
|
||||
alongShadeSum = 0
|
||||
|
||||
# Sum all shades along the skew line
|
||||
perpLengthIterated = 0
|
||||
for perpIter in range(0, perpLength, perpStep):
|
||||
perpValue = int(round(perpIter * slope)) + alongIter
|
||||
|
||||
# If coordinates are out of bounds of the image, use white pixels
|
||||
if perpValue < 0 or perpValue >= alongLength:
|
||||
# TODO: Make all white, or else start at perp values that will all be contained in the image
|
||||
# alongShadeSum = 1.0 * perpLength / perpStep # White border
|
||||
|
||||
alongShadeSum += 1.0
|
||||
continue
|
||||
|
||||
y = perpValue if vertical else perpIter
|
||||
x = perpIter if vertical else perpValue
|
||||
|
||||
pixelShade = utils.average(page.getPixel(y, x))
|
||||
alongShadeSum += pixelShade
|
||||
|
||||
channelShadeAvg.append(alongShadeSum * perpStep / perpLength)
|
||||
|
||||
return channelShadeAvg
|
||||
|
||||
@staticmethod
|
||||
def getSkewSectorBounds(
|
||||
verticalBounds,
|
||||
horizontalBounds,
|
||||
verticalSkew,
|
||||
horizontalSkew):
|
||||
"""Given skews, get skewed bounds from non-skewed ones
|
||||
"""
|
||||
bounds = list()
|
||||
|
||||
for top, bottom in verticalBounds:
|
||||
for left, right in horizontalBounds:
|
||||
bound = (top, bottom, left, right)
|
||||
bounds.append(bound)
|
||||
|
||||
return bounds
|
||||
|
||||
# TODO: Consider moving this logic to a new ChannelsGrid object
|
||||
# TODO: Set previousCount as average pixel width when called
|
||||
@staticmethod
|
||||
def findBounds(l, previousCount=6):
|
||||
"""Given a 1D black and white grid matrix (one axis of a 2D grid matrix) return a list of beginnings and ends.
|
||||
A beginning is the first whitespace (gap) after any black border, and an end is the last whitespace (gap)
|
||||
before the next black border.
|
||||
|
||||
Use whitespace after borders rather than searching for the data within each border, since the
|
||||
data may be empty or inconsistent. Borders are the most reliable unit of recognition.
|
||||
"""
|
||||
minLengthSector = max(defaults.sectorWidth, defaults.sectorHeight)
|
||||
|
||||
lowBorderThreshold = 0.35
|
||||
highGapThreshold = 0.65
|
||||
# Threshold for throwing out a value and using the mode end-begin diff
|
||||
# instead
|
||||
diffThreshold = 0.1
|
||||
|
||||
# Trim leading/trailing whitespace for better min/max normalization
|
||||
borderBeginning = 0
|
||||
for i, val in enumerate(l):
|
||||
if ColorSafeImagesDecoder.normalize(val, min(l), max(l)) < highGapThreshold:
|
||||
borderBeginning = i
|
||||
break
|
||||
|
||||
borderEnding = len(l)
|
||||
for i, val in reversed(list(enumerate(l))):
|
||||
if ColorSafeImagesDecoder.normalize(val, min(l), max(l)) < highGapThreshold:
|
||||
borderEnding = i
|
||||
break
|
||||
|
||||
lTruncated = l[borderBeginning:borderEnding + 1]
|
||||
|
||||
# TODO: Use top/bottom 10th percentiles to threshold, for better accuracy
|
||||
minVal = min(lTruncated)
|
||||
maxVal = max(lTruncated)
|
||||
|
||||
begins = list()
|
||||
ends = list()
|
||||
|
||||
# Find beginning border, and then all begin/end gaps that surround
|
||||
# sector data
|
||||
for i, val in enumerate(l):
|
||||
if i < borderBeginning or i > borderEnding:
|
||||
continue
|
||||
|
||||
val = ColorSafeImagesDecoder.normalize(val, minVal, maxVal)
|
||||
|
||||
# Look for expected values to cross thresholds anywhere in the last
|
||||
# previousCount values.
|
||||
previousVals = list()
|
||||
for shift in range(previousCount):
|
||||
prevIndex = i - shift - 1
|
||||
if prevIndex >= 0:
|
||||
previousVals.append(
|
||||
ColorSafeImagesDecoder.normalize(
|
||||
l[prevIndex], minVal, maxVal))
|
||||
|
||||
# Begins and ends matched, looking for new begin gap
|
||||
if len(begins) == len(ends):
|
||||
# Boundary where black turns white
|
||||
if val > highGapThreshold and any(
|
||||
v < lowBorderThreshold for v in previousVals):
|
||||
begins.append(i)
|
||||
continue
|
||||
|
||||
# More begins than ends, looking for new end gap
|
||||
if len(ends) < len(begins):
|
||||
# Boundary where white turns black
|
||||
if val < lowBorderThreshold and any(
|
||||
v > highGapThreshold for v in previousVals):
|
||||
if i >= begins[-1] + minLengthSector:
|
||||
ends.append(i - 1)
|
||||
continue
|
||||
|
||||
# If begins and ends don't match, correct by cutting off excess
|
||||
# beginning
|
||||
if len(begins) > len(ends):
|
||||
begins = begins[0:len(ends)]
|
||||
|
||||
# Correct bounds
|
||||
if len(begins) > 1:
|
||||
beginsDiffs = list()
|
||||
endsDiffs = list()
|
||||
for i in range(1, len(begins)):
|
||||
beginsDiffs.append(begins[i] - begins[i - 1])
|
||||
endsDiffs.append(ends[i] - ends[i - 1])
|
||||
|
||||
beginsDiffsMode = max(beginsDiffs, key=beginsDiffs.count)
|
||||
endsDiffsMode = max(endsDiffs, key=endsDiffs.count)
|
||||
|
||||
# TODO: Doesn't correct first begin or end - assumes first one is
|
||||
# correct
|
||||
for i in range(1, len(begins)):
|
||||
if abs(begins[i] - begins[i - 1]) / float(beginsDiffsMode) > diffThreshold:
|
||||
begins[i] = begins[i - 1] + beginsDiffsMode
|
||||
|
||||
if abs(ends[i] - ends[i - 1]) / float(endsDiffsMode) > diffThreshold:
|
||||
ends[i] = ends[i - 1] + endsDiffsMode
|
||||
|
||||
bounds = list()
|
||||
for i in range(0, len(begins)):
|
||||
bounds.append((begins[i], ends[i]))
|
||||
|
||||
return bounds
|
||||
|
||||
@staticmethod
|
||||
def getBoundaryChanges(page, begin, end, perp_begin, perp_end, rows = True):
|
||||
""" For all pixels in sector, mark and sum boundary changes for all rows or columns
|
||||
TODO: Generalize to multiple shades, e.g. shadesPerChannel = 2
|
||||
|
||||
:param page
|
||||
:param begin:
|
||||
:param end:
|
||||
:param perp_begin:
|
||||
:param perp_end:
|
||||
:param rows
|
||||
:return:
|
||||
"""
|
||||
|
||||
BoundaryThreshold = 0.8
|
||||
|
||||
boundaryChanges = list()
|
||||
|
||||
for i in range(begin + 1, end + 1):
|
||||
allBoundaryChanges = 0
|
||||
for j in range(perp_begin, perp_end + 1):
|
||||
current = page.getPixel(j if rows else i, i if rows else j)
|
||||
previous = page.getPixel(j if rows else i - 1, i - 1 if rows else j)
|
||||
|
||||
for k in range(len(current)):
|
||||
bucketCurrent = (0 if current[k] < BoundaryThreshold else 1)
|
||||
bucketPrevious = (0 if previous[k] < BoundaryThreshold else 1)
|
||||
|
||||
# Get white to black only, seems to be more
|
||||
# consistent
|
||||
if bucketCurrent != bucketPrevious and bucketCurrent == 0:
|
||||
allBoundaryChanges += 1
|
||||
|
||||
boundaryChanges.append(allBoundaryChanges)
|
||||
|
||||
return boundaryChanges
|
||||
|
||||
@staticmethod
|
||||
def dotStartLocations(widthPerDot, boundaryChanges, sectorLength):
|
||||
"""Find the most likely dot start locations
|
||||
|
||||
:param sectorLength SectorHeight for columns, SectorWidth for rows
|
||||
:return:
|
||||
"""
|
||||
|
||||
avgPixelsWidth = int(round(widthPerDot))
|
||||
|
||||
if widthPerDot == 1.0: # Exactly 1.0, e.g. original output, or perfectly scanned
|
||||
maxPixelsWidth = 1
|
||||
else:
|
||||
maxPixelsWidth = avgPixelsWidth + 1
|
||||
|
||||
minPixelsWidth = max(
|
||||
avgPixelsWidth - 1,
|
||||
1) # Cannot be less than 1
|
||||
|
||||
dotStartLocations = list()
|
||||
currentLocation = 0
|
||||
for i in range(sectorLength):
|
||||
# TODO: Account for the gap, find initial data start
|
||||
mnw = minPixelsWidth if i else 0
|
||||
possible = boundaryChanges[currentLocation + mnw: currentLocation + maxPixelsWidth + (1 if i else 0)]
|
||||
if possible:
|
||||
index = possible.index(max(possible))
|
||||
else:
|
||||
index = 0
|
||||
currentLocation += index + mnw
|
||||
dotStartLocations.append(currentLocation)
|
||||
|
||||
# For ending, add average width to the end so that dot
|
||||
# padding/fill is correct
|
||||
dotStartLocations.append(dotStartLocations[-1] + avgPixelsWidth)
|
||||
|
||||
return dotStartLocations
|
||||
|
||||
@staticmethod
|
||||
def getRealGaps(page, heightPerDot, widthPerDot, topTemp, bottomTemp, leftTemp, rightTemp):
|
||||
"""Find real gaps, since small rotation across a large page may distort this.
|
||||
Look within one-dot unit of pixels away
|
||||
"""
|
||||
|