diff --git a/README.md b/README.md index c6e08d8..b0a04cb 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,12 @@ Makes a [voroni diagram](https://en.wikipedia.org/wiki/Voronoi_diagram) and fills each cell with the average color the image below in. ### examples -![Sierra mountains original](examples/sierra.jpg) -![Sierra mountains post-filter](examples/sierra-result.svg) +![Sierra mountains original](examples/sierra.jpeg) +![Sierra mountains post-filter](examples/sierra-result.jpeg) ![Hawaii original](examples/birds-eye-view.png) -![Hawaii post-filter](examples/birds-eye-view.svg) -![Dog original](examples/Luna.jpeg) -![Dog post-filter](examples/luna.svg) +![Hawaii post-filter](examples/birds-eye-view-result.jpeg) +![Dog original](examples/luna.jpeg) +![Dog post-filter](examples/luna-result.jpeg) ### to run: @@ -16,7 +16,7 @@ Makes a [voroni diagram](https://en.wikipedia.org/wiki/Voronoi_diagram) and fill #### with nix ``` -nix run --experimental-features "nix-command flakes" ".#" -- --minDistance 0.05 --input examples/birds-eye-view.png --output output.svg +nix run --experimental-features "nix-command flakes" ".#" -- --minDistance 0.02 --input examples/birds-eye-view.png --output output.jpeg ``` #### with cabal @@ -24,6 +24,6 @@ Install [cabal & ghc](https://www.haskell.org/ghcup/) if you don't have them. ``` cabal update -cabal run image-triangles -- --minDistance 0.05 --input examples/birds-eye-view.png --output output.svg +cabal run image-triangles -- --minDistance 0.02 --input examples/birds-eye-view.png --output output.jpeg ``` diff --git a/examples/birds-eye-view-result.jpeg b/examples/birds-eye-view-result.jpeg new file mode 100644 index 0000000..ad44028 Binary files /dev/null and b/examples/birds-eye-view-result.jpeg differ diff --git a/examples/birds-eye-view.svg b/examples/birds-eye-view.svg deleted file mode 100644 index 9bb9891..0000000 --- a/examples/birds-eye-view.svg +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file diff --git a/examples/luna-result.jpeg b/examples/luna-result.jpeg new file mode 100644 index 0000000..b41a2e9 Binary files /dev/null and b/examples/luna-result.jpeg differ diff --git a/examples/Luna.jpeg b/examples/luna.jpeg similarity index 100% rename from examples/Luna.jpeg rename to examples/luna.jpeg diff --git a/examples/luna.svg b/examples/luna.svg deleted file mode 100644 index 0600a39..0000000 --- a/examples/luna.svg +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file diff --git a/examples/sierra-result.jpeg b/examples/sierra-result.jpeg new file mode 100644 index 0000000..ca3a274 Binary files /dev/null and b/examples/sierra-result.jpeg differ diff --git a/examples/sierra-result.svg b/examples/sierra-result.svg deleted file mode 100644 index 34f2e8a..0000000 --- a/examples/sierra-result.svg +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file diff --git a/examples/sierra.jpg b/examples/sierra.jpeg similarity index 100% rename from examples/sierra.jpg rename to examples/sierra.jpeg diff --git a/flake.lock b/flake.lock index ae657ec..429e547 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1706830856, - "narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=", + "lastModified": 1717285511, + "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f", + "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", "type": "github" }, "original": { @@ -23,11 +23,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -38,11 +38,11 @@ }, "haskell-flake": { "locked": { - "lastModified": 1708536395, - "narHash": "sha256-z6rxsqQ9/xS3FWc2iLvFnElNt45XehD7bzPe4Yooz08=", + "lastModified": 1717339509, + "narHash": "sha256-ySll9pAYK78v8GY5wrCFLg2iMI6Ms/4tNfrvlMaUQ1M=", "owner": "srid", "repo": "haskell-flake", - "rev": "1e297173b23c5113dd90a2d299d6e0d864af35f1", + "rev": "cfb7db6b7b66cb7d1499dd53b8aeaa7c866f565f", "type": "github" }, "original": { @@ -53,11 +53,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1708475490, - "narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=", + "lastModified": 1717602782, + "narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0e74ca98a74bc7270d28838369593635a5db3260", + "rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6", "type": "github" }, "original": { @@ -69,20 +69,14 @@ }, "nixpkgs-lib": { "locked": { - "dir": "lib", - "lastModified": 1706550542, - "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", - "type": "github" + "lastModified": 1717284937, + "narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" }, "original": { - "dir": "lib", - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" } }, "root": { @@ -116,11 +110,11 @@ ] }, "locked": { - "lastModified": 1708335038, - "narHash": "sha256-ETLZNFBVCabo7lJrpjD6cAbnE11eDOjaQnznmg/6hAE=", + "lastModified": 1717278143, + "narHash": "sha256-u10aDdYrpiGOLoxzY/mJ9llST9yO8Q7K/UlROoNxzDw=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "e504621290a1fd896631ddbc5e9c16f4366c9f65", + "rev": "3eb96ca1ae9edf792a8e0963cc92fddfa5a87706", "type": "github" }, "original": { diff --git a/image-triangles.cabal b/image-triangles.cabal index debe9e6..3c4641a 100644 --- a/image-triangles.cabal +++ b/image-triangles.cabal @@ -64,6 +64,7 @@ executable image-triangles MultiParamTypeClasses OverloadedLabels OverloadedStrings + StrictData RankNTypes RecordWildCards RecursiveDo @@ -82,17 +83,19 @@ executable image-triangles , Color , colour , containers - , diagrams-cairo , diagrams-contrib , diagrams-lib - , diagrams-svg + , diagrams-rasterific , linear , massiv , massiv-io , monad-parallel , optparse-generic , parallel + , pqueue + , psqueues , random + , heaps , splitmix , vector , vector-th-unbox @@ -102,7 +105,5 @@ executable image-triangles -- Base language which the package is written in. default-language: GHC2021 - --- ghc-options: --- -fprof-auto --- "-with-rtsopts=-p" + ghc-options: -threaded "-with-rtsopts=-N" + -- ghc-options: -fprof-auto -threaded "-with-rtsopts=-N" "-with-rtsopts=-p" diff --git a/src/Main.hs b/src/Main.hs index 0f63bb6..5e398fa 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -25,10 +25,9 @@ import Debug.Trace qualified as D import Debug.Trace qualified as DT import Debug.Trace qualified as T import Diagrams qualified as DP -import Diagrams.Backend.SVG -import Diagrams.Backend.SVG.CmdLine -import Diagrams.Prelude -import Diagrams.Prelude qualified as D +import Diagrams.Backend.Rasterific +import Diagrams.Backend.Rasterific.CmdLine +import Diagrams.Prelude as D import GHC.Generics import Graphics.Color.Space qualified as Co import MinDistanceSample qualified as MDS @@ -49,24 +48,27 @@ corners = (,) <$> [0, 1] <*> [0, 1] shapeCircumference :: [Point V2 Double] -> Double shapeCircumference = Data.List.sum . map D.norm . loopOffsets . fromVertices -genImage :: M.Image M.S (Co.SRGB 'Co.Linear) Double -> V2 Double -> Double -> StdGen -> QDiagram SVG V2 Double Any +genImage :: M.Image M.S (Co.SRGB 'Co.Linear) Double -> V2 Double -> Double -> StdGen -> QDiagram Rasterific V2 Double Any genImage image dimensionsVec minDistance gen = - scaleX widthHeightRatio - . reflectY - . rectEnvelope (mkP2 0 0) (1 ^& 1) - . mconcat - . map drawVoroniRegion - . sortOn shapeCircumference - . withStrategy (parListChunk 200 rdeepseq) - . map (uncurry Tri.voroniDiagramCorners) - $ voroni + scaleX widthHeightRatio + . reflectY + . rectEnvelope (mkP2 0 0) (1 ^& 1) + . mconcat + . map ((\x -> drawVoroniRegion x <> overlayEdges x) . uncurry Tri.voroniDiagramCorners) + $ voroni where + overlayEdges = + lw 3.0 + . lc black + . strokeLocLoop + . fromVertices + drawVoroniRegion shape = - lw 0 - . fillColor (Tri.voroniRegionAverageColor image dimensions shape) - . strokeLocLoop - . fromVertices - $ shape + lw 0 + . fillColor (Tri.voroniRegionAverageColor image dimensions shape) + . strokeLocLoop + . fromVertices + $ shape widthHeightRatio :: Double widthHeightRatio = (dimensionsVec ^. _x) / (dimensionsVec ^. _y) @@ -76,19 +78,7 @@ genImage image dimensionsVec minDistance 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 = - Tri.findVoroniDiagram - . Tri.toPlanarGraph - $ corners' + voroni = map (\x -> (x, Tri.nClosestPoints 20 x corners')) corners' averageSideSize = (fromIntegral (uncurry (+) dimensions)) / 2 @@ -96,9 +86,9 @@ genImage image dimensionsVec minDistance gen = corners' :: [P2 Double] corners' = - map (p2 . Bi.first (/ widthHeightRatio) . unp2 . (.-^ ((/ 2) <$> padding))) - . MDS.randomPoints ((mkP2 widthHeightRatio 1) .+^ padding) (minDistance * widthHeightRatio) - $ gen + map (p2 . Bi.first (/ widthHeightRatio) . unp2 . (.-^ ((/ 2) <$> padding))) + . MDS.randomPoints ((mkP2 widthHeightRatio 1) .+^ padding) (minDistance * widthHeightRatio) + $ gen deriving instance Generic (CL.RGB a) @@ -106,22 +96,22 @@ deriving instance (NFData a) => NFData (CL.RGB a) toDimensionVector :: (Ma.Size r, Fractional a) => Ma.Array r Ma.Ix2 e -> V2 a toDimensionVector image = - p2 (fromIntegral cols, fromIntegral rows) .-. p2 (0.0, 0.0) + p2 (fromIntegral cols, fromIntegral rows) .-. p2 (0.0, 0.0) where (M.Sz2 rows cols) = Ma.size image data CLIOptions = CLIOptions - { input :: FilePath, - output :: FilePath, - minDistance :: Double - } - deriving (Generic, ParseRecord) + { input :: FilePath, + output :: FilePath, + minDistance :: Double + } + deriving (Generic, ParseRecord) main :: IO () main = do - CLIOptions {..} <- getRecord "image options" - gen' <- getStdGen -- for consistency, swap with something like: pure . mkStdGen $ 2344 - print gen' - image :: M.Image M.S (Co.SRGB 'Co.Linear) Double <- M.readImageAuto input - let dims = toDimensionVector image - renderSVG output (Diagrams.Prelude.dims dims) (genImage image dims minDistance gen') + CLIOptions {..} <- getRecord "image options" + gen' <- getStdGen + print gen' + image :: M.Image M.S (Co.SRGB 'Co.Linear) Double <- M.readImageAuto input + let dims = toDimensionVector image + renderRasterific output (D.dims dims) (genImage image dims minDistance gen') diff --git a/src/Triangles.hs b/src/Triangles.hs index 234ce54..c606af0 100644 --- a/src/Triangles.hs +++ b/src/Triangles.hs @@ -17,7 +17,9 @@ import Data.Massiv.Array qualified as Ma import Data.Massiv.Array.IO qualified as Ma import Data.Maybe import Data.Ord qualified as O +import Data.PQueue.Prio.Min qualified as PQ import Data.Ratio +import Diagrams.Prelude import Data.Set qualified as S import Data.Vector.Generic.Base (Vector) import Data.Vector.Generic.Mutable (MVector) @@ -26,13 +28,14 @@ import Data.Vector.Unboxed.Deriving import Debug.Trace (traceShow) import Debug.Trace qualified import Debug.Trace qualified as D -import Diagrams.Prelude import Diagrams.Trail (trailPoints) import Diagrams.TwoD +import Data.Heap qualified as H import Diagrams.TwoD.Path.IntersectionExtras qualified as I import Diagrams.TwoD.Segment.Bernstein (listToBernstein) import Graphics.Color.Space qualified as Co import System.Random +import Data.Containers.ListUtils (nubOrd) toColour :: (Fractional a) => Co.Color (Co.SRGB Co.Linear) a -> Colour a toColour (Co.ColorSRGB r g b) = CL.rgb r g b @@ -49,28 +52,26 @@ randomPoints = map (bimap toZeroToOneTuple toZeroToOneTuple) . randomRs ((0 :: W combinations :: (Ord b, Floating b, NFData b) => [P2 b] -> [(P2 b, P2 b)] combinations = - sortOn (abs . uncurry distanceA) - . S.toList -- deduplicate - . S.fromList - . filter (uncurry (/=)) - . concat - . withStrategy (parListChunk 50 rdeepseq) - . map (\(x : xs) -> take 10 . sortOn (abs . uncurry distanceA) . map (x,) $ xs) - . init -- last output of tails is empty list - . tails + sortOn (abs . uncurry distanceA) + . nubOrd + . filter (uncurry (/=)) + . concat + . map (\(x : xs) -> takeSortOn 10 (abs . uncurry distanceA) . map (x,) $ xs) + . init -- last output of tails is empty list + . tails toPlanarGraph :: forall n. (NFData n, Floating n, Ord n) => [P2 n] -> [(Point V2 n, Point V2 n)] toPlanarGraph = - removeIntersections - . sortOn (abs . uncurry distanceA) - . combinations + removeIntersections + . sortOn (abs . uncurry distanceA) + . combinations where removeIntersections :: [(Point V2 n, Point V2 n)] -> [(Point V2 n, Point V2 n)] removeIntersections = foldl' addIfNoIntersection [] addIfNoIntersection xs x - | all (noIntersection x) xs = x : xs - | otherwise = xs + | all (noIntersection x) xs = x : xs + | otherwise = xs noIntersection l1 l2 = sharedEndPoint || (null $ intersectPointsT (uncurry toLocatedTrail l1) (uncurry toLocatedTrail l2)) where @@ -88,12 +89,14 @@ sortOnAngle center = sortOn (normalizeAngle . signedAngleBetweenDirs xDir . dirB voroniDiagramCorners :: (RealFloat n) => Point V2 n -> [Point V2 n] -> [Point V2 n] voroniDiagramCorners center midpoints = - sortOnAngle center - . filter isValidMidpoint - . concat - $ [intersectPointsT l0 l1 | l0 <- tangentTrails, l1 <- tangentTrails] + sortOnAngle center + . filter isValidMidpoint + . concat + $ [intersectPointsT l0 l1 | l0 <- tangentTrails, l1 <- tangentTrails] where - lessThanQuarterTurn candidate = candidate <= (10001 / 40000) @@ turn || candidate >= (29999 / 40000) @@ turn + lessThanQuarterTurn candidate = candidate <= quarterTurn @@ turn || candidate >= (1 - quarterTurn) @@ turn + + quarterTurn = 0.251 tangentTrails = map tangentTrail midpoints @@ -102,42 +105,83 @@ voroniDiagramCorners center midpoints = isValidMidpoint candidate = all isNonObtuseMidpoint . filter (/= candidate) $ midpoints where isNonObtuseMidpoint m = - lessThanQuarterTurn . normalizeAngle $ angleBetweenDirs (dirBetween m center) (dirBetween m candidate) + lessThanQuarterTurn + . normalizeAngle + $ angleBetweenDirs (dirBetween m center) (dirBetween m candidate) tangentTrail midpoint = fromVertices [midpoint .-^ tangentVec, midpoint .+^ tangentVec] where -- implicitly uses the unit vector * 8 as an infinitely long vector tangentVec = - scale 2 - . fromDirection - . rotateBy (1 / 4) - $ dirBetween midpoint center + scale 2 + . fromDirection + . rotateBy (1 / 4) + $ dirBetween midpoint center + + + +-- real 0m47.306s +-- user 0m45.160s +-- sys 0m0.345s + +-- takeSortOn n f = map snd . H.toUnsortedList . H.take n . H.fromList . map (\x -> (f x, x)) + + + + +-- real 0m43.514s +-- user 0m41.489s +-- sys 0m0.271s + +takeSortOn :: Ord a => Int -> (b -> a) -> [b] -> [b] +takeSortOn n f = + map snd + . PQ.take n + . PQ.fromList + . map (\x -> (f x, x)) + + +-- real 0m44.868s +-- user 0m44.199s +-- sys 0m0.322s +-- takeSortOn :: Ord a => Int -> (b -> a) -> [b] -> [b] +-- takeSortOn n f = +-- take n +-- . sortOn f + +nClosestPoints :: (RealFloat n) => Int -> Point V2 n -> [Point V2 n] -> [Point V2 n] +nClosestPoints n p = + map (pointBetween p) + . takeSortOn n (abs . distanceA p) + . filter (/= p) findVoroniDiagram :: (RealFloat n) => [(Point V2 n, Point V2 n)] -> [(Point V2 n, [Point V2 n])] findVoroniDiagram = - M.toList - . M.mapWithKey - ( \key -> - L.sortOn (normalizeAngle . signedAngleBetweenDirs xDir . dirBetween key) - . map (pointBetween key) - . S.toList - ) - . adjacencyMapOf - where - pointBetween p0 p1 = p0 .+^ ((p1 .-. p0) ^/ 2) + M.toList + . M.mapWithKey + ( \key -> + L.sortOn (normalizeAngle . signedAngleBetweenDirs xDir . dirBetween key) + . map (pointBetween key) + . S.toList + ) + . adjacencyMapOf + +pointBetween :: (Affine p, Fractional a) => p a -> p a -> p a +pointBetween p0 p1 = p0 .+^ ((p1 .-. p0) ^/ 2) findTriangles :: (Ord b) => [(b, b)] -> S.Set (S.Set b) findTriangles edges = S.unions . S.map threeCyclesOf . M.keysSet $ adjacencyMap where threeCyclesOf node = - S.unions - . S.map (\x -> S.map (\y -> S.fromList [node, x, y]) $ S.delete node . S.intersection originalNodeNeighbors . (M.!) adjacencyMap $ x) - $ originalNodeNeighbors + S.unions + . S.map (\x -> S.map (\y -> S.fromList [node, x, y]) $ S.delete node . S.intersection originalNodeNeighbors . (M.!) adjacencyMap $ x) + $ originalNodeNeighbors where originalNodeNeighbors = fromMaybe S.empty (adjacencyMap M.!? node) adjacencyMap = adjacencyMapOf edges +adjacencyMapOf :: Ord b => [(b, b)] -> M.Map b (S.Set b) adjacencyMapOf edges = M.fromListWith S.union . map (second S.singleton) $ (edges ++ edgesReversed) where edgesReversed = map (\(a, b) -> (b, a)) edges @@ -147,11 +191,11 @@ triangleAdjacencyMap s = M.fromListWith S.union . concatMap (\s' -> map (,S.sing getPointsInTriangle :: p -> S.Set (P2 Int) -> [(Int, Int)] getPointsInTriangle image pts = - S.toList . S.unions . map S.fromList $ - [ ptsBtween (makeLine p1 p3) (makeLine p1 p2), - ptsBtween (makeLine p1 p3) (makeLine p2 p3), - ptsBtween (makeLine p1 p2) (makeLine p2 p3) - ] + S.toList . S.unions . map S.fromList $ + [ ptsBtween (makeLine p1 p3) (makeLine p1 p2), + ptsBtween (makeLine p1 p3) (makeLine p2 p3), + ptsBtween (makeLine p1 p2) (makeLine p2 p3) + ] where [p1, p2, p3] = sortOn fst . map unp2 . S.toList $ pts @@ -162,13 +206,13 @@ blendEqually colors = C.affineCombo (map (fraction,) colors) C.white voroniRegionAverageColor :: (Integral a, Integral b) => Ma.Image Ma.S (Co.SRGB 'Co.Linear) Double -> (a, b) -> [P2 Double] -> Colour Double voroniRegionAverageColor image (x', y') = - blendEqually - . concatMap (getColorsInTriangle image (x', y')) - . filter ((== 3) . S.size) - . map (S.fromList . take 3) - . tails - . L.nub - . map scaleToImageCoords + blendEqually + . concatMap (getColorsInTriangle image (x', y')) + . filter ((== 3) . S.size) + . map (S.fromList . take 3) + . tails + . nubOrd + . map scaleToImageCoords where scaleToImageCoords :: P2 Double -> P2 Int scaleToImageCoords p = round <$> p2 (fromIntegral x' * p ^. _x, fromIntegral y' * p ^. _y) @@ -208,22 +252,22 @@ yAt (LineMXB {..}) x = round $ (m * (fromIntegral x)) + b makeLine :: (Int, Int) -> (Int, Int) -> LineMXB makeLine (x1, y1) (x2, y2) = - LineMXB - { m = slope, - b = (fromIntegral y1) - (slope * (fromIntegral x1)), - startX = min x1 x2, - endX = max x1 x2 - } + LineMXB + { m = slope, + b = (fromIntegral y1) - (slope * (fromIntegral x1)), + startX = min x1 x2, + endX = max x1 x2 + } where slope = - if x1 /= x2 - then (fromIntegral $ y1 - y2) % (fromIntegral $ x1 - x2) - else fromIntegral . ceiling $ ((10.0 :: Double) ** 100.0) + if x1 /= x2 + then (fromIntegral $ y1 - y2) % (fromIntegral $ x1 - x2) + else fromIntegral . ceiling $ ((10.0 :: Double) ** 100.0) data LineMXB = LineMXB - { m :: Rational, - b :: Rational, - startX :: Int, - endX :: Int - } - deriving (Show, Ord, Eq) + { m :: Rational, + b :: Rational, + startX :: Int, + endX :: Int + } + deriving (Show, Ord, Eq)