| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007 |
- package controllers
- import (
- "bytes"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "image"
- "image/color"
- "image/draw"
- _ "image/gif"
- "image/jpeg"
- "image/png"
- "io"
- "net/http"
- "net/url"
- "os"
- "shudao-chat-go/models"
- "shudao-chat-go/utils"
- "strings"
- "time"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/credentials"
- "github.com/aws/aws-sdk-go/aws/session"
- "github.com/aws/aws-sdk-go/service/s3"
- "github.com/beego/beego/v2/server/web"
- "github.com/fogleman/gg"
- )
- type HazardController struct {
- web.Controller
- }
- // 图片压缩配置 - 使用shudaooss.go中的配置
- type HazardRequest struct {
- //场景名称
- SceneName string `json:"scene_name"`
- //图片
- Image string `json:"image"`
- // 用户ID、手机号、用户姓名从token中获取
- //日期
- Date string `json:"date"`
- }
- // YOLOResponse 定义YOLO响应结构
- type YOLOResponse struct {
- Boxes [][]float64 `json:"boxes"`
- Labels []string `json:"labels"`
- Scores []float64 `json:"scores"`
- ModelType string `json:"model_type"`
- }
- // HazardResponse 定义隐患识别响应结构
- type HazardResponse struct {
- Code int `json:"code"`
- Msg string `json:"msg"`
- Data map[string]interface{} `json:"data"`
- }
- // 隐患识别
- func (c *HazardController) Hazard() {
- // 从token中获取用户信息
- userInfo, err := utils.GetUserInfoFromContext(c.Ctx.Input.GetData("userInfo"))
- if err != nil {
- c.Data["json"] = HazardResponse{
- Code: 401,
- Msg: "获取用户信息失败: " + err.Error(),
- }
- c.ServeJSON()
- return
- }
- var requestData HazardRequest
- if err := json.Unmarshal(c.Ctx.Input.RequestBody, &requestData); err != nil {
- c.Data["json"] = HazardResponse{
- Code: 400,
- Msg: "参数错误",
- }
- c.ServeJSON()
- return
- }
- fmt.Println("requestData", requestData)
- // 验证参数
- if requestData.SceneName == "" || requestData.Image == "" {
- c.Data["json"] = HazardResponse{
- Code: 400,
- Msg: "场景名称和图片链接不能为空",
- }
- c.ServeJSON()
- return
- }
- // 从OSS下载图片
- imageData, err := downloadImageFromOSS(requestData.Image)
- if err != nil {
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "下载图片失败: " + err.Error(),
- }
- c.ServeJSON()
- return
- }
- // 验证图片数据
- if len(imageData) == 0 {
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "下载的图片数据为空",
- }
- c.ServeJSON()
- return
- }
- fmt.Printf("下载图片成功,大小: %d 字节\n", len(imageData))
- // 将图片转换为base64
- imageBase64 := base64.StdEncoding.EncodeToString(imageData)
- // 获取YOLO API配置
- yoloBaseURL, err := web.AppConfig.String("yolo_base_url")
- if err != nil || yoloBaseURL == "" {
- yoloBaseURL = "http://172.16.35.50:18080" // 默认值
- }
- // 构建YOLO请求
- yoloRequest := map[string]interface{}{
- "modeltype": requestData.SceneName,
- "image": imageBase64,
- "conf_threshold": 0.5, // 默认置信度
- }
- jsonData, err := json.Marshal(yoloRequest)
- if err != nil {
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "序列化请求失败: " + err.Error(),
- }
- c.ServeJSON()
- return
- }
- // 调用YOLO API
- resp, err := http.Post(
- fmt.Sprintf("%s/predict", yoloBaseURL),
- "application/json",
- bytes.NewBuffer(jsonData),
- )
- if err != nil {
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "调用YOLO API失败: " + err.Error(),
- }
- c.ServeJSON()
- return
- }
- defer resp.Body.Close()
- // 读取响应
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "读取YOLO响应失败: " + err.Error(),
- }
- c.ServeJSON()
- return
- }
- fmt.Println("body111", string(body))
- if resp.StatusCode != http.StatusOK {
- c.Data["json"] = HazardResponse{
- Code: resp.StatusCode,
- Msg: "YOLO API返回错误: " + string(body),
- }
- c.ServeJSON()
- return
- }
- // 解析YOLO响应
- var yoloResp YOLOResponse
- if err := json.Unmarshal(body, &yoloResp); err != nil {
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "解析YOLO响应失败: " + err.Error(),
- }
- c.ServeJSON()
- return
- }
- fmt.Println("yoloResp222", yoloResp)
- //如果labels为空,则返回错误
- if len(yoloResp.Labels) == 0 {
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "没有识别到任何隐患",
- }
- c.ServeJSON()
- return
- }
- // 构建返回数据
- detectionResults := make([]map[string]interface{}, 0)
- for i, label := range yoloResp.Labels {
- if i < len(yoloResp.Scores) && i < len(yoloResp.Boxes) {
- result := map[string]interface{}{
- "label": label,
- "score": yoloResp.Scores[i],
- "box": yoloResp.Boxes[i],
- "percent": fmt.Sprintf("%.2f%%", yoloResp.Scores[i]*100),
- }
- detectionResults = append(detectionResults, result)
- }
- }
- // 生成标注后的图片并上传到OSS
- var annotatedImageURL string
- if len(detectionResults) > 0 {
- // 解码原始图片
- fmt.Printf("开始解码图片,数据大小: %d 字节\n", len(imageData))
- // 检查图片格式
- img, format, err := image.Decode(bytes.NewReader(imageData))
- if err != nil {
- fmt.Printf("图片解码失败: %v\n", err)
- fmt.Printf("图片数据前20字节: %x\n", imageData[:min(20, len(imageData))])
- // 尝试手动检测PNG格式并转换
- if len(imageData) >= 8 && string(imageData[:8]) == "\x89PNG\r\n\x1a\n" {
- fmt.Printf("检测到PNG格式,尝试特殊处理...\n")
- // 尝试使用PNG解码器
- img, err = decodePNGImage(imageData)
- if err != nil {
- fmt.Printf("PNG特殊解码也失败: %v\n", err)
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "PNG图片解码失败: " + err.Error() + ",请检查图片是否损坏",
- }
- c.ServeJSON()
- return
- }
- format = "png"
- fmt.Printf("PNG特殊解码成功\n")
- } else {
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "解码图片失败: " + err.Error() + ",请检查图片格式是否支持(JPEG、PNG、GIF等)",
- }
- c.ServeJSON()
- return
- }
- }
- fmt.Printf("图片解码成功,格式: %s,尺寸: %dx%d\n", format, img.Bounds().Dx(), img.Bounds().Dy())
- // 绘制边界框 - 使用token中的用户信息
- annotatedImg := drawBoundingBox(img, yoloResp.Boxes, yoloResp.Labels, yoloResp.Scores, userInfo.Name, userInfo.ContactNumber, requestData.Date)
- // 将标注后的图片编码为JPEG
- var buf bytes.Buffer
- if err := jpeg.Encode(&buf, annotatedImg, &jpeg.Options{Quality: 95}); err != nil {
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "编码标注图片失败: " + err.Error(),
- }
- c.ServeJSON()
- return
- }
- // 生成文件名
- utcNow := time.Now().UTC()
- timestamp := utcNow.Unix()
- fileName := fmt.Sprintf("hazard_annotated/%d/%s_%d.jpg",
- utcNow.Year(),
- utcNow.Format("0102"),
- timestamp)
- // 上传标注后的图片到OSS
- annotatedImageURL, err = uploadImageToOSS(buf.Bytes(), fileName)
- if err != nil {
- c.Data["json"] = HazardResponse{
- Code: 500,
- Msg: "上传标注图片到OSS失败: " + err.Error(),
- }
- c.ServeJSON()
- return
- }
- fmt.Printf("标注图片上传成功: %s\n", fileName)
- }
- // fmt.Println("annotatedImageURL", annotatedImageURL)
- // 使用token中的user_id
- user_id := int(userInfo.ID)
- if user_id == 0 {
- user_id = 1
- }
- //查询场景名称
- var scene models.Scene
- models.DB.Where("scene_en_name = ? and is_deleted = ?", requestData.SceneName, 0).First(&scene)
- if scene.ID == 0 {
- c.Data["json"] = map[string]interface{}{
- "statusCode": 500,
- "msg": "场景名称不存在",
- }
- c.ServeJSON()
- return
- }
- var tx = models.DB.Begin()
- recognitionRecord := &models.RecognitionRecord{
- OriginalImageUrl: requestData.Image,
- RecognitionImageUrl: annotatedImageURL,
- UserID: user_id,
- Labels: removeDuplicateLabels(yoloResp.Labels), // 存储YOLO识别的标签(去重)
- Title: scene.SceneName,
- TagType: requestData.SceneName,
- SecondScene: "", // 默认空字符串
- ThirdScene: "", // 默认空字符串
- }
- if err := tx.Create(recognitionRecord).Error; err != nil {
- tx.Rollback()
- c.Data["json"] = map[string]interface{}{
- "statusCode": 500,
- "msg": "识别失败: " + err.Error(),
- }
- c.ServeJSON()
- return
- }
- //获取labels对应的二级场景和三级场景
- var thirdSceneNames []string
- // 判断是否为高速公路场景
- isHighwayScene := strings.Contains(scene.SceneName, "运营高速公路")
- //隧道和简支梁
- isTunnelScene := strings.Contains(scene.SceneName, "隧道")
- isSimplySupportedBeamScene := strings.Contains(scene.SceneName, "简支梁")
- fmt.Println("yoloResp.Labels", yoloResp.Labels)
- for _, label := range yoloResp.Labels {
- // 处理标签:对于高速公路场景,需要去掉前缀
- processedLabel := label
- if isHighwayScene || isTunnelScene || isSimplySupportedBeamScene {
- processedLabel = processHighwayLabel(label)
- }
- var secondScene models.SecondScene
- models.DB.Where("second_scene_name = ? and is_deleted = ?", processedLabel, 0).First(&secondScene)
- if secondScene.ID != 0 {
- tx.Create(&models.RecognitionRecordSecondScene{
- RecognitionRecordID: int(recognitionRecord.ID),
- SecondSceneID: int(secondScene.ID),
- })
- // 通过secondScene.ID查询ThirdScene
- var thirdScene []models.ThirdScene
- models.DB.Where("second_scene_id = ? and is_deleted = ?", secondScene.ID, 0).Find(&thirdScene)
- if len(thirdScene) > 0 {
- for _, thirdScene := range thirdScene {
- thirdSceneNames = append(thirdSceneNames, thirdScene.ThirdSceneName)
- }
- }
- }
- }
- //对thirdSceneNames去重
- thirdSceneNames = removeDuplicates(thirdSceneNames)
- // 将三级场景名称数组更新到recognitionRecord的Description
- if len(thirdSceneNames) > 0 {
- // 将数组转换为空格分隔的字符串
- description := strings.Join(thirdSceneNames, " ")
- tx.Model(recognitionRecord).Update("Description", description)
- }
- tx.Commit()
- fmt.Println("thirdSceneNames", thirdSceneNames)
- // 返回成功响应
- c.Data["json"] = HazardResponse{
- Code: 200,
- Msg: "识别成功",
- Data: map[string]interface{}{
- "scene_name": requestData.SceneName,
- "total_detections": len(detectionResults),
- "detections": detectionResults,
- "model_type": yoloResp.ModelType,
- "original_image": requestData.Image,
- "annotated_image": annotatedImageURL, //前端用这个预链接渲染
- "labels": strings.Join(yoloResp.Labels, ", "),
- "third_scenes": thirdSceneNames, //三级场景名称数组
- },
- }
- c.ServeJSON()
- }
- // min 返回两个整数中的较小值
- func min(a, b int) int {
- if a < b {
- return a
- }
- return b
- }
- // decodePNGImage 专门解码PNG图片
- func decodePNGImage(imageData []byte) (image.Image, error) {
- // 直接使用PNG解码器
- img, err := png.Decode(bytes.NewReader(imageData))
- if err != nil {
- return nil, fmt.Errorf("PNG解码失败: %v", err)
- }
- return img, nil
- }
- // isImageContentType 检查Content-Type是否是图片类型
- func isImageContentType(contentType string) bool {
- imageTypes := []string{
- "image/jpeg",
- "image/jpg",
- "image/png",
- "image/gif",
- "image/bmp",
- "image/webp",
- "image/tiff",
- }
- for _, imgType := range imageTypes {
- if contentType == imgType {
- return true
- }
- }
- return false
- }
- // downloadImageFromOSS 从OSS链接下载图片(支持代理URL)
- func downloadImageFromOSS(imageURL string) ([]byte, error) {
- fmt.Printf("开始下载图片: %s\n", imageURL)
- // 检查是否是代理URL,如果是则直接使用代理接口
- if strings.Contains(imageURL, "/apiv1/oss/parse/?url=") {
- fmt.Printf("检测到代理URL,直接使用代理接口\n")
- return downloadImageFromProxy(imageURL)
- }
- // 原始OSS URL的处理
- client := &http.Client{
- Timeout: 30 * time.Second,
- }
- resp, err := client.Get(imageURL)
- if err != nil {
- return nil, fmt.Errorf("下载图片失败: %v", err)
- }
- defer resp.Body.Close()
- fmt.Printf("下载响应状态码: %d\n", resp.StatusCode)
- fmt.Printf("响应头: %+v\n", resp.Header)
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("下载图片失败,状态码: %d", resp.StatusCode)
- }
- imageData, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("读取图片数据失败: %v", err)
- }
- fmt.Printf("图片下载完成,大小: %d 字节\n", len(imageData))
- // 检查Content-Type
- contentType := resp.Header.Get("Content-Type")
- if contentType != "" {
- fmt.Printf("图片Content-Type: %s\n", contentType)
- // 检查是否是图片类型
- if !isImageContentType(contentType) {
- fmt.Printf("警告: Content-Type不是图片类型: %s\n", contentType)
- }
- }
- return imageData, nil
- }
- // downloadImageFromProxy 通过代理接口下载图片
- func downloadImageFromProxy(proxyURL string) ([]byte, error) {
- fmt.Printf("通过代理接口下载图片: %s\n", proxyURL)
- // 从代理URL中提取加密的URL参数
- // 格式: /apiv1/oss/parse/?url=<加密字符串>
- parsedURL, err := url.Parse(proxyURL)
- if err != nil {
- return nil, fmt.Errorf("解析代理URL失败: %v", err)
- }
- // 获取url参数(加密的)
- encryptedURL := parsedURL.Query().Get("url")
- if encryptedURL == "" {
- return nil, fmt.Errorf("代理URL中缺少URL参数")
- }
- fmt.Printf("从代理URL提取的加密URL: %s\n", encryptedURL)
- // 解密URL
- originalURL, err := utils.DecryptURL(encryptedURL)
- if err != nil {
- return nil, fmt.Errorf("解密URL失败: %v", err)
- }
- fmt.Printf("解密后的原始URL: %s\n", originalURL)
- // 使用解密后的原始URL下载图片
- client := &http.Client{
- Timeout: 30 * time.Second,
- }
- resp, err := client.Get(originalURL)
- if err != nil {
- return nil, fmt.Errorf("通过原始URL下载图片失败: %v", err)
- }
- defer resp.Body.Close()
- fmt.Printf("原始URL下载响应状态码: %d\n", resp.StatusCode)
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("原始URL下载图片失败,状态码: %d", resp.StatusCode)
- }
- imageData, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("读取原始URL图片数据失败: %v", err)
- }
- fmt.Printf("原始URL图片下载完成,大小: %d 字节\n", len(imageData))
- // 检查Content-Type
- contentType := resp.Header.Get("Content-Type")
- if contentType != "" {
- fmt.Printf("原始URL图片Content-Type: %s\n", contentType)
- // 检查是否是图片类型
- if !isImageContentType(contentType) {
- fmt.Printf("警告: 原始URL Content-Type不是图片类型: %s\n", contentType)
- }
- }
- return imageData, nil
- }
- // drawBoundingBox 在图像上绘制边界框和标签
- func drawBoundingBox(img image.Image, boxes [][]float64, labels []string, scores []float64, username, account, date string) image.Image {
- // 创建可绘制的图像副本
- bounds := img.Bounds()
- drawableImg := image.NewRGBA(bounds)
- draw.Draw(drawableImg, bounds, img, image.Point{}, draw.Src)
- // 在左上角添加logo图片
- drawableImg = addLogoToImage(drawableImg)
- // 添加文字水印 - 传递动态参数
- drawableImg = addTextWatermark(drawableImg, username, account, date)
- // 红色边界框
- red := image.NewUniform(color.RGBA{255, 0, 0, 255})
- for _, box := range boxes {
- if len(box) >= 4 {
- x1, y1, x2, y2 := int(box[0]), int(box[1]), int(box[2]), int(box[3])
- // 绘制边界框
- drawRect(drawableImg, x1, y1, x2, y2, red)
- }
- }
- return drawableImg
- }
- // addLogoToImage 在图片左上角添加logo
- func addLogoToImage(img *image.RGBA) *image.RGBA {
- // 读取本地logo图片
- logoPath := "static/image/1.png"
- logoFile, err := os.Open(logoPath)
- if err != nil {
- fmt.Printf("无法打开logo文件: %v\n", err)
- return img // 如果无法打开logo,返回原图
- }
- defer logoFile.Close()
- // 解码logo图片
- logoImg, err := png.Decode(logoFile)
- if err != nil {
- fmt.Printf("解码logo图片失败: %v\n", err)
- return img // 如果解码失败,返回原图
- }
- // 获取logo图片尺寸
- logoBounds := logoImg.Bounds()
- originalWidth := logoBounds.Dx()
- originalHeight := logoBounds.Dy()
- // 缩小两倍
- logoWidth := originalWidth / 2
- logoHeight := originalHeight / 2
- // 设置logo在左上角的位置(留一些边距)
- margin := 10
- logoX := margin
- logoY := margin
- // 确保logo不会超出图片边界
- imgBounds := img.Bounds()
- if logoX+logoWidth > imgBounds.Max.X {
- logoX = imgBounds.Max.X - logoWidth - margin
- if logoX < 0 {
- logoX = 0
- }
- }
- if logoY+logoHeight > imgBounds.Max.Y {
- logoY = imgBounds.Max.Y - logoHeight - margin
- if logoY < 0 {
- logoY = 0
- }
- }
- // 创建缩放后的logo图像
- scaledLogo := image.NewRGBA(image.Rect(0, 0, logoWidth, logoHeight))
- // 使用简单的最近邻缩放算法
- for y := 0; y < logoHeight; y++ {
- for x := 0; x < logoWidth; x++ {
- // 计算原始图像中的对应位置
- srcX := x * 2
- srcY := y * 2
- // 确保不超出原始图像边界
- if srcX < originalWidth && srcY < originalHeight {
- scaledLogo.Set(x, y, logoImg.At(srcX, srcY))
- }
- }
- }
- // 将缩放后的logo绘制到图片上
- logoRect := image.Rect(logoX, logoY, logoX+logoWidth, logoY+logoHeight)
- draw.Draw(img, logoRect, scaledLogo, image.Point{}, draw.Over)
- fmt.Printf("成功在位置(%d,%d)添加logo,原始尺寸: %dx%d,缩放后尺寸: %dx%d\n", logoX, logoY, originalWidth, originalHeight, logoWidth, logoHeight)
- return img
- }
- // drawRect 绘制矩形
- func drawRect(img *image.RGBA, x1, y1, x2, y2 int, color *image.Uniform) {
- bounds := img.Bounds()
- // 确保坐标在图像边界内
- if x1 < bounds.Min.X {
- x1 = bounds.Min.X
- }
- if y1 < bounds.Min.Y {
- y1 = bounds.Min.Y
- }
- if x2 >= bounds.Max.X {
- x2 = bounds.Max.X - 1
- }
- if y2 >= bounds.Max.Y {
- y2 = bounds.Max.Y - 1
- }
- // 设置线条粗细(像素数)
- lineThickness := 6
- // 绘制上边(粗线)
- for i := 0; i < lineThickness; i++ {
- y := y1 + i
- if y >= bounds.Max.Y {
- break
- }
- for x := x1; x <= x2; x++ {
- img.Set(x, y, color)
- }
- }
- // 绘制下边(粗线)
- for i := 0; i < lineThickness; i++ {
- y := y2 - i
- if y < bounds.Min.Y {
- break
- }
- for x := x1; x <= x2; x++ {
- img.Set(x, y, color)
- }
- }
- // 绘制左边(粗线)
- for i := 0; i < lineThickness; i++ {
- x := x1 + i
- if x >= bounds.Max.X {
- break
- }
- for y := y1; y <= y2; y++ {
- img.Set(x, y, color)
- }
- }
- // 绘制右边(粗线)
- for i := 0; i < lineThickness; i++ {
- x := x2 - i
- if x < bounds.Min.X {
- break
- }
- for y := y1; y <= y2; y++ {
- img.Set(x, y, color)
- }
- }
- }
- // removeDuplicates 去除字符串数组中的重复元素
- func removeDuplicates(strs []string) []string {
- keys := make(map[string]bool)
- var result []string
- for _, str := range strs {
- if !keys[str] {
- keys[str] = true
- result = append(result, str)
- }
- }
- return result
- }
- // OSS配置信息 - 使用shudaooss.go中的配置
- // uploadImageToOSS 上传图片数据到OSS并返回预签名URL
- func uploadImageToOSS(imageData []byte, fileName string) (string, error) {
- // 压缩图片
- if EnableImageCompression {
- fmt.Printf("开始压缩标注图片到200KB以下...\n")
- compressedBytes, err := compressImage(imageData, MaxImageWidth, MaxImageHeight, 0)
- if err != nil {
- fmt.Printf("图片压缩失败,使用原始图片: %v\n", err)
- // 压缩失败时使用原始图片
- } else {
- fmt.Printf("标注图片压缩完成,最终大小: %.2f KB\n", float64(len(compressedBytes))/1024)
- imageData = compressedBytes
- }
- } else {
- fmt.Printf("图片压缩已禁用,使用原始图片\n")
- }
- // 获取S3会话
- s3Config := &aws.Config{
- Credentials: credentials.NewStaticCredentials(ossAccessKey, ossSecretKey, ""),
- Endpoint: aws.String(ossEndpoint),
- Region: aws.String(ossRegion),
- S3ForcePathStyle: aws.Bool(true),
- }
- sess, err := session.NewSession(s3Config)
- if err != nil {
- return "", fmt.Errorf("创建S3会话失败: %v", err)
- }
- // 创建S3服务
- s3Client := s3.New(sess)
- // 上传图片到S3
- _, err = s3Client.PutObject(&s3.PutObjectInput{
- Bucket: aws.String(ossBucket),
- Key: aws.String(fileName),
- Body: aws.ReadSeekCloser(strings.NewReader(string(imageData))),
- ContentType: aws.String("image/jpeg"),
- ACL: aws.String("public-read"),
- })
- if err != nil {
- return "", fmt.Errorf("上传图片到OSS失败: %v", err)
- }
- // // 生成预签名URL(24小时有效期)
- // req, _ := s3Client.GetObjectRequest(&s3.GetObjectInput{
- // Bucket: aws.String(ossBucket),
- // Key: aws.String(fileName),
- // })
- // presignedURL, err := req.Presign(24 * time.Hour)
- presignedURL := fmt.Sprintf("%s/%s/%s", ossEndpoint, ossBucket, fileName)
- // 使用代理接口包装URL,前端需要显示图片
- proxyURL := utils.GetProxyURL(presignedURL)
- if err != nil {
- return "", fmt.Errorf("生成预签名URL失败: %v", err)
- }
- return proxyURL, nil
- }
- // TestWatermarkFunction 测试文字水印功能的简单函数
- func TestWatermarkFunction() {
- fmt.Println("开始测试文字水印功能...")
- // 读取测试图片
- imagePath := "static/image/1.png"
- imageFile, err := os.Open(imagePath)
- if err != nil {
- fmt.Printf("无法打开测试图片: %v\n", err)
- return
- }
- defer imageFile.Close()
- // 解码图片
- img, err := png.Decode(imageFile)
- if err != nil {
- fmt.Printf("解码测试图片失败: %v\n", err)
- return
- }
- fmt.Printf("成功读取测试图片,尺寸: %dx%d\n", img.Bounds().Dx(), img.Bounds().Dy())
- // 转换为可绘制的RGBA图像
- bounds := img.Bounds()
- drawableImg := image.NewRGBA(bounds)
- draw.Draw(drawableImg, bounds, img, image.Point{}, draw.Src)
- // 添加logo
- fmt.Println("添加logo...")
- drawableImg = addLogoToImage(drawableImg)
- // 添加文字水印
- fmt.Println("添加文字水印...")
- drawableImg = addTextWatermark(drawableImg, "测试用户", "1234", "2025/01/15")
- // 将处理后的图片保存到static/image目录
- outputPath := "static/image/test_output.jpg"
- outputFile, err := os.Create(outputPath)
- if err != nil {
- fmt.Printf("创建输出文件失败: %v\n", err)
- return
- }
- defer outputFile.Close()
- // 编码为JPEG
- if err := jpeg.Encode(outputFile, drawableImg, &jpeg.Options{Quality: 95}); err != nil {
- fmt.Printf("编码图片失败: %v\n", err)
- return
- }
- fmt.Printf("测试完成!处理后的图片已保存到: %s\n", outputPath)
- fmt.Println("请查看图片效果,确认logo和文字水印是否正确显示")
- }
- // addTextWatermark 使用gg库添加45度角的文字水印(改进版)
- func addTextWatermark(img *image.RGBA, username, account, date string) *image.RGBA {
- // 水印文本 - 使用传入的动态数据
- watermarks := []string{username, account, date}
- // 获取图片尺寸
- bounds := img.Bounds()
- width, height := bounds.Max.X, bounds.Max.Y
- // 创建一个新的绘图上下文
- dc := gg.NewContextForImage(img)
- // 设置字体和颜色
- fontSize := 20.0 // 从30调整为20,字体更小
- // 尝试多种字体加载方案
- fontLoaded := false
- // 方案1:优先使用项目中的字体文件
- localFontPaths := []string{
- "static/font/AlibabaPuHuiTi-3-55-Regular.ttf", // 阿里巴巴普惠体(项目字体)
- }
- for _, fontPath := range localFontPaths {
- if err := dc.LoadFontFace(fontPath, fontSize); err == nil {
- fmt.Printf("成功加载本地字体: %s\n", fontPath)
- fontLoaded = true
- break
- }
- }
- // 方案2:如果本地字体失败,尝试使用内置字体
- if !fontLoaded {
- fmt.Printf("本地字体加载失败,尝试使用内置字体\n")
- // 使用空字符串加载默认字体,这在大多数情况下都能工作
- if err := dc.LoadFontFace("", fontSize); err == nil {
- fmt.Printf("成功加载内置默认字体\n")
- fontLoaded = true
- } else {
- fmt.Printf("内置字体加载失败: %v\n", err)
- }
- }
- // 方案3:如果所有字体都失败,使用像素绘制(备用方案)
- if !fontLoaded {
- fmt.Printf("所有字体加载失败,将使用像素绘制方案\n")
- // 这里可以添加像素绘制的备用方案
- }
- // 设置更深的颜色(从灰色改为深灰色)
- dc.SetColor(color.RGBA{R: 120, G: 120, B: 120, A: 120}) // 深灰色,更不透明
- // 设置旋转角度
- angle := gg.Radians(-45) // 设置为-45度
- // 循环绘制水印
- textWidthEstimate := 150.0 // 估算文本宽度(字体变小,间距也相应调整)
- textHeightEstimate := 80.0 // 估算文本高度,作为行间距(字体变小,间距也相应调整)
- // 旋转整个画布来绘制
- dc.Rotate(angle)
- // 计算旋转后的绘制范围
- // 为了覆盖整个图片,我们需要在更大的范围内绘制
- // 简单起见,我们循环一个足够大的范围
- // 这里的x,y是旋转后的坐标
- for y := -float64(height); y < float64(height)*1.5; y += textHeightEstimate {
- for x := -float64(width); x < float64(width)*1.5; x += textWidthEstimate {
- // 每排使用相同的内容:根据y坐标确定使用哪个文本
- rowIndex := int(y/textHeightEstimate) % len(watermarks)
- if rowIndex < 0 {
- rowIndex = (rowIndex%len(watermarks) + len(watermarks)) % len(watermarks)
- }
- text := watermarks[rowIndex]
- dc.DrawString(text, x, y)
- }
- }
- fmt.Printf("成功添加45度角文字水印(改进版:颜色更深,每排内容相同)\n")
- return dc.Image().(*image.RGBA)
- }
- // 前端传值步骤、json文件、封面图过来
- type SaveStepRequest struct {
- AIConversationID uint64 `json:"ai_conversation_id"`
- Step int `json:"step"`
- PPTJsonUrl string `json:"ppt_json_url"`
- CoverImage string `json:"cover_image"`
- PPTJsonContent string `json:"ppt_json_content"`
- }
- func (c *HazardController) SaveStep() {
- var requestData SaveStepRequest
- if err := json.Unmarshal(c.Ctx.Input.RequestBody, &requestData); err != nil {
- c.Data["json"] = map[string]interface{}{
- "statusCode": 400,
- "msg": "请求数据解析失败",
- }
- c.ServeJSON()
- return
- }
- tx := models.DB.Begin()
- //更新到ai_conversation表
- if err := tx.Model(&models.AIConversation{}).Where("id = ?", requestData.AIConversationID).Updates(map[string]interface{}{
- "step": requestData.Step,
- "ppt_json_url": requestData.PPTJsonUrl,
- "cover_image": requestData.CoverImage,
- "ppt_json_content": requestData.PPTJsonContent,
- }).Error; err != nil {
- tx.Rollback()
- c.Data["json"] = map[string]interface{}{
- "statusCode": 500,
- "msg": "更新步骤失败",
- }
- c.ServeJSON()
- return
- }
- tx.Commit()
- c.Data["json"] = map[string]interface{}{
- "statusCode": 200,
- "msg": "success",
- }
- c.ServeJSON()
- }
- // removeDuplicateLabels 去除重复的标签并返回逗号分隔的字符串
- func removeDuplicateLabels(labels []string) string {
- if len(labels) == 0 {
- return ""
- }
- // 使用map来去重
- labelMap := make(map[string]bool)
- var uniqueLabels []string
- for _, label := range labels {
- // 去除前后空格
- label = strings.TrimSpace(label)
- if label != "" && !labelMap[label] {
- labelMap[label] = true
- uniqueLabels = append(uniqueLabels, label)
- }
- }
- return strings.Join(uniqueLabels, ", ")
- }
- // processHighwayLabel 处理高速公路场景的标签
- // 例如:"绿化_路侧植株_路侧植株" -> "路侧植株_路侧植株"
- // 去掉前缀(第一个下划线及其之前的内容)
- func processHighwayLabel(label string) string {
- // 查找第一个下划线的位置
- underscoreIndex := strings.Index(label, "_")
- if underscoreIndex == -1 {
- // 如果没有下划线,直接返回原标签
- return label
- }
- // 返回从下划线之后的内容
- return label[underscoreIndex+1:]
- }
|