Files
Sanctification/spikes/rejected-compositions/finish-mask-line-continuity-proof/line_detector.py

55 lines
2.8 KiB
Python

"""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