Skip to content

Commit

Permalink
Merge pull request #151 from Appsilon/kamil.set-thumbnail-size
Browse files Browse the repository at this point in the history
Make media tools configurable
  • Loading branch information
kamilzyla authored Feb 17, 2021
2 parents c98c8c8 + 2bb4aee commit 24a347c
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 50 deletions.
18 changes: 14 additions & 4 deletions app/assets/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@
},
"tools": {
"title": "Media tools",
"mode": "Select mode of operation:",
"extractFramesDetail": "Extract frames from videos at 5 second intervals",
"mode": "Mode of operation",
"extractFramesDetail": "Extract frames from videos at regular intervals",
"createThumbnailsDetail": "Create a thumbnail for each image / video",
"extractionInterval": "Interval between frames in seconds",
"thumbnailSize": "Maximum thumbnail width/height in pixels",
"thumbnailSizeSmall": "Small",
"thumbnailSizeMedium": "Medium",
"thumbnailSizeLarge": "Large",
"chooseInput": "Choose input directory",
"chooseOutput": "Choose output directory",
"extractFrames": "Extract frames",
Expand Down Expand Up @@ -120,9 +125,14 @@
},
"tools": {
"title": "Outils médias",
"mode": "Sélectionnez le mode de fonctionnement :",
"extractFramesDetail": "Extraire les images des vidéos à 5 secondes d'intervalle",
"mode": "Mode de fonctionnement",
"extractFramesDetail": "Extraire les images des vidéos à intervalles réguliers",
"createThumbnailsDetail": "Créer une vignette pour chaque image / vidéo",
"extractionInterval": "Intervalle entre les images en secondes",
"thumbnailSize": "Largeur/hauteur maximale des vignettes en pixels",
"thumbnailSizeSmall": "Petit",
"thumbnailSizeMedium": "Moyen",
"thumbnailSizeLarge": "Grande",
"chooseInput": "Choisissez le dossier d'entrée",
"chooseOutput": "Choisissez les dossiers de sortie",
"extractFrames": "Extraire les cadres d'image",
Expand Down
113 changes: 81 additions & 32 deletions app/containers/MediaToolsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
Toaster,
Callout,
RadioGroup,
Radio
Radio,
Slider,
FormGroup
} from '@blueprintjs/core';
import { useTranslation, Trans } from 'react-i18next';
import path from 'path';
Expand Down Expand Up @@ -61,10 +63,18 @@ function runExtractProcess(
return spawn(program, args, { cwd: workdir });
}

type ToolMode = 'EXTRACT_FRAMES' | 'CREATE_THUMBNAILS';

type MediaToolOptions = {
inputDir: string;
outputDir: string;
toolMode: ToolMode;
frameInterval?: number;
maxThumbnailDimensions?: number;
};

const extractImages = (
inputPath: string,
outputPath: string,
thumbnailMode: boolean,
options: MediaToolOptions,
changeLogMessage: (message: string) => void,
setIsRunning: (isRunning: boolean) => void,
setExitCode: (exitCode: number | null | undefined) => void,
Expand All @@ -73,12 +83,19 @@ const extractImages = (
const args: string[] = [
'extract_images',
'--input_folder',
inputPath,
options.inputDir,
'--output_folder',
outputPath
options.outputDir
];
if (thumbnailMode) {
if (options.toolMode === 'EXTRACT_FRAMES') {
if (options.frameInterval !== undefined) {
args.push('--frame_interval', options.frameInterval.toFixed());
}
} else if (options.toolMode === 'CREATE_THUMBNAILS') {
args.push('--thumbnails');
if (options.maxThumbnailDimensions !== undefined) {
args.push('--max_thumbnail_dimensions', options.maxThumbnailDimensions.toFixed());
}
}
const process = runExtractProcess(args, t);
if (process !== null) {
Expand Down Expand Up @@ -121,13 +138,18 @@ const chooseDirectory = (changeDirectoryChoice: changePathChoiceType) => {
});
};

const EXTRACT_FRAMES = 'EXTRACT_FRAMES';
const CREATE_THUMBNAILS = 'CREATE_THUMBNAILS';
const THUMBNAIL_SIZE_LABELS = new Map<number, string>([
[200, 'tools.thumbnailSizeSmall'],
[500, 'tools.thumbnailSizeMedium'],
[800, 'tools.thumbnailSizeLarge']
]);

export default function MediaToolsPage() {
const { t } = useTranslation();

const [currentMode, setCurrentMode] = useState<string>(EXTRACT_FRAMES);
const [toolMode, setToolMode] = useState<ToolMode>('EXTRACT_FRAMES');
const [thumbnailSize, setThumbnailSize] = useState<number>(350);
const [extractionInterval, setExtractionInterval] = useState<number>(5);
const [inputDir, setInputDir] = useState<string>('');
const [outputDir, setOutputDir] = useState<string>('');

Expand All @@ -141,18 +163,59 @@ export default function MediaToolsPage() {

const rootModelsDirectoryExists = fs.existsSync(rootModelsDirectory);

const runTool = () => {
const options: MediaToolOptions = { inputDir, outputDir, toolMode };
if (toolMode === 'EXTRACT_FRAMES') {
options.frameInterval = extractionInterval;
} else if (toolMode === 'CREATE_THUMBNAILS') {
options.maxThumbnailDimensions = thumbnailSize;
}
setLogMessage(''); // Remove any log from previous runs.
extractImages(options, appendLogMessage, setIsRunning, setExitCode, t);
};

let parameterSlider;
if (toolMode === 'EXTRACT_FRAMES') {
parameterSlider = (
<FormGroup label={t('tools.extractionInterval')}>
<Slider value={extractionInterval} onChange={setExtractionInterval} min={1} max={10} />
</FormGroup>
);
} else if (toolMode === 'CREATE_THUMBNAILS') {
const labelRenderer = (value: number, options?: { isHandleTooltip: boolean }) => {
if (options && options.isHandleTooltip) {
return `${value.toFixed()}\u00a0px`; // Use non-breaking space between number and unit.
}
const label = THUMBNAIL_SIZE_LABELS.get(value);
return label ? t(label) : '';
};
parameterSlider = (
<FormGroup label={t('tools.thumbnailSize')}>
<Slider
value={thumbnailSize}
onChange={setThumbnailSize}
labelValues={[...THUMBNAIL_SIZE_LABELS.keys()]}
labelRenderer={labelRenderer}
min={200}
max={800}
stepSize={10}
/>
</FormGroup>
);
}

const extractionForm = (
<div style={{ padding: '30px 30px', width: '60vw' }}>
<RadioGroup
selectedValue={currentMode}
onChange={e => setCurrentMode(e.currentTarget.value)}
selectedValue={toolMode}
onChange={e => setToolMode(e.currentTarget.value as ToolMode)}
label={t('tools.mode')}
>
<Radio value={EXTRACT_FRAMES} label={t('tools.extractFramesDetail')} />
<Radio value={CREATE_THUMBNAILS} label={t('tools.createThumbnailsDetail')} />
<Radio value="EXTRACT_FRAMES" label={t('tools.extractFramesDetail')} />
<Radio value="CREATE_THUMBNAILS" label={t('tools.createThumbnailsDetail')} />
</RadioGroup>

<div className="bp3-input-group" style={{ marginBottom: '10px' }}>
<div style={{ marginTop: '30px' }}>{parameterSlider}</div>
<div className="bp3-input-group" style={{ marginTop: '30px', marginBottom: '10px' }}>
<input
type="text"
className="bp3-input"
Expand All @@ -171,7 +234,6 @@ export default function MediaToolsPage() {
}}
/>
</div>

<div className="bp3-input-group" style={{ marginBottom: '10px' }}>
<input
type="text"
Expand All @@ -191,27 +253,14 @@ export default function MediaToolsPage() {
}}
/>
</div>

<Button
text={
currentMode === EXTRACT_FRAMES ? t('tools.extractFrames') : t('tools.createThumbnails')
toolMode === 'EXTRACT_FRAMES' ? t('tools.extractFrames') : t('tools.createThumbnails')
}
onClick={() => {
setLogMessage(''); // Remove any log from previous runs.
extractImages(
inputDir,
outputDir,
currentMode === CREATE_THUMBNAILS,
appendLogMessage,
setIsRunning,
setExitCode,
t
);
}}
onClick={runTool}
disabled={isRunning || inputDir === '' || outputDir === ''}
style={{ marginBottom: '10px', backgroundColor: '#fff' }}
/>

{exitCode !== undefined || isRunning ? (
<PythonLogViewer
title={t('tools.logTitle')}
Expand Down
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mbaza-ai",
"productName": "Mbaza AI",
"version": "1.1.1",
"version": "1.2.0",
"description": "A desktop application that allows bioconservation researchers to classify camera trap animal images and analyze the results.",
"main": "./main.prod.js",
"author": {
Expand Down
2 changes: 1 addition & 1 deletion models/runner/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
warnings.filterwarnings('ignore')

N_TOP_RESULTS = 3
APP_VERSION = '1.1.1'
APP_VERSION = '1.2.0'

def get_images(directory):
images = [os.path.join(directory, f) for f in get_all_files(directory) if is_image(f)]
Expand Down
6 changes: 6 additions & 0 deletions models/runner/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ def setup_extract_images_parser(parser):
action="store_true",
help="Thumbnail mode (extract a single, scaled down frame from each video / image)"
)
parser.add_argument(
"--max_thumbnail_dimensions",
type = int,
default = 350,
help="Maximum width/height of a thumbnail in pixels",
)

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='Choose the task to run', dest='command')
Expand Down
14 changes: 5 additions & 9 deletions models/runner/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
from file_utils import get_all_files, is_image, is_video, basename_without_ext


MAX_THUMBNAIL_PIXELS = 50_000


def copy_dir_tree(video_data_folder, new_image_data_folder):
""" Replicate the folder structure from video_data_folder into new_image_data_folder """
for dirpath, dirnames, filenames in os.walk(video_data_folder):
Expand All @@ -34,15 +31,14 @@ def get_frame(self, position_seconds):
return frame if success else None


def scale_down_image(image):
pixels = image.shape[0] * image.shape[1]
scale = sqrt(MAX_THUMBNAIL_PIXELS / pixels)
def scale_down_image(image, max_dimensions):
scale = max_dimensions / max(image.shape[0], image.shape[1])
if scale < 1:
image = cv.resize(image, None, fx=scale, fy=scale, interpolation=cv.INTER_AREA)
return image


def extract_thumbnail(input_file, output_dir):
def extract_thumbnail(input_file, output_dir, max_dimensions):
print(f"Processing {os.path.basename(input_file)!r}... ", end="", flush=True)
if is_image(input_file):
print("scaling image... ", end="", flush=True)
Expand All @@ -53,7 +49,7 @@ def extract_thumbnail(input_file, output_dir):
image = video.get_frame(0)
else:
return
thumbnail = scale_down_image(image)
thumbnail = scale_down_image(image, max_dimensions)

name = basename_without_ext(input_file)
output_file = os.path.join(output_dir, f"{name}.jpg")
Expand Down Expand Up @@ -82,7 +78,7 @@ def extract_images(args):
input_file = os.path.join(args.input_folder, path)
output_dir = os.path.join(args.output_folder, os.path.dirname(path))
if args.thumbnails:
extract_thumbnail(input_file, output_dir)
extract_thumbnail(input_file, output_dir, args.max_thumbnail_dimensions)
else:
extract_frames(input_file, output_dir, args.frame_interval)
print("Processing completed.")
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mbaza-ai",
"productName": "Mbaza AI",
"version": "1.1.1",
"version": "1.2.0",
"description": "A desktop application that allows bioconservation researchers to classify camera trap animal images and analyze the results.",
"scripts": {
"build": "concurrently \"yarn build-main\" \"yarn build-renderer\"",
Expand Down Expand Up @@ -286,7 +286,7 @@
"yarn": "^1.21.1"
},
"dependencies": {
"@blueprintjs/core": "^3.29.0",
"@blueprintjs/core": "^3.38.2",
"@blueprintjs/table": "^3.8.10",
"@fortawesome/fontawesome-free": "^5.12.1",
"@hot-loader/react-dom": "^16.12.0",
Expand Down
27 changes: 26 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,23 @@
resize-observer-polyfill "^1.5.1"
tslib "~1.10.0"

"@blueprintjs/core@^3.38.2":
version "3.38.2"
resolved "https://registry.yarnpkg.com/@blueprintjs/core/-/core-3.38.2.tgz#fae1ab6ae9c2b21af63d9afd8cb851bb94a7168f"
integrity sha512-YOTZ8UkaKCjP/g8fm9e2py5/9TvAWHwhhDQAH30NV53FD5oBOjYu0+mUU1hRys9TrBflvA3cQn8EgHRP1y/K9A==
dependencies:
"@blueprintjs/icons" "^3.24.0"
"@types/dom4" "^2.0.1"
classnames "^2.2"
dom4 "^2.1.5"
normalize.css "^8.0.1"
popper.js "^1.16.1"
react-lifecycles-compat "^3.0.4"
react-popper "^1.3.7"
react-transition-group "^2.9.0"
resize-observer-polyfill "^1.5.1"
tslib "~1.13.0"

"@blueprintjs/icons@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@blueprintjs/icons/-/icons-3.19.0.tgz#83ad9fe213705f6656dbec6e9d6bec75d3564455"
Expand All @@ -1149,6 +1166,14 @@
classnames "^2.2"
tslib "~1.10.0"

"@blueprintjs/icons@^3.24.0":
version "3.24.0"
resolved "https://registry.yarnpkg.com/@blueprintjs/icons/-/icons-3.24.0.tgz#aa7e6042e40806d22f85da8d62990ff0296adcf2"
integrity sha512-OvDDI5EUueS1Y3t594iS8LAGoHhLhYjC2GuN/01a85n+ASLSp0jf0/+uix2JeCOj41iTdRRCINbWuRwVQNNGPw==
dependencies:
classnames "^2.2"
tslib "~1.13.0"

"@blueprintjs/table@^3.8.10":
version "3.8.10"
resolved "https://registry.yarnpkg.com/@blueprintjs/table/-/table-3.8.10.tgz#97535d95d11a498774642b2330a0fbe70bbc93f8"
Expand Down Expand Up @@ -17427,7 +17452,7 @@ tsconfig-paths@^3.9.0:
minimist "^1.2.0"
strip-bom "^3.0.0"

tslib@^1.8.1, tslib@^1.9.0:
tslib@^1.8.1, tslib@^1.9.0, tslib@~1.13.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
Expand Down

0 comments on commit 24a347c

Please sign in to comment.