packages feed

fei-datasets-1.0.0: src/MXNet/NN/DataIter/Coco.hs

{-# LANGUAGE TemplateHaskell      #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns         #-}
module MXNet.NN.DataIter.Coco(
    module MXNet.NN.DataIter.Common,
    Configuration(..), CocoConfig, conf_width, Coco(..),
    classes, coco,
    cocoImageList, cocoImages, cocoImagesBBoxes, cocoImagesBBoxesMasks,
    loadImage, loadBoundingBoxes, loadMasks,
    augmentWithBBoxes,
) where

import           Control.Lens                  (ix, makeLenses, (^?!))
import qualified Data.Aeson                    as Aeson
import           Data.Array.Repa               ((:.) (..), Z (..),
                                                fromListUnboxed)
import qualified Data.Array.Repa               as Repa
import           Data.Conduit
import qualified Data.Conduit.Combinators      as C (yieldMany)
import qualified Data.Conduit.List             as C
import qualified Data.Random                   as RND (runRVar, shuffleN,
                                                       stdUniform)
import           Data.Random.Source.StdGen     (StdGen)
import qualified Data.Store                    as Store
import qualified Data.Vector.Storable          as SV (unsafeCast)
import           GHC.Float                     (double2Float)
import           GHC.Generics                  (Generic)
import qualified Graphics.Image                as HIP
import qualified Graphics.Image.Interface      as HIP
import qualified Graphics.Image.Interface.Repa as HIP
import           RIO
import qualified RIO.ByteString                as SBS
import           RIO.Directory
import           RIO.FilePath
import qualified RIO.Map                       as M
import qualified RIO.NonEmpty                  as RNE
import qualified RIO.Vector.Boxed              as V
import qualified RIO.Vector.Storable           as SV

import           MXNet.Coco.Mask
import           MXNet.Coco.Types
import           MXNet.NN.DataIter.Common

classes :: V.Vector String
classes = V.fromList [
    "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train",
    "truck", "boat", "traffic light", "fire hydrant", "stop sign",
    "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep",
    "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella",
    "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard",
    "sports ball", "kite", "baseball bat", "baseball glove", "skateboard",
    "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork",
    "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange",
    "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair",
    "couch", "potted plant", "bed", "dining table", "toilet", "tv",
    "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave",
    "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase",
    "scissors", "teddy bear", "hair drier", "toothbrush"]

data Coco = Coco FilePath String Instance (M.Map Int Int)
    deriving Generic
instance Store.Store Coco

data FileNotFound = FileNotFound String String
    deriving Show
instance Exception FileNotFound

data instance Configuration "coco" = CocoConfig {
    _conf_coco :: Coco,
    _conf_width :: Int,
    _conf_mean :: (Float, Float, Float),
    _conf_std :: (Float, Float, Float)
}

makeLenses 'CocoConfig
type CocoConfig = Configuration "coco"

instance HasDatasetConfig CocoConfig where
    type DatasetTag CocoConfig = "coco"
    datasetConfig = id

instance ImageDataset "coco" where
    imagesMean = conf_mean
    imagesStdDev = conf_std

cached :: Store.Store a => String -> IO a -> IO a
cached name action = do
    createDirectoryIfMissing True "cache"
    hitCache <- doesFileExist path
    if hitCache then
        SBS.readFile path >>= Store.decodeIO
    else do
        obj <- action
        SBS.writeFile path (Store.encode obj)
        return obj
  where
    path = "cache/" ++ name

coco :: String -> String -> IO Coco
coco base datasplit = cached (datasplit ++ ".store") $ do
    let annotationFile = base </> "annotations" </> ("instances_" ++ datasplit ++ ".json")
    inst <- raiseLeft (FileNotFound annotationFile) $ Aeson.eitherDecodeFileStrict' annotationFile
    let cat_to_classid = M.fromList $ V.toList $ V.map
           (\cat -> (cat ^. odc_id, get_cat_classid (cat ^. odc_name)))
           (inst ^. categories)
    return $ Coco base datasplit inst cat_to_classid

    where
        get_cat_classid (flip V.elemIndex classes -> Just index) = index
        get_cat_classid _ = error "index not found in classes"


cocoImageList :: (MonadReader env m, HasDatasetConfig env, DatasetTag env ~ "coco", MonadIO m)
              => IORef StdGen -> ConduitT () Image m ()
cocoImageList rand_gen = do
    Coco _ _ inst _ <- view (datasetConfig . conf_coco)
    let all_images = inst ^. images
    all_images_shuffle <- liftIO $
        RND.runRVar (RND.shuffleN (length all_images) (V.toList all_images)) rand_gen
    C.yieldMany all_images_shuffle -- .| C.iterM (liftIO . print)


cocoImages :: (MonadReader env m, HasDatasetConfig env, DatasetTag env ~ "coco", MonadIO m)
           => IORef StdGen -> ConduitT () (String, ImageTensor, ImageInfo) m ()
cocoImages rand_gen = cocoImageList rand_gen .| C.mapM build
    where
        build image = do
            let filename = image ^. img_file_name
            (img, info) <- loadImage image
            return $!! (filename, img, info)


cocoImagesBBoxes :: (MonadReader env m, HasDatasetConfig env, DatasetTag env ~ "coco", MonadIO m)
                 => IORef StdGen -> ConduitT () (String, ImageTensor, ImageInfo, GTBoxes) m ()
cocoImagesBBoxes rand_gen = cocoImageList rand_gen .| C.mapM build .| C.catMaybes
    where
        build image = do
            let filename = image ^. img_file_name
            (img, info) <- loadImage image
            mboxes <- loadBoundingBoxes image
            case mboxes of
              Nothing    -> return Nothing
              Just boxes -> return $!! Just (filename, img, info, boxes)


cocoImagesBBoxesMasks :: (MonadReader env m, HasDatasetConfig env, DatasetTag env ~ "coco", MonadIO m)
                      => IORef StdGen -> ConduitT () (String, ImageTensor, ImageInfo, GTBoxes, Masks) m ()
cocoImagesBBoxesMasks rand_gen = cocoImageList rand_gen .| C.mapM build .| C.catMaybes
    where
        build image = do
            let filename = image ^. img_file_name
            (img, info) <- loadImage image
            mboxes <- loadBoundingBoxes image
            mmasks  <- loadMasks image
            case liftA2 (,) mboxes mmasks of
              Nothing             -> return Nothing
              Just (boxes, masks) -> return $!! Just (filename, img, info, boxes, masks)


augmentWithBBoxes :: MonadIO m
                  => IORef StdGen
                  -> (String, ImageTensor, ImageInfo, GTBoxes)
                  -> m (String, ImageTensor, ImageInfo, GTBoxes)
augmentWithBBoxes rand_gen inp@(ident, img, info, bboxes) = liftIO $ do
    do_flip <- RND.runRVar RND.stdUniform rand_gen
    return $
        if not do_flip
          then inp
          else let shp@(Z :. c :. h :. w) = Repa.extent img
                   flip_img (Z :. c :. y :. x) = Z :. c :. y :. (w - x - 1)
                   img_flipped = Repa.computeUnboxedS $ Repa.backpermute shp flip_img img
                   w' = fromIntegral w
                   flip_box box = Repa.fromUnboxed (Z :. 5) $ case Repa.toUnboxed box of
                                    -- flip, and rebuild the top-left, bottom-right coords
                                    [x0, y0, x1, y1, sc] -> [w'-x1-1, y0, w'-x0-1, y1, sc]
                   boxes_flipped = V.map flip_box bboxes
               in (ident, img_flipped, info, boxes_flipped)


getImageScale :: Image -> Int -> (Float, Int, Int)
getImageScale img size
  | oriW >= oriH = (sizeF / oriW, floor (oriH * sizeF / oriW), size)
  | otherwise    = (sizeF / oriH, size, floor (oriW * sizeF / oriH))
    where
        oriW = fromIntegral $ img ^. img_width
        oriH = fromIntegral $ img ^. img_height
        sizeF = fromIntegral size


loadImage :: (MonadReader env m, HasDatasetConfig env, DatasetTag env ~ "coco", MonadIO m)
    => Image -> m (ImageTensor, ImageInfo)
loadImage img = do
    Coco base datasplit _ _ <- view (datasetConfig . conf_coco)
    width <- view (datasetConfig . conf_width)

    let imgFilePath = base </> datasplit </> img ^. img_file_name
    imgRGB <- liftIO $ raiseLeft (FileNotFound imgFilePath) $ HIP.readImage imgFilePath

    let (scale, imgH, imgW) = getImageScale img width
        imgInfo = fromListUnboxed (Z :. 3) [fromIntegral imgH, fromIntegral imgW, scale]

        imgResized = HIP.resize HIP.Bilinear HIP.Edge (imgH, imgW) (imgRGB :: HIP.Image HIP.VS HIP.RGB Double)
        imgPadded  = HIP.canvasSize (HIP.Fill $ HIP.PixelRGB 0.5 0.5 0.5) (width, width) imgResized
        imgRepa    = Repa.fromUnboxed (Z:.width:.width:.3) $
                        SV.convert $
                        SV.unsafeCast $
                        HIP.toVector imgPadded

    imgEval <- transform $ Repa.map double2Float imgRepa
    return (Repa.computeUnboxedS imgEval, imgInfo)


loadBoundingBoxes :: (MonadReader env m, HasDatasetConfig env, DatasetTag env ~ "coco", MonadIO m)
    => Image -> m (Maybe GTBoxes)
loadBoundingBoxes img = do
    Coco _ _ inst cat_to_classid <- view (datasetConfig . conf_coco)
    size <- view (datasetConfig . conf_width)

    let imgAnns = V.filter (\ann -> ann ^. ann_image_id == imageId) (inst ^. annotations)
        (scale, _, _) = getImageScale img size
        gt_boxes = V.fromList $ catMaybes $ map (makeGTBox cat_to_classid scale) $ V.toList imgAnns

    return $ if V.null gt_boxes then Nothing else Just gt_boxes
    where
        imageId = img ^. img_id
        width   = img ^. img_width
        height  = img ^. img_height
        makeGTBox cat_to_classid scale ann =
            let (x0, y0, x1, y1) = cleanBBox (ann ^. ann_bbox)
                classId = cat_to_classid ^?! ix (ann ^. ann_category_id)
            in
            if ann ^. ann_area > 0 && x1 > x0 && y1 > y0
                then Just $ fromListUnboxed (Z :. 5) [x0*scale, y0*scale, x1*scale, y1*scale, fromIntegral classId]
                else Nothing
        cleanBBox (x, y, w, h) =
            let x0 = max 0 x
                y0 = max 0 y
                x1 = min (fromIntegral width - 1)  (x0 + max 0 (w-1))
                y1 = min (fromIntegral height - 1) (y0 + max 0 (h-1))
             in (x0, y0, x1, y1)


loadMasks :: (MonadReader env m, HasDatasetConfig env, DatasetTag env ~ "coco", MonadIO m)
          => Image -> m (Maybe Masks)
loadMasks img = do
    Coco _ _ inst _ <- view (datasetConfig . conf_coco)
    size <- view (datasetConfig . conf_width)

    let imgAnns = V.filter (\ann -> ann ^. ann_image_id == imageId) (inst ^. annotations)
        (_, imgH, imgW) = getImageScale img size
    masks <- V.mapM (getMask imgH imgW size) imgAnns
    return $ if V.null masks then Nothing else Just masks
    where
        imageId = img ^. img_id
        width   = img ^. img_width
        height  = img ^. img_height
        getMask upH upW size anno = liftIO $ do
            crle <- case anno ^. ann_segmentation of
                      SegRLE cnts _    -> frUncompressedRLE cnts height width
                      SegPolygon (RNE.nonEmpty -> Just polys) ->
                          frPoly (RNE.map SV.fromList polys) height width
                      _ -> throwString "Cannot build CRLE"
            crle <- merge crle False
            mask <- decode crle
            let -- always single channel, since we have merged the masks
                -- also note that HIP uses image HxW
                image      = HIP.fromRepaArrayS $
                                Repa.transpose $
                                Repa.map (HIP.PixelY . (*255)) $
                                Repa.reshape (Z :. height :. width) mask
                imgResized = HIP.resize HIP.Bilinear HIP.Edge
                                (upH, upW)
                                image
                imgPadded  = HIP.canvasSize (HIP.Fill $ HIP.PixelY 0)
                                (size, size)
                                imgResized
            return $ Repa.computeUnboxedS $ Repa.map (\(HIP.PixelY e) -> e) $ HIP.toRepaArray imgPadded