Image-Bitmap2Paths
view release on metacpan or search on metacpan
lib/Image/Bitmap2Paths.pm view on Meta::CPAN
# x CC
# / x f
# c Lxx
# E v
# Input encodes a sequence of rectangles made of grid squares; rectangles share UR/LL corners:
# â¡ is encoded as 2,4,4,3,1 (all positive)
# â¡â¡â¡
# â¡â¡â¡â¡
# â¡â¡â¡â¡
# â¡â¡
# We want to find a line which rasterizes to these squares, i.e., intersect the (âredâ) vertical disector of every square.
#
# It is the same as intersecting a (âgreenâ) horizontal line of length=1 centered at the shared corners,
# plus intersecting the red lines of the leftmost and the rightmost square. Suppose that non-on-edge rectanles
# are only of two sizes, s and s+1. Swapping x and y axis, and subtracting y'=y-sx moves the green lines to a
# collection of red lines of a new configuration of rectanges. This gives a step of recursion. Above configuration is moved to
# â¡â¡
# â¡
# â¡
# On this picture, the âoldâ red lines (âpinkâ, two at edges) become sloped lines with slope -s, with horizontal projection 1
# (ending on horizontal grid lines, below the center of first square, and below the center of the last square.
# The right end of the left pink line is below the new-red line of the leftmost new square; hence it is below any fitting
# line. One must only check that the left end of the left pink line is above the fitting line. If this left end is on level
# (or above) the top of the left square, everything is OK.
# If it is on the level or below the bottom of the left square, then draw a new green line: horizontal line of lenght 1 going right
# from the left end of the pink like. Obviously, the fitting line intersects the pink line iff it intersects the new green line.
# There are several cases when we may exclude the new green line being below the bottom of the left square:
# ⢠if all rectangles are actually squares, one could replace s by s+1 above, and have one rectangle instead; exclude this;
# ⢠if there is one rectangle longer than 2 (or two of length 2) the slope of the fitting line is < 1, so intersection with
# such green line is impossible;
# ⢠If there is one rectangle of length 2, and the rest are squares, the green line may be 1 unit below the bottom (and it
# is unique; this may be repeated on both ends).
# Only two cases remain: the rectangles consist of 1 square (total), and that with a pink line which may be either forgotten,
# or replaced by an âadditionalâ green line. (The additional green lines have no associated red lines, so on the NEXT step of induction
# they would give no pink lines.)
# In the first (âtrivialâ) case, the preceding step is of two rectangles with no added green lines.
# So induction step: We start with n rectangles with kâ¤2 added green lines; coordinate change gives rectangles of total length
# n-1+k with 2-k pink lines. A pink line is either forgotten, or impossible, or gives a unique solution, or is convertible to
# a green line. So either we exclude a configuration, or find a unique solution or a trivial case, or get rectangles with n-1+k
# squares and â¤2-k added green lines. The only cases when the number of squares did not decrease is:
# All rectangles at start are squares except one of length 2; we had 2 added green lines.
# But then on the next step we have no added green lines, so the next step is the trivial one.
# (To avoid the trivial step [which is tricky] we ensure that we call recursively, there are at least two squares.
# This means at least two green intervals on the previous stage.)
# Provided that this case is handled in the caller, the additional green lines appear when the length of the start/end rectangle
# is s+1; if it is above s+1, this is an impossible situation.
# In particular, every case is reduced to a âunique solutionâ one, or the âtrivialâ one. The last one is equivalent to
# having 3 equidistant paralle lines with an an interval [AB] on the middle one (the preimage [AB] of the last red line), and
# opposite to each other rays XA' and YB' on the other two lines. The fitting line must intersect all 3 of them.
# Oone of ray may be the whole line). It is easy to see that this is equivalent to the line intersecting intervals [XA], [AB]
# and [BY]. If the quadrilateral XAYB is not convex, it may be decreased (so that X,A,B,Y are 3 vertices of a â³, and a point
# on a side. If it is convex, then intersecting [AB] is a corollary of other two. ???
# Possibly unknown squares: the 2nd and 3rd row âshareâ a x-coordinate; assume that intersecting a red line in any of them is OK.
# â¡ This is equivalent to having the red line of double length, which is equivalent to
# â¡â¡â¡ the green line of double length at this position (assuming it is not at edge).
# â¡â¡â¡â¡ Hence this allows the induction step as well.
# â¡â¡â¡â¡â¡ (Encode ignoring the extra square at second line, with second line marked as: $extended->{1}=1.)
# â¡â¡
# Returns empty or a,b,db of the line y=ax+b-db which rasterizes to the rectangles of widths @$CC (connected UR â LL corners);
# The other two arguments as as above. LL corner of the leftmost of bottom squares is at 0,0.
sub encodes_line ($;$$$); # Refused degenerated cases when there is a unique solutionâââwith choices in rasterization
sub encodes_line ($;$$$) { # %$extended should not have negative keys
my ($CC, $green_at_left, $green_at_right, $extended) = (shift, shift, shift, shift||{}); # Every elt encodes delta between cells
# A horrible mess of special-cases before we can recognize "runs", and flip axes...
# warn "Got: [@$CC], $green_at_left, $green_at_right";
# Banal case: one rectangle
return 0, 1/2 if 1 >= @$CC and not ($green_at_left or $green_at_right); # No greens at all; one rectangle
my(@jumps, $left_red, $right_red, %seen) = @$CC; # jumps between greens
if ($green_at_left) { $left_red = $CC->[0] - 1 }
else { shift @jumps }
if ($green_at_right) { $right_red = $CC->[-1] - 1 }
else { pop @jumps }
# Need to exclude a trivial case
unless (@jumps) { # Only one green
if ($green_at_left or $green_at_right) { # Maximize some metric (âdistances to mid-red points)
my $sl = 3/(4*$CC->[0] - 1); # ==> intersects right at height â¾.
return ($green_at_left ? ($sl, $sl/4) : ($sl, 1/4 - $sl/2)) # Cut the green interval in the same proportion
} else { # Two rectangles; likewise: if ratio of lengths is tâ¤1, use ¾(1+t²)/(1+t³)
my $addHalf = 0;
if ($extended->{0} and $CC->[0] < $CC->[1] - 1) {
$CC = [$CC->[0] + 1, $CC->[1] - 1];
} elsif ($extended->{0} and $CC->[0] == $CC->[1] - 1) {
$addHalf = 1;
}
if ($CC->[0] + $addHalf == $CC->[1]) { # Go through the center of symmetry, with intersection of edge red lines as above:
my $sl = 3/(4*$CC->[0] + 2*$addHalf - 2);
return($sl, 1/4 - $sl/2);
} elsif ($CC->[0] < $CC->[1]) { # One strategy is to continue periodically, then make a best fit; this joints
# midpoints of rectangles. If differ by one, this breaks the green line 1:3 (with 1 on the side of longer rectangle).
# On the other hand, to avoid close-to pathological rasterizations, we should divide the green line in the middle!
# The best derasterization of 1+2 cuts the red lines at heights ¾,¼,¾ (the last ¾ makes it also good-in-L²-norm).
# (It is also better since there are two ways to treat it: one can consider the main direction to be horizontal,
# or to be diagonal. This âbestâ approximation is the same in these two approaches.)
my $t = $CC->[0] / ($CC->[1] - 1); # Break green as 1:3, but use the slope as above with t-correction
my $sl = 3*(1+$t*$t)/(1+$t*$t*$t)/(4*$CC->[1] - 2);
return($sl, 1 - $sl*$CC->[0]);
} else { # Likewise
my $t = $CC->[1] / ($CC->[0] - 1); # If differ by 1, the distances to red lines are ¼, ¾, âµââ,... exactly as ½,3/3 for equal lengths
my $sl = 3*(1+$t*$t)/(1+$t*$t*$t)/(4*$CC->[0] - 2);
return($sl, 1 - $sl*$CC->[0]);
}
}
}
# Up to this moment, always successfully return; below, unsuccessful returns indented (the only successful is the last one):
my %jump_pre_ext = map { ($_ + !!$green_at_left, $extended->{$_}) } keys %$extended; # shift keys of extended to be pre-jump
my $tot_jumps = 0;
$tot_jumps += $_ for @jumps;
my($slope_min, $slope_max) = (($tot_jumps - !!$jump_pre_ext{0})/@jumps, ($tot_jumps + !!$jump_pre_ext{@jumps})/@jumps);
if (int($slope_min) != int $slope_max) { # differ by â¤1 unless @jumps = 1
# There is a chance that after shear transformation with slope = int $slope_max, we have both increasing and decreasing paths.
( run in 1.023 second using v1.01-cache-2.11-cpan-e1769b4cff6 )