photo filter now uses voroni diagram

There's still a couple bugs, some of the shapes are kinda long and
pointy. In the meantime, shapes are placed on the board longest
perimeter first to cover it up.

Also, there's still some gaps occasionally.
This commit is contained in:
Jack Wines 2024-02-04 03:34:12 -08:00
parent dd9bb2c88a
commit 5c981917bd
No known key found for this signature in database
GPG key ID: 25B20640600571E6
3 changed files with 189 additions and 53 deletions

View file

@ -82,6 +82,7 @@ executable image-triangles
, diagrams-lib , diagrams-lib
, diagrams-cairo , diagrams-cairo
, diagrams-svg , diagrams-svg
, diagrams-contrib
, parallel , parallel
, linear , linear
, vector , vector
@ -102,4 +103,3 @@ executable image-triangles
"-with-rtsopts= -N" "-with-rtsopts= -N"
-- -prof -- -prof
-- -fexternal-interpreter -- -fexternal-interpreter
-- import Servant.Client

View file

@ -32,6 +32,9 @@ import Control.Arrow
import Data.Colour.RGBSpace (uncurryRGB) import Data.Colour.RGBSpace (uncurryRGB)
import qualified Control.Monad.Parallel as MP import qualified Control.Monad.Parallel as MP
import qualified CircumCircle as CC import qualified CircumCircle as CC
import qualified Debug.Trace as T
import qualified Diagrams as DP
import qualified Diagrams.Prelude as D
data Options = Options { data Options = Options {
numPoints :: Int, numPoints :: Int,
@ -53,52 +56,70 @@ convImage = Vec.map tosRGB' . Int.toVector
corners :: [(Double, Double)] corners :: [(Double, Double)]
corners = (,) <$> [0, 1] <*> [0, 1] corners = (,) <$> [0, 1] <*> [0, 1]
scalePointToImage :: (Int, Int) -> Point V2 Double -> Point V2 Int -- scalePointToImage :: (Int, Int) -> Point V2 Double -> Point V2 Int
scalePointToImage (ymax, xmax) p = round <$> (p2 (fromIntegral xmax, fromIntegral ymax) * p) -- scalePointToImage (ymax, xmax) p = round <$> (p2 (fromIntegral xmax, fromIntegral ymax) * p)
genImage :: Image VU G.RGB Double -> StdGen -> Int -> QDiagram B V2 Double Any -- genImage :: Image VU G.RGB Double -> StdGen -> Int -> QDiagram B V2 Double Any
genImage image gen cornerCount = -- genImage image gen cornerCount =
scaleY widthHeightRatio -- scaleY widthHeightRatio
. reflectY -- . reflectY
. rectEnvelope (mkP2 0 0) (1 ^& 1) -- . rectEnvelope (mkP2 0 0) (1 ^& 1)
. mconcat -- . mconcat
. map (uncurry Ren.placeTri . second (uncurryRGB CL.rgb)) -- . map (uncurry Ren.placeTri . second (uncurryRGB CL.rgb))
. withStrategy (parListChunk 1000 rdeepseq) -- . withStrategy (parListChunk 1000 rdeepseq)
$ My.mapMaybe (\tri -> (tri,) . CL.toRGB <$> triColor tri) triangles -- $ My.mapMaybe (\tri -> (tri,) . CL.toRGB <$> triColor tri) triangles
where -- where
widthHeightRatio = (fromIntegral $ fst dimensions) / ((fromIntegral $ snd dimensions) :: Double) -- widthHeightRatio = (fromIntegral $ fst dimensions) / ((fromIntegral $ snd dimensions) :: Double)
img' = convImage image -- img' = convImage image
dimensions = Img.dims image -- dimensions = Img.dims image
triangles = S.toList . Tri.findTriangles . withStrategy (parListChunk 1000 rdeepseq) . Tri.toPlanarGraph . take cornerCount . map p2 $ corners ++ Tri.randomPoints gen -- triangles = S.toList . Tri.findTriangles . withStrategy (parListChunk 1000 rdeepseq) . Tri.toPlanarGraph . take cornerCount . map p2 $ corners ++ Tri.randomPoints gen
triColor tri = getTriangleAverageRGB img' dimensions <$> (if S.size scaled == 3 then Just scaled else Nothing) -- triColor tri = getTriangleAverageRGB img' dimensions <$> (if S.size scaled == 3 then Just scaled else Nothing)
where -- where
scaled = S.map (scalePointToImage dimensions) tri -- scaled = S.map (scalePointToImage dimensions) tri
shapeCircumference = Data.List.sum . map D.norm . loopOffsets . fromVertices
-- genImage' :: Image VU G.RGB Double -> StdGen -> Int -> QDiagram Cairo V2 Double Any genImage' :: Image VU G.RGB Double -> StdGen -> Int -> QDiagram SVG V2 Double Any
genImage' image gen cornerCount = -- image gen cornerCount = genImage' image gen cornerCount = -- image gen cornerCount =
scaleY widthHeightRatio scaleY widthHeightRatio
. reflectY . reflectY
. rectEnvelope (mkP2 0 0) (1 ^& 1) . rectEnvelope (mkP2 0 0) (1 ^& 1)
. position -- . atop visualizeGraph
. map (\shape -> (head shape, ) . showOrigin . mconcat
. fillColor (Tri.getShapeAverageRGB img' dimensions . map (scalePointToImage dimensions) $ shape) . map drawVoroniRegion
. strokeLoop . closeLine . fromVertices $ shape) -- . concatMap (\(center, shape) -> (zip shape (repeat $ circle 0.002)))
-- . withStrategy (parListChunk 1000 rdeepseq) . sortOn shapeCircumference
. withStrategy (parListChunk 50 rdeepseq)
. map (uncurry Tri.voroniDiagramCorners)
$ voroni $ voroni
where where
widthHeightRatio = (fromIntegral $ fst dimensions) / ((fromIntegral $ snd dimensions) :: Double)
triColor tri = getTriangleAverageRGB img' dimensions <$> (if S.size scaled == 3 then Just scaled else Nothing) drawVoroniRegion shape = lw 0
where . fillColor (Tri.voroniRegionAverageColor img' dimensions shape )
scaled = S.map (scalePointToImage dimensions) tri . strokeLocLoop . fromVertices $ shape
widthHeightRatio = (fromIntegral $ fst dimensions) / ((fromIntegral $ snd dimensions) :: Double)
img' = convImage image img' = convImage image
dimensions = Img.dims image dimensions = Img.dims image
voroni = take 40 . Tri.findVoroniDiagram . withStrategy (parListChunk 1000 rdeepseq) . Tri.toPlanarGraph . take cornerCount . map p2 $ corners ++ Tri.randomPoints gen
singleVoroni = last voroni
visualizeGraph :: QDiagram SVG V2 Double Any
visualizeGraph = lc red . lw 1 . position
. map (\(x0, x1) -> (x0,) . strokeLine $ (x0 ~~ x1))
. Tri.toPlanarGraph
$ corners'
voroni = -- take 14 . drop 24 .
Tri.findVoroniDiagram -- . withStrategy (parListChunk 1000 rdeepseq)
. Tri.toPlanarGraph $ corners'
corners' = take cornerCount . map p2 $ corners ++ Tri.randomPoints gen
deriving instance Generic (CL.RGB a) deriving instance Generic (CL.RGB a)
deriving instance NFData a => NFData (CL.RGB a) deriving instance NFData a => NFData (CL.RGB a)
@ -118,11 +139,11 @@ main :: IO ()
main = do main = do
CLIOptions{..} <- getRecord "image options" CLIOptions{..} <- getRecord "image options"
let (Options {gen = gen}) = defaultOpts let (Options {gen = gen}) = defaultOpts
gen' <- pure . mkStdGen $ 2345 -- getStdGen gen' <- getStdGen -- pure . mkStdGen $ 2344
let gens :: [StdGen] = map fst . iterate (split . snd) . split $ gen' let gens :: [StdGen] = map fst . iterate (split . snd) . split $ gen'
print gen' print gen'
image <- Img.readImageRGB VU input image <- Img.readImageRGB VU input
let diagram = genImage image gen' cornerCount -- let diagram = genImage image gen' cornerCount
let nums = zip gens $ map show [0..60] let nums = zip gens $ map show [0..60]
let dimVector = toDimensionVector image let dimVector = toDimensionVector image
renderSVG output dimVector (genImage' image gen' cornerCount) renderSVG output dimVector (genImage' image gen' cornerCount)

View file

@ -29,6 +29,14 @@ import qualified Data.Map as M
import qualified Data.Set as S import qualified Data.Set as S
import Control.Arrow import Control.Arrow
import qualified Data.List as L import qualified Data.List as L
import qualified Data.Function as F
import qualified Diagrams.TwoD.Path.IntersectionExtras as I
import Diagrams.Trail (trailPoints)
import qualified Graphics.Image as H
import Debug.Trace (traceShow)
import qualified Data.Colour.Names as C
import qualified Debug.Trace as D
import Diagrams.TwoD.Segment.Bernstein (listToBernstein)
-- import qualified Linear.Affine as L -- import qualified Linear.Affine as L
type Image_ = Vec.Vector Pixel_ type Image_ = Vec.Vector Pixel_
type Pixel_ = Colour Double type Pixel_ = Colour Double
@ -53,45 +61,110 @@ randomPoints :: StdGen -> [(Double, Double)]
randomPoints = map (bimap toZeroToOneTuple toZeroToOneTuple) . randomRs ((0 :: Word, 0 :: Word), (maxBound, maxBound)) randomPoints = map (bimap toZeroToOneTuple toZeroToOneTuple) . randomRs ((0 :: Word, 0 :: Word), (maxBound, maxBound))
where where
toZeroToOneTuple :: Word -> Double toZeroToOneTuple :: Word -> Double
toZeroToOneTuple x = ((fromIntegral x / (fromIntegral (maxBound :: Word))) * 1.1) - 0.05 toZeroToOneTuple x = ((fromIntegral x / (fromIntegral (maxBound :: Word))) * 1.2) - 0.1
combinations :: (Ord b, Floating b, NFData b) => [P2 b] -> [(P2 b, P2 b)] combinations :: (Ord b, Floating b, NFData b) => [P2 b] -> [(P2 b, P2 b)]
combinations xs = combinations xs =
sortOn (abs . uncurry distanceA) sortOn (abs . uncurry distanceA)
. S.toList . S.fromList . S.toList . S.fromList
. filter (uncurry (/=)) . filter (uncurry (/=))
. concat . withStrategy (parListChunk 2000 rdeepseq) . sortOn (abs . uncurry distanceA)
. map (\x -> take edgeLengthThreshold . sortOn (abs . uncurry distanceA) -- . concat . withStrategy (parListChunk 2000 rdeepseq) $
. map (\y -> (min x y, max x y)) $ xs) $ xs $ [(x,y) | x <- xs, y <- xs]
-- . map (\x -> take edgeLengthThreshold .
-- sortOn (abs . uncurry distanceA)
-- . map (\y -> (min x y, max x y)) $ xs) $ xs
where where
edgeLengthThreshold = 45 edgeLengthThreshold = 10
-- toPlanarGraph :: (NFData n, Floating n, Ord n) => [Point V2 n] -> [(Point V2 n, Point V2 n)] toPlanarGraph :: forall n . (NFData n, Floating n, Ord n) => [P2 n] -> [(Point V2 n, Point V2 n)]
toPlanarGraph :: (NFData n, Floating n, Ord n) => [P2 n] -> [(Point V2 n, Point V2 n)]
toPlanarGraph points = toPlanarGraph points =
removeIntersections . combinations $ points removeIntersections .
combinations $ points
where where
numPoints = length points numPoints = length points
removeIntersections :: [(Point V2 n, Point V2 n)] -> [(Point V2 n, Point V2 n)]
removeIntersections = foldl' addIfNoIntersection [] removeIntersections = foldl' addIfNoIntersection []
addIfNoIntersection xs x = if all (noIntersection x) xs then (x:xs) else xs
noIntersection l1 l2 = (==) sharedEndPoint . length $ intersectPointsT (uncurry toLocatedTrail l1) (uncurry toLocatedTrail l2) addIfNoIntersection xs x | all (noIntersection x) xs = x:xs
| otherwise = xs
noIntersection l1 l2 = sharedEndPoint || (null $ intersectPointsT (uncurry toLocatedTrail l1) (uncurry toLocatedTrail l2))
where where
sharedEndPoint = (-) 4 . length . nub $ [fst l1, snd l1, fst l2, snd l2] sharedEndPoint = (< 4) . length . nub $ [fst l1, snd l1, fst l2, snd l2]
toLocatedTrail :: TrailLike a => Point (V a) (N a) -> Point (V a) (N a) -> Located a toLocatedTrail :: TrailLike a => Point (V a) (N a) -> Point (V a) (N a) -> Located a
toLocatedTrail p1 p2 = fromVertices [p1, p2] `at` p1 toLocatedTrail p1 p2 = fromVertices [p1, p2] `at` p1
-- data VoroniRegion = VoroniRegion {
-- center :: P2 Double,
-- neighbors :: [P2 Double]
-- }
-- voroniDiagramCorners :: Point V2 Double -> [Point V2 Double] -> [Point V2 Double]
-- voroniDiagramCorners :: Point (V c) (N c) -> [Point V2 Double] -> c
-- voroniDiagramCorners :: forall t. (N t ~ Double, V t ~ V2, TrailLike t) => Point V2 Double -> [Point V2 Double] -> t
withinShape :: RealFloat v => Point V2 v -> [Point V2 v] -> Point V2 v -> Bool
withinShape pointInShape verticies candidate = all ((< quarterTurn) . fmap abs . uncurry signedAngleBetweenDirs) $ zip (shapeDirections pointInShape) (shapeDirections candidate)
where
shapeDirections p = map (dirBetween p) verticies
sortOnAngle center = sortOn (normalizeAngle . signedAngleBetweenDirs xDir . dirBetween center)
-- voroniDiagramCorners :: forall n . (Floating n, Real n, Show n) => Point V2 n -> [P2 n] -> [P2 n]
voroniDiagramCorners center midpoints -- = midpoints
= sortOnAngle center . filter isValidMidpoint . concat $ [intersectPointsT l0 l1 | l0 <- tangentTrails, l1 <- tangentTrails]
where
lessThanQuarterTurn candidate = candidate <= (10001 / 40000) @@ turn || candidate >= (29999 / 40000) @@ turn
-- where
-- candidate' = (candidate ^. turn) - (fromIntegral . floor $ (candidate ^. turn))
-- candidateVertecies = sortOn (normalizeAngle . signedAngleBetweenDirs xDir . dirBetween center) . concat $ [ intersectPointsT (tangentTrail x) (tangentTrail y) | x <- midpoints, y <- midpoints ]
tangentTrails = map tangentTrail midpoints
-- validMidpoints = sortOn (normalizeAngle . signedAngleBetweenDirs xDir . dirBetween center) . filter isValidMidpoint $ midpoints
appendHead (x:xs) = xs ++ [x]
isValidMidpoint candidate = all isNonObtuseMidpoint . filter (/= candidate) $ midpoints
where
isNonObtuseMidpoint m = -- traceShow ((dirBetween m center, dirBetween m candidate), (candidate, m)) .
lessThanQuarterTurn . normalizeAngle $ angleBetweenDirs (dirBetween m center) (dirBetween m candidate)
tangentTrail midpoint = fromVertices $ [midpoint .-^ tangentVec, midpoint .+^ tangentVec]
where
-- implicitly uses the unit vector as an infinitely long vector
tangentVec = scale 8 .
fromDirection . rotateBy (1 / 4) $ dirBetween midpoint center
-- findVoroniDiagram :: (Ord (v n), Additive v, Fractional n) => [(Point v n, Point v n)] -> [[Point v n]] -- findVoroniDiagram :: (Ord (v n), Additive v, Fractional n) => [(Point v n, Point v n)] -> [[Point v n]]
findVoroniDiagram :: (Ord n, Ord (v n), Metric v, Floating n, R1 v, Real n) => [(Point v n, Point v n)] -> [[Point v n]] -- findVoroniDiagram :: (Ord n, Ord (v n), Metric v, Floating n, R1 v, Real n, r) => [(Point v n, Point v n)] -> [[Point v n]]
findVoroniDiagram edges = M.elems . M.mapWithKey (\key -> L.sortOn (normalizeAngle . angleBetweenDirs xDir . dirBetween key) . findVoroniDiagram :: RealFloat n => [(Point V2 n, Point V2 n)] -> [(Point V2 n, [Point V2 n])]
map (pointBetween key) . S.toList) $ adjacencyMap findVoroniDiagram edges =
M.toList
. M.mapWithKey (\key -> L.sortOn (normalizeAngle . signedAngleBetweenDirs xDir . dirBetween key) . map (pointBetween key) .
S.toList)
$ adjacencyMap
where where
adjacencyMap = adjacencyMapOf edges adjacencyMap = adjacencyMapOf edges
pointBetween p0 p1 = p0 .+^ ((p0 .-. p1) ^/ 2) -- I'm not sure this part works
pointBetween p0 p1 = p0 .+^ ((p1 .-. p0) ^/ 2)
-- filterNotInVoroniRegion :: RealFloat n => Point V2 n -> [Point V2 n] -> [Point V2 n] -> [Point V2 n]
-- filterNotInVoroniRegion center midpoints = id -- filter allowed
-- where
-- allowed point
-- = all ((< 0.25) . abs . (^. turn))
-- . zipWith signedAngleBetweenDirs midpointAngles
-- . map (dirBetween point) $ midpoints
-- midpointAngles = map (dirBetween center) midpoints
findTriangles :: Ord b => [(b, b)] -> S.Set (S.Set b) findTriangles :: Ord b => [(b, b)] -> S.Set (S.Set b)
findTriangles edges = S.unions . S.map threeCyclesOf . M.keysSet $ adjacencyMap findTriangles edges = S.unions . S.map threeCyclesOf . M.keysSet $ adjacencyMap
@ -119,15 +192,57 @@ getPointsInTriangle image pts = S.toList . S.unions . map S.fromList $ [
where where
[p1, p2, p3] = sortOn fst . map (\(y, x) -> (x,y)) . map unp2 . S.toList $ pts [p1, p2, p3] = sortOn fst . map (\(y, x) -> (x,y)) . map unp2 . S.toList $ pts
blendEqually colors = C.affineCombo (zip (repeat fraction) colors) $ C.black blendEqually colors = C.affineCombo (zip (repeat fraction) colors) $ C.blue
where where
fraction = 1.0 / (fromIntegral . length $ colors) fraction = 1.0 / (fromIntegral . length $ colors)
getShapeAverageRGB :: Image_ -> (Int, Int) -> [P2 Int] -> C.Colour Double -- voroniRegionAverageColor :: Image_ -> (Int, Int) -> (P2 Int, [P2 Int]) -> C.Colour Double
getShapeAverageRGB image sizes = blendEqually . concatMap (getColorsInTriangle image sizes) . filter ((== 3) . S.size) . map (S.fromList . take 3) . tails . L.sortOn (fst . unp2) voroniRegionAverageColor image (x', y') verticies
= blendEqually
. concatMap (getColorsInTriangle image (x', y'))
. filter ((== 3) . S.size)
. map (S.fromList . take 3)
. tails
. L.nub
. map scaleToImageCoords
$ verticies
where
scaleToImageCoords :: P2 Double -> P2 Int
scaleToImageCoords p = fmap round $ p2 ((fromIntegral x' * p ^. _x), fromIntegral y' * p ^. _y)
scaleToUnitCoords :: P2 Int -> P2 Double
scaleToUnitCoords p = p2 ((fromIntegral x' / (fromIntegral $ p ^. _x)), fromIntegral y' / (fromIntegral $ p ^. _y))
-- = blendEqually
-- . mapMaybe (index' . unp2)
-- . filter withinShape'
-- $ candidatePoints
-- where
-- withinShape' :: P2 Int -> Bool
-- withinShape' = withinShape center verticies . scaleToUnitCoords
-- candidatePoints = [p2 (x, y) | x <- [minX .. maxX], y <- [minY .. maxY]]
-- maxX = fst . unp2 $ maximumBy (compare `F.on` fst . unp2) verticies'
-- minX = fst . unp2 $ minimumBy (compare `F.on` fst . unp2) verticies'
-- maxY = snd . unp2 $ maximumBy (compare `F.on` snd . unp2) verticies'
-- minY = snd . unp2 $ minimumBy (compare `F.on` snd . unp2) verticies'
-- verticies' = map scaleToImageCoords verticies
-- index' :: (Int, Int) -> Maybe Pixel_
-- index' (y, x)
-- | y >= y' = Nothing
-- | x >= x' = Nothing
-- | y < 0 = Nothing
-- | x < 0 = Nothing
-- | otherwise = image Vec.!? ((y * x') + x)
getColorsInTriangle :: Image_ -> (Int, Int) -> S.Set (P2 Int) -> [C.Colour Double] getColorsInTriangle :: Image_ -> (Int, Int) -> S.Set (P2 Int) -> [C.Colour Double]
getColorsInTriangle image (y', x') triangle = pixels getColorsInTriangle image (x', y') triangle = pixels
where where
pixels :: [Pixel_] pixels :: [Pixel_]
@ -146,7 +261,7 @@ getColorsInTriangle image (y', x') triangle = pixels
getTriangleAverageRGB :: Image_ -> (Int, Int) -> S.Set (P2 Int) -> C.Colour Double getTriangleAverageRGB :: Image_ -> (Int, Int) -> S.Set (P2 Int) -> C.Colour Double
getTriangleAverageRGB image (y', x') triangle = blendEqually $ getColorsInTriangle image (y', x') triangle getTriangleAverageRGB image (x', y') triangle = blendEqually $ getColorsInTriangle image (x', y') triangle
ptsBtween :: LineMXB -> LineMXB -> [(Int, Int)] ptsBtween :: LineMXB -> LineMXB -> [(Int, Int)]