Files
MagicSetEditor2/src/data/format/image_to_symbol.cpp
T
coppro b73f081b60 Updated copyright information (2009 + my real name)
MSE now handles opening more gracefully - can handle directories and failures
Changed behavior to always save cards to separate files with intent to add VCS later (note: shouldn't do this for zip, but can't see a good way to approach this)


git-svn-id: svn://svn.code.sf.net/p/magicseteditor/code/trunk@1388 0fc631ac-6414-0410-93d0-97cfa31319b6
2009-06-19 03:09:05 +00:00

483 lines
15 KiB
C++

//+----------------------------------------------------------------------------+
//| Description: Magic Set Editor - Program to make Magic (tm) cards |
//| Copyright: (C) 2001 - 2009 Twan van Laarhoven and Sean Hunt |
//| License: GNU General Public License 2 or later (see file COPYING) |
//+----------------------------------------------------------------------------+
// ----------------------------------------------------------------------------- : Includes
#if defined(_MSC_VER) && _MSC_VER >= 1400
// VS 8 has the audacity to give a warning about fill_n
// That is both STUPID and WRONG, so disable that warning
// This has to be done before includes, because the warning is reported in standard headers!
#pragma warning(disable:4996)
#endif
#include <util/prec.hpp>
#include <data/format/image_to_symbol.hpp>
#include <gfx/bezier.hpp>
#include <util/error.hpp>
#include <util/platform.hpp>
DECLARE_TYPEOF_COLLECTION(ControlPointP);
DECLARE_TYPEOF_COLLECTION(SymbolPartP);
// ----------------------------------------------------------------------------- : Image preprocessing
enum ImageMarker
{ EMPTY = 0 // This cell is empty
, FULL = 1 // This cell is full
, MARKED = 2 // This cell is full, but it has been used as a starting point for finding symbols
};
/// Convert an image to greyscale
/** The image becomes a single channel image, just an array of bytes.
* This means only the first 1/3 of the image data is used, and the image
* is no longer an actual image.
*/
void greyscale(Image& img) {
UInt size = img.GetWidth() * img.GetHeight();
Byte* data = img.GetData();
Byte* out = data;
for (UInt i = 0 ; i < size ; ++i) {
*out++ = (data[0] + data[1] + data[2]) / 3;
data += 3;
}
}
/// Thresholds an image, giving a black & white result
/** The threshold is determined automatically
* The output is stored in the data array,
* EMPTY for the 'border' color, FULL for the interior
*/
void threshold(Byte* data, int w, int h) {
size_t size = w * h;
// make histogram of data
size_t hist[256];
fill_n(hist,256,0);
for (size_t i = 0 ; i < size ; ++i) {
hist[data[i]]++;
}
// find threshold
size_t threshold_pos = size / 2;
int threshold = 255;
size_t below = 0;
for (int i = 0 ; i < 255 ; ++i) {
if (below + hist[i]/2 > threshold_pos) {
threshold = i;
break;
}
below += hist[i];
if (below >= threshold_pos) {
threshold = i + 1;
break;
}
}
// threshold data
for (size_t i = 0 ; i < size ; ++i) {
data[i] = data[i] >= threshold ? FULL : EMPTY;
}
// should the colors be inverted?
int border_count = 0;
for (int x = 0 ; x < w ; ++x) {
border_count += data[x] + data[x+(h-1)*w];
}
for (int y = 0 ; y < h ; ++y) {
border_count += data[y*w] + data[w-1+y*w];
}
if (border_count > w + h) {
// more then half the border if FULL, invert
for (size_t i = 0 ; i < size ; ++i) {
data[i] = data[i] == FULL ? EMPTY : FULL;
}
}
}
// ----------------------------------------------------------------------------- : Image to symbol
bool is_mse1_symbol(const Image& img) {
// mse1 symbols are 60x80
if (img.GetWidth() != 60 || img.GetHeight() != 80) return false;
// the right side is black & white
int delta = 0;
for (int y = 0 ; y < 80 ; ++y) {
Byte* d = img.GetData() + 3 * (y * 60 + 20);
for (int x = 20 ; x < 60 ; ++x) {
int r = *d++;
int g = *d++;
int b = *d++;
delta += abs(r - b) + abs(r - g) + abs(b - g);
}
}
if (delta > 5000) return false; // not black & white enough
// TODO : more checks?
return true;
}
struct ImageData {
int width, height;
Byte* data;
mutable Byte dummy;
inline Byte& operator () (int x, int y) const {
if (x < 0 || x >= width || y < 0 || y >= height) {
return (dummy = EMPTY); // outside, return empty
} else {
return data[x + y*width];
}
}
};
bool find_symbol_shape_start(const ImageData& data, int& x_out, int& y_out) {
for (int x = 0 ; x < data.width ; ++x) {
for (int y = 0 ; y < data.height ; ++y) {
if (data(x, y) == FULL && data(x, y-1) == EMPTY) {
// the point above must be clear, we don't want to start in the 'ground'
// also, we don't want to find things we found before
x_out = x;
y_out = y;
return true;
}
}
}
return false;
}
SymbolShapeP read_symbol_shape(const ImageData& data) {
// find start point
int xs, ys;
if (!find_symbol_shape_start(data, xs, ys)) return SymbolShapeP();
data(xs, ys) |= MARKED;
SymbolShapeP shape(new SymbolShape);
// walk around, clockwise
xs += 1; // start right of the found point, otherwise last_move might think we came from above
int x = xs, y = ys;
int old_x = x, old_y = y;
int last_move = 1; // 1 = right or down, (as in x|y += 1)
do {
// the cursor (x,y) is between four pounts:
// a b
// .
// c d
bool a = data(x-1, y-1) & FULL;
bool b = data(x, y-1) & FULL;
bool c = data(x-1, y ) & FULL;
bool d = data(x, y ) & FULL;
UInt pack = (a << 12) + (b << 8) + (c << 4) + d; // 0xabcd
switch (pack) {
case 0x0001 : x += 1; break;
case 0x0010 : y += 1; break;
case 0x0011 : x += 1; break;
case 0x0100 : y -= 1; break;
case 0x0101 : y -= 1; break;
case 0x0110 : y -= last_move; break; // diagonal, we can come here from two sides, from left and right
case 0x0111 : y -= 1; break; // last_move indicates which of {b,c} we are 'attached' to
case 0x1000 : x -= 1; break;
case 0x1001 : x += last_move; break;
case 0x1010 : y += 1; break;
case 0x1011 : x += 1; break;
case 0x1100 : x -= 1; break;
case 0x1101 : x -= 1; break;
case 0x1110 : y += 1; break;
default:
throw InternalError(_("in the ground/air"));
}
// add to shape and place a mark
shape->points.push_back(new_intrusive2<ControlPoint>(
double(x) / data.width,
double(y) / data.height
));
if (x > old_x) data(old_x, y) |= MARKED; // mark when moving right -> only mark the top of the shape
last_move = (x + y) - (old_x + old_y);
old_x = x;
old_y = y;
} while (x != xs || y != ys); // we will end up in the start point
// are we on the inside or the outside?
if (data(x-2,y-1) & FULL) {
shape->combine = SYMBOL_COMBINE_SUBTRACT;
} else {
shape->combine = SYMBOL_COMBINE_MERGE;
}
return shape;
}
SymbolP image_to_symbol(Image& img) {
int w = img.GetWidth(), h = img.GetHeight();
// 1. threshold the image
greyscale(img);
threshold(img.GetData(), w, h);
// 2. read as many symbol shapes as we can
ImageData data = {w,h,img.GetData()};
SymbolP symbol(new Symbol);
while (true) {
SymbolShapeP shape = read_symbol_shape(data);
if (!shape) break;
symbol->parts.push_back(shape);
}
reverse(symbol->parts.begin(), symbol->parts.end());
return symbol;
}
SymbolP import_symbol(Image& img) {
SymbolP symbol;
if (is_mse1_symbol(img)) {
Image img2 = img.GetSubImage(wxRect(20,0,40,40));
symbol = image_to_symbol(img2);
} else if (img.GetWidth() > 100 || img.GetHeight() > 100) {
// 100x100 ought to be enough, we trow out most afterwards data anyway
Image resampled = img.Rescale(100,100);
symbol = image_to_symbol(resampled);
} else {
symbol = image_to_symbol(img);
}
simplify_symbol(*symbol);
return symbol;
}
// ----------------------------------------------------------------------------- : Simplify symbol
/// Finds corners, marks corners as LOCK_FREE, non-corners as LOCK_DIR
/** A corner is a point that has an angle between tangent greater then a treshold
*/
void mark_corners(SymbolShape& shape) {
for (int i = 0 ; (size_t)i < shape.points.size() ; ++i) {
ControlPoint& current = *shape.getPoint(i);
Vector2D before = .6 * shape.getPoint(i-1)->pos + .2 * shape.getPoint(i-2)->pos + .1 * shape.getPoint(i-3)->pos + .1 * shape.getPoint(i-4)->pos;
Vector2D after = .6 * shape.getPoint(i+1)->pos + .2 * shape.getPoint(i+2)->pos + .1 * shape.getPoint(i+3)->pos + .1 * shape.getPoint(i+4)->pos;
before = (before - current.pos).normalized();
after = (after - current.pos).normalized();
if (dot(before,after) >= -0.25f) {
// corner
current.lock = LOCK_FREE;
} else {
current.lock = LOCK_DIR;
}
}
}
/// Merge adjacent corners
/** Triangles will result in adjecent corners:
* XX
* XXXX _ corner 1;
* XXXXXX _ corner 2;
* XXXX
* XX
*
* Not all adjectent corners should be merged, for example
* X _ 1
* XXXXXXXXXXXXX _ 2
* should be kept
*
* The solution is to look at the tangent lines.
* Where these two lines (one for each corner) intersect,
* is the merged corner. If it is too far away, don't merge
*/
void merge_corners(SymbolShape& shape) {
for (int i = 0 ; (size_t)i < shape.points.size() ; ++i) {
ControlPoint& cur = *shape.getPoint(i);
ControlPoint& prev = *shape.getPoint(i - 1);
if (prev.lock != LOCK_FREE || cur.lock != LOCK_FREE) continue;
// step 1. find tangent lines: try tangent lines to the first point, the second, etc.
// and take the one that has the largest angle with ab, i.e. the smallest dot,
// where ab is the line between the two corners
Vector2D ab = cur.pos - prev.pos;
double min_a_dot = 1e100, min_b_dot = 1e100;
Vector2D a, b;
for (int j = 0 ; j < 4 ; ++j) {
Vector2D a_ = (shape.getPoint(i-j-1)->pos - prev.pos).normalized();
Vector2D b_ = (shape.getPoint(i+j)->pos - cur.pos).normalized();
double a_dot = dot(a_, ab);
double b_dot = -dot(b_, ab);
if (a_dot < min_a_dot) {
min_a_dot = a_dot;
a = a_;
}
if (b_dot < min_b_dot) {
min_b_dot = b_dot;
b = b_;
}
}
// step 2. find intersection point, to solve:
// t a + ab = u b, solve for t,u
// Gives us:
// t = ab cross b / b cross a
double tden = max(0.00000001, cross(b,a));
double t = cross(ab,b) / tden;
// do these tangent lines intersect, and not too far away?
// if so, then the intersection point is the merged point
if (t >= 0 && t < 20.0) {
prev.pos += a * -t;
shape.points.erase(shape.points.begin() + i);
i -= 1;
}
}
}
/// Avarage/'blur' a symbol shape
void avarage(SymbolShape& shape) {
// create a copy of the points
vector<Vector2D> old_points;
FOR_EACH(p, shape.points) {
old_points.push_back(p->pos);
}
// avarage points
for (int i = 0 ; (size_t)i < shape.points.size() ; ++i) {
ControlPoint& p = *shape.getPoint(i);
if (p.lock == LOCK_DIR) {
p.pos = .25 * old_points[mod(i-1, old_points.size())]
+ .50 * p.pos
+ .25 * old_points[mod(i+1, old_points.size())];
}
}
}
/// Convert a symbol shape to curves
void convert_to_curves(SymbolShape& shape) {
// mark all segments as curves
for (int i = 0 ; (size_t)i < shape.points.size() ; ++i) {
ControlPoint& cur = *shape.getPoint(i);
ControlPoint& next = *shape.getPoint(i + 1);
cur.segment_after = SEGMENT_CURVE;
cur.segment_before = SEGMENT_CURVE;
cur.delta_after = (next.pos - cur.pos) / 3.0;
next.delta_before = (cur.pos - next.pos) / 3.0;
}
// make the curves smooth by enforcing direction constraints
FOR_EACH(p, shape.points) {
p->onUpdateLock();
}
}
/// Convert almost straight curves in a symbol shape to lines
void straighten(SymbolShape& shape) {
const double treshold = 0.2;
for (int i = 0 ; (size_t)i < shape.points.size() ; ++i) {
ControlPoint& cur = *shape.getPoint(i);
ControlPoint& next = *shape.getPoint(i + 1);
Vector2D ab = (next.pos - cur.pos).normalized();
Vector2D aa = cur.delta_after.normalized();
Vector2D bb = next.delta_before.normalized();
// if the area beneath the polygon formed by the handles is small
// then it is a straight line
double cpDot = abs(cross(aa,ab)) + abs(cross(bb,ab));
if (cpDot < treshold) {
cur.segment_after = next.segment_before = SEGMENT_LINE;
cur.delta_after = next.delta_before = Vector2D();
cur.lock = next.lock = LOCK_FREE;
}
}
}
/// Remove unneeded points between straight lines
void merge_lines(SymbolShape& shape) {
for (int i = 0 ; (size_t)i < shape.points.size() ; ++i) {
ControlPoint& cur = *shape.getPoint(i);
if (cur.segment_before != cur.segment_after) continue;
Vector2D a = shape.getPoint(i-1)->pos, b = cur.pos, c = shape.getPoint(i+1)->pos;
Vector2D ab = (a-b).normalized();
Vector2D bc = (b-c).normalized();
double angle_len = abs( atan2(ab.x,ab.y) - atan2(bc.x,bc.y)) * (a-c).lengthSqr();
bool keep = angle_len >= .0001;
if (!keep) {
shape.points.erase(shape.points.begin() + i);
i -= 1;
}
}
}
double cost_of_point_removal(SymbolShape& shape, int i);
void remove_point(SymbolShape& shape, int i);
/// Simplify a symbol shape by removing points
/** Always remove the point with the lowest cost,
* stop when the cost becomes too high
*/
void remove_points(SymbolShape& shape) {
const double treshold = 0.0002; // maximum cost
while (true) {
// Find the point with the lowest cost of removal
int best = -1;
double best_cost = 1e100;
for (int i = 0 ; (size_t)i < shape.points.size() ; ++i) {
double cost = cost_of_point_removal(shape, i);
if (cost < best_cost) {
best_cost = cost;
best = i;
}
}
if (best_cost > treshold) break;
// ... and remove it
remove_point(shape, best);
}
}
/// Cost of removing point i from a symbol shape
double cost_of_point_removal(SymbolShape& shape, int i) {
ControlPoint& cur = *shape.getPoint(i);
ControlPoint& prev = *shape.getPoint(i-1);
ControlPoint& next = *shape.getPoint(i+1);
if (cur.lock != LOCK_DIR) return 1e100; // don't remove corners
Vector2D before = cur.delta_before;
Vector2D after = cur.delta_after;
Vector2D ac = prev.pos - next.pos;
// Based on SinglePointRemoveAction
double bl = before.length() + 0.00001; // prevent division by 0
double al = after.length() + 0.00001;
double totl = bl + al;
// set new handle sizes
Vector2D after0 = prev.delta_after * totl / bl;
Vector2D before2 = next.delta_before * totl / al;
// determine closest point on the merged curve
BezierCurve c(prev.pos, prev.pos + after0, next.pos + before2, next.pos);
double t = bl/totl;
Vector2D np = cur.pos - c.pointAt(t);
// cost is distance to new point * length of line ~= area added/removed from shape
return np.length() * ac.length();
}
/// Remove a point from a bezier curve
/** See SinglePointRemoveAction for algorithm */
void remove_point(SymbolShape& shape, int i) {
ControlPoint& cur = *shape.getPoint(i);
ControlPoint& prev = *shape.getPoint(i-1);
ControlPoint& next = *shape.getPoint(i+1);
Vector2D before = cur.delta_before;
Vector2D after = cur.delta_after;
// Based on SinglePointRemoveAction
double bl = before.length() + 0.00001; // prevent division by 0
double al = after.length() + 0.00001;
double totl = bl + al;
// set new handle sizes
prev.delta_after *= totl / bl;
next.delta_before *= totl / al;
// remove
shape.points.erase(shape.points.begin() + i);
}
void simplify_symbol_shape(SymbolShape& shape) {
mark_corners(shape);
merge_corners(shape);
for (int i = 0 ; i < 3 ; ++i) {
avarage(shape);
}
convert_to_curves(shape);
remove_points(shape);
straighten(shape);
merge_lines(shape);
}
void simplify_symbol(Symbol& symbol) {
FOR_EACH(pb, symbol.parts) {
if (SymbolShape* p = pb->isSymbolShape()) {
simplify_symbol_shape(*p);
}
}
}