Exemple #1
0
 def export_croplets(self, anno_index, dir_out, width=256):
     """Save croplets to disk"""
     fiox.ensure_dir(dir_out)
     anno_index_croplets = self.append_croplets(anno_index, width=width)
     for class_idx, anno_obj in anno_index_croplets.items():
         class_label_index = anno_obj['class_index']
         dir_class_idx = os.path.join(dir_out, str(class_label_index))
         fiox.ensure_dir(dir_class_idx)
         for region in anno_obj['regions']:
             try:
                 im = region['croplet']
                 anno_id = region['id']
                 fp_croplet = os.path.join(dir_class_idx,
                                           '{}.jpg'.format(anno_id))
                 cv.imwrite(fp_croplet, im)
             except:
                 print('[-] Error. No croplet.')
                 return False
     return True
Exemple #2
0
def main(args):

    if os.path.isdir(args.input):
        from glob import glob
        files = glob(join(args.input, '*.mp4'))
    else:
        files = [args.input]

    if len(files) == 0:
        print('No files in "{}"'.format(args.input))
        sys.exit()

    # create directories where needed
    if args.save_debug:
        dir_debug = join(args.output, 'debug')
        fiox.ensure_dir(dir_debug)

    if not args.no_save:
        fiox.ensure_dir(args.output)

    if not args.no_save_log:
        # write to log file
        dir_log = join(args.output, 'log')
        fiox.ensure_dir(dir_log)
        f_logfile = join(dir_log, 'log.csv')
        # start file
        with open(f_logfile, 'w') as fp:
            fp.write('# filename, fps, frames, duplicates, logos_found\n')

    for f in files:
        print('Process {}'.format(os.path.basename(f)))
        if os.path.isfile(f):
            extract_logo(args, f)
Exemple #3
0
def convert_vid2sceneframes(args, fpath):

    bname = os.path.basename(fpath)
    name, ext = os.path.splitext(bname)
    if args.littlefork:
        save_name = os.path.basename(os.path.dirname(fpath))
    else:
        save_name = bname

    # reset frames, max read is about 200FPS
    fps, nframes, frameset = imx.vid2frames(fpath, width=args.width)
    if nframes == 0:
        print('fps: {}, nframes: {}, len frames: {}'.format(
            fps, nframes, len(frameset)))
        print('[-] ERROR could not read movie {}'.format(bname))
        return
    print('[+] fps: {}, frames: {}'.format(fps, nframes))

    # Create Focal Mask
    # add active zones: horizontal and vertical
    try:
        h, w = frameset[0].shape[:2]
    except:
        print('[-] Error. No frames available for {}. Skipping video.'.format(
            fpath))
    focal_zone_dim = (0.33, 0.33)
    x1 = int(((.5 - (focal_zone_dim[0] / 2)) * w))
    x2 = int(((.5 + (focal_zone_dim[0] / 2)) * w))
    y1 = int(((.5 - (focal_zone_dim[1] / 2)) * h))
    y2 = int(((.5 + (focal_zone_dim[1] / 2)) * h))
    focal_zone_col = (x1, 0, x2, h)
    focal_zone_row = (0, y1, w, y2)

    # add active zone: center rect
    focal_zone_center_dim = (0.5, 0.5)
    x1 = int(((.5 - (focal_zone_center_dim[0] / 2)) * w))
    x2 = int(((.5 + (focal_zone_center_dim[0] / 2)) * w))
    y1 = int(((.5 - (focal_zone_center_dim[1] / 2)) * h))
    y2 = int(((.5 + (focal_zone_center_dim[1] / 2)) * h))
    focal_zone_center = (x1, y1, x2, y2)

    focal_regions = [focal_zone_col, focal_zone_row, focal_zone_center]

    im_focal_mask = np.zeros_like(imx.bgr2gray(frameset[0])).astype(np.bool)
    for x1, y1, x2, y2 in focal_regions:
        im_focal_mask[y1:y2, x1:x2] = 1

    num_total_pixels = w * h
    num_focal_pixels = len(np.where(im_focal_mask > 0)[0])
    focal_mask_per = num_focal_pixels / (w * h)

    # # Use Canny Edges to find empty frames

    # mask focal region of canny images
    ims_canny_orig = [
        cv.Canny(f, args.canny_min, args.canny_max) for f in frameset
    ]
    ims_canny = [
        np.logical_and(im.astype(np.bool), im_focal_mask)
        for im in ims_canny_orig
    ]  # focal mask

    # Compute sum of edge information in focal zone in canny images
    sum_thresh_per = 1.  # max percentage of non-normalized summed edge pixels
    sum_thresh_pixels = sum_thresh_per / 100. * num_focal_pixels  # num pixels

    ims_canny_sums = np.array([(len(np.where(im > 0)[0])) for im in ims_canny])
    canny_max = np.max(ims_canny_sums)
    ims_canny_sums_norm = [v / canny_max for v in ims_canny_sums]
    ims_canny_flags = np.array([v > sum_thresh_pixels for v in ims_canny_sums])

    # Visualize the frames that will be ignored
    ims_canny_keep_idxs = np.where(ims_canny_flags == 1)[0]
    ims_canny_ignore_idxs = np.where(ims_canny_flags == 0)[0]

    print('[+] Keeping frames more than {} edge pixels ({}%)'.format(
        int(sum_thresh_per / 100. * num_focal_pixels), sum_thresh_per))
    print("[+] Keep: {}, Ignore: {}".format(len(ims_canny_keep_idxs),
                                            len(ims_canny_ignore_idxs)))

    # Use Pixel Mean to find empty frames
    # use focal mask to average only focal pixels
    im_focal_mask_uint8 = im_focal_mask.astype(np.uint8)
    mean_adj = num_total_pixels / num_focal_pixels
    ims_mean = np.array([
        mean_adj *
        np.mean(imx.bgr2gray(cv.bitwise_and(im, im, mask=im_focal_mask_uint8)))
        for im in frameset
    ])
    #ims_mean = np.array([mean_adj*imx.bgr2gray(cv.bitwise_and(im,im,mask=im_focal_mask_uint8)) for im in frameset])
    max_mean = np.max(ims_mean)
    ims_mean_norm = ims_mean / max_mean
    ims_mean_flags = np.array([v > args.mean_thresh
                               for v in ims_mean])  # Keep 1-flag frames

    # # Combine Edge/Mean Vals to Filter Empty Frames
    # Mark frames as 1 to keep, 0 to ignore
    # use logical OR on inverted logic to keep, then de-invert to keep 1(keep), 0(skip) structure
    ims_flags_comb = np.invert(
        np.logical_or(np.invert(ims_canny_flags), np.invert(ims_mean_flags)))
    print('[+] Combined mean + edge frames to skip: {}'.format(
        len(np.where(ims_flags_comb == 0)[0])))

    # gather the frames
    # Visualize the frames that will be ignored
    ims_keep_idxs = np.where(ims_flags_comb == 1)[0]
    ims_ignore_idxs = np.where(ims_flags_comb == 0)[0]

    # Load frames
    frames_ignore = [frameset[i] for i in ims_ignore_idxs]
    frames_keep = [frameset[i] for i in ims_keep_idxs]

    # TODO, inspect src --> dst error
    print('[+] Computing feature vectors...(ignoring warnings)')
    vals_phash = [imx.compute_phash(f) for f in frameset]

    #vals_feats = imx.compute_img2vec_feats(frameset,vals_phash,phash_thresh=1)

    # Paramerterize
    fe = feature_extractor_pytorch.FeatureExtractor(cuda=True, net='alexnet')
    vals_feats = imx.compute_features(fe, frameset, vals_phash, phash_thresh=2)

    scenes = []
    scene_idxs = []
    start_at = 0
    end_at = len(frameset)
    last_scene_idx = 0

    for idx in range(start_at, end_at):
        if ims_flags_comb[idx] == False:
            continue

        feat_delta = imx.cosine_delta(vals_feats[idx],
                                      vals_feats[last_scene_idx])
        phash_delta = (vals_phash[idx] - vals_phash[last_scene_idx])

        if feat_delta > args.thresh_feat_a or phash_delta > args.thresh_phash_a:
            last_scene_idx = idx
            scenes.append(scene_idxs)
            scene_idxs = []

        scene_idxs.append(idx)

    scenes.append(scene_idxs)

    # reduce scenes by removing transition frames
    min_scene_frames = int(fps * args.min_scene_duration)
    scenes_tmp = scenes.copy()
    scenes = [s for s in scenes_tmp if len(s) > min_scene_frames]

    scene_rep_idxs = []
    for scene in scenes:
        scene_edge_sum = ims_canny_sums[scene[0]:]
        # ensure there are
        best_idx = np.argmax(scene_edge_sum) + scene[0]
        scene_rep_idxs.append(best_idx)

    print("[+] Feature based scenes: {}, best: {}".format(
        len(scenes), len(scene_rep_idxs)))

    # Get final scene indic
    scene_deduped_idxs = imx.dedupe_idxs(scene_rep_idxs,
                                         phashes=vals_phash,
                                         feats=vals_feats,
                                         feat_thresh=args.thresh_feat_b,
                                         phash_thresh=args.thresh_phash_b)

    # if too few, decrease thresholding
    nscene_frames = len(scene_deduped_idxs)
    print('[+] {} scenes with default thresh'.format(nscene_frames))

    if nscene_frames > 0 and nscene_frames < args.min_frames:
        print('[+] dec thresh')
        scene_deduped_idxs = imx.dedupe_idxs(
            scene_rep_idxs,
            phashes=vals_phash,
            feats=vals_feats,
            feat_thresh=args.thresh_feat_b / 2,
            phash_thresh=args.thresh_phash_b / 2)
    # if too few, increase thresholding
    elif nscene_frames > args.max_frames:
        print('[+] too many frames, increasing threshold')
        scene_deduped_idxs = imx.dedupe_idxs(
            scene_rep_idxs,
            phashes=vals_phash,
            feats=vals_feats,
            feat_thresh=args.thresh_feat_b * 1.25,
            phash_thresh=args.thresh_phash_b * 1.25)

    # Create dense keyframes representation with KMeans clusters
    if len(scene_deduped_idxs) < args.max_dense_frames:
        scene_frames_dense_idxs = scene_deduped_idxs.copy()
    else:
        dense_max_frames = min(args.max_dense_frames, len(scene_deduped_idxs))
        n_pca_components = min(12, len(scene_deduped_idxs))

        vec_length = len(vals_feats[0])
        vec_mat = np.zeros((len(scene_deduped_idxs), vec_length))

        for i in range(len(scene_deduped_idxs)):
            idx = scene_deduped_idxs[i]
            vec = vals_feats[idx]
            vec_mat[i, :] = vec

        print('[+] using {} PCA components'.format(n_pca_components))

        reduced_data = PCA(
            n_components=n_pca_components).fit_transform(vec_mat)
        kmeans = KMeans(init='k-means++',
                        n_clusters=dense_max_frames,
                        n_init=10)
        kmeans.fit(reduced_data)
        preds = kmeans.predict(reduced_data)

        scene_frames_dense_idxs = []
        clusters_used = []

        for i in range(len(scene_deduped_idxs)):
            idx = scene_deduped_idxs[i]
            if not preds[i] in clusters_used:
                clusters_used.append(preds[i])
                scene_frames_dense_idxs.append(idx)

    # reload video
    cap = cv.VideoCapture(fpath)
    print('[+] {}'.format(fpath))

    # Create list of poster frames (resized)
    # set capture object to frame index - 1 (read next frame)
    scene_frames_poster = []
    for idx in scene_deduped_idxs:
        cap.set(cv.CAP_PROP_POS_FRAMES, idx - 1)
        res, frame = cap.read()
        scene_frames_poster.append(
            imutils.resize(frame, width=args.poster_im_width))

    # Create list of full resolution frames
    scene_frames_full = []
    for idx in scene_deduped_idxs:
        cap.set(cv.CAP_PROP_POS_FRAMES, idx - 1)
        res, frame = cap.read()
        scene_frames_full.append(frame)

    scene_frames_poster_dense = []
    for idx in scene_frames_dense_idxs:
        cap.set(cv.CAP_PROP_POS_FRAMES, idx - 1)
        res, frame = cap.read()
        scene_frames_poster_dense.append(
            imutils.resize(frame, width=args.poster_im_width))

    scene_frames_dense = []
    for idx in scene_frames_dense_idxs:
        cap.set(cv.CAP_PROP_POS_FRAMES, idx - 1)
        res, frame = cap.read()
        scene_frames_dense.append(frame)

    # pad with empty frames
    if len(scene_frames_poster_dense) > 0:
        while len(scene_frames_poster_dense) < args.max_dense_frames:
            scene_frames_poster_dense.append(
                np.zeros_like(scene_frames_poster_dense[0]))

    #scene_frames_all = [frameset[idx] for idx in scene_rep_idxs]

    n = len(scene_rep_idxs) - len(scene_deduped_idxs)
    print('[+] {} scene keyframes removed from de-duplication'.format(n))
    print('[+] {} final scene frames'.format(len(scene_deduped_idxs)))

    if len(scene_deduped_idxs) > 0:
        # save the montage of all keyframes
        im_summary = imx.ims2montage(scene_frames_poster, ncols=args.num_cols)
        #fname = os.path.splitext(os.path.basename(f))[0]
        dir_poster = join(args.output, 'posters')
        fiox.ensure_dir(dir_poster)
        fp_im = join(dir_poster, '{}.jpg'.format(save_name))
        cv.imwrite(fp_im, im_summary)

        # save the montage of dense keyframes
        im_summary = imx.ims2montage(scene_frames_poster_dense,
                                     ncols=args.num_cols_dense)
        #fname = os.path.splitext(os.path.basename(f))[0]
        dir_poster_dense = join(args.output, 'posters_dense')
        fiox.ensure_dir(dir_poster_dense)
        fp_im = join(dir_poster_dense, '{}.jpg'.format(save_name))
        cv.imwrite(fp_im, im_summary)

        # output all video scene keyframes
        dir_frames_png = join(args.output, 'frames_png', save_name)
        dir_frames_jpg = join(args.output, 'frames_jpg', save_name)
        fiox.ensure_dir(dir_frames_png)
        fiox.ensure_dir(dir_frames_jpg)
        for idx, im in zip(scene_deduped_idxs, scene_frames_full):
            fp_im_png = join(dir_frames_png, '{:06d}.png'.format(idx))
            fp_im_jpg = join(dir_frames_jpg, '{:06d}.jpg'.format(idx))
            cv.imwrite(fp_im_png, im)
            cv.imwrite(fp_im_jpg, im)

        # write all dense keyframes for web
        dir_dense_frames_jpg = join(args.output, 'frames_dense_jpg', save_name)
        fiox.ensure_dir(dir_dense_frames_jpg)
        for idx, im in zip(scene_frames_dense_idxs, scene_frames_dense):
            fp_im_jpg = join(dir_dense_frames_jpg, '{:06d}.jpg'.format(idx))
            cv.imwrite(fp_im_jpg, im)

    else:
        print('[+] no scenes found')
Exemple #4
0
    def create_project(self, kwargs):

        vcat_utils = VcatUtils()

        # load vcat JSON
        vcat_data = json.load(kwargs['input'])

        # hierarchy is
        hierarchy = vcat_data['hierarchy']

        # if exclusions, remove classes
        if kwargs['exclude'] is not None:
            exclusions = [int(i) for i in kwargs['exclude'].split(',')]
            print('[+] Excluding: {}'.format(exclusions))
            # remove from hierarchy
            hierarchy_tmp = hierarchy.copy()
            for obj_class_id, obj_class_meta in hierarchy_tmp.items():
                if int(obj_class_id) in exclusions:
                    print('[+] Removing ID: {} ({})'.format(
                        obj_class_id, obj_class_meta['slug']))
                    del hierarchy[obj_class_id]

        # class label index lookup (YOLO  ID --> VCAT ID)
        class_label_index_lkup = vcat_utils.create_class_label_index(
            vcat_data, kwargs)

        print('----Classes----')
        for k, v in class_label_index_lkup.items():
            print('VCAT: {} --> YOLO: {}, Label: {}'.format(
                k, v, hierarchy[str(k)]['slug']))

        # create object class lookup
        anno_index = vcat_utils.create_annotation_index(
            vcat_data, class_label_index_lkup, kwargs)

        for a in anno_index:
            print('Annotation index: {}'.format(a))

        sys.exit()

        # inflate annotation index to include parent class (ghost) annotations
        # TODO

        # create image index lookup
        image_anno_index = vcat_utils.create_image_anno_index(anno_index)

        # Create randomize, class-based splits for train and valid
        anno_splits = vcat_utils.create_splits(anno_index,
                                               split_val=kwargs['split_val'])
        annos_train = anno_splits['train']
        annos_valid = anno_splits['valid']

        # Statistics
        n_regions_train = np.sum(
            [len(v['regions']) for k, v in annos_train.items()])
        n_regions_test = np.sum(
            [len(v['regions']) for k, v in annos_valid.items()])
        print('[+] Regions train: {}, Test: {}'.format(n_regions_train,
                                                       n_regions_test))

        print('----Classes----')
        for k, v in class_label_index_lkup.items():
            print('VCAT: {} --> YOLO: {}, Label: {}'.format(
                k, v, hierarchy[str(k)]['slug']))

        print('----Train----')
        for k, v in annos_train.items():
            print('{}\t{}'.format(len(v['regions']), v['slug_vcat']))

        print('----Validate----')
        for k, v in annos_valid.items():
            print('{}\t{}'.format(len(v['regions']), v['slug_vcat']))

        print('----Labels----')
        for k, v in class_label_index_lkup.items():
            print('{}\t{}'.format(k, hierarchy[str(k)]['slug']))

        sys.exit()

        # ---------------------------------------------------
        # Filenames and paths
        # ---------------------------------------------------

        # convenience vars
        n_classes = len(anno_index)
        yolov = str(kwargs['yolo_version'])
        project_name = kwargs['name']
        dir_output = kwargs['dir_output']
        dir_project = os.path.join(dir_output, project_name)

        # create project_name directory
        fiox.ensure_dir(dir_project)

        # create images directory
        dir_project_images = os.path.join(dir_project, 'images')
        fiox.ensure_dir(dir_project_images)

        # Create dirs if not exist
        dir_labels = os.path.join(dir_project, 'labels')
        fiox.ensure_dir(dir_labels)

        # create dir for weights/backup
        dir_weights = os.path.join(dir_project, 'weights')
        fiox.ensure_dir(dir_weights)

        # ---------------------------------------------------
        # Create "classes.txt"
        # ---------------------------------------------------

        fp_classes = os.path.join(dir_project, 'classes.txt')
        class_labels = []
        for k, v in class_label_index_lkup.items():
            # maps vcat id to yolo id
            slug = hierarchy[str(k)]['slug']
            slug_sp = slug.split(':')
            if len(slug_sp) > 1:
                display_name = '{} ({})'.format(slug_sp[0], slug_sp[1])
            else:
                display_name = slug_sp[0]

            class_labels.append(display_name)

        with open(fp_classes, 'w') as fp:
            fp.write('\n'.join(class_labels))

        # ---------------------------------------------------
        # Create "train.txt", "valid.txt", "classes.txt"
        # ---------------------------------------------------

        fp_train = os.path.join(dir_project, 'train.txt')
        im_list = self.generate_image_list(annos_train, dir_project_images)
        with open(fp_train, 'w') as fp:
            fp.write('\n'.join(im_list))

        fp_valid = os.path.join(dir_project, 'valid.txt')
        im_list = self.generate_image_list(annos_valid, dir_project_images)
        with open(fp_valid, 'w') as fp:
            fp.write('\n'.join(im_list))

        # ---------------------------------------------------
        # Crate sym-linked image files
        # ---------------------------------------------------

        # TODO remove folder if exists

        self.create_image_symlinks(image_anno_index, dir_project_images)

        # ---------------------------------------------------
        # Create "labels.txt"
        # ---------------------------------------------------

        # labels.txt contains one line per annotation
        # "1 0.434 0.605 0.2980 0.37222"
        # class_idx, cx, cy, w, h
        # for YoloV3 add parent class hierarchy to the label

        label_anno_index = self.create_label_anno_index(image_anno_index)

        for fname, anno_obj in label_anno_index.items():
            fp_txt = os.path.join(dir_labels, '{}.txt'.format(fname))
            with open(fp_txt, 'w') as fp:
                fp.write('\n'.join(anno_obj))

        # ---------------------------------------------------
        # Create "meta.data"
        # ---------------------------------------------------

        txts = []
        txts.append('classes = {}'.format(n_classes))
        txts.append('train = {}'.format(fp_train))
        txts.append('valid = {}'.format(fp_valid))
        txts.append('names = {}'.format(fp_classes))
        txts.append('backup = {}'.format(dir_weights))

        fp_meta = os.path.join(dir_project, 'meta.data')
        with open(fp_meta, 'w') as fp:
            fp.write('\n'.join(txts))

        # ---------------------------------------------------
        # create train and test .cfg files
        # ---------------------------------------------------

        cfg_train_data = self.generate_yolo_config(
            yolov,
            n_classes,
            'train',
            n_subdiv=kwargs['subdivisions'],
            n_batch=kwargs['batch_size'])
        cfg_test_data = self.generate_yolo_config(yolov, n_classes, 'test')

        # write data to new .cfg files
        fp_cfg_train = os.path.join(dir_project, 'yolov{}.cfg'.format(yolov))
        fp_cfg_test = os.path.join(dir_project,
                                   'yolov{}_test.cfg'.format(yolov))

        with open(fp_cfg_train, 'w') as fp:
            fp.write('\n'.join(cfg_train_data))

        with open(fp_cfg_test, 'w') as fp:
            fp.write('\n'.join(cfg_test_data))

        # ---------------------------------------------------
        # Create shell scripts to start and resume training
        # ---------------------------------------------------

        txts_base = []
        txts_base.append('#!/bin/bash')
        txts_base.append('DARKNET=/opt/darknet_pjreddie/darknet')
        txts_base.append('DIR_PROJECT={}'.format(dir_project))
        txts_base.append('FP_CFG={}'.format(fp_cfg_train))
        txts_base.append('FP_META={}'.format(fp_meta))
        txts_base.append('CMD="detector train"')
        txts_base.append('LOGFILE="2>&1 | tee train_log.txt"')

        # run_train_init.sh
        fp_sh_train_init = os.path.join(dir_project, 'run_train_init.sh')

        txts_init = txts_base.copy()
        txts_init.append('GPUS="-gpus {}"'.format(kwargs['gpu_init']))
        txts_init.append('FP_WEIGHTS={}'.format(kwargs['init_weights']))
        txts_init.append('$DARKNET $CMD $FP_META $FP_CFG $FP_WEIGHTS $GPUS')

        with open(fp_sh_train_init, 'w') as fp:
            fp.write('\n'.join(txts_init))

        # run_train_resume.sh
        fp_sh_train_resume = os.path.join(dir_project, 'run_train_resume.sh')

        txts_resume = txts_base.copy()
        txts_resume.append('GPUS="-gpus {}"'.format(kwargs['gpu_full']))
        txts_resume.append('FP_WEIGHTS={}/{}.backup'.format(
            dir_weights, project_name))
        txts_resume.append('$DARKNET $CMD $FP_META $FP_CFG $FP_WEIGHTS $GPUS')

        with open(fp_sh_train_resume, 'w') as fp:
            fp.write('\n'.join(txts_resume))

        # run_test.sh
        fp_sh_test_image = os.path.join(dir_project, 'run_test_image.sh')

        txts = []
        txts.append('#!/bin/bash')
        txts.append('DIR_DARKNET=/opt/darknet_pjreddie/')
        txts.append('DIR_PROJECT={}'.format(dir_project))
        txts.append('FP_CFG={}'.format(fp_cfg_test))
        txts.append('FP_META={}'.format(fp_meta))
        rn_filepath = self.get_random_image(annos_valid)
        txts.append('FP_IMG={}'.format(rn_filepath))
        txts.append('FP_WEIGHTS={}/{}_900.weights'.format(
            dir_weights, project_name))
        txts.append('#FP_WEIGHTS={}/{}_10000.weights'.format(
            dir_weights, project_name))
        txts.append('#FP_WEIGHTS={}/{}_30000.weights'.format(
            dir_weights, project_name))
        txts.append('CMD="detector test"')
        txts.append('cd $DIR_DARKNET')
        txts.append('./darknet $CMD $FP_META $FP_CFG $FP_WEIGHTS $FP_IMG')

        with open(fp_sh_test_image, 'w') as fp:
            fp.write('\n'.join(txts))

        print('[+] Wrote Darknet files for {}'.format(project_name))
        print('[+] Project path: {}'.format(dir_project))
