{-# LANGUAGE UnicodeSyntax #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE RankNTypes #-} -- | -- Module : Data.ChainCodes -- Copyright : (c) Mateusz Kowalczyk, 2014 -- License : GPL-3 -- -- Decoding of chain codes embedded in images. module Data.ChainCodes where import Codec.Picture import Codec.Picture.Types import Data.Function import Control.Monad import Data.Word import Data.Map hiding (map) -- | Friendly alias for 'PixelRGB8' type Colour = PixelRGB8 -- | Position of a pixel along with the clock -- direction used by 'chainCode'. type PixelPos = (Int, Int, Int) -- | Simple alias for 'Int' pair type Position = (Int, Int) -- | In a chain code, we'll keep the original pixel positions as well as -- the direction which we can use for signal processing. type ChainCode = [PixelPos] -- | Reads in an 'Image' that uses 'PixelRGB8' as its base. -- Rejects any other format. readRGB8 ∷ FilePath → IO (Either String (Image PixelRGB8)) readRGB8 = readImage >=> return . \case Right (ImageRGB8 i) → Right i Right _ → Left "Unsuported image format. RGB8 images only please." Left err → Left err -- | Given an 'Image' parametrised by 'PixelRGB8' and given a -- background 'Colour', we try to find the first pixel that doesn't match the -- 'Colour'. -- -- We start checking at the top left corner of the image, checking each row -- fully before progressing a column: we check @(height, width)@ then -- @(height, width + 1)@ and so on where top left corner of the image is (0, 0) -- and positive height is towards the bottom. Uses colour 'average' for -- comparison with a small leeway of 1. findSpot ∷ Image PixelRGB8 → Colour → Maybe Position findSpot img@(Image w h d) c | w <= 0 || h <= 0 = Nothing | otherwise = go [ (hi, wi) | wi ← [0 .. w - 1], hi ← [0 .. h - 1] ] where go ∷ [(Int, Int)] → Maybe Position go [] = Nothing go (x:xs) = let ac = average c ap = average $ uncurry (pixelAt img) x in if 1 < abs (ac - ap) then Just x else go xs -- | Averages each colour value and sees how close it is too fully-white. intensity ∷ PixelRGB8 → Double intensity p = fromIntegral (average p) / 255 -- | Takes the average of RGB values average ∷ PixelRGB8 → Integer average (PixelRGB8 r g b) = toInteger $ r `div` 3 + g `div` 3 + b `div` 3 -- | Specialised version of 'chainCodeWith' which uses a comparison function -- which simply asserts that the shape colour is not the same as the background. chainCode ∷ Image PixelRGB8 → Colour → Maybe ChainCode chainCode i c = chainCodeWith i c (/= c) -- | Given an 'Image' parametrised by 'PixelRGB8' and given a -- 'Colour' and a comparison function, we try to find the chain code in the -- binary image which has the passed in colour. The comparison function's -- purpose is to tell whether something is part of the shape. -- -- Note that only a single shape is accepted inside of the image. The -- starting positing is determined using 'findSpot'. -- -- The output list contains unique positions only: the beginning and -- end position are not treated the same. If 'findSpot' fails, we return -- 'Nothing'. chainCodeWith ∷ Image PixelRGB8 → Colour → (Colour → Bool) → Maybe ChainCode chainCodeWith img@(Image w h d) c f = findSpot img c >>= \pos → let ppos = (fst pos, snd pos, 0) in Just $ go [1 ..] (fromList [(0, (-1, -1, 0)), (1, ppos)]) ppos where ns ∷ Map Int (Int, Int) ns = fromList $ zip [0 ..] [ (0, 1), (1, 1), (1, 0), (1, -1) , (0, -1), (-1, -1), (-1, 0), (-1, 1)] go ∷ [Int] → Map Int PixelPos → PixelPos → ChainCode go cs positions p@(sx, sy, _) = map snd . toList $ loop cs positions where loop ∷ [Int] → Map Int PixelPos → Map Int PixelPos loop (count:counts) positions | count == 1 || not (eqV positions count) = let current@(fx, fy, fd) = positions ! count inBounds (x, y) = x >= 0 && y >= 0 && x < w && y < h places = [ (mx, my, m) | i ← [fd + 5 .. fd + 13] , let m = i `mod` 8 (nx, ny) = ns ! m o@(mx, my) = (fx + ny, fy + nx) , inBounds o , f $ uncurry (pixelAt img) o ] in loop counts $ case places of [] → positions x:_ → insert (count + 1) x positions | otherwise = delete 1 (delete 0 positions) eqV ∷ Map Int PixelPos → Int → Bool eqV p i = let (x, y, _) = p ! i in (x, y) == (sx, sy)