diff --git a/modules/core/src/persistence_json.cpp b/modules/core/src/persistence_json.cpp index a66c3d18f2..2890058154 100644 --- a/modules/core/src/persistence_json.cpp +++ b/modules/core/src/persistence_json.cpp @@ -527,7 +527,33 @@ public: case 't' : { buf[i++] = '\t'; break; } case 'b' : { buf[i++] = '\b'; break; } case 'f' : { buf[i++] = '\f'; break; } - case 'u' : { CV_PARSE_ERROR_CPP( "'\\uXXXX' currently not supported" ); break; } + case 'u' : { + if (i + 4 >= CV_FS_MAX_LEN) + CV_PARSE_ERROR_CPP("string is too long"); + ptr++; + uint32_t codepoint = 0; + for (int k = 0; k < 4; k++, ptr++) { + char hex = *ptr; + uint32_t digit = 0; + if (hex >= '0' && hex <= '9') digit = (uint32_t)(hex - '0'); + else if (hex >= 'a' && hex <= 'f') digit = (uint32_t)(hex - 'a') + 10u; + else if (hex >= 'A' && hex <= 'F') digit = (uint32_t)(hex - 'A') + 10u; + else CV_PARSE_ERROR_CPP("invalid \\uXXXX escape sequence"); + codepoint = (codepoint << 4) | digit; + } + if (codepoint < 0x80) { + buf[i++] = (char)codepoint; + } else if (codepoint < 0x800) { + buf[i++] = (char)(0xC0 | (codepoint >> 6)); + buf[i++] = (char)(0x80 | (codepoint & 0x3F)); + } else { + buf[i++] = (char)(0xE0 | (codepoint >> 12)); + buf[i++] = (char)(0x80 | ((codepoint >> 6) & 0x3F)); + buf[i++] = (char)(0x80 | (codepoint & 0x3F)); + } + beg = ptr; + continue; + } default : { CV_PARSE_ERROR_CPP( "Invalid escape character" ); } break; } diff --git a/modules/core/test/test_io.cpp b/modules/core/test/test_io.cpp index 2588726861..e0417244a1 100644 --- a/modules/core/test/test_io.cpp +++ b/modules/core/test/test_io.cpp @@ -1685,6 +1685,53 @@ TEST(Core_InputOutput, FileStorage_json_bool) fs.release(); } +TEST(Core_InputOutput, FileStorage_json_unicode_escape) +{ + // Test \uXXXX Unicode escape sequences in JSON strings + std::string test = R"({ + "ascii": "\u0041\u0042\u0043", + "copyright": "\u00A9", + "chinese": "\u4E2D", + "emoji_base": "\u263A", + "mixed": "Hello \u4E16\u754C" + })"; + FileStorage fs(test, FileStorage::READ | FileStorage::MEMORY); + + // Test ASCII characters (\u0041=A, \u0042=B, \u0043=C) + ASSERT_TRUE(fs["ascii"].isString()); + ASSERT_EQ((std::string)fs["ascii"], "ABC"); + + // Test 2-byte UTF-8 character (\u00A9=©) + ASSERT_TRUE(fs["copyright"].isString()); + std::string copyright_str = (std::string)fs["copyright"]; + ASSERT_EQ(copyright_str.size(), 2u); // © is 2 bytes in UTF-8 + ASSERT_EQ((unsigned char)copyright_str[0], 0xC2); + ASSERT_EQ((unsigned char)copyright_str[1], 0xA9); + + // Test 3-byte UTF-8 character (\u4E2D=中) + ASSERT_TRUE(fs["chinese"].isString()); + std::string chinese_str = (std::string)fs["chinese"]; + ASSERT_EQ(chinese_str.size(), 3u); // 中 is 3 bytes in UTF-8 + ASSERT_EQ((unsigned char)chinese_str[0], 0xE4); + ASSERT_EQ((unsigned char)chinese_str[1], 0xB8); + ASSERT_EQ((unsigned char)chinese_str[2], 0xAD); + + // Test another 3-byte character (\u263A=☺) + ASSERT_TRUE(fs["emoji_base"].isString()); + std::string emoji_str = (std::string)fs["emoji_base"]; + ASSERT_EQ(emoji_str.size(), 3u); + ASSERT_EQ((unsigned char)emoji_str[0], 0xE2); + ASSERT_EQ((unsigned char)emoji_str[1], 0x98); + ASSERT_EQ((unsigned char)emoji_str[2], 0xBA); + + // Test mixed ASCII and Unicode + ASSERT_TRUE(fs["mixed"].isString()); + std::string mixed = (std::string)fs["mixed"]; + ASSERT_EQ(mixed.substr(0, 6), "Hello "); // First 6 chars are "Hello " + + fs.release(); +} + TEST(Core_InputOutput, FileStorage_free_file_after_exception) { const std::string fileName = cv::tempfile("FileStorage_free_file_after_exception_test.yml"); diff --git a/modules/dnn/src/onnx/onnx_graph_simplifier.cpp b/modules/dnn/src/onnx/onnx_graph_simplifier.cpp index 3e91894f18..ec06ff6a30 100644 --- a/modules/dnn/src/onnx/onnx_graph_simplifier.cpp +++ b/modules/dnn/src/onnx/onnx_graph_simplifier.cpp @@ -65,8 +65,9 @@ public: class ONNXGraphWrapper : public ImportGraphWrapper { public: - ONNXGraphWrapper(opencv_onnx::GraphProto& _net) : net(_net) + ONNXGraphWrapper(opencv_onnx::GraphProto& _net, const std::string& _basePath = "") : net(_net) { + basePath = _basePath; // Add a fake initializer with empty name. // Some ONNX models skip their inputs. For example, // Resize which has 4 inputs but 2 of them have empty names. @@ -129,7 +130,7 @@ public: Mat getMatFromInitializer(int idx) { const opencv_onnx::TensorProto& tensor_proto = net.initializer(idx); - return getMatFromTensor(tensor_proto); + return getMatFromTensor(tensor_proto, false, basePath); } std::string getNameOfInitializer(int idx) const @@ -176,6 +177,9 @@ public: private: int numInputs, numInitializers; opencv_onnx::GraphProto& net; + +public: + std::string basePath; }; static Mat extractConstant(const Ptr& net, int node_id, int input_id) @@ -193,7 +197,7 @@ static Mat extractConstant(const Ptr& net, int node_id, int Ptr constant_ptr = net->getNode(constant_id); opencv_onnx::NodeProto* constant_node = constant_ptr.dynamicCast()->node; opencv_onnx::TensorProto constant_proto = constant_node->attribute(0).t(); - return getMatFromTensor(constant_proto); + return getMatFromTensor(constant_proto, false, onnx_net->basePath); } } @@ -1725,7 +1729,7 @@ public: } }; -void simplifySubgraphs(opencv_onnx::GraphProto& net) +void simplifySubgraphs(opencv_onnx::GraphProto& net, const std::string& basePath) { std::vector > subgraphs; subgraphs.push_back(makePtr()); @@ -1762,7 +1766,7 @@ void simplifySubgraphs(opencv_onnx::GraphProto& net) subgraphs.push_back(makePtr()); } - simplifySubgraphs(Ptr(new ONNXGraphWrapper(net)), subgraphs); + simplifySubgraphs(Ptr(new ONNXGraphWrapper(net, basePath)), subgraphs); } diff --git a/modules/dnn/src/onnx/onnx_graph_simplifier.hpp b/modules/dnn/src/onnx/onnx_graph_simplifier.hpp index bbe2611b63..490fddd60c 100644 --- a/modules/dnn/src/onnx/onnx_graph_simplifier.hpp +++ b/modules/dnn/src/onnx/onnx_graph_simplifier.hpp @@ -21,7 +21,7 @@ namespace cv { namespace dnn { CV__DNN_INLINE_NS_BEGIN -void simplifySubgraphs(opencv_onnx::GraphProto& net); +void simplifySubgraphs(opencv_onnx::GraphProto& net, const std::string& basePath = ""); template void convertInt64ToInt32(const T1& src, T2& dst, int size) diff --git a/modules/dnn/src/onnx/onnx_importer2.cpp b/modules/dnn/src/onnx/onnx_importer2.cpp index f642b09d97..0cd9873da3 100644 --- a/modules/dnn/src/onnx/onnx_importer2.cpp +++ b/modules/dnn/src/onnx/onnx_importer2.cpp @@ -520,7 +520,7 @@ LayerParams ONNXImporter2::getLayerParams(const opencv_onnx::NodeProto& node_pro else if (attribute_proto.has_t()) { opencv_onnx::TensorProto tensor = attribute_proto.t(); - Mat blob = getMatFromTensor2(tensor); + Mat blob = parseTensor(tensor); lp.blobs.push_back(blob); lp.set("original_dims_of_mat", tensor.dims_size()); } @@ -774,7 +774,7 @@ Mat ONNXImporter2::parseTensor(const opencv_onnx::TensorProto& tensor_proto) Ptr ONNXImporter2::parseGraph(opencv_onnx::GraphProto* graph_proto, bool mainGraph_) { CV_LOG_DEBUG(NULL, "DNN/ONNX: parsing graph '" << graph_proto->name() << "' of " << graph_proto->node_size() << " nodes"); - simplifySubgraphs(*graph_proto); + simplifySubgraphs(*graph_proto, onnxBasePath); int n_nodes = graph_proto->node_size(); CV_LOG_DEBUG(NULL, "DNN/ONNX: simplified the graph to " << n_nodes << " nodes"); diff --git a/modules/dnn/src/tokenizer/core_bpe.cpp b/modules/dnn/src/tokenizer/core_bpe.cpp index 6600e8d5ee..d0fcc80465 100644 --- a/modules/dnn/src/tokenizer/core_bpe.cpp +++ b/modules/dnn/src/tokenizer/core_bpe.cpp @@ -74,7 +74,7 @@ std::vector> bytePairMerge(const ByteVecRa std::vector bytePairEncode(const std::vector& piece, const ByteVecRankMap& ranks) { - if (piece.size() == 1) { + if (piece.size() == 1u) { auto it = ranks.find(piece); return it == ranks.end() ? std::vector{} : std::vector{it->second}; } diff --git a/modules/dnn/src/tokenizer/core_gemma.hpp b/modules/dnn/src/tokenizer/core_gemma.hpp new file mode 100644 index 0000000000..400e6a4b50 --- /dev/null +++ b/modules/dnn/src/tokenizer/core_gemma.hpp @@ -0,0 +1,225 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (C) 2025, BigVision LLC, all rights reserved. +// Third party copyrights are property of their respective owners. + +#ifndef __OPENCV_DNN_TOKENIZER_CORE_GEMMA_HPP__ +#define __OPENCV_DNN_TOKENIZER_CORE_GEMMA_HPP__ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cv { namespace dnn { + +static const std::string GEMMA_METASPACE = "\xe2\x96\x81"; // UTF-8 for ▁ (U+2581) + +struct CoreGemmaBPE { + std::unordered_map pieceToId; + std::vector idToPiece; + std::unordered_map mergeRanks; // key = piece_a + '\0' + piece_b + std::unordered_map specialToId; + std::unordered_map idToSpecial; + + void addMerge(const std::string& a, const std::string& b, uint32_t rank) { + mergeRanks[a + '\0' + b] = rank; + } + + static std::string normalize(const std::string& text) { + std::string out; + out.reserve(text.size() + text.size() / 4); + for (char c : text) { + if (c == ' ') + out += GEMMA_METASPACE; + else + out += c; + } + return out; + } + + static std::vector splitUtf8Chars(const std::string& text) { + std::vector out; + out.reserve(text.size()); + size_t i = 0; + while (i < text.size()) { + unsigned char c = static_cast(text[i]); + int len; + if (c < 0x80) len = 1; + else if (c < 0xE0) len = 2; + else if (c < 0xF0) len = 3; + else len = 4; + if (i + static_cast(len) > text.size()) len = 1; + out.push_back(text.substr(i, len)); + i += static_cast(len); + } + return out; + } + + static std::vector byteFallback(const std::string& ch) { + std::vector out; + out.reserve(ch.size()); + char buf[8]; + for (unsigned char b : ch) { + std::snprintf(buf, sizeof(buf), "<0x%02X>", static_cast(b)); + out.push_back(buf); + } + return out; + } + + std::vector encodePiece(const std::string& text) const { + if (text.empty()) return {}; + + std::vector pieces; + for (const std::string& ch : splitUtf8Chars(text)) { + if (pieceToId.count(ch)) { + pieces.push_back(ch); + } else { + for (const std::string& fb : byteFallback(ch)) + pieces.push_back(fb); + } + } + + static constexpr uint32_t RANK_INF = std::numeric_limits::max(); + while (pieces.size() > 1) { + uint32_t best_rank = RANK_INF; + int best_i = -1; + + for (int i = 0; i + 1 < static_cast(pieces.size()); ++i) { + std::string key; + key.reserve(pieces[i].size() + 1 + pieces[i + 1].size()); + key = pieces[i]; + key += '\0'; + key += pieces[i + 1]; + + auto it = mergeRanks.find(key); + if (it != mergeRanks.end() && it->second < best_rank) { + best_rank = it->second; + best_i = i; + } + } + + if (best_i == -1) break; + + pieces[best_i] += pieces[best_i + 1]; + pieces.erase(pieces.begin() + best_i + 1); + } + + std::vector ids; + ids.reserve(pieces.size()); + for (const std::string& p : pieces) { + auto it = pieceToId.find(p); + if (it != pieceToId.end()) { + ids.push_back(it->second); + } else { + for (const std::string& fb : byteFallback(p)) { + auto it2 = pieceToId.find(fb); + if (it2 != pieceToId.end()) + ids.push_back(it2->second); + } + } + } + return ids; + } + + std::vector encode(const std::string& text, + const std::unordered_set& allowedSpecial = {}) const { + std::vector result; + size_t pos = 0; + + while (pos < text.size()) { + bool foundSpecial = false; + for (const auto& kv : specialToId) { + const std::string& sp = kv.first; + if (!allowedSpecial.count(sp)) continue; + if (text.compare(pos, sp.size(), sp) == 0) { + result.push_back(kv.second); + pos += sp.size(); + foundSpecial = true; + break; + } + } + if (foundSpecial) continue; + + size_t chunkStart = pos; + while (pos < text.size()) { + bool atSpecial = false; + for (const auto& kv : specialToId) { + if (allowedSpecial.count(kv.first) && + text.compare(pos, kv.first.size(), kv.first) == 0) { + atSpecial = true; + break; + } + } + if (atSpecial) break; + ++pos; + } + + std::string chunk = text.substr(chunkStart, pos - chunkStart); + std::string norm = normalize(chunk); + auto ids = encodePiece(norm); + result.insert(result.end(), ids.begin(), ids.end()); + } + + return result; + } + + std::string decode(const std::vector& ids) const { + std::string raw; + raw.reserve(ids.size() * 4); + for (int id : ids) { + auto sp = idToSpecial.find(id); + if (sp != idToSpecial.end()) { + raw += sp->second; + continue; + } + if (id >= 0 && id < static_cast(idToPiece.size())) + raw += idToPiece[id]; + } + + std::string out; + out.reserve(raw.size()); + size_t i = 0; + while (i < raw.size()) { + if (raw[i] == '<' && i + 5 < raw.size() && + raw[i + 1] == '0' && raw[i + 2] == 'x' && + raw[i + 5] == '>') { + char hi = raw[i + 3], lo = raw[i + 4]; + auto hexVal = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; + }; + int hv = hexVal(hi), lv = hexVal(lo); + if (hv >= 0 && lv >= 0) { + out += static_cast((hv << 4) | lv); + i += 6; + continue; + } + } + if (i + 2 < raw.size() && + static_cast(raw[i]) == 0xE2 && + static_cast(raw[i + 1]) == 0x96 && + static_cast(raw[i + 2]) == 0x81) { + out += ' '; + i += 3; + continue; + } + out += raw[i++]; + } + + if (!out.empty() && out[0] == ' ') + out.erase(0, 1); + + return out; + } +}; + +}} // namespace cv::dnn +#endif // __OPENCV_DNN_TOKENIZER_CORE_GEMMA_HPP__ diff --git a/modules/dnn/src/tokenizer/tokenizer.cpp b/modules/dnn/src/tokenizer/tokenizer.cpp index 6fdad454e6..2f332604d1 100644 --- a/modules/dnn/src/tokenizer/tokenizer.cpp +++ b/modules/dnn/src/tokenizer/tokenizer.cpp @@ -6,8 +6,10 @@ #include "utils.hpp" #include "unicode.hpp" #include "core_bpe.hpp" +#include "core_gemma.hpp" #include +#include namespace cv { namespace dnn { CV__DNN_INLINE_NS_BEGIN @@ -54,6 +56,97 @@ struct BpeTokenizerImpl : public Tokenizer::Impl { } }; +struct GemmaBpeTokenizerImpl : public Tokenizer::Impl { + CoreGemmaBPE model; + std::unordered_set allowedSpecial; + + explicit GemmaBpeTokenizerImpl(CoreGemmaBPE m, + std::unordered_set special = {}) + : model(std::move(m)), allowedSpecial(std::move(special)) {} + + std::vector encode(const std::string& text) override { + return model.encode(text, allowedSpecial); + } + + std::string decode(const std::vector& tokens) override { + return model.decode(tokens); + } +}; + +static Ptr buildGemmaFromJson( + const std::string& json_path, + std::unordered_set* outSpecial = nullptr) { + + cv::FileStorage fs(json_path, cv::FileStorage::READ | cv::FileStorage::FORMAT_JSON); + if (!fs.isOpened()) + CV_Error(cv::Error::StsError, "Failed to open tokenizer.json: " + json_path); + + cv::FileNode model_node = fs["model"]; + CV_CheckFalse(model_node.empty(), "tokenizer.json missing 'model'"); + + std::string model_type; + model_node["type"] >> model_type; + if (model_type != "BPE") + CV_Error(cv::Error::StsError, + "Expected BPE model in tokenizer.json for Gemma3, got: " + model_type); + + CoreGemmaBPE gemma; + + cv::FileNode vocab_node = model_node["vocab"]; + CV_CheckFalse(vocab_node.empty(), "tokenizer.json model missing 'vocab'"); + + int maxId = -1; + for (auto it = vocab_node.begin(); it != vocab_node.end(); ++it) { + cv::FileNode entry = *it; + std::string piece = entry.name(); + int id = (int)entry; + if (id > maxId) maxId = id; + gemma.pieceToId[piece] = id; + } + + gemma.idToPiece.resize(maxId + 1); + for (const auto& kv : gemma.pieceToId) + gemma.idToPiece[kv.second] = kv.first; + + cv::FileNode merges_node = model_node["merges"]; + if (!merges_node.empty()) { + uint32_t rank = 0; + for (auto it = merges_node.begin(); it != merges_node.end(); ++it) { + cv::FileNode entry = *it; + if (static_cast(entry.size()) != 2) { + ++rank; + continue; + } + std::string a, b; + entry[0] >> a; + entry[1] >> b; + gemma.addMerge(a, b, rank); + ++rank; + } + } + + std::unordered_set special; + cv::FileNode added = fs["added_tokens"]; + if (!added.empty()) { + for (auto it = added.begin(); it != added.end(); ++it) { + cv::FileNode t = *it; + int id = -1; t["id"] >> id; + std::string content; t["content"] >> content; + bool is_special = false; t["special"] >> is_special; + if (id >= 0 && !content.empty()) { + gemma.specialToId[content] = id; + gemma.idToSpecial[id] = content; + // All added tokens bypass BPE (matching HuggingFace behavior), + // not just those marked special. + special.insert(content); + if (outSpecial) outSpecial->insert(content); + } + } + } + + return makePtr(std::move(gemma), std::move(special)); +} + static void registerDefaultTokenizers() { auto& reg = tokenizerRegistry(); if (reg.find("BPE") == reg.end()) { @@ -74,6 +167,14 @@ static void registerDefaultTokenizers() { return makePtr(std::move(core), std::move(special)); }; } + + if (reg.find("Gemma") == reg.end()) { + reg["Gemma"] = [](const FileStorage& /*cfg*/, const std::string& dir) -> Ptr { + std::string tok_json = dir + "tokenizer.json"; + std::unordered_set special; + return buildGemmaFromJson(tok_json, &special); + }; + } } Tokenizer::Tokenizer() : impl_(nullptr) {} @@ -174,7 +275,7 @@ Tokenizer Tokenizer::load(const std::string& model_config) { auto it = reg.find(methodType); if (it == reg.end()) CV_Error(cv::Error::StsError, - "Unsupported tokenizer method: '" + methodType + "'. Supported: BPE"); + "Unsupported tokenizer method: '" + methodType + "'. Supported: BPE, Gemma"); Tokenizer tok; tok.impl_ = it->second(cfg, dir); diff --git a/modules/dnn/test/test_tokenizer.cpp b/modules/dnn/test/test_tokenizer.cpp index 3daee402f7..ad0965f94f 100644 --- a/modules/dnn/test/test_tokenizer.cpp +++ b/modules/dnn/test/test_tokenizer.cpp @@ -151,4 +151,50 @@ TEST(Tokenizer_BPE, Tokenizer_Qwen2_5_Roundtrip) { } } + +TEST(Tokenizer_Gemma, Tokenizer_Gemma3_English) { + std::string model = _tf("gemma3/config.json"); + Tokenizer tok = Tokenizer::load(model); + EXPECT_EQ(tok.encode("Hello world"), (std::vector{9259, 1902})); +} + +TEST(Tokenizer_Gemma, Tokenizer_Gemma3_Phrase) { + std::string model = _tf("gemma3/config.json"); + Tokenizer tok = Tokenizer::load(model); + EXPECT_EQ(tok.encode("the quick brown fox"), + (std::vector{1437, 3823, 8864, 37423})); +} + +TEST(Tokenizer_Gemma, Tokenizer_Gemma3_Mixed) { + std::string model = _tf("gemma3/config.json"); + Tokenizer tok = Tokenizer::load(model); + EXPECT_EQ(tok.encode("OpenCV"), (std::vector{7084, 20741})); +} + +TEST(Tokenizer_Gemma, Tokenizer_Gemma3_Numbers) { + std::string model = _tf("gemma3/config.json"); + Tokenizer tok = Tokenizer::load(model); + EXPECT_EQ(tok.encode("2024"), (std::vector{236778, 236771, 236778, 236812})); +} + +TEST(Tokenizer_Gemma, Tokenizer_Gemma3_SpecialTokens) { + std::string model = _tf("gemma3/config.json"); + Tokenizer tok = Tokenizer::load(model); + EXPECT_EQ(tok.encode("Hello"), (std::vector{2, 9259, 1})); +} + +TEST(Tokenizer_Gemma, Tokenizer_Gemma3_Roundtrip) { + std::string model = _tf("gemma3/config.json"); + Tokenizer tok = Tokenizer::load(model); + std::vector cases = { + "Hello world", + "the quick brown fox", + "OpenCV", + "hello world", + }; + for (const auto& text : cases) { + EXPECT_EQ(tok.decode(tok.encode(text)), text); + } +} + }} diff --git a/samples/dnn/gemma3_inference.py b/samples/dnn/gemma3_inference.py new file mode 100644 index 0000000000..c46a7cc30b --- /dev/null +++ b/samples/dnn/gemma3_inference.py @@ -0,0 +1,105 @@ +# This file is part of OpenCV project. +# It is subject to the license terms in the LICENSE file found in the top-level directory +# of this distribution and at http://opencv.org/license.html. +# Copyright (C) 2025, BigVision LLC, all rights reserved. +# Third party copyrights are property of their respective owners. + +''' +This is a sample script to run Gemma3 inference in OpenCV using ONNX model. +The script loads the Gemma3 model and runs inference on a given prompt using +the Gemma3 chat format ( / special tokens). + +Model: https://huggingface.co/google/gemma-3-1b-it + +Exporting Gemma3 model to ONNX: + +1. Install the required dependencies: + + pip install optimum[exporters] torch transformers + +2. Export the model to ONNX: + + optimum-cli export onnx --model google/gemma-3-1b-it --task causal-lm gemma3_instruct_onnx/ + + +Run the script: +1. Install the required dependencies: + + pip install numpy + +2. Run the script: + + python gemma3_inference.py --model= \ + --tokenizer_path= \ + --prompt="What is OpenCV?" + + The tokenizer_path should point to an OpenCV-format config.json (e.g., from + opencv_extra/testdata/dnn/llm/gemma3/config.json), NOT the HuggingFace tokenizer_config.json. +''' + +import numpy as np +import argparse +import cv2 as cv + +def parse_args(): + parser = argparse.ArgumentParser(description='Use this script to run Gemma3 inference in OpenCV', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--model', type=str, required=True, help='Path to Gemma3 ONNX model file.') + parser.add_argument('--tokenizer_path', type=str, required=True, help='Path to Gemma3 tokenizer config.json.') + parser.add_argument('--prompt', type=str, default='What is OpenCV?', help='User prompt.') + parser.add_argument('--max_new_tokens', type=int, default=64, help='Maximum number of new tokens to generate.') + parser.add_argument('--seed', type=int, default=0, help='Random seed.') + return parser.parse_args() + +def build_gemma3_prompt(user_prompt): + '''Wrap user prompt in Gemma3 chat format.''' + return 'user\n' + user_prompt + '\nmodel\n' + +def gemma3_inference(net, prompt, max_new_tokens, tokenizer): + + print("Inferencing Gemma3 model...") + + tokens = tokenizer.encode(prompt) + # Prepend BOS token (id=2) as required by Gemma3 + tokens = [2] + list(tokens) + tokens = np.array(tokens, dtype=np.int64).reshape(1, -1) + + # Gemma3 special token IDs + eos_id = 1 # + eot_id = 106 # + stop_ids = (eos_id, eot_id) + + for _ in range(max_new_tokens): + seq_len = tokens.shape[1] + attention_mask = np.ones((1, seq_len), dtype=np.int64) + + net.setInput(tokens, 'input_ids') + net.setInput(attention_mask, 'attention_mask') + logits = net.forward() # (1, seq_len, vocab_size) + logits = logits[:, -1, :] # take last token logits + + new_id = int(np.argmax(logits.reshape(-1))) + tokens = np.concatenate((tokens, np.array([[new_id]], dtype=np.int64)), axis=1) + + if new_id in stop_ids: + break + + return tokens + +if __name__ == '__main__': + + args = parse_args() + np.random.seed(args.seed) + + print("Preparing Gemma3 model...") + tokenizer = cv.dnn.Tokenizer.load(args.tokenizer_path) + + net = cv.dnn.readNetFromONNX(args.model, cv.dnn.ENGINE_NEW) + + gemma3_prompt = build_gemma3_prompt(args.prompt) + print(f"Prompt:\n{gemma3_prompt}") + + prompt_len = len(tokenizer.encode(gemma3_prompt)) + 1 # +1 for BOS token + tokens = gemma3_inference(net, gemma3_prompt, args.max_new_tokens, tokenizer) + response = tokenizer.decode(tokens[0][prompt_len:].tolist()) + print(f"Response:\n{response}")