| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999 |
- 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中提取原始OSS URL
- // 格式: /apiv1/oss/parse/?url=http://172.16.17.52:8060/...
- parsedURL, err := url.Parse(proxyURL)
- if err != nil {
- return nil, fmt.Errorf("解析代理URL失败: %v", err)
- }
- // 获取url参数
- originalURL := parsedURL.Query().Get("url")
- if originalURL == "" {
- return nil, fmt.Errorf("代理URL中缺少原始URL参数")
- }
- fmt.Printf("从代理URL提取的原始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:]
- }
|