Commit 4b3f9388 authored by Andrey S. Petrov's avatar Andrey S. Petrov
Browse files

Initial commit

parents
This source diff could not be displayed because it is too large. You can view the blob instead.
#!/usr/bin/env python
'''Converts sequence of images to compact PDF while removing speckles,
bleedthrough, etc.
'''
# for some reason pylint complains about members being undefined :(
# pylint: disable=E1101
from __future__ import print_function
import sys
import os
import re
import subprocess
import shlex
from argparse import ArgumentParser
import numpy as np
from PIL import Image
from scipy.cluster.vq import kmeans, vq
######################################################################
def quantize(image, bits_per_channel=None):
'''Reduces the number of bits per channel in the given image.'''
if bits_per_channel is None:
bits_per_channel = 6
assert image.dtype == np.uint8
shift = 8-bits_per_channel
halfbin = (1 << shift) >> 1
return ((image.astype(int) >> shift) << shift) + halfbin
######################################################################
def pack_rgb(rgb):
'''Packs a 24-bit RGB triples into a single integer,
works on both arrays and tuples.'''
orig_shape = None
if isinstance(rgb, np.ndarray):
assert rgb.shape[-1] == 3
orig_shape = rgb.shape[:-1]
else:
assert len(rgb) == 3
rgb = np.array(rgb)
rgb = rgb.astype(int).reshape((-1, 3))
packed = (rgb[:, 0] << 16 |
rgb[:, 1] << 8 |
rgb[:, 2])
if orig_shape is None:
return packed
else:
return packed.reshape(orig_shape)
######################################################################
def unpack_rgb(packed):
'''Unpacks a single integer or array of integers into one or more
24-bit RGB values.
'''
orig_shape = None
if isinstance(packed, np.ndarray):
assert packed.dtype == int
orig_shape = packed.shape
packed = packed.reshape((-1, 1))
rgb = ((packed >> 16) & 0xff,
(packed >> 8) & 0xff,
(packed) & 0xff)
if orig_shape is None:
return rgb
else:
return np.hstack(rgb).reshape(orig_shape + (3,))
######################################################################
def get_bg_color(image, bits_per_channel=None):
'''Obtains the background color from an image or array of RGB colors
by grouping similar colors into bins and finding the most frequent
one.
'''
assert image.shape[-1] == 3
quantized = quantize(image, bits_per_channel).astype(int)
packed = pack_rgb(quantized)
unique, counts = np.unique(packed, return_counts=True)
packed_mode = unique[counts.argmax()]
return unpack_rgb(packed_mode)
######################################################################
def rgb_to_sv(rgb):
'''Convert an RGB image or array of RGB colors to saturation and
value, returning each one as a separate 32-bit floating point array or
value.
'''
if not isinstance(rgb, np.ndarray):
rgb = np.array(rgb)
axis = len(rgb.shape)-1
cmax = rgb.max(axis=axis).astype(np.float32)
cmin = rgb.min(axis=axis).astype(np.float32)
delta = cmax - cmin
saturation = delta.astype(np.float32) / cmax.astype(np.float32)
saturation = np.where(cmax == 0, 0, saturation)
value = cmax/255.0
return saturation, value
######################################################################
def postprocess(output_filename, options):
'''Runs the postprocessing command on the file provided.'''
assert options.postprocess_cmd
base, _ = os.path.splitext(output_filename)
post_filename = base + options.postprocess_ext
cmd = options.postprocess_cmd
cmd = cmd.replace('%i', output_filename)
cmd = cmd.replace('%o', post_filename)
cmd = cmd.replace('%e', options.postprocess_ext)
subprocess_args = shlex.split(cmd)
if os.path.exists(post_filename):
os.unlink(post_filename)
if not options.quiet:
print(' running "{}"...'.format(cmd), end=' ')
sys.stdout.flush()
try:
result = subprocess.call(subprocess_args)
before = os.stat(output_filename).st_size
after = os.stat(post_filename).st_size
except OSError:
result = -1
if result == 0:
if not options.quiet:
print('{:.1f}% reduction'.format(
100*(1.0-float(after)/before)))
return post_filename
else:
sys.stderr.write('warning: postprocessing failed!\n')
return None
######################################################################
def percent(string):
'''Convert a string (i.e. 85) to a fraction (i.e. .85).'''
return float(string)/100.0
######################################################################
def get_argument_parser():
'''Parse the command-line arguments for this program.'''
parser = ArgumentParser(
description='convert scanned, hand-written notes to PDF')
show_default = ' (default %(default)s)'
parser.add_argument('filenames', metavar='IMAGE', nargs='+',
help='files to convert')
parser.add_argument('-q', dest='quiet', action='store_true',
default=False,
help='reduce program output')
parser.add_argument('-b', dest='basename', metavar='BASENAME',
default='page',
help='output PNG filename base' + show_default)
parser.add_argument('-o', dest='pdfname', metavar='PDF',
default='output.pdf',
help='output PDF filename' + show_default)
parser.add_argument('-v', dest='value_threshold', metavar='PERCENT',
type=percent, default='25',
help='background value threshold %%'+show_default)
parser.add_argument('-s', dest='sat_threshold', metavar='PERCENT',
type=percent, default='20',
help='background saturation '
'threshold %%'+show_default)
parser.add_argument('-n', dest='num_colors', type=int,
default='8',
help='number of output colors '+show_default)
parser.add_argument('-p', dest='sample_fraction',
metavar='PERCENT',
type=percent, default='5',
help='%% of pixels to sample' + show_default)
parser.add_argument('-w', dest='white_bg', action='store_true',
default=False, help='make background white')
parser.add_argument('-g', dest='global_palette',
action='store_true', default=False,
help='use one global palette for all pages')
parser.add_argument('-S', dest='saturate', action='store_false',
default=True, help='do not saturate colors')
parser.add_argument('-K', dest='sort_numerically',
action='store_false', default=True,
help='keep filenames ordered as specified; '
'use if you *really* want IMG_10.png to '
'precede IMG_2.png')
parser.add_argument('-P', dest='postprocess_cmd', default=None,
help='set postprocessing command (see -O, -C, -Q)')
parser.add_argument('-e', dest='postprocess_ext',
default='_post.png',
help='filename suffix/extension for '
'postprocessing command')
parser.add_argument('-O', dest='postprocess_cmd',
action='store_const',
const='optipng -silent %i -out %o',
help='same as -P "%(const)s"')
parser.add_argument('-C', dest='postprocess_cmd',
action='store_const',
const='pngcrush -q %i %o',
help='same as -P "%(const)s"')
parser.add_argument('-Q', dest='postprocess_cmd',
action='store_const',
const='pngquant --ext %e %i',
help='same as -P "%(const)s"')
parser.add_argument('-c', dest='pdf_cmd', metavar="COMMAND",
default='convert %i %o',
help='PDF command (default "%(default)s")')
return parser
######################################################################
def get_filenames(options):
'''Get the filenames from the command line, optionally sorted by
number, so that IMG_10.png is re-arranged to come after IMG_9.png.
This is a nice feature because some scanner programs (like Image
Capture on Mac OS X) automatically number files without leading zeros,
and this way you can supply files using a wildcard and still have the
pages ordered correctly.
'''
if not options.sort_numerically:
return options.filenames
filenames = []
for filename in options.filenames:
basename = os.path.basename(filename)
root, _ = os.path.splitext(basename)
matches = re.findall(r'[0-9]+', root)
if matches:
num = int(matches[-1])
else:
num = -1
filenames.append((num, filename))
return [fn for (_, fn) in sorted(filenames)]
######################################################################
def load(input_filename):
'''Load an image with Pillow and convert it to numpy array. Also
returns the image DPI in x and y as a tuple.'''
try:
pil_img = Image.open(input_filename)
except IOError:
sys.stderr.write('warning: error opening {}\n'.format(
input_filename))
return None, None
if pil_img.mode != 'RGB':
pil_img = pil_img.convert('RGB')
if 'dpi' in pil_img.info:
dpi = pil_img.info['dpi']
else:
dpi = (300, 300)
img = np.array(pil_img)
return img, dpi
######################################################################
def sample_pixels(img, options):
'''Pick a fixed percentage of pixels in the image, returned in random
order.'''
pixels = img.reshape((-1, 3))
num_pixels = pixels.shape[0]
num_samples = int(num_pixels*options.sample_fraction)
idx = np.arange(num_pixels)
np.random.shuffle(idx)
return pixels[idx[:num_samples]]
######################################################################
def get_fg_mask(bg_color, samples, options):
'''Determine whether each pixel in a set of samples is foreground by
comparing it to the background color. A pixel is classified as a
foreground pixel if either its value or saturation differs from the
background by a threshold.'''
s_bg, v_bg = rgb_to_sv(bg_color)
s_samples, v_samples = rgb_to_sv(samples)
s_diff = np.abs(s_bg - s_samples)
v_diff = np.abs(v_bg - v_samples)
return ((v_diff >= options.value_threshold) |
(s_diff >= options.sat_threshold))
######################################################################
def get_palette(samples, options, return_mask=False, kmeans_iter=40):
'''Extract the palette for the set of sampled RGB values. The first
palette entry is always the background color; the rest are determined
from foreground pixels by running K-means clustering. Returns the
palette, as well as a mask corresponding to the foreground pixels.
'''
if not options.quiet:
print(' getting palette...')
bg_color = get_bg_color(samples, 6)
fg_mask = get_fg_mask(bg_color, samples, options)
centers, _ = kmeans(samples[fg_mask].astype(np.float32),
options.num_colors-1,
iter=kmeans_iter)
palette = np.vstack((bg_color, centers)).astype(np.uint8)
if not return_mask:
return palette
else:
return palette, fg_mask
######################################################################
def apply_palette(img, palette, options):
'''Apply the pallete to the given image. The first step is to set all
background pixels to the background color; then, nearest-neighbor
matching is used to map each foreground color to the closest one in
the palette.
'''
if not options.quiet:
print(' applying palette...')
bg_color = palette[0]
fg_mask = get_fg_mask(bg_color, img, options)
orig_shape = img.shape
pixels = img.reshape((-1, 3))
fg_mask = fg_mask.flatten()
num_pixels = pixels.shape[0]
labels = np.zeros(num_pixels, dtype=np.uint8)
labels[fg_mask], _ = vq(pixels[fg_mask], palette)
return labels.reshape(orig_shape[:-1])
######################################################################
def save(output_filename, labels, palette, dpi, options):
'''Save the label/palette pair out as an indexed PNG image. This
optionally saturates the pallete by mapping the smallest color
component to zero and the largest one to 255, and also optionally sets
the background color to pure white.
'''
if not options.quiet:
print(' saving {}...'.format(output_filename))
if options.saturate:
palette = palette.astype(np.float32)
pmin = palette.min()
pmax = palette.max()
palette = 255 * (palette - pmin)/(pmax-pmin)
palette = palette.astype(np.uint8)
if options.white_bg:
palette = palette.copy()
palette[0] = (255, 255, 255)
output_img = Image.fromarray(labels, 'P')
output_img.putpalette(palette.flatten())
output_img.save(output_filename, dpi=dpi)
######################################################################
def get_global_palette(filenames, options):
'''Fetch the global palette for a series of input files by merging
their samples together into one large array.
'''
input_filenames = []
all_samples = []
if not options.quiet:
print('building global palette...')
for input_filename in filenames:
img, _ = load(input_filename)
if img is None:
continue
if not options.quiet:
print(' processing {}...'.format(input_filename))
samples = sample_pixels(img, options)
input_filenames.append(input_filename)
all_samples.append(samples)
num_inputs = len(input_filenames)
all_samples = [s[:int(round(float(s.shape[0])/num_inputs))]
for s in all_samples]
all_samples = np.vstack(tuple(all_samples))
global_palette = get_palette(all_samples, options)
if not options.quiet:
print(' done\n')
return input_filenames, global_palette
######################################################################
def emit_pdf(outputs, options):
'''Runs the PDF conversion command to generate the PDF.'''
cmd = options.pdf_cmd
cmd = cmd.replace('%o', options.pdfname)
if len(outputs) > 2:
cmd_print = cmd.replace('%i', ' '.join(outputs[:2] + ['...']))
else:
cmd_print = cmd.replace('%i', ' '.join(outputs))
cmd = cmd.replace('%i', ' '.join(outputs))
if not options.quiet:
print('running PDF command "{}"...'.format(cmd_print))
try:
result = subprocess.call(shlex.split(cmd))
except OSError:
result = -1
if result == 0:
if not options.quiet:
print(' wrote', options.pdfname)
else:
sys.stderr.write('warning: PDF command failed\n')
######################################################################
def notescan_main(options):
'''Main function for this program when run as script.'''
filenames = get_filenames(options)
outputs = []
do_global = options.global_palette and len(filenames) > 1
if do_global:
filenames, palette = get_global_palette(filenames, options)
do_postprocess = bool(options.postprocess_cmd)
for input_filename in filenames:
img, dpi = load(input_filename)
if img is None:
continue
output_filename = '{}{:04d}.png'.format(
options.basename, len(outputs))
if not options.quiet:
print('opened', input_filename)
if not do_global:
samples = sample_pixels(img, options)
palette = get_palette(samples, options)
labels = apply_palette(img, palette, options)
save(output_filename, labels, palette, dpi, options)
if do_postprocess:
post_filename = postprocess(output_filename, options)
if post_filename:
output_filename = post_filename
else:
do_postprocess = False
outputs.append(output_filename)
if not options.quiet:
print(' done\n')
emit_pdf(outputs, options)
######################################################################
def main():
'''Parse args and call notescan_main().'''
notescan_main(options=get_argument_parser().parse_args())
if __name__ == '__main__':
main()
#include <opencv2/opencv.hpp>
#include <chrono>
#include <cmath>
#include <algorithm>
#define IMG_WIDTH 1280 // image width for resize to small
#define IMG_HEIGHT 700 // image height for resize to small
#define PAGE_MARGIN_X 50 // reduced px to ignore near L/R edge
#define PAGE_MARGIN_Y 20 // reduced px to ignore near T/B edge
#define OUTPUT_ZOOM 1.0 // how much to zoom output relative to *original* image
#define OUTPUT_DPI 300 // just affects stated DPI of PNG, not appearance
#define REMAP_DECIMATE 16 // downscaling factor for remapping image
#define ADAPTIVE_WINSZ 55 // window size for adaptive threshold in reduced px
#define TEXT_MIN_WIDTH 15 // min reduced px width of detected text contour
#define TEXT_MIN_HEIGHT 2 // min reduced px height of detected text contour
#define TEXT_MIN_ASPECT 1.5 // filter out text contours below this w/h ratio
#define TEXT_MAX_THICKNESS 10 // max reduced px thickness of detected text contour
#define EDGE_MAX_OVERLAP 1.0 // max reduced px horiz. overlap of contours in span
#define EDGE_MAX_LENGTH 100.0 // max reduced px length of edge connecting contours
#define EDGE_ANGLE_COST 10.0 // cost of angles in edges (tradeoff vs. length)
#define EDGE_MAX_ANGLE 7.5 // maximum change in angle allowed between contours
//#define RVEC_IDX slice(0, 3) // index of rvec in params vector
//#define TVEC_IDX slice(3, 6) // index of tvec in params vector
//#define CUBIC_IDX slice(6, 8) // index of cubic slopes in params vector
#define SPAN_MIN_WIDTH 30 // minimum reduced px width for span
#define SPAN_PX_PER_STEP 20 // reduced px spacing for sampling along spans
#define FOCAL_LENGTH 1.2 // normalized focal length of camera
#ifdef __ANDROID__
#include <android/log.h>
#endif
using namespace cv;
using namespace std;
long long int get_now() {
return chrono::duration_cast<std::chrono::milliseconds>(
chrono::system_clock::now().time_since_epoch()
).count();
}
void platform_log(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
#ifdef __ANDROID__
__android_log_vprint(ANDROID_LOG_VERBOSE, "ndk", fmt, args);
#else
vprintf(fmt, args);
#endif
va_end(args);
}
Mat resize_to_small(Mat src, int maxw=IMG_WIDTH, int maxh=IMG_HEIGHT, bool copy=false)
{
Mat img;
//height, width = src.shape[:2]
int height = src.rows;
int width = src.cols;
float scl_x = float(width)/maxw;
float scl_y = float(height)/maxh;
int scl = ceil(max(scl_x, scl_y));
float inv_scl;
if (scl > 1.0) {
inv_scl = 1.0 / scl;
resize(src, img, Size(0, 0), inv_scl, inv_scl, INTER_AREA);
} else if (copy) {
src.copyTo(img);
} else {
img = src;
}
return img;
}
Mat get_mask(Mat small, Mat pagemask, string masktype)
{
Mat sgray, mask;
cvtColor(small, sgray, COLOR_RGB2GRAY);
if (masktype == "text") {
adaptiveThreshold(sgray, mask, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY_INV, ADAPTIVE_WINSZ, 25);
} else {
adaptiveThreshold(sgray, mask, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY_INV, ADAPTIVE_WINSZ, 7);
}
return min(mask, pagemask); //np.minimum(mask, pagemask)
}
/*
def make_tight_mask(contour, xmin, ymin, width, height):
tight_mask = np.zeros((height, width), dtype=np.uint8)
tight_contour = contour - np.array((xmin, ymin)).reshape((-1, 1, 2))
cv2.drawContours(tight_mask, [tight_contour], 0,
(1, 1, 1), -1)
return tight_mask
*/
vector<vector<Point>> get_contours(Mat small, Mat pagemask, string masktype) {
vector<vector<Point>> contours, contours_out;
Rect rect;
int xmin, ymin, width, height;
Mat mask = get_mask(small, pagemask, masktype);
findContours(mask, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
//for contour in contours:
for ( int i = 0; i < contours.size(); i++ )
{
rect = boundingRect(contours[i]);
xmin = rect.x; ymin = rect.y; width = rect.width; height = rect.height;
if (width < TEXT_MIN_WIDTH or
height < TEXT_MIN_HEIGHT or
width < TEXT_MIN_ASPECT*height) {
continue;
}
/*
tight_mask = make_tight_mask(contour, xmin, ymin, width, height);
if (tight_mask.sum(axis=0).max() > TEXT_MAX_THICKNESS) {
continue;
}
contours_out.push_back(ContourInfo(contour, rect, tight_mask));
*/
}
return contours_out;
}
// Avoiding name mangling
extern "C" {
__attribute__((visibility("default"))) __attribute__((used))
void braille_dewarp(char* inputImagePath, char* outputImagePath) {
long long start = get_now();
/*
Mat input = imread(inputImagePath, IMREAD_GRAYSCALE);
Mat threshed, withContours;
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
// Doing a lot of work
for (int i = 0; i < 1; i++) {
adaptiveThreshold(input, threshed, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 77, 6);
findContours(threshed, contours, hierarchy, RETR_TREE, CHAIN_APPROX_TC89_L1);
}
cvtColor(threshed, withContours, COLOR_GRAY2BGR);
drawContours(withContours, contours, -1, Scalar(0, 255, 0), 4);
imwrite(outputImagePath, withContours);
*/
Mat pagemask;
Mat img = imread(inputImagePath, IMREAD_GRAYSCALE);
Mat small = resize_to_small(img);
//---------------- pagemask, page_outline = get_page_extents(small)
int height = small.rows;
int width = small.cols;
int xmin = PAGE_MARGIN_X;
int ymin = PAGE_MARGIN_Y;
int xmax = width - PAGE_MARGIN_X;
int ymax = height - PAGE_MARGIN_Y;
pagemask = Mat(Size(height, width), CV_64FC1, Scalar(0)); //page = np.zeros((height, width), dtype=np.uint8)
rectangle(pagemask, Point(xmin, ymin), Point(xmax, ymax), Scalar(255, 255, 255), -1);
vector< vector<Point> > page_outline { { {xmin, ymin}, {xmin, ymax}, {xmax, ymax}, {xmax, ymin} } };
//-----------------
vector<vector<Point>> cinfo_list = get_contours(small, pagemask, "text");
int evalInMillis = static_cast<int>(get_now() - start);
platform_log("Processing done in %dms\n", evalInMillis);
}
}
#include <opencv2/opencv.hpp>
#include <chrono>
#include <cmath>
#include <algorithm>
#define IMG_WIDTH 1280 // image width for resize to small
#define IMG_HEIGHT 700 // image height for resize to small
#ifdef __ANDROID__
#include <android/log.h>
#endif
using namespace cv;
using namespace std;
Mat resize_to_small(Mat src, int maxw=IMG_WIDTH, int maxh=IMG_HEIGHT, bool copy=false)
{
Mat img;
//height, width = src.shape[:2]
int height = src.rows;
int width = src.cols;
float scl_x = float(width)/maxw;
float scl_y = float(height)/maxh;
int scl = ceil(max(scl_x, scl_y));
float inv_scl;
if (scl > 1.0) {
inv_scl = 1.0 / scl;
resize(src, img, Size(0, 0), inv_scl, inv_scl, INTER_AREA);
} else if (copy) {
src.copyTo(img);
} else {
img = src;
}
return img;
}
// https://stackoverflow.com/questions/13495207/opencv-c-sorting-contours-by-their-contourarea
// comparison function object according to their size in descending order
bool compareContourAreas ( vector<Point> contour1, vector<Point> contour2 ) {
double i = fabs( contourArea(Mat(contour1)) );
double j = fabs( contourArea(Mat(contour2)) );
return (i > j);
}
bool compare_dot_x ( int dot_x1, int dot_x2 ) {
return (dot_x1 < dot_x2);
}
bool compare_dot_y ( int dot_y1, int dot_y2 ) {
return (dot_y1 < dot_y2);
}
bool compare_dotCtrs ( vector<Point> dotCtrs1, vector<Point> dotCtrs2 ) {
return ((dotCtrs1[1].y < dotCtrs2[1].y) || ((dotCtrs1[1].y == dotCtrs2[1].y) && (dotCtrs1[1].x < dotCtrs2[1].x)) );
}
bool compare_spacingX ( int spacingX1, int spacingX2 ) {
return (spacingX1 < spacingX2);
}
bool compare_spacingY ( int spacingY1, int spacingY2 ) {
return (spacingY1 < spacingY2);
}
static void recognition(string url, int iter = 2) {
Mat gray, blurred, edged, edged_conturs, accumEdged, paper, thresh, thresh_conturs, kernel;
vector<vector<Point>> ctrs, dotCtrs;
vector<Vec4i> hierarchy;
vector<Point> approx;
vector<Point> docCnt;
Rect rect;
vector<int> rect_width;
vector<int> rect_height;
vector<int> rect_x;
vector<int> rect_y;
vector<int> dot_x;
vector<int> dot_y;
int m, index_m;
double diam, tol;
int width = IMG_WIDTH;
int height = IMG_HEIGHT;
double peri;
Mat image = imread(url);
Mat ans = resize_to_small(image, width, height, true);
accumEdged = Mat(Size(height, width), CV_64FC1, Scalar(0)); // accumEdged = np.zeros(image.shape[:2], dtype="uint8")
// convert image to black and white
cvtColor(image, gray, COLOR_BGR2GRAY);
// blur to remove some of the noise
GaussianBlur(gray, blurred, Size(5, 5), 0);
// get edges
Canny(blurred, edged, 75, 200);
bitwise_or(accumEdged, edged, accumEdged);
// get contours
// cv::findContours( binary_image, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, cv::Point(0, 0) );
edged.copyTo(edged_conturs); // !!! del
findContours(edged_conturs, ctrs, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
//docCnt = NULL;
// ensure that at least one contour was found
if (ctrs.size() > 0) {
// sort the contours according to their size in descending order
// ctrs = sorted(ctrs, key=cv2.contourArea, reverse=True)
// https://stackoverflow.com/questions/13495207/opencv-c-sorting-contours-by-their-contourarea
sort(ctrs.begin(), ctrs.end(), compareContourAreas);
// loop over the sorted contours
for ( int i = 0; i < ctrs.size(); i++ ) {
// approximate the contour
peri = arcLength(ctrs[i], true);
approxPolyDP(ctrs[i], approx, 0.02 * peri, true);
// if our approximated contour has four points,
// then we can assume we have found the paper
if (approx.size() == 4) {
docCnt = approx;
break;
}
}
}
image.copyTo(paper);
// apply Otsu's thresholding method to binarize the image
threshold(gray, thresh, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
//kernel = np.ones((5,5), np.uint8)
kernel = Mat(Size(5,5), CV_64FC1, Scalar(1));
// erode and dilate to remove some of the unnecessary detail
erode(thresh, thresh, kernel, Point(-1,-1), iter);
dilate(thresh, thresh, kernel, Point(-1,-1), iter);
// find contours in the thresholded image
thresh.copyTo(thresh_conturs); // !!! del
findContours(thresh_conturs, ctrs, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
//return image, ctrs, paper, gray, edged, thresh
//def get_diameter() -----------------------------------
// https://habr.com/ru/post/167177/
//find the most frequently width (or height) among the bounding boxes
//for contour in contours:
int n_width = 0; // the number of people who did not find a pair and remained standing
int n_height = 0;
int i_width = NULL; // the last person to find a match --
int i_height = NULL;
// perhaps its element occurs most often we go through the array and seat pairs with different elements
// we go through the array and seat pairs with different elements
for ( int i = 0; i < ctrs.size(); i++ ) {
rect = boundingRect(ctrs[i]);
rect_width.push_back(rect.width);
rect_height.push_back(rect.height);
rect_x.push_back(rect.x);
rect_y.push_back(rect.y);
// we go through the array and seat pairs with different elements
if (n_width == 0) {
i_width = i;
n_width++;
// otherwise the next one either sits down with one of the standing,
// or will stand with them if it has the same element
} else {
if (rect_width[i_width] == rect_width[i]) {
n_width++;
} else {
n_width--;
}
}
if (n_height == 0) {
i_height = i;
n_height++;
} else {
if (rect_height[i_width] == rect_height[i]) {
n_height++;
} else {
n_height--;
}
}
i_width = n_width > 0 ? i_width : 0;
i_height = n_height > 0 ? i_height : 0;
}
if (i_width != 0) {
diam = rect_width[i_width];
} else {
if (i_height != 0) {
diam = rect_height[i_height];
} else {
diam = ( rect_width[rect_width.size() / 2] + rect_height[rect_height.size() / 2] ) / 2;
}
}
// get_circles() ---------------
double ar;
for ( int i = 0; i < ctrs.size(); i++ ) {
ar = rect_width[i] / double(rect_height[i]);
// in order to label the contour as a question, region
// should be sufficiently wide, sufficiently tall, and
// have an aspect ratio approximately equal to 1
// if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1:
if (diam*0.8 <= rect_width[i] && rect_width[i] <= diam*1.2 && 0.8 <= ar && ar <= 1.2) {
dotCtrs.push_back(ctrs[i]);
dot_x.push_back(rect_x[i]);
dot_y.push_back(rect_x[i]);
}
}
// questionCtrs, boundingBoxes, xs, ys = sort_contours(dotCtrs)
// choose tolerance for x, y coordinates of the bounding boxes to be binned together
tol = 0.7 * diam;
// change x coordinates of bounding boxes to their corresponding bins
sort(dot_x.begin(), dot_x.end(), compare_dot_x);
m = dot_x[0]; index_m = 0;
for (int i = 0; i < dot_x.size(); i++) {
if ( (m - tol < dot_x[i] && dot_x[i] < m) || (m < dot_x[i] && dot_x[i] < m + tol) ) {
dot_x[i] = m; index_m = i;
} else {
if (dot_x[i] > m + diam) {
for (int j = index_m; j < dot_x.size(); j++) {
if (dot_x[j] > m + diam) {
m = dot_x[j];
break;
}
}
}
}
}
sort(dot_x.begin(), dot_x.end(), compare_dot_x);
// change y coordinates of bounding boxes to their corresponding bins
sort(dot_y.begin(), dot_y.end(), compare_dot_y);
m = dot_y[0]; index_m = 0;
for (int i = 0; i < dot_y.size(); i++) {
if ( (m - tol < dot_y[i] && dot_y[i] < m) || (m < dot_y[i] && dot_y[i] < m + tol) ) {
dot_y[i] = m; index_m = i;
} else {
if (dot_y[i] > m + diam) {
for (int j = index_m; j < dot_y.size(); j++) {
if (dot_y[j] > m + diam) {
m = dot_y[j];
break;
}
}
}
}
}
sort(dot_y.begin(), dot_y.end(), compare_dot_y);
// (ctrs, BB) = zip(*sorted(zip(ctrs, BB), key = lambda b: b[1][1]*len(image) + b[1][0]))
sort(dotCtrs.begin(), dotCtrs.end(), compare_dotCtrs);
// linesV, d1, d2, d3, spacingX, spacingY = get_spacing() ------------------------------
int c, d1, d2, d3, prev, diff;
vector<int> spacingX, spacingY, linesV;
// spacingX
for (int i = 0; i < dot_x.size() - 1; i++) {
c = dot_x[i+1] - dot_x[i];
if (c > diam / 2) {
spacingX.push_back(c);
}
}
sort(spacingX.begin(), spacingX.end(), compare_spacingX);
// spacingY
for (int i = 0; i < dot_y.size() - 1; i++) {
c = dot_y[i+1] - dot_y[i];
if (c > diam / 2) {
spacingY.push_back(c);
}
}
sort(spacingY.begin(), spacingY.end(), compare_spacingY);
// smallest x-serapation (between two adjacent dots in a letter)
m = *min_element(spacingX.begin(), spacingX.end());
c = 0;
d1 = spacingX[0];
d2 = 0;
d3 = 0;
for (int i = 0; i < spacingX.size(); i++) {
if (d2 == 0 && spacingX[i] > d1 * 1.3) {
d2 = spacingX[i];
}
if (d2 > 0 && spacingX[i] > d2 * 1.3) {
d3 = spacingX[i];
break;
}
}
prev = 0; // outside
linesV.push_back(*min_element(dot_x.begin(), dot_x.end()) - ((d2 - diam) / 2));
for (int i = 1; i < dot_x.size(); i++) {
diff = dot_x[i] - dot_x[i-1];
if (i == 1 && d2 * 0.9 < diff) {
linesV.push_back(*min_element(dot_x.begin(), dot_x.end()) - d2 - diam/2);
prev = 1;
}
if (d1 * 0.8 < diff && diff < d1 * 1.2) {
linesV.push_back(dot_x[i-1] + diam + (d1 - diam)/2);
prev = 1;
} else if (d2 * 0.8 < diff && diff < d2 * 1.1) {
linesV.push_back(dot_x[i-1] + diam + (d2 - diam)/2);
prev = 0;
} else if (d3 * 0.9 < diff && diff < d3 * 1.1) {
if (prev == 1) {
linesV.push_back(dot_x[i-1] + diam + (d2 - diam)/2);
linesV.push_back(dot_x[i-1] + d2 + diam + (d1 - diam)/2);
} else {
linesV.push_back(dot_x[i-1] + diam + (d1 - diam)/2);
linesV.push_back(dot_x[i-1] + d1 + diam + (d2 - diam)/2);
}
} else if (d3 * 1.1 < diff) {
if (prev == 1) {
linesV.push_back(dot_x[i-1] + diam + (d2 - diam)/2);
linesV.push_back(dot_x[i-1] + d2 + diam + (d1 - diam)/2);
linesV.push_back(dot_x[i-1] + d3 + diam + (d2 - diam)/2);
prev = 0;
} else {
linesV.push_back(dot_x[i-1] + diam + (d1 - diam)/2);
linesV.push_back(dot_x[i-1] + d1 + diam + (d2 - diam)/2);
linesV.push_back(dot_x[i-1] + d1 + d2 + diam + (d1 - diam)/2);
linesV.push_back(dot_x[i-1] + d1 + d3 + diam + (d2 - diam)/2);
prev = 1;
}
}
}
linesV.push_back(*max_element(dot_x.begin(), dot_x.end()) + diam * 1.5);
if (linesV.size() % 2 == 0) {
linesV.push_back(*max_element(dot_x.begin(), dot_x.end()) + d2 + diam);
}
// get_letters() --------------------------------------------
double minYD;
vector<int> letters_dots_x;
vector<int> letters_dots_y;
bool showID = false;
//
minYD = 0;
dot_x.push_back(100000);
dot_y.push_back(0);
sort(spacingY.begin(), spacingY.end(), compare_spacingY);
for (int i = 0; i < spacingY.size(); i++) {
if (spacingY[i] > 1.3 * diam) {
minYD = spacingY[i] * 1.5;
break;
}
}
// get lines of dots
for (int i = 0; i < dot_x.size() - 1; i++) {
if (dot_x[i] < dot_x[i+1]) {
if (showID) {
letters_dots_x.push_back(dot_x[i]);
letters_dots_y.push_back(dot_y[i]);
} else {
letters_dots_x.push_back(dot_x[i]);
}
} else {
if (abs(dot_y[i+1] - dot_y[i]) < minYD) {
if (showID) {
letters_dots_x.push_back(dot_x[i]);
letters_dots_y.push_back(dot_y[i]);
} else {
letters_dots_x.push_back(dot_x[i]);
}
letters_dots_x.push_back(0);
letters_dots_y.push_back(0);
} else {
if (showID) {
letters_dots_x.push_back(dot_x[i]);
letters_dots_y.push_back(dot_y[i]);
} else {
letters_dots_x.push_back(dot_x[i]);
}
letters_dots_x.push_back(0);
letters_dots_y.push_back(0);
if (letters_dots_x.size() % 3 == 0) {
letters_dots_x.push_back(0);
letters_dots_y.push_back(0);
}
}
}
}
//
vector<int> letters;
int count = 0;
int ii;
for (int ri = 0; ri < letters_dots_x.size(); ri++) {
if (letters_dots_x[ri] == 0) {
//letters.append([0 for _ in range(len(linesV)-1)])
letters.push_back(0);
continue;
} else {
letters.push_back(0);
c = 0; ii = 0;
while (ii < linesV.size() - 1) {
//if ( c < letters_dots_x[ri].size() ) {
if ( linesV[ii] < letters_dots_x[ri] && letters_dots_x[ri] < linesV[ii+1] ) {
letters.push_back(1);
c++;
} else {
letters.push_back(0);
}
//} else {
// letters.push_back(0);
//}
ii++;
}
}
}
//
}
long long int get_now() {
return chrono::duration_cast<std::chrono::milliseconds>(
chrono::system_clock::now().time_since_epoch()
).count();
}
void platform_log(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
#ifdef __ANDROID__
__android_log_vprint(ANDROID_LOG_VERBOSE, "ndk", fmt, args);
#else
vprintf(fmt, args);
#endif
va_end(args);
}
// Avoiding name mangling
extern "C" {
__attribute__((visibility("default"))) __attribute__((used))
void braille_recognition(char* inputImagePath, char* outputImagePath) {
long long start = get_now();
/*
Mat input = imread(inputImagePath, IMREAD_GRAYSCALE);
Mat threshed, withContours;
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
// Doing a lot of work
for (int i = 0; i < 1; i++) {
adaptiveThreshold(input, threshed, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 77, 6);
findContours(threshed, contours, hierarchy, RETR_TREE, CHAIN_APPROX_TC89_L1);
}
cvtColor(threshed, withContours, COLOR_GRAY2BGR);
drawContours(withContours, contours, -1, Scalar(0, 255, 0), 4);
imwrite(outputImagePath, withContours);
*/
int evalInMillis = static_cast<int>(get_now() - start);
platform_log("Processing done in %dms\n", evalInMillis);
}
}
# Общее описание решения
Проект является цифровым решением в рамках конкурса World AI&DATA Challenge, соответствующиим задаче
["Braille text optical recognition"](https://git.asi.ru/tasks/world-ai-and-data-challenge/braille-text-optical-recognition)
Задача состоит в автоматическом распознавании текстов, написанных с помощью алфавита Брайля, и их перевода на русский и английский.
Распознавание должно справляться с фотографиями и сканированными изображениями, полученными без использования профессионального оборудования.
Мой школьный проект “**Распознавание Азбуки Брайля**” очень важен для создания **равных возможностей** для слабовидящих учеников, чтобы они тоже могли участвовать во
Всероссийской олимпиаде школьников и многих других конкурсах. Среди них много талантливых учеников, и я рада помочь им стать успешными.
Опробованы алгоритмы **улучшения качества изображения**, **выпрямления изогнутого листа книги**, **устранение перспективных искажений**, **распознавания всевозможных кодов Брайля со всеми нюансами**,
включая два математических **(Немет и Марбург**), поддерживаются **110 языков** (библиотека **LibLouis**). Создан прототип мобильного приложения на Flutter с
поддержкой **Android, iOS, Web, Linux, Windows** (проверено пока только на Android) **без использования сервисов, интернета, машинного обучения**. Продолжаю реализацию на С++ для быстрого исполнения.
На всех этапах используются **быстрые алгоритмы**, минимизировано использование OpenCV, не используется машинное обучение (чтобы увеличить скорость). Решение работает на **мобильном телефоне без использования сервисов и интернета**, сохраняя **конфиденциальность** распознаваемых изображений кодов Брайля.
Это может быть полезно при оперативной проверки работы на экзамене или в условиях отсутствия интернета, **исключая утечки решенных заданий**.
Посмотрите [**Презентацию решения**](https://git.asi.ru/ElenaFomina/Braille/blob/master/%D0%9F%D1%80%D0%B5%D0%B7%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D1%8F_%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D1%8F.pptx)
Посмотрите [**Демонстрацию решения**](https://drive.google.com/drive/folders/1hiuIZKw9gjYjpgO1yhMArczSbGuqrIK3?usp=sharing)
**Открытый исходный код** мобильного приложения можно скачать по ссылке:
(Мобильное приложение написано на Flutter, его можно открыть в Android Studio, установив плагин Flutter)
https://drive.google.com/drive/folders/1ZR80eGAbDia8q9j8ahrow2qyxHbhTPaW?usp=sharing
**Мобильное приложение можно скачать по ссылке** (файл .apk для Андроид):
https://drive.google.com/drive/folders/1EQgEQBYoteiylipXarrg3Ui32SJZuyqE?usp=sharing
**Масштабируемость.** Приложение не требует больших вычислительных ресурсов и работает на мобильном телефоне (не используется машинное обучение), приложение не использует интернет, сохраняет конфиденциальность данных и соблюдает закон России о персональных данных. Поддерживает 110 языков и всевозможные коды Брайля со всеми нюансами. Мобильное приложение создано на Flutter с поддержкой Android, iOS, Web, desktop.
**Для исключения неудачно сфотографированных изображений Брайля** в текущем решении “на лету” распознается текст, планирую добавить распознавание кодов Брайля. Перед тем как сфотографировать можно будет выбрать удачный ракурс позволяющий распознать весь текст. Часто рисунки изображают кодом Брайля, при распознавании “на лету” это можно учесть.
## Общее описание логики работы решения
![](Images/P_STEPS.JPG)
Быстрые алгоритмы описаны в [**Презентации решения**](https://git.asi.ru/ElenaFomina/Braille/blob/master/%D0%9F%D1%80%D0%B5%D0%B7%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D1%8F_%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D1%8F.pptx)
## Что удалось сделать и дальнейшие планы
**Сделано:**
* Опробованы **алгоритмы улучшения качества изображения**, **выпрямления изогнутого листа книги**, **устранение перспективных искажений** на Python, перевожу эти алгоритмы на С++ для мобильного приложения.
* Создан прототип **мобильного приложения** на Flutter с поддержкой **Android, iOS, Web, Linux, Windows** (проверено пока только на Android) с примером перевода кода Брайля в текст.
* **Распознавание текста "на лету" с камеры** (библиотека tesseract) (планирую добавить распознавание кода Брайля "на лету")
* **Улучшение качества изображения**
* **Устранение перспективных искажений** изображения с возможной ручной корректировкой границ.
* **Выпрямление изогнутого листа книги** (алгоритм реализован на Python, перевожу его на С++ для мобильного приложения).
* **Распознавание всевозможных кодов Брайля с поддержкой 110 языков** и двух математических кодов Брайля **(Немет и Марбург**)
* **Озвучивание распознанного текста** с автоматическим определением языка или с указанием языка.
* **Перевод на другой язык** с автоматическим определением языка источника или с указанием языка источника, и озвучивание переведенного текста.
* Подключены библиотеки liblouis и OpenCV в мобильное приложение
* В мобильном приложении реализованы различные источники изображения, хранение, поиск изображений, пересылка изображений по разным каналам связи, сохранение изображения в PDF, печать.
**Планы развития:**
* Доделать перевод с Python на С++ алгоритмов улучшения качества изображения, выпрямления изогнутого листа книги в мобильном приложении
* Для исключения неудачно сфотографированных изображений Брайля **в текущем решении “на лету” распознается текст**, планирую добавить распознавание кодов Брайля для смешанных текстов (код Брайля и текст). Перед тем как сфотографировать можно будет выбрать удачный ракурс позволяющий распознать весь текст. Часто рисунки изображают кодом Брайля, при распознавании “на лету” это можно учесть.
**Улучшение качества изображения**
![](Images/P_CLEAR.JPG)
**Выпрямление изогнутого листа книги**
![](Images/P_DEWARP.JPG)
**Быстрое распознавание кода Брайля без использования машинного обучения**
![](Images/P_BRAILLE.JPG)
**Поддерживаемые языки распознавания кода Брайля:**
• af - африкаанс
• am - амхарский
• ar - арабский
• as - ассамский
• az - азербайджанский
• be - белорусский
• bg - болгарский
• bn - бенгальский бангла
• bs - боснийский
• ca - каталонский валенсийский
• cs - Чешский
• da - датский
• de - Немецкий (плюс вариант одной страны)
• el - современный греческий
• en - Английский (плюс 8 вариантов страны)
• es - Испанский кастильский (плюс 20 вариантов страны)
• et - эстонский
• eu - баскский
• fa - персидский
• fi - финский
• fil - филиппинский пилипинский
• fr - Французский (плюс вариант одной страны)
• gl - галицкий
• gsw - швейцарский немецкий алеманский эльзасский
• gu - гуджарати
• he - иврит
• hi - хинди
• hr - хорватский
• hu - Венгерский язык
• hy - армянский
• id - индонезийский
• is - исландский
• it - итальянский
• ja - Японский язык
• ka - грузинский
• kk - казахский
• km - кхмерский центральный кхмерский
• kn - каннада
• ko - Корейский язык
• ky - киргизский киргизский
• lo - Лаосский
• lt - Литовский
• lv - Латышский
• mk - македонский
• ml - малаялам
• mn - монгольский
• mr - маратхи
• ms - малайский
• my - бирманский
• nb - Норвежский букмол
• ne - непальский
• nl - голландский фламандский
• no - Норвежский
• or - Ория
• pa - панджаби пенджаби
• pl - польский
• ps - пушту пушту
• pt - Португальский (плюс вариант для одной страны)
• ro - румынский молдавский молдавский
• ru - Русский
• si - сингальский сингальский
• sk - словацкий
• sl - словенский
• sq - албанский
• sr - сербский (плюс 2 скрипта)
• sv - шведский
• sw - суахили
• ta - тамильский
• te - телугу
• th - тайский
• tl - тагальский
• tr - Турецкий
• uk - Украинец
• ur - урду
• uz - узбекский
• vi - вьетнамский
• zh - Китайский (плюс 2 варианта страны и 2 алфавита)
• zu - зулусский
## Требования к окружению для запуска продукта
* Android Studio 4.0 (установленное на Linux, Mac или Windows)
* Android-устройство версией 7.0 или выше
Используемый язык программирования: **C/С++, Dart, Java. Python для исследования**.
Библиотеки и компоненты: **liblouis, OpenCV, пакеты Flutter**
Сторонние сервисы: **Для распознавания кода Брайля сторонние сервисы не использовались**, **распознавание работает быстро** на малых ресурсах мобильного телефона, **распознавание работает без интернета** (это может быть полезно при оперативной проверки работы на экзамене, **исключая утечки решений**)
Среда для запуска: Мобильное приложение написано на Flutter с поддержкой Android, iOS, Web, Linux, Windows (проверено пока только на Android)
## Сценарий сборки и запуска проекта
### Запуск мобильного приложения на Android
Откройте файл .apk на своем мобильном телефоне по ссылке:
https://drive.google.com/drive/folders/1EQgEQBYoteiylipXarrg3Ui32SJZuyqE?usp=sharing
### Сценарий сборки
### Шаг 1. Клонирование исходного кода приложения
Скопируйте каталог с исходным кодом на свой компьютер по ссылке:
https://drive.google.com/drive/folders/1ZR80eGAbDia8q9j8ahrow2qyxHbhTPaW?usp=sharing
### Шаг 2. Импортируйте исходный код приложения в Android Studio
Откройте исходный код в Android Studio. Для этого откройте Android Studio и выберите `Open an existing project`, затем путь к архиву с исходным кодом.
### Шаг 3. Соберите проект Android Studio
Выберите `Build - > Make Project` и убедитесь, что проект успешно построен.
По любым вопросам обращайтесь на fomina4950@gmail.com
## Примеры использования
Посмотрите [**Демонстрацию решения**](https://drive.google.com/drive/folders/1hiuIZKw9gjYjpgO1yhMArczSbGuqrIK3?usp=sharing)
## Используемые наборы данных
[Набор данных](https://git.asi.ru/tasks/world-ai-and-data-challenge/braille-text-optical-recognition/blob/master/Braile_Photos_and_Scans-20200127T071421Z-001.zip).
## Дополнительный инструментарий
Нет дополнительных инструментов, которые требуются для развёртывания решения.
## Пилотные внедрения своего решения
* Готова участвовать в реализации пилотных внедрений своего решения в субъектах РФ в период с сентября 2020 по январь 2021.
* Это мой школьный проект, который мне **нужно сделать и провести пилотное внедрение как раз в эти же сроки**!!! Планирую пилотное внедрение моего решения на базе **ГБОУ Москвы “Школа-интернат № 1 для обучения и реабилитации слепых”**, как и последующее внедрение в других интернатах России и мира с поддержкой 110 языков.
* Участвовала в конкурсе **Большая Перемена** https://bolshayaperemena.online/ на этапе **Призовой фонд** заполнила обоснование на возможность получить **“Школе-интернату № 1 для обучения и реабилитации слепых”** или **Всероссийскому обществу слепых** 2 млн. руб. призовых на развитие и внедрение проекта Брайль.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment