ply-loader 0.1.1.2 → 0.2
raw patch · 6 files changed
+195/−163 lines, 6 filesdep +transformers
Dependencies added: transformers
Files
- ply-loader.cabal +5/−4
- src/PLY.hs +151/−0
- src/PLY/Ascii.hs +26/−0
- src/PLY/Conf.hs +10/−9
- src/PLY/Data.hs +0/−147
- src/executable/Main.hs +3/−3
ply-loader.cabal view
@@ -1,5 +1,5 @@ name: ply-loader-version: 0.1.1.2+version: 0.2 synopsis: PLY file loader. description: PLY is a lightweight file format for representing 3D@@ -32,8 +32,8 @@ library ghc-options: -O2 -Wall- exposed-modules: PLY.Data, PLY.Types, PLY.Conf, PLY.Internal.Parsers,- PLY.Internal.StrictReplicate+ exposed-modules: PLY, PLY.Ascii, PLY.Conf, PLY.Types,+ PLY.Internal.Parsers, PLY.Internal.StrictReplicate build-depends: base >= 4.6 && < 5, attoparsec >= 0.10, @@ -43,7 +43,8 @@ vector >= 0.9, filepath, directory,- parallel-io >= 0.3.2+ parallel-io >= 0.3.2,+ transformers hs-source-dirs: src default-language: Haskell2010
+ src/PLY.hs view
@@ -0,0 +1,151 @@+{-# LANGUAGE ScopedTypeVariables, BangPatterns #-}+-- |The loading of a @ply@ file is broken down into two steps: header+-- parsing, and data loading. The 'loadPLY' function will, if+-- successful, return a data structure that may be queried to extract+-- numeric data using 'loadElements' and 'loadElementsV3'. For example, +--+-- > {-# LANGUAGE OverloadedStrings #-}+-- > import Data.Vector.Storable (Vector)+-- > import Linear.V3+-- > import PLY+-- >+-- > loadVerts :: FilePath -> IO (Either String (Vector (V3 Float)))+-- > loadVerts = loadElementsV3 "vertex"+-- +-- To load all vertex data from a series of @ply@ files identified by+-- a @.conf@ file, consider using,+--+-- > fromConf :: FilePath -> IO (Either String (Vector (V3 Float)))+-- > fromConf = loadConfV3 "vertex"+--+module PLY (-- * Easy loading interface+ loadElements, loadElementsV3, loadConfV3,++ -- * Loading components+ PLYData, loadHeader, preloadPly, + loadPlyElements, loadPlyElementsV3) where+import Control.Applicative+import Control.Concurrent.ParallelIO (parallel)+import Control.Monad ((>=>))+import Control.Monad.Trans.Error+import Data.Attoparsec.Char8+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BC+import Data.Either (partitionEithers)+import Data.Vector (Vector)+import qualified Data.Vector.Storable as VS+import Linear+import System.Directory (canonicalizePath)+import System.FilePath (takeDirectory, (</>))++import PLY.Ascii+import PLY.Conf+import PLY.Internal.Parsers (line, parseSkip, header)+import PLY.Types++type Header = (Format, [Element])++-- |A PLY header and the associated raw data. Use 'loadElements' or+-- 'loadElementsV3' to extract a particular element array.+data PLYData = PLYData !ByteString !Header++instance Show PLYData where+ show (PLYData _ h) = "PLYData <bytes> " ++ show h++-- Helper to ensure that that an 'Either' is strict in the argument to+-- the data constructor. This is important to keep Vector operations+-- flowing efficiently.+strictE :: Either a b -> Either a b+strictE l@(Left !_x) = l+strictE r@(Right !_x) = r++-- |Load a PLY header from a file.+loadHeader :: FilePath -> IO (Either String PLYData)+loadHeader = fmap preloadPly . BS.readFile++-- |Attempt to parse a PLY file from the given bytes. If the PLY+-- header is successfully parsed, the 'PLYData' value returned may be+-- used with 'loadElements' and 'loadElementsV3' to extract data.+preloadPly :: ByteString -> Either String PLYData+preloadPly = aux . parse header+ where aux (Fail _t ctxt msg) = Left $ "Parse failed: "++msg++" in "++show ctxt+ aux (Partial _) = Left "Incomplete header"+ aux (Done t r) = Right $ PLYData t r++-- |@loadPlyElements elementName ply@ loads a 'Vector' of each vertex of+-- the requested element array. If you are extracting 3D data,+-- consider using 'loadPlyElementsV3'.+loadPlyElements :: ByteString -> PLYData -> + Either String (Vector (Vector Scalar))+loadPlyElements n (PLYData body (ASCII, ess)) = strictE $ go ess body+ where go [] _ = Left "Unknown element"+ go (e:es) b | elName e == n = parseOnly (parseASCII e) b+ | otherwise = go es $+ parseSkip (count (elNum e) line *> pure ()) b+loadPlyElements _ _ = error "Binary PLY is unsupported"+{-# INLINABLE loadPlyElements #-}++-- |@loadElements elementName plyFile@ loads a 'Vector' of each+-- vertex of the requested element array from @plyFile@.+loadElements :: ByteString -> FilePath+ -> IO (Either String (Vector (Vector Scalar)))+loadElements name file =+ (preloadPly >=> loadPlyElements name) <$> BS.readFile file+{-# INLINABLE loadElements #-}++-- |Like 'loadPlyElements', but restricted to 3D vectors. When it can be+-- used, this function is much more efficient than 'loadPlyElements'.+loadPlyElementsV3 :: PLYType a => ByteString -> PLYData -> + Either String (VS.Vector (V3 a))+loadPlyElementsV3 n (PLYData body (ASCII, ess)) = strictE $ go ess body+ where go [] _ = Left "Unknown element"+ go (e:es) b | elName e == n = parseOnly (parseASCIIv3 e) b+ | otherwise = go es $+ parseSkip (count (elNum e) line *> pure ()) b+loadPlyElementsV3 _ _ = error "Binary PLY is unsupported"+{-# INLINABLE loadPlyElementsV3 #-}++-- |Like 'loadElements', but restricted to 3D vectors. When it can+-- be used, this function is much more efficient thatn+-- 'loadPlyElements'.+loadElementsV3 :: PLYType a => ByteString -> FilePath+ -> IO (Either String (VS.Vector (V3 a)))+loadElementsV3 name file =+ (preloadPly >=> loadPlyElementsV3 name) <$> BS.readFile file+{-# INLINABLE loadElementsV3 #-}++type ErrorMsg a = Either String a++-- |Load all meshes identified by a @.conf@ file in parallel, and+-- transform vertex data into the coordinate frame specified by the+-- @.conf@ file. The application @loadMeshesV3 confFile element@ loads+-- @confFile@ to identify every @ply@ mesh to load. The @ply@ files+-- are loaded from the same directory that contained the @.conf@ file,+-- and the data associated with @element@ (e.g. @\"vertex\"@) is+-- loaded, transformed, and concatenated from all the meshes.+loadConfV3 :: forall a. (PLYType a, Fractional a, Conjugate a, RealFloat a)+ => ByteString -> FilePath -> IO (Either String (VS.Vector (V3 a)))+loadConfV3 element confFile = + do dir <- takeDirectory <$> canonicalizePath confFile+ runErrorT $+ do c <- ErrorT $ parseConf <$> BS.readFile confFile+ ErrorT $ checkConcat <$> loadAll dir c+ where checkErrors :: [ErrorMsg b] -> ErrorMsg [b]+ checkErrors xs = let (ls,rs) = partitionEithers xs+ in if null ls then Right rs else Left (unlines ls)+ checkConcat :: [ErrorMsg (VS.Vector (V3 a))]+ -> ErrorMsg (VS.Vector (V3 a))+ checkConcat = (fmap VS.concat $!) . checkErrors+ loadMesh :: FilePath -> M44 a -> (ByteString, Transformation a)+ -> IO (ErrorMsg (VS.Vector (V3 a)))+ loadMesh d _cam (f, (t,r)) = + -- It is convenient to ignore the camera transformation so+ -- that the object is at the origin.+ let m = (^+^ fmap realToFrac t ) . rotate (conjugate (fmap realToFrac r))+ in fmap (VS.map m) <$> loadElementsV3 element (d </> BC.unpack f)+ loadAll :: FilePath -> Conf a -> IO ([ErrorMsg (VS.Vector (V3 a))])+ loadAll dir (Conf (t,r) ms) = + let cam = mkTransformation r t+ in parallel $ map (loadMesh dir cam) ms+{-# INLINABLE loadConfV3 #-}
+ src/PLY/Ascii.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE ScopedTypeVariables #-}+module PLY.Ascii where+import Control.Applicative+import Data.Attoparsec.Char8+import Data.Vector (Vector)+import qualified Data.Vector as V+import qualified Data.Vector.Storable as VS+import Linear.V3 (V3(..))++import PLY.Internal.Parsers+import PLY.Internal.StrictReplicate+import PLY.Types++parseASCII :: Element -> Parser (Vector (Vector Scalar))+parseASCII e = replicateM' (elNum e)+ (skip *> (V.fromList <$> multiProps (elProps e)))++parseASCIIv3 :: forall a. PLYType a => Element -> Parser (VS.Vector (V3 a))+parseASCIIv3 (Element _ n ps@[_,_,_])+ | all samePropType ps = replicateM' n (skip *> (V3 <$> p <*> p <*> p))+ | otherwise = empty+ where t = plyType (undefined::a)+ p = unsafeUnwrap <$> (parseScalar t <* skipSpace)+ samePropType (ScalarProperty t' _) = t == t'+ samePropType (ListProperty _ _) = False+parseASCIIv3 _ = empty
src/PLY/Conf.hs view
@@ -16,28 +16,29 @@ -- |A @.conf@ file includes a base transformation matrix, and a -- list of meshes identified by their file name and a 'Transformation' -- to place geometry in a consistent coordinate frame.-data Conf = Conf { camera :: Transformation Double - , meshes :: [(ByteString, Transformation Double)] }- deriving Show+data Conf a = Conf { camera :: Transformation a+ , meshes :: [(ByteString, Transformation a)] }+ deriving Show -- |Parse a 3D translation vector followed by a quaternion.-transformation :: Parser (V3 Double, Quaternion Double)+transformation :: Fractional a => Parser (V3 a, Quaternion a) transformation = (,) <$> vec <*> rotation- where vec = (\[x,y,z] -> V3 x y z) <$> count 3 (skipSpace *> double)- rotation = flip Quaternion <$> vec <*> (skipSpace *> double)+ where vec = (\[x,y,z] -> V3 x y z) <$> count 3 (skipSpace *> double')+ rotation = flip Quaternion <$> vec <*> (skipSpace *> double')+ double' = realToFrac <$> double --rotation = Quaternion <$> (skipSpace *> double) <*> vec --rev (V3 x y z) = V3 z y x -- |Parse a mesh file specification.-mesh :: Parser (ByteString, Transformation Double)+mesh :: Fractional a => Parser (ByteString, Transformation a) mesh = (,) <$> ("bmesh " .*> word) <*> transformation -- |Parser for a Stanford .conf file.-conf :: Parser Conf+conf :: Fractional a => Parser (Conf a) conf = Conf <$> ("camera " .*> transformation <* skipSpace) <*> (skipSpace *> cybmesh *> many1 (skipSpace *> mesh)) where cybmesh = skipMany ("mesh " .*> line) -- Not a PLY file! -- |Parse a Stanford .conf file.-parseConf :: ByteString -> Either String Conf+parseConf :: Fractional a => ByteString -> Either String (Conf a) parseConf = parseOnly conf
− src/PLY/Data.hs
@@ -1,147 +0,0 @@-{-# LANGUAGE ScopedTypeVariables, BangPatterns #-}--- |The loading of a @ply@ file is broken down into two steps: header--- parsing, and data loading. The 'loadPLY' function will, if--- successful, return a data structure that may be queried to extract--- numeric data using 'loadElements' and 'loadElementsV3'. For example, ------ > {-# LANGUAGE OverloadedStrings #-}--- > import Control.Monad ((>=>))--- > import Data.ByteString (ByteString)--- > import qualified Data.Vector.Storable as VS--- > import Linear.V3--- >--- > loadVerts :: ByteString -> Either String (VS.Vector (V3 Float))--- > loadVerts = loadPLY >=> loadElementsV3 "vertex"--- --- To load all vertex data from a series of @ply@ files identified by--- a @.conf@ file, consider using,------ > loadConf :: FilePath -> IO (Either [String] (VS.Vector (V3 Float)))--- > loadConf confFile = loadMeshesV3 confFile "vertex"----module PLY.Data (PLYData, loadPLY, loadElements, loadElementsV3, - loadMeshesV3, loadHeader) where-import Control.Applicative-import Control.Concurrent.ParallelIO (parallel)-import Data.Attoparsec.Char8-import Data.ByteString (ByteString)-import qualified Data.ByteString as BS-import qualified Data.ByteString.Char8 as BC-import Data.Either (partitionEithers)-import Data.Vector (Vector)-import qualified Data.Vector as V-import qualified Data.Vector.Storable as VS-import Linear-import System.Directory (canonicalizePath)-import System.FilePath (takeDirectory, (</>))--import PLY.Conf-import PLY.Internal.Parsers (line, parseSkip, skip, multiProps, - parseScalar, header)-import PLY.Internal.StrictReplicate-import PLY.Types--type Header = (Format, [Element])---- |A PLY header and the associated raw data. Use 'loadElements' or--- 'loadElementsV3' to extract a particular element array.-newtype PLYData = PLYData (ByteString, Header)--instance Show PLYData where- show (PLYData (_,h)) = "PLYData <bytes> " ++ show h--parseASCII :: Element -> Parser (Vector (Vector Scalar))-parseASCII e = replicateM' (elNum e)- (skip *> (V.fromList <$> multiProps (elProps e)))--parseASCIIv3 :: forall a. PLYType a => Element -> Parser (VS.Vector (V3 a))-parseASCIIv3 (Element _ n ps@[_,_,_])- | all samePropType ps = replicateM' n (skip *> (V3 <$> p <*> p <*> p))- | otherwise = empty- where t = plyType (undefined::a)- p = unsafeUnwrap <$> (parseScalar t <* skipSpace)- samePropType (ScalarProperty t' _) = t == t'- samePropType (ListProperty _ _) = False-parseASCIIv3 _ = empty---- |If the PLY header is successfully parsed, the 'PLYData' value--- returned may be used with 'loadElements' and 'loadElementsV3' to--- extract data.-loadPLY :: ByteString -> Either String PLYData-loadPLY = aux . parse header- where aux (Fail _t ctxt msg) = Left $ "Parse failed: "++msg++" in "++show ctxt- aux (Partial _) = Left "Incomplete header"- aux (Done t r) = Right $ PLYData (t, r)---- |Load a PLY header from a file.-loadHeader :: FilePath -> IO (Either String PLYData)-loadHeader = fmap loadPLY . BS.readFile---- |@loadElements elementName ply@ loads a 'Vector' of each vertex of--- the requested element array. If you are extracting 3D data,--- consider using 'loadElementsV3'.-loadElements :: ByteString -> PLYData -> - Either String (Vector (Vector Scalar))-loadElements n (PLYData (body, (ASCII, ess))) = go ess body- where go [] _ = Left "Unknown element"- go (e:es) b | elName e == n = parseOnly (parseASCII e) b- | otherwise = go es $- parseSkip (count (elNum e) line *> pure ()) b-loadElements _ _ = error "Binary PLY is unsupported"-{-# INLINABLE loadElements #-}---- |Like 'loadElements', but restricted to 3D vectors. When it can be--- used, this function is much more efficient than 'loadElements'.-loadElementsV3 :: PLYType a => ByteString -> PLYData -> - Either String (VS.Vector (V3 a))-loadElementsV3 n (PLYData (body, (ASCII, ess))) = go ess body- where go [] _ = Left "Unknown element"- go (e:es) b | elName e == n = parseOnly (parseASCIIv3 e) b- | otherwise = go es $- parseSkip (count (elNum e) line *> pure ()) b-loadElementsV3 _ _ = error "Binary PLY is unsupported"-{-# INLINABLE loadElementsV3 #-}--(>=!>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c-f >=!> g !x = f x >>= (g $!)-infixr 1 >=!>---- |Load all meshes identified by a @.conf@ file in parallel, and--- transform vertex data into the coordinate frame specified by the--- @.conf@ file. The application @loadMeshesV3 confFile element@ loads--- @confFile@ to identify every @ply@ mesh to load. The @ply@ files--- are loaded from the same directory that contained the @.conf@ file,--- and the data associated with @element@ (e.g. @\"vertex\"@) is--- loaded, transformed, and concatenated from all the meshes.-loadMeshesV3 :: forall a. (PLYType a, Fractional a, Conjugate a, RealFloat a) =>- FilePath -> ByteString -> IO (Either [String] (VS.Vector (V3 a)))-loadMeshesV3 confFile element = do dir <- takeDirectory <$> - canonicalizePath confFile- c <- parseConf <$> BS.readFile confFile- let cam = let Right (Conf (t,r) _) = c- in fmap (fmap realToFrac) $- mkTransformation r t- either (return . Left . (:[]))- (fmap checkConcat . loadAllMeshes dir cam)- c- where checkErrors :: [Either String (VS.Vector (V3 a))]- -> Either [String] [VS.Vector (V3 a)]- checkErrors xs = let (ls,rs) = partitionEithers xs- in if null ls then Right rs else Left ls- checkConcat :: [Either String (VS.Vector (V3 a))]- -> Either [String] (VS.Vector (V3 a))- checkConcat = (fmap VS.concat $!) . checkErrors- loadMesh :: FilePath -> M44 a -> (ByteString, Transformation Double)- -> IO (Either String (VS.Vector (V3 a)))- loadMesh d _cam (f, (t,r)) = - -- It is convenient to ignore the camera transformation so- -- that the object is at the origin.- let m = (^+^ fmap realToFrac t ) . rotate (conjugate (fmap realToFrac r))- in (loadPLY - >=!> loadElementsV3 element- >=!> return . VS.map m)- <$> BS.readFile (d </> BC.unpack f)- loadAllMeshes :: FilePath -> M44 a -> Conf -> - IO ([Either String (VS.Vector (V3 a))])- loadAllMeshes dir cam = parallel . map (loadMesh dir cam) . meshes-{-# INLINABLE loadMeshesV3 #-}
src/executable/Main.hs view
@@ -5,7 +5,7 @@ import Control.Monad (when) import qualified Data.Vector.Storable as VS import Linear.V3-import PLY.Data+import PLY import System.Environment (getArgs) import System.IO (withBinaryFile, IOMode(WriteMode), hPutBuf) @@ -13,8 +13,8 @@ main = do args@(~[confFile, outputFile]) <- getArgs when (length args /= 2) (error "Usage: ply2bin confFile outputFile")- Right pts <- loadMeshesV3 confFile "vertex" - :: IO (Either [String] (VS.Vector (V3 Float)))+ Right pts <- loadConfV3 "vertex" confFile+ :: IO (Either String (VS.Vector (V3 Float))) putStrLn $ "Loaded "++show (VS.length pts)++" vertices" withBinaryFile outputFile WriteMode $ \h -> VS.unsafeWith pts $ \ptr ->