Skip to content

Adding option to preserve all-digit text in CVAT as string#7258

Draft
andresjguevara wants to merge 1 commit intovoxel51:developfrom
andresjguevara:feature/cvat-utils-string-type-option
Draft

Adding option to preserve all-digit text in CVAT as string#7258
andresjguevara wants to merge 1 commit intovoxel51:developfrom
andresjguevara:feature/cvat-utils-string-type-option

Conversation

@andresjguevara
Copy link
Copy Markdown

@andresjguevara andresjguevara commented Mar 30, 2026

🔗 Related Issues

#7221

📋 What changes are proposed in this pull request?

This PR adds a CVAT-specific, backward-compatible opt-in for annotation loading:

dataset.load_annotations(anno_key, preserve_cvat_text_strings=True)

When enabled, CVAT attributes declared as input_type == "text" are preserved as raw strings instead of being schema-blind coerced (e.g. "0020" stays "0020"). Default behavior is unchanged. Non-text CVAT attributes keep current parsing behavior, and when schema metadata is missing, parsing falls back to existing behavior.

The change is scoped to CVAT ingestion paths (including XML consistency), with updated docstrings/signatures and CVAT docs.

🧪 How is this patch tested? If it is not, please explain why.

Added/updated tests cover:

  • Default regression (flag omitted): existing coercion remains ("100" -> 100, "1.5" -> 1.5, "False" -> False, "0020" -> 20)
  • Opt-in behavior: CVAT text attributes remain exact strings ("0020", "100", "False")
  • Non-text behavior under opt-in: checkbox/radio/select/scalar parsing remains unchanged
  • Fallback safety: missing/unknown attribute type metadata falls back to current parser behavior
  • Load-path plumbing: verifies preserve_cvat_text_strings=True reaches CVAT parsing flow

Manual validation also confirmed representative before/after parsing outcomes for CVAT text attributes.

📝 Release Notes

Is this a user-facing change that should be mentioned in the release notes?

  • Yes. Give a description of this change to be included in the release notes for FiftyOne users.

Adds an opt-in CVAT annotation import flag, preserve_cvat_text_strings, to preserve raw string values for CVAT text attributes during dataset.load_annotations(). This prevents string-fidelity loss (for example, leading zeros) while keeping default behavior unchanged.

What areas of FiftyOne does this PR affect?

  • Core: Core fiftyone Python library changes
  • Documentation: FiftyOne documentation changes

Summary by CodeRabbit

  • New Features

    • Added preserve_cvat_text_strings parameter for CVAT annotation loading, enabling retention of original string values in text attributes without automatic type conversion.
  • Documentation

    • Added documentation section describing the new preserve_cvat_text_strings option and usage examples for CVAT tasks.

@andresjguevara andresjguevara requested a review from a team as a code owner March 30, 2026 19:22
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

Walkthrough

This pull request adds a preserve_cvat_text_strings loading option to prevent automatic type coercion of CVAT text attribute values during annotation import. The feature threads this flag through XML and API-based CVAT loaders, detects which attributes are declared as text type, and skips numeric/boolean parsing when enabled. Supporting helper functions parse attribute values conditionally based on input type. Documentation is updated with usage guidance. Unit tests verify behavior across both XML and backend-based workflows.

Sequence Diagram

sequenceDiagram
    actor User
    participant Loader as CVAT Loader
    participant AttrDetector as Attribute<br/>Detector
    participant Parser as Value Parser
    participant Dataset as FiftyOne<br/>Dataset

    User->>Loader: load_cvat_image_annotations(xml, preserve_cvat_text_strings=True)
    Loader->>AttrDetector: Identify text-typed attributes per label
    AttrDetector-->>Loader: text_attribute_names_by_label map
    
    loop For each annotation
        Loader->>Parser: Parse attribute value<br/>(input_type, value, preserve flag)
        alt preserve_cvat_text_strings=True
            Parser-->>Loader: Return raw string
        else preserve_cvat_text_strings=False
            Parser-->>Loader: Coerce to numeric/bool
        end
    end
    
    Loader->>Dataset: Populate with typed attributes
    Dataset-->>User: Annotations loaded
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding an option to preserve CVAT text attributes as strings rather than parsing them to numeric/boolean types.
Description check ✅ Passed The description covers all required sections: related issues, proposed changes, testing approach, and release notes with user-facing details and affected areas.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Burhan-Q
Copy link
Copy Markdown
Member

