hazard.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  1. package controllers
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "image"
  8. "image/color"
  9. "image/draw"
  10. _ "image/gif"
  11. "image/jpeg"
  12. "image/png"
  13. "io"
  14. "net/http"
  15. "net/url"
  16. "os"
  17. "shudao-chat-go/models"
  18. "shudao-chat-go/utils"
  19. "strings"
  20. "time"
  21. "github.com/aws/aws-sdk-go/aws"
  22. "github.com/aws/aws-sdk-go/aws/credentials"
  23. "github.com/aws/aws-sdk-go/aws/session"
  24. "github.com/aws/aws-sdk-go/service/s3"
  25. "github.com/beego/beego/v2/server/web"
  26. "github.com/fogleman/gg"
  27. )
  28. type HazardController struct {
  29. web.Controller
  30. }
  31. // 图片压缩配置 - 使用shudaooss.go中的配置
  32. type HazardRequest struct {
  33. //场景名称
  34. SceneName string `json:"scene_name"`
  35. //图片
  36. Image string `json:"image"`
  37. // 用户ID、手机号、用户姓名从token中获取
  38. //日期
  39. Date string `json:"date"`
  40. }
  41. // YOLOResponse 定义YOLO响应结构
  42. type YOLOResponse struct {
  43. Boxes [][]float64 `json:"boxes"`
  44. Labels []string `json:"labels"`
  45. Scores []float64 `json:"scores"`
  46. ModelType string `json:"model_type"`
  47. }
  48. // HazardResponse 定义隐患识别响应结构
  49. type HazardResponse struct {
  50. Code int `json:"code"`
  51. Msg string `json:"msg"`
  52. Data map[string]interface{} `json:"data"`
  53. }
  54. // 隐患识别
  55. func (c *HazardController) Hazard() {
  56. // 从token中获取用户信息
  57. userInfo, err := utils.GetUserInfoFromContext(c.Ctx.Input.GetData("userInfo"))
  58. if err != nil {
  59. c.Data["json"] = HazardResponse{
  60. Code: 401,
  61. Msg: "获取用户信息失败: " + err.Error(),
  62. }
  63. c.ServeJSON()
  64. return
  65. }
  66. var requestData HazardRequest
  67. if err := json.Unmarshal(c.Ctx.Input.RequestBody, &requestData); err != nil {
  68. c.Data["json"] = HazardResponse{
  69. Code: 400,
  70. Msg: "参数错误",
  71. }
  72. c.ServeJSON()
  73. return
  74. }
  75. fmt.Println("requestData", requestData)
  76. // 验证参数
  77. if requestData.SceneName == "" || requestData.Image == "" {
  78. c.Data["json"] = HazardResponse{
  79. Code: 400,
  80. Msg: "场景名称和图片链接不能为空",
  81. }
  82. c.ServeJSON()
  83. return
  84. }
  85. // 从OSS下载图片
  86. imageData, err := downloadImageFromOSS(requestData.Image)
  87. if err != nil {
  88. c.Data["json"] = HazardResponse{
  89. Code: 500,
  90. Msg: "下载图片失败: " + err.Error(),
  91. }
  92. c.ServeJSON()
  93. return
  94. }
  95. // 验证图片数据
  96. if len(imageData) == 0 {
  97. c.Data["json"] = HazardResponse{
  98. Code: 500,
  99. Msg: "下载的图片数据为空",
  100. }
  101. c.ServeJSON()
  102. return
  103. }
  104. fmt.Printf("下载图片成功,大小: %d 字节\n", len(imageData))
  105. // 将图片转换为base64
  106. imageBase64 := base64.StdEncoding.EncodeToString(imageData)
  107. // 获取YOLO API配置
  108. yoloBaseURL := utils.GetYOLOBaseURL()
  109. // 构建YOLO请求
  110. yoloRequest := map[string]interface{}{
  111. "modeltype": requestData.SceneName,
  112. "image": imageBase64,
  113. "conf_threshold": 0.5, // 默认置信度
  114. }
  115. jsonData, err := json.Marshal(yoloRequest)
  116. if err != nil {
  117. c.Data["json"] = HazardResponse{
  118. Code: 500,
  119. Msg: "序列化请求失败: " + err.Error(),
  120. }
  121. c.ServeJSON()
  122. return
  123. }
  124. // 调用YOLO API
  125. resp, err := http.Post(
  126. fmt.Sprintf("%s/predict", yoloBaseURL),
  127. "application/json",
  128. bytes.NewBuffer(jsonData),
  129. )
  130. if err != nil {
  131. c.Data["json"] = HazardResponse{
  132. Code: 500,
  133. Msg: "调用YOLO API失败: " + err.Error(),
  134. }
  135. c.ServeJSON()
  136. return
  137. }
  138. defer resp.Body.Close()
  139. // 读取响应
  140. body, err := io.ReadAll(resp.Body)
  141. if err != nil {
  142. c.Data["json"] = HazardResponse{
  143. Code: 500,
  144. Msg: "读取YOLO响应失败: " + err.Error(),
  145. }
  146. c.ServeJSON()
  147. return
  148. }
  149. fmt.Println("body111", string(body))
  150. if resp.StatusCode != http.StatusOK {
  151. c.Data["json"] = HazardResponse{
  152. Code: resp.StatusCode,
  153. Msg: "YOLO API返回错误: " + string(body),
  154. }
  155. c.ServeJSON()
  156. return
  157. }
  158. // 解析YOLO响应
  159. var yoloResp YOLOResponse
  160. if err := json.Unmarshal(body, &yoloResp); err != nil {
  161. c.Data["json"] = HazardResponse{
  162. Code: 500,
  163. Msg: "解析YOLO响应失败: " + err.Error(),
  164. }
  165. c.ServeJSON()
  166. return
  167. }
  168. fmt.Println("yoloResp222", yoloResp)
  169. //如果labels为空,则返回错误
  170. if len(yoloResp.Labels) == 0 {
  171. c.Data["json"] = HazardResponse{
  172. Code: 500,
  173. Msg: "当前照片与场景不匹配",
  174. }
  175. c.ServeJSON()
  176. return
  177. }
  178. // 构建返回数据
  179. detectionResults := make([]map[string]interface{}, 0)
  180. for i, label := range yoloResp.Labels {
  181. if i < len(yoloResp.Scores) && i < len(yoloResp.Boxes) {
  182. result := map[string]interface{}{
  183. "label": label,
  184. "score": yoloResp.Scores[i],
  185. "box": yoloResp.Boxes[i],
  186. "percent": fmt.Sprintf("%.2f%%", yoloResp.Scores[i]*100),
  187. }
  188. detectionResults = append(detectionResults, result)
  189. }
  190. }
  191. // 生成标注后的图片并上传到OSS
  192. var annotatedImageURL string
  193. if len(detectionResults) > 0 {
  194. // 解码原始图片
  195. fmt.Printf("开始解码图片,数据大小: %d 字节\n", len(imageData))
  196. // 检查图片格式
  197. img, format, err := image.Decode(bytes.NewReader(imageData))
  198. if err != nil {
  199. fmt.Printf("图片解码失败: %v\n", err)
  200. fmt.Printf("图片数据前20字节: %x\n", imageData[:min(20, len(imageData))])
  201. // 尝试手动检测PNG格式并转换
  202. if len(imageData) >= 8 && string(imageData[:8]) == "\x89PNG\r\n\x1a\n" {
  203. fmt.Printf("检测到PNG格式,尝试特殊处理...\n")
  204. // 尝试使用PNG解码器
  205. img, err = decodePNGImage(imageData)
  206. if err != nil {
  207. fmt.Printf("PNG特殊解码也失败: %v\n", err)
  208. c.Data["json"] = HazardResponse{
  209. Code: 500,
  210. Msg: "PNG图片解码失败: " + err.Error() + ",请检查图片是否损坏",
  211. }
  212. c.ServeJSON()
  213. return
  214. }
  215. format = "png"
  216. fmt.Printf("PNG特殊解码成功\n")
  217. } else {
  218. c.Data["json"] = HazardResponse{
  219. Code: 500,
  220. Msg: "解码图片失败: " + err.Error() + ",请检查图片格式是否支持(JPEG、PNG、GIF等)",
  221. }
  222. c.ServeJSON()
  223. return
  224. }
  225. }
  226. fmt.Printf("图片解码成功,格式: %s,尺寸: %dx%d\n", format, img.Bounds().Dx(), img.Bounds().Dy())
  227. // 绘制边界框 - 使用token中的用户信息
  228. annotatedImg := drawBoundingBox(img, yoloResp.Boxes, yoloResp.Labels, yoloResp.Scores, userInfo.Name, userInfo.ContactNumber, requestData.Date)
  229. // 将标注后的图片编码为JPEG
  230. var buf bytes.Buffer
  231. if err := jpeg.Encode(&buf, annotatedImg, &jpeg.Options{Quality: 95}); err != nil {
  232. c.Data["json"] = HazardResponse{
  233. Code: 500,
  234. Msg: "编码标注图片失败: " + err.Error(),
  235. }
  236. c.ServeJSON()
  237. return
  238. }
  239. // 生成文件名
  240. utcNow := time.Now().UTC()
  241. timestamp := utcNow.Unix()
  242. fileName := fmt.Sprintf("hazard_annotated/%d/%s_%d.jpg",
  243. utcNow.Year(),
  244. utcNow.Format("0102"),
  245. timestamp)
  246. // 上传标注后的图片到OSS
  247. annotatedImageURL, err = uploadImageToOSS(buf.Bytes(), fileName)
  248. if err != nil {
  249. c.Data["json"] = HazardResponse{
  250. Code: 500,
  251. Msg: "上传标注图片到OSS失败: " + err.Error(),
  252. }
  253. c.ServeJSON()
  254. return
  255. }
  256. fmt.Printf("标注图片上传成功: %s\n", fileName)
  257. }
  258. // fmt.Println("annotatedImageURL", annotatedImageURL)
  259. // 使用token中的user_id
  260. user_id := int(userInfo.ID)
  261. if user_id == 0 {
  262. user_id = 1
  263. }
  264. //查询场景名称
  265. var scene models.Scene
  266. models.DB.Where("scene_en_name = ? and is_deleted = ?", requestData.SceneName, 0).First(&scene)
  267. if scene.ID == 0 {
  268. c.Data["json"] = map[string]interface{}{
  269. "statusCode": 500,
  270. "msg": "场景名称不存在",
  271. }
  272. c.ServeJSON()
  273. return
  274. }
  275. var tx = models.DB.Begin()
  276. recognitionRecord := &models.RecognitionRecord{
  277. OriginalImageUrl: requestData.Image,
  278. RecognitionImageUrl: annotatedImageURL,
  279. UserID: user_id,
  280. Labels: removeDuplicateLabels(yoloResp.Labels), // 存储YOLO识别的标签(去重)
  281. Title: scene.SceneName,
  282. TagType: requestData.SceneName,
  283. SecondScene: "", // 默认空字符串
  284. ThirdScene: "", // 默认空字符串
  285. }
  286. if err := tx.Create(recognitionRecord).Error; err != nil {
  287. tx.Rollback()
  288. c.Data["json"] = map[string]interface{}{
  289. "statusCode": 500,
  290. "msg": "识别失败: " + err.Error(),
  291. }
  292. c.ServeJSON()
  293. return
  294. }
  295. //获取labels对应的二级场景和三级场景
  296. var thirdSceneNames []string
  297. var displayLabels []string
  298. elementHazards := make(map[string][]string)
  299. fmt.Println("yoloResp.Labels", yoloResp.Labels)
  300. for _, label := range yoloResp.Labels {
  301. processedLabel := normalizeSceneLabel(label, scene.SceneName)
  302. displayLabels = append(displayLabels, processedLabel)
  303. var secondScene models.SecondScene
  304. models.DB.Where("second_scene_name = ? and is_deleted = ?", processedLabel, 0).First(&secondScene)
  305. if secondScene.ID != 0 {
  306. tx.Create(&models.RecognitionRecordSecondScene{
  307. RecognitionRecordID: int(recognitionRecord.ID),
  308. SecondSceneID: int(secondScene.ID),
  309. })
  310. // 通过secondScene.ID查询ThirdScene
  311. var thirdScene []models.ThirdScene
  312. models.DB.Where("second_scene_id = ? and is_deleted = ?", secondScene.ID, 0).Find(&thirdScene)
  313. if len(thirdScene) > 0 {
  314. var currentElementHazards []string
  315. for _, thirdScene := range thirdScene {
  316. thirdSceneNames = append(thirdSceneNames, thirdScene.ThirdSceneName)
  317. currentElementHazards = append(currentElementHazards, thirdScene.ThirdSceneName)
  318. }
  319. elementHazards[processedLabel] = removeDuplicates(
  320. append(elementHazards[processedLabel], currentElementHazards...),
  321. )
  322. } else if _, exists := elementHazards[processedLabel]; !exists {
  323. elementHazards[processedLabel] = []string{}
  324. }
  325. }
  326. }
  327. //对thirdSceneNames去重
  328. thirdSceneNames = removeDuplicates(thirdSceneNames)
  329. displayLabels = removeDuplicates(displayLabels)
  330. // 将三级场景名称数组更新到recognitionRecord的Description
  331. if len(thirdSceneNames) > 0 {
  332. // 将数组转换为空格分隔的字符串
  333. description := strings.Join(thirdSceneNames, " ")
  334. tx.Model(recognitionRecord).Update("Description", description)
  335. }
  336. tx.Commit()
  337. fmt.Println("thirdSceneNames", thirdSceneNames)
  338. // 返回成功响应
  339. c.Data["json"] = HazardResponse{
  340. Code: 200,
  341. Msg: "识别成功",
  342. Data: map[string]interface{}{
  343. "scene_name": requestData.SceneName,
  344. "total_detections": len(detectionResults),
  345. "detections": detectionResults,
  346. "model_type": yoloResp.ModelType,
  347. "original_image": requestData.Image,
  348. "annotated_image": annotatedImageURL, //前端用这个预链接渲染
  349. "labels": strings.Join(yoloResp.Labels, ", "),
  350. "display_labels": displayLabels,
  351. "third_scenes": thirdSceneNames, //三级场景名称数组
  352. "element_hazards": elementHazards,
  353. },
  354. }
  355. c.ServeJSON()
  356. }
  357. // min 返回两个整数中的较小值
  358. func min(a, b int) int {
  359. if a < b {
  360. return a
  361. }
  362. return b
  363. }
  364. // decodePNGImage 专门解码PNG图片
  365. func decodePNGImage(imageData []byte) (image.Image, error) {
  366. // 直接使用PNG解码器
  367. img, err := png.Decode(bytes.NewReader(imageData))
  368. if err != nil {
  369. return nil, fmt.Errorf("PNG解码失败: %v", err)
  370. }
  371. return img, nil
  372. }
  373. // isImageContentType 检查Content-Type是否是图片类型
  374. func isImageContentType(contentType string) bool {
  375. imageTypes := []string{
  376. "image/jpeg",
  377. "image/jpg",
  378. "image/png",
  379. "image/gif",
  380. "image/bmp",
  381. "image/webp",
  382. "image/tiff",
  383. }
  384. for _, imgType := range imageTypes {
  385. if contentType == imgType {
  386. return true
  387. }
  388. }
  389. return false
  390. }
  391. // downloadImageFromOSS 从OSS链接下载图片(支持代理URL)
  392. func downloadImageFromOSS(imageURL string) ([]byte, error) {
  393. fmt.Printf("开始下载图片: %s\n", imageURL)
  394. // 检查是否是代理URL,如果是则直接使用代理接口
  395. if strings.Contains(imageURL, "/apiv1/oss/parse/?url=") {
  396. fmt.Printf("检测到代理URL,直接使用代理接口\n")
  397. return downloadImageFromProxy(imageURL)
  398. }
  399. // 原始OSS URL的处理
  400. client := &http.Client{
  401. Timeout: 30 * time.Second,
  402. }
  403. resp, err := client.Get(imageURL)
  404. if err != nil {
  405. return nil, fmt.Errorf("下载图片失败: %v", err)
  406. }
  407. defer resp.Body.Close()
  408. fmt.Printf("下载响应状态码: %d\n", resp.StatusCode)
  409. fmt.Printf("响应头: %+v\n", resp.Header)
  410. if resp.StatusCode != http.StatusOK {
  411. return nil, fmt.Errorf("下载图片失败,状态码: %d", resp.StatusCode)
  412. }
  413. imageData, err := io.ReadAll(resp.Body)
  414. if err != nil {
  415. return nil, fmt.Errorf("读取图片数据失败: %v", err)
  416. }
  417. fmt.Printf("图片下载完成,大小: %d 字节\n", len(imageData))
  418. // 检查Content-Type
  419. contentType := resp.Header.Get("Content-Type")
  420. if contentType != "" {
  421. fmt.Printf("图片Content-Type: %s\n", contentType)
  422. // 检查是否是图片类型
  423. if !isImageContentType(contentType) {
  424. fmt.Printf("警告: Content-Type不是图片类型: %s\n", contentType)
  425. }
  426. }
  427. return imageData, nil
  428. }
  429. // downloadImageFromProxy 通过代理接口下载图片
  430. func downloadImageFromProxy(proxyURL string) ([]byte, error) {
  431. fmt.Printf("通过代理接口下载图片: %s\n", proxyURL)
  432. // 从代理URL中提取加密的URL参数
  433. // 格式: /apiv1/oss/parse/?url=<加密字符串>
  434. parsedURL, err := url.Parse(proxyURL)
  435. if err != nil {
  436. return nil, fmt.Errorf("解析代理URL失败: %v", err)
  437. }
  438. // 获取url参数(加密的)
  439. encryptedURL := parsedURL.Query().Get("url")
  440. if encryptedURL == "" {
  441. return nil, fmt.Errorf("代理URL中缺少URL参数")
  442. }
  443. fmt.Printf("从代理URL提取的加密URL: %s\n", encryptedURL)
  444. // 解密URL
  445. originalURL, err := utils.DecryptURL(encryptedURL)
  446. if err != nil {
  447. return nil, fmt.Errorf("解密URL失败: %v", err)
  448. }
  449. fmt.Printf("解密后的原始URL: %s\n", originalURL)
  450. // 使用解密后的原始URL下载图片
  451. client := &http.Client{
  452. Timeout: 30 * time.Second,
  453. }
  454. resp, err := client.Get(originalURL)
  455. if err != nil {
  456. return nil, fmt.Errorf("通过原始URL下载图片失败: %v", err)
  457. }
  458. defer resp.Body.Close()
  459. fmt.Printf("原始URL下载响应状态码: %d\n", resp.StatusCode)
  460. if resp.StatusCode != http.StatusOK {
  461. return nil, fmt.Errorf("原始URL下载图片失败,状态码: %d", resp.StatusCode)
  462. }
  463. imageData, err := io.ReadAll(resp.Body)
  464. if err != nil {
  465. return nil, fmt.Errorf("读取原始URL图片数据失败: %v", err)
  466. }
  467. fmt.Printf("原始URL图片下载完成,大小: %d 字节\n", len(imageData))
  468. // 检查Content-Type
  469. contentType := resp.Header.Get("Content-Type")
  470. if contentType != "" {
  471. fmt.Printf("原始URL图片Content-Type: %s\n", contentType)
  472. // 检查是否是图片类型
  473. if !isImageContentType(contentType) {
  474. fmt.Printf("警告: 原始URL Content-Type不是图片类型: %s\n", contentType)
  475. }
  476. }
  477. return imageData, nil
  478. }
  479. // drawBoundingBox 在图像上绘制边界框和标签
  480. func drawBoundingBox(img image.Image, boxes [][]float64, labels []string, scores []float64, username, account, date string) image.Image {
  481. // 创建可绘制的图像副本
  482. bounds := img.Bounds()
  483. drawableImg := image.NewRGBA(bounds)
  484. draw.Draw(drawableImg, bounds, img, image.Point{}, draw.Src)
  485. // 在左上角添加logo图片
  486. drawableImg = addLogoToImage(drawableImg)
  487. // 添加文字水印 - 传递动态参数
  488. drawableImg = addTextWatermark(drawableImg, username, account, date)
  489. // 红色边界框
  490. red := image.NewUniform(color.RGBA{255, 0, 0, 255})
  491. for _, box := range boxes {
  492. if len(box) >= 4 {
  493. x1, y1, x2, y2 := int(box[0]), int(box[1]), int(box[2]), int(box[3])
  494. // 绘制边界框
  495. drawRect(drawableImg, x1, y1, x2, y2, red)
  496. }
  497. }
  498. return drawableImg
  499. }
  500. // addLogoToImage 在图片左上角添加logo
  501. func addLogoToImage(img *image.RGBA) *image.RGBA {
  502. // 读取本地logo图片
  503. logoPath := "static/image/1.png"
  504. logoFile, err := os.Open(logoPath)
  505. if err != nil {
  506. fmt.Printf("无法打开logo文件: %v\n", err)
  507. return img // 如果无法打开logo,返回原图
  508. }
  509. defer logoFile.Close()
  510. // 解码logo图片
  511. logoImg, err := png.Decode(logoFile)
  512. if err != nil {
  513. fmt.Printf("解码logo图片失败: %v\n", err)
  514. return img // 如果解码失败,返回原图
  515. }
  516. // 获取logo图片尺寸
  517. logoBounds := logoImg.Bounds()
  518. originalWidth := logoBounds.Dx()
  519. originalHeight := logoBounds.Dy()
  520. // 缩小两倍
  521. logoWidth := originalWidth / 2
  522. logoHeight := originalHeight / 2
  523. // 设置logo在左上角的位置(留一些边距)
  524. margin := 10
  525. logoX := margin
  526. logoY := margin
  527. // 确保logo不会超出图片边界
  528. imgBounds := img.Bounds()
  529. if logoX+logoWidth > imgBounds.Max.X {
  530. logoX = imgBounds.Max.X - logoWidth - margin
  531. if logoX < 0 {
  532. logoX = 0
  533. }
  534. }
  535. if logoY+logoHeight > imgBounds.Max.Y {
  536. logoY = imgBounds.Max.Y - logoHeight - margin
  537. if logoY < 0 {
  538. logoY = 0
  539. }
  540. }
  541. // 创建缩放后的logo图像
  542. scaledLogo := image.NewRGBA(image.Rect(0, 0, logoWidth, logoHeight))
  543. // 使用简单的最近邻缩放算法
  544. for y := 0; y < logoHeight; y++ {
  545. for x := 0; x < logoWidth; x++ {
  546. // 计算原始图像中的对应位置
  547. srcX := x * 2
  548. srcY := y * 2
  549. // 确保不超出原始图像边界
  550. if srcX < originalWidth && srcY < originalHeight {
  551. scaledLogo.Set(x, y, logoImg.At(srcX, srcY))
  552. }
  553. }
  554. }
  555. // 将缩放后的logo绘制到图片上
  556. logoRect := image.Rect(logoX, logoY, logoX+logoWidth, logoY+logoHeight)
  557. draw.Draw(img, logoRect, scaledLogo, image.Point{}, draw.Over)
  558. fmt.Printf("成功在位置(%d,%d)添加logo,原始尺寸: %dx%d,缩放后尺寸: %dx%d\n", logoX, logoY, originalWidth, originalHeight, logoWidth, logoHeight)
  559. return img
  560. }
  561. // drawRect 绘制矩形
  562. func drawRect(img *image.RGBA, x1, y1, x2, y2 int, color *image.Uniform) {
  563. bounds := img.Bounds()
  564. // 确保坐标在图像边界内
  565. if x1 < bounds.Min.X {
  566. x1 = bounds.Min.X
  567. }
  568. if y1 < bounds.Min.Y {
  569. y1 = bounds.Min.Y
  570. }
  571. if x2 >= bounds.Max.X {
  572. x2 = bounds.Max.X - 1
  573. }
  574. if y2 >= bounds.Max.Y {
  575. y2 = bounds.Max.Y - 1
  576. }
  577. // 设置线条粗细(像素数)
  578. lineThickness := 6
  579. // 绘制上边(粗线)
  580. for i := 0; i < lineThickness; i++ {
  581. y := y1 + i
  582. if y >= bounds.Max.Y {
  583. break
  584. }
  585. for x := x1; x <= x2; x++ {
  586. img.Set(x, y, color)
  587. }
  588. }
  589. // 绘制下边(粗线)
  590. for i := 0; i < lineThickness; i++ {
  591. y := y2 - i
  592. if y < bounds.Min.Y {
  593. break
  594. }
  595. for x := x1; x <= x2; x++ {
  596. img.Set(x, y, color)
  597. }
  598. }
  599. // 绘制左边(粗线)
  600. for i := 0; i < lineThickness; i++ {
  601. x := x1 + i
  602. if x >= bounds.Max.X {
  603. break
  604. }
  605. for y := y1; y <= y2; y++ {
  606. img.Set(x, y, color)
  607. }
  608. }
  609. // 绘制右边(粗线)
  610. for i := 0; i < lineThickness; i++ {
  611. x := x2 - i
  612. if x < bounds.Min.X {
  613. break
  614. }
  615. for y := y1; y <= y2; y++ {
  616. img.Set(x, y, color)
  617. }
  618. }
  619. }
  620. // removeDuplicates 去除字符串数组中的重复元素
  621. func removeDuplicates(strs []string) []string {
  622. keys := make(map[string]bool)
  623. var result []string
  624. for _, str := range strs {
  625. if !keys[str] {
  626. keys[str] = true
  627. result = append(result, str)
  628. }
  629. }
  630. return result
  631. }
  632. // OSS配置信息 - 使用shudaooss.go中的配置
  633. // uploadImageToOSS 上传图片数据到OSS并返回预签名URL
  634. func uploadImageToOSS(imageData []byte, fileName string) (string, error) {
  635. // 压缩图片
  636. // if EnableImageCompression {
  637. // fmt.Printf("开始压缩标注图片到200KB以下...\n")
  638. // compressedBytes, err := compressImage(imageData, MaxImageWidth, MaxImageHeight, 0)
  639. // if err != nil {
  640. // fmt.Printf("图片压缩失败,使用原始图片: %v\n", err)
  641. // // 压缩失败时使用原始图片
  642. // } else {
  643. // fmt.Printf("标注图片压缩完成,最终大小: %.2f KB\n", float64(len(compressedBytes))/1024)
  644. // imageData = compressedBytes
  645. // }
  646. // } else {
  647. // fmt.Printf("图片压缩已禁用,使用原始图片\n")
  648. // }
  649. fmt.Printf("后端图片压缩已禁用,直接上传原始图片\n")
  650. // 获取S3会话
  651. s3Config := &aws.Config{
  652. Credentials: credentials.NewStaticCredentials(ossAccessKey, ossSecretKey, ""),
  653. Endpoint: aws.String(ossEndpoint),
  654. Region: aws.String(ossRegion),
  655. S3ForcePathStyle: aws.Bool(true),
  656. }
  657. sess, err := session.NewSession(s3Config)
  658. if err != nil {
  659. return "", fmt.Errorf("创建S3会话失败: %v", err)
  660. }
  661. // 创建S3服务
  662. s3Client := s3.New(sess)
  663. // 上传图片到S3
  664. _, err = s3Client.PutObject(&s3.PutObjectInput{
  665. Bucket: aws.String(ossBucket),
  666. Key: aws.String(fileName),
  667. Body: aws.ReadSeekCloser(strings.NewReader(string(imageData))),
  668. ContentType: aws.String("image/jpeg"),
  669. ACL: aws.String("public-read"),
  670. })
  671. if err != nil {
  672. return "", fmt.Errorf("上传图片到OSS失败: %v", err)
  673. }
  674. // // 生成预签名URL(24小时有效期)
  675. // req, _ := s3Client.GetObjectRequest(&s3.GetObjectInput{
  676. // Bucket: aws.String(ossBucket),
  677. // Key: aws.String(fileName),
  678. // })
  679. // presignedURL, err := req.Presign(24 * time.Hour)
  680. presignedURL := fmt.Sprintf("%s/%s/%s", ossEndpoint, ossBucket, fileName)
  681. // 使用代理接口包装URL,前端需要显示图片
  682. proxyURL := utils.GetProxyURL(presignedURL)
  683. if err != nil {
  684. return "", fmt.Errorf("生成预签名URL失败: %v", err)
  685. }
  686. return proxyURL, nil
  687. }
  688. // addTextWatermark 使用gg库添加45度角的文字水印
  689. func addTextWatermark(img *image.RGBA, username, account, date string) *image.RGBA {
  690. // 水印文本 - 使用传入的动态数据
  691. watermarks := []string{username, account, date}
  692. // 获取图片尺寸
  693. bounds := img.Bounds()
  694. width, height := bounds.Max.X, bounds.Max.Y
  695. // 创建一个新的绘图上下文
  696. dc := gg.NewContextForImage(img)
  697. // 设置字体和颜色
  698. fontSize := 20.0 // 从30调整为20,字体更小
  699. // 尝试多种字体加载方案
  700. fontLoaded := false
  701. // 方案1:优先使用项目中的字体文件
  702. localFontPaths := []string{
  703. "static/font/AlibabaPuHuiTi-3-55-Regular.ttf", // 阿里巴巴普惠体(项目字体)
  704. }
  705. for _, fontPath := range localFontPaths {
  706. if err := dc.LoadFontFace(fontPath, fontSize); err == nil {
  707. fmt.Printf("成功加载本地字体: %s\n", fontPath)
  708. fontLoaded = true
  709. break
  710. }
  711. }
  712. // 方案2:如果本地字体失败,尝试使用内置字体
  713. if !fontLoaded {
  714. fmt.Printf("本地字体加载失败,尝试使用内置字体\n")
  715. // 使用空字符串加载默认字体,这在大多数情况下都能工作
  716. if err := dc.LoadFontFace("", fontSize); err == nil {
  717. fmt.Printf("成功加载内置默认字体\n")
  718. fontLoaded = true
  719. } else {
  720. fmt.Printf("内置字体加载失败: %v\n", err)
  721. }
  722. }
  723. // 方案3:如果所有字体都失败,使用像素绘制(备用方案)
  724. if !fontLoaded {
  725. fmt.Printf("所有字体加载失败,将使用像素绘制方案\n")
  726. // 这里可以添加像素绘制的备用方案
  727. }
  728. // 设置更深的颜色(从灰色改为深灰色)
  729. dc.SetColor(color.RGBA{R: 120, G: 120, B: 120, A: 120}) // 深灰色,更不透明
  730. // 设置旋转角度
  731. angle := gg.Radians(-45) // 设置为-45度
  732. // 循环绘制水印
  733. textWidthEstimate := 150.0 // 估算文本宽度(字体变小,间距也相应调整)
  734. textHeightEstimate := 80.0 // 估算文本高度,作为行间距(字体变小,间距也相应调整)
  735. // 旋转整个画布来绘制
  736. dc.Rotate(angle)
  737. // 计算旋转后的绘制范围
  738. // 为了覆盖整个图片,我们需要在更大的范围内绘制
  739. // 简单起见,我们循环一个足够大的范围
  740. // 这里的x,y是旋转后的坐标
  741. for y := -float64(height); y < float64(height)*1.5; y += textHeightEstimate {
  742. for x := -float64(width); x < float64(width)*1.5; x += textWidthEstimate {
  743. // 每排使用相同的内容:根据y坐标确定使用哪个文本
  744. rowIndex := int(y/textHeightEstimate) % len(watermarks)
  745. if rowIndex < 0 {
  746. rowIndex = (rowIndex%len(watermarks) + len(watermarks)) % len(watermarks)
  747. }
  748. text := watermarks[rowIndex]
  749. dc.DrawString(text, x, y)
  750. }
  751. }
  752. fmt.Printf("成功添加45度角文字水印(改进版:颜色更深,每排内容相同)\n")
  753. return dc.Image().(*image.RGBA)
  754. }
  755. // 前端传值步骤、json文件、封面图过来
  756. type SaveStepRequest struct {
  757. AIConversationID uint64 `json:"ai_conversation_id"`
  758. Step int `json:"step"`
  759. PPTJsonUrl string `json:"ppt_json_url"`
  760. CoverImage string `json:"cover_image"`
  761. PPTJsonContent string `json:"ppt_json_content"`
  762. }
  763. func (c *HazardController) SaveStep() {
  764. var requestData SaveStepRequest
  765. if err := json.Unmarshal(c.Ctx.Input.RequestBody, &requestData); err != nil {
  766. c.Data["json"] = map[string]interface{}{
  767. "statusCode": 400,
  768. "msg": "请求数据解析失败",
  769. }
  770. c.ServeJSON()
  771. return
  772. }
  773. tx := models.DB.Begin()
  774. //更新到ai_conversation表
  775. if err := tx.Model(&models.AIConversation{}).Where("id = ?", requestData.AIConversationID).Updates(map[string]interface{}{
  776. "step": requestData.Step,
  777. "ppt_json_url": requestData.PPTJsonUrl,
  778. "cover_image": requestData.CoverImage,
  779. "ppt_json_content": requestData.PPTJsonContent,
  780. }).Error; err != nil {
  781. tx.Rollback()
  782. c.Data["json"] = map[string]interface{}{
  783. "statusCode": 500,
  784. "msg": "更新步骤失败",
  785. }
  786. c.ServeJSON()
  787. return
  788. }
  789. tx.Commit()
  790. c.Data["json"] = map[string]interface{}{
  791. "statusCode": 200,
  792. "msg": "success",
  793. }
  794. c.ServeJSON()
  795. }
  796. // removeDuplicateLabels 去除重复的标签并返回逗号分隔的字符串
  797. func removeDuplicateLabels(labels []string) string {
  798. if len(labels) == 0 {
  799. return ""
  800. }
  801. // 使用map来去重
  802. labelMap := make(map[string]bool)
  803. var uniqueLabels []string
  804. for _, label := range labels {
  805. // 去除前后空格
  806. label = strings.TrimSpace(label)
  807. if label != "" && !labelMap[label] {
  808. labelMap[label] = true
  809. uniqueLabels = append(uniqueLabels, label)
  810. }
  811. }
  812. return strings.Join(uniqueLabels, ", ")
  813. }
  814. // processHighwayLabel 处理高速公路场景的标签
  815. // 例如:"绿化_路侧植株_路侧植株" -> "路侧植株_路侧植株"
  816. // 去掉前缀(第一个下划线及其之前的内容)
  817. func processHighwayLabel(label string) string {
  818. // 查找第一个下划线的位置
  819. underscoreIndex := strings.Index(label, "_")
  820. if underscoreIndex == -1 {
  821. // 如果没有下划线,直接返回原标签
  822. return label
  823. }
  824. // 返回从下划线之后的内容
  825. return label[underscoreIndex+1:]
  826. }
  827. func normalizeSceneLabel(label, sceneName string) string {
  828. if shouldTrimSceneLabelPrefix(sceneName) {
  829. return processHighwayLabel(label)
  830. }
  831. return label
  832. }
  833. func shouldTrimSceneLabelPrefix(sceneName string) bool {
  834. return strings.Contains(sceneName, "运营高速公路") ||
  835. strings.Contains(sceneName, "operate_highway") ||
  836. strings.Contains(sceneName, "隧道") ||
  837. strings.Contains(sceneName, "tunnel") ||
  838. strings.Contains(sceneName, "简支梁") ||
  839. strings.Contains(sceneName, "桥梁") ||
  840. strings.Contains(sceneName, "simple_supported_bridge")
  841. }