SpringBoot对接火山引擎大模型api实现图片识别与分析

news2025/4/17 12:08:47

文章目录

  • 一、前言
  • 二、创建应用
  • 三、后端
    • 1.SDK集成
    • 2.调用Rest API
  • 四、前端

一、前言

Spring AI实战初体验——实现可切换模型AI聊天助手-CSDN博客

在这里插入图片描述

如上,在上一篇博客,我们已经实现了spring ai对接本地大模型实现了聊天机器人,但是目前有个新需求:

  • 上传某场所的图片,通过AI进行分析,描述图片里的内容以及存在的安全隐患
  • 进一步通过AI分析场所的安全隐患如何治理,需要依据法律法规(联网)分析

最终效果如下所示:

在这里插入图片描述

在这里插入图片描述

由于目前了解到的本地大模型都无法实现上述的需求,于是这次借助了火山引擎平台来实现

https://console.volcengine.com/ark/

火山引擎目前新用户会赠送每个模型50万token的体验量,对于学习、测试用还是足够的

如下所示,本次对接的模型有 doubao-vision-pro(图片识别)deepseek-v3(联网分析)

在这里插入图片描述

整体的逻辑:

  1. 先传入图片到doubao模型,分析图片里的场所和存在的隐患
  2. 然后将1分析的文字结果传到deepseek-v3模型联网结合法律法规分析隐患的整改措施

二、创建应用

https://console.volcengine.com/ark/

如下所示,创建2个零代码应用

在这里插入图片描述

在这里插入图片描述

  1. 图片识别

    在这里插入图片描述

  2. 联网分析

    在这里插入图片描述

三、后端

1.SDK集成

如下图所示,火山引擎里部分模型像deepseek-v3是可以直接集成SDK来对接的

在这里插入图片描述

代码示例

@RestController
@RequestMapping("/huoShan")
public class HuoShanController {
    private final ArkService service;
    private final String imageAnalyzeBotId;

    public HuoShanController(@Value("${ai.ark.apiKey}") String apiKey, @Value("${ai.ark.base-url}") String baseUrl, @Value("${ai.ark.image-analyze-botId}") String imageAnalyzeBotId) {
        this.imageAnalyzeBotId = imageAnalyzeBotId;
        this.service = ArkService.builder()
                .dispatcher(new Dispatcher())
                .connectionPool(new ConnectionPool(5, 1, TimeUnit.SECONDS))
                .baseUrl(baseUrl)
                .apiKey(apiKey)
                .build();
    }

    @PostMapping("/image/chat")
    public ResponseEntity<String> imageChat(@RequestBody String userMessage) {
        List<ChatMessage> messages = new ArrayList<>();
        messages.add(ChatMessage.builder().role(ChatMessageRole.SYSTEM).content("你是一个对中国法律法规有深入理解的专家").build());
        messages.add(ChatMessage.builder().role(ChatMessageRole.USER).content(userMessage).build());

        BotChatCompletionRequest chatCompletionRequest = BotChatCompletionRequest.builder()
                .botId(imageAnalyzeBotId)
                .messages(messages)
                .build();

        BotChatCompletionResult chatCompletionResult = service.createBotChatCompletion(chatCompletionRequest);
        StringBuilder result = new StringBuilder();
        chatCompletionResult.getChoices().forEach(choice -> result.append(choice.getMessage().getContent()));

        return ResponseEntity.ok(result.toString());
    }
}

相关的apiKey、base-url、botId都可以从火山的API调用指南获取,获取完我们配置在application.yml里就可以从上面的代码获取

2.调用Rest API

有些模型例如doubao-pro-vision就没提供java SDK,所以需要采用直接调用rest api的方式来对接

在这里插入图片描述

代码示例

    @PostMapping("/notStream")
    public Mono<String> imageAnalysis(MultipartFile file) {
        return imageAiService.imageAnalysisNotStream(file);
    }
@Service
@Slf4j
public class ImageAiService {

    @Value("${ai.ark.image-botId}")
    private String MODEL;

    private final WebClient webClient;

    public ImageAiService(@Value("${ai.ark.apiKey}") String apiKey, @Value("${ai.ark.base-url}") String baseUrl) {
        this.webClient = WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(HttpClient.create()))
                .baseUrl(baseUrl)
                .defaultHeader("Authorization", "Bearer "+ apiKey)
                .build();
        
    public Mono<String> imageAnalysisNotStream(MultipartFile file) {
        // 压缩图片并转成 Base64 格式
        String base64 = ImageCompressor.compressImageFileToBase64UnderSize(file, 400, 400, 100);
        if (base64 == null || base64.isEmpty()) {
            log.error("图片压缩失败");
            return Mono.just("图片压缩失败");
        }

        // 构造请求体
        Map<String, Object> body = new HashMap<>();
        body.put("model", MODEL);
        // 非流式返回
        body.put("stream", false);
        body.put("stream_options", Map.of("include_usage", true));

        Map<String, Object> imageContent = Map.of(
                "type", "image_url",
                "image_url", Map.of("url", base64)
        );

        Map<String, Object> message = Map.of(
                "role", "user",
                "content", List.of(imageContent)
        );
        body.put("messages", List.of(message));

        // 调用非流式接口,直接返回拼接后的完整结果字符串
        return webClient.post()
                .uri("/bots/chat/completions")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(body)
                // 此时接口返回的是 JSON 数据,所以指定 JSON 类型
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(String.class)
                .map(responseStr -> {
                    try {
                        // 解析返回结果,取出 assistant 返回的内容
                        JsonNode jsonNode = new ObjectMapper().readTree(responseStr);
                        // 此处根据实际返回结构调整解析逻辑
                        JsonNode contentNode = jsonNode.path("choices")
                                .get(0)
                                .path("message")
                                .path("content");
                        return contentNode.asText();
                    } catch (Exception e) {
                        log.error("解析返回结果异常", e);
                        return "解析返回结果异常";
                    }
                });
    }    
        
    }

注意:

上述调用火山引擎api都是非流式的,如果流式输出就把stream设置成true,再使用Flux类或SseEmitter类去接收返回就行,但是由于我流式输出得到的结果前端进行格式处理时候总是有问题,所以改用了非流式,等完整答案出来后再一次性处理格式化

/**
 * 压缩到不超过100KB的Base64编码
 */
public static String compressImageFileToBase64UnderSize(MultipartFile file, int maxWidth, int maxHeight, int maxSizeKB) {
    try {
        // 读取 MultipartFile 图片
        BufferedImage originalImage = ImageIO.read(file.getInputStream());

        // 按比例缩放图片
        Image scaledImage = originalImage.getScaledInstance(maxWidth, maxHeight, Image.SCALE_SMOOTH);
        BufferedImage resizedImage = new BufferedImage(maxWidth, maxHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = resizedImage.createGraphics();
        g2d.drawImage(scaledImage, 0, 0, null);
        g2d.dispose();

        // 获取JPEG写入器
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
        if (!writers.hasNext()) throw new IllegalStateException("No writers found for jpg");
        ImageWriter writer = writers.next();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        MemoryCacheImageOutputStream output = new MemoryCacheImageOutputStream(baos);
        writer.setOutput(output);

        // 设置初始压缩质量
        float quality = 1.0f;
        byte[] imageBytes;

        do {
            baos.reset();
            ImageWriteParam param = writer.getDefaultWriteParam();
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionQuality(quality);

            writer.write(null, new IIOImage(resizedImage, null, null), param);
            output.flush();
            imageBytes = baos.toByteArray();

            quality -= 0.05f; // 每次降低压缩质量
        } while (imageBytes.length > maxSizeKB * 1024 && quality > 0.05f);

        writer.dispose();
        output.close();

        //System.out.println("Final image size: " + (imageBytes.length / 1024) + " KB, final quality: " + quality);

        return "data:image/jpeg;base64," + Base64.getEncoder().encodeToString(imageBytes);

    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}

注意:

Map<String, Object> imageContent = Map.of(
        "type", "image_url",
        "image_url", Map.of("url", base64)
);

这里的图片可以传递http/https网络地址或者图片的base64编码,由于我想是用电脑本地的文件来测试,所以采用图片转base64编码的方式来传递

四、前端

基于上次的html,新增了图片上传,图片回显,调用新接口等处理

直接放完整代码:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI 聊天</title>
    <style>
        html, body {
            height: 100%;
            width: 100%;
            margin: 0;
            background-color: #f9f9f9;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .container {
            display: flex;
            flex-direction: column;
            height: 90vh;
            max-width: 800px;
            width: 100%;
            margin: auto;
        }

        .chat-container {
            flex: 1;
            display: flex;
            flex-direction: column;
            background: white;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            overflow-y: auto; /* 确保内容超出时显示滚动条 */
            min-height: 0; /* 防止 flex 容器压缩子元素 */
        }

        .chat-container::-webkit-scrollbar {
            width: 8px;
        }

        .chat-container::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 4px;
        }

        .chat-container::-webkit-scrollbar-thumb {
            background: #888;
            border-radius: 4px;
        }

        .chat-container::-webkit-scrollbar-thumb:hover {
            background: #555;
        }
        .ai-message h3 {
            font-size: 1.2em;
            margin-top: 1em;
        }

        .ai-message ul {
            padding-left: 1.5em;
        }

        .ai-message li {
            margin-bottom: 0.5em;
        }

        .ai-message {
            white-space: pre-wrap;
        }
        .loading-spinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #007bff;
            border-radius: 50%;
            width: 30px;
            height: 30px;
            animation: spin 1s linear infinite;
            margin: 10px auto;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        .message {
            padding: 10px 15px;
            border-radius: 15px;
            margin: 5px 0;
            max-width: 80%;
            word-wrap: break-word;
        }

        .user-message {
            background-color: #007bff;
            color: white;
            align-self: flex-end;
        }

        .ai-message {
            background-color: #e5e5e5;
            color: black;
            align-self: flex-start;
        }

        .think-message {
            background-color: #add8e6;
            color: black;
            border-radius: 10px;
            padding: 10px;
            margin: 5px 0;
            max-width: 80%;
            align-self: flex-start;
            font-style: italic;
        }

        .think-content {
            flex: 1; /* 允许内容自由扩展 */
            overflow-y: auto; /* 内容过多时显示滚动条 */
            padding: 5px;
        }

        .think-title {
            font-weight: bold;
            margin-bottom: 5px;
            display: flex;
            align-items: center;
        }

        .toggle-button {
            padding: 5px 10px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            margin-right: 10px;
        }

        .toggle-button:hover {
            background-color: #0056b3;
        }

        .input-container {
            display: flex;
            flex-direction: column;
            padding: 10px;
            background: white;
            box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
        }

        .model-container {
            display: flex;
            align-items: center;
            margin-bottom: 5px;
        }

        .model-label {
            margin-right: 10px;
            font-weight: bold;
        }

        .model-select {
            padding: 5px;
            border-radius: 5px;
            border: 1px solid #ccc;
        }

        .input-box-container {
            display: flex;
            align-items: center;
        }

        .input-box {
            flex: 1;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }

        .send-button, .clear-button, .stop-button {
            padding: 10px 20px;
            margin-left: 10px;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }

        .send-button { background-color: #007bff; }
        .send-button:hover { background-color: #0056b3; }
        .send-button:disabled { background-color: #a0c4ff; cursor: not-allowed; }

        .clear-button { background-color: #dc3545; }
        .clear-button:hover { background-color: #a71d2a; }
        .clear-button:disabled { background-color: #f5a6a6; cursor: not-allowed; }

        .stop-button { background-color: #ff9800; }
        .stop-button:hover { background-color: #e68900; }
        .stop-button:disabled { background-color: #ffb74d; cursor: not-allowed; }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body>
<div class="container">
    <div class="chat-container" id="chatContainer">
        <div class="message ai-message">👋 你好,我是你的 AI 助手!</div>
    </div>
    <div class="input-container">
        <div class="model-container">
            <span class="model-label">选择模型:</span>
            <select id="modelSelect" class="model-select" onchange="changeModel()">
                <option value="deepseek-r1:latest">DeepSeek-R1(推理)</option>
                <option value="qwen:7b">Qwen</option>
                <option value="image-analysis">火山引擎-Doubao(场所图片分析,无记忆)</option>
            </select>
        </div>
        <div class="input-box-container">
            <input id="userInput" class="input-box" placeholder="请输入消息...">
            <input id="imageUpload" type="file" accept="image/jpeg, image/png" style="display: none;" onchange="validateFile()" />
            <button id="sendButton" class="send-button" onclick="handleSend()">发送</button>
            <button id="clearButton" class="clear-button" onclick="clearMemory()">清除上下文</button>
            <button id="stopButton" class="stop-button" onclick="stopAIResponse()">停止回答</button>
        </div>
    </div>
</div>
<script>
    const chatContainer = document.getElementById('chatContainer');
    const userInput = document.getElementById('userInput');
    const modelSelect = document.getElementById('modelSelect');
    const sendButton = document.getElementById('sendButton');
    const clearButton = document.getElementById('clearButton');
    const stopButton = document.getElementById('stopButton');
    let userId = '1';
    let currentModel = modelSelect.value;
    let eventSource = null;

    function validateFile() {
        const fileInput = document.getElementById('imageUpload');
        const file = fileInput.files[0];

        if (file) {
            const fileSize = file.size / 1024; // 文件大小,单位为KB
            const fileType = file.type.toLowerCase();

            // 判断文件大小是否小于200KB,格式是否为JPG或PNG
            if (fileSize > 200) {
                alert('文件大小必须小于200KB。');
                fileInput.value = ''; // 清空选择框
                return;
            }

            if (fileType !== 'image/jpeg' && fileType !== 'image/png') {
                alert('仅允许上传JPG和PNG格式的图片。');
                fileInput.value = ''; // 清空选择框
                return;
            }

        }
    }
    function handleSend() {
        if (currentModel === 'image-analysis') {
            const fileInput = document.getElementById('imageUpload');
            const file = fileInput.files[0];
            if (!file) {
                alert("请选择图片文件");
                return;
            }
            uploadAndAnalyzeImage(file);
        } else {
            sendMessage();
        }
    }

    function uploadAndAnalyzeImage(file) {
        const formData = new FormData();
        formData.append('file', file);

        // 回显图片
        const reader = new FileReader();
        reader.onload = function(e) {
            const imgElement = document.createElement('img');
            imgElement.src = e.target.result;
            imgElement.style.maxWidth = '200px';
            imgElement.style.borderRadius = '10px';
            imgElement.style.margin = '10px 0';

            const userImgMessage = document.createElement('div');
            userImgMessage.classList.add('message', 'user-message');
            userImgMessage.appendChild(imgElement);
            chatContainer.appendChild(userImgMessage);

            toggleAllButtons(false);           // 禁用按钮
            showLoadingSpinner();          // 显示加载动画

            chatContainer.scrollTop = chatContainer.scrollHeight;
        };
        reader.readAsDataURL(file);



        fetch('http://192.168.100.72:8081/image/notStream', {
            method: 'POST',
            body: formData
        })
            .then(response => response.text())
            .then(text => {
                const fixedText = text.replace(/\\n/g, '\n');
                const html = marked.parse(fixedText);

                const aiMessage = document.createElement('div');
                aiMessage.classList.add('message', 'ai-message');
                aiMessage.innerHTML = html;
                chatContainer.appendChild(aiMessage);

                // 添加进一步分析提示与按钮
                const followUp = document.createElement('div');
                followUp.classList.add('message', 'ai-message');
                followUp.innerHTML = `
    <div style="display: flex; align-items: center;">
        <span style="margin-right: 10px;">是否进一步分析风险隐患及对应整改措施?</span>
        <button class="send-button" onclick="startRiskAnalysis(\`${text.replace(/`/g, '\\`')}\`)">是</button>
    </div>
`;
                chatContainer.appendChild(followUp);
            })
            .catch(err => {
                console.error("图片分析请求失败:", err);
                appendMessage("❌ 图片分析失败", "ai-message");
            })
            .finally(() => {
                hideLoadingSpinner();     // 移除加载动画
                toggleAllButtons(true);      // 启用按钮
                chatContainer.scrollTop = chatContainer.scrollHeight;
            });
    }

    function startRiskAnalysis(content) {
        toggleAllButtons(false); // 禁用按钮

        // 显示转圈动画
        showLoadingSpinner();

        fetch('http://192.168.100.72:8081/huoShan/image/chat', {
            method: 'POST',
            headers: {
                'Content-Type': 'text/plain'
            },
            body: content
        })
            .then(response => response.text())  // 获取文本响应
            .then(result => {
                // 隐藏转圈动画
                hideLoadingSpinner()

                // 格式化结果并解析为 HTML
                const fixedText = result.replace(/\\n/g, '\n');  // 解除转义
                const html = marked.parse(fixedText);

                // 创建新的 AI 消息并添加到界面
                const aiMessage = document.createElement('div');
                aiMessage.classList.add('message', 'ai-message');
                aiMessage.innerHTML = html;
                chatContainer.appendChild(aiMessage);

                chatContainer.scrollTop = chatContainer.scrollHeight;  // 滚动到底部
                toggleAllButtons(true); // 启用按钮
            })
            .catch(err => {
                console.error("风险分析请求失败:", err);

                // 隐藏转圈动画并显示失败消息
                hideLoadingSpinner()
                appendMessage("❌ 风险分析请求失败", "ai-message");

                toggleAllButtons(true); // 启用按钮
            });
    }




    function showLoadingSpinner() {
        const spinner = document.createElement('div');
        spinner.id = 'loadingSpinner';
        spinner.className = 'loading-spinner';
        chatContainer.appendChild(spinner);
        chatContainer.scrollTop = chatContainer.scrollHeight;
    }

    function hideLoadingSpinner() {
        const spinner = document.getElementById('loadingSpinner');
        if (spinner) spinner.remove();
    }

    function sendMessage() {
        let message = userInput.value.trim();
        if (!message) return;
        appendMessage(message, 'user-message');
        streamAIResponse(userId, message);
        userInput.value = '';
    }

    function appendMessage(text, type) {
        const messageElement = document.createElement('div');
        messageElement.classList.add('message', type);
        messageElement.textContent = text;
        chatContainer.appendChild(messageElement);
        chatContainer.scrollTop = chatContainer.scrollHeight;
    }

    function streamAIResponse(userId, message) {
        // 先终止可能存在的旧 eventSource
        if (eventSource) {
            eventSource.close();
        }

        eventSource = new EventSource(`http://192.168.100.72:8081/ai/chatStreamWithMemory?userId=${encodeURIComponent(userId)}&message=${encodeURIComponent(message)}&model=${encodeURIComponent(currentModel)}`);

        let aiMessage = null;
        let thinkMode = false;
        let thinkMessage = null;

        eventSource.onmessage = event => {
            let response = event.data;

            if (response.includes('<think>') && currentModel === 'deepseek-r1:latest') {
                thinkMode = true;
                response = response.replace('<think>', '');

                // 创建思考过程气泡
                thinkMessage = document.createElement('div');
                thinkMessage.classList.add('think-message');
                thinkMessage.innerHTML = `
                <div class="think-title">
                    <button class="toggle-button" onclick="toggleThinkMessage(this)">折叠</button>
                    <span class="think-title-text">思考过程:</span>
                </div>
                <div class="think-content" style="display: block;"></div>
            `;
                chatContainer.appendChild(thinkMessage);
            }

            if (thinkMode) {
                const thinkContent = thinkMessage.querySelector('.think-content');
                if (response.includes('</think>')) {
                    response = response.replace('</think>', '');
                    thinkMode = false;
                    aiMessage = document.createElement('div');
                    aiMessage.classList.add('message', 'ai-message');
                    chatContainer.appendChild(aiMessage);
                }
                thinkContent.innerHTML += response;
            } else {
                if (!aiMessage) {
                    aiMessage = document.createElement('div');
                    aiMessage.classList.add('message', 'ai-message');
                    chatContainer.appendChild(aiMessage);
                }
                aiMessage.textContent += response;
            }

            chatContainer.scrollTop = chatContainer.scrollHeight;
        };

        eventSource.onerror = () => {
            eventSource.close();
            toggleButtons(true);
        };

        eventSource.onopen = () => {
            toggleButtons(false);
        };

        eventSource.addEventListener("close", () => {
            toggleButtons(true);
        });
    }
    function toggleAllButtons(enabled) {
        sendButton.disabled = !enabled;
        clearButton.disabled = !enabled;
        stopButton.disabled = !enabled;
    }
    function toggleButtons(enabled) {
        sendButton.disabled = !enabled;
        clearButton.disabled = !enabled;
    }
    function toggleThinkMessage(button) {
        const thinkMessage = button.closest('.think-message');
        const thinkContent = thinkMessage.querySelector('.think-content');

        if (thinkContent.style.display === 'none') {
            thinkContent.style.display = 'block';
            button.textContent = '折叠';
        } else {
            thinkContent.style.display = 'none';
            button.textContent = '展开';
        }
    }

    function stopAIResponse() {
        if (eventSource) {
            eventSource.close();
            eventSource = null;
        }

        fetch(`http://192.168.100.72:8081/ai/stopChat?userId=${userId}`, { method: 'GET' })
            .then(() => appendMessage('AI 回答已停止。', 'ai-message'))
            .catch(error => console.error(error));
    }

    function clearMemory() {
        fetch(`http://192.168.100.72:8081/ai/clearMemory?userId=${userId}`, { method: 'GET' })
            .then(() => appendMessage('上下文已清除。', 'ai-message'))
            .catch(error => console.error(error));
    }

    function changeModel() {
        currentModel = modelSelect.value;
        let currentModelName = modelSelect.options[modelSelect.selectedIndex].text;
        appendMessage(`已切换模型为 ${currentModelName}`, 'ai-message');

        if (currentModel === 'image-analysis') {
            userInput.disabled = true;
            userInput.placeholder = '请选择图片进行分析...';
            sendButton.textContent = '分析图片';
            document.getElementById('imageUpload').style.display = 'block';
        } else {
            userInput.disabled = false;
            userInput.placeholder = '请输入消息...';
            sendButton.textContent = '发送';
            document.getElementById('imageUpload').style.display = 'none';
        }
    }

    userInput.addEventListener('keypress', event => {
        if (event.key === 'Enter') {
            sendMessage();
        }
    });
</script>
</body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2335453.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

单片机方案开发 代写程序/烧录芯片 九齐/应广等 电动玩具 小家电 语音开发

在电子产品设计中&#xff0c;单片机&#xff08;MCU&#xff09;无疑是最重要的组成部分之一。无论是消费电子、智能家居、工业控制&#xff0c;还是可穿戴设备&#xff0c;小家电等&#xff0c;单片机的应用无处不在。 单片机&#xff0c;简而言之&#xff0c;就是将计算机…

ARCGIS PRO 在已建工程地图中添加在线地图

一、手工添加 如图所示&#xff1a; 1、在上方的菜单栏中点击“插入”&#xff0c;选择“连接” 2、新建ArcGIS Server 3、在弹出框中输入在线图集的URL&#xff0c;点击“确定” https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer 4、查看在…

ScholarCopilot:“学术副驾驶“

这里写目录标题 引言&#xff1a;学术写作的痛点与 AI 的曙光ScholarCopilot 的核心武器库&#xff1a;智能生成与精准引用智能文本生成&#xff1a;不止于“下一句”智能引用管理&#xff1a;让引用恰到好处 揭秘背后机制&#xff1a;检索与生成的动态协同快速上手&#xff1a…

MATLAB仿真多相滤波抽取与插值的频谱变化(可视化混叠和镜像)

MATLAB画图仿真多相滤波抽取与插值的频谱变化 可视化多速率信号处理抽取与插值的频谱变化 实信号/复信号 可视化混叠和镜像 目录 前言 一、抽取的基本原理 二、MATLAB仿真抽取运算 三、内插的基本原理 四、MATLAB仿真内插运算 总结 前言 在多速率系统中增加信号采样率的运…

MySQL-存储引擎索引

存储引擎 MySQL体系结构 1). 连接层 最上层是一些客户端和链接服务&#xff0c;包含本地sock 通信和大多数基于客户端/服务端工具实现的类似于 TCP/IP的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程 池的概念&#xff0c;为通过认证安…

图像处理有哪些核心技术?技术发展现状如何?

在数字化信息爆炸的时代&#xff0c;文档图像预处理技术正悄然改变着我们处理文字信息的方式。无论是手持拍摄的收据、扫描仪中的身份证&#xff0c;还是工业机器人采集的复杂文档&#xff0c;预处理技术都在背后默默提升着OCR&#xff08;光学字符识别&#xff09;系统的性能。…

【小沐学GIS】基于C++绘制三维数字地球Earth(QT5、OpenGL、GIS、卫星)第五期

&#x1f37a;三维数字地球系列相关文章如下&#x1f37a;&#xff1a;1【小沐学GIS】基于C绘制三维数字地球Earth&#xff08;OpenGL、glfw、glut&#xff09;第一期2【小沐学GIS】基于C绘制三维数字地球Earth&#xff08;OpenGL、glfw、glut&#xff09;第二期3【小沐学GIS】…

spring cloud OpenFeign 详解:安装配置、客户端负载均衡、声明式调用原理及代码示例

OpenFeign 详解&#xff1a;安装配置、客户端负载均衡、声明式调用原理及代码示例 1. OpenFeign 安装与配置 (1) 依赖管理 <!-- pom.xml 添加以下依赖 --> <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud…

用 Deepseek 写的uniapp血型遗传查询工具

引言 在现代社会中&#xff0c;了解血型遗传规律对于优生优育、医疗健康等方面都有重要意义。本文将介绍如何使用Uniapp开发一个跨平台的血型遗传查询工具&#xff0c;帮助用户预测孩子可能的血型。 一、血型遗传基础知识 人类的ABO血型系统由三个等位基因决定&#xff1a;I…

【眼底辅助诊断开放平台】项目笔记

这是一个标题 任务一前端页面开发&#xff1a;后端接口配置&#xff1a; 任务二自行部署接入服务 日志修改样式和解析MD文档接入服务 Note前端登陆不进去/更改后端api接口304 Not Modifiedlogin.cache.jsonERR_CONNECTION_TIMED_OUT跨域一般提交格式proxy.ts src/coponents 目录…

Java笔记5——面向对象(下)

目录 一、抽象类和接口 1-1、抽象类&#xff08;包含抽象方法的类&#xff09; 1-2、接口 ​编辑​编辑 二、多态 ​编辑 1. 自动类型转换&#xff08;向上转型&#xff09; 示例&#xff1a; 注意&#xff1a; 2. 强制类型转换&#xff08;向下转型&#xff09; 示…

NI的LABVIEW工具安装及卸载步骤说明

一.介绍 最近接到个转交的项目&#xff0c;项目主要作为上位机工具开发&#xff0c;在对接下位机时&#xff0c;有用到NI的labview工具。labview软件是由美国国家仪器&#xff08;NI&#xff09;公司研制开发的一种程序开发环境&#xff0c;主要用于汽车测试、数据采集、芯片测…

[reinforcement learning] 是什么 | 应用场景 | Andrew Barto and Richard Sutton

目录 什么是强化学习&#xff1f; 强化学习的应用场景 广告和推荐 对话系统 强化学习的主流算法 纽约时报&#xff1a;Turing Award Goes to 2 Pioneers of Artificial Intelligence wiki 资料混合&#xff1a;youtube, wiki, github 今天下午上课刷到了不少&#xff0…

[从零开始学数据库] 基本SQL

注意我们的主机就是我们的Mysql数据库服务器 这里我们可以用多个库 SQL分类(核心是字段的CRUD)![](https://i-blog.csdnimg.cn/img_convert/0432d8db050082a49258ba8a606056c7.png) ![](https://i-blog.csdnimg.cn/img_convert/bdf5421c2b83e22beca12da8ca89b654.png) 重点是我…

git 提交标签

Git 提交标签 提交消息格式&#xff1a; <type>: <description> &#xff08;示例&#xff1a;git commit -m "feat: add user login API"&#xff09; 标签适用场景feat新增功能&#xff08;Feature&#xff09;。fix修复 Bug&#xff08;Bug fix&…

关于 Spring Batch 的详细解析及其同类框架的对比分析,以及如何自己设计一个java批处理框架(类似spring batch)的步骤

以下是关于 Spring Batch 的详细解析及其同类框架的对比分析&#xff1a; 一、Spring Batch 核心详解 1. 核心概念 作业&#xff08;Job&#xff09;&#xff1a;批处理任务的顶层容器&#xff0c;由多个步骤&#xff08;Step&#xff09;组成。 步骤&#xff08;Step&#…

【第十三届“泰迪杯”数据挖掘挑战赛】【2025泰迪杯】【论文篇+改进】A题解题全流程(持续更新)

【第十三届“泰迪杯”数据挖掘挑战赛】【2025泰迪杯】【论文篇改进】A题解题全流程&#xff08;持续更新&#xff09; 写在前面&#xff1a; 我是一个人&#xff0c;没有团队&#xff0c;所以出的比较慢&#xff0c;每年只做一次赛题&#xff0c;泰迪杯&#xff0c;我会认真对…

数据结构——哈希详解

数据结构——哈希详解 目录 一、哈希的定义 二、六种哈希函数的构造方法 2.1 除留取余法 2.2 平方取中法 2.3 随机数法 2.4 折叠法 2.5 数字分析法 2.6 直接定值法 三、四种解决哈希冲突的方法 3.1 开放地址法 3.1.1 线性探测法 3.1.2 二次探测法 3.2 链地址法 3…

Spark-SQL核心编程

简介 Hadoop与Spark-SQL的对比 Hadoop在处理结构化数据方面存在局限性&#xff0c;无法有效处理某些类型的数据。 Spark应运而生&#xff0c;特别设计了处理结构化数据的模块&#xff0c;称为Spark SQL&#xff08;原称Shark&#xff09;。 SparkSQL的发展历程&#xff1a; Sp…

Docker 与 Podman常用知识汇总

一、常用命令的对比汇总 1、基础说明 Docker&#xff1a;传统的容器引擎&#xff0c;使用 dockerd 守护进程。 Podman&#xff1a;无守护进程、无root容器引擎&#xff0c;兼容 Docker CLI。 Podman 命令几乎完全兼容 Docker 命令&#xff0c;只需将 docker 替换为 podman。…