FYI, there is another PR for this issue #7252

@andresjguevara
Copy link
Copy Markdown
Author

andresjguevara commented Mar 30, 2026

Burhan-Q Thank you! I just realized after looking at the status of the issue. Let me know if I should just close this PR. Thanks for you attention

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
fiftyone/utils/cvat.py (2)

7689-7755: ⚠️ Potential issue | 🟠 Major

Thread this flag through the CVAT dataset importers as well.

These helpers now support the opt-in, but CVATImageDatasetImporter.setup() and CVATVideoDatasetImporter.__next__() still call them with defaults. As written, Dataset.from_dir(..., fo.types.CVATImageDataset/fo.types.CVATVideoDataset) cannot preserve CVAT text attrs and still coerces values like "0020" on the high-level XML import path.

Also applies to: 7758-7835

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fiftyone/utils/cvat.py` around lines 7689 - 7755, The importers call the
helper loaders with defaults so the preserve_cvat_text_strings opt-in is
ignored; update CVATImageDatasetImporter.setup() and
CVATVideoDatasetImporter.__next__() to pass the importer-level flag through into
load_cvat_image_annotations (and the analogous load_cvat_video_annotations) by
threading a preserve_cvat_text_strings attribute (e.g.
self.preserve_cvat_text_strings or a parameter on setup/constructor) and using
it when calling CVATImage.from_image_dict / the video loader; ensure both call
sites referenced (CVATImageDatasetImporter.setup,
CVATVideoDatasetImporter.__next__) forward the flag instead of relying on the
loader default so CVAT text attributes are preserved when requested.

7376-7388: ⚠️ Potential issue | 🟠 Major

Preserved text attrs are still re-typed in the attribute: branch.

This now preserves raw strings at parse time, but Line 7387 immediately sends attribute: values back through CVATAttribute.to_attribute(), which re-inferrs the type from the content. That means API-loaded attrs stored in label.attributes can still lose "0020" and diverge from the XML path.

💡 Suggested fix
         for attr in attrs:
             name = attr_id_map_rev[attr["spec_id"]]
             input_type = _get_cvat_attr_input_type(
                 attr_input_type_map, cvat_id, attr["spec_id"]
             )
             value = _parse_cvat_attribute_value(
                 attr["value"],
                 input_type=input_type,
                 preserve_cvat_text_strings=preserve_cvat_text_strings,
             )
             if value is not None:
                 if name.startswith("attribute:"):
                     name = name[len("attribute:") :]
-                    fo_attr = CVATAttribute(name, value).to_attribute()
+                    if (
+                        preserve_cvat_text_strings
+                        and _is_cvat_text_input_type(input_type)
+                    ):
+                        fo_attr = fol.CategoricalAttribute(value=value)
+                    else:
+                        fo_attr = CVATAttribute(name, value).to_attribute()
                     self.fo_attributes[name] = fo_attr
                 else:
                     self.attributes[name] = value
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fiftyone/utils/cvat.py` around lines 7376 - 7388, The code preserves raw text
in _parse_cvat_attribute_value but then re-infers types by calling
CVATAttribute.to_attribute() in the "attribute:" branch; change the branch in
the loop that handles names starting with "attribute:" so that when
preserve_cvat_text_strings is True you do not call CVATAttribute.to_attribute(),
instead store the parsed raw string value directly (or construct a FO attribute
that explicitly marks it as a preserved string) into self.fo_attributes[name];
keep the existing CVATAttribute.to_attribute() path only when
preserve_cvat_text_strings is False so API-loaded attrs do not lose values like
"0020".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unittests/utils_tests.py`:
- Around line 1188-1196: The XML fixture in tests/unittests/utils_tests.py omits
the input_type for the attributes named "bib" and "score", making the test rely
on fallback semantics; update the fixture to include explicit input_type
attributes for <attribute><name>bib</name> and <attribute><name>score</name> so
they match the CVAT input_type expected by the test assertions (e.g., set
input_type="text" or the specific type the assertions assume), ensuring the test
validates declared input types rather than implicit defaults.

---

Outside diff comments:
In `@fiftyone/utils/cvat.py`:
- Around line 7689-7755: The importers call the helper loaders with defaults so
the preserve_cvat_text_strings opt-in is ignored; update
CVATImageDatasetImporter.setup() and CVATVideoDatasetImporter.__next__() to pass
the importer-level flag through into load_cvat_image_annotations (and the
analogous load_cvat_video_annotations) by threading a preserve_cvat_text_strings
attribute (e.g. self.preserve_cvat_text_strings or a parameter on
setup/constructor) and using it when calling CVATImage.from_image_dict / the
video loader; ensure both call sites referenced (CVATImageDatasetImporter.setup,
CVATVideoDatasetImporter.__next__) forward the flag instead of relying on the
loader default so CVAT text attributes are preserved when requested.
- Around line 7376-7388: The code preserves raw text in
_parse_cvat_attribute_value but then re-infers types by calling
CVATAttribute.to_attribute() in the "attribute:" branch; change the branch in
the loop that handles names starting with "attribute:" so that when
preserve_cvat_text_strings is True you do not call CVATAttribute.to_attribute(),
instead store the parsed raw string value directly (or construct a FO attribute
that explicitly marks it as a preserved string) into self.fo_attributes[name];
keep the existing CVATAttribute.to_attribute() path only when
preserve_cvat_text_strings is False so API-loaded attrs do not lose values like
"0020".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2023cdc0-3213-465f-9de7-887455b30996

📥 Commits

Reviewing files that changed from the base of the PR and between 8594f09 and 1aedfe5.

📒 Files selected for processing (3)
  • docs/source/integrations/cvat.rst
  • fiftyone/utils/cvat.py
  • tests/unittests/utils_tests.py

Comment on lines +1188 to +1196
<attribute>
<name>bib</name>
<values></values>
</attribute>
<attribute>
<name>score</name>
<values>low
high</values>
</attribute>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align the XML fixture with the flag contract (explicit input_type).

Line 1188-Line 1196 omits input_type for bib/score, but the assertions validate behavior that is supposed to be gated by declared CVAT input type. This makes the test ambiguous and can drift from the fallback semantics for missing metadata.

Proposed fix
                                     <attribute>
                                         <name>bib</name>
+                                        <input_type>text</input_type>
                                         <values></values>
                                     </attribute>
                                     <attribute>
                                         <name>score</name>
+                                        <input_type>select</input_type>
                                         <values>low
 high</values>
                                     </attribute>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<attribute>
<name>bib</name>
<values></values>
</attribute>
<attribute>
<name>score</name>
<values>low
high</values>
</attribute>
<attribute>
<name>bib</name>
<input_type>text</input_type>
<values></values>
</attribute>
<attribute>
<name>score</name>
<input_type>select</input_type>
<values>low
high</values>
</attribute>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unittests/utils_tests.py` around lines 1188 - 1196, The XML fixture in
tests/unittests/utils_tests.py omits the input_type for the attributes named
"bib" and "score", making the test rely on fallback semantics; update the
fixture to include explicit input_type attributes for
<attribute><name>bib</name> and <attribute><name>score</name> so they match the
CVAT input_type expected by the test assertions (e.g., set input_type="text" or
the specific type the assertions assume), ensuring the test validates declared
input types rather than implicit defaults.

@Burhan-Q
Copy link
Copy Markdown
Member

Burhan-Q commented Mar 30, 2026

No worries @andresjguevara and maybe you can set your PR to draft for now. If the other PR doesn't get updated or is stalled, we can investigate alternatives. I will say the other proposed change is much smaller, and even with that one I suggested trimming it more (see this comment for details if interested). Appreciate you opening the PR and there are other issues you're very welcome to contribute towards!

@andresjguevara
Copy link
Copy Markdown
Author

@Burhan-Q Thank you! Will set as Draft and review the other PR and your suggestion to get myself familiarized. Thanks

@andresjguevara andresjguevara marked this pull request as draft March 30, 2026 20:17
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.90%. Comparing base (c35f162) to head (1aedfe5).
⚠️ Report is 2282 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #7258      +/-   ##
===========================================
- Coverage    99.10%   98.90%   -0.20%     
===========================================
  Files           85      102      +17     
  Lines        23353    29510    +6157     
===========================================
+ Hits         23143    29186    +6043     
- Misses         210      324     +114     
Flag Coverage Δ
python 98.90% <100.00%> (-0.20%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants