241 lines
12 KiB
C++
241 lines
12 KiB
C++
#pragma once
|
|
#include "screenshot.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <vector>
|
|
|
|
namespace ks::stroke_detail {
|
|
struct Point { double x, y; };
|
|
inline double distance(Point a, Point b) { return std::hypot(a.x-b.x,a.y-b.y); }
|
|
inline double length(const std::vector<Point> &p) {
|
|
double total=0;
|
|
for(size_t i=1;i<p.size();++i) total+=distance(p[i-1],p[i]);
|
|
return total;
|
|
}
|
|
// Arc-length sampling makes fitting independent of mouse speed and bounds its cost.
|
|
inline std::vector<Point> sample(const std::vector<Point> &p, size_t count) {
|
|
std::vector<Point> out{p.front()};
|
|
double step=length(p)/(count-1), traversed=0;
|
|
size_t segment=1;
|
|
for(size_t i=1;i+1<count;++i) {
|
|
double target=i*step;
|
|
while(segment+1<p.size() && traversed+distance(p[segment-1],p[segment])<target) {
|
|
traversed+=distance(p[segment-1],p[segment]); ++segment;
|
|
}
|
|
double span=distance(p[segment-1],p[segment]);
|
|
double t=span>0?(target-traversed)/span:0;
|
|
out.push_back({p[segment-1].x+t*(p[segment].x-p[segment-1].x),
|
|
p[segment-1].y+t*(p[segment].y-p[segment-1].y)});
|
|
}
|
|
out.push_back(p.back()); return out;
|
|
}
|
|
inline double segmentDistance(Point p, Point a, Point b) {
|
|
double dx=b.x-a.x,dy=b.y-a.y,den=dx*dx+dy*dy;
|
|
double t=den?std::clamp(((p.x-a.x)*dx+(p.y-a.y)*dy)/den,0.0,1.0):0;
|
|
return distance(p,{a.x+t*dx,a.y+t*dy});
|
|
}
|
|
inline double cross(Point a, Point b, Point c) { return (b.x-a.x)*(c.y-a.y)-(b.y-a.y)*(c.x-a.x); }
|
|
}
|
|
|
|
namespace ks {
|
|
// Returns a replacement path; an empty result means no visible improvement.
|
|
// Report recognized geometry separately from a smoothed freehand path.
|
|
inline std::vector<POINT> improveScreenshotStroke(const std::vector<POINT> &input, ScreenshotShape *shape=nullptr,
|
|
bool *straightLine=nullptr) {
|
|
if(shape) *shape=ScreenshotShape::None;
|
|
if(straightLine) *straightLine=false;
|
|
using namespace stroke_detail;
|
|
if(input.size()<3) return {};
|
|
std::vector<Point> raw;
|
|
for(auto p:input) if(raw.empty() || distance(raw.back(),{double(p.x),double(p.y)})>0)
|
|
raw.push_back({double(p.x),double(p.y)});
|
|
double total=length(raw);
|
|
if(raw.size()>=3) {
|
|
double span=distance(raw.front(),raw.back());
|
|
if(span>=24 && total<=span*1.35) {
|
|
double tolerance=std::clamp(span*.03,2.0,6.0),maximum=0,backtracking=0,previous=0;
|
|
Point a=raw.front(),b=raw.back();
|
|
// Check every original point so a short excursion cannot disappear in resampling.
|
|
for(auto p:raw) {
|
|
maximum=std::max(maximum,segmentDistance(p,a,b));
|
|
double along=((p.x-a.x)*(b.x-a.x)+(p.y-a.y)*(b.y-a.y))/span;
|
|
backtracking+=std::max(0.0,previous-along); previous=along;
|
|
}
|
|
double squared=0;
|
|
for(auto p:sample(raw,64)) { double d=segmentDistance(p,a,b); squared+=d*d; }
|
|
if(maximum<=tolerance && std::sqrt(squared/64)<=tolerance*.7 &&
|
|
backtracking<=std::max(2.0,span*.02)) {
|
|
if(straightLine) *straightLine=true;
|
|
return {input.front(),input.back()};
|
|
}
|
|
}
|
|
}
|
|
if(raw.size()<5 || total<16) return {};
|
|
auto points=sample(raw,128);
|
|
double left=points[0].x,right=left,top=points[0].y,bottom=top;
|
|
for(auto p:points) { left=std::min(left,p.x); right=std::max(right,p.x); top=std::min(top,p.y); bottom=std::max(bottom,p.y); }
|
|
double scale=std::min(right-left,bottom-top);
|
|
std::vector<Point> best;
|
|
double bestScore=1e30;
|
|
double gap=distance(raw.front(),raw.back());
|
|
if(scale>=20 && gap<=scale*.5) {
|
|
auto consider=[&](std::vector<Point> candidate, ScreenshotShape kind=ScreenshotShape::Rectangle) {
|
|
bool oval=kind==ScreenshotShape::Circle || kind==ScreenshotShape::Ellipse;
|
|
if(!oval && gap>scale*.22) return;
|
|
double perimeter=length(candidate);
|
|
if(total<perimeter*.82 || total>perimeter*1.25) return;
|
|
auto probes=sample(candidate,128);
|
|
double sum=0,maximum=0;
|
|
auto measure=[&](const std::vector<Point> &a,const std::vector<Point> &b) {
|
|
for(auto p:a) {
|
|
double d=1e30;
|
|
for(size_t i=1;i<b.size();++i) d=std::min(d,segmentDistance(p,b[i-1],b[i]));
|
|
sum+=d*d; maximum=std::max(maximum,d);
|
|
}
|
|
};
|
|
measure(points,candidate); measure(probes,points);
|
|
double score=std::sqrt(sum/256);
|
|
if(maximum<=std::max(3.0,scale*(oval?.22:.12)) && score<=std::max(1.5,scale*(oval?.085:.055)) && score<bestScore) {
|
|
bestScore=score; best=std::move(candidate);
|
|
if(shape) *shape=kind;
|
|
}
|
|
};
|
|
// Fit oriented bounding boxes; testing angles also handles tilted ovals.
|
|
for(int degrees=0;degrees<180;degrees+=5) {
|
|
double angle=degrees*3.141592653589793/180,cs=std::cos(angle),sn=std::sin(angle);
|
|
double x0=1e30,y0=x0,x1=-x0,y1=-x0;
|
|
for(auto p:points) {
|
|
double x=p.x*cs+p.y*sn,y=-p.x*sn+p.y*cs;
|
|
x0=std::min(x0,x); x1=std::max(x1,x); y0=std::min(y0,y); y1=std::max(y1,y);
|
|
}
|
|
auto world=[&](double x,double y)->Point { return {x*cs-y*sn,x*sn+y*cs}; };
|
|
consider({world(x0,y0),world(x1,y0),world(x1,y1),world(x0,y1),world(x0,y0)});
|
|
double rx=(x1-x0)/2,ry=(y1-y0)/2;
|
|
double cx=(x0+x1)/2,cy=(y0+y1)/2;
|
|
std::vector<Point> local;
|
|
for(auto p:points) local.push_back({p.x*cs+p.y*sn,-p.x*sn+p.y*cs});
|
|
// Fit four sides to their supporting samples. Extremal bounds exaggerate
|
|
// crooked corners and can unfairly make an oval score better than a box.
|
|
double sides[]{x0,x1,y0,y1};
|
|
bool supported=true;
|
|
for(int iteration=0;iteration<6;++iteration) {
|
|
double sums[4]{}; int counts[4]{};
|
|
for(auto p:local) {
|
|
double distances[]{std::abs(p.x-sides[0]),std::abs(p.x-sides[1]),
|
|
std::abs(p.y-sides[2]),std::abs(p.y-sides[3])};
|
|
auto side=std::min_element(distances,distances+4)-distances;
|
|
sums[side]+=side<2?p.x:p.y; ++counts[side];
|
|
}
|
|
for(int side=0;side<4;++side) {
|
|
if(counts[side]<10) { supported=false; break; }
|
|
sides[side]=sums[side]/counts[side];
|
|
}
|
|
if(!supported) break;
|
|
}
|
|
if(supported && sides[1]-sides[0]>=16 && sides[3]-sides[2]>=16)
|
|
consider({world(sides[0],sides[2]),world(sides[1],sides[2]),
|
|
world(sides[1],sides[3]),world(sides[0],sides[3]),world(sides[0],sides[2])});
|
|
// Fit the whole gesture instead of letting a single mouse wobble set the bounds.
|
|
auto error=[&](double x,double y,double a,double b) {
|
|
if(a<8 || b<8) return 1e30;
|
|
double sum=0;
|
|
for(auto p:local) {
|
|
double u=(p.x-x)/a,v=(p.y-y)/b,r=std::hypot(u,v);
|
|
double normal=std::hypot(u/a,v/b);
|
|
double d=normal>1e-9?(r-1)*r/normal:std::min(a,b);
|
|
sum+=d*d;
|
|
}
|
|
return sum;
|
|
};
|
|
double fit=error(cx,cy,rx,ry),step=std::max(rx,ry)*.08;
|
|
for(int iteration=0;iteration<10;++iteration) {
|
|
for(int parameter=0;parameter<4;++parameter) for(int sign:{-1,1}) {
|
|
double values[]{cx,cy,rx,ry}; values[parameter]+=sign*step;
|
|
double next=error(values[0],values[1],values[2],values[3]);
|
|
if(next<fit) { fit=next; cx=values[0]; cy=values[1]; rx=values[2]; ry=values[3]; }
|
|
}
|
|
step*=.65;
|
|
}
|
|
// Require almost one complete turn, with little retracing. This admits a small
|
|
// closing gap or overshoot but rejects C-shaped arcs, spirals and scribbles.
|
|
double turn=0,travel=0;
|
|
for(size_t i=1;i<local.size();++i) {
|
|
double a=std::atan2((local[i-1].y-cy)/ry,(local[i-1].x-cx)/rx);
|
|
double b=std::atan2((local[i].y-cy)/ry,(local[i].x-cx)/rx);
|
|
double delta=std::remainder(b-a,2*3.141592653589793);
|
|
turn+=delta; travel+=std::abs(delta);
|
|
}
|
|
if(std::abs(turn)<5.4 || std::abs(turn)>7.1 || travel-std::abs(turn)>.65) continue;
|
|
// Near-circular gestures become true circles.
|
|
bool circle=std::abs(rx-ry)<=std::max(rx,ry)*.12;
|
|
if(circle) rx=ry=(rx+ry)/2;
|
|
std::vector<Point> oval;
|
|
for(int i=0;i<96;++i) {
|
|
double t=i*2*3.141592653589793/96;
|
|
oval.push_back(world(cx+rx*std::cos(t),cy+ry*std::sin(t)));
|
|
}
|
|
oval.push_back(oval.front()); consider(std::move(oval),circle?ScreenshotShape::Circle:ScreenshotShape::Ellipse);
|
|
}
|
|
auto sorted=points;
|
|
std::sort(sorted.begin(),sorted.end(),[](Point a,Point b){return a.x<b.x || (a.x==b.x && a.y<b.y);});
|
|
std::vector<Point> hull;
|
|
for(auto p:sorted) {
|
|
while(hull.size()>1 && cross(hull[hull.size()-2],hull.back(),p)<=0) hull.pop_back();
|
|
hull.push_back(p);
|
|
}
|
|
size_t lower=hull.size();
|
|
for(size_t i=sorted.size()-1;i-->0;) {
|
|
auto p=sorted[i];
|
|
while(hull.size()>lower && cross(hull[hull.size()-2],hull.back(),p)<=0) hull.pop_back();
|
|
hull.push_back(p);
|
|
}
|
|
hull.pop_back();
|
|
while(hull.size()>3) {
|
|
if(hull.size()==4 && bestScore>scale*.035) {
|
|
// Opposite edges must already be nearly parallel; retain their unequal
|
|
// lengths and fit a shared direction instead of accepting any quadrilateral.
|
|
for(size_t i=0;i<2;++i) {
|
|
Point a=hull[i],b=hull[i+1],c=hull[(i+2)%4],d=hull[(i+3)%4];
|
|
double ab=distance(a,b),cd=distance(c,d);
|
|
if(std::min(ab,cd)<16 || std::min(ab,cd)/std::max(ab,cd)>.8) continue;
|
|
Point u{(b.x-a.x)/ab,(b.y-a.y)/ab},v{(c.x-d.x)/cd,(c.y-d.y)/cd};
|
|
if(u.x*v.x+u.y*v.y<.96) continue;
|
|
double norm=std::hypot(u.x+v.x,u.y+v.y);
|
|
Point axis{(u.x+v.x)/norm,(u.y+v.y)/norm};
|
|
auto along=[&](Point p){return p.x*axis.x+p.y*axis.y;};
|
|
auto across=[&](Point p){return -p.x*axis.y+p.y*axis.x;};
|
|
double y0=(across(a)+across(b))/2,y1=(across(c)+across(d))/2;
|
|
if(std::abs(y1-y0)<20) continue;
|
|
auto fit=[&](Point p,double y)->Point {double x=along(p);return {x*axis.x-y*axis.y,x*axis.y+y*axis.x};};
|
|
std::vector<Point> candidate{fit(a,y0),fit(b,y0),fit(c,y1),fit(d,y1),fit(a,y0)};
|
|
consider(std::move(candidate),ScreenshotShape::Trapezoid);
|
|
}
|
|
}
|
|
size_t remove=0; double area=1e30;
|
|
for(size_t i=0;i<hull.size();++i) {
|
|
double a=std::abs(cross(hull[(i+hull.size()-1)%hull.size()],hull[i],hull[(i+1)%hull.size()]));
|
|
if(a<area) { area=a; remove=i; }
|
|
}
|
|
hull.erase(hull.begin()+remove);
|
|
}
|
|
if(hull.size()==3) { hull.push_back(hull.front()); consider(hull,ScreenshotShape::Triangle); }
|
|
}
|
|
if(best.empty()) {
|
|
// A small spatial filter suppresses jitter without moving endpoints or flattening broad curves.
|
|
best=sample(raw,size_t(std::clamp(std::ceil(total/2)+1,5.0,2048.0)));
|
|
auto original=best;
|
|
for(size_t i=1;i+1<best.size();++i)
|
|
best[i]={(original[i-1].x+2*original[i].x+original[i+1].x)/4,
|
|
(original[i-1].y+2*original[i].y+original[i+1].y)/4};
|
|
}
|
|
std::vector<POINT> result;
|
|
for(auto p:best) {
|
|
POINT q{LONG(std::lround(p.x)),LONG(std::lround(p.y))};
|
|
if(result.empty() || result.back().x!=q.x || result.back().y!=q.y) result.push_back(q);
|
|
}
|
|
if(result.size()==input.size() && std::equal(result.begin(),result.end(),input.begin(),
|
|
[](POINT a,POINT b){return a.x==b.x && a.y==b.y;})) return {};
|
|
return result;
|
|
}
|
|
}
|