cv-combinators 0.1.1 → 0.1.2
raw patch · 4 files changed
+174/−67 lines, 4 filesdep ~HOpenCVPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: HOpenCV
API changes (from Hackage documentation)
+ AI.CV.ImageProcessors: capture :: IO (Ptr CvCapture) -> ImageSource
+ AI.CV.ImageProcessors: videoFile :: String -> ImageSource
+ AI.CV.Processor: differentiate :: (Real b) => Clock IO -> IOSource a b -> IOSource a Double
+ AI.CV.Processor: integrate :: (Real b) => Clock IO -> IOSource a b -> IOSource a Double
+ AI.CV.Processor: max_ :: (Ord b) => Clock IO -> b -> IOSource a b -> IOSource a b
+ AI.CV.Processor: min_ :: (Ord b) => Clock IO -> b -> IOSource a b -> IOSource a b
+ AI.CV.Processor: scanlT :: Clock IO -> (b -> b -> DTime -> c -> c) -> c -> IOSource a b -> IOSource a c
+ AI.CV.Processor: type Clock m = m Double
+ AI.CV.Processor: type DTime = Double
+ AI.CV.Processor: type IOProcessor a b = Processor IO a b
+ AI.CV.Processor: type IOSink a = IOProcessor a ()
+ AI.CV.Processor: type IOSource a b = Processor IO a b
- AI.CV.ImageProcessors: drawRects :: Processor IO (Ptr IplImage, [CvRect]) (Ptr IplImage)
+ AI.CV.ImageProcessors: drawRects :: IOProcessor (Ptr IplImage, [CvRect]) (Ptr IplImage)
- AI.CV.ImageProcessors: haarDetect :: String -> Double -> Int -> HaarDetectFlag -> CvSize -> Processor IO (Ptr IplImage) [CvRect]
+ AI.CV.ImageProcessors: haarDetect :: String -> Double -> Int -> HaarDetectFlag -> CvSize -> IOProcessor (Ptr IplImage) [CvRect]
- AI.CV.ImageProcessors: runTill :: (Monad m) => Processor m () b -> (b -> m Bool) -> m b
+ AI.CV.ImageProcessors: runTill :: IOProcessor () b -> (b -> IO Bool) -> IO b
- AI.CV.ImageProcessors: runTillKeyPressed :: (Show a) => Processor IO () a -> IO ()
+ AI.CV.ImageProcessors: runTillKeyPressed :: (Show a) => IOProcessor () a -> IO ()
- AI.CV.ImageProcessors: type ImageProcessor = Processor IO (Ptr IplImage) (Ptr IplImage)
+ AI.CV.ImageProcessors: type ImageProcessor = IOProcessor (Ptr IplImage) (Ptr IplImage)
- AI.CV.ImageProcessors: type ImageSink = Processor IO (Ptr IplImage) ()
+ AI.CV.ImageProcessors: type ImageSink = IOSink (Ptr IplImage)
- AI.CV.ImageProcessors: type ImageSource = Processor IO () (Ptr IplImage)
+ AI.CV.ImageProcessors: type ImageSource = IOSource () (Ptr IplImage)
- AI.CV.Processor: chain :: (Monad m) => Processor m a b' -> Processor m b' b -> Processor m a b
+ AI.CV.Processor: chain :: Processor m a b' -> Processor m b' b -> Processor m a b
- AI.CV.Processor: forkJoin :: (Monad m) => Processor m a b -> Processor m a b' -> Processor m a (b, b')
+ AI.CV.Processor: forkJoin :: Processor m a b -> Processor m a b' -> Processor m a (b, b')
- AI.CV.Processor: parallel :: (Monad m) => Processor m a b -> Processor m c d -> Processor m (a, c) (b, d)
+ AI.CV.Processor: parallel :: Processor m a b -> Processor m c d -> Processor m (a, c) (b, d)
Files
- cv-combinators.cabal +5/−3
- src/AI/CV/ImageProcessors.hs +24/−16
- src/AI/CV/Processor.hs +136/−45
- src/Test.hs +9/−3
cv-combinators.cabal view
@@ -1,15 +1,17 @@ name: cv-combinators-version: 0.1.1+version: 0.1.2 license: BSD3 maintainer: Noam Lewis <jones.noamle@gmail.com> bug-reports: mailto:jones.noamle@gmail.com category: AI, Graphics synopsis: Functional Combinators for Computer Vision description:- Initial version; using the HOpenCV package as a backend.+ Initial version; using the "HOpenCV" package as a backend. . Provides a functional combinator library, naturally expressed as Arrow instances (but also Category, Functor and Applicative). .+ Online documentation, if not built below, can be found at <http://www.ee.bgu.ac.il/~noamle/>.+ . Read the module docs for more information. See the test program (@src/Test.hs@) for example usage. .@@ -22,7 +24,7 @@ exposed-modules: AI.CV.Processor, AI.CV.ImageProcessors hs-Source-Dirs: src- build-depends: base >= 3 && < 5, HOpenCV+ build-depends: base >= 3 && < 5, HOpenCV >= 0.1.2 ghc-options: -Wall executable test-cv-combinators
src/AI/CV/ImageProcessors.hs view
@@ -20,7 +20,9 @@ -- > cam = camera 0 -- Autodetect camera -- > edge = canny 30 190 3 -- Edge detecting processor using canny operator -- >--- > test = win . edge . cam -- A processor that captures frames from camera and displays edge-detected version in the window.+-- > test = cam >>> edge >>> win +--+-- The last expression is a processor that captures frames from camera and displays edge-detected version in the window. -------------------------------------------------------------- module AI.CV.ImageProcessors where @@ -35,9 +37,9 @@ import Foreign.Ptr -type ImageSink = Processor IO (Ptr IplImage) ()-type ImageSource = Processor IO () (Ptr IplImage)-type ImageProcessor = Processor IO (Ptr IplImage) (Ptr IplImage)+type ImageSink = IOSink (Ptr IplImage) +type ImageSource = IOSource () (Ptr IplImage)+type ImageProcessor = IOProcessor (Ptr IplImage) (Ptr IplImage) @@ -51,21 +53,16 @@ -- | Runs the processor until a predicate is true, for predicates, and processors that take () as input -- (such as chains that start with a camera).-runTill :: Monad m => Processor m () b -> (b -> m Bool) -> m b+runTill :: IOProcessor () b -> (b -> IO Bool) -> IO b runTill = flip runUntil () -- | Name (and type) says it all.-runTillKeyPressed :: (Show a) => Processor IO () a -> IO ()+runTillKeyPressed :: (Show a) => IOProcessor () a -> IO () runTillKeyPressed f = (f `runTill` keyPressed) >> (return ()) ----------------------------------------------------------------------- | A capture device, using OpenCV's HighGui lib's cvCreateCameraCapture--- should work with most webcames. See OpenCV's docs for information.--- This processor outputs the latest image from the camera at each invocation.-camera :: Int -> ImageSource-camera index = processor processQueryFrame allocateCamera fromState releaseNext+capture :: IO (Ptr HighGui.CvCapture) -> ImageSource+capture pCap = processor processQueryFrame allocateCamera fromState releaseNext where processQueryFrame :: () -> (Ptr CxCore.IplImage, Ptr HighGui.CvCapture) -> IO (Ptr CxCore.IplImage, Ptr HighGui.CvCapture) processQueryFrame _ (_, cap) = do@@ -74,7 +71,7 @@ allocateCamera :: () -> IO (Ptr CxCore.IplImage, Ptr HighGui.CvCapture) allocateCamera _ = do- cap <- HighGui.cvCreateCameraCapture (fromIntegral index)+ cap <- pCap newFrame <- HighGui.cvQueryFrame cap return (newFrame, cap) @@ -83,6 +80,17 @@ releaseNext (_, cap) = do HighGui.cvReleaseCapture $ cap+++-- | A capture device, using OpenCV's HighGui lib's cvCreateCameraCapture+-- should work with most webcames. See OpenCV's docs for information.+-- This processor outputs the latest image from the camera at each invocation.+camera :: Int -> ImageSource+camera index = capture (HighGui.cvCreateCameraCapture (fromIntegral index))++videoFile :: String -> ImageSource+videoFile fileName = capture (HighGui.cvCreateFileCapture fileName)+ ------------------------------------------------------------------ -- GUI stuff @@ -155,7 +163,7 @@ -> Int -- ^ min neighbors -> CV.HaarDetectFlag -- ^ flags -> CvSize -- ^ min size- -> Processor IO (Ptr IplImage) [CvRect]+ -> IOProcessor (Ptr IplImage) [CvRect] haarDetect cascadeFileName scaleFactor minNeighbors flags minSize = processor procFunc allocFunc convFunc freeFunc where procFunc :: (Ptr IplImage) -> ([CvRect], (Ptr CvHaarClassifierCascade, Ptr CvMemStorage)) -> IO ([CvRect], (Ptr CvHaarClassifierCascade, Ptr CvMemStorage))@@ -184,7 +192,7 @@ -- need a datatype that combines the shape types for that. -- | OpenCV's cvRectangle, currently without width, color or line type control-drawRects :: Processor IO (Ptr IplImage, [CvRect]) (Ptr IplImage)+drawRects :: IOProcessor (Ptr IplImage, [CvRect]) (Ptr IplImage) drawRects = processor procFunc (CxCore.cvCloneImage . fst) (do return) CxCore.cvReleaseImage where procFunc (src,rects) dst = do CxCore.cvCopy src dst
src/AI/CV/Processor.hs view
@@ -8,23 +8,26 @@ -- Stability : experimental -- Portability : tested on GHC only ----- Framework for expressing monadic actions that require initialization and finalizers.+-- Framework for expressing IO actions that require initialization and finalizers. -- This module provides a *functional* interface for defining and chaining a series of processors. -- -- Motivating example: bindings to C libraries that use functions such as: f(foo *src, foo *dst), -- where the pointer `dst` must be pre-allocated. In this case we normally do: ----- foo *dst = allocateFoo();--- ... --- while (something) {--- f(src, dst);--- ...--- }--- releaseFoo(dst);+-- > foo *dst = allocateFoo();+-- > ... +-- > while (something) {+-- > f(src, dst);+-- > ...+-- > }+-- > releaseFoo(dst); -- -- You can use the 'runUntil' function below to emulate that loop. -- -- Processor is an instance of Category, Functor, Applicative and Arrow. +--+-- In addition to the general type @'Processor' m a b@, this module also defines the semantic model+-- for @'Processor' IO a b@, which has synonym @'IOProcessor' a b@. module AI.CV.Processor where @@ -38,18 +41,9 @@ -- | The type of Processors ----- The semantic model is: ------ > [[ Processor m o a b ]] = a -> b------ The idea is that the monad m is usually IO, and that a and b are usually pointers.--- It is meant for functions that require a pre-allocated output pointer to operate.--- -- * a, b = the input and output types of the processor (think a -> b) ----- * m = monad in which the processor operates------ * x = type of internal state+-- * x = type of internal state (existentially quantified) -- -- The arguments to the constructor are: --@@ -64,13 +58,58 @@ data Processor m a b where Processor :: Monad m => (a -> x -> m x) -> (a -> m x) -> (x -> m b) -> (x -> m ()) -> (Processor m a b) -processor :: (Monad m) =>+-- | The semantic model for 'IOProcessor' is a function:+--+-- > [[ 'IOProcessor' a b ]] = a -> b+--+-- And the following laws:+--+-- 1. The processing function (@a -> x -> m x@) must act as if purely, so that indeed for a given input the+-- output is always the same. One particular thing to be careful with is that the output does not depend+-- on time (for example, you shouldn't use IOProcessor to implement an input device). The @IOSource@ type+-- is defined exactly for time-dependent processors. For pointer typed inputs and outputs, see next law.+--+-- 2. For processors that work on pointers, @[[ Ptr t ]] = t@. This is guaranteed by the following+-- implementation constraints for @IOProcessor a b@:+--+-- 1. If `a` is a pointer type (@a = Ptr p@), then the processor must NOT write (modify) the referenced data.+--+-- 2. If `b` is a pointer, the memory it points to (and its allocation status) is only allowed to change+-- by the processor that created it (in the processing and releasing functions). In a way this+-- generalizes the first constraint.+--+-- Note, that unlike "Yampa", this model does not allow transformations of the type @(Time -> a) -> (Time ->+-- b)@. The reason is that I want to prevent arbitrary time access (whether causal or not). This limitation+-- means that everything is essentially "point-wise" in time. To allow memory-full operations under this+-- model, 'scanlT' is defined. See <http://www.ee.bgu.ac.il/~noamle/_downloads/gaccum.pdf> for more about+-- arbitrary time access.+type IOProcessor a b = Processor IO a b++-- | @'IOSource' a b@ is the type of time-dependent processors, such that:+--+-- > [[ 'IOSource' a b ]] = (a, Time) -> b+--+-- Thus, it is ok to implement a processing action that outputs arbitrary time-dependent values during runtime+-- regardless of input. (Although the more useful case is to calculate something from the input @a@ that is+-- also time-dependent. The @a@ input is often not required and in those cases @a = ()@ is used.+--+-- Notice that this means that IOSource doesn't qualify as an 'IOProcessor'. However, currently the+-- implementation /does NOT/ enforce this, i.e. IOSource is not a newtype; I don't know how to implement it+-- correctly. Also, one question is whether primitives like "chain" will have to disallow placing 'IOSource'+-- as the second element in a chain. Maybe they should, maybe they shouldn't.+type IOSource a b = Processor IO a b++-- | TODO: What's the semantic model for @'IOSink' a@?+type IOSink a = IOProcessor a ()++-- | TODO: do we need this? we're exporting the data constructor anyway for now, so maybe we don't.+processor :: Monad m => (a -> x -> m x) -> (a -> m x) -> (x -> m b) -> (x -> m ()) -> Processor m a b processor = Processor -- | Chains two processors serially, so one feeds the next.-chain :: (Monad m) => Processor m a b' -> Processor m b' b -> Processor m a b+chain :: Processor m a b' -> Processor m b' b -> Processor m a b chain (Processor pf1 af1 cf1 rf1) (Processor pf2 af2 cf2 rf2) = processor pf3 af3 cf3 rf3 where pf3 a (x1,x2) = do x1' <- pf1 a x1@@ -94,7 +133,7 @@ -- | A processor that represents two sub-processors in parallel (although the current implementation runs them -- sequentially, but that may change in the future)-parallel :: (Monad m) => Processor m a b -> Processor m c d -> Processor m (a,c) (b,d)+parallel :: Processor m a b -> Processor m c d -> Processor m (a,c) (b,d) parallel (Processor pf1 af1 cf1 rf1) (Processor pf2 af2 cf2 rf2) = processor pf3 af3 cf3 rf3 where pf3 (a,c) (x1,x2) = do x1' <- pf1 a x1@@ -120,29 +159,31 @@ -- -- Semantic meaning, using Arrow's (&&&) operator: -- [[ forkJoin ]] = &&& --- Or, considering the Monad instance of functions (which are the semantic meanings of a processor):--- [[ forkJoin ]] = liftM2 (,)+-- Or, considering the Applicative instance of functions (which are the semantic meanings of a processor):+-- [[ forkJoin ]] = liftA2 (,) -- Alternative implementation to consider: f &&& g = (,) <&> f <*> g-forkJoin :: (Monad m) => Processor m a b -> Processor m a b' -> Processor m a (b,b')+forkJoin :: Processor m a b -> Processor m a b' -> Processor m a (b,b') forkJoin (Processor pf1 af1 cf1 rf1) (Processor pf2 af2 cf2 rf2) = processor pf3 af3 cf3 rf3- where pf3 a (x1,x2) = do+ where --pf3 :: a -> (x1,x2) -> m (x1,x2)+ pf3 a (x1,x2) = do x1' <- pf1 a x1 x2' <- pf2 a x2 return (x1', x2') + --af3 :: a -> m (x1, x2) af3 a = do x1 <- af1 a x2 <- af2 a return (x1,x2)- + + --cf3 :: (x1,x2) -> m (b,b') cf3 (x1,x2) = do- b <- cf1 x1+ b <- cf1 x1 b' <- cf2 x2 return (b,b')- - rf3 (x1,x2) = do- rf2 x2- rf1 x1+ + --rf3 :: (x1,x2) -> m ()+ rf3 (x1,x2) = rf2 x2 >> rf1 x1 -------------------------------------------------------------@@ -167,18 +208,7 @@ fmap f (Processor pf af cf rf) = processor pf af cf' rf where cf' x = liftM f (cf x) --- | Splits (duplicates) the output of a functor, or on this case a processor.-split :: Functor f => f a -> f (a,a)-split = (join (,) <$>)---- | 'f --< g' means: split f and feed it into g. Useful for feeding parallelized (***'d) processors.--- For example, a --< (b &&& c)-(--<) :: (Functor (cat a), Category cat) => cat a a1 -> cat (a1, a1) c -> cat a c-f --< g = split f >>> g-infixr 1 --<---instance (Monad m) => Applicative (Processor m a) where+instance Monad m => Applicative (Processor m a) where -- | -- > [[ pure ]] = const pure b = processor pf af cf rf@@ -222,15 +252,29 @@ first = (*** id) second = (id ***) + -------------------------------------------------------------++-- | Splits (duplicates) the output of a functor, or on this case a processor.+split :: Functor f => f a -> f (a,a)+split = (join (,) <$>)++-- | 'f --< g' means: split f and feed it into g. Useful for feeding parallelized (***'d) processors.+-- For example, a --< (b *** c) = a >>> (b &&& c)+(--<) :: (Functor (cat a), Category cat) => cat a a1 -> cat (a1, a1) c -> cat a c+f --< g = split f >>> g+infixr 1 --<+++------------------------------------------------------------- -- | Runs the processor once: allocates, processes, converts to output, and deallocates.-run :: (Monad m) => Processor m a b -> a -> m b+run :: Monad m => Processor m a b -> a -> m b run = runWith id -- | Keeps running the processing function in a loop until a predicate on the output is true. -- Useful for processors whose main function is after the allocation and before deallocation.-runUntil :: (Monad m) => Processor m a b -> a -> (b -> m Bool) -> m b+runUntil :: Monad m => Processor m a b -> a -> (b -> m Bool) -> m b runUntil (Processor pf af cf rf) a untilF = do x <- af a let repeatF y = do@@ -250,4 +294,51 @@ b' <- f (pf a x >>= cf) rf x return b'+++-------------------------------------------------------------+type DTime = Double++type Clock m = m Double++-- | scanlT provides the primitive for performing memory-full operations on time-dependent processors, as described in <http://www.ee.bgu.ac.il/~noamle/_downloads/gaccum.pdf>.+--+-- /Untested/.+scanlT :: Clock IO -> (b -> b -> DTime -> c -> c) -> c -> IOSource a b -> IOSource a c+scanlT clock transFunc initOut (Processor pf af cf rf) = processor procFunc allocFunc convFunc releaseFunc+ where procFunc curIn' (prevIn, prevTime, prevOut, x) = do+ x' <- pf curIn' x+ curIn <- cf x'+ curTime <- clock+ let dtime = curTime - prevTime+ curOut = transFunc prevIn curIn dtime prevOut+ return (curIn, curTime, curOut, x')+ + allocFunc firstIn' = do+ x <- af firstIn'+ firstIn <- cf x+ curTime <- clock+ return (firstIn, curTime, initOut, x)+ + convFunc (_, _, curOut, _) = return curOut+ + releaseFunc (_, _, _, x') = rf x'+ + +-- | Differentiate using scanlT. TODO: test, and also generalize for any monad (trivial change of types).+differentiate :: (Real b) => Clock IO -> IOSource a b -> IOSource a Double+differentiate clock = scanlT clock diffFunc 0+ where diffFunc y' y dt _ = (realToFrac (y' - y)) / dt -- horrible approximation!+ +integrate :: (Real b) => Clock IO -> IOSource a b -> IOSource a Double+integrate clock p = scanlT clock intFunc 0 p+ where intFunc y' y dt prevSum = prevSum + (realToFrac (y' + y)) * dt / 2 -- horrible approximation!++max_ :: Ord b => Clock IO -> b -> IOSource a b -> IOSource a b+max_ clock minVal = scanlT clock maxFunc minVal+ where maxFunc y' y _ _ = max y' y+ +min_ :: Ord b => Clock IO -> b -> IOSource a b -> IOSource a b+min_ clock maxVal = scanlT clock minFunc maxVal+ where minFunc y' y _ _ = min y' y
src/Test.hs view
@@ -19,12 +19,18 @@ edges :: ImageProcessor edges = canny 30 190 3 -faceDetect :: Processor.Processor IO PImage [CvRect]-faceDetect = haarDetect "/usr/share/opencv/haarcascades/haarcascade_frontalface_default.xml" 1.2 3 CV.cvHaarDoCannyPruning (CvSize 50 50)+faceDetect :: Processor.IOProcessor PImage [CvRect]+faceDetect = haarDetect "/usr/share/opencv/haarcascades/haarcascade_frontalface_alt.xml" 1.1 3 CV.cvHaarFlagNone (CvSize 20 20) +captureDev :: ImageSource+captureDev = videoFile "/tmp/video.flv" -- Many formats are supported, not just flv (FFMPEG-based, normally). +-- If you have a webcam, uncomment this, and comment the other definition.+-- captureDev = camera 0+ main :: IO ()-main = runTillKeyPressed (camera 0 --< (second faceDetect) >>> drawRects >>> window 0) +main = runTillKeyPressed (captureDev >>> resizer --< (faces *** edges) >>> (window 0 *** window 1))+ where faces = (id &&& faceDetect) >>> drawRects -- Shows the camera output in two windows (same images in both). --main = runTillKeyPressed ((camera 0) --< (window 0 *** window 1))