Skip to content

Commit

Permalink
#51: Checking if image is an illustration in separate process to prev…
Browse files Browse the repository at this point in the history
…ent imagick memory footprint grow
  • Loading branch information
dooman87 committed Jul 28, 2024
1 parent 562d867 commit c8d1d79
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 144 deletions.
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ RUN mkdir -p /go/src/github.com/Pixboost/
WORKDIR /go/src/github.com/Pixboost/
RUN git clone https://github.com/Pixboost/transformimgs.git

WORKDIR /go/src/github.com/Pixboost/transformimgs/illustration

RUN go install

WORKDIR /go/src/github.com/Pixboost/transformimgs/cmd

RUN go build -o /transformimgs
Expand Down
7 changes: 7 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,15 @@ ENV PATH $GOPATH/bin:$PATH
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 1777 "$GOPATH"
WORKDIR $GOPATH



ENV IM_HOME /usr/local/bin

VOLUME /go/src/github.com/Pixboost/transformimgs/
WORKDIR /go/src/github.com/Pixboost/transformimgs/

RUN cd illustration && \
go install && \
cd ../

ENTRYPOINT ["sh", "/go/src/github.com/Pixboost/transformimgs/run.sh"]
5 changes: 5 additions & 0 deletions illustration/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/Pixboost/illustration

go 1.18

require gopkg.in/gographics/imagick.v3 v3.7.0 // indirect
2 changes: 2 additions & 0 deletions illustration/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
gopkg.in/gographics/imagick.v3 v3.7.0 h1:w8iQa58ikuqjX4l2OVML3pgqFcDMD8ywXJ9/cXa33fk=
gopkg.in/gographics/imagick.v3 v3.7.0/go.mod h1:+Q9nyA2xRZXrDyTtJ/eko+8V/5E7bWYs08ndkZp8UmA=
114 changes: 114 additions & 0 deletions illustration/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package main

import (
"fmt"
"gopkg.in/gographics/imagick.v3/imagick"
"io"
"log"
"math"
"os"
"sort"
)

type colorSlice []*imagick.PixelWand

func (c colorSlice) Len() int { return len(c) }
func (c colorSlice) Less(i, j int) bool { return c[i].GetColorCount() < c[j].GetColorCount() }
func (c colorSlice) Swap(i, j int) { c[i], c[j] = c[j], c[i] }

func main() {
imgData, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
}

var (
colors colorSlice
colorsCnt uint
)

mw := imagick.NewMagickWand()

err = mw.ReadImageBlob(imgData)
if err != nil {
log.Fatal(err)
}

if (mw.GetImageWidth() * mw.GetImageHeight()) > 500*500 {
aspectRatio := float32(mw.GetImageWidth()) / float32(mw.GetImageHeight())
err = mw.ScaleImage(500, uint(500/aspectRatio))
if err != nil {
log.Fatal(err)
}
}

colorsCnt, colors = mw.GetImageHistogram()
if colorsCnt > 30000 {
fmt.Print(false)
return
}

sort.Sort(sort.Reverse(colors))

var (
colorIdx int
count uint
currColor *imagick.PixelWand
pixelsCount = uint(0)
totalPixelsCount = float32(mw.GetImageHeight() * mw.GetImageWidth())
tenPercent = uint(totalPixelsCount * 0.1)
fiftyPercent = uint(totalPixelsCount * 0.5)
isBackground = false
lastBackgroundColor *imagick.PixelWand
colorsInBackground = uint(0)
pixelsInBackground = uint(0)
)

for colorIdx, currColor = range colors {
if pixelsCount > fiftyPercent {
break
}

count = currColor.GetColorCount()

switch {
case colorIdx == 0:
isBackground = true
lastBackgroundColor = currColor
pixelsInBackground += count
colorsInBackground++
case isBackground:
// Comparing colors to find out if it's still background or not.
// This logic addresses backgrounds with more than one similar color.
alphaDiff := currColor.GetAlpha() - lastBackgroundColor.GetAlpha()
redDiff := currColor.GetRed() - lastBackgroundColor.GetRed()
greenDiff := currColor.GetGreen() - lastBackgroundColor.GetGreen()
blueDiff := currColor.GetBlue() - lastBackgroundColor.GetBlue()
distance :=
math.Max(math.Pow(redDiff, 2), math.Pow(redDiff-alphaDiff, 2)) +
math.Max(math.Pow(greenDiff, 2), math.Pow(greenDiff-alphaDiff, 2)) +
math.Max(math.Pow(blueDiff, 2), math.Pow(blueDiff-alphaDiff, 2))
if distance < 0.1 {
lastBackgroundColor = currColor
pixelsInBackground += count
colorsInBackground++
} else {
isBackground = false
if pixelsInBackground < tenPercent {
pixelsCount = pixelsInBackground
colorsInBackground = 0
pixelsInBackground = 0
} else {
pixelsCount += count
fiftyPercent = uint((totalPixelsCount - float32(pixelsInBackground)) * 0.5)
}
}
default:
pixelsCount += count
}
}

colorsCntIn50Pct := uint(colorIdx) - colorsInBackground

fmt.Print(colorsCntIn50Pct < 10 || (float32(colorsCntIn50Pct)/float32(colorsCnt)) <= 0.02)
}
136 changes: 23 additions & 113 deletions img/processor/imagemagick.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@ import (
"fmt"
"github.com/Pixboost/transformimgs/v8/img"
"github.com/Pixboost/transformimgs/v8/img/processor/internal"
"gopkg.in/gographics/imagick.v3/imagick"
"math"
"os"
"io"
"os/exec"
"os/signal"
"sort"
"strconv"
)

Expand Down Expand Up @@ -70,20 +66,6 @@ const (
AvifMime = "image/avif"
)

func init() {
imagick.Initialize()

// time resource limit is static and doesn't work with long-running processes, hence disabling it
imagick.SetResourceLimit(imagick.RESOURCE_TIME, 0)

c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
imagick.Terminate()
}()
}

// NewImageMagick creates a new ImageMagick processor. It does require
// ImageMagick binaries to be installed on the local machine.
//
Expand Down Expand Up @@ -286,6 +268,27 @@ func (p *ImageMagick) execImagemagick(in *bytes.Reader, args []string, imgId str
return out.Bytes(), nil
}

func (p *ImageMagick) execIllustration(in io.Reader) bool {
var out, cmderr bytes.Buffer
cmd := exec.Command("illustration")

cmd.Stdin = in
cmd.Stdout = &out
cmd.Stderr = &cmderr

if Debug {
img.Log.Printf("Running resize command, args '%v'\n", cmd.Args)
}
err := cmd.Run()
if err != nil {
img.Log.Printf("Error executing illustration command: %s\n", err.Error())
img.Log.Printf("ERROR: %s\n", cmderr.String())
return false
}

return string(out.Bytes()) == "true"
}

func (p *ImageMagick) LoadImageInfo(src *img.Image) (*img.Info, error) {
var out, cmderr bytes.Buffer
imgId := src.Id
Expand Down Expand Up @@ -328,12 +331,6 @@ func (p *ImageMagick) LoadImageInfo(src *img.Image) (*img.Info, error) {
return imageInfo, nil
}

type colorSlice []*imagick.PixelWand

func (c colorSlice) Len() int { return len(c) }
func (c colorSlice) Less(i, j int) bool { return c[i].GetColorCount() < c[j].GetColorCount() }
func (c colorSlice) Swap(i, j int) { c[i], c[j] = c[j], c[i] }

// isIllustration returns true if image is cartoon like, including
// icons, logos, illustrations.
//
Expand All @@ -354,94 +351,7 @@ func (p *ImageMagick) isIllustration(src *img.Image, info *img.Info) (bool, erro
return false, nil
}

var (
colors colorSlice
colorsCnt uint
)

mw := imagick.NewMagickWand()

err := mw.ReadImageBlob(src.Data)
if err != nil {
return false, err
}

if (info.Width * info.Height) > 500*500 {
aspectRatio := float32(info.Width) / float32(info.Height)
err = mw.ScaleImage(500, uint(500/aspectRatio))
if err != nil {
return false, err
}
}

colorsCnt, colors = mw.GetImageHistogram()
if colorsCnt > 30000 {
return false, nil
}

sort.Sort(sort.Reverse(colors))

var (
colorIdx int
count uint
currColor *imagick.PixelWand
pixelsCount = uint(0)
totalPixelsCount = float32(mw.GetImageHeight() * mw.GetImageWidth())
tenPercent = uint(totalPixelsCount * 0.1)
fiftyPercent = uint(totalPixelsCount * 0.5)
isBackground = false
lastBackgroundColor *imagick.PixelWand
colorsInBackground = uint(0)
pixelsInBackground = uint(0)
)

for colorIdx, currColor = range colors {
if pixelsCount > fiftyPercent {
break
}

count = currColor.GetColorCount()

switch {
case colorIdx == 0:
isBackground = true
lastBackgroundColor = currColor
pixelsInBackground += count
colorsInBackground++
case isBackground:
// Comparing colors to find out if it's still background or not.
// This logic addresses backgrounds with more than one similar color.
alphaDiff := currColor.GetAlpha() - lastBackgroundColor.GetAlpha()
redDiff := currColor.GetRed() - lastBackgroundColor.GetRed()
greenDiff := currColor.GetGreen() - lastBackgroundColor.GetGreen()
blueDiff := currColor.GetBlue() - lastBackgroundColor.GetBlue()
distance :=
math.Max(math.Pow(redDiff, 2), math.Pow(redDiff-alphaDiff, 2)) +
math.Max(math.Pow(greenDiff, 2), math.Pow(greenDiff-alphaDiff, 2)) +
math.Max(math.Pow(blueDiff, 2), math.Pow(blueDiff-alphaDiff, 2))
if distance < 0.1 {
lastBackgroundColor = currColor
pixelsInBackground += count
colorsInBackground++
} else {
isBackground = false
if pixelsInBackground < tenPercent {
pixelsCount = pixelsInBackground
colorsInBackground = 0
pixelsInBackground = 0
} else {
pixelsCount += count
fiftyPercent = uint((totalPixelsCount - float32(pixelsInBackground)) * 0.5)
}
}
default:
pixelsCount += count
}
}

colorsCntIn50Pct := uint(colorIdx) - colorsInBackground

return colorsCntIn50Pct < 10 || (float32(colorsCntIn50Pct)/float32(colorsCnt)) <= 0.02, nil
return p.execIllustration(bytes.NewBuffer(src.Data)), nil
}

func getOutputFormat(src *img.Info, target *img.Info, supportedFormats []string) (string, string) {
Expand Down
Loading

0 comments on commit c8d1d79

Please sign in to comment.