Skip to content

Commit

Permalink
Merge pull request #143 from Appsilon/kamil.correct-predictions
Browse files Browse the repository at this point in the history
Allow users to correct predictions and export a modified CSV
  • Loading branch information
kamilzyla authored Jan 24, 2021
2 parents 7c023c7 + 8a01fe6 commit a8d8be4
Show file tree
Hide file tree
Showing 14 changed files with 1,227 additions and 274 deletions.
8 changes: 8 additions & 0 deletions app/assets/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
"mapHint": "Size of circle corresponds to the number of detected species (species richness). Click on a station to see details.",
"plotHint": "To save the plot to disk, hover on the plot and use the button with a camera icon",
"changeFile": "Change data",
"overrides": "{{count}} overriden predictions",
"overridesTooltip": "Inspect images on the map to override predictions",
"mainView": "Main Information",
"tableView": "Observations Table",
"imagesCount": "Images classified",
Expand All @@ -83,6 +85,8 @@
"photoHeader": "{{species}} {{date}}",
"prediction": "Prediction",
"probability": "Probability",
"overrideTooltip": "You can override the top prediction and export a modified CSV",
"overridePlaceholder": "Override prediction",
"camera": "Camera",
"check": "Check",
"file": "File"
Expand Down Expand Up @@ -154,6 +158,8 @@
"mapHint": "La taille du cercle correspond au nombre d'espèces détectées. Cliquez sur une station pour voir les détails.",
"plotHint": "Pour enregistrer le graphique sur le disque, passez le curseur sur le graphique et utilisez le bouton avec l'icône d'un appareil photo.",
"changeFile": "Modifier les données",
"overrides": "TODO",
"overridesTooltip": "TODO",
"mainView": "Informations principales",
"tableView": "Tableau des observations",
"imagesCount": "Images classifiées",
Expand All @@ -174,6 +180,8 @@
"photoHeader": "{{species}} {{date}}",
"prediction": "Prédiction",
"probability": "Probabilité",
"overrideTooltip": "TODO",
"overridePlaceholder": "TODO",
"camera": "ID de l'appareil photo",
"check": "Check",
"file": "Fichier"
Expand Down
50 changes: 11 additions & 39 deletions app/components/Classifier.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,17 @@ import fs from 'fs';
import { TFunction } from 'i18next';

import PythonLogViewer from './PythonLogViewer';
import showSaveCsvDialog from '../utils/showSaveCsvDialog';
import {
isDev,
isLinux,
isWin,
rootModelsDirectory
} from '../utils/environment';

type changeLogMessageType = (newChangeLogMessage: string | null) => {};
type changePathChoiceType = (newPath: string) => {};

const isDev = process.env.NODE_ENV === 'development';
const isWin = !isDev && process.platform === 'win32';
const isLinux = !isDev && process.platform === 'linux';

function getUserDataPath() {
if (isDev) {
return path.resolve('.');
}
// eslint-disable-next-line global-require
const { app } = require('electron').remote;
return app.getPath('userData');
}

const rootModelsDirectory = path.join(getUserDataPath(), 'models');

const toaster = Toaster.create({});

function displayErrorToast(message: string) {
Expand Down Expand Up @@ -189,29 +181,6 @@ const chooseDirectory = (changeDirectoryChoice: changePathChoiceType) => {
});
};

function chooseSavePath(changeSavePathChoice: changePathChoiceType) {
// eslint-disable-next-line global-require
const { dialog, app } = require('electron').remote;
dialog
.showSaveDialog({
defaultPath: `${app.getPath('documents')}/classification_result.csv`,
filters: [
{ name: 'CSV', extensions: ['csv'] },
{ name: 'All Files', extensions: ['*'] }
]
})
.then(result => {
if (!result.canceled) {
changeSavePathChoice(result.filePath ? result.filePath : '');
}
return null;
})
.catch(error => {
// eslint-disable-next-line no-alert
alert(error);
});
}

type Props = {
changeLogMessage: changeLogMessageType;
changeDirectoryChoice: changePathChoiceType;
Expand Down Expand Up @@ -295,7 +264,10 @@ export default function Classifier(props: Props) {
type="submit"
className="bp3-button bp3-minimal bp3-intent-primary bp3-icon-search"
onClick={() => {
chooseSavePath(changeSavePathChoice);
showSaveCsvDialog(
'classification_result.csv',
changeSavePathChoice
);
}}
/>
</div>
Expand Down
160 changes: 14 additions & 146 deletions app/components/Map.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
import mapboxgl from 'mapbox-gl';
import _ from 'lodash';
import path from 'path';
import {
Button,
Card,
Classes,
Drawer,
Elevation,
Position,
Tooltip
} from '@blueprintjs/core';
import { Button, Position, Tooltip } from '@blueprintjs/core';
import ReactDOM from 'react-dom';
import styles from './Map.css';
import AnimalsListTooltipContent from './AnimalsListTooltipContent';
import {
EmptyClasses,
formatAnimalClassName
} from '../constants/animalsClasses';
import { EmptyClasses } from '../constants/animalsClasses';

/*
To produce a country file please have a look at download_map.sh
Expand All @@ -46,10 +34,6 @@ mapboxStyle.sources.jsonsource = {

// COMPONENT RENDERING

type Props = {
data: ObservationsData;
};

function getObservationCoordinates(row: Observation): [number, number] {
return [row.coordinates_long, row.coordinates_lat];
}
Expand Down Expand Up @@ -189,144 +173,28 @@ function addMarkers(
}
}

function observationCard(t: TFunction, observation: Observation): JSX.Element {
const predictions = [
[formatAnimalClassName(observation.pred_1), observation.score_1],
[formatAnimalClassName(observation.pred_2), observation.score_2],
[formatAnimalClassName(observation.pred_3), observation.score_3]
];
return (
<Card
elevation={Elevation.TWO}
key={observation.location}
style={{ marginTop: 10 }}
>
<h3 style={{ marginTop: 0 }}>
{t('explore.inspect.photoHeader', {
species: formatAnimalClassName(observation.pred_1),
date: observation.date
})}
</h3>
<div style={{ display: 'flex' }}>
<div>
<img
src={observation.location}
width={400}
alt={observation.pred_1}
/>
</div>
<div style={{ marginLeft: 24 }}>
<table className="bp3-html-table bp3-html-table-condensed">
<thead>
<tr>
<th>{t('explore.inspect.prediction')}</th>
<th>{t('explore.inspect.probability')}</th>
</tr>
</thead>
<tbody>
{predictions.map(i => (
<tr key={i[0]}>
<td>{i[0]}</td>
<td>
{((i[1] as number) * 100).toFixed(2)}
&nbsp;%
</td>
</tr>
))}
</tbody>
</table>
<div style={{ margin: '20px 10px' }}>
<p>
<strong>
{t('explore.inspect.camera')}
:&nbsp;
</strong>
{observation.camera}
</p>
<p>
<strong>
{t('explore.inspect.check')}
:&nbsp;
</strong>
{observation.check}
</p>
<p>
<strong>
{t('explore.inspect.file')}
:&nbsp;
</strong>
{path.basename(observation.location)}
</p>
</div>
</div>
</div>
</Card>
);
}

function observationsInspector(
inspectedObservations: Observation[],
setInspectedObservations: React.Dispatch<React.SetStateAction<Observation[]>>
): React.ReactNode {
const { t } = useTranslation();
if (inspectedObservations.length === 0) {
return null;
}
return (
<Drawer
title={t('explore.inspect.header', {
station: inspectedObservations[0].station
})}
icon="camera"
isOpen={inspectedObservations.length > 0}
onClose={() => setInspectedObservations([])}
hasBackdrop={false}
>
<div className={Classes.DRAWER_BODY}>
<div className={Classes.DIALOG_BODY}>
{inspectedObservations.map(observation =>
observationCard(t, observation)
)}
</div>
</div>
<div className={Classes.DRAWER_FOOTER}>
{t('explore.inspect.observations', {
count: inspectedObservations.length
})}
</div>
</Drawer>
);
}
type MapProps = {
data: ObservationsData;
onInspect: (observations: Observation[]) => void;
};

export default function Map(props: Props) {
const { data } = props;
export default function Map(props: MapProps) {
const { data, onInspect } = props;
const mapRef = React.createRef<HTMLDivElement>();
const { t } = useTranslation();
const [inspectedObservations, setInspectedObservations] = useState<
Observation[]
>([]);

useEffect(() => {
const map = new mapboxgl.Map({
container: mapRef.current as HTMLElement,
style: mapboxStyle,
center: [12, -0.8],
zoom: 6
});
addMarkers(t, data.observations, map, setInspectedObservations);
addMarkers(t, data.observations, map, onInspect);
return function cleanup() {
map.remove();
};
});
return (
<div
style={{
width: '100%',
position: 'relative',
overflow: 'hidden'
}}
>
<div ref={mapRef} className={styles.mapContainer} />
{observationsInspector(inspectedObservations, setInspectedObservations)}
</div>
);
}, [data.observations]);

return <div ref={mapRef} className={styles.mapContainer} />;
}
Loading

0 comments on commit a8d8be4

Please sign in to comment.