packages feed

patch-image-0.3.2: src/LinearAlgebra.hs

module LinearAlgebra where

import qualified Matrix.Sparse as Sparse
import qualified Matrix.Vector as Vector
import qualified Matrix.QR.Givens as QR

import Data.Complex (Complex((:+)))

import qualified Data.List.HT as ListHT
import qualified Data.List as List
import qualified Data.Array as Array
import qualified Data.Map as Map
import Data.Array (Array)
import Data.Tuple.HT (mapPair, mapSnd)
import Data.Maybe (isJust)
import Data.Map (Map)

import Control.Applicative ((<$>), (<$))


fixAtLeastOne :: a -> [Maybe a] -> [Maybe a]
fixAtLeastOne zero ms =
   case (any isJust ms, ms) of
      (True, _) -> ms
      (False, _:nothings) -> Just zero : nothings
      (False, []) -> error "fixAtLeastOne: empty image list"

{- |
If no coordinate is fixed, then the first one will be fixed to the given value.
This is not strictly necessary.
Without a fixed coordinate,
the solver will center all solutions around zero.
However, there will not necessarily be an image with a zero coordinate,
which is somehow ugly.
-}
fixAtLeastOnePosition ::
   (a,b) -> [(Maybe a, Maybe b)] -> [(Maybe a, Maybe b)]
fixAtLeastOnePosition (a,b) =
   parallel (fixAtLeastOne a, fixAtLeastOne b)

fixAtLeastOneAnglePosition ::
   (angle, (a,b)) ->
   [(Maybe angle, (Maybe a, Maybe b))] ->
   [(Maybe angle, (Maybe a, Maybe b))]
fixAtLeastOneAnglePosition (angle, ab) =
   parallel (fixAtLeastOne angle, fixAtLeastOnePosition ab)

parallel :: ([a0] -> [a1], [b0] -> [b1]) -> ([(a0,b0)] -> [(a1,b1)])
parallel fs = uncurry zip . mapPair fs . unzip


type Matrix = Sparse.Matrix Int Int
type Vector = Array Int

sparseMatrix :: Int -> Int -> [((Int, Int), Float)] -> Matrix Float
sparseMatrix numRows numCols =
   Sparse.fromMap ((0,0), (numRows-1, numCols-1)) . Map.fromList

-- every column must be non-empty
sparseToColumns :: Matrix a -> [Map Int a]
sparseToColumns = Map.elems . Sparse.toColumns

sparseFromColumns :: (Int,Int) -> [Map Int a] -> Matrix a
sparseFromColumns (m0,m1) cols =
   Sparse.fromColumns ((m0,0), (m1, length cols-1)) $
   Map.fromList $ zip [0..] cols

addSparseColumn :: (Num a) => Vector a -> Map Int a -> Vector a
addSparseColumn v col = Array.accum (+) v $ Map.toList col


elm :: Int -> Int -> a -> ((Int, Int), a)
elm row col x = ((row, col), x)


absolutePositionsFromPairDisplacements ::
   [(Maybe Float, Maybe Float)] -> [((Int, Int), (Float, Float))] ->
   ([(Float,Float)], [(Float,Float)])
absolutePositionsFromPairDisplacements mxys displacements =
   let numPics = length mxys
       (mxs, mys) = unzip mxys
       (is, (dxs, dys)) = mapSnd unzip $ unzip displacements
       matrix =
          sparseMatrix (length is) numPics $ concat $
          zipWith (\k (ia,ib) -> [elm k ia (-1), elm k ib 1]) [0..] is
       solve ms ds = leastSquaresSelected matrix ms (Vector.fromList ds)
       (pxs, achievedDxs) = solve mxs dxs
       (pys, achievedDys) = solve mys dys
   in  (zip pxs pys, zip achievedDxs achievedDys)


leastSquaresSelected ::
   Matrix Float -> [Maybe Float] -> Vector Float -> ([Float], [Float])
leastSquaresSelected m mas rhs0 =
   let (lhsCols,rhsCols) =
          ListHT.unzipEithers $
          zipWith
             (\col ma ->
                case ma of
                   Nothing -> Left col
                   Just a -> Right $ fmap (a*) col)
             (sparseToColumns m) mas
       lhs = sparseFromColumns (Array.bounds rhs0) lhsCols
       rhs = foldl addSparseColumn (0 <$ rhs0) rhsCols
       sol = QR.leastSquares lhs $ Vector.sub rhs0 rhs
   in  (snd $
        List.mapAccumL
           (curry $ \x ->
               case x of
                  (as, Just a) -> (as, a)
                  (a:as, Nothing) -> (as, a)
                  ([], Nothing) -> error "too few elements in solution vector")
           (Vector.toList sol) mas,
        Vector.toList $ Vector.add (Sparse.mulVector lhs sol) rhs)


zeroVector :: Int -> Vector Float
zeroVector n = Vector.fromList $ replicate n 0

{-
Approximate rotation from point correspondences.
Here (dx, dy) is the displacement with respect to the origin (0,0),
that is, the pair plays the role of the absolute position.

x1 = dx + c*x0 - s*y0
y1 = dy + s*x0 + c*y0

               /dx\
/1 0 x0 -y0\ . |dy| = /x1\
\0 1 y0  x0/   |c |   \y1/
               \s /

Formerly, we tried to scale dx and dy down using a weight,
such that dx and dy had magnitude similar to c and s.
However the weight would only influence the result
for under-constrained equation systems.
This is usually not the case and
the solver does not support rank-deficient matrices, anyway.
-}
layoutFromPairDisplacements ::
   [(Maybe (Float, Float), (Maybe Float, Maybe Float))] ->
   [((Int, Int), ((Float, Float), (Float, Float)))] ->
   ([((Float,Float), Complex Float)], [(Float,Float)])
layoutFromPairDisplacements mrxys correspondences =
   let numPics = length mrxys
       matrix =
          sparseMatrix (2 * length correspondences) (4*numPics) $ concat $
          zipWith
             (\k ((ia,ib), ((xa,ya),(xb,yb))) ->
                elm (k+0) (4*ia+0) (-1) :
                elm (k+1) (4*ia+1) (-1) :
                elm (k+0) (4*ia+2) (-xa) :
                elm (k+0) (4*ia+3) ya :
                elm (k+1) (4*ia+2) (-ya) :
                elm (k+1) (4*ia+3) (-xa) :
                elm (k+0) (4*ib+0) 1 :
                elm (k+1) (4*ib+1) 1 :
                elm (k+0) (4*ib+2) xb :
                elm (k+0) (4*ib+3) (-yb) :
                elm (k+1) (4*ib+2) yb :
                elm (k+1) (4*ib+3) xb :
                [])
             [0,2..] correspondences
       (solution, projection) =
          leastSquaresSelected matrix
             (concatMap (\(mr, (mx,my)) -> [mx, my, fst <$> mr, snd <$> mr]) $
              mrxys)
             (zeroVector (2 * length correspondences))
   in  (map (\[dx,dy,rx,ry] -> ((dx,dy), rx :+ ry)) $
        ListHT.sliceVertical 4 solution,
        map (\[x,y] -> (x,y)) $
        ListHT.sliceVertical 2 projection)