repa-io-2.0.0.1: Data/Array/Repa/IO/BMP.hs
{-# LANGUAGE PackageImports, PatternGuards, ExplicitForAll #-}
-- | Reading and writing arrays as uncompressed 24 and 32 bit Windows BMP files.
module Data.Array.Repa.IO.BMP
( readImageFromBMP
, readComponentsFromBMP
, readComponentsListFromBMP
, readMatrixFromGreyscaleBMP
-- Writing.
, writeImageToBMP
, writeComponentsToBMP
, writeComponentsListToBMP
, writeMatrixToGreyscaleBMP)
where
import Data.Array.Repa as A
import Data.Array.Repa.ByteString as A
import Prelude as P
import Codec.BMP
import Data.Word
-- NOTE: We set most of these functions as NOINLINE so it's easier to understand
-- what's going on in the core programs. The top-level IO functions are
-- only called a few times each, so it doesn't matter if they're not
-- worker/wrappered etc.
-- Read -------------------------------------------------------------------------------------------
-- | Read a matrix from a `BMP` file.
-- Each pixel is converted to greyscale, normalised to [0..1] and used
-- as the corresponding array element. If anything goes wrong when loading the file then `Error`.
readMatrixFromGreyscaleBMP
:: FilePath
-> IO (Either Error (Array DIM2 Double))
{-# NOINLINE readMatrixFromGreyscaleBMP #-}
readMatrixFromGreyscaleBMP filePath
= do eComps <- readComponentsFromBMP filePath
case eComps of
Left err -> return $ Left err
Right (arrRed, arrGreen, arrBlue)
-> let arr = force2
$ A.fromFunction (extent arrRed)
(\ix -> sqrt ( (fromIntegral (arrRed ! ix) / 255) ^ (2 :: Int)
+ (fromIntegral (arrGreen ! ix) / 255) ^ (2 :: Int)
+ (fromIntegral (arrBlue ! ix) / 255) ^ (2 :: Int)))
in arr `deepSeqArray` return (Right arr)
-- | Like `readComponentsFromBMP`, but return the components as a list.
readComponentsListFromBMP
:: FilePath
-> IO (Either Error [Array DIM2 Word8])
readComponentsListFromBMP filePath
= do eComps <- readComponentsFromBMP filePath
case eComps of
Left err
-> return $ Left err
Right (arrRed, arrGreen, arrBlue)
-> return $ Right [arrRed, arrGreen, arrBlue]
-- | Read RGB components from a BMP file.
-- Returns arrays of red, green and blue components, all with the same extent.
-- If anything goes wrong when loading the file then then `Error`.
readComponentsFromBMP
:: FilePath
-> IO (Either Error (Array DIM2 Word8, Array DIM2 Word8, Array DIM2 Word8))
{-# NOINLINE readComponentsFromBMP #-}
readComponentsFromBMP filePath
= do ebmp <- readBMP filePath
case ebmp of
Left err -> return $ Left err
Right bmp -> return $ Right (readComponentsFromBMP' bmp)
readComponentsFromBMP' bmp
= let (width, height) = bmpDimensions bmp
arr = A.fromByteString (Z :. height :. width * 4)
$ unpackBMPToRGBA32 bmp
shapeFn _ = Z :. height :. width
arrRed
= force2 $ traverse arr shapeFn
(\get (sh :. x) -> get (sh :. (x * 4)))
arrGreen
= force2 $ traverse arr shapeFn
(\get (sh :. x) -> get (sh :. (x * 4 + 1)))
arrBlue
= force2 $ traverse arr shapeFn
(\get (sh :. x) -> get (sh :. (x * 4 + 2)))
in [arrRed, arrGreen, arrBlue] `deepSeqArrays` (arrRed, arrGreen, arrBlue)
-- | Read a RGBA image from a BMP file.
-- In the result, the higher two dimensions are the height and width,
-- and the lower indexes the RGBA component of each pixel.
-- If the BMP read has no alpha channel then alpha of the resulting pixels is set to 255.
-- If anything goes wrong when loading the file then `Error`.
readImageFromBMP
:: FilePath
-> IO (Either Error (Array DIM3 Word8))
{-# NOINLINE readImageFromBMP #-}
readImageFromBMP filePath
= do ebmp <- readBMP filePath
case ebmp of
Left err -> return $ Left err
Right bmp -> return $ Right (readImageFromBMP' bmp)
readImageFromBMP' bmp
= let (width, height) = bmpDimensions bmp
arr = fromByteString (Z :. height :. width :. 4)
$ unpackBMPToRGBA32 bmp
in arr
-- Write ------------------------------------------------------------------------------------------
-- | Write a matrix to a BMP file.
-- Negative values are discarded. Positive values are normalised to the maximum
-- value in the matrix and used as greyscale pixels.
writeMatrixToGreyscaleBMP
:: forall a. (Num a, Elt a, Fractional a, RealFrac a)
=> FilePath
-> Array DIM2 a
-> IO ()
{-# NOINLINE writeMatrixToGreyscaleBMP #-}
{-# SPECIALISE writeMatrixToGreyscaleBMP :: FilePath -> Array DIM2 Float -> IO () #-}
{-# SPECIALISE writeMatrixToGreyscaleBMP :: FilePath -> Array DIM2 Double -> IO () #-}
writeMatrixToGreyscaleBMP fileName arr
= let arrNorm = normalisePositive01 arr
scale x = fromIntegral (truncate (x * 255) :: Int)
arrWord8 = A.map scale arrNorm
in writeComponentsToBMP fileName arrWord8 arrWord8 arrWord8
-- | Like `writeComponentsToBMP` but take the components as a list.
-- The list must have 3 arrays, for the red, green blue components
-- respectively, else `error`.
writeComponentsListToBMP
:: FilePath
-> [Array DIM2 Word8]
-> IO ()
writeComponentsListToBMP filePath comps
| [red, green, blue] <- comps
= writeComponentsToBMP filePath red green blue
| otherwise
= error "Data.Array.Repa.IO.BMP.writeComponentsListToBMP: wrong number of components"
-- | Write RGB components to a BMP file.
-- All arrays must have the same extent, else `error`.
writeComponentsToBMP
:: FilePath
-> Array DIM2 Word8
-> Array DIM2 Word8
-> Array DIM2 Word8
-> IO ()
{-# NOINLINE writeComponentsToBMP #-}
writeComponentsToBMP fileName arrRed arrGreen arrBlue
| not $ ( extent arrRed == extent arrGreen
&& extent arrGreen == extent arrBlue)
= error "Data.Array.Repa.IO.BMP.writeComponentsToBMP: arrays don't have same extent"
| otherwise
= do let Z :. height :. width
= extent arrRed
-- Build image data from the arrays.
let arrAlpha = fromFunction (extent arrRed) (\_ -> 255)
let arrRGBA = interleave4 arrRed arrGreen arrBlue arrAlpha
let bmp = packRGBA32ToBMP width height
$ A.toByteString arrRGBA
writeBMP fileName bmp
-- | Write a RGBA image to a BMP file.
-- The higher two dimensions are the height and width of the image.
-- The lowest dimension must have size 4, corresponding to the RGBA components
-- of each pixel, else `error`.
writeImageToBMP
:: FilePath
-> Array DIM3 Word8
-> IO ()
{-# NOINLINE writeImageToBMP #-}
writeImageToBMP fileName arrImage
| comps /= 4
= error "Data.Array.Repa.IO.BMP: lowest order dimension must be 4"
| otherwise
= let bmp = packRGBA32ToBMP height width
$ A.toByteString arrImage
in writeBMP fileName bmp
where Z :. height :. width :. comps
= extent arrImage
-- Normalise --------------------------------------------------------------------------------------
-- | Normalise a matrix to to [0 .. 1], discarding negative values.
-- If the maximum value is 0 then return the array unchanged.
normalisePositive01
:: (Shape sh, Elt a, Fractional a, Ord a)
=> Array sh a
-> Array sh a
{-# INLINE normalisePositive01 #-}
normalisePositive01 arr
= let mx = foldAll max 0 arr
elemFn x
| x >= 0 = x / mx
| otherwise = x
in mx `seq`
if mx == 0
then arr
else A.map elemFn arr