"""Generic oriented ridge-width and continuity estimator; NumPy/Pillow only.""" import math import numpy as np from PIL import Image,ImageFilter def smooth(lo,hi,x): t=np.clip((x-lo)/(hi-lo),0,1) return t*t*(3-2*t) def shift(a,dx,dy): """Sample at (x+dx,y+dy), clamping at source edges.""" h,w=a.shape p=np.pad(a,((abs(dy),abs(dy)),(abs(dx),abs(dx))),mode='edge') return p[abs(dy)+dy:abs(dy)+dy+h,abs(dx)+dx:abs(dx)+dx+w] def offset(direction,distance): return (round(direction[0]*distance),round(direction[1]*distance)) def detect(image,recipe,diagnostics=False): # Luminance rather than brightest RGB channel handles dark colored leading. image=image.convert('RGB').filter(ImageFilter.GaussianBlur(recipe['lineAnalysisSmoothingRadius'])) gray=np.asarray(image.convert('L'),dtype=np.float32)/255. darkness=1-smooth(*recipe['darkLuminanceSmoothstep'],gray) bridge_darkness=1-smooth(*recipe['bridgeLuminanceSmoothstep'],gray) result=np.zeros_like(gray);raw_max=np.zeros_like(gray);bridge_max=np.zeros_like(gray) for index in range(recipe['orientations']): angle=math.pi*index/recipe['orientations'] normal=(math.cos(angle),math.sin(angle));tangent=(-normal[1],normal[0]) # The same shoulder distance is used along each candidate line: support # at a different width cannot rescue this orientation/scale's texture. for radius in recipe['normalSampleRadii']: dx,dy=offset(normal,radius) shoulders=np.minimum(shift(gray,dx,dy),shift(gray,-dx,-dy)) shape=smooth(*recipe['ridgeContrastSmoothstep'],shoulders-gray) ridge=shape*darkness support=np.zeros_like(gray) for distance in recipe['tangentSupportDistances']: tx,ty=offset(tangent,distance) support+=shift(ridge,tx,ty)+shift(ridge,-tx,-ty) support/=2*len(recipe['tangentSupportDistances']) coherent=ridge*smooth(*recipe['continuitySmoothstep'],support) # Restore only short interruptions bracketed by matching ridges. # Both shoulders and longer tangent support must agree at the gap. for distance in recipe['bridgeDistances']: tx,ty=offset(tangent,distance) bracket=np.minimum(shift(ridge,tx,ty),shift(ridge,-tx,-ty)) bridge=bracket*np.maximum(shape,darkness)*bridge_darkness*smooth(*recipe['bridgeSupportSmoothstep'],support) np.maximum(coherent,bridge,out=coherent) if diagnostics: np.maximum(bridge_max,bridge,out=bridge_max) np.maximum(result,coherent,out=result) if diagnostics: np.maximum(raw_max,ridge,out=raw_max) result[result>=recipe['highConfidenceProtectionThreshold']]=1. if diagnostics: return result,{'raw-ridges':raw_max,'bridge-candidates':bridge_max} return result