Exemple #5
0
    def extract_keyframes(self, fp_src_video, scenes, sha256, kwargs):
        """Write scene indices to disk"""

        # this should be multithreaded
        cap = cv.VideoCapture(fp_src_video)
        sha256_path = fiox.sha256_tree(sha256)
        fp_dst_keyframes = join(kwargs['output'], sha256_path, sha256)
        fiox.ensure_dir(fp_dst_keyframes)

        # load video
        cap = cv.VideoCapture(fp_src_video)  # this should be multithreaded
        sha256_tree = fiox.sha256_tree(sha256)

        # provision vars
        montage_im_width = kwargs['montage_image_width']
        num_zeros = cfg.FRAME_NAME_ZERO_PADDING
        im_sizes = cfg.WEB_IMAGE_SIZES

        # ---------------------------------------------
        # Copy keyframes from video
        # ---------------------------------------------

        # expanded contains all frames
        frameset = {}
        for idx in scenes['expanded']:
            cap.set(cv.CAP_PROP_POS_FRAMES, idx - 1)
            res, frame = cap.read()
            frames_expanded.append(frame)

        # pad dense summary to maintain consistent row x col layout
        if len(frames_poster_dense) > 0:
            while len(frames_poster_dense) < kwargs['max_dense_frames']:
                frames_poster_dense.append(
                    np.zeros_like(frames_poster_dense[0]))

        #output_keyframes = join(kwargs['output'],'keyframes',save_name)
        #output_keyframes_training = join(kwargs['output'],'keyframes_training',save_name)
        dir_frames = join(kwargs['output'], 'frames', sha256_path, sha256)
        dir_posters_default = join(kwargs['output'], 'posters/default',
                                   sha256_path, sha256)
        dir_posters_dense = join(kwargs['output'], 'posters/dense',
                                 sha256_path, sha256)
        dir_posters_expanded = join(kwargs['output'], 'posters/expanded',
                                    sha256_path, sha256)

        # save all exapnded frames to frames at raw size
        for idx, frame in zip(scenes['expanded'], frames_expanded):
            fp_im = join(dir_frames, '{:07d}'.format(idx), 'index.png')
            fiox.ensure_dir(fp_im, parent=True)
            cv.imwrite(fp_im, frame)

        if len(scenes['basic']) > 0:
            # save the montage of all keyframes
            im_summary = imx.montage(frames_poster, ncols=kwargs['num_cols'])
            dir_poster = join(kwargs['output'], 'posters')
            fiox.ensure_dir(dir_poster)

            fp_im = join(dir_poster, 'index.jpg')
            cv.imwrite(fp_im, im_summary)

            # save the montage of dense keyframes
            im_summary = imx.montage(frames_poster_dense,
                                     ncols=kwargs['num_cols_dense'])
            dir_poster_dense = join(kwargs['output'], 'posters_dense')
            fiox.ensure_dir(dir_poster_dense)
            fp_im = join(dir_poster_dense, 'index.jpg')
            cv.imwrite(fp_im, im_summary)

            # write full size local files for training
            fiox.ensure_dir(dir_frames)
            frameset[idx] = frame

        if kwargs['scene_type']:
            scene_types = [kwargs['scene_type']]
        else:
            scene_types = ['basic', 'expanded', 'dense']

        for scene_type in scene_types:
            frames = []

            # choose scenes to write
            scene_type = 'expanded' if kwargs['verified'] else 'basic'
            for idx, im in zip(scenes[scene_type], frames_full):
                fp_im = join(dir_frames, '{:06d}.png'.format(idx))
                cv.imwrite(fp_im, im)

                for size_dir, width in jpg_sizes.items():
                    d = join(output_keyframes, size_dir)
                    im_sized = imutils.resize(im, width=width)
                    fp_im = join(output_keyframes, size_dir,
                                 '{:06d}.jpg'.format(idx))
                    cv.imwrite(fp_im, im_sized)

            # load frames
            for idx in scenes[scene_type]:
                frames.append(frameset[idx])

            # create montages
            if kwargs['output_montages'] is not None:
                dp_montage = join(kwargs['output_montages'], scene_type,
                                  sha256_tree, sha256)
                Path(dp_montage).mkdir(parents=True, exist_ok=True)
                ncols, nrows = kwargs['montage_size']
                im_montage = imx.montage(frames,
                                         nrows=nrows,
                                         ncols=ncols,
                                         width=montage_im_width)
                fp_im = join(dp_montage, 'index.jpg')
                cv.imwrite(fp_im, im_montage)

            # path to web images JPG
            dp_web = join(kwargs['output_web'], sha256_tree, sha256)
            Path(dp_web).mkdir(parents=True, exist_ok=True)

            # path to training PNG frames
            if kwargs['output_training'] is not None:
                dp_train = join(kwargs['output_training'], sha256_tree, sha256)
                Path(dp_train).mkdir(parents=True, exist_ok=True)

            for idx, im in zip(scenes[scene_type], frames):
                idx_zpad = str(idx).zfill(num_zeros)

                # save training images (PNG)
                if kwargs['output_training'] is not None:
                    dp = join(dp_train, idx_zpad)
                    Path(dp).mkdir(parents=True, exist_ok=True)
                    fp = join(dp_train, idx_zpad, 'index.png')
                    cv.imwrite(fp, im)

                # generate directories
                for label, width in im_sizes.items():
                    dp = join(dp_web, idx_zpad, label)
                    Path(dp).mkdir(parents=True, exist_ok=True)

                # generate images
                for label, width in im_sizes.items():
                    fp = join(dp_web, idx_zpad, label, 'index.jpg')
                    im_resized = imutils.resize(im, width=width)
                    cv.imwrite(fp, im_resized)

        return True