fgerlits commented on code in PR #2258:
URL: https://github.com/apache/nifi-minifi-cpp/pull/2258#discussion_r4060487812
##########
minifi_rust/extensions/minifi_tensor/features/resources/.gitignore:
##########
Review Comment:
could these resources go in the build directory?
##########
behave_framework/pyproject.toml:
##########
@@ -9,7 +9,8 @@ dependencies = [
"PyYAML==6.0.3",
"humanfriendly==10.0",
"cryptography==50.0.0",
- "pyjks==20.0.0"
+ "pyjks==20.0.0",
+ "certifi==2026.7.22"
Review Comment:
`certifi` is just a list of root certificates, right? if so, then newer is
better:
```suggestion
"certifi>=2026.7.22"
```
##########
minifi_rust/extensions/minifi_tensor/src/low_level_processors/classify_output/classify_output_def.rs:
##########
@@ -0,0 +1,164 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use super::{ClassifyOutput, ScoreActivation};
+use minifi_native::{
+ OutputAttribute, ProcessorDefinition, ProcessorInputRequirement, Property,
PropertyDefinition,
+ Relationship, property_definitions,
+};
+use std::path::PathBuf;
+
+pub(crate) const TOP_K: Property<usize> = Property::new(
+ "Top K",
+ "Number of highest-scoring classes to include in the output JSON, in
descending \
+ order of confidence. Values above the total class count are
clamped. Set to 1 \
+ for pure top-1 classification.",
+)
+.with_default("5");
+
+pub(crate) const SCORE_OUTPUT_INDEX: Property<usize> = Property::new(
+ "Score output index",
+ "Zero-based index of the model output tensor that holds classification
scores. \
+ The processor slices the concatenated payload from
InvokeTractModel according \
+ to the 'tensor.N.bytes' attributes. Almost always 0 for
single-head \
+ classifiers.",
+)
+.with_default("0");
+
+pub(crate) const SCORE_ACTIVATION: Property<ScoreActivation> = Property::new(
+ "Score activation",
+ "Activation applied to the raw score vector before ranking. \
+ Softmax = mutually-exclusive classes (ImageNet-trained
ResNet/MobileNet/\
+ EfficientNet raw logits). \
+ Sigmoid = independent classes (multi-label classifiers). \
+ None = the model already emits probabilities/scores; rank
the raw values.",
+)
+.with_default(ScoreActivation::Softmax.into_str());
+
+pub(crate) const CONFIDENCE_THRESHOLD: Property<f32> = Property::new(
+ "Confidence Threshold",
+ "Minimum confidence a class must reach to be included in the output JSON. \
+ Applied AFTER activation, so the units match the chosen
activation \
+ (0.0..=1.0 for Softmax/Sigmoid, model-native for None). Set
to 0.0 to always \
+ emit exactly Top K predictions.",
+)
+.with_default("0.0")
+.supports_expression_language();
+
+pub(crate) const LABELS_FILE_PATH: Property<Option<PathBuf>> = Property::new(
+ "Labels file path",
+ "Optional path to a newline-separated labels file (line N = name of class
N). \
+ Loaded once at service enable time. When set, each
prediction in the output \
+ JSON gains a 'class_name' field and the 'class.top1.name'
flow file attribute \
+ is populated. Leave empty to emit numeric class IDs only.",
+);
+
+pub(crate) const LABEL_INDEX_OFFSET: Property<usize> = Property::new(
+ "Label index offset",
+ "Offset added to the model's class ID when looking up a name in the labels
file. \
+ Defaults to 0 (labels file line N = class N). Set to 1 for
label files that \
+ start with a dummy/background entry — e.g. the ONNX
MobileNetV2 model emits \
+ 1000 class scores while 'imagenet_slim_labels.txt' has 1001
lines (line 0 = \
+ 'dummy'), so class ID 653 maps to line 654 = 'military
uniform'.",
+)
+.with_default("0");
+
+pub(super) const SUCCESS: Relationship = Relationship {
+ name: "success",
+ description: "Classification completed. The flow file content is a JSON
array of the Top K \
+ predictions (possibly fewer if the confidence threshold
filtered some out).",
+};
+
+pub(super) const FAILURE: Relationship = Relationship {
+ name: "failure",
+ description: "The upstream output attributes were missing/invalid or the
score tensor could \
+ not be interpreted as f32 values.",
+};
+
+const MIME_TYPE_ATTR: OutputAttribute = OutputAttribute {
+ name: "mime.type",
+ relationships: &["success"],
+ description: "Always 'application/json' — the output payload is a JSON
array of prediction \
+ objects with fields class_id, confidence, and optional
class_name.",
+};
+
+const CLASS_COUNT_ATTR: OutputAttribute = OutputAttribute {
+ name: "class.count",
+ relationships: &["success"],
+ description: "Number of predictions retained after Top K selection and
confidence filtering.",
+};
+
+const CLASS_TOP1_ID_ATTR: OutputAttribute = OutputAttribute {
+ name: "class.top1.id",
+ relationships: &["success"],
+ description: "Numeric class ID of the highest-confidence prediction, when
at least one \
+ prediction cleared the confidence threshold.",
+};
+
+const CLASS_TOP1_CONFIDENCE_ATTR: OutputAttribute = OutputAttribute {
+ name: "class.top1.confidence",
+ relationships: &["success"],
+ description: "Confidence (post-activation) of the highest-confidence
prediction, when at \
+ least one prediction cleared the confidence threshold.",
+};
+
+const CLASS_TOP1_NAME_ATTR: OutputAttribute = OutputAttribute {
+ name: "class.top1.name",
+ relationships: &["success"],
+ description: "Label of the highest-confidence prediction. Only present
when 'Labels file \
+ path' was configured and at least one prediction cleared the
threshold.",
+};
+
+pub(crate) const OUTPUT_ATTRIBUTE_NAME: Property<Option<String>> =
Property::new(
+ "Output attribute name",
+ "Specify the attribute to use as output, if not provided, the content is
overridden instead.",
+)
+.supports_expression_language();
Review Comment:
please move this up in this file, so all the properties are together
##########
minifi_rust/extensions/minifi_tensor/src/low_level_processors/classify_output.rs:
##########
@@ -0,0 +1,476 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::utils::score_activation::{ScoreActivation, SoftmaxTerms};
+use crate::utils::tensor_helpers::{deserialize_tensors, tensor_as_f32,
tensor_shape};
+use classify_output_def::SUCCESS;
+pub(crate) use classify_output_def::{
+ CLASSIFY_OUTPUT_ATTRIBUTES, CONFIDENCE_THRESHOLD, LABEL_INDEX_OFFSET,
LABELS_FILE_PATH,
+ OUTPUT_ATTRIBUTE_NAME, SCORE_ACTIVATION, SCORE_OUTPUT_INDEX, TOP_K,
+};
+use minifi_native::macros::ComponentIdentifier;
+use minifi_native::{
+ Content, FlowFileTransform, GetAttribute, GetId, GetProperty, InputStream,
Logger, MinifiError,
+ ProcessError, RouteErrorExt, Schedule, TransformedFlowFile, warn,
+};
+use serde::Serialize;
+use std::path::Path;
+use tract::Tensor;
+
+mod classify_output_def;
+
+#[derive(Serialize, Clone, Debug, PartialEq)]
+struct Prediction {
+ class_id: usize,
+ confidence: f32,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ class_name: Option<String>,
+}
+
+fn load_labels(path: &Path) -> Result<Vec<String>, MinifiError> {
+ let content = std::fs::read_to_string(path).map_err(|e| {
+ MinifiError::custom(format!("Failed to read labels file '{:?}': {}",
path, e))
+ })?;
+ Ok(content
+ .lines()
+ .map(|line| line.trim().to_string())
+ .collect())
+}
+
+fn top_k(mut scored: Vec<(usize, f32)>, k: usize) -> Vec<(usize, f32)> {
+ scored.sort_by(|&(ai, a), &(bi, b)| b.total_cmp(&a).then(ai.cmp(&bi)));
+ scored.truncate(k);
+ scored
+}
+
+#[derive(ComponentIdentifier)]
+pub(crate) struct ClassifyOutput {
+ top_k: usize,
+ score_output_index: usize,
+ score_activation: ScoreActivation,
+ confidence_threshold: f32,
+ labels: Vec<String>,
+ label_index_offset: usize,
+}
+
+impl Schedule for ClassifyOutput {
+ fn schedule<Ctx: GetProperty, L: Logger>(
+ context: &Ctx,
+ _logger: &L,
+ ) -> Result<Self, MinifiError>
+ where
+ Self: Sized,
+ {
+ let top_k = context.get_property(&TOP_K)?;
+ if top_k == 0 {
+ return Err(MinifiError::validation("Top K must be >= 1"));
+ }
+ let score_output_index = context.get_property(&SCORE_OUTPUT_INDEX)?;
+ let score_activation = context.get_property(&SCORE_ACTIVATION)?;
+ let confidence_threshold =
context.get_property(&CONFIDENCE_THRESHOLD)?;
+
+ let labels = match context.get_property(&LABELS_FILE_PATH)? {
+ Some(path) => load_labels(&path)?,
+ _ => Vec::new(),
Review Comment:
Nitpicking, but why isn't `labels` an `Option<Vec<String>>`, set to `None`
if the property is unset? I think the code would read better that way, and
`label_for` could throw an error if the index is not found.
##########
minifi_rust/extensions/minifi_tensor/src/low_level_processors/mod.rs:
##########
Review Comment:
How does `#[cfg(feature = "low-level-processors")]` work? Specifically, why
do we have it only on `invoke_tract_model` here, but on all 4 low-level
processors in `lib.rs`?
##########
minifi_rust/extensions/minifi_tensor/src/low_level_processors/classify_output.rs:
##########
Review Comment:
There are no unit tests for `ScoreActivation::Sigmoid`, only for `None` and
`Softmax`.
##########
minifi_rust/extensions/minifi_tensor/features/detection.feature:
##########
@@ -0,0 +1,131 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+@SUPPORTS_WINDOWS
+Feature: Face detection with UltraFace (SSD)
Review Comment:
Can we write this abbreviation out, please? I had to google it, and it isn't
even easy to google, because most of the pages which come up are about
"solid-state drive".
```suggestion
Feature: Face detection with UltraFace Single Shot MultiBox Detector (SSD)
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]