mirror of
https://github.com/esimov/caire.git
synced 2025-09-27 12:52:12 +08:00
fix: seams dispersion issue on image enlargment #74
This commit is contained in:
@@ -29,7 +29,7 @@ func Benchmark_Carver(b *testing.B) {
|
|||||||
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
||||||
c = NewCarver(width, height)
|
c = NewCarver(width, height)
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
seams := c.FindLowestEnergySeams()
|
seams := c.FindLowestEnergySeams(p)
|
||||||
img = c.RemoveSeam(img, seams, p.Debug)
|
img = c.RemoveSeam(img, seams, p.Debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
92
carver.go
92
carver.go
@@ -13,8 +13,9 @@ import (
|
|||||||
const maxFaceDetAttempts = 20
|
const maxFaceDetAttempts = 20
|
||||||
|
|
||||||
var (
|
var (
|
||||||
usedSeams []UsedSeams
|
|
||||||
detAttempts int
|
detAttempts int
|
||||||
|
sobel *image.NRGBA
|
||||||
|
energySeams = make([][]Seam, 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Carver is the main entry struct having as parameters the newly generated image width, height and seam points.
|
// Carver is the main entry struct having as parameters the newly generated image width, height and seam points.
|
||||||
@@ -31,17 +32,6 @@ type Seam struct {
|
|||||||
Y int
|
Y int
|
||||||
}
|
}
|
||||||
|
|
||||||
// UsedSeams contains the already generated seams.
|
|
||||||
type UsedSeams struct {
|
|
||||||
ActiveSeam []ActiveSeam
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActiveSeam contains the current seam position and color.
|
|
||||||
type ActiveSeam struct {
|
|
||||||
Seam
|
|
||||||
Pix color.Color
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCarver returns an initialized Carver structure.
|
// NewCarver returns an initialized Carver structure.
|
||||||
func NewCarver(width, height int) *Carver {
|
func NewCarver(width, height int) *Carver {
|
||||||
return &Carver{
|
return &Carver{
|
||||||
@@ -75,7 +65,7 @@ func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) error {
|
|||||||
var srcImg *image.NRGBA
|
var srcImg *image.NRGBA
|
||||||
|
|
||||||
width, height := img.Bounds().Dx(), img.Bounds().Dy()
|
width, height := img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
sobel := c.SobelDetector(img, float64(p.SobelThreshold))
|
sobel = c.SobelDetector(img, float64(p.SobelThreshold))
|
||||||
|
|
||||||
if p.FaceDetect && detAttempts < maxFaceDetAttempts {
|
if p.FaceDetect && detAttempts < maxFaceDetAttempts {
|
||||||
var ratio float64
|
var ratio float64
|
||||||
@@ -132,7 +122,7 @@ func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) error {
|
|||||||
face.Col+face.Scale/2,
|
face.Col+face.Scale/2,
|
||||||
face.Row+face.Scale/2,
|
face.Row+face.Scale/2,
|
||||||
)
|
)
|
||||||
draw.Draw(sobel, rect, &image.Uniform{color.White}, image.ZP, draw.Src)
|
draw.Draw(sobel, rect, &image.Uniform{color.White}, image.Point{}, draw.Src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,6 +179,17 @@ func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Increase the energy value for each of the selected seam from the seams table
|
||||||
|
// in order to avoid picking the same seam over and over again.
|
||||||
|
// We expand the energy level of the selected seams to have a better redistribution.
|
||||||
|
if len(energySeams) > 0 {
|
||||||
|
for i := 0; i < len(energySeams); i++ {
|
||||||
|
for _, seam := range energySeams[i] {
|
||||||
|
sobel.Set(seam.X, seam.Y, &image.Uniform{color.White})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if p.BlurRadius > 0 {
|
if p.BlurRadius > 0 {
|
||||||
srcImg = c.StackBlur(sobel, uint32(p.BlurRadius))
|
srcImg = c.StackBlur(sobel, uint32(p.BlurRadius))
|
||||||
} else {
|
} else {
|
||||||
@@ -226,7 +227,7 @@ func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FindLowestEnergySeams find the lowest vertical energy seam.
|
// FindLowestEnergySeams find the lowest vertical energy seam.
|
||||||
func (c *Carver) FindLowestEnergySeams() []Seam {
|
func (c *Carver) FindLowestEnergySeams(p *Processor) []Seam {
|
||||||
// Find the lowest cost seam from the energy matrix starting from the last row.
|
// Find the lowest cost seam from the energy matrix starting from the last row.
|
||||||
var (
|
var (
|
||||||
min = math.MaxFloat64
|
min = math.MaxFloat64
|
||||||
@@ -246,8 +247,8 @@ func (c *Carver) FindLowestEnergySeams() []Seam {
|
|||||||
seams = append(seams, Seam{X: px, Y: c.Height - 1})
|
seams = append(seams, Seam{X: px, Y: c.Height - 1})
|
||||||
var left, middle, right float64
|
var left, middle, right float64
|
||||||
|
|
||||||
// Walk up in the matrix table, check the immediate three top pixel seam level
|
// Walk up in the matrix table, check the immediate three top pixels seam level
|
||||||
// and add the one which has the lowest cumulative energy.
|
// and add that one which has the lowest cumulative energy.
|
||||||
for y := c.Height - 2; y >= 0; y-- {
|
for y := c.Height - 2; y >= 0; y-- {
|
||||||
middle = c.get(px, y)
|
middle = c.get(px, y)
|
||||||
// Leftmost seam, no child to the left
|
// Leftmost seam, no child to the left
|
||||||
@@ -275,6 +276,14 @@ func (c *Carver) FindLowestEnergySeams() []Seam {
|
|||||||
}
|
}
|
||||||
seams = append(seams, Seam{X: px, Y: y})
|
seams = append(seams, Seam{X: px, Y: y})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compare against c.Width and NOT c.Height, because the image is rotated.
|
||||||
|
if p.NewWidth > c.Width || (p.NewHeight > 0 && p.NewHeight > c.Width) {
|
||||||
|
// Include the currently processed energy seam into the seams table,
|
||||||
|
// but only when an image enlargement operation is commenced.
|
||||||
|
// We need to take this approach in order to avoid picking the same seam each time.
|
||||||
|
energySeams = append(energySeams, seams)
|
||||||
|
}
|
||||||
return seams
|
return seams
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,10 +313,8 @@ func (c *Carver) RemoveSeam(img *image.NRGBA, seams []Seam, debug bool) *image.N
|
|||||||
// AddSeam add a new seam.
|
// AddSeam add a new seam.
|
||||||
func (c *Carver) AddSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGBA {
|
func (c *Carver) AddSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGBA {
|
||||||
var (
|
var (
|
||||||
currentSeam []ActiveSeam
|
lr, lg, lb uint32
|
||||||
lr, lg, lb uint32
|
rr, rg, rb uint32
|
||||||
rr, rg, rb uint32
|
|
||||||
py int
|
|
||||||
)
|
)
|
||||||
|
|
||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
@@ -320,47 +327,22 @@ func (c *Carver) AddSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGB
|
|||||||
if debug {
|
if debug {
|
||||||
c.Seams = append(c.Seams, Seam{X: x, Y: y})
|
c.Seams = append(c.Seams, Seam{X: x, Y: y})
|
||||||
}
|
}
|
||||||
// Calculate the current seam pixel color by averaging the neighboring pixels color.
|
if x > 0 && x != bounds.Max.X {
|
||||||
if y > 0 {
|
lr, lg, lb, _ = img.At(x-1, y).RGBA()
|
||||||
py = y - 1
|
|
||||||
} else {
|
|
||||||
py = y
|
|
||||||
}
|
|
||||||
|
|
||||||
if x > 0 {
|
|
||||||
lr, lg, lb, _ = img.At(x-1, py).RGBA()
|
|
||||||
} else {
|
} else {
|
||||||
lr, lg, lb, _ = img.At(x, y).RGBA()
|
lr, lg, lb, _ = img.At(x, y).RGBA()
|
||||||
}
|
}
|
||||||
|
|
||||||
if y < bounds.Max.Y-1 {
|
|
||||||
py = y + 1
|
|
||||||
} else {
|
|
||||||
py = y
|
|
||||||
}
|
|
||||||
|
|
||||||
if x < bounds.Max.X-1 {
|
if x < bounds.Max.X-1 {
|
||||||
rr, rg, rb, _ = img.At(x+1, py).RGBA()
|
rr, rg, rb, _ = img.At(x+1, y).RGBA()
|
||||||
} else {
|
} else if x == bounds.Max.X {
|
||||||
rr, rg, rb, _ = img.At(x, y).RGBA()
|
rr, rg, rb, _ = img.At(x, y).RGBA()
|
||||||
}
|
}
|
||||||
avr, avg, avb := (lr+rr)/2, (lg+rg)/2, (lb+rb)/2
|
|
||||||
dst.Set(x, y, color.RGBA{uint8(avr >> 8), uint8(avg >> 8), uint8(avb >> 8), 255})
|
|
||||||
|
|
||||||
// Append the current seam position and color to the existing seams.
|
// calculate the average color of the neighboring pixels
|
||||||
// To avoid picking the same optimal seam over and over again,
|
avr, avg, avb := (lr+rr)>>1, (lg+rg)>>1, (lb+rb)>>1
|
||||||
// each time we detect an optimal seam we assign a large positive value
|
dst.Set(x, y, color.RGBA{uint8(avr >> 8), uint8(avg >> 8), uint8(avb >> 8), 0xff})
|
||||||
// to the corresponding pixels in the energy map.
|
dst.Set(x+1, y, img.At(x, y))
|
||||||
// We will increase the seams weight by duplicating the pixel value.
|
|
||||||
currentSeam = append(currentSeam,
|
|
||||||
ActiveSeam{Seam{x + 1, y},
|
|
||||||
color.RGBA{
|
|
||||||
R: uint8((avr + avr) >> 8),
|
|
||||||
G: uint8((avg + avg) >> 8),
|
|
||||||
B: uint8((avb + avb) >> 8),
|
|
||||||
A: 255,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else if seam.X < x {
|
} else if seam.X < x {
|
||||||
dst.Set(x, y, img.At(x-1, y))
|
dst.Set(x, y, img.At(x-1, y))
|
||||||
dst.Set(x+1, y, img.At(x, y))
|
dst.Set(x+1, y, img.At(x, y))
|
||||||
@@ -369,7 +351,7 @@ func (c *Carver) AddSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGB
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
usedSeams = append(usedSeams, UsedSeams{currentSeam})
|
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -37,7 +37,7 @@ func TestCarver_EnergySeamShouldNotBeDetected(t *testing.T) {
|
|||||||
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
||||||
c = NewCarver(width, height)
|
c = NewCarver(width, height)
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
les := c.FindLowestEnergySeams()
|
les := c.FindLowestEnergySeams(p)
|
||||||
seams = append(seams, les)
|
seams = append(seams, les)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ func TestCarver_DetectHorizontalEnergySeam(t *testing.T) {
|
|||||||
var totalEnergySeams int
|
var totalEnergySeams int
|
||||||
|
|
||||||
img := image.NewNRGBA(image.Rect(0, 0, ImgWidth, ImgHeight))
|
img := image.NewNRGBA(image.Rect(0, 0, ImgWidth, ImgHeight))
|
||||||
draw.Draw(img, img.Bounds(), &image.Uniform{image.White}, image.ZP, draw.Src)
|
draw.Draw(img, img.Bounds(), &image.Uniform{image.White}, image.Point{}, draw.Src)
|
||||||
|
|
||||||
// Replace the pixel colors in a single row from 0xff to 0xdd. 5 is an arbitrary value.
|
// Replace the pixel colors in a single row from 0xff to 0xdd. 5 is an arbitrary value.
|
||||||
// The seam detector should recognize that line as being of low energy density
|
// The seam detector should recognize that line as being of low energy density
|
||||||
@@ -75,7 +75,7 @@ func TestCarver_DetectHorizontalEnergySeam(t *testing.T) {
|
|||||||
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
||||||
c = NewCarver(width, height)
|
c = NewCarver(width, height)
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
les := c.FindLowestEnergySeams()
|
les := c.FindLowestEnergySeams(p)
|
||||||
seams = append(seams, les)
|
seams = append(seams, les)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ func TestCarver_DetectVerticalEnergySeam(t *testing.T) {
|
|||||||
var totalEnergySeams int
|
var totalEnergySeams int
|
||||||
|
|
||||||
img := image.NewNRGBA(image.Rect(0, 0, ImgWidth, ImgHeight))
|
img := image.NewNRGBA(image.Rect(0, 0, ImgWidth, ImgHeight))
|
||||||
draw.Draw(img, img.Bounds(), &image.Uniform{image.White}, image.ZP, draw.Src)
|
draw.Draw(img, img.Bounds(), &image.Uniform{image.White}, image.Point{}, draw.Src)
|
||||||
|
|
||||||
// Replace the pixel colors in a single column from 0xff to 0xdd. 5 is an arbitrary value.
|
// Replace the pixel colors in a single column from 0xff to 0xdd. 5 is an arbitrary value.
|
||||||
// The seam detector should recognize that line as being of low energy density
|
// The seam detector should recognize that line as being of low energy density
|
||||||
@@ -114,7 +114,7 @@ func TestCarver_DetectVerticalEnergySeam(t *testing.T) {
|
|||||||
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
||||||
c = NewCarver(width, height)
|
c = NewCarver(width, height)
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
les := c.FindLowestEnergySeams()
|
les := c.FindLowestEnergySeams(p)
|
||||||
seams = append(seams, les)
|
seams = append(seams, les)
|
||||||
}
|
}
|
||||||
img = c.RotateImage270(img)
|
img = c.RotateImage270(img)
|
||||||
@@ -135,7 +135,7 @@ func TestCarver_RemoveSeam(t *testing.T) {
|
|||||||
|
|
||||||
// We choose to fill up the background with an uniform white color
|
// We choose to fill up the background with an uniform white color
|
||||||
// and afterwards we replace the colors in a single row with lower intensity ones.
|
// and afterwards we replace the colors in a single row with lower intensity ones.
|
||||||
draw.Draw(img, bounds, &image.Uniform{image.White}, image.ZP, draw.Src)
|
draw.Draw(img, bounds, &image.Uniform{image.White}, image.Point{}, draw.Src)
|
||||||
origImg := img
|
origImg := img
|
||||||
|
|
||||||
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
|
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
@@ -146,7 +146,7 @@ func TestCarver_RemoveSeam(t *testing.T) {
|
|||||||
|
|
||||||
c := NewCarver(dx, dy)
|
c := NewCarver(dx, dy)
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
seams := c.FindLowestEnergySeams()
|
seams := c.FindLowestEnergySeams(p)
|
||||||
img = c.RemoveSeam(img, seams, false)
|
img = c.RemoveSeam(img, seams, false)
|
||||||
|
|
||||||
isEq := true
|
isEq := true
|
||||||
@@ -174,7 +174,7 @@ func TestCarver_AddSeam(t *testing.T) {
|
|||||||
|
|
||||||
// We choose to fill up the background with an uniform white color
|
// We choose to fill up the background with an uniform white color
|
||||||
// Afterwards we'll replace the colors in a single row with lower intensity ones.
|
// Afterwards we'll replace the colors in a single row with lower intensity ones.
|
||||||
draw.Draw(img, bounds, &image.Uniform{image.White}, image.ZP, draw.Src)
|
draw.Draw(img, bounds, &image.Uniform{image.White}, image.Point{}, draw.Src)
|
||||||
origImg := img
|
origImg := img
|
||||||
|
|
||||||
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
|
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
@@ -185,7 +185,7 @@ func TestCarver_AddSeam(t *testing.T) {
|
|||||||
|
|
||||||
c := NewCarver(dx, dy)
|
c := NewCarver(dx, dy)
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
seams := c.FindLowestEnergySeams()
|
seams := c.FindLowestEnergySeams(p)
|
||||||
img = c.AddSeam(img, seams, false)
|
img = c.AddSeam(img, seams, false)
|
||||||
|
|
||||||
dx, dy = img.Bounds().Dx(), img.Bounds().Dy()
|
dx, dy = img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
@@ -212,8 +212,6 @@ func TestCarver_ComputeSeams(t *testing.T) {
|
|||||||
|
|
||||||
// We choose to fill up the background with an uniform white color
|
// We choose to fill up the background with an uniform white color
|
||||||
// Afterwards we'll replace the colors in a single row with lower intensity ones.
|
// Afterwards we'll replace the colors in a single row with lower intensity ones.
|
||||||
//draw.Draw(img, img.Bounds(), &image.Uniform{image.White}, image.ZP, draw.Src)
|
|
||||||
|
|
||||||
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
|
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
// Replace the pixels in row 5 with lower intensity colors.
|
// Replace the pixels in row 5 with lower intensity colors.
|
||||||
for x := 0; x < dx; x++ {
|
for x := 0; x < dx; x++ {
|
||||||
@@ -356,11 +354,11 @@ func TestCarver_ShouldNotRemoveFaceZone(t *testing.T) {
|
|||||||
face.Col+face.Scale/2,
|
face.Col+face.Scale/2,
|
||||||
face.Row+face.Scale/2,
|
face.Row+face.Scale/2,
|
||||||
)
|
)
|
||||||
draw.Draw(sobel, rect, &image.Uniform{image.White}, image.ZP, draw.Src)
|
draw.Draw(sobel, rect, &image.Uniform{image.White}, image.Point{}, draw.Src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
seams := c.FindLowestEnergySeams()
|
seams := c.FindLowestEnergySeams(p)
|
||||||
|
|
||||||
for _, seam := range seams {
|
for _, seam := range seams {
|
||||||
if seam.X >= rect.Min.X && seam.X <= rect.Max.X {
|
if seam.X >= rect.Min.X && seam.X <= rect.Max.X {
|
||||||
|
@@ -17,7 +17,7 @@ func TestResize_ShrinkImageWidth(t *testing.T) {
|
|||||||
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
||||||
c = NewCarver(width, height)
|
c = NewCarver(width, height)
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
seams := c.FindLowestEnergySeams()
|
seams := c.FindLowestEnergySeams(p)
|
||||||
img = c.RemoveSeam(img, seams, p.Debug)
|
img = c.RemoveSeam(img, seams, p.Debug)
|
||||||
}
|
}
|
||||||
imgWidth := img.Bounds().Max.X
|
imgWidth := img.Bounds().Max.X
|
||||||
@@ -40,7 +40,7 @@ func TestResize_ShrinkImageHeight(t *testing.T) {
|
|||||||
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
||||||
c = NewCarver(width, height)
|
c = NewCarver(width, height)
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
seams := c.FindLowestEnergySeams()
|
seams := c.FindLowestEnergySeams(p)
|
||||||
img = c.RemoveSeam(img, seams, p.Debug)
|
img = c.RemoveSeam(img, seams, p.Debug)
|
||||||
}
|
}
|
||||||
img = c.RotateImage270(img)
|
img = c.RotateImage270(img)
|
||||||
@@ -64,7 +64,7 @@ func TestResize_EnlargeImageWidth(t *testing.T) {
|
|||||||
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
||||||
c = NewCarver(width, height)
|
c = NewCarver(width, height)
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
seams := c.FindLowestEnergySeams()
|
seams := c.FindLowestEnergySeams(p)
|
||||||
img = c.AddSeam(img, seams, p.Debug)
|
img = c.AddSeam(img, seams, p.Debug)
|
||||||
}
|
}
|
||||||
imgWidth := img.Bounds().Max.X - origImgWidth
|
imgWidth := img.Bounds().Max.X - origImgWidth
|
||||||
@@ -88,7 +88,7 @@ func TestResize_EnlargeImageHeight(t *testing.T) {
|
|||||||
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
||||||
c = NewCarver(width, height)
|
c = NewCarver(width, height)
|
||||||
c.ComputeSeams(p, img)
|
c.ComputeSeams(p, img)
|
||||||
seams := c.FindLowestEnergySeams()
|
seams := c.FindLowestEnergySeams(p)
|
||||||
img = c.AddSeam(img, seams, p.Debug)
|
img = c.AddSeam(img, seams, p.Debug)
|
||||||
}
|
}
|
||||||
img = c.RotateImage270(img)
|
img = c.RotateImage270(img)
|
||||||
|
Reference in New Issue
Block a user