hazard.go 27 KB

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