piet-0.1: sources/Language/Piet/Compiler.hs
-- |
-- This module implements the image processing part of this library. It
-- is able to do basic image I/O and provides funcions for labelling
-- images and extracting Piet-relevant information at the same time.
module Language.Piet.Compiler
(
-- * I/O
imgFromFile
-- * The \"compiler\"
, compile
-- * Labelling
, label4, label4With
) where
import Control.Exception
import Control.Monad
import Data.IntMap hiding (filter)
import Data.List hiding (insert)
import Data.Monoid
import Graphics.Imlib
import Language.Piet.Types
-- | Load an 'Image' holding Piet 'Colour's from a given file.
-- If the codel length is known, it should be passed as 'Just'
-- argument, otherwise, it is guessed from the file. Note that
-- \"codel length\" means the edge length of the codels and
-- not their size.
--
-- /This function is not thread safe due to imlib2!/
imgFromFile :: Maybe Int -- ^ Codel length or 'Nothing' if unknown
-> FilePath -- ^ The image file location
-> IO (Either ImlibLoadError (Image Colour))
imgFromFile codelInfo file = do
(img, err) <- loadImageWithErrorReturn file
case err of
ImlibLoadErrorNone -> bracket
(contextSetImage img)
(const freeImageAndDecache)
$ const $ do
codelLength <- maybe imageGuessCodelLength return codelInfo
img' <- imageFromContext (max 1 codelLength)
return (Right img')
_ -> return (Left err)
-- | Build an @'Image' 'Colour'@ from the current imlib2 context.
imageFromContext :: Int -- ^ Codel length (not size)
-> IO (Image Colour) -- ^ The image data
imageFromContext codelLength = do
width <- (`div` codelLength) `liftM` imageGetWidth
height <- (`div` codelLength) `liftM` imageGetHeight
alpha <- imageHasAlpha
pixels <- mapM (\xy@(x, y) -> do
ImlibColor a r g b <- imageQueryPixel
(x * codelLength)
(y * codelLength)
return (xy, if alpha then rgba2Colour r g b a else rgb2Colour r g b)
) [ (x, y) | x <- [ 0 .. width-1 ], y <- [ 0 .. height-1 ] ]
return $ imgNew width height pixels
-- | Guess the codel length from the image that is currently loaded in
-- the imlib2 buffer. The guess simply is the the gcd of the image
-- width, length and the length of all equally coloured subrows and
-- -cols.
imageGuessCodelLength :: IO Int
imageGuessCodelLength = do
width <- imageGetWidth
height <- imageGetHeight
rows <- mapM (\y -> mapM (\x -> imageQueryPixel x y)
[ 0 .. width-1 ]) [ 0 .. height-1 ]
cols <- mapM (\x -> mapM (\y -> imageQueryPixel x y)
[ 0 .. height-1 ]) [ 0 .. width-1 ]
return $ lastUntil (==1) $ scanl gcd (gcd width height)
$ fmap length (group rows) ++ fmap length (group cols)
where
-- Get the first item of a list that fulfills @p@ or its
-- last element.
lastUntil :: Ord a => (a -> Bool) -> [a] -> a
lastUntil _ [x] = x
lastUntil p (x:xs) = if p x then x else lastUntil p xs
lastUntil _ _ = error "empty list in lastUntil helper (imageGuessCodelLength)"
-- | Compile an @'Image' 'Colour'@ to a Piet 'Program'.
compile :: Image Colour -> Program
compile image_ = let
(mask_, info_) = label4 image_
in
Program
{ image = image_
, mask = mask_
, info = info_
}
-- | Status of the labelling algorithm.
data LabellingStatus = LabellingStatus
{ _currentCoords :: (Int, Int) -- ^ Current pixel to investigate
, _nextKey :: LabelKey -- ^ Next unused 'LabelKey'
, _mask :: Image LabelKey -- ^ Each pixel contains a label key
, _infoMap :: IntMap LabelInfo -- ^ Mapping from 'LabelKey's to 'LabelInfo's
, _equivalences :: EquivalenceMap -- ^ Holds information about label equivalences
} deriving (Show, Eq, Ord)
-- | Label an image with 4-neighbourship and equivalence as neighbouring
-- condition, which is @'label4With' (==)@.
label4 :: Eq a => Image a -> (Image LabelKey, IntMap LabelInfo)
label4 = label4With (==)
-- | Labels an image with 4-neighbourship.
label4With :: (a -> a -> Bool) -- ^ Decides whether two neighbouring pixels are adjacent.
-> Image a -- ^ The 'Image' to be labelled.
-> (Image LabelKey, IntMap LabelInfo)
-- ^ A mask 'Image' (containing a key for every pixel)
-- and a mapping from these keys to 'LabelInfo'.
label4With neighbours img = let
status = label4With' neighbours img LabellingStatus
{ _currentCoords = (0, 0)
, _nextKey = 0
, _mask = imgNew (imgWidth img) (imgHeight img) []
, _infoMap = mempty
, _equivalences = mempty
}
img' = fmap ((flip equivClass) (_equivalences status)) $ _mask status
inf = foldWithKey
(\label labelInfo mergedMap -> let
label' = equivClass label $ _equivalences status
in
alter (maybe (Just labelInfo) (Just . (mappend labelInfo))) label' mergedMap
) mempty
$ _infoMap status
in (img', inf)
-- | Labelling algorithm expressed in terms of a 'LabellingStatus', see
-- 'label4With'.
label4With' :: (a -> a -> Bool) -> Image a -> LabellingStatus -> LabellingStatus
{-# SPECIALISE
label4With' :: (Colour -> Colour -> Bool) -> Image Colour -> LabellingStatus -> LabellingStatus
#-}
label4With' neighbours img status = let
xy@(x, y) = _currentCoords status
pixel = imgPixel x y img
mergeLabels = fmap (\(x', y', _) -> imgPixel x' y' (_mask status))
$ filter (\(_, _, e) -> neighbours pixel e)
$ fmap (\(x', y') -> (x', y', imgPixel x' y' img))
$ previousNeighbours (_currentCoords status)
status' = case mergeLabels of
[] -> let label = _nextKey status in status
{ _nextKey = succ label
, _mask = imgSetPixel x y label (_mask status)
, _infoMap = insert label (addPixel x y mempty) (_infoMap status)
}
[label] -> status
{ _mask = imgSetPixel x y label (_mask status)
, _infoMap = adjust (addPixel x y) label (_infoMap status)
}
[l1, l2] -> let
label = max l1 l2
in status
{ _mask = imgSetPixel x y label (_mask status)
, _infoMap = adjust (addPixel x y) label (_infoMap status)
, _equivalences = equivInsert l1 l2 (_equivalences status)
}
_ -> error
"too many neighbours in Language.Piet.Compiler.ImageProcessor.label4With'"
in case nextCoords xy of
Just xy' -> label4With' neighbours img $ status' { _currentCoords = xy' }
Nothing -> status'
where
previousNeighbours :: (Int, Int) -> [(Int, Int)]
previousNeighbours (x, y) = filter (\(x', y') -> x' >= 0 && y' >= 0) [ (x-1, y), (x, y-1) ]
nextCoords :: (Int, Int) -> Maybe (Int, Int)
nextCoords (x, y)
| x < imgWidth img - 1 = Just (x + 1, y)
| y < imgHeight img - 1 = Just (0, y + 1)
| otherwise = Nothing
-- | Detects equivalence classes. Invariant: Every element is mapped to the
-- minimum of it's equivalence class.
type EquivalenceMap = IntMap LabelKey
-- | Find the equivalence class of a given element.
equivClass :: LabelKey -> EquivalenceMap -> LabelKey
equivClass e = findWithDefault e e
-- | Insert a new equivalence.
equivInsert :: LabelKey -> LabelKey -> EquivalenceMap -> EquivalenceMap
equivInsert x y mp = let
class1 = equivClass x mp
class2 = equivClass y mp
classes = [x, y, class1, class2]
newClass = minimum classes
in
if x /= y
then fmap (\eqClass -> if or (fmap (== eqClass) classes) then newClass else eqClass)
$ insert x newClass
$ insert y newClass mp
else mp