Add higher rank mode unrestricted to offline suggestions (#2711)

* Use unrestricted commonAncestorRankType for offline predictions from file

* The plugin provides an interface for this

* Fix type error

* Add missing type

* The iconic taxon is no longer part of the predictions result

If we use common ancestor rollup mode the ancestor taxa are no longer included in the result (which in turn are the top 10 species predictions). So, we have to get the iconic taxon name for each result in a different manner.

* Not needed as results are returned with highest score first

* Remove number wrapper

* Update comment

* There is no special offline not-confident criterion

This now returns common ancestor as top ID in case there is none over the threshold. So, same as online.

* No distinction between debug types

* Update ModelPrediction.js

* This is actually redundant

* Offline suggestions need to be sorted same as online

* Should not have removed the export

* Update comments
This commit is contained in:
Johannes Klein
2025-03-04 16:37:14 +01:00
committed by GitHub
parent 25d0ac7e22
commit 271d0bd856
8 changed files with 60 additions and 63 deletions

View File

@@ -14,3 +14,8 @@ export const MODE = {
BEST_BRANCH: "BEST_BRANCH",
COMMON_ANCESTOR: "COMMON_ANCESTOR"
};
export const COMMON_ANCESTOR_RANK_TYPE = {
MAJOR: "major",
UNRESTRICTED: "unrestricted"
};

View File

@@ -39,8 +39,7 @@ export const FETCH_STATUS_OFFLINE_ERROR = "offline-error";
export const TOP_SUGGESTION_NONE = "none";
export const TOP_SUGGESTION_HUMAN = "human";
export const TOP_SUGGESTION_ABOVE_ONLINE_THRESHOLD = "above-online-threshold";
export const TOP_SUGGESTION_ABOVE_OFFLINE_THRESHOLD = "above-offline-threshold";
export const TOP_SUGGESTION_ABOVE_THRESHOLD = "above-threshold";
export const TOP_SUGGESTION_COMMON_ANCESTOR = "common-ancestor";
export const TOP_SUGGESTION_NOT_CONFIDENT = "not-confident";

View File

@@ -4,6 +4,7 @@ import Config from "react-native-config";
import RNFS from "react-native-fs";
import type { Location } from "vision-camera-plugin-inatvision";
import {
COMMON_ANCESTOR_RANK_TYPE,
getPredictionsForImage,
getPredictionsForLocation,
MODE
@@ -67,7 +68,8 @@ export const predictImage = ( uri: string, location: Location ) => {
location: hasLocation
? location
: undefined,
mode: MODE.COMMON_ANCESTOR
mode: MODE.COMMON_ANCESTOR,
commonAncestorRankType: COMMON_ANCESTOR_RANK_TYPE.UNRESTRICTED
} );
};

View File

@@ -1,7 +1,6 @@
import {
initialSuggestions,
TOP_SUGGESTION_ABOVE_OFFLINE_THRESHOLD,
TOP_SUGGESTION_ABOVE_ONLINE_THRESHOLD,
TOP_SUGGESTION_ABOVE_THRESHOLD,
TOP_SUGGESTION_COMMON_ANCESTOR,
TOP_SUGGESTION_HUMAN,
TOP_SUGGESTION_NONE,
@@ -10,24 +9,24 @@ import {
import _ from "lodash";
import isolateHumans, { humanFilter } from "./isolateHumans";
import sortSuggestions from "./sortSuggestions";
const THRESHOLD = 78;
// this function does a few things:
// 1. it makes sure that if there is a human suggestion, human is the only result returned
// 2. it makes sure we're filtering out any results below the THRESHOLD
// so we only show results we're fairly confident in
// 3. it splits the top suggestion result out from the rest of the suggestions, which is helpful for
// 2. it splits the top suggestion result out from the rest of the suggestions, which is helpful for
// displaying in SuggestionsContainer
// 4. it checks for a common ancestor as a fallback top suggestion if user is online
// 4. it checks for a common ancestor as a fallback top suggestion
// 5. it returns topSuggestionType which is useful for debugging, since we're doing a lot of
// filtering of both online and offline suggestions
const filterSuggestions = ( suggestionsToFilter, usingOfflineSuggestions, commonAncestor ) => {
const sortedSuggestions = sortSuggestions(
const filterSuggestions = ( suggestionsToFilter, commonAncestor ) => {
const sortedSuggestions = _.orderBy(
// TODO: handling humans is implemented in the vision-plugin, can it be removed here?
isolateHumans( suggestionsToFilter ),
{ usingOfflineSuggestions }
"combined_score",
"desc"
);
const newSuggestions = {
...initialSuggestions,
otherSuggestions: sortedSuggestions
@@ -64,17 +63,7 @@ const filterSuggestions = ( suggestionsToFilter, usingOfflineSuggestions, common
return {
...newSuggestions,
topSuggestion: firstSuggestion,
topSuggestionType: usingOfflineSuggestions
? TOP_SUGGESTION_ABOVE_OFFLINE_THRESHOLD
: TOP_SUGGESTION_ABOVE_ONLINE_THRESHOLD
};
}
if ( !suggestionAboveThreshold && usingOfflineSuggestions ) {
// no top suggestion for offline
return {
...newSuggestions,
topSuggestion: null,
topSuggestionType: TOP_SUGGESTION_NOT_CONFIDENT
topSuggestionType: TOP_SUGGESTION_ABOVE_THRESHOLD
};
}

View File

@@ -1,17 +0,0 @@
import type { Suggestion } from "components/Suggestions/SuggestionsContainer";
import _ from "lodash";
// eslint-disable-next-line no-undef
export default function sortSuggestions(
suggestions: Suggestion[],
options = {
usingOfflineSuggestions: false
}
) {
const { usingOfflineSuggestions } = options;
if ( usingOfflineSuggestions ) {
// sort finest to coarsest rank
return _.orderBy( suggestions, "taxon.rank_level", "asc" );
}
return _.orderBy( suggestions, "combined_score", "desc" );
}

View File

@@ -6,6 +6,7 @@ import {
} from "react";
import { log } from "sharedHelpers/logger";
import { predictImage } from "sharedHelpers/mlModel.ts";
import { Prediction } from "vision-camera-plugin-inatvision";
const logger = log.extend( "useOfflineSuggestions" );
@@ -24,8 +25,8 @@ interface OfflineSuggestion {
const useOfflineSuggestions = (
photoUri: string,
options: {
onFetchError: ( { isOnline: boolean } ) => void,
onFetchError: ( { isOnline: boolean } ) => void,
onFetchError: ( _p: { isOnline: boolean } ) => void,
onFetched: ( _p: { isOnline: boolean } ) => void,
latitude: number,
longitude: number,
tryOfflineSuggestions: boolean
@@ -35,6 +36,7 @@ const useOfflineSuggestions = (
results: OfflineSuggestion[],
commonAncestor: OfflineSuggestion | undefined
};
refetchOfflineSuggestions: () => void;
} => {
const realm = useRealm( );
const [offlineSuggestions, setOfflineSuggestions] = useState<{
@@ -66,25 +68,41 @@ const useOfflineSuggestions = (
// but we're offline so we only need the local list from realm
// and don't need to fetch taxon from the API
const iconicTaxa = realm?.objects( "Taxon" ).filtered( "isIconic = true" );
const branchIDs = [...rawPredictions.map( t => t.taxon_id ), ...( commonAncestor
? [commonAncestor.taxon_id]
: [] )];
const iconicTaxonName = iconicTaxa?.find( t => branchIDs.indexOf( t.id ) >= 0 )?.name;
const iconicTaxaIds = iconicTaxa.map( t => t.id );
const iconicTaxaLookup: {
[key: number]: string
} = iconicTaxa.reduce( ( acc, t ) => {
acc[t.id] = t.name;
return acc;
}, { } );
// This function handles either regular or common ancestor predictions as input objects. I'm
// not going to define an interface for them in the middle of refactoring and changing logic.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const formatPrediction = ( prediction: any ): OfflineSuggestion => ( {
combined_score: prediction.combined_score,
taxon: {
id: Number( prediction.taxon_id ),
name: prediction.name,
rank_level: prediction.rank_level,
iconic_taxon_name: iconicTaxonName
// This function handles either regular or common ancestor predictions as input objects.
const formatPrediction = ( prediction: Prediction ): OfflineSuggestion => {
// The "lowest" ancestor_id that matches an iconic taxon
// is the iconic taxon of this prediction.
const iconicTaxonId = prediction.ancestor_ids
// Need to reverse so we find the most specific iconic taxon first as an ancestor_ids is
// a list of ancestor ids from tip to root of taxonomy
// e.g. Aves is included in Animalia
.reverse()
.find( id => iconicTaxaIds.includes( id ) );
let iconicTaxonName;
if ( iconicTaxonId !== undefined ) {
iconicTaxonName = iconicTaxaLookup[iconicTaxonId];
}
} );
const formattedPredictions = rawPredictions?.reverse( )
return {
combined_score: prediction.combined_score,
taxon: {
id: prediction.taxon_id,
name: prediction.name,
rank_level: prediction.rank_level,
iconic_taxon_name: iconicTaxonName
}
};
};
const formattedPredictions = rawPredictions
.map( prediction => formatPrediction( prediction ) );
const commonAncestorSuggestion = commonAncestor
@@ -123,7 +141,8 @@ const useOfflineSuggestions = (
photoUri,
tryOfflineSuggestions,
setError,
onFetchError] );
onFetchError
] );
if ( error ) throw error;

View File

@@ -95,12 +95,10 @@ export const useSuggestions = ( photoUri, options ) => {
const suggestions = useMemo(
( ) => filterSuggestions(
unfilteredSuggestions,
usingOfflineSuggestions,
commonAncestor
),
[
unfilteredSuggestions,
usingOfflineSuggestions,
commonAncestor
]
);

View File

@@ -13,5 +13,7 @@ export default define( "ModelPrediction", faker => ( {
10
] ),
combined_score: faker.number.float( { min: 80, max: 100 } ),
taxon_id: faker.number.int( )
vision_score: faker.number.float( { min: 80, max: 100 } ),
taxon_id: faker.number.int( ),
ancestor_ids: faker.helpers.arrayElements( [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] )
} ) );