packages feed

keera-posture-0.1.3: src/AI/CV/PostureProcessors.hs

module AI.CV.PostureProcessors where

-- External imports
import AI.CV.ImageProcessors
import AI.CV.OpenCV.CV
import AI.CV.OpenCV.CxCore
import Control.Arrow
import Control.Concurrent
import Control.Monad
import Control.Processor (processor, IOProcessor, IOSource)
import Data.Time.Clock
import Foreign.C.Types

-- Internal imports
import Paths

createInitialState :: IO InternalState
createInitialState = do
  time <- getCurrentTime
  return ((0, 0), time)

-- Returns true if exactly one rectangle has been detected
-- or 5 seconds have passed since 't'.
detectedOrFiveSeconds :: UTCTime -> [CvRect] -> IO Bool
detectedOrFiveSeconds t []  = do
  time <- getCurrentTime
  let diff = diffUTCTime time t
  return (diff > 5)
detectedOrFiveSeconds _ [_] = return True
detectedOrFiveSeconds _ _   = return False

initialiseDetector :: Int -> DetectionParams -> InternalState -> IO (IOProcessor () InternalState)
initialiseDetector cam params state = do
  fn <- getDataFileName "haarcascade_frontalface_alt.xml"
  return (detector cam params fn state)

-- Detects the current person, calculates its position and size
-- and notifies the system when necessary.
detector :: Int -> DetectionParams -> String -> InternalState -> IOProcessor () InternalState
detector n params fn state = camera n >>> haar fn >>> detectPosture params state

runFinder :: Int -> IO (Maybe (Int, Int, Int, Int))
runFinder cam = do
   -- Run for a few seconds or until once face is detected
   finderProc <- initialiseFinder cam
   time       <- getCurrentTime
   res        <- runTill finderProc (detectedOrFiveSeconds time)
   case res of
     [CvRect x y w h] -> return (Just (fI x, fI y, fI w, fI h))
     _                -> return (Nothing :: Maybe (Int, Int, Int, Int))
 where fI = fromIntegral :: CInt -> Int
  
initialiseFinder :: Int -> IO (IOProcessor () [CvRect])
initialiseFinder cam = do
  fn  <- getDataFileName "haarcascade_frontalface_alt.xml"
  return (finder cam fn)

-- Runs the face detector on the first camera
finder :: Int -> String -> IOProcessor () [CvRect]
finder n fn = camera n >>> haar fn

-- Internal state is
-- CvRect -> Head position when sitting correctly
-- Int    -> Correction factor
-- Int    -> Delay
-- Int    -> wrong frames in the past 'Delay'
-- Int    -> missing frames in the past 'Delay'
data DetectionParams = DetectionParams
  { faceLoc    :: (Int, Int, Int, Int)
  , correction :: Int
  , delay      :: Int
  }

type InternalState = (PostureState, UTCTime)
type PostureState = (Int, Int)

-- Detects faces, compares with known references and notifies the system if the
-- user should correct the posture
detectPosture :: DetectionParams -> InternalState -> IOSource [CvRect] InternalState
detectPosture params initialState = processor
  (\rects (state, time) -> do
     -- Delays for several milisecons if the previous call happened
     -- not too long ago
     time' <- getCurrentTime
     let diff = diffUTCTime time' time
         wait = 1000000 * (0.8 - diff)
     when (wait > 0) $ threadDelay $ round wait

     -- Processes the current internal state and calculates the new one
     let state' = process params rects state

     -- Get a new timestamp and return the new state
     t' <- getCurrentTime
     return (state', t')
  )
  (\_           -> return initialState)
  (\s           -> return s)
  (\_           -> return ())

-- Calculates the new internal state from the previous one and the list of
-- detected faces
process :: DetectionParams -> [CvRect] -> PostureState -> PostureState
process params es (wrong, missing) =
   (withinRange (0, delay params) wrong', withinRange (0, delay params) missing')
 where (wrong', missing') 
           | null es                             = (wrong - 1, missing + 1)
           | all (>= defaultMargin) corDistances = (wrong + 1, missing - 1)
           | otherwise                           = (0, 0)

       corDistances = map (\v -> v / (fromIntegral (correction params) + 1)) distances

       calculateDistance :: CvRect -> Double
       calculateDistance e = let (d1, d2) = calculateDistances e rect
                             in d1 * d2

       distances :: [ Double ]
       distances = map calculateDistance es

       rect = CvRect (fI x) (fI y) (fI w) (fI h)
       (x,y,w,h) = faceLoc params
       fI = fromIntegral

-- Face detector image processor
haar :: String -> IOProcessor Image [CvRect]
haar fn = haarDetect fn 1.1 3 (HaarDetectFlag 1) (CvSize 40 40)
        
-- This value is used to compare the calculated values from the 'correct' ones.
-- It is corrected (or relaxed) using a value in the internal state.
defaultMargin :: Double
defaultMargin = 500.0

-- Auxiliary operations

-- Calculates the distance between two rectangles in terms of position and
-- size.
calculateDistances :: CvRect -> CvRect -> (Double, Double)
calculateDistances r1 r2 = (d1, d2)
 where d1 = sqrt (f1 + f2)
       d2 = sqrt (f3 + f4)
       f1 = varDistance (rectX r1)      (rectX r2)      ^ (2 :: Int)
       f2 = varDistance (rectY r1)      (rectY r2)      ^ (2 :: Int)
       f3 = varDistance (rectWidth r1)  (rectWidth r2)  ^ (2 :: Int)
       f4 = varDistance (rectHeight r1) (rectHeight r2) ^ (2 :: Int)

-- Calculates the distance between two points
varDistance :: CInt -> CInt -> Double
varDistance v1 v2 = abs (fromIntegral v1 - fromIntegral v2)

-- withinRange (b1, b2) a returns the closes value to a in the range [b1, b2]
withinRange :: Ord a => (a, a) -> a -> a
withinRange (a, b) x
 | x >= a && x <= b = x
 | x < a            = a
 | otherwise        = b

data DetectionStatus = DetectionOk
                     | DetectionWrong
                     | DetectionUnknown

detectionStatus :: DetectionParams -> InternalState -> DetectionStatus
detectionStatus params ((w, m), _)
  | w + m >= d && w > d - 2  = DetectionWrong
  | m >= d                   = DetectionUnknown
  | otherwise                = DetectionOk
 where d = delay params