@@ -55,7 +55,7 @@ use crate::error::{Error, Result};
5555use crate :: geometry:: { Location , Point , Quad } ;
5656use crate :: image:: GrayFrame ;
5757use crate :: imgproc:: binary:: BinaryImage ;
58- use crate :: imgproc:: components:: { Connectivity , connected_components} ;
58+ use crate :: imgproc:: components:: { Connectivity , connected_components, flood_region } ;
5959use crate :: imgproc:: homography:: Homography ;
6060use crate :: imgproc:: line:: { Line , fit_line_least_squares} ;
6161use crate :: imgproc:: sample:: sample_bilinear;
@@ -280,6 +280,13 @@ fn corner_targets(geom: &Geometry, swap: bool) -> [Point; 4] {
280280
281281/// Extract corners, the L orientation, a size estimate and the binarization threshold
282282/// from a supplied binarization `bin` (and its global sampling `threshold`).
283+ /// How far beyond its parallelogram-completed position the symbol's far corner (where
284+ /// the timing borders meet) may lie and still be kept in the working mask, as growth
285+ /// factors about the centroid, tried in order. Tight first: it keeps print that crowds
286+ /// that corner out of the fit. Then generous: under perspective the true corner is well
287+ /// off the parallelogram, and clipping it costs the timing borders their meeting point.
288+ const FAR_CORNER_SLACK : [ f32 ; 2 ] = [ 1.08 , 1.35 ] ;
289+
283290/// Dark components tried as the symbol, largest first.
284291const MAX_COMPONENTS : usize = 6 ;
285292
@@ -309,7 +316,12 @@ fn extract_geometries(
309316 c. area >= 16 && w >= MIN_FRAME && h >= MIN_FRAME && w <= 4 * h && h <= 4 * w
310317 } )
311318 . take ( MAX_COMPONENTS )
312- . filter_map ( |c| extract_geometry ( frame, bin, threshold, & c. bounds ) . ok ( ) )
319+ . collect :: < Vec < _ > > ( )
320+ . into_iter ( )
321+ . flat_map ( |c| FAR_CORNER_SLACK . map ( |slack| ( c, slack) ) )
322+ . filter_map ( |( c, slack) | {
323+ extract_geometry ( frame, bin, threshold, & c. bounds , c. area , slack) . ok ( )
324+ } )
313325 . collect ( ) ;
314326 if geoms. is_empty ( ) {
315327 return Err ( Error :: undecodable ( "no dark region shaped like a symbol" ) ) ;
@@ -323,13 +335,102 @@ fn extract_geometry(
323335 bin : & BinaryImage ,
324336 threshold : u8 ,
325337 b : & crate :: imgproc:: components:: BoundingBox ,
338+ area : usize ,
339+ far_slack : f32 ,
326340) -> Result < Geometry > {
327341 let pad = 2usize ;
328342 let x0 = b. min_x . saturating_sub ( pad) ;
329343 let y0 = b. min_y . saturating_sub ( pad) ;
330344 let x1 = ( b. max_x + pad) . min ( bin. width ( ) - 1 ) ;
331345 let y1 = ( b. max_y + pad) . min ( bin. height ( ) - 1 ) ;
332346
347+ // Everything below reasons about "the symbol's dark boundary", so ink that is not
348+ // the symbol's — print beside the code, the dark surface behind a label — must not
349+ // be allowed to attract the corner search or the edge-line fits, which otherwise
350+ // lock onto it and skew the grid. Two stages: a first quad from the L component's
351+ // *own* pixels (the box of a rotated symbol has other ink in its corners), then the
352+ // working mask is the binarization clipped to that quad, slightly grown — which
353+ // brings back the symbol's modules that are not connected to the L (timing dots,
354+ // interior islands) and nothing else.
355+ // The component's pixels: flood from a dark pixel on its top row. Another component
356+ // can share that row, so floods are tried until one has this component's area.
357+ let mut component = Vec :: new ( ) ;
358+ let mut x = b. min_x ;
359+ while x <= b. max_x {
360+ if bin. get ( x, b. min_y ) {
361+ let pixels = flood_region ( bin, ( x, b. min_y ) , true ) ;
362+ if pixels. len ( ) == area {
363+ component = pixels;
364+ break ;
365+ }
366+ // Skip the rest of this dark run: it floods to the same region.
367+ while x <= b. max_x && bin. get ( x, b. min_y ) {
368+ x += 1 ;
369+ }
370+ } else {
371+ x += 1 ;
372+ }
373+ }
374+ let mut own = BinaryImage :: new ( bin. width ( ) , bin. height ( ) ) ;
375+ for & ( x, y) in & component {
376+ own. set ( x, y, true ) ;
377+ }
378+ let rough = find_corners ( & own, x0, y0, x1, y1) ?;
379+ let centre = centroid_of ( & rough) ;
380+ // The L reaches three of the symbol's corners; the fourth — where the two timing
381+ // borders meet — may not be connected to it at all, leaving that corner of the
382+ // rough quad recessed (or somewhere inside the symbol). The L's vertex is the
383+ // corner whose two edges are solid; the corner opposite it is replaced by the
384+ // parallelogram completion of the other three when that lies farther out.
385+ let solid: [ f32 ; 4 ] =
386+ core:: array:: from_fn ( |i| edge_darkness ( & own, rough[ i] , rough[ ( i + 1 ) % 4 ] , centre) ) ;
387+ let vertex = ( 0 ..4 )
388+ . max_by ( |& i, & j| {
389+ ( solid[ ( i + 3 ) % 4 ] + solid[ i] ) . total_cmp ( & ( solid[ ( j + 3 ) % 4 ] + solid[ j] ) )
390+ } )
391+ . unwrap_or ( 0 ) ;
392+ let far = ( vertex + 2 ) % 4 ;
393+ let mut hull = rough;
394+ let completed = Point :: new (
395+ rough[ ( far + 3 ) % 4 ] . x + rough[ ( far + 1 ) % 4 ] . x - rough[ vertex] . x ,
396+ rough[ ( far + 3 ) % 4 ] . y + rough[ ( far + 1 ) % 4 ] . y - rough[ vertex] . y ,
397+ ) ;
398+ if completed. distance ( centre) > rough[ far] . distance ( centre) {
399+ hull[ far] = completed;
400+ }
401+ for ( i, p) in hull. iter_mut ( ) . enumerate ( ) {
402+ // Then grown a little about the centroid, for blur and the rough fit; the far
403+ // corner by `far_slack` (see [`FAR_CORNER_SLACK`]).
404+ let ( dx, dy) = ( p. x - centre. x , p. y - centre. y ) ;
405+ let len = dx. hypot ( dy) . max ( 1.0 ) ;
406+ let k = if i == far { far_slack } else { 1.08 } + 2.0 / len;
407+ * p = Point :: new ( centre. x + dx * k, centre. y + dy * k) ;
408+ }
409+ let inside = |x : f32 , y : f32 | {
410+ // Same side of all four edges of the (convex, consistently wound) quad.
411+ let mut sign = 0.0f32 ;
412+ ( 0 ..4 ) . all ( |i| {
413+ let ( a, c) = ( hull[ i] , hull[ ( i + 1 ) % 4 ] ) ;
414+ let cross = ( c. x - a. x ) * ( y - a. y ) - ( c. y - a. y ) * ( x - a. x ) ;
415+ if cross == 0.0 {
416+ return true ;
417+ }
418+ if sign == 0.0 {
419+ sign = cross. signum ( ) ;
420+ }
421+ cross. signum ( ) == sign
422+ } )
423+ } ;
424+ let mut window = BinaryImage :: new ( bin. width ( ) , bin. height ( ) ) ;
425+ for y in y0..=y1 {
426+ for x in x0..=x1 {
427+ if bin. get ( x, y) && inside ( x as f32 , y as f32 ) {
428+ window. set ( x, y, true ) ;
429+ }
430+ }
431+ }
432+ let bin = & window;
433+
333434 let corners = find_corners ( bin, x0, y0, x1, y1) ?;
334435 let centroid = centroid_of ( & corners) ;
335436
0 commit comments