Repository: esimov/caire
Branch: master
Commit: 072a5888af55
Files: 37
Total size: 153.7 KB
Directory structure:
gitextract_jflphtww/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ └── build.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── build.sh
├── carver.go
├── carver_benchmark_test.go
├── carver_test.go
├── cmd/
│ └── caire/
│ └── main.go
├── data/
│ └── facefinder
├── doc.go
├── draw.go
├── exec.go
├── go.mod
├── go.sum
├── gui.go
├── image.go
├── image_test.go
├── imop/
│ ├── blend.go
│ ├── blend_test.go
│ ├── comp.go
│ └── comp_test.go
├── preview.go
├── processor.go
├── processor_test.go
├── snapcraft.yaml
├── sobel.go
├── stackblur.go
└── utils/
├── download.go
├── download_test.go
├── format.go
├── spinner.go
└── utils.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
github: esimov
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help me improve the library
labels: ''
---
### Describe the bug
<!--A clear and concise description of what the bug is.
- Describe if it has been cloned via `git clone` or downloaded with `go get`?
- You don't know how to install and build Caire from the source code?
If you have followed the README file, but still have issues post your action here.
- Describe your bug if it haven't been already reported.
-->
### API related bug
<!--A clear and concise description of the API usage.-->
### Expected behavior
<!--A clear and concise description of what you expected to happen.-->
### Screenshots
<!--Add screenshots to help explain your problem.-->
- [Screenshots, logs or errors]
### Bug with the Desktop version (please complete the following information):
- Sytem information like OS: [e.g. macOS, Ubuntu]
- You are using the binary file from the uploaded releases or you are doing a manual build?
### Additional context
<!--Add any other context about the problem here.-->
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
labels: ''
---
### Is your feature request related to a problem? Please describe.
<!--A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]-->
### Describe the solution you'd like
<!--A clear and concise description of what you want to happen.-->
### Describe alternatives you've considered
<!--A clear and concise description of any alternative solutions or features you've considered.-->
### Additional context
<!--Add any other context or screenshots about the feature request here.-->
================================================
FILE: .github/workflows/build.yml
================================================
name: build
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
name: Build
strategy:
fail-fast: false
matrix:
go-version: [~1.21, ~1.22]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
env:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Cache-Go
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod # Module download cache
~/.cache/go-build # Build cache (Linux)
~/Library/Caches/go-build # Build cache (Mac)
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- name: Install Linux Dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update -y
sudo apt-get install -y gcc pkg-config libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev libvulkan-dev
- name: Checkout code
uses: actions/checkout@v2
- name: Download Go modules
run: go mod download
- name: Run Tests
id: makefile
run: |
make test
================================================
FILE: .gitignore
================================================
*.jpg
*.png
*.jpeg
coverage.out
test-report.json
/packages
!/testdata/*.png
!/testdata/*.jpg
!/examples/**/*.png
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Endre Simo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
all:
@./build.sh
clean:
@rm -f caire
install: all
@cp caire /usr/local/bin
uninstall:
@rm -f /usr/local/bin/caire
package:
@NOCOPY=1 ./build.sh package
test:
go test -v -json ./... -run=. > ./test-report.json -coverprofile=coverage.out
================================================
FILE: README.md
================================================
<h1 align="center"><img alt="Caire Logo" src="https://user-images.githubusercontent.com/883386/51555990-a1762600-1e81-11e9-9a6a-0cd815870358.png" height="180"></h1>
[](https://github.com/esimov/caire/actions/workflows/build.yml)
[](https://pkg.go.dev/github.com/esimov/caire)
[](./LICENSE)
[](https://github.com/esimov/caire/releases/tag/v1.5.0)
[](https://formulae.brew.sh/formula/caire)
[](https://snapcraft.io/caire)
**Caire** is a content aware image resize library based on *[Seam Carving for Content-Aware Image Resizing](https://inst.eecs.berkeley.edu/~cs194-26/fa16/hw/proj4-seamcarving/imret.pdf)* paper.
## How does it work
* An energy map (edge detection) is generated from the provided image.
* The algorithm tries to find the least important parts of the image taking into account the lowest energy values.
* Using a dynamic programming approach the algorithm will generate individual seams across the image from top to down, or from left to right (depending on the horizontal or vertical resizing) and will allocate for each seam a custom value, the least important pixels having the lowest energy cost and the most important ones having the highest cost.
* We traverse the image from the second row to the last row and compute the cumulative minimum energy for all possible connected seams for each entry.
* The minimum energy level is calculated by summing up the current pixel value with the lowest value of the neighboring pixels obtained from the previous row.
* We traverse the image from top to bottom and compute the minimum energy level. For each pixel in a row we compute the energy of the current pixel plus the energy of one of the three possible pixels above it.
* Find the lowest cost seam from the energy matrix starting from the last row and remove it.
* Repeat the process.
#### The process illustrated:
| Original image | Energy map | Seams applied
|:--:|:--:|:--:|
|  |  |  |  |
## Features
Key features which differentiates this library from the other existing open source solutions:
- [x] **GUI progress indicator**
- [x] Customizable command line support
- [x] Support for both shrinking or enlarging the image
- [x] Resize image both vertically and horizontally
- [x] Face detection to avoid face deformation
- [x] Support for multiple output image type (jpg, jpeg, png, bmp)
- [x] Support for `stdin` and `stdout` pipe commands
- [x] Can process whole directories recursively and concurrently
- [x] Use of sobel threshold for fine tuning
- [x] Use of blur filter for increased edge detection
- [x] Support for squaring the image with a single command
- [x] Support for proportional scaling
- [x] Support for protective mask
- [x] Support for removal mask
- [x] [GUI debug mode support](#masks-support)
## Install
First, install Go, set your `GOPATH`, and make sure `$GOPATH/bin` is on your `PATH`.
```bash
$ go install github.com/esimov/caire/cmd/caire@latest
```
## MacOS (Brew) install
The library can also be installed via Homebrew.
```bash
$ brew install caire
```
## Usage
```bash
$ caire -in input.jpg -out output.jpg
```
### Supported commands:
```bash
$ caire --help
```
The following flags are supported:
| Flag | Default | Description |
| --- | --- | --- |
| `in` | - | Input file |
| `out` | - | Output file |
| `width` | n/a | New width |
| `height` | n/a | New height |
| `preview` | true | Show GUI window |
| `perc` | false | Reduce image by percentage |
| `square` | false | Reduce image to square dimensions |
| `blur` | 4 | Blur radius |
| `sobel` | 2 | Sobel filter threshold |
| `debug` | false | Use debugger |
| `face` | false | Use face detection |
| `angle` | float | Plane rotated faces angle |
| `mask` | string | Mask file path |
| `rmask` | string | Remove mask file path |
| `color` | string | Seam color (default `#ff0000`) |
| `shape` | string | Shape type used for debugging: `circle`,`line` (default `circle`) |
## Face detection
The library is capable of detecting human faces prior resizing the images by using the lightweight Pigo (https://github.com/esimov/pigo) face detection library.
The image below illustrates the application capabilities for human face detection prior resizing. It's clearly visible that with face detection activated the algorithm will avoid cropping pixels inside the detected faces, retaining the face zone unaltered.
| Original image | With face detection | Without face detection
|:--:|:--:|:--:|
|  |  |  |
[Sample image source](http://www.lens-rumors.com/wp-content/uploads/2014/12/EF-M-55-200mm-f4.5-6.3-IS-STM-sample.jpg)
### GUI progress indicator
<p align="center"><img alt="GUI preview" title="GUI preview" src="https://github.com/esimov/caire/raw/master/gui_preview.gif"></p>
A GUI preview mode is also incorporated into the library for in time process visualization. The [Gio](http://gioui.org/) GUI library has been used because of its robustness and modern architecture. Prior running it please make sure that you have installed all the required dependencies noted in the installation section (https://gioui.org/#installation) .
The preview window is activated by default but you can deactivate it any time by setting the `-preview` flag to false. When the images are processed concurrently from a directory the preview mode is deactivated.
### Face detection to avoid face deformation
In order to detect faces prior rescaling, use the `-face` flag. There is no need to provide a face classification file, since it's already embedded into the generated binary file. The sample code below will resize the provided image with 20%, but checks for human faces in order tot avoid face deformations.
For face detection related settings please check the Pigo [documentation](https://github.com/esimov/pigo/blob/master/README.md).
```bash
$ caire -in input.jpg -out output.jpg -face=1 -perc=1 -width=20
```
### Support for `stdin` and `stdout` pipe commands
You can also use `stdin` and `stdout` with `-`:
```bash
$ cat input/source.jpg | caire -in - -out - >out.jpg
```
`in` and `out` default to `-` so you can also use:
```bash
$ cat input/source.jpg | caire >out.jpg
$ caire -out out.jpg < input/source.jpg
```
You can provide also an image URL for the `-in` flag or even use **curl** or **wget** as a pipe command in which case there is no need to use the `-in` flag.
```bash
$ caire -in <image_url> -out <output-folder>
$ curl -s <image_url> | caire > out.jpg
```
### Process multiple images from a directory concurrently
The library can also process multiple images from a directory **concurrently**. You have to provide only the source and the destination folder and the new width or height in this case.
```bash
$ caire -in <input_folder> -out <output-folder>
```
### Support for multiple output image type
There is no need to define the output file type, just use the correct extension and the library will encode the image to that specific type.
### Other options
In case you wish to scale down the image by a specific percentage, it can be used the **`-perc`** boolean flag. In this case the values provided for the `width` and `height` are expressed in percentage and not pixel values. For example to reduce the image dimension by 20% both horizontally and vertically you can use the following command:
```bash
$ caire -in input/source.jpg -out ./out.jpg -perc=1 -width=20 -height=20 -debug=false
```
Also the library supports the **`-square`** option. When this option is used the image will be resized to a square, based on the shortest edge.
When an image is resized on both the X and Y axis, the algorithm will first try to rescale it prior resizing, but also will preserve the image aspect ratio. The seam carving algorithm is applied only to the remaining points. Ex. : given an image of dimensions 2048x1536 if we want to resize to the 1024x500, the tool first rescale the image to 1024x768 and then will remove only the remaining 268px.
### Masks support:
- `-mask`: The path to the protective mask. The mask should be in binary format and have the same size as the input image. White areas represent regions where no seams should be carved.
- `-rmask`: The path to the removal mask. The mask should be in binary format and have the same size as the input image. White areas represent regions to be removed.
Mask | Mask removal
:-: | :-:
<video src='https://user-images.githubusercontent.com/883386/197509861-86733da8-0846-419a-95eb-4fb5a97607d5.mp4' width=180/> | <video src='https://user-images.githubusercontent.com/883386/197397857-7b785d7c-2f80-4aed-a5d2-75c429389060.mp4' width=180/>
### Caire integrations
- [x] Caire can be used as a serverless function via OpenFaaS: https://github.com/esimov/caire-openfaas
- [x] Caire can also be used as a `snap` function (https://snapcraft.io/caire): `$ snap run caire --h`
<a href="https://snapcraft.io/caire"><img src="https://raw.githubusercontent.com/snapcore/snap-store-badges/master/EN/%5BEN%5D-snap-store-white-uneditable.png" alt="snapcraft caire"></a>
## Results
#### Shrunk images
| Original | Shrunk |
| --- | --- |
|  |  |
|  |  |
|  |  |
|  |  |
#### Enlarged images
| Original | Extended |
| --- | --- |
|  |  |
|  |  |
### Useful resources
* https://en.wikipedia.org/wiki/Seam_carving
* https://inst.eecs.berkeley.edu/~cs194-26/fa16/hw/proj4-seamcarving/imret.pdf
* http://pages.cs.wisc.edu/~moayad/cs766/download_files/alnammi_cs_766_final_report.pdf
* https://stacks.stanford.edu/file/druid:my512gb2187/Zargham_Nassirpour_Content_aware_image_resizing.pdf
## Author
* Endre Simo ([@simo_endre](https://twitter.com/simo_endre))
## License
Copyright © 2018 Endre Simo
This project is under the MIT License. See the LICENSE file for the full license text.
================================================
FILE: build.sh
================================================
#!/bin/bash
set -e
VERSION="1.5.0"
PROTECTED_MODE="no"
export GO15VENDOREXPERIMENT=1
cd $(dirname "${BASH_SOURCE[0]}")
OD="$(pwd)"
WD=$OD
package() {
echo Packaging $1 Binary
bdir=caire-${VERSION}-$2-$3
rm -rf packages/$bdir && mkdir -p packages/$bdir
GOOS=$2 GOARCH=$3 ./build.sh
if [ "$2" == "windows" ]; then
mv caire packages/$bdir/caire.exe
else
mv caire packages/$bdir
fi
cp README.md packages/$bdir
cd packages
if [ "$2" == "linux" ]; then
tar -zcf $bdir.tar.gz $bdir
else
zip -r -q $bdir.zip $bdir
fi
rm -rf $bdir
cd ..
}
if [ "$1" == "package" ]; then
rm -rf packages/
package "Windows" "windows" "amd64"
package "Mac" "darwin" "amd64"
package "Linux" "linux" "amd64"
package "FreeBSD" "freebsd" "amd64"
exit
fi
# temp directory for storing isolated environment.
TMP="$(mktemp -d -t sdb.XXXX)"
rmtemp() {
rm -rf "$TMP"
}
trap rmtemp EXIT
if [ "$NOCOPY" != "1" ]; then
# copy all files to an isolated directory.
WD="$TMP/src/github.com/esimov/caire"
export GOPATH="$TMP"
for file in `find . -type f`; do
# TODO: use .gitignore to ignore, or possibly just use git to determine the file list.
if [[ "$file" != "." && "$file" != ./.git* && "$file" != ./caire ]]; then
mkdir -p "$WD/$(dirname "${file}")"
cp -P "$file" "$WD/$(dirname "${file}")"
fi
done
cd $WD
fi
# build and store objects into original directory.
go build -ldflags "-X main.Version=$VERSION" -o "$OD/caire" cmd/caire/main.go
================================================
FILE: carver.go
================================================
package caire
import (
"fmt"
"image"
"image/color"
"image/draw"
"math"
"github.com/esimov/caire/utils"
pigo "github.com/esimov/pigo/core"
)
// SeamCarver defines the Carve interface method, which have to be
// implemented by the Processor struct.
type SeamCarver interface {
Resize(*image.NRGBA) (image.Image, error)
}
// maxFaceDetAttempts defines the maximum number of attempts of face detections
const maxFaceDetAttempts = 20
var (
detAttempts int
isFaceDetected bool
)
var (
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.
type Carver struct {
Points []float64
Seams []Seam
Width int
Height int
}
// Seam struct contains the seam pixel coordinates.
type Seam struct {
X int
Y int
}
// NewCarver returns an initialized Carver structure.
func NewCarver(width, height int) *Carver {
return &Carver{
Points: make([]float64, width*height),
Seams: []Seam{},
Width: width,
Height: height,
}
}
// Get energy pixel value.
func (c *Carver) get(x, y int) float64 {
px := x + y*c.Width
return c.Points[px]
}
// Set energy pixel value.
func (c *Carver) set(x, y int, px float64) {
idx := x + y*c.Width
c.Points[idx] = px
}
// ComputeSeams compute the minimum energy level based on the following logic:
//
// - traverse the image from the second row to the last row
// and compute the cumulative minimum energy M for all possible
// connected seams for each entry (i, j).
//
// - the minimum energy level is calculated by summing up the current pixel value
// with the minimum pixel value of the neighboring pixels from the previous row.
func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image.NRGBA, error) {
width, height := img.Bounds().Dx(), img.Bounds().Dy()
sobel = c.SobelDetector(img, float64(p.SobelThreshold))
dets := []pigo.Detection{}
if p.FaceDetector != nil && p.FaceDetect && detAttempts < maxFaceDetAttempts {
var ratio float64
if width < height {
ratio = float64(width) / float64(height)
} else {
ratio = float64(height) / float64(width)
}
minSize := float64(utils.Min(width, height)) * ratio / 3
// Transform the image to pixel array.
pixels := rgbToGrayscale(img)
cParams := pigo.CascadeParams{
MinSize: int(minSize),
MaxSize: utils.Min(width, height),
ShiftFactor: 0.1,
ScaleFactor: 1.1,
ImageParams: pigo.ImageParams{
Pixels: pixels,
Rows: height,
Cols: width,
Dim: width,
},
}
if p.vRes {
p.FaceAngle = 0.2
}
// Run the classifier over the obtained leaf nodes and return the detection results.
// The result contains quadruplets representing the row, column, scale and detection score.
dets = p.FaceDetector.RunCascade(cParams, p.FaceAngle)
// Calculate the intersection over union (IoU) of two clusters.
dets = p.FaceDetector.ClusterDetections(dets, 0.1)
if len(dets) == 0 {
// Retry detecting faces for a certain amount of time.
if detAttempts < maxFaceDetAttempts {
detAttempts++
}
} else {
detAttempts = 0
isFaceDetected = true
}
}
// Traverse the pixel data of the binary file used for protecting the regions
// which we do not want to be altered by the seam carver,
// obtain the white patches and apply it to the sobel image.
if len(p.MaskPath) > 0 && p.Mask != nil {
p.DebugMask = image.NewNRGBA(img.Bounds())
for i := 0; i < width*height; i++ {
x := i % width
y := (i - x) / width
r, g, b, _ := p.Mask.At(x, y).RGBA()
if r>>8 == 0xff && g>>8 == 0xff && b>>8 == 0xff {
if isFaceDetected {
// Reduce the brightness of the mask with a small factor if human faces are detected.
// This way we can avoid the seam carver to remove
// the pixels inside the detected human faces.
sobel.Set(x, y, color.RGBA{R: 225, G: 225, B: 225, A: 255})
} else {
sobel.Set(x, y, color.White)
}
}
}
}
// Traverse the pixel data of the binary file used to remove the image regions
// we do not want to be retained in the final image, obtain the white patches,
// but this time inverse the colors to black and merge it back to the sobel image.
if len(p.RMaskPath) > 0 && p.RMask != nil {
p.DebugMask = image.NewNRGBA(img.Bounds())
for i := 0; i < width*height; i++ {
x := i % width
y := (i - x) / width
r, g, b, _ := p.RMask.At(x, y).RGBA()
// Replace the white pixels with black.
if r>>8 == 0xff && g>>8 == 0xff && b>>8 == 0xff {
if isFaceDetected {
// Reduce the brightness of the mask with a small factor if human faces are detected.
// This way we can avoid the seam carver to remove
// the pixels inside the detected human faces.
sobel.Set(x, y, color.RGBA{R: 25, G: 25, B: 25, A: 255})
} else {
sobel.Set(x, y, color.Black)
}
p.DebugMask.Set(x, y, color.Black)
} else {
p.DebugMask.Set(x, y, color.Transparent)
}
}
}
// Iterate over the detected faces and fill out the rectangles with white.
// We need to trick the sobel detector to consider them as important image parts.
for _, face := range dets {
if (p.NewHeight != 0 && p.NewHeight < face.Scale) ||
(p.NewWidth != 0 && p.NewWidth < face.Scale) {
return nil, fmt.Errorf("%s %s",
"cannot resize the image to the specified dimension without face deformation.\n",
"\tRemove the face detection option in case you still wish to resize the image.")
}
if face.Q > 5.0 {
scale := int(float64(face.Scale) / 1.7)
rect := image.Rect(
face.Col-scale,
face.Row-scale,
face.Col+scale,
face.Row+scale,
)
p.DebugMask = image.NewNRGBA(img.Bounds())
draw.Draw(sobel, rect, &image.Uniform{color.White}, image.Point{}, draw.Src)
draw.Draw(p.DebugMask, rect, &image.Uniform{color.White}, image.Point{}, draw.Src)
}
}
// 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})
}
}
}
var srcImg *image.NRGBA
if p.BlurRadius > 0 {
srcImg = image.NewNRGBA(img.Bounds())
err := Stackblur(srcImg, sobel, uint32(p.BlurRadius))
if err != nil {
return nil, fmt.Errorf("error bluring the image: %w", err)
}
} else {
srcImg = sobel
}
for x := 0; x < c.Width; x++ {
for y := 0; y < c.Height; y++ {
r, _, _, a := srcImg.At(x, y).RGBA()
c.set(x, y, float64(r)/float64(a))
}
}
var left, middle, right float64
// Traverse the image from top to bottom and compute the minimum energy level.
// For each pixel in a row we compute the energy of the current pixel
// plus the energy of one of the three possible pixels above it.
for y := 1; y < c.Height; y++ {
for x := 1; x < c.Width-1; x++ {
left = c.get(x-1, y-1)
middle = c.get(x, y-1)
right = c.get(x+1, y-1)
min := math.Min(math.Min(left, middle), right)
// Set the minimum energy level.
c.set(x, y, c.get(x, y)+min)
}
// Special cases: pixels are far left or far right
left := c.get(0, y) + math.Min(c.get(0, y-1), c.get(1, y-1))
c.set(0, y, left)
right := c.get(0, y) + math.Min(c.get(c.Width-1, y-1), c.get(c.Width-2, y-1))
c.set(c.Width-1, y, right)
}
return srcImg, nil
}
// FindLowestEnergySeams find the lowest vertical energy seam.
func (c *Carver) FindLowestEnergySeams(p *Processor) []Seam {
// Find the lowest cost seam from the energy matrix starting from the last row.
var (
min = math.MaxFloat64
px int
)
seams := make([]Seam, 0)
// Find the pixel on the last row with the minimum cumulative energy and use this as the starting pixel
for x := 0; x < c.Width; x++ {
seam := c.get(x, c.Height-1)
if seam < min {
min = seam
px = x
}
}
seams = append(seams, Seam{X: px, Y: c.Height - 1})
var left, middle, right float64
// Walk up in the matrix table, check the immediate three top pixels seam level
// and add that one which has the lowest cumulative energy.
for y := c.Height - 2; y >= 0; y-- {
middle = c.get(px, y)
// Leftmost seam, no child to the left
if px == 0 {
right = c.get(px+1, y)
if right < middle {
px++
}
// Rightmost seam, no child to the right
} else if px == c.Width-1 {
left = c.get(px-1, y)
if left < middle {
px--
}
} else {
left = c.get(px-1, y)
right = c.get(px+1, y)
min := math.Min(math.Min(left, middle), right)
if min == left {
px--
} else if min == right {
px++
}
}
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
}
// RemoveSeam remove the least important columns based on the stored energy (seams) level.
func (c *Carver) RemoveSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGBA {
bounds := img.Bounds()
// Reduce the image width with one pixel on each iteration.
dst := image.NewNRGBA(image.Rect(0, 0, bounds.Dx()-1, bounds.Dy()))
for _, seam := range seams {
y := seam.Y
for x := 0; x < bounds.Max.X; x++ {
if seam.X == x {
if debug {
c.Seams = append(c.Seams, Seam{X: x, Y: y})
}
} else if seam.X < x {
dst.Set(x-1, y, img.At(x, y))
} else {
dst.Set(x, y, img.At(x, y))
}
}
}
return dst
}
// AddSeam add a new seam.
func (c *Carver) AddSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGBA {
var (
lr, lg, lb uint32
rr, rg, rb uint32
)
bounds := img.Bounds()
dst := image.NewNRGBA(image.Rect(0, 0, bounds.Dx()+1, bounds.Dy()))
for _, seam := range seams {
y := seam.Y
for x := 0; x < bounds.Max.X; x++ {
if seam.X == x {
if debug {
c.Seams = append(c.Seams, Seam{X: x, Y: y})
}
if x > 0 && x != bounds.Max.X {
lr, lg, lb, _ = img.At(x-1, y).RGBA()
} else {
lr, lg, lb, _ = img.At(x, y).RGBA()
}
if x < bounds.Max.X-1 {
rr, rg, rb, _ = img.At(x+1, y).RGBA()
} else if x == bounds.Max.X {
rr, rg, rb, _ = img.At(x, y).RGBA()
}
// calculate the average color of the neighboring pixels
avr, avg, avb := (lr+rr)>>1, (lg+rg)>>1, (lb+rb)>>1
dst.Set(x, y, color.RGBA{uint8(avr >> 8), uint8(avg >> 8), uint8(avb >> 8), 0xff})
dst.Set(x+1, y, img.At(x, y))
} else if seam.X < x {
dst.Set(x, y, img.At(x-1, y))
dst.Set(x+1, y, img.At(x, y))
} else {
dst.Set(x, y, img.At(x, y))
}
}
}
return dst
}
================================================
FILE: carver_benchmark_test.go
================================================
package caire
import (
"image"
"os"
"path/filepath"
"testing"
)
func Benchmark_Carver(b *testing.B) {
sampleImg := filepath.Join("./testdata", "sample.jpg")
f, err := os.Open(sampleImg)
if err != nil {
b.Fatalf("could not load sample image: %v", err)
}
defer f.Close()
src, _, err := image.Decode(f)
if err != nil {
b.Fatalf("error decoding image: %v", err)
}
b.ResetTimer()
img := imgToNRGBA(src)
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c := NewCarver(width, height)
for i := 0; i < b.N; i++ {
_, err := c.ComputeSeams(p, img)
if err != nil {
b.FailNow()
}
seams := c.FindLowestEnergySeams(p)
img = c.RemoveSeam(img, seams, p.Debug)
}
}
================================================
FILE: carver_test.go
================================================
package caire
import (
"image"
"image/color"
"image/draw"
"os"
"path/filepath"
"testing"
"github.com/esimov/caire/utils"
pigo "github.com/esimov/pigo/core"
"github.com/stretchr/testify/assert"
)
const (
imgWidth = 10
imgHeight = 10
)
var p *Processor
func init() {
p = &Processor{
NewWidth: imgWidth,
NewHeight: imgHeight,
BlurRadius: 1,
SobelThreshold: 4,
Percentage: false,
Square: false,
Debug: false,
}
}
func TestCarver_EnergySeamShouldNotBeDetected(t *testing.T) {
assert := assert.New(t)
var seams [][]Seam
var totalEnergySeams int
img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
var c = NewCarver(dx, dy)
for range imgWidth {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c = NewCarver(width, height)
_, err := c.ComputeSeams(p, img)
assert.NoError(err)
les := c.FindLowestEnergySeams(p)
seams = append(seams, les)
}
for i := range seams {
for s := range seams[i] {
totalEnergySeams += seams[i][s].X
}
}
assert.Equal(0, totalEnergySeams)
}
func TestCarver_DetectHorizontalEnergySeam(t *testing.T) {
var seams [][]Seam
var totalEnergySeams int
img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
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.
// The seam detector should recognize that line as being of low energy density
// and should perform the seam computation process.
// This way we'll make sure, that the seam detector correctly detects one and only one line.
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
for x := 0; x < dx; x++ {
img.Pix[(5*dx+x)*4+0] = 0xdd
img.Pix[(5*dx+x)*4+1] = 0xdd
img.Pix[(5*dx+x)*4+2] = 0xdd
img.Pix[(5*dx+x)*4+3] = 0xdd
}
var c = NewCarver(dx, dy)
for x := 0; x < imgWidth; x++ {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c = NewCarver(width, height)
_, err := c.ComputeSeams(p, img)
assert.NoError(t, err)
les := c.FindLowestEnergySeams(p)
seams = append(seams, les)
}
for i := range seams {
for s := range seams[i] {
totalEnergySeams += seams[i][s].X
}
}
assert.Greater(t, totalEnergySeams, 0)
}
func TestCarver_DetectVerticalEnergySeam(t *testing.T) {
var seams [][]Seam
var totalEnergySeams int
img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
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.
// The seam detector should recognize that line as being of low energy density
// and should perform the seam computation process.
// This way we'll make sure, that the seam detector correctly detects one and only one line.
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
for y := 0; y < dy; y++ {
img.Pix[5*4+(dx*y)*4+0] = 0xdd
img.Pix[5*4+(dx*y)*4+1] = 0xdd
img.Pix[5*4+(dx*y)*4+2] = 0xdd
img.Pix[5*4+(dx*y)*4+3] = 0xff
}
var c = NewCarver(dx, dy)
img = rotateImage90(img)
for x := 0; x < imgHeight; x++ {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c = NewCarver(width, height)
_, err := c.ComputeSeams(p, img)
assert.NoError(t, err)
les := c.FindLowestEnergySeams(p)
seams = append(seams, les)
}
for i := range seams {
for s := range seams[i] {
totalEnergySeams += seams[i][s].X
}
}
assert.Greater(t, totalEnergySeams, 0)
}
func TestCarver_RemoveSeam(t *testing.T) {
img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
bounds := img.Bounds()
// 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.
draw.Draw(img, bounds, &image.Uniform{image.White}, image.Point{}, draw.Src)
origImg := img
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
// Replace the pixels in row 5 with lower intensity colors.
for x := 0; x < dx; x++ {
img.Set(x, 5, color.RGBA{R: 0xdd, G: 0xdd, B: 0xdd, A: 0xff})
}
c := NewCarver(dx, dy)
_, err := c.ComputeSeams(p, img)
assert.NoError(t, err)
seams := c.FindLowestEnergySeams(p)
img = c.RemoveSeam(img, seams, false)
isEq := true
// The test should pass if the detector correctly finds the row which pixel values are of lower intensity.
for x := 0; x < dx; x++ {
for y := 0; y < dy; y++ {
// In case the seam detector correctly recognize the modified line as of low importance
// it should remove it, which means the new image width should be 1px less then the original image.
r0, g0, b0, _ := origImg.At(x, y).RGBA()
r1, g1, b1, _ := img.At(x, y).RGBA()
if r0>>8 != r1>>8 && g0>>8 != g1>>8 && b0>>8 != b1>>8 {
isEq = false
}
}
}
assert.False(t, isEq)
}
func TestCarver_AddSeam(t *testing.T) {
img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
bounds := img.Bounds()
// 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.
draw.Draw(img, bounds, &image.Uniform{image.White}, image.Point{}, draw.Src)
origImg := img
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
// Replace the pixels in row 5 with lower intensity colors.
for x := 0; x < dx; x++ {
img.Set(x, 5, color.RGBA{R: 0xdd, G: 0xdd, B: 0xdd, A: 0xff})
}
c := NewCarver(dx, dy)
_, err := c.ComputeSeams(p, img)
assert.NoError(t, err)
seams := c.FindLowestEnergySeams(p)
img = c.AddSeam(img, seams, false)
dx, dy = img.Bounds().Dx(), img.Bounds().Dy()
isEq := true
// The test should pass if the detector correctly finds the row which has lower intensity colors.
for x := 0; x < dx; x++ {
for y := 0; y < dy; y++ {
r0, g0, b0, _ := origImg.At(x, y).RGBA()
r1, g1, b1, _ := img.At(x, y).RGBA()
if r0>>8 != r1>>8 && g0>>8 != g1>>8 && b0>>8 != b1>>8 {
isEq = false
}
}
}
assert.False(t, isEq)
}
func TestCarver_ComputeSeams(t *testing.T) {
img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
// 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.
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
// Replace the pixels in row 5 with lower intensity colors.
for x := 0; x < dx; x++ {
img.Pix[(5*dx+x)*4+0] = 0xdd
img.Pix[(5*dx+x)*4+1] = 0xdd
img.Pix[(5*dx+x)*4+2] = 0xdd
img.Pix[(5*dx+x)*4+3] = 0xdd
}
c := NewCarver(dx, dy)
_, err := c.ComputeSeams(p, img)
assert.NoError(t, err)
otherThenZero := findNonZeroValue(c.Points)
assert.True(t, otherThenZero)
}
func TestCarver_ShouldDetectFace(t *testing.T) {
p.FaceDetect = true
sampleImg := filepath.Join("./testdata", "sample.jpg")
f, err := os.Open(sampleImg)
if err != nil {
t.Fatalf("could not load sample image: %v", err)
}
defer f.Close()
p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
if err != nil {
t.Fatalf("error unpacking the cascade file: %v", err)
}
src, _, err := image.Decode(f)
if err != nil {
t.Fatalf("error decoding image: %v", err)
}
img := imgToNRGBA(src)
dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y
// Transform the image to a pixel array.
pixels := rgbToGrayscale(img)
cParams := pigo.CascadeParams{
MinSize: 100,
MaxSize: utils.Max(dx, dy),
ShiftFactor: 0.1,
ScaleFactor: 1.1,
ImageParams: pigo.ImageParams{
Pixels: pixels,
Rows: dy,
Cols: dx,
Dim: dx,
},
}
// Run the classifier over the obtained leaf nodes and return the detection results.
// The result contains quadruplets representing the row, column, scale and detection score.
faces := p.FaceDetector.RunCascade(cParams, p.FaceAngle)
// Calculate the intersection over union (IoU) of two clusters.
faces = p.FaceDetector.ClusterDetections(faces, 0.2)
assert.Equal(t, 1, len(faces))
}
func TestCarver_ShouldNotRemoveFaceZone(t *testing.T) {
p.FaceDetect = true
p.BlurRadius = 10
sampleImg := filepath.Join("./testdata", "sample.jpg")
f, err := os.Open(sampleImg)
if err != nil {
t.Fatalf("could not load sample image: %v", err)
}
defer f.Close()
p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
if err != nil {
t.Fatalf("error unpacking the cascade file: %v", err)
}
src, _, err := image.Decode(f)
if err != nil {
t.Fatalf("error decoding image: %v", err)
}
img := imgToNRGBA(src)
dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y
c := NewCarver(dx, dy)
// Transform the image to a pixel array.
pixels := rgbToGrayscale(img)
sobel := c.SobelDetector(img, float64(p.SobelThreshold))
err = Stackblur(img, sobel, uint32(p.BlurRadius))
assert.NoError(t, err)
cParams := pigo.CascadeParams{
MinSize: 100,
MaxSize: utils.Max(dx, dy),
ShiftFactor: 0.1,
ScaleFactor: 1.1,
ImageParams: pigo.ImageParams{
Pixels: pixels,
Rows: dy,
Cols: dx,
Dim: dx,
},
}
// Run the classifier over the obtained leaf nodes and return the detection results.
// The result contains quadruplets representing the row, column, scale and detection score.
faces := p.FaceDetector.RunCascade(cParams, p.FaceAngle)
// Calculate the intersection over union (IoU) of two clusters.
faces = p.FaceDetector.ClusterDetections(faces, 0.2)
// Range over all the detected faces and draw a white rectangle mask over each of them.
// We need to trick the sobel detector to consider them as important image parts.
var rect image.Rectangle
for _, face := range faces {
if face.Q > 5.0 {
rect = image.Rect(
face.Col-face.Scale/2,
face.Row-face.Scale/2,
face.Col+face.Scale/2,
face.Row+face.Scale/2,
)
draw.Draw(sobel, rect, &image.Uniform{image.White}, image.Point{}, draw.Src)
}
}
_, err = c.ComputeSeams(p, img)
assert.Error(t, err)
seams := c.FindLowestEnergySeams(p)
for _, seam := range seams {
if seam.X >= rect.Min.X && seam.X <= rect.Max.X {
t.Errorf("Carver shouldn't remove seams from face zone")
break
}
}
}
func TestCarver_ShouldNotResizeWithFaceDistorsion(t *testing.T) {
p.FaceDetect = true
p.BlurRadius = 10
p.NewHeight = 200
sampleImg := filepath.Join("./testdata", "sample.jpg")
f, err := os.Open(sampleImg)
if err != nil {
t.Fatalf("could not load sample image: %v", err)
}
defer f.Close()
p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
if err != nil {
t.Fatalf("error unpacking the cascade file: %v", err)
}
src, _, err := image.Decode(f)
if err != nil {
t.Fatalf("error decoding image: %v", err)
}
img := imgToNRGBA(src)
dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y
// Transform the image to a pixel array.
pixels := rgbToGrayscale(img)
cParams := pigo.CascadeParams{
MinSize: 100,
MaxSize: utils.Max(dx, dy),
ShiftFactor: 0.1,
ScaleFactor: 1.1,
ImageParams: pigo.ImageParams{
Pixels: pixels,
Rows: dy,
Cols: dx,
Dim: dx,
},
}
// Run the classifier over the obtained leaf nodes and return the detection results.
// The result contains quadruplets representing the row, column, scale and detection score.
faces := p.FaceDetector.RunCascade(cParams, p.FaceAngle)
// Calculate the intersection over union (IoU) of two clusters.
faces = p.FaceDetector.ClusterDetections(faces, 0.2)
for _, face := range faces {
if p.NewHeight < face.Scale {
t.Errorf("Should not resize image without face deformation.")
}
}
}
// findNonZeroValue utility function to check if the slice contains values other then zeros.
func findNonZeroValue(points []float64) bool {
var found = false
for i := 0; i < len(points); i++ {
if points[i] != 0 {
found = true
}
}
return found
}
================================================
FILE: cmd/caire/main.go
================================================
package main
import (
"flag"
"fmt"
"log"
"os"
"runtime"
"gioui.org/app"
"github.com/esimov/caire"
"github.com/esimov/caire/utils"
)
const HelpBanner = `
┌─┐┌─┐┬┬─┐┌─┐
│ ├─┤│├┬┘├┤
└─┘┴ ┴┴┴└─└─┘
Content aware image resize library.
Version: %s
`
// pipeName indicates that stdin/stdout is being used as file names.
const pipeName = "-"
// Version indicates the current build version.
var Version string
var (
// Flags
source = flag.String("in", pipeName, "Source")
destination = flag.String("out", pipeName, "Destination")
blurRadius = flag.Int("blur", 4, "Blur radius")
sobelThreshold = flag.Int("sobel", 2, "Sobel filter threshold")
newWidth = flag.Int("width", 0, "New width")
newHeight = flag.Int("height", 0, "New height")
percentage = flag.Bool("perc", false, "Reduce image by percentage")
square = flag.Bool("square", false, "Reduce image to square dimensions")
debug = flag.Bool("debug", false, "Show the seams")
shapeType = flag.String("shape", "circle", "Shape type used for debugging: circle|line")
seamColor = flag.String("color", "#ff0000", "Seam color")
preview = flag.Bool("preview", true, "Show GUI window")
maskPath = flag.String("mask", "", "Mask file path for retaining area")
rMaskPath = flag.String("rmask", "", "Mask file path for removing area")
faceDetect = flag.Bool("face", false, "Use face detection")
faceAngle = flag.Float64("angle", 0.0, "Face rotation angle")
workers = flag.Int("conc", runtime.NumCPU(), "Number of files to process concurrently")
)
func main() {
log.SetFlags(0)
flag.Usage = func() {
fmt.Fprintf(os.Stderr, fmt.Sprintf(HelpBanner, Version))
flag.PrintDefaults()
}
flag.Parse()
proc := &caire.Processor{
BlurRadius: *blurRadius,
SobelThreshold: *sobelThreshold,
NewWidth: *newWidth,
NewHeight: *newHeight,
Percentage: *percentage,
Square: *square,
Debug: *debug,
Preview: *preview,
FaceDetect: *faceDetect,
FaceAngle: *faceAngle,
MaskPath: *maskPath,
RMaskPath: *rMaskPath,
SeamColor: *seamColor,
ShapeType: caire.ShapeType(*shapeType),
}
if !(*newWidth > 0 || *newHeight > 0 || *percentage || *square) {
flag.Usage()
log.Fatalf("%s%s",
utils.DecorateText("\nPlease provide a width, height or percentage for image rescaling!", utils.ErrorMessage),
utils.DefaultColor,
)
} else {
op := &caire.Image{
Src: *source,
Dst: *destination,
Workers: *workers,
PipeName: pipeName,
}
if *preview {
// When the preview mode is activated we have to execute the resizing process
// in a separate goroutine in order to not block the Gio thread,
// which have to run on the main OS thread of the operating systems like MacOS.
go proc.Execute(op)
app.Main()
} else {
proc.Execute(op)
}
}
}
================================================
FILE: doc.go
================================================
/*
Package caire is a content aware image resize library, which can rescale the source image seamlessly
both vertically and horizontally by eliminating the less important parts of the image.
The package provides a command line interface, supporting various flags for different types of rescaling operations.
To check the supported commands type:
$ caire --help
In case you wish to integrate the API in a self constructed environment here is a simple example:
package main
import (
"fmt"
"github.com/esimov/caire"
)
func main() {
p := &caire.Processor{
// Initialize struct variables
}
if err := p.Process(in, out); err != nil {
fmt.Printf("Error rescaling image: %s", err.Error())
}
}
*/
package caire
================================================
FILE: draw.go
================================================
package caire
import (
"image/color"
"math"
"gioui.org/f32"
"gioui.org/op/clip"
"gioui.org/op/paint"
"github.com/esimov/caire/utils"
)
type ShapeType string
const (
Circle ShapeType = "circle"
Line ShapeType = "line"
)
// DrawSeam visualizes the seam carver in action when the preview mode is activated.
// It receives as parameters the shape type, the seam (x,y) coordinates and it's thickness.
func (g *Gui) DrawSeam(shape ShapeType, x, y, thickness float32) {
r := getRatio(g.cfg.window.width, g.cfg.window.height)
switch shape {
case Circle:
g.drawCircle(x*r, y*r, thickness)
case Line:
g.drawLine(x*r, y*r, thickness)
}
}
// drawCircle draws a circle at the seam (x,y) coordinate with the provided size.
func (g *Gui) drawCircle(x, y, radius float32) {
var (
sq float64
p1 f32.Point
p2 f32.Point
orig = g.point(x-radius, y)
)
sq = math.Sqrt(float64(radius*radius) - float64(radius*radius))
p1 = g.point(x+float32(sq), y).Sub(orig)
p2 = g.point(x-float32(sq), y).Sub(orig)
col := utils.HexToRGBA(g.proc.SeamColor)
g.setFillColor(col)
var path clip.Path
path.Begin(g.ctx.Ops)
path.Move(orig)
path.Arc(p1, p2, 2*math.Pi)
path.Close()
defer clip.Outline{Path: path.End()}.Op().Push(g.ctx.Ops).Pop()
paint.ColorOp{Color: g.setColor(g.getFillColor())}.Add(g.ctx.Ops)
paint.PaintOp{}.Add(g.ctx.Ops)
}
// drawLine draws a line at the seam (x,y) coordinate with the provided line thickness.
func (g *Gui) drawLine(x, y, thickness float32) {
var (
p1 = g.point(x, y)
p2 = g.point(x, y+1)
path clip.Path
)
path.Begin(g.ctx.Ops)
path.Move(p1)
path.Line(p2.Sub(path.Pos()))
path.Close()
col := utils.HexToRGBA(g.proc.SeamColor)
g.setFillColor(col)
defer clip.Stroke{Path: path.End(), Width: float32(thickness)}.Op().Push(g.ctx.Ops).Pop()
paint.ColorOp{Color: g.setColor(g.getFillColor())}.Add(g.ctx.Ops)
paint.PaintOp{}.Add(g.ctx.Ops)
}
// point converts the seam (x,y) coordinate to Gio f32.Point.
func (g *Gui) point(x, y float32) f32.Point {
return f32.Point{
X: x,
Y: y,
}
}
// setColor sets the seam color.
func (g *Gui) setColor(c color.Color) color.NRGBA {
rc, gc, bc, ac := c.RGBA()
return color.NRGBA{
R: uint8(rc >> 8),
G: uint8(gc >> 8),
B: uint8(bc >> 8),
A: uint8(ac >> 8),
}
}
// setFillColor sets the paint fill color.
func (g *Gui) setFillColor(c color.Color) {
g.cfg.color.fill = c
}
// getFillColor retrieve the paint fill color.
func (g *Gui) getFillColor() color.Color {
return g.cfg.color.fill
}
// getRatio returns the image aspect ratio.
func getRatio(w, h float32) float32 {
var r float32 = 1
if w > maxScreenX && h > maxScreenY {
wr := maxScreenX / float32(w) // width ratio
hr := maxScreenY / float32(h) // height ratio
r = utils.Max(wr, hr)
}
return r
}
================================================
FILE: exec.go
================================================
package caire
import (
"errors"
"fmt"
"image"
"io"
"log"
"os"
"os/signal"
"path/filepath"
"runtime"
"sync"
"syscall"
"time"
"slices"
"github.com/esimov/caire/utils"
"golang.org/x/term"
)
var (
// imgFile holds the file being accessed, be it normal file or pipe name.
imgFile *os.File
// Common file related variable
fs os.FileInfo
)
type Image struct {
Src, Dst, PipeName string
Workers int
}
// result holds the relevant information about the resizing process and the generated image.
type result struct {
path string
err error
}
func Resize(s SeamCarver, img *image.NRGBA) (image.Image, error) {
return s.Resize(img)
}
// Execute executes the image resizing process.
// In case the preview mode is activated it will be invoked in a separate goroutine
// in order to avoid blocking the main OS thread. Otherwise it will be called normally.
func (p *Processor) Execute(img *Image) {
var err error
defaultMsg := fmt.Sprintf("%s %s",
utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
utils.DecorateText("⇢ resizing image (be patient, it may take a while)...", utils.DefaultMessage),
)
p.Spinner = utils.NewSpinner(defaultMsg, time.Millisecond*80)
// Supported files
validExtensions := []string{".jpg", ".png", ".jpeg", ".bmp", ".gif"}
// Check if source path is a local image or URL.
if utils.IsValidUrl(img.Src) {
src, err := utils.DownloadImage(img.Src)
if src != nil {
defer os.Remove(src.Name())
}
if err != nil {
log.Fatalf(
utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage),
utils.DecorateText(err.Error(), utils.DefaultMessage),
)
}
fs, err = src.Stat()
if err != nil {
log.Fatalf(
utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage),
utils.DecorateText(err.Error(), utils.DefaultMessage),
)
}
img, err := os.Open(src.Name())
if err != nil {
log.Fatalf(
utils.DecorateText("Unable to open the temporary image file: %v", utils.ErrorMessage),
utils.DecorateText(err.Error(), utils.DefaultMessage),
)
}
imgFile = img
} else {
// Check if the source is a pipe name or a regular file.
if img.Src == img.PipeName {
fs, err = os.Stdin.Stat()
} else {
fs, err = os.Stat(img.Src)
}
if err != nil {
log.Fatalf(
utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage),
utils.DecorateText(err.Error(), utils.DefaultMessage),
)
}
}
now := time.Now()
switch mode := fs.Mode(); {
case mode.IsDir():
var wg sync.WaitGroup
// Read destination file or directory.
_, err := os.Stat(img.Dst)
if err != nil {
err = os.Mkdir(img.Dst, 0755)
if err != nil {
log.Fatalf(
utils.DecorateText("Unable to get dir stats: %v\n", utils.ErrorMessage),
utils.DecorateText(err.Error(), utils.DefaultMessage),
)
}
}
p.Preview = false
// Limit the concurrently running workers to maxWorkers.
if img.Workers <= 0 || img.Workers > runtime.NumCPU() {
img.Workers = runtime.NumCPU()
}
// Process recursively the image files from the specified directory concurrently.
ch := make(chan result)
done := make(chan any)
defer close(done)
paths, errc := walkDir(done, img.Src, validExtensions)
wg.Add(img.Workers)
for range img.Workers {
go func() {
defer wg.Done()
img.consumer(p, img.Dst, ch, done, paths)
}()
}
// Close the channel after the values are consumed.
go func() {
defer close(ch)
wg.Wait()
}()
// Consume the channel values.
for res := range ch {
if res.err != nil {
err = res.err
}
img.printOpStatus(res.path, err)
}
if err = <-errc; err != nil {
fmt.Fprintf(os.Stderr, utils.DecorateText(err.Error(), utils.ErrorMessage))
}
case mode.IsRegular() || mode&os.ModeNamedPipe != 0: // check for regular files or pipe names
ext := filepath.Ext(img.Dst)
if !slices.Contains(validExtensions, ext) && img.Dst != img.PipeName {
log.Fatalf(utils.DecorateText(fmt.Sprintf("%v file type not supported", ext), utils.ErrorMessage))
}
err = img.process(p, img.Src, img.Dst)
img.printOpStatus(img.Dst, err)
}
if err == nil {
fmt.Fprintf(os.Stderr, "\nExecution time: %s\n", utils.DecorateText(
utils.FormatTime(time.Since(now)), utils.SuccessMessage),
)
}
}
// consumer reads the path names from the paths channel and calls the resizing processor against the source image.
func (img *Image) consumer(
p *Processor,
dest string,
res chan<- result,
done <-chan any,
paths <-chan string,
) {
for src := range paths {
dst := filepath.Join(dest, filepath.Base(src))
err := img.process(p, src, dst)
select {
case <-done:
return
case res <- result{
path: src,
err: err,
}:
}
}
}
// processor calls the resizer method over the source image and returns the error in case exists.
func (img *Image) process(p *Processor, in, out string) error {
var (
successMsg string
errorMsg string
)
// Start the progress indicator.
p.Spinner.Start()
successMsg = fmt.Sprintf("%s %s %s",
utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
utils.DecorateText("⇢", utils.DefaultMessage),
utils.DecorateText("the image has been resized successfully ✔", utils.SuccessMessage),
)
errorMsg = fmt.Sprintf("%s %s %s",
utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
utils.DecorateText("resizing image failed...", utils.DefaultMessage),
utils.DecorateText("✘", utils.ErrorMessage),
)
src, dst, err := img.pathToFile(in, out)
if err != nil {
p.Spinner.StopMsg = errorMsg
return err
}
// Capture CTRL-C signal and restores back the cursor visibility.
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-signalChan
func() {
p.Spinner.RestoreCursor()
os.Remove(dst.(*os.File).Name())
os.Exit(1)
}()
}()
defer func() {
if img, ok := src.(*os.File); ok {
if err := img.Close(); err != nil {
log.Printf("could not close the opened file: %v", err)
}
}
}()
defer func() {
if img, ok := dst.(*os.File); ok {
if err := img.Close(); err != nil {
log.Printf("could not close the opened file: %v", err)
}
}
}()
if len(p.MaskPath) > 0 {
mask, err := decodeImg(p.MaskPath)
if err != nil {
return fmt.Errorf("cannot decode image: %w", err)
}
p.Mask = dither(imgToNRGBA(mask))
p.DebugMask = p.Mask
}
if len(p.RMaskPath) > 0 {
rmask, err := decodeImg(p.RMaskPath)
if err != nil {
return fmt.Errorf("cannot decode image: %w", err)
}
p.RMask = dither(imgToNRGBA(rmask))
p.DebugMask = p.RMask
}
err = p.Process(src, dst)
if err != nil {
// remove the generated image file in case of an error
os.Remove(dst.(*os.File).Name())
p.Spinner.StopMsg = errorMsg
// Stop the progress indicator.
p.Spinner.Stop()
return err
} else {
p.Spinner.StopMsg = successMsg
// Stop the progress indicator.
p.Spinner.Stop()
}
return nil
}
// pathToFile converts the source and destination paths to readable and writable files.
func (img *Image) pathToFile(in, out string) (io.Reader, io.Writer, error) {
var (
src io.Reader
dst io.Writer
err error
)
// Check if the source path is a local image or URL.
if utils.IsValidUrl(in) {
src = imgFile
} else {
// Check if the source is a pipe name or a regular file.
if in == img.PipeName {
if term.IsTerminal(int(os.Stdin.Fd())) {
return nil, nil, errors.New("`-` should be used with a pipe for stdin")
}
src = os.Stdin
} else {
src, err = os.Open(in)
if err != nil {
return nil, nil, fmt.Errorf("unable to open the source file: %v", err)
}
}
}
// Check if the destination is a pipe name or a regular file.
if out == img.PipeName {
if term.IsTerminal(int(os.Stdout.Fd())) {
return nil, nil, errors.New("`-` should be used with a pipe for stdout")
}
dst = os.Stdout
} else {
dst, err = os.OpenFile(out, os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
return nil, nil, fmt.Errorf("unable to create the destination file: %v", err)
}
}
return src, dst, nil
}
// printOpStatus displays the relevant information about the image resizing process.
func (img *Image) printOpStatus(fname string, err error) {
if err != nil {
log.Fatalf(
utils.DecorateText("\nError resizing the image: %s", utils.ErrorMessage),
utils.DecorateText(fmt.Sprintf("\n\tReason: %v\n", err.Error()), utils.DefaultMessage),
)
} else {
if fname != img.PipeName {
fmt.Fprintf(os.Stderr, "\nThe image has been saved as: %s %s\n\n",
utils.DecorateText(filepath.Base(fname), utils.SuccessMessage),
utils.DefaultColor,
)
}
}
}
// walkDir starts a new goroutine to walk the specified directory tree
// in recursive manner and sends the path of each regular file to a new channel.
// It finishes in case the done channel is getting closed.
func walkDir(
done <-chan any,
src string,
srcExts []string,
) (<-chan string, <-chan error) {
pathChan := make(chan string)
errChan := make(chan error, 1)
go func() {
// Close the paths channel after Walk returns.
defer close(pathChan)
errChan <- filepath.Walk(src, func(path string, f os.FileInfo, err error) error {
isFileSupported := false
if err != nil {
return err
}
if !f.Mode().IsRegular() {
return nil
}
// Get the file base name.
fx := filepath.Ext(f.Name())
if slices.Contains(srcExts, fx) {
isFileSupported = true
}
if isFileSupported {
select {
case <-done:
return errors.New("directory walk cancelled")
case pathChan <- path:
}
}
return nil
})
}()
return pathChan, errChan
}
================================================
FILE: go.mod
================================================
module github.com/esimov/caire
go 1.22
require (
gioui.org v0.8.0
github.com/disintegration/imaging v1.6.2
github.com/esimov/pigo v1.4.5
github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20240707233637-46b078467d37
golang.org/x/image v0.23.0
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
)
require (
gioui.org/shader v1.0.8 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
gioui.org v0.8.0 h1:QV5p5JvsmSmGiIXVYOKn6d9YDliTfjtLlVf5J+BZ9Pg=
gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/esimov/pigo v1.4.5 h1:ySG0QqMh02VNALvHnx04L1ScRu66N6XA5vLLga8GiLg=
github.com/esimov/pigo v1.4.5/go.mod h1:SGkOUpm4wlEmQQJKlaymAkThY8/8iP+XE0gFo7g8G6w=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: gui.go
================================================
package caire
import (
"fmt"
"image"
"image/color"
"image/draw"
"math"
"math/rand"
"time"
"gioui.org/app"
"gioui.org/f32"
"gioui.org/font/gofont"
"gioui.org/io/key"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/esimov/caire/imop"
"github.com/esimov/caire/utils"
)
type hudControlType int
const (
hudShowSeams hudControlType = iota
hudShowDebugMask
)
const (
// The starting colors for the linear gradient, used when the image is resized both horizontally and vertically.
// In this case the preview mode is deactivated and a dynamic gradient overlay is shown.
redStart = 137
greenStart = 47
blueStart = 54
// The ending colors for the linear gradient. The starting colors and ending colors are lerped.
redEnd = 255
greenEnd = 112
blueEnd = 105
)
var (
maxScreenX float32 = 1280
maxScreenY float32 = 720
defaultBkgColor = color.Transparent
defaultFillColor = color.Black
)
type interval struct {
min, max float64
}
// Gui is the basic struct containing all of the information needed for the UI operation.
// It receives the resized image transferred through a channel which is called in a separate goroutine.
type Gui struct {
cfg struct {
x interval
y interval
chrot bool
angle float32
window struct {
width float32
height float32
title string
}
color struct {
randR uint8
randG uint8
randB uint8
background color.Color
fill color.Color
}
timeStamp time.Time
}
process struct {
isDone bool
img image.Image
seams []Seam
worker <-chan worker
err chan<- error
}
proc *Processor
compOp *imop.Composite
blendOp *imop.Blend
theme *material.Theme
ctx layout.Context
huds map[hudControlType]*hudCtrl
view struct {
huds layout.List
}
}
type hudCtrl struct {
enabled widget.Bool
hudType hudControlType
title string
}
// NewGUI initializes the Gio interface.
func NewGUI(width, height int) *Gui {
defaultColor := color.NRGBA{R: 0x2d, G: 0x23, B: 0x2e, A: 0xff}
gui := &Gui{
ctx: layout.Context{
Ops: new(op.Ops),
Constraints: layout.Constraints{
Max: image.Pt(width, height),
},
},
compOp: imop.InitOp(),
blendOp: imop.NewBlend(),
theme: material.NewTheme(),
huds: make(map[hudControlType]*hudCtrl),
}
gui.theme.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
gui.theme.TextSize = unit.Sp(16)
gui.theme.Palette.ContrastBg = defaultColor
gui.theme.FingerSize = 10
gui.initWindow(width, height)
return gui
}
// AddHudControl adds a new hud control for debugging.
func (g *Gui) AddHudControl(hudControlType hudControlType, title string, enabled bool) {
control := &hudCtrl{
hudType: hudControlType,
title: title,
enabled: widget.Bool{},
}
control.enabled.Value = enabled
g.huds[hudControlType] = control
}
// initWindow creates and initializes the GUI window.
func (g *Gui) initWindow(width, height int) {
rand.NewSource(time.Now().UnixNano())
g.cfg.angle = 45
g.cfg.color.randR = uint8(random(1, 2))
g.cfg.color.randG = uint8(random(1, 2))
g.cfg.color.randB = uint8(random(1, 2))
g.cfg.window.width, g.cfg.window.height = float32(width), float32(height)
g.cfg.x = interval{min: 0, max: float64(width)}
g.cfg.y = interval{min: 0, max: float64(height)}
g.cfg.color.background = defaultBkgColor
g.cfg.color.fill = defaultFillColor
if !resizeXY {
g.cfg.window.width, g.cfg.window.height = g.getWindowSize()
}
g.cfg.window.title = "Preview process..."
}
// getWindowSize returns the resized image dimension.
func (g *Gui) getWindowSize() (float32, float32) {
w, h := g.cfg.window.width, g.cfg.window.height
// Maintain the image aspect ratio in case the image width and height is greater than the predefined window.
r := getRatio(w, h)
if w > maxScreenX && h > maxScreenY {
w = w * r
h = h * r
}
return w, h
}
// Run is the core method of the Gio GUI application.
// This updates the window with the resized image received from a channel
// and terminates when the image resizing operation completes.
func (g *Gui) Run() error {
var (
rc uint8 = redStart
gc uint8 = greenStart
bc uint8 = blueStart
descRed, descGreen, descBlue bool
)
width := unit.Dp(g.cfg.window.width)
height := unit.Dp(g.cfg.window.height)
w := new(app.Window)
w.Option(
app.Title(g.cfg.window.title),
app.Size(width, height),
app.MinSize(width, height),
app.MaxSize(width, height),
)
// Center the window.
w.Perform(system.ActionCenter)
g.cfg.timeStamp = time.Now()
if g.proc.Debug {
g.AddHudControl(hudShowSeams, "Show seams", true)
if len(g.proc.MaskPath) > 0 || len(g.proc.RMaskPath) > 0 || g.proc.FaceDetect {
g.AddHudControl(hudShowDebugMask, "Debug mode", false)
}
}
abortFn := func() {
var dx, dy int
if g.process.img != nil {
bounds := g.process.img.Bounds()
dx, dy = bounds.Max.X, bounds.Max.Y
}
if !g.process.isDone {
if (g.proc.NewWidth > 0 && g.proc.NewWidth != dx) ||
(g.proc.NewHeight > 0 && g.proc.NewHeight != dy) {
errorMsg := fmt.Sprintf("%s %s %s",
utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
utils.DecorateText("⇢ process aborted by the user...", utils.DefaultMessage),
utils.DecorateText("✘\n", utils.ErrorMessage),
)
g.proc.Spinner.StopMsg = errorMsg
g.proc.Spinner.Stop()
}
}
g.proc.Spinner.RestoreCursor()
}
for {
select {
case res := <-g.process.worker:
if res.done {
w.Option(app.Title("Done!"))
g.process.isDone = true
break
}
if resizeXY {
continue
}
g.process.img = res.img
g.process.seams = res.seams
if mask, ok := g.huds[hudShowDebugMask]; ok {
if mask.enabled.Value && res.mask != nil {
bounds := res.img.Bounds()
srcBitmap := imop.NewBitmap(bounds)
dstBitmap := imop.NewBitmap(bounds)
uniformCol := image.NewNRGBA(bounds)
col := color.RGBA{R: 0x2f, G: 0xf3, B: 0xe0, A: 0xff}
draw.Draw(uniformCol, uniformCol.Bounds(), &image.Uniform{col}, image.Point{}, draw.Src)
_ = g.compOp.Set(imop.DstIn)
g.compOp.Draw(srcBitmap, res.mask, uniformCol, nil)
_ = g.blendOp.Set(imop.Screen)
_ = g.compOp.Set(imop.SrcAtop)
g.compOp.Draw(dstBitmap, res.img, srcBitmap.Img, g.blendOp)
g.process.img = dstBitmap.Img
}
}
if g.proc.vRes {
g.process.img = rotateImage270(g.process.img.(*image.NRGBA))
}
w.Invalidate()
default:
switch e := w.Event().(type) {
case app.FrameEvent:
g.ctx = app.NewContext(g.ctx.Ops, e)
for {
event, ok := g.ctx.Event(key.Filter{
Name: key.NameEscape,
})
if !ok {
break
}
switch event := event.(type) {
case key.Event:
switch event.Name {
case key.NameEscape:
w.Perform(system.ActionClose)
abortFn()
return nil
}
}
}
{ // red
if descRed {
rc--
} else {
rc++
}
if rc >= redEnd {
descRed = !descRed
}
if rc == redStart {
descRed = !descRed
}
}
{ // green
if descGreen {
gc--
} else {
gc++
}
if gc >= greenEnd {
descGreen = !descGreen
}
if gc == greenStart {
descGreen = !descGreen
}
}
{ // blue
if descBlue {
bc--
} else {
bc++
}
if bc >= blueEnd {
descBlue = !descBlue
}
if bc == blueStart {
descBlue = !descBlue
}
}
g.draw(color.NRGBA{R: rc, G: gc, B: bc})
e.Frame(g.ctx.Ops)
case app.DestroyEvent:
abortFn()
return e.Err
}
}
}
}
type (
C = layout.Context
D = layout.Dimensions
)
// draw draws the resized image in the GUI window (obtained from a channel)
// and in case the debug mode is activated it prints out the seams.
func (g *Gui) draw(bgColor color.NRGBA) {
g.ctx.Execute(op.InvalidateCmd{})
c := g.setColor(g.cfg.color.background)
paint.Fill(g.ctx.Ops, c)
if g.process.img != nil {
src := paint.NewImageOp(g.process.img)
src.Add(g.ctx.Ops)
layout.Stack{}.Layout(g.ctx,
layout.Stacked(func(gtx C) D {
paint.FillShape(gtx.Ops, c,
clip.Rect{Max: g.ctx.Constraints.Max}.Op(),
)
return layout.UniformInset(unit.Dp(0)).Layout(gtx,
func(gtx C) D {
widget.Image{
Src: src,
Scale: 1 / float32(unit.Dp(1)),
Fit: widget.Contain,
}.Layout(gtx)
if seam, ok := g.huds[hudShowSeams]; ok {
if seam.enabled.Value {
tr := f32.Affine2D{}
screen := layout.FPt(g.ctx.Constraints.Max)
width, height := float32(g.process.img.Bounds().Dx()), float32(g.process.img.Bounds().Dy())
sw, sh := float32(screen.X), float32(screen.Y)
if sw > width {
ratio := sw / width
tr = tr.Scale(f32.Pt(sw/2, sh/2), f32.Pt(1, ratio))
} else if sh > height {
ratio := sh / height
tr = tr.Scale(f32.Pt(sw/2, sh/2), f32.Pt(ratio, 1))
}
if g.proc.vRes {
angle := float32(270 * math.Pi / 180)
half := float32(math.Round(float64(sh*0.5-height*0.5) * 0.5))
ox := math.Abs(float64(sw - (sw - (sw/2 - sh/2))))
oy := math.Abs(float64(sh - (sh - (sw/2 - height/2 + half))))
tr = tr.Rotate(f32.Pt(sw/2, sh/2), -angle)
if screen.X > screen.Y {
tr = tr.Offset(f32.Pt(float32(ox), float32(oy)))
} else {
tr = tr.Offset(f32.Pt(float32(-ox), float32(-oy)))
}
}
op.Affine(tr).Add(gtx.Ops)
for _, s := range g.process.seams {
dpx := gtx.Dp(unit.Dp(s.X))
dpy := gtx.Dp(unit.Dp(s.Y))
g.DrawSeam(g.proc.ShapeType, float32(dpx), float32(dpy), 1.0)
}
}
}
return layout.Dimensions{Size: gtx.Constraints.Max}
})
}),
)
}
if g.proc.Debug {
layout.Stack{}.Layout(g.ctx,
layout.Stacked(func(gtx C) D {
hudHeight := 30
r := image.Rectangle{
Max: image.Point{
X: gtx.Constraints.Max.X,
Y: hudHeight,
},
}
defer op.Offset(image.Pt(0, gtx.Constraints.Max.Y-hudHeight)).Push(gtx.Ops).Pop()
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx C) D {
paint.FillShape(gtx.Ops, color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xcc}, clip.Rect(r).Op())
return layout.Dimensions{Size: r.Max}
}),
layout.Stacked(func(gtx C) D {
border := image.Rectangle{
Max: image.Point{
X: gtx.Constraints.Max.X,
Y: gtx.Dp(unit.Dp(0.5)),
},
}
paint.FillShape(gtx.Ops, color.NRGBA{R: 0xd0, G: 0xcd, B: 0xd7, A: 0xaa}, clip.Rect(border).Op())
return layout.Dimensions{Size: r.Max}
}),
layout.Stacked(func(gtx C) D {
return g.view.huds.Layout(gtx, len(g.huds),
func(gtx layout.Context, index int) D {
if hud, ok := g.huds[hudControlType(index)]; ok {
checkbox := material.CheckBox(g.theme, &hud.enabled, fmt.Sprintf("%v", hud.title))
checkbox.Size = 20
return checkbox.Layout(gtx)
}
return D{}
})
}),
)
}),
)
}
// Disable the preview mode and warn the user in case the image is resized both horizontally and vertically.
if resizeXY {
var msg string
if !g.process.isDone {
msg = "Preview is not available while the image is resized both horizontally and vertically!"
} else {
msg = "Done, you may close this window!"
bgColor = color.NRGBA{R: 45, G: 45, B: 42, A: 0xff}
}
g.displayMessage(g.ctx, bgColor, msg)
}
}
// displayMessage show a static message when the image is resized both horizontally and vertically.
func (g *Gui) displayMessage(ctx layout.Context, bgCol color.NRGBA, msg string) {
g.theme.Palette.Fg = color.NRGBA{R: 251, G: 254, B: 249, A: 0xff}
paint.ColorOp{Color: bgCol}.Add(ctx.Ops)
rect := image.Rectangle{
Max: ctx.Constraints.Max,
}
defer clip.Rect(rect).Push(ctx.Ops).Pop()
paint.PaintOp{}.Add(ctx.Ops)
layout.Stack{}.Layout(ctx,
layout.Stacked(func(gtx C) D {
return layout.UniformInset(unit.Dp(4)).Layout(ctx, func(gtx C) D {
if !g.process.isDone {
gtx.Constraints.Min.Y = 0
tr := f32.Affine2D{}
dr := image.Rectangle{Max: gtx.Constraints.Min}
tr = tr.Rotate(f32.Pt(float32(ctx.Constraints.Max.X/2), float32(ctx.Constraints.Max.Y/2)), 0.005*-g.cfg.angle)
op.Affine(tr).Add(gtx.Ops)
since := time.Since(g.cfg.timeStamp)
if since.Seconds() > 5 {
g.cfg.timeStamp = time.Now()
g.cfg.color.randR = uint8(random(1, 2))
g.cfg.color.randG = uint8(random(1, 2))
g.cfg.color.randB = uint8(random(1, 2))
}
paint.LinearGradientOp{
Stop1: layout.FPt(dr.Min.Div(2)),
Stop2: layout.FPt(dr.Max.Mul(2)),
Color1: color.NRGBA{R: 41, G: bgCol.G * g.cfg.color.randG, B: bgCol.B * g.cfg.color.randB, A: 0xFF},
Color2: color.NRGBA{R: bgCol.R * g.cfg.color.randR, G: 29, B: 54, A: 0xFF},
}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
if g.cfg.chrot {
g.cfg.angle--
} else {
g.cfg.angle++
}
if g.cfg.angle == -90 || g.cfg.angle == 90 {
g.cfg.chrot = !g.cfg.chrot
}
}
return layout.Dimensions{
Size: gtx.Constraints.Max,
}
})
}),
layout.Stacked(func(gtx C) D {
return layout.UniformInset(unit.Dp(4)).Layout(ctx, func(gtx C) D {
return layout.Center.Layout(ctx, func(gtx C) D {
m := material.Label(g.theme, unit.Sp(40), msg)
m.Alignment = text.Middle
return m.Layout(gtx)
})
})
}),
layout.Stacked(func(gtx C) D {
info := "(You will be notified once the process is finished.)"
if g.process.isDone {
return layout.Dimensions{}
}
return layout.Inset{Top: 70}.Layout(ctx, func(gtx C) D {
return layout.Center.Layout(ctx, func(gtx C) D {
return material.Label(g.theme, unit.Sp(13), info).Layout(gtx)
})
})
}),
)
}
// random generates a random number between two numbers.
func random(min, max float32) float32 {
return rand.Float32()*(max-min) + min
}
================================================
FILE: image.go
================================================
package caire
import (
"errors"
"fmt"
"image"
"image/color"
"image/jpeg"
"image/png"
"io"
"os"
"path/filepath"
"strings"
"github.com/esimov/caire/utils"
"golang.org/x/image/bmp"
)
// decodeImg decodes an image file to type image.Image
func decodeImg(src string) (image.Image, error) {
file, err := os.Open(src)
if err != nil {
return nil, fmt.Errorf("could not open the mask file: %v", err)
}
ctype, err := utils.DetectContentType(file.Name())
if err != nil {
return nil, err
}
if !strings.Contains(ctype.(string), "image") {
return nil, fmt.Errorf("the mask should be an image file")
}
img, _, err := image.Decode(file)
if err != nil {
return nil, fmt.Errorf("could not decode the mask file: %v", err)
}
return img, nil
}
// encodeImg encodes an image to a destination of type io.Writer.
func encodeImg(p *Processor, w io.Writer, img *image.NRGBA) error {
switch w := w.(type) {
case *os.File:
ext := filepath.Ext(w.Name())
switch ext {
case "", ".jpg", ".jpeg":
res, err := Resize(p, img)
if err != nil {
return err
}
return jpeg.Encode(w, res, &jpeg.Options{Quality: 100})
case ".png":
res, err := Resize(p, img)
if err != nil {
return err
}
return png.Encode(w, res)
case ".bmp":
res, err := Resize(p, img)
if err != nil {
return err
}
return bmp.Encode(w, res)
default:
return errors.New("unsupported image format")
}
default:
res, err := Resize(p, img)
if err != nil {
return err
}
return jpeg.Encode(w, res, &jpeg.Options{Quality: 100})
}
}
// rotateImage90 rotate the image by 90 degree counter clockwise.
func rotateImage90(src *image.NRGBA) *image.NRGBA {
b := src.Bounds()
dst := image.NewNRGBA(image.Rect(0, 0, b.Max.Y, b.Max.X))
for dstY := 0; dstY < b.Max.X; dstY++ {
for dstX := 0; dstX < b.Max.Y; dstX++ {
srcX := b.Max.X - dstY - 1
srcY := dstX
srcOff := srcY*src.Stride + srcX*4
dstOff := dstY*dst.Stride + dstX*4
copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
}
}
return dst
}
// rotateImage270 rotate the image by 270 degree counter clockwise.
func rotateImage270(src *image.NRGBA) *image.NRGBA {
b := src.Bounds()
dst := image.NewNRGBA(image.Rect(0, 0, b.Max.Y, b.Max.X))
for dstY := 0; dstY < b.Max.X; dstY++ {
for dstX := 0; dstX < b.Max.Y; dstX++ {
srcX := dstY
srcY := b.Max.Y - dstX - 1
srcOff := srcY*src.Stride + srcX*4
dstOff := dstY*dst.Stride + dstX*4
copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
}
}
return dst
}
// imgToNRGBA converts any image type to *image.NRGBA with min-point at (0, 0).
func imgToNRGBA(img image.Image) *image.NRGBA {
srcBounds := img.Bounds()
if srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 {
if src0, ok := img.(*image.NRGBA); ok {
return src0
}
}
srcMinX := srcBounds.Min.X
srcMinY := srcBounds.Min.Y
dstBounds := srcBounds.Sub(srcBounds.Min)
dstW := dstBounds.Dx()
dstH := dstBounds.Dy()
dst := image.NewNRGBA(dstBounds)
switch src := img.(type) {
case *image.NRGBA:
rowSize := srcBounds.Dx() * 4
for dstY := 0; dstY < dstH; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize])
}
}
case *image.YCbCr:
for dstY := 0; dstY < dstH; dstY++ {
di := dst.PixOffset(0, dstY)
for dstX := 0; dstX < dstW; dstX++ {
srcX := srcMinX + dstX
srcY := srcMinY + dstY
siy := src.YOffset(srcX, srcY)
sic := src.COffset(srcX, srcY)
r, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic])
dst.Pix[di+0] = r
dst.Pix[di+1] = g
dst.Pix[di+2] = b
dst.Pix[di+3] = 0xff
di += 4
}
}
default:
for dstY := 0; dstY < dstH; dstY++ {
di := dst.PixOffset(0, dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)
dst.Pix[di+0] = c.R
dst.Pix[di+1] = c.G
dst.Pix[di+2] = c.B
dst.Pix[di+3] = c.A
di += 4
}
}
}
return dst
}
// imgToPix converts an image to a pixel array.
func imgToPix(src *image.NRGBA) []uint8 {
bounds := src.Bounds()
pixels := make([]uint8, 0, bounds.Max.X*bounds.Max.Y*4)
for x := bounds.Min.X; x < bounds.Max.X; x++ {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
r, g, b, _ := src.At(y, x).RGBA()
pixels = append(pixels, uint8(r>>8), uint8(g>>8), uint8(b>>8), 255)
}
}
return pixels
}
// pixToImage converts an array buffer to an image.
func pixToImage(pixels []uint8, width, height int) image.Image {
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
bounds := dst.Bounds()
dx, dy := bounds.Max.X, bounds.Max.Y
col := color.NRGBA{
R: uint8(0),
G: uint8(0),
B: uint8(0),
A: uint8(255),
}
for x := bounds.Min.X; x < dx; x++ {
for y := bounds.Min.Y; y < dy*4; y += 4 {
col.R = uint8(pixels[y+x*dy*4])
col.G = uint8(pixels[y+x*dy*4+1])
col.B = uint8(pixels[y+x*dy*4+2])
col.A = uint8(pixels[y+x*dy*4+3])
dst.SetNRGBA(x, int(y/4), col)
}
}
return dst
}
// rgbToGrayscale converts an image to grayscale mode and
// returns the pixel values as an one dimensional array.
func rgbToGrayscale(src *image.NRGBA) []uint8 {
width, height := src.Bounds().Dx(), src.Bounds().Dy()
gray := make([]uint8, width*height)
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
r, g, b, _ := src.At(x, y).RGBA()
gray[y*width+x] = uint8(
(0.299*float64(r) +
0.587*float64(g) +
0.114*float64(b)) / 256,
)
}
}
return gray
}
// dither converts an image to black and white image, where the white is fully transparent.
func dither(src *image.NRGBA) *image.NRGBA {
var (
bounds = src.Bounds()
dithered = image.NewNRGBA(bounds)
dx = bounds.Dx()
dy = bounds.Dy()
)
for x := 0; x < dx; x++ {
for y := 0; y < dy; y++ {
r, g, b, _ := src.At(x, y).RGBA()
threshold := func() color.Color {
if r > 127 && g > 127 && b > 127 {
return color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
}
return color.NRGBA{A: 0x00}
}
dithered.Set(x, y, threshold())
}
}
return dithered
}
================================================
FILE: image_test.go
================================================
package caire
import (
"image"
"image/color"
"image/color/palette"
"image/draw"
"os"
"path/filepath"
"testing"
"github.com/esimov/caire/utils"
)
func TestImage_ShouldGetSampleImage(t *testing.T) {
path := filepath.Join("./testdata", "sample.jpg")
_, err := os.ReadFile(path)
if err != nil {
t.Errorf("Should get the sample image")
}
}
func TestImage_ImgToNRGBA(t *testing.T) {
rect := image.Rect(-1, -1, 15, 15)
colors := palette.Plan9
testCases := []struct {
name string
img image.Image
}{
{
name: "NRGBA",
img: makeNRGBAImage(rect, colors),
},
{
name: "YCbCr-444",
img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio444),
},
{
name: "YCbCr-422",
img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio422),
},
{
name: "YCbCr-420",
img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio420),
},
{
name: "YCbCr-440",
img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio440),
},
{
name: "YCbCr-410",
img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio410),
},
{
name: "YCbCr-411",
img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio411),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := tc.img.Bounds()
for y := r.Min.Y; y < r.Max.Y; y++ {
buf := make([]byte, r.Dx()*4)
scan(tc.img, 0, y-r.Min.Y, r.Dx(), y+1-r.Min.Y, buf)
wantBuf := readRow(tc.img, y)
if !compareBytes(buf, wantBuf, 1) {
t.Errorf("scan horizontal line (y=%d): got %v want %v", y, buf, wantBuf)
}
}
for x := r.Min.X; x < r.Max.X; x++ {
buf := make([]byte, r.Dy()*4)
scan(tc.img, x-r.Min.X, 0, x+1-r.Min.X, r.Dy(), buf)
wantBuf := readColumn(tc.img, x)
if !compareBytes(buf, wantBuf, 1) {
t.Errorf("scan vertical line (x=%d): got %v want %v", x, buf, wantBuf)
}
}
})
}
}
func scan(img image.Image, x1, y1, x2, y2 int, dst []uint8) {
switch img := img.(type) {
case *image.NRGBA:
size := (x2 - x1) * 4
j := 0
i := y1*img.Stride + x1*4
for y := y1; y < y2; y++ {
copy(dst[j:j+size], img.Pix[i:i+size])
j += size
i += img.Stride
}
case *image.YCbCr:
j := 0
x1 += img.Rect.Min.X
x2 += img.Rect.Min.X
y1 += img.Rect.Min.Y
y2 += img.Rect.Min.Y
for y := y1; y < y2; y++ {
iy := (y-img.Rect.Min.Y)*img.YStride + (x1 - img.Rect.Min.X)
for x := x1; x < x2; x++ {
var ic int
switch img.SubsampleRatio {
case image.YCbCrSubsampleRatio444:
ic = (y-img.Rect.Min.Y)*img.CStride + (x - img.Rect.Min.X)
case image.YCbCrSubsampleRatio422:
ic = (y-img.Rect.Min.Y)*img.CStride + (x/2 - img.Rect.Min.X/2)
case image.YCbCrSubsampleRatio420:
ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x/2 - img.Rect.Min.X/2)
case image.YCbCrSubsampleRatio440:
ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x - img.Rect.Min.X)
default:
ic = img.COffset(x, y)
}
yy := int(img.Y[iy])
cb := int(img.Cb[ic]) - 128
cr := int(img.Cr[ic]) - 128
r := (yy<<16 + 91881*cr + 1<<15) >> 16
if r > 0xff {
r = 0xff
} else if r < 0 {
r = 0
}
g := (yy<<16 - 22554*cb - 46802*cr + 1<<15) >> 16
if g > 0xff {
g = 0xff
} else if g < 0 {
g = 0
}
b := (yy<<16 + 116130*cb + 1<<15) >> 16
if b > 0xff {
b = 0xff
} else if b < 0 {
b = 0
}
dst[j+0] = uint8(r)
dst[j+1] = uint8(g)
dst[j+2] = uint8(b)
dst[j+3] = 0xff
iy++
j += 4
}
}
}
}
func makeYCbCrImage(rect image.Rectangle, colors []color.Color, sr image.YCbCrSubsampleRatio) *image.YCbCr {
img := image.NewYCbCr(rect, sr)
j := 0
for y := rect.Min.Y; y < rect.Max.Y; y++ {
for x := rect.Min.X; x < rect.Max.X; x++ {
iy := img.YOffset(x, y)
ic := img.COffset(x, y)
c := color.NRGBAModel.Convert(colors[j]).(color.NRGBA)
img.Y[iy], img.Cb[ic], img.Cr[ic] = color.RGBToYCbCr(c.R, c.G, c.B)
j++
}
}
return img
}
func makeNRGBAImage(rect image.Rectangle, colors []color.Color) *image.NRGBA {
img := image.NewNRGBA(rect)
fillDrawImage(img, colors)
return img
}
func fillDrawImage(img draw.Image, colors []color.Color) {
colorsNRGBA := make([]color.NRGBA, len(colors))
for i, c := range colors {
nrgba := color.NRGBAModel.Convert(c).(color.NRGBA)
nrgba.A = uint8(i % 256)
colorsNRGBA[i] = nrgba
}
rect := img.Bounds()
i := 0
for y := rect.Min.Y; y < rect.Max.Y; y++ {
for x := rect.Min.X; x < rect.Max.X; x++ {
img.Set(x, y, colorsNRGBA[i])
i++
}
}
}
func readRow(img image.Image, y int) []uint8 {
row := make([]byte, img.Bounds().Dx()*4)
i := 0
for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
row[i+0] = c.R
row[i+1] = c.G
row[i+2] = c.B
row[i+3] = c.A
i += 4
}
return row
}
func readColumn(img image.Image, x int) []uint8 {
column := make([]byte, img.Bounds().Dy()*4)
i := 0
for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
column[i+0] = c.R
column[i+1] = c.G
column[i+2] = c.B
column[i+3] = c.A
i += 4
}
return column
}
func compareBytes(a, b []uint8, delta int) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if utils.Abs(int(a[i])-int(b[i])) > delta {
return false
}
}
return true
}
================================================
FILE: imop/blend.go
================================================
// Package imop implements the Porter-Duff composition operations
// used for mixing a graphic element with its backdrop.
// Porter and Duff presented in their paper 12 different composition operation,
// but the image/draw core package implements only the source-over-destination and source.
// This package is aimed to overcome the missing composite operations.
// It is mainly used to debug the seam carving operation correctness
// with face detection and image mask enabled.
// When the GUI mode and the debugging option is activated it will show
// the image mask and the detected faces rectangles in a distinct color.
package imop
import (
"fmt"
"math"
"sort"
"github.com/esimov/caire/utils"
)
type BlendType int
const (
Normal BlendType = iota
Darken
Lighten
Multiply
Screen
Overlay
SoftLight
HardLight
ColorDodge
ColorBurn
Difference
Exclusion
// Non-separable blend modes
Hue
Saturation
ColorMode
Luminosity
)
// Blend struct contains the currently active blend mode and all the supported blend modes.
type Blend struct {
CurrentOp BlendType
Modes []BlendType
}
// Color represents the RGB channel of a specific color.
type Color struct {
R, G, B float64
}
// NewBlend intantiates a new Blend.
func NewBlend() *Blend {
return &Blend{
Modes: []BlendType{
Normal, Darken, Lighten, Multiply,
Screen, Overlay, SoftLight, HardLight,
ColorDodge, ColorBurn, Difference, Exclusion,
Hue, Saturation, ColorMode, Luminosity,
},
}
}
// Set activate one of the supported blend modes.
func (bl *Blend) Set(blendType BlendType) error {
if utils.Contains(bl.Modes, blendType) {
bl.CurrentOp = blendType
return nil
}
return fmt.Errorf("unsupported blend mode")
}
// Get returns the active blend mode.
func (bl *Blend) Get() BlendType {
return bl.CurrentOp
}
// Lum gets the luminosity of a color.
func (bl *Blend) Lum(rgb Color) float64 {
return 0.3*rgb.R + 0.59*rgb.G + 0.11*rgb.B
}
// SetLum set the luminosity on a color.
func (bl *Blend) SetLum(rgb Color, l float64) Color {
delta := l - bl.Lum(rgb)
return bl.clip(Color{
rgb.R + delta,
rgb.G + delta,
rgb.B + delta,
})
}
// clip clips the channels of a color between certain min and max values.
func (bl *Blend) clip(rgb Color) Color {
r, g, b := rgb.R, rgb.G, rgb.B
l := bl.Lum(rgb)
min := utils.Min(r, g, b)
max := utils.Max(r, g, b)
if min < 0 {
r = l + (((r - l) * l) / (l - min))
g = l + (((g - l) * l) / (l - min))
b = l + (((b - l) * l) / (l - min))
}
if max > 1 {
r = l + (((r - l) * (1 - l)) / (max - l))
g = l + (((g - l) * (1 - l)) / (max - l))
b = l + (((b - l) * (1 - l)) / (max - l))
}
return Color{R: r, G: g, B: b}
}
// Sat gets the saturation of a color.
func (bl *Blend) Sat(rgb Color) float64 {
return utils.Max(rgb.R, rgb.G, rgb.B) - utils.Min(rgb.R, rgb.G, rgb.B)
}
// channel is a key/value struct pair used for sorting the color channels
// based on the color components having the minimum, middle, and maximum
// values upon entry to the function.
// The key component holds the channel name and val is the value it has.
type channel struct {
key string
val float64
}
func (bl *Blend) SetSat(rgb Color, s float64) Color {
color := map[string]float64{
"R": rgb.R,
"G": rgb.G,
"B": rgb.B,
}
channels := make([]channel, 0, 3)
for k, v := range color {
channels = append(channels, channel{k, v})
}
// Sort the color channels based on their values.
sort.Slice(channels, func(i, j int) bool { return channels[i].val < channels[j].val })
minChan, midChan, maxChan := channels[0].key, channels[1].key, channels[2].key
if color[maxChan] > color[minChan] {
color[midChan] = (((color[midChan] - color[minChan]) * s) / (color[maxChan] - color[minChan]))
color[maxChan] = s
} else {
color[midChan], color[maxChan] = 0, 0
}
color[minChan] = 0
return Color{
R: color["R"],
G: color["G"],
B: color["B"],
}
}
// Applies the alpha blending formula for a blend operation.
// See: https://www.w3.org/TR/compositing-1/#blending
func (bl *Blend) AlphaCompose(
backdropAlpha,
sourceAlpha,
compositeAlpha,
backdropColor,
sourceColor,
compositeColor float64,
) float64 {
return ((1 - sourceAlpha/compositeAlpha) * backdropColor) +
(sourceAlpha / compositeAlpha *
math.Round((1-backdropAlpha)*sourceColor+backdropAlpha*compositeColor))
}
================================================
FILE: imop/blend_test.go
================================================
package imop
import (
"image"
"image/color"
"image/draw"
"testing"
"github.com/stretchr/testify/assert"
)
func TestBlend_Basic(t *testing.T) {
assert := assert.New(t)
op := NewBlend()
assert.Empty(op.Get())
op.Set(Darken)
assert.Equal(Darken, op.Get())
op.Set(Lighten)
assert.Equal(Lighten, op.Get())
rgb := Color{R: 0xff, G: 0xff, B: 0xff}
lum := op.Lum(rgb)
assert.Equal(255.0, lum)
rgb = Color{R: 0, G: 0, B: 0}
lum = op.Lum(rgb)
assert.Equal(0.0, lum)
rgb = Color{R: 127, G: 127, B: 127}
lum = op.Lum(rgb)
assert.Equal(127.0, lum)
foreground := Color{R: 0xff, G: 0xff, B: 0xff}
background := Color{R: 0, G: 0, B: 0}
assert.Equal(0.0, op.Sat(foreground))
sat := op.SetSat(background, op.Sat(foreground))
assert.Equal(Color{R: 0, G: 0, B: 0}, sat)
}
func TestBlend_Modes(t *testing.T) {
// Note: all the expected values are taken by using as reference the results
// obtained in Photoshop by overlapping two layers and applying the blend mode.
assert := assert.New(t)
op := InitOp()
blend := NewBlend()
pinkFront := color.RGBA{R: 214, G: 20, B: 65, A: 255}
orangeBack := color.RGBA{R: 250, G: 121, B: 17, A: 255}
rect := image.Rect(0, 0, 1, 1)
bmp := NewBitmap(rect)
source := image.NewNRGBA(rect)
backdrop := image.NewNRGBA(rect)
op.Set(SrcOver)
// Darken
blend.Set(Darken)
draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected := []uint8{214, 20, 17, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// Multiply
blend.Set(Multiply)
draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{209, 9, 4, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// Screen
blend.Set(Screen)
draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{254, 131, 77, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// Overlay
blend.Set(Overlay)
draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{253, 18, 8, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// SoftLight
blend.Set(SoftLight)
draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{232, 19, 23, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// HardLight
blend.Set(HardLight)
draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{251, 67, 9, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// ColorDodge
blend.Set(ColorDodge)
draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{255, 131, 22, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// ColorBurn
blend.Set(ColorBurn)
draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{249, 0, 0, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// Difference
blend.Set(Difference)
draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{35, 101, 48, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// Exclusion
blend.Set(Exclusion)
draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{44, 122, 73, 255}
assert.EqualValues(expected, bmp.Img.Pix)
}
func TestBlend_NonSeparableModes(t *testing.T) {
assert := assert.New(t)
op := InitOp()
blend := NewBlend()
frontColor := color.RGBA{R: 250, G: 121, B: 17, A: 255}
backColor := color.RGBA{R: 214, G: 20, B: 65, A: 255}
rect := image.Rect(0, 0, 1, 1)
bmp := NewBitmap(rect)
source := image.NewNRGBA(rect)
backdrop := image.NewNRGBA(rect)
op.Set(SrcOver)
// Hue
blend.Set(Hue)
draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected := []uint8{255, 97, 133, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// Saturation
blend.Set(Saturation)
draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{233, 126, 39, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// Color
blend.Set(ColorMode)
draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{255, 97, 133, 255}
assert.EqualValues(expected, bmp.Img.Pix)
// Luminosity
blend.Set(Luminosity)
draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)
draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, blend)
expected = []uint8{148, 66, 0, 255}
assert.EqualValues(expected, bmp.Img.Pix)
}
================================================
FILE: imop/comp.go
================================================
// Package imop implements the Porter-Duff composition operations
// used for mixing a graphic element with its backdrop.
// Porter and Duff presented in their paper 12 different composition operation, but the
// core image/draw core package implements only the source-over-destination and source.
// This package implements all of the 12 composite operation together with some blending modes.
package imop
import (
"fmt"
"image"
"image/color"
"math"
"github.com/esimov/caire/utils"
)
type CompType int
const (
Clear CompType = iota
Copy
Dst
SrcOver
DstOver
SrcIn
DstIn
SrcOut
DstOut
SrcAtop
DstAtop
Xor
)
// Bitmap holds an image type as a placeholder for the Porter-Duff composition
// operations which can be used as a source or destination image.
type Bitmap struct {
Img *image.NRGBA
}
// Composite struct contains the currently active composition operation and all the supported operations.
type Composite struct {
CurrentOp CompType
Ops []CompType
}
// NewBitmap initializes a new Bitmap.
func NewBitmap(rect image.Rectangle) *Bitmap {
return &Bitmap{
Img: image.NewNRGBA(rect),
}
}
// InitOp initializes a new composition operation.
func InitOp() *Composite {
return &Composite{
CurrentOp: SrcOver,
Ops: []CompType{
Clear,
Copy,
Dst,
SrcOver,
DstOver,
SrcIn,
DstIn,
SrcOut,
DstOut,
SrcAtop,
DstAtop,
Xor,
},
}
}
// Set changes the current composition operation.
func (op *Composite) Set(compType CompType) error {
if utils.Contains(op.Ops, compType) {
op.CurrentOp = compType
return nil
}
return fmt.Errorf("unsupported composition operation")
}
// Set changes the current composition operation.
func (op *Composite) Get() CompType {
return op.CurrentOp
}
// Draw applies the currently active Ported-Duff composition operation formula,
// taking as parameter the source and the destination image and draws the result into the bitmap.
// If a blend mode is activated it will plug in the alpha blending formula also into the equation.
func (op *Composite) Draw(bitmap *Bitmap, src, dst *image.NRGBA, blend *Blend) {
dx, dy := src.Bounds().Dx(), src.Bounds().Dy()
var (
r, g, b, a uint32
rn, gn, bn, an float64
)
for x := 0; x < dx; x++ {
for y := 0; y < dy; y++ {
r1, g1, b1, a1 := src.At(x, y).RGBA()
r2, g2, b2, a2 := dst.At(x, y).RGBA()
rs, gs, bs, as := r1>>8, g1>>8, b1>>8, a1>>8
rb, gb, bb, ab := r2>>8, g2>>8, b2>>8, a2>>8
// normalize the values.
rsn := float64(rs) / 255
gsn := float64(gs) / 255
bsn := float64(bs) / 255
asn := float64(as) / 255
rbn := float64(rb) / 255
gbn := float64(gb) / 255
bbn := float64(bb) / 255
abn := float64(ab) / 255
// applying the alpha composition formula
switch op.CurrentOp {
case Clear:
rn, gn, bn, an = 0, 0, 0, 0
case Copy:
rn = asn * rsn
gn = asn * gsn
bn = asn * bsn
an = asn * asn
case Dst:
rn = abn * rbn
gn = abn * gbn
bn = abn * bbn
an = abn * abn
case SrcOver:
rn = asn*rsn + abn*rbn*(1-asn)
gn = asn*gsn + abn*gbn*(1-asn)
bn = asn*bsn + abn*bbn*(1-asn)
an = asn + abn*(1-asn)
case DstOver:
rn = asn*rsn*(1-abn) + abn*rbn
gn = asn*gsn*(1-abn) + abn*gbn
bn = asn*bsn*(1-abn) + abn*bbn
an = asn*(1-abn) + abn
case SrcIn:
rn = asn * rsn * abn
gn = asn * gsn * abn
bn = asn * bsn * abn
an = asn * abn
case DstIn:
rn = abn * rbn * asn
gn = abn * gbn * asn
bn = abn * bbn * asn
an = abn * asn
case SrcOut:
rn = asn * rsn * (1 - abn)
gn = asn * gsn * (1 - abn)
bn = asn * bsn * (1 - abn)
an = asn * (1 - abn)
case DstOut:
rn = abn * rbn * (1 - asn)
gn = abn * gbn * (1 - asn)
bn = abn * bbn * (1 - asn)
an = abn * (1 - asn)
case SrcAtop:
rn = asn*rsn*abn + (1-asn)*abn*rbn
gn = asn*gsn*abn + (1-asn)*abn*gbn
bn = asn*bsn*abn + (1-asn)*abn*bbn
an = asn*abn + abn*(1-asn)
case DstAtop:
rn = asn*rsn*(1-abn) + abn*rbn*asn
gn = asn*gsn*(1-abn) + abn*gbn*asn
bn = asn*bsn*(1-abn) + abn*bbn*asn
an = asn*(1-abn) + abn*asn
case Xor:
rn = asn*rsn*(1-abn) + abn*rbn*(1-asn)
gn = asn*gsn*(1-abn) + abn*gbn*(1-asn)
bn = asn*bsn*(1-abn) + abn*bbn*(1-asn)
an = asn*(1-abn) + abn*(1-asn)
}
r = uint32(rn * 255)
g = uint32(gn * 255)
b = uint32(bn * 255)
a = uint32(an * 255)
bitmap.Img.Set(x, y, color.NRGBA{
R: uint8(r),
G: uint8(g),
B: uint8(b),
A: uint8(a),
})
// applying the blending mode
if blend != nil {
rn, gn, bn, an = 0, 0, 0, 0 // reset the colors
r1, g1, b1, a1 = src.At(x, y).RGBA()
r2, g2, b2, a2 = dst.At(x, y).RGBA()
rs, gs, bs, as = r1>>8, g1>>8, b1>>8, a1>>8
rb, gb, bb, ab = r2>>8, g2>>8, b2>>8, a2>>8
rsn = float64(rs) / 255
gsn = float64(gs) / 255
bsn = float64(bs) / 255
asn = float64(as) / 255
rbn = float64(rb) / 255
gbn = float64(gb) / 255
bbn = float64(bb) / 255
abn = float64(ab) / 255
foreground := Color{R: rsn, G: gsn, B: bsn}
background := Color{R: rbn, G: gbn, B: bbn}
switch blend.CurrentOp {
case Normal:
rn, gn, bn, an = rsn, gsn, bsn, asn
case Darken:
rn = utils.Min(rsn, rbn)
gn = utils.Min(gsn, gbn)
bn = utils.Min(bsn, bbn)
an = utils.Min(asn, abn)
case Lighten:
rn = utils.Max(rsn, rbn)
gn = utils.Max(gsn, gbn)
bn = utils.Max(bsn, bbn)
an = utils.Max(asn, abn)
case Screen:
rn = 1 - (1-rsn)*(1-rbn)
gn = 1 - (1-gsn)*(1-gbn)
bn = 1 - (1-bsn)*(1-bbn)
an = 1 - (1-asn)*(1-abn)
case Multiply:
rn = rsn * rbn
gn = gsn * gbn
bn = bsn * bbn
an = asn * abn
case Overlay:
if rsn <= 0.5 {
rn = 2 * rsn * rbn
} else {
rn = 1 - 2*(1-rsn)*(1-rbn)
}
if gsn <= 0.5 {
gn = 2 * gsn * gbn
} else {
gn = 1 - 2*(1-gsn)*(1-gbn)
}
if bsn <= 0.5 {
bn = 2 * bsn * bbn
} else {
bn = 1 - 2*(1-bsn)*(1-bbn)
}
if asn <= 0.5 {
an = 2 * asn * abn
} else {
an = 1 - 2*(1-asn)*(1-abn)
}
case SoftLight:
if rbn < 0.5 {
rn = rsn - (1-2*rbn)*rsn*(1-rsn)
} else {
var w3r float64
if rsn < 0.25 {
w3r = ((16*rsn-12)*rsn + 4) * rsn
} else {
w3r = math.Sqrt(rsn)
}
rn = rsn + (2*rbn-1)*(w3r-rsn)
}
if gbn < 0.5 {
gn = gsn - (1-2*gbn)*gsn*(1-gsn)
} else {
var w3g float64
if gsn < 0.25 {
w3g = ((16*gsn-12)*gsn + 4) * gsn
} else {
w3g = math.Sqrt(gsn)
}
gn = gsn + (2*gbn-1)*(w3g-gsn)
}
if bbn < 0.5 {
bn = bsn - (1-2*bbn)*bsn*(1-bsn)
} else {
var w3b float64
if bsn < 0.25 {
w3b = ((16*bsn-12)*bsn + 4) * bsn
} else {
w3b = math.Sqrt(bsn)
}
bn = bsn + (2*bbn-1)*(w3b-bsn)
}
if abn < 0.5 {
an = asn - (1-2*abn)*asn*(1-asn)
} else {
var w3a float64
if asn < 0.25 {
w3a = ((16*asn-12)*asn + 4) * asn
} else {
w3a = math.Sqrt(asn)
}
an = asn + (2*abn-1)*(w3a-asn)
}
case HardLight:
if rbn < 0.5 {
rn = rbn - (1-2*rsn)*rbn*(1-rbn)
} else {
var w3r float64
if rbn < 0.25 {
w3r = ((16*rbn-12)*rbn + 4) * rbn
} else {
w3r = math.Sqrt(rbn)
}
rn = rbn + (2*rsn-1)*(w3r-rbn)
}
if gbn < 0.5 {
gn = gbn - (1-2*gsn)*gbn*(1-gbn)
} else {
var w3g float64
if gbn < 0.25 {
w3g = ((16*gbn-12)*gbn + 4) * gbn
} else {
w3g = math.Sqrt(gbn)
}
gn = gbn + (2*gsn-1)*(w3g-gbn)
}
if bbn < 0.5 {
bn = bbn - (1-2*bsn)*bbn*(1-bbn)
} else {
var w3b float64
if bbn < 0.25 {
w3b = ((16*bbn-12)*bbn + 4) * bbn
} else {
w3b = math.Sqrt(bbn)
}
bn = bbn + (2*bsn-1)*(w3b-bbn)
}
if abn < 0.5 {
an = abn - (1-2*asn)*abn*(1-abn)
} else {
var w3a float64
if abn < 0.25 {
w3a = ((16*abn-12)*abn + 4) * abn
} else {
w3a = math.Sqrt(abn)
}
an = abn + (2*asn-1)*(w3a-abn)
}
case ColorDodge:
if rsn < 1 {
rn = utils.Min(1, rbn/(1-rsn))
} else if rsn == 1 {
rn = 1
}
if gsn < 1 {
gn = utils.Min(1, gbn/(1-gsn))
} else if gsn == 1 {
gn = 1
}
if bsn < 1 {
bn = utils.Min(1, bbn/(1-bsn))
} else if bsn == 1 {
bn = 1
}
if asn < 1 {
an = utils.Min(1, abn/(1-asn))
} else if asn == 1 {
an = 1
}
case ColorBurn:
if rsn > 0 {
rn = 1 - utils.Min(1, (1-rbn)/rsn)
} else if rsn == 0 {
rn = 0
}
if gsn > 0 {
gn = 1 - utils.Min(1, (1-gbn)/gsn)
} else if gsn == 0 {
gn = 0
}
if bsn > 0 {
bn = 1 - utils.Min(1, (1-bbn)/bsn)
} else if bsn == 0 {
bn = 0
}
if asn > 0 {
an = 1 - utils.Min(1, (1-abn)/asn)
} else if asn == 0 {
an = 0
}
case Difference:
rn = utils.Abs(rbn - rsn)
gn = utils.Abs(gbn - gsn)
bn = utils.Abs(bbn - bsn)
an = 1
case Exclusion:
rn = rsn + rbn - 2*rsn*rbn
gn = gsn + gbn - 2*gsn*gbn
bn = bsn + bbn - 2*bsn*bbn
an = 1
// Non-separable blend modes
// https://www.w3.org/TR/compositing-1/#blendingnonseparable
case Hue:
sat := blend.SetSat(background, blend.Sat(foreground))
rgb := blend.SetLum(sat, blend.Lum(foreground))
a := asn + abn - asn*abn
rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
rn, gn, bn = rn/255, gn/255, bn/255
an = a
case Saturation:
sat := blend.SetSat(foreground, blend.Sat(background))
rgb := blend.SetLum(sat, blend.Lum(foreground))
a := asn + abn - asn*abn
rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
rn, gn, bn = rn/255, gn/255, bn/255
an = a
case ColorMode:
rgb := blend.SetLum(background, blend.Lum(foreground))
a := asn + abn - asn*abn
rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
rn, gn, bn = rn/255, gn/255, bn/255
an = a
case Luminosity:
rgb := blend.SetLum(foreground, blend.Lum(background))
a := asn + abn - asn*abn
rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
rn, gn, bn = rn/255, gn/255, bn/255
an = a
}
r = uint32(rn * 255)
g = uint32(gn * 255)
b = uint32(bn * 255)
a = uint32(an * 255)
bitmap.Img.Set(x, y, color.NRGBA{
R: uint8(r),
G: uint8(g),
B: uint8(b),
A: uint8(a),
})
}
}
}
}
================================================
FILE: imop/comp_test.go
================================================
package imop
import (
"image"
"image/color"
"image/draw"
"testing"
"github.com/stretchr/testify/assert"
)
func TestComp_Basic(t *testing.T) {
assert := assert.New(t)
op := InitOp()
op.Set(Clear)
assert.Equal(Clear, op.Get())
assert.NotEqual("unsupported_composite_operation", op.Get())
op.Set(Dst)
assert.Equal(Dst, op.Get())
}
func TestComp_Ops(t *testing.T) {
assert := assert.New(t)
op := InitOp()
transparent := color.NRGBA{R: 0, G: 0, B: 0, A: 0}
cyan := color.NRGBA{R: 33, G: 150, B: 243, A: 255}
magenta := color.NRGBA{R: 233, G: 30, B: 99, A: 255}
rect := image.Rect(0, 0, 10, 10)
bmp := NewBitmap(rect)
source := image.NewNRGBA(rect)
backdrop := image.NewNRGBA(rect)
// No composition operation applied. The SrcOver is the default one.
draw.Draw(source, image.Rect(0, 4, 6, 10), &image.Uniform{cyan}, image.Point{}, draw.Src)
draw.Draw(backdrop, image.Rect(4, 0, 10, 6), &image.Uniform{magenta}, image.Point{}, draw.Src)
op.Draw(bmp, source, backdrop, nil)
// Pick three representative points/pixels from the generated image output.
// Depending on the applied composition operation the colors of the
// selected pixels should be the source color, the destination color or transparent.
topRight := bmp.Img.At(9, 0)
bottomLeft := bmp.Img.At(0, 9)
center := bmp.Img.At(5, 5)
assert.EqualValues(topRight, magenta)
assert.EqualValues(bottomLeft, cyan)
assert.EqualValues(center, cyan)
// Clear
op.Set(Clear)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, transparent)
assert.EqualValues(bottomLeft, transparent)
assert.EqualValues(center, transparent)
// Copy
op.Set(Copy)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, transparent)
assert.EqualValues(bottomLeft, cyan)
assert.EqualValues(center, cyan)
// Dst
op.Set(Dst)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, magenta)
assert.EqualValues(bottomLeft, transparent)
assert.EqualValues(center, magenta)
// SrcOver
op.Set(SrcOver)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, magenta)
assert.EqualValues(bottomLeft, cyan)
assert.EqualValues(center, cyan)
// DstOver
op.Set(DstOver)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, magenta)
assert.EqualValues(bottomLeft, cyan)
assert.EqualValues(center, magenta)
// SrcIn
op.Set(SrcIn)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, transparent)
assert.EqualValues(bottomLeft, transparent)
assert.EqualValues(center, cyan)
// DstIn
op.Set(DstIn)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, transparent)
assert.EqualValues(bottomLeft, transparent)
assert.EqualValues(center, magenta)
// SrcOut
op.Set(SrcOut)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, transparent)
assert.EqualValues(bottomLeft, cyan)
assert.EqualValues(center, transparent)
// DstOut
op.Set(DstOut)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, magenta)
assert.EqualValues(bottomLeft, transparent)
assert.EqualValues(center, transparent)
// SrcAtop
op.Set(SrcAtop)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, magenta)
assert.EqualValues(bottomLeft, transparent)
assert.EqualValues(center, cyan)
// DstAtop
op.Set(DstAtop)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, transparent)
assert.EqualValues(bottomLeft, cyan)
assert.EqualValues(center, magenta)
// Xor
op.Set(Xor)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, magenta)
assert.EqualValues(bottomLeft, cyan)
assert.EqualValues(center, transparent)
// DstAtop
op.Set(DstAtop)
op.Draw(bmp, source, backdrop, nil)
topRight = bmp.Img.At(9, 0)
bottomLeft = bmp.Img.At(0, 9)
center = bmp.Img.At(5, 5)
assert.EqualValues(topRight, transparent)
assert.EqualValues(bottomLeft, cyan)
assert.EqualValues(center, magenta)
}
================================================
FILE: preview.go
================================================
package caire
import (
"os"
)
// showPreview spawns a new Gio GUI window and updates its content with the resized image received from a channel.
func (p *Processor) showPreview(
imgWorker <-chan worker,
errChan chan<- error,
guiWindow struct {
width int
height int
},
) {
var gui = NewGUI(guiWindow.width, guiWindow.height)
gui.proc = p
gui.process.worker = imgWorker
// Run the Gio GUI app in a separate goroutine
go func() {
if err := gui.Run(); err != nil {
errChan <- err
}
// It's important to call os.Exit(0) in order to terminate
// the execution of the GUI app when pressing ESC key.
os.Exit(0)
}()
}
================================================
FILE: processor.go
================================================
package caire
import (
_ "embed"
"errors"
"fmt"
"image"
"image/draw"
"io"
"math"
"github.com/disintegration/imaging"
"github.com/esimov/caire/utils"
pigo "github.com/esimov/pigo/core"
)
//go:embed data/facefinder
var cascadeFile []byte
var (
resizeXY = false // the image is resized both vertically and horizontally
imgWorker = make(chan worker) // channel used to transfer the image to the GUI
errs = make(chan error)
)
var _ SeamCarver = (*Processor)(nil)
// worker struct contains all the information needed for transferring the resized image to the Gio GUI.
type worker struct {
img *image.NRGBA
mask *image.NRGBA
seams []Seam
done bool
}
// shrinkFn is a generic function used to shrink an image.
type shrinkFn func(*image.NRGBA) (*image.NRGBA, error)
// enlargeFn is a generic function used to enlarge an image.
type enlargeFn func(*image.NRGBA) (*image.NRGBA, error)
// Processor options
type Processor struct {
FaceAngle float64
SeamColor string
MaskPath string
RMaskPath string
ShapeType ShapeType
SobelThreshold int
BlurRadius int
NewWidth int
NewHeight int
FaceDetector *pigo.Pigo
Spinner *utils.Spinner
Mask *image.NRGBA
RMask *image.NRGBA
DebugMask *image.NRGBA
Percentage bool
Square bool
Debug bool
Preview bool
FaceDetect bool
vRes bool
}
var (
shrinkHorizFn shrinkFn
shrinkVertFn shrinkFn
enlargeHorizFn enlargeFn
enlargeVertFn enlargeFn
)
// Carve is the main entry point for the image resize operation.
// The new image can be resized either horizontally or vertically (or both).
// Depending on the provided options the image can be either reduced or enlarged.
func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
var (
newImg image.Image
newWidth int
newHeight int
pw, ph int
err error
)
c := NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
if p.NewWidth > c.Width {
newWidth = p.NewWidth - (p.NewWidth - (p.NewWidth - c.Width))
} else {
newWidth = c.Width - (c.Width - (c.Width - p.NewWidth))
}
if p.NewHeight > c.Height {
newHeight = p.NewHeight - (p.NewHeight - (p.NewHeight - c.Height))
} else {
newHeight = c.Height - (c.Height - (c.Height - p.NewHeight))
}
if p.NewWidth == 0 {
newWidth = p.NewWidth
}
if p.NewHeight == 0 {
newHeight = p.NewHeight
}
// shrinkHorizFn calls itself recursively to shrink the image horizontally.
// If the image is resized on both X and Y axis it calls the shrink and enlarge
// function intermittently up until the desired dimension is reached.
// I opted for this solution instead of resizing the image sequentially,
// to avoid resulting visual artefacts, since this way
// the horizontal and vertical seams are merged together seamlessly.
shrinkHorizFn = func(img *image.NRGBA) (*image.NRGBA, error) {
p.vRes = false
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
if dx > p.NewWidth {
img, err = p.shrink(img)
if err != nil {
return nil, err
}
if p.NewHeight > 0 && p.NewHeight != dy {
if p.NewHeight <= dy {
img, err = shrinkVertFn(img)
if err != nil {
return nil, err
}
} else {
img, err = enlargeVertFn(img)
if err != nil {
return nil, err
}
}
} else {
img, err = shrinkHorizFn(img)
if err != nil {
return nil, err
}
}
}
return img, nil
}
// enlargeHorizFn calls itself recursively to enlarge the image horizontally.
enlargeHorizFn = func(img *image.NRGBA) (*image.NRGBA, error) {
p.vRes = false
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
if dx < p.NewWidth {
img, err = p.enlarge(img)
if err != nil {
return nil, err
}
if p.NewHeight > 0 && p.NewHeight != dy {
if p.NewHeight <= dy {
img, err = shrinkVertFn(img)
if err != nil {
return nil, err
}
} else {
img, err = enlargeVertFn(img)
if err != nil {
return nil, err
}
}
} else {
img, err = enlargeHorizFn(img)
if err != nil {
return nil, err
}
}
}
return img, nil
}
// shrinkVertFn calls itself recursively to shrink the image vertically.
shrinkVertFn = func(img *image.NRGBA) (*image.NRGBA, error) {
p.vRes = true
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
// If the image is resized both horizontally and vertically we need
// to rotate the image each time when invoking the shrink function.
// Otherwise we rotate the image only once, right before calling this function.
if resizeXY {
dx, dy = img.Bounds().Dy(), img.Bounds().Dx()
img = rotateImage90(img)
}
if dx > p.NewHeight {
img, err = p.shrink(img)
if err != nil {
return nil, err
}
if resizeXY {
img = rotateImage270(img)
}
if p.NewWidth > 0 && p.NewWidth != dy {
if p.NewWidth <= dy {
img, err = shrinkHorizFn(img)
if err != nil {
return nil, err
}
} else {
img, err = enlargeHorizFn(img)
if err != nil {
return nil, err
}
}
} else {
img, err = shrinkVertFn(img)
if err != nil {
return nil, err
}
}
} else {
if resizeXY {
img = rotateImage270(img)
}
}
return img, nil
}
// enlargeVertFn calls itself recursively to enlarge the image vertically.
enlargeVertFn = func(img *image.NRGBA) (*image.NRGBA, error) {
p.vRes = true
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
if resizeXY {
dx, dy = img.Bounds().Dy(), img.Bounds().Dx()
img = rotateImage90(img)
}
if dx < p.NewHeight {
img, err = p.enlarge(img)
if err != nil {
return nil, err
}
if resizeXY {
img = rotateImage270(img)
}
if p.NewWidth > 0 && p.NewWidth != dy {
if p.NewWidth <= dy {
img, err = shrinkHorizFn(img)
if err != nil {
return nil, err
}
} else {
img, err = enlargeHorizFn(img)
if err != nil {
return nil, err
}
}
} else {
img, err = enlargeVertFn(img)
if err != nil {
return nil, err
}
}
} else {
if resizeXY {
img = rotateImage270(img)
}
}
return img, nil
}
if p.Percentage || p.Square {
pw = c.Width - c.Height
ph = c.Height - c.Width
// In case pw and ph is zero, it means that the target image is square.
// In this case we can simply resize the image without running the carving operation.
if p.Percentage && pw == 0 && ph == 0 {
pw = c.Width - int(float64(c.Width)-(float64(p.NewWidth)/100*float64(c.Width)))
ph = c.Height - int(float64(c.Height)-(float64(p.NewHeight)/100*float64(c.Height)))
p.NewWidth = utils.Abs(c.Width - pw)
p.NewHeight = utils.Abs(c.Height - ph)
resImgSize := utils.Min(p.NewWidth, p.NewHeight)
return imaging.Resize(img, resImgSize, 0, imaging.Lanczos), nil
}
// When the square option is used the image will be resized to a square based on the shortest edge.
if p.Square {
// Calling the image rescale method only when both a new width and height is provided.
if p.NewWidth != 0 && p.NewHeight != 0 {
p.NewWidth = utils.Min(p.NewWidth, p.NewHeight)
p.NewHeight = p.NewWidth
newImg = p.calculateFitness(img, c)
dst := image.NewNRGBA(newImg.Bounds())
draw.Draw(dst, newImg.Bounds(), newImg, image.Point{}, draw.Src)
img = dst
nw, nh := img.Bounds().Dx(), img.Bounds().Dy()
p.NewWidth = utils.Min(nw, nh)
p.NewHeight = p.NewWidth
} else {
return nil, errors.New("please provide a new WIDTH and HEIGHT when using the square option")
}
}
// Use the Percentage flag only for shrinking the image.
if p.Percentage {
// Calculate the new image size based on the provided percentage.
pw = c.Width - int(float64(c.Width)-(float64(p.NewWidth)/100*float64(c.Width)))
ph = c.Height - int(float64(c.Height)-(float64(p.NewHeight)/100*float64(c.Height)))
if p.NewWidth != 0 {
p.NewWidth = utils.Abs(c.Width - pw)
}
if p.NewHeight != 0 {
p.NewHeight = utils.Abs(c.Height - ph)
}
if pw >= c.Width || ph >= c.Height {
return nil, errors.New("cannot use the percentage flag for image enlargement")
}
}
}
// Rescale the image when it is resized both horizontally and vertically.
// First the image is scaled down or up by preserving the image aspect ratio,
// then the seam carving algorithm is applied only to the remaining pixels.
// Scale the width and height by the smaller factor (i.e Min(wScaleFactor, hScaleFactor))
// Example: input: 5000x2500, scale: 2160x1080, final target: 1920x1080
if (c.Width > p.NewWidth && c.Height > p.NewHeight) &&
(p.NewWidth != 0 && p.NewHeight != 0) {
newImg = p.calculateFitness(img, c)
dx0, dy0 := img.Bounds().Max.X, img.Bounds().Max.Y
dx1, dy1 := newImg.Bounds().Max.X, newImg.Bounds().Max.Y
// Rescale the image when the new image width or height are preserved, otherwise
// it might happen, that the generated image size does not match with the requested image size.
if !((p.NewWidth == 0 && dx0 == dx1) || (p.NewHeight == 0 && dy0 == dy1)) {
dst := image.NewNRGBA(newImg.Bounds())
draw.Draw(dst, newImg.Bounds(), newImg, image.Point{}, draw.Src)
img = dst
}
}
// Run the carver function if the desired image width is not identical with the rescaled image width.
if newWidth > 0 && p.NewWidth != c.Width {
if p.NewWidth > c.Width {
img, err = enlargeHorizFn(img)
if err != nil {
return nil, err
}
} else {
img, err = shrinkHorizFn(img)
if err != nil {
return nil, err
}
}
}
// Run the carver function if the desired image height is not identical with the rescaled image height.
if newHeight > 0 && p.NewHeight != c.Height {
if !resizeXY {
img = rotateImage90(img)
if p.Mask != nil {
p.Mask = rotateImage90(p.Mask)
}
if p.RMask != nil {
p.RMask = rotateImage90(p.RMask)
}
}
if p.NewHeight > c.Height {
img, err = enlargeVertFn(img)
if err != nil {
return nil, err
}
} else {
img, err = shrinkVertFn(img)
if err != nil {
return nil, err
}
}
if !resizeXY {
img = rotateImage270(img)
if p.Mask != nil {
p.Mask = rotateImage270(p.Mask)
}
if p.RMask != nil {
p.RMask = rotateImage270(p.RMask)
}
}
}
// Signal that the process is done and no more data is sent through the channel.
go func() {
imgWorker <- worker{
img: nil,
mask: nil,
seams: nil,
done: true,
}
}()
return img, nil
}
// calculateFitness iteratively try to find the best image aspect ratio for the rescale.
func (p *Processor) calculateFitness(img *image.NRGBA, c *Carver) *image.NRGBA {
var (
w = float64(c.Width)
h = float64(c.Height)
nw = float64(p.NewWidth)
nh = float64(p.NewHeight)
newImg *image.NRGBA
)
wsf := w / nw
hsf := h / nh
sw := math.Round(w / math.Min(wsf, hsf))
sh := math.Round(h / math.Min(wsf, hsf))
if sw <= sh {
newImg = imaging.Resize(img, 0, int(sw), imaging.Lanczos)
if p.Mask != nil {
p.Mask = imaging.Resize(p.Mask, 0, int(sw), imaging.Lanczos)
}
if p.RMask != nil {
p.RMask = imaging.Resize(p.RMask, 0, int(sw), imaging.Lanczos)
}
} else {
newImg = imaging.Resize(img, 0, int(sh), imaging.Lanczos)
if p.Mask != nil {
p.Mask = imaging.Resize(p.Mask, 0, int(sh), imaging.Lanczos)
}
if p.RMask != nil {
p.RMask = imaging.Resize(p.RMask, 0, int(sh), imaging.Lanczos)
}
}
dx, dy := newImg.Bounds().Max.X, newImg.Bounds().Max.Y
c.Width = dx
c.Height = dy
if int(sw) < p.NewWidth || int(sh) < p.NewHeight {
newImg = p.calculateFitness(newImg, c)
}
return newImg
}
// Process encodes the resized image into an io.Writer interface.
// We are using the io package, since we can provide different input and output types,
// as long as they implement the io.Reader and io.Writer interface.
func (p *Processor) Process(r io.Reader, w io.Writer) error {
var err error
if p.FaceDetect {
// Instantiate a new Pigo object in case the face detection option is used.
p.FaceDetector = pigo.NewPigo()
// Unpack the binary file. This will return the number of cascade trees,
// the tree depth, the threshold and the prediction from tree's leaf nodes.
p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
if err != nil {
return fmt.Errorf("error unpacking the cascade file: %v", err)
}
}
if p.NewWidth != 0 && p.NewHeight != 0 {
resizeXY = true
}
src, _, err := image.Decode(r)
if err != nil {
return err
}
img := imgToNRGBA(src)
if p.Preview {
guiWidth := img.Bounds().Max.X
guiHeight := img.Bounds().Max.Y
if p.NewWidth > guiWidth {
guiWidth = p.NewWidth
}
if p.NewHeight > guiHeight {
guiHeight = p.NewHeight
}
if resizeXY {
guiWidth = int(maxScreenX)
guiHeight = int(maxScreenY)
}
guiWindow := struct {
width int
height int
}{
width: guiWidth,
height: guiHeight,
}
// Lunch Gio GUI thread.
go p.showPreview(imgWorker, errs, guiWindow)
}
return encodeImg(p, w, img)
}
// shrink reduces the image dimension either horizontally or vertically.
func (p *Processor) shrink(img *image.NRGBA) (*image.NRGBA, error) {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c := NewCarver(width, height)
if _, err := c.ComputeSeams(p, img); err != nil {
return nil, err
}
seams := c.FindLowestEnergySeams(p)
img = c.RemoveSeam(img, seams, p.Debug)
if p.Mask != nil {
p.Mask = c.RemoveSeam(p.Mask, seams, false)
draw.Draw(p.DebugMask, img.Bounds(), p.Mask, image.Point{}, draw.Over)
}
if p.RMask != nil {
p.RMask = c.RemoveSeam(p.RMask, seams, false)
draw.Draw(p.DebugMask, img.Bounds(), p.RMask, image.Point{}, draw.Over)
}
go func() {
select {
case imgWorker <- worker{
img: img,
mask: p.DebugMask,
seams: c.Seams,
done: false,
}:
case <-errs:
return
}
}()
return img, nil
}
// enlarge increases the image dimension either horizontally or vertically.
func (p *Processor) enlarge(img *image.NRGBA) (*image.NRGBA, error) {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c := NewCarver(width, height)
if _, err := c.ComputeSeams(p, img); err != nil {
return nil, err
}
seams := c.FindLowestEnergySeams(p)
img = c.AddSeam(img, seams, p.Debug)
if p.Mask != nil {
p.Mask = c.AddSeam(p.Mask, seams, false)
p.DebugMask = p.Mask
}
if p.RMask != nil {
p.RMask = c.AddSeam(p.RMask, seams, false)
p.DebugMask = p.RMask
}
go func() {
select {
case imgWorker <- worker{
img: img,
mask: p.DebugMask,
seams: c.Seams,
done: false,
}:
case <-errs:
return
}
}()
return img, nil
}
================================================
FILE: processor_test.go
================================================
package caire
import (
"image"
"testing"
"github.com/stretchr/testify/assert"
)
func TestResize_ShrinkImageWidth(t *testing.T) {
img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
var c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
newWidth := imgWidth / 2
p.NewWidth = newWidth
p.NewHeight = imgHeight
for x := 0; x < newWidth; x++ {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c = NewCarver(width, height)
_, err := c.ComputeSeams(p, img)
assert.NoError(t, err)
seams := c.FindLowestEnergySeams(p)
img = c.RemoveSeam(img, seams, p.Debug)
}
imgWidth := img.Bounds().Max.X
assert.Equal(t, imgWidth, newWidth)
}
func TestResize_ShrinkImageHeight(t *testing.T) {
img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
var c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
newHeight := imgHeight / 2
p.NewWidth = imgWidth
p.NewHeight = newHeight
img = rotateImage90(img)
for x := 0; x < newHeight; x++ {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c = NewCarver(width, height)
_, err := c.ComputeSeams(p, img)
assert.NoError(t, err)
seams := c.FindLowestEnergySeams(p)
img = c.RemoveSeam(img, seams, p.Debug)
}
img = rotateImage270(img)
imgHeight := img.Bounds().Max.Y
assert.Equal(t, imgHeight, newHeight)
}
func TestResize_EnlargeImageWidth(t *testing.T) {
img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
var c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
newWidth := imgWidth * 2
p.NewWidth = newWidth
p.NewHeight = imgHeight
for x := 0; x < newWidth; x++ {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c = NewCarver(width, height)
_, err := c.ComputeSeams(p, img)
assert.NoError(t, err)
seams := c.FindLowestEnergySeams(p)
img = c.AddSeam(img, seams, p.Debug)
}
imgWidth := img.Bounds().Max.X - img.Bounds().Dx()
assert.NotEqual(t, imgWidth, newWidth)
}
func TestResize_EnlargeImageHeight(t *testing.T) {
img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
var c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
newHeight := imgHeight * 2
p.NewWidth = imgWidth
p.NewHeight = newHeight
img = rotateImage90(img)
for x := 0; x < newHeight; x++ {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c = NewCarver(width, height)
_, err := c.ComputeSeams(p, img)
assert.NoError(t, err)
seams := c.FindLowestEnergySeams(p)
img = c.AddSeam(img, seams, p.Debug)
}
img = rotateImage270(img)
imgHeight := img.Bounds().Max.Y - img.Bounds().Dy()
assert.NotEqual(t, imgHeight, newHeight)
}
================================================
FILE: snapcraft.yaml
================================================
name: caire
version: '1.4.4'
summary: Content aware image resize library
description: |
Content aware image resize library
grade: stable
confinement: strict
base: core18
parts:
caire:
plugin: go
source: https://github.com/esimov/caire.git
go-importpath: github.com/esimov/caire
stage-packages:
- libegl1
- libglvnd0
- libwayland-client0
- libwayland-cursor0
- libwayland-egl1
- libx11-6
- libx11-xcb1
- libxau6
- libxcb-xkb1
- libxcb1
- libxcursor1
- libxdmcp6
- libxfixes3
- libxkbcommon-x11-0
- libxkbcommon0
- libxrender1
build-packages:
- gcc
- pkg-config
- libwayland-dev
- libx11-dev
- libx11-xcb-dev
- libxkbcommon-x11-dev
- libgles2-mesa-dev
- libegl1-mesa-dev
- libffi-dev
- libxcursor-dev
- libvulkan-dev
apps:
caire:
command: bin/caire
plugs:
- home
- x11
================================================
FILE: sobel.go
================================================
package caire
import (
"image"
"math"
)
type kernel [][]int32
var (
kernelX = kernel{
{-1, 0, 1},
{-2, 0, 2},
{-1, 0, 1},
}
kernelY = kernel{
{-1, -2, -1},
{0, 0, 0},
{1, 2, 1},
}
)
// SobelDetector uses the sobel filter operator for detecting image edges.
// See https://en.wikipedia.org/wiki/Sobel_operator
func (c *Carver) SobelDetector(img *image.NRGBA, threshold float64) *image.NRGBA {
var sumX, sumY int32
dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y
dst := image.NewNRGBA(img.Bounds())
// Get 3x3 window of pixels because image data given is just a 1D array of pixels
maxPixelOffset := dx*2 + len(kernelX) - 1
data := c.getImageData(img)
length := len(data)*4 - maxPixelOffset
magnitudes := make([]uint8, length)
for i := 0; i < length; i++ {
// Sum each pixel with the kernel value
sumX, sumY = 0, 0
for x := 0; x < len(kernelX); x++ {
for y := 0; y < len(kernelY); y++ {
if idx := i + (dx * y) + x; idx < len(data) {
r := data[i+(dx*y)+x]
sumX += int32(r) * kernelX[y][x]
sumY += int32(r) * kernelY[y][x]
}
}
}
magnitude := math.Sqrt(float64(sumX*sumX) + float64(sumY*sumY))
// Check for pixel color boundaries
if magnitude < 0 {
magnitude = 0
} else if magnitude > 255 {
magnitude = 255
}
// Set magnitude to 0 if doesn't exceed threshold, else set to magnitude
if magnitude > threshold {
magnitudes[i] = uint8(magnitude)
} else {
magnitudes[i] = 0
}
}
dataLength := dx * dy * 4
edges := make([]int32, dataLength)
// Apply the kernel values
for i := 0; i < dataLength; i++ {
edges[i] = 0
if i%4 != 0 {
m := magnitudes[i/4]
if m != 0 {
edges[i-1] = int32(m)
}
}
}
// Generate the new image with the sobel filter applied
for idx := 0; idx < len(edges); idx += 4 {
dst.Pix[idx] = uint8(edges[idx])
dst.Pix[idx+1] = uint8(edges[idx+1])
dst.Pix[idx+2] = uint8(edges[idx+2])
dst.Pix[idx+3] = 0xff
}
return dst
}
// getImageData gets the red component of an image and returns an array of pixel brightness values.
func (c *Carver) getImageData(img *image.NRGBA) []uint8 {
dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y
pixels := make([]uint8, dx*dy)
for i := range pixels {
pixels[i] = img.Pix[i*4]
}
return pixels
}
================================================
FILE: stackblur.go
================================================
// Go implementation of the StackBlur algorithm
// http://incubator.quasimondo.com/processing/fast_blur_deluxe.php
package caire
import (
"errors"
"image"
"image/color"
)
// blurStack is a linked list containing the color value and a pointer to the next struct.
type blurStack struct {
r, g, b, a uint32
next *blurStack
}
var mulTable = []uint32{
512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512,
454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512,
482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456,
437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512,
497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328,
320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456,
446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335,
329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512,
505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405,
399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328,
324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271,
268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456,
451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388,
385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335,
332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292,
289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259,
}
var shgTable = []uint32{
9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
}
// Stackblur takes the source image and returns it's blurred version by applying the blur radius defined as parameter. The destination image must be a image.NRGBA.
func Stackblur(dst, src image.Image, radius uint32) error {
// Limit the maximum blur radius to 255 to avoid overflowing the multable.
if int(radius) >= len(mulTable) {
radius = uint32(len(mulTable) - 1)
}
if radius < 1 {
return errors.New("blur radius must be greater than 0")
}
img, ok := dst.(*image.NRGBA)
if !ok {
return errors.New("the destination image must be image.NRGBA")
}
process(img, src, radius)
return nil
}
func process(dst *image.NRGBA, src image.Image, radius uint32) {
srcBounds := src.Bounds()
srcMinX := srcBounds.Min.X
srcMinY := srcBounds.Min.Y
dstBounds := srcBounds.Sub(srcBounds.Min)
dstW := dstBounds.Dx()
dstH := dstBounds.Dy()
switch src0 := src.(type) {
case *image.NRGBA:
rowSize := srcBounds.Dx() * 4
for dstY := 0; dstY < dstH; dstY++ {
di := src0.PixOffset(0, dstY)
si := src0.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
copy(dst.Pix[di:di+rowSize], src0.Pix[si:si+rowSize])
}
}
case *image.YCbCr:
for dstY := 0; dstY < dstH; dstY++ {
di := dst.PixOffset(0, dstY)
for dstX := 0; dstX < dstW; dstX++ {
srcX := srcMinX + dstX
srcY := srcMinY + dstY
siy := src0.YOffset(srcX, srcY)
sic := src0.COffset(srcX, srcY)
r, g, b := color.YCbCrToRGB(src0.Y[siy], src0.Cb[sic], src0.Cr[sic])
dst.Pix[di+0] = r
dst.Pix[di+1] = g
dst.Pix[di+2] = b
dst.Pix[di+3] = 0xff
di += 4
}
}
default:
for dstY := 0; dstY < dstH; dstY++ {
di := dst.PixOffset(0, dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := color.NRGBAModel.Convert(src.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)
dst.Pix[di+0] = c.R
dst.Pix[di+1] = c.G
dst.Pix[di+2] = c.B
dst.Pix[di+3] = c.A
di += 4
}
}
}
blurImage(dst, radius)
}
func blurImage(src *image.NRGBA, radius uint32) {
var (
stackEnd *blurStack
stackIn *blurStack
stackOut *blurStack
)
var width, height = uint32(src.Bounds().Dx()), uint32(src.Bounds().Dy())
var (
div, widthMinus1, heightMinus1, radiusPlus1, sumFactor uint32
x, y, i, p, yp, yi, yw,
rSum, gSum, bSum, aSum,
rOutSum, gOutSum, bOutSum, aOutSum,
rInSum, gInSum, bInSum, aInSum,
pr, pg, pb, pa uint32
)
div = radius + radius + 1
widthMinus1 = width - 1
heightMinus1 = height - 1
radiusPlus1 = radius + 1
sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2
stackStart := new(blurStack)
stack := stackStart
for i = 1; i < div; i++ {
stack.next = new(blurStack)
stack = stack.next
if i == radiusPlus1 {
stackEnd = stack
}
}
stack.next = stackStart
mulSum := mulTable[radius]
shgSum := shgTable[radius]
for y = 0; y < height; y++ {
rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0
pr = uint32(src.Pix[yi])
pg = uint32(src.Pix[yi+1])
pb = uint32(src.Pix[yi+2])
pa = uint32(src.Pix[yi+3])
rOutSum = radiusPlus1 * pr
gOutSum = radiusPlus1 * pg
bOutSum = radiusPlus1 * pb
aOutSum = radiusPlus1 * pa
rSum += sumFactor * pr
gSum += sumFactor * pg
bSum += sumFactor * pb
aSum += sumFactor * pa
stack = stackStart
for i = 0; i < radiusPlus1; i++ {
stack.r = pr
stack.g = pg
stack.b = pb
stack.a = pa
stack = stack.next
}
for i = 1; i < radiusPlus1; i++ {
var diff uint32
if widthMinus1 < i {
diff = widthMinus1
} else {
diff = i
}
p = yi + (diff << 2)
pr = uint32(src.Pix[p])
pg = uint32(src.Pix[p+1])
pb = uint32(src.Pix[p+2])
pa = uint32(src.Pix[p+3])
stack.r = pr
stack.g = pg
stack.b = pb
stack.a = pa
rSum += stack.r * (radiusPlus1 - i)
gSum += stack.g * (radiusPlus1 - i)
bSum += stack.b * (radiusPlus1 - i)
aSum += stack.a * (radiusPlus1 - i)
rInSum += pr
gInSum += pg
bInSum += pb
aInSum += pa
stack = stack.next
}
stackIn = stackStart
stackOut = stackEnd
for x = 0; x < width; x++ {
pa = (aSum * mulSum) >> shgSum
src.Pix[yi+3] = uint8(pa)
if pa != 0 {
src.Pix[yi] = uint8((rSum * mulSum) >> shgSum)
src.Pix[yi+1] = uint8((gSum * mulSum) >> shgSum)
src.Pix[yi+2] = uint8((bSum * mulSum) >> shgSum)
} else {
src.Pix[yi] = 0
src.Pix[yi+1] = 0
src.Pix[yi+2] = 0
}
rSum -= rOutSum
gSum -= gOutSum
bSum -= bOutSum
aSum -= aOutSum
rOutSum -= stackIn.r
gOutSum -= stackIn.g
bOutSum -= stackIn.b
aOutSum -= stackIn.a
p = x + radius + 1
if p > widthMinus1 {
p = widthMinus1
}
p = (yw + p) << 2
stackIn.r = uint32(src.Pix[p])
stackIn.g = uint32(src.Pix[p+1])
stackIn.b = uint32(src.Pix[p+2])
stackIn.a = uint32(src.Pix[p+3])
rInSum += stackIn.r
gInSum += stackIn.g
bInSum += stackIn.b
aInSum += stackIn.a
rSum += rInSum
gSum += gInSum
bSum += bInSum
aSum += aInSum
stackIn = stackIn.next
pr = stackOut.r
pg = stackOut.g
pb = stackOut.b
pa = stackOut.a
rOutSum += pr
gOutSum += pg
bOutSum += pb
aOutSum += pa
rInSum -= pr
gInSum -= pg
bInSum -= pb
aInSum -= pa
stackOut = stackOut.next
yi += 4
}
yw += width
}
for x = 0; x < width; x++ {
rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0
yi = x << 2
pr = uint32(src.Pix[yi])
pg = uint32(src.Pix[yi+1])
pb = uint32(src.Pix[yi+2])
pa = uint32(src.Pix[yi+3])
rOutSum = radiusPlus1 * pr
gOutSum = radiusPlus1 * pg
bOutSum = radiusPlus1 * pb
aOutSum = radiusPlus1 * pa
rSum += sumFactor * pr
gSum += sumFactor * pg
bSum += sumFactor * pb
aSum += sumFactor * pa
stack = stackStart
for i = 0; i < radiusPlus1; i++ {
stack.r = pr
stack.g = pg
stack.b = pb
stack.a = pa
stack = stack.next
}
yp = width
for i = 1; i <= radius; i++ {
yi = (yp + x) << 2
pr = uint32(src.Pix[yi])
pg = uint32(src.Pix[yi+1])
pb = uint32(src.Pix[yi+2])
pa = uint32(src.Pix[yi+3])
stack.r = pr
stack.g = pg
stack.b = pb
stack.a = pa
rSum += stack.r * (radiusPlus1 - i)
gSum += stack.g * (radiusPlus1 - i)
bSum += stack.b * (radiusPlus1 - i)
aSum += stack.a * (radiusPlus1 - i)
rInSum += pr
gInSum += pg
bInSum += pb
aInSum += pa
stack = stack.next
if i < heightMinus1 {
yp += width
}
}
yi = x
stackIn = stackStart
stackOut = stackEnd
for y = 0; y < height; y++ {
p = yi << 2
pa = (aSum * mulSum) >> shgSum
src.Pix[p+3] = uint8(pa)
if pa > 0 {
src.Pix[p] = uint8((rSum * mulSum) >> shgSum)
src.Pix[p+1] = uint8((gSum * mulSum) >> shgSum)
src.Pix[p+2] = uint8((bSum * mulSum) >> shgSum)
} else {
src.Pix[p] = 0
src.Pix[p+1] = 0
src.Pix[p+2] = 0
}
rSum -= rOutSum
gSum -= gOutSum
bSum -= bOutSum
aSum -= aOutSum
rOutSum -= stackIn.r
gOutSum -= stackIn.g
bOutSum -= stackIn.b
aOutSum -= stackIn.a
p = y + radiusPlus1
if p > heightMinus1 {
p = heightMinus1
}
p = (x + (p * width)) << 2
stackIn.r = uint32(src.Pix[p])
stackIn.g = uint32(src.Pix[p+1])
stackIn.b = uint32(src.Pix[p+2])
stackIn.a = uint32(src.Pix[p+3])
rInSum += stackIn.r
gInSum += stackIn.g
bInSum += stackIn.b
aInSum += stackIn.a
rSum += rInSum
gSum += gInSum
bSum += bInSum
aSum += aInSum
stackIn = stackIn.next
pr = stackOut.r
pg = stackOut.g
pb = stackOut.b
pa = stackOut.a
rOutSum += pr
gOutSum += pg
bOutSum += pb
aOutSum += pa
rInSum -= pr
gInSum -= pg
bInSum -= pb
aInSum -= pa
stackOut = stackOut.next
yi += width
}
}
}
================================================
FILE: utils/download.go
================================================
package utils
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
)
// DownloadImage downloads the image from the internet and saves it into a temporary file.
func DownloadImage(url string) (*os.File, error) {
// Retrieve the url and decode the response body.
res, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("unable to download image file from URI: %s, status %v", url, res.Status)
}
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("unable to read response body: %w", err)
}
tmpfile, err := os.CreateTemp("/tmp", "image")
if err != nil {
return nil, fmt.Errorf("unable to create temporary file: %w", err)
}
// Copy the image binary data into the temporary file.
_, err = io.Copy(tmpfile, bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("unable to copy the source URI into the destination file")
}
ctype, err := DetectContentType(tmpfile.Name())
if err != nil {
return nil, err
}
if !strings.Contains(ctype.(string), "image") {
return nil, fmt.Errorf("the downloaded file is not a valid image type")
}
return tmpfile, nil
}
// IsValidUrl tests a string to determine if it is a well-structured url or not.
func IsValidUrl(uri string) bool {
_, err := url.ParseRequestURI(uri)
if err != nil {
return false
}
u, err := url.Parse(uri)
if err != nil || u.Scheme == "" || u.Host == "" {
return false
}
return true
}
// DetectContentType detects the file type by reading MIME type information of the file content.
func DetectContentType(fname string) (any, error) {
file, err := os.Open(fname)
if err != nil {
return nil, err
}
defer func() {
if err := file.Close(); err != nil {
log.Printf("could not close the opened file: %v", err)
}
}()
// Only the first 512 bytes are used to sniff the content type.
buffer := make([]byte, 512)
_, err = file.Read(buffer)
if err != nil {
return nil, err
}
// Reset the read pointer if necessary.
if _, err := file.Seek(0, 0); err != nil {
return nil, err
}
// Always returns a valid content-type and "application/octet-stream" if no others seemed to match.
contentType := http.DetectContentType(buffer)
return string(contentType), nil
}
================================================
FILE: utils/download_test.go
================================================
package utils
import (
"path/filepath"
"strings"
"testing"
)
func TestUtils_ShouldDownloadImage(t *testing.T) {
f, err := DownloadImage("https://raw.githubusercontent.com/esimov/caire/master/testdata/sample.jpg")
if err != nil {
t.Fatalf("could't download test file: %v", err)
}
if !strings.Contains(f.Name(), "tmp") {
t.Errorf("The downloaded image should have been saved in a temporary folder")
}
}
func TestUtils_ShouldBeValidUrl(t *testing.T) {
ok := IsValidUrl("https://github.com/esimov/caire/")
if !ok {
t.Errorf("A valid URL should have been provided")
}
}
func TestUtils_ShouldDetectValidFileType(t *testing.T) {
sampleImg := filepath.Join("../testdata", "sample.jpg")
ftype, err := DetectContentType(sampleImg)
if err != nil {
t.Fatalf("could not detect content type: %v", err)
}
if !strings.Contains(ftype.(string), "image") {
t.Errorf("Content type expected to be of type image, got: %v", ftype)
}
}
================================================
FILE: utils/format.go
================================================
package utils
import (
"fmt"
"image/color"
"math"
"strings"
"time"
)
// MessageType is a custom type used as a placeholder for various message types.
type MessageType int
// The message types used across the CLI application.
const (
DefaultMessage MessageType = iota
SuccessMessage
ErrorMessage
StatusMessage
)
// Colors used across the CLI application.
const (
DefaultColor = "\x1b[0m"
StatusColor = "\x1b[36m"
SuccessColor = "\x1b[32m"
ErrorColor = "\x1b[31m"
)
// DecorateText shows the message types in different colors.
func DecorateText(s string, msgType MessageType) string {
switch msgType {
case DefaultMessage:
s = DefaultColor + s
case StatusMessage:
s = StatusColor + s
case SuccessMessage:
s = SuccessColor + s
case ErrorMessage:
s = ErrorColor + s
default:
return s
}
return s + DefaultColor
}
// FormatTime formats time.Duration output to a human readable value.
func FormatTime(d time.Duration) string {
if d.Seconds() < 60.0 {
return fmt.Sprintf("%.2fs", d.Seconds())
}
if d.Minutes() < 60.0 {
remainingSeconds := math.Mod(d.Seconds(), 60)
return fmt.Sprintf("%dm %.2fs", int64(d.Minutes()), remainingSeconds)
}
if d.Hours() < 24.0 {
remainingMinutes := math.Mod(d.Minutes(), 60)
remainingSeconds := math.Mod(d.Seconds(), 60)
return fmt.Sprintf("%dh %dm %.2fs",
int64(d.Hours()), int64(remainingMinutes), remainingSeconds)
}
remainingHours := math.Mod(d.Hours(), 24)
remainingMinutes := math.Mod(d.Minutes(), 60)
remainingSeconds := math.Mod(d.Seconds(), 60)
return fmt.Sprintf("%dd %dh %dm %.2fs",
int64(d.Hours()/24), int64(remainingHours),
int64(remainingMinutes), remainingSeconds)
}
// HexToRGBA converts a color expressed as hexadecimal string to RGBA color.
func HexToRGBA(x string) color.NRGBA {
var r, g, b, a uint8
x = strings.TrimPrefix(x, "#")
a = 255
if len(x) == 2 {
format := "%03x"
fmt.Sscanf(x, format, &r, &g, &b)
}
if len(x) == 3 {
format := "%1x%1x%1x"
fmt.Sscanf(x, format, &r, &g, &b)
r |= r << 4
g |= g << 4
b |= b << 4
}
if len(x) == 6 {
format := "%02x%02x%02x"
fmt.Sscanf(x, format, &r, &g, &b)
}
if len(x) == 8 {
format := "%02x%02x%02x%02x"
fmt.Sscanf(x, format, &r, &g, &b, &a)
}
return color.NRGBA{R: r, G: g, B: b, A: a}
}
// RGB returns color based on RGB in range 0..1
func RGB(r, g, b float32) color.NRGBA {
return color.NRGBA{R: sat8(r), G: sat8(g), B: sat8(b), A: 0xFF}
}
// RGBA returns color based on RGBA in range 0..1
func RGBA(r, g, b, a float32) color.NRGBA {
return color.NRGBA{R: sat8(r), G: sat8(g), B: sat8(b), A: sat8(a)}
}
// HSLA returns color based on HSLA in range 0..1
func HSLA(h, s, l, a float32) color.NRGBA { return RGBA(hsla(h, s, l, a)) }
// HSL returns color based on HSL in range 0..1
func HSL(h, s, l float32) color.NRGBA { return HSLA(h, s, l, 1) }
func hue(v1, v2, h float32) float32 {
if h < 0 {
h += 1
}
if h > 1 {
h -= 1
}
if 6*h < 1 {
return v1 + (v2-v1)*6*h
} else if 2*h < 1 {
return v2
} else if 3*h < 2 {
return v1 + (v2-v1)*(2.0/3.0-h)*6
}
return v1
}
func hsla(h, s, l, a float32) (r, g, b, ra float32) {
if s == 0 {
return l, l, l, a
}
h = mod32(h, 1)
var v2 float32
if l < 0.5 {
v2 = l * (1 + s)
} else {
v2 = (l + s) - s*l
}
v1 := 2*l - v2
r = hue(v1, v2, h+1.0/3.0)
g = hue(v1, v2, h)
b = hue(v1, v2, h-1.0/3.0)
ra = a
return
}
func sat8(v float32) uint8 {
v *= 255.0
if v >= 255 {
return 255
} else if v <= 0 {
return 0
}
return uint8(v)
}
func mod32(x, y float32) float32 { return float32(math.Mod(float64(x), float64(y))) }
================================================
FILE: utils/spinner.go
================================================
package utils
import (
"fmt"
"io"
"os"
"runtime"
"strings"
"sync"
"time"
"unicode/utf8"
)
// Spinner initializes the progress indicator.
type Spinner struct {
mu *sync.RWMutex
delay time.Duration
writer io.Writer
message string
lastOutput string
StopMsg string
hideCursor bool
stopChan chan struct{}
}
// NewSpinner instantiates a new progress indicator.
func NewSpinner(msg string, d time.Duration) *Spinner {
return &Spinner{
mu: &sync.RWMutex{},
delay: d,
writer: os.Stderr,
message: msg,
hideCursor: true,
stopChan: make(chan struct{}, 1),
}
}
// Start starts the progress indicator.
func (s *Spinner) Start() {
if s.hideCursor && runtime.GOOS != "windows" {
// hides the cursor
fmt.Fprintf(s.writer, "\033[?25l")
}
charSet := []string{"⠈⠁", "⠈⠑", "⠈⠱", "⠈⡱", "⢀⡱", "⢄⡱", "⢄⡱", "⢆⡱", "⢎⡱", "⢎⡰", "⢎⡠", "⢎⡀", "⢎⠁", "⠎⠁", "⠊⠁"}
go func() {
for {
for _, r := range charSet {
select {
case <-s.stopChan:
return
default:
s.mu.Lock()
output := fmt.Sprintf("\r%s%s %s%s", s.message, StatusColor, r, DefaultColor)
fmt.Fprintf(s.writer, output)
s.lastOutput = output
s.mu.Unlock()
time.Sleep(s.delay)
}
}
}
}()
}
// Stop stops the progress indicator.
func (s *Spinner) Stop() {
s.mu.Lock()
defer s.mu.Unlock()
s.clear()
s.RestoreCursor()
if len(s.StopMsg) > 0 {
fmt.Fprintf(s.writer, s.StopMsg)
}
s.stopChan <- struct{}{}
}
// RestoreCursor restores back the cursor visibility.
func (s *Spinner) RestoreCursor() {
if s.hideCursor && runtime.GOOS != "windows" {
// makes the cursor visible
fmt.Fprint(s.writer, "\033[?25h")
}
}
// clear deletes the last line. Caller must hold the the locker.
func (s *Spinner) clear() {
n := utf8.RuneCountInString(s.lastOutput)
if runtime.GOOS == "windows" {
clearString := "\r" + strings.Repeat(" ", n) + "\r"
fmt.Fprint(s.writer, clearString)
s.lastOutput = ""
return
}
for _, c := range []string{"\b", "\127", "\b", "\033[K"} { // "\033[K" for macOS Terminal
fmt.Fprint(s.writer, strings.Repeat(c, n))
}
fmt.Fprintf(s.writer, "\r\033[K") // clear line
s.lastOutput = ""
}
================================================
FILE: utils/utils.go
================================================
package utils
import (
"slices"
"golang.org/x/exp/constraints"
)
// Min returns the slowest value of the provided parameters.
func Min[T constraints.Ordered](values ...T) T {
var acc T = values[0]
for _, v := range values {
if v < acc {
acc = v
}
}
return acc
}
// Max returns the biggest value of the provided parameters.
func Max[T constraints.Ordered](values ...T) T {
var acc T = values[0]
for _, v := range values {
if v > acc {
acc = v
}
}
return acc
}
// Abs returns the absolut value of x.
func Abs[T constraints.Signed | constraints.Float](x T) T {
if x < 0 {
return -x
}
return x
}
// Contains returns true if a value is available in the collection.
func Contains[T comparable](slice []T, value T) bool {
return slices.Contains(slice, value)
}
gitextract_jflphtww/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ └── build.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── build.sh
├── carver.go
├── carver_benchmark_test.go
├── carver_test.go
├── cmd/
│ └── caire/
│ └── main.go
├── data/
│ └── facefinder
├── doc.go
├── draw.go
├── exec.go
├── go.mod
├── go.sum
├── gui.go
├── image.go
├── image_test.go
├── imop/
│ ├── blend.go
│ ├── blend_test.go
│ ├── comp.go
│ └── comp_test.go
├── preview.go
├── processor.go
├── processor_test.go
├── snapcraft.yaml
├── sobel.go
├── stackblur.go
└── utils/
├── download.go
├── download_test.go
├── format.go
├── spinner.go
└── utils.go
SYMBOL INDEX (197 symbols across 23 files)
FILE: carver.go
type SeamCarver (line 16) | type SeamCarver interface
constant maxFaceDetAttempts (line 21) | maxFaceDetAttempts = 20
type Carver (line 34) | type Carver struct
method get (line 58) | func (c *Carver) get(x, y int) float64 {
method set (line 64) | func (c *Carver) set(x, y int, px float64) {
method ComputeSeams (line 77) | func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image....
method FindLowestEnergySeams (line 258) | func (c *Carver) FindLowestEnergySeams(p *Processor) []Seam {
method RemoveSeam (line 319) | func (c *Carver) RemoveSeam(img *image.NRGBA, seams []Seam, debug bool...
method AddSeam (line 342) | func (c *Carver) AddSeam(img *image.NRGBA, seams []Seam, debug bool) *...
type Seam (line 42) | type Seam struct
function NewCarver (line 48) | func NewCarver(width, height int) *Carver {
FILE: carver_benchmark_test.go
function Benchmark_Carver (line 10) | func Benchmark_Carver(b *testing.B) {
FILE: carver_test.go
constant imgWidth (line 17) | imgWidth = 10
constant imgHeight (line 18) | imgHeight = 10
function init (line 23) | func init() {
function TestCarver_EnergySeamShouldNotBeDetected (line 35) | func TestCarver_EnergySeamShouldNotBeDetected(t *testing.T) {
function TestCarver_DetectHorizontalEnergySeam (line 64) | func TestCarver_DetectHorizontalEnergySeam(t *testing.T) {
function TestCarver_DetectVerticalEnergySeam (line 103) | func TestCarver_DetectVerticalEnergySeam(t *testing.T) {
function TestCarver_RemoveSeam (line 143) | func TestCarver_RemoveSeam(t *testing.T) {
function TestCarver_AddSeam (line 182) | func TestCarver_AddSeam(t *testing.T) {
function TestCarver_ComputeSeams (line 221) | func TestCarver_ComputeSeams(t *testing.T) {
function TestCarver_ShouldDetectFace (line 244) | func TestCarver_ShouldDetectFace(t *testing.T) {
function TestCarver_ShouldNotRemoveFaceZone (line 293) | func TestCarver_ShouldNotRemoveFaceZone(t *testing.T) {
function TestCarver_ShouldNotResizeWithFaceDistorsion (line 373) | func TestCarver_ShouldNotResizeWithFaceDistorsion(t *testing.T) {
function findNonZeroValue (line 428) | func findNonZeroValue(points []float64) bool {
FILE: cmd/caire/main.go
constant HelpBanner (line 15) | HelpBanner = `
constant pipeName (line 26) | pipeName = "-"
function main (line 52) | func main() {
FILE: draw.go
type ShapeType (line 13) | type ShapeType
constant Circle (line 16) | Circle ShapeType = "circle"
constant Line (line 17) | Line ShapeType = "line"
method DrawSeam (line 22) | func (g *Gui) DrawSeam(shape ShapeType, x, y, thickness float32) {
method drawCircle (line 34) | func (g *Gui) drawCircle(x, y, radius float32) {
method drawLine (line 61) | func (g *Gui) drawLine(x, y, thickness float32) {
method point (line 82) | func (g *Gui) point(x, y float32) f32.Point {
method setColor (line 90) | func (g *Gui) setColor(c color.Color) color.NRGBA {
method setFillColor (line 101) | func (g *Gui) setFillColor(c color.Color) {
method getFillColor (line 106) | func (g *Gui) getFillColor() color.Color {
function getRatio (line 111) | func getRatio(w, h float32) float32 {
FILE: exec.go
type Image (line 31) | type Image struct
method consumer (line 177) | func (img *Image) consumer(
method process (line 200) | func (img *Image) process(p *Processor, in, out string) error {
method pathToFile (line 292) | func (img *Image) pathToFile(in, out string) (io.Reader, io.Writer, er...
method printOpStatus (line 333) | func (img *Image) printOpStatus(fname string, err error) {
type result (line 37) | type result struct
function Resize (line 42) | func Resize(s SeamCarver, img *image.NRGBA) (image.Image, error) {
method Execute (line 49) | func (p *Processor) Execute(img *Image) {
function walkDir (line 352) | func walkDir(
FILE: gui.go
type hudControlType (line 29) | type hudControlType
constant hudShowSeams (line 32) | hudShowSeams hudControlType = iota
constant hudShowDebugMask (line 33) | hudShowDebugMask
constant redStart (line 39) | redStart = 137
constant greenStart (line 40) | greenStart = 47
constant blueStart (line 41) | blueStart = 54
constant redEnd (line 44) | redEnd = 255
constant greenEnd (line 45) | greenEnd = 112
constant blueEnd (line 46) | blueEnd = 105
type interval (line 57) | type interval struct
type Gui (line 63) | type Gui struct
method AddHudControl (line 137) | func (g *Gui) AddHudControl(hudControlType hudControlType, title strin...
method initWindow (line 148) | func (g *Gui) initWindow(width, height int) {
method getWindowSize (line 170) | func (g *Gui) getWindowSize() (float32, float32) {
method Run (line 184) | func (g *Gui) Run() error {
method draw (line 360) | func (g *Gui) draw(bgColor color.NRGBA) {
method displayMessage (line 483) | func (g *Gui) displayMessage(ctx layout.Context, bgCol color.NRGBA, ms...
type hudCtrl (line 103) | type hudCtrl struct
function NewGUI (line 110) | func NewGUI(width, height int) *Gui {
function random (line 563) | func random(min, max float32) float32 {
FILE: image.go
function decodeImg (line 20) | func decodeImg(src string) (image.Image, error) {
function encodeImg (line 44) | func encodeImg(p *Processor, w io.Writer, img *image.NRGBA) error {
function rotateImage90 (line 80) | func rotateImage90(src *image.NRGBA) *image.NRGBA {
function rotateImage270 (line 97) | func rotateImage270(src *image.NRGBA) *image.NRGBA {
function imgToNRGBA (line 115) | func imgToNRGBA(img image.Image) *image.NRGBA {
function imgToPix (line 174) | func imgToPix(src *image.NRGBA) []uint8 {
function pixToImage (line 189) | func pixToImage(pixels []uint8, width, height int) image.Image {
function rgbToGrayscale (line 216) | func rgbToGrayscale(src *image.NRGBA) []uint8 {
function dither (line 235) | func dither(src *image.NRGBA) *image.NRGBA {
FILE: image_test.go
function TestImage_ShouldGetSampleImage (line 15) | func TestImage_ShouldGetSampleImage(t *testing.T) {
function TestImage_ImgToNRGBA (line 23) | func TestImage_ImgToNRGBA(t *testing.T) {
function scan (line 83) | func scan(img image.Image, x1, y1, x2, y2 int, dst []uint8) {
function makeYCbCrImage (line 154) | func makeYCbCrImage(rect image.Rectangle, colors []color.Color, sr image...
function makeNRGBAImage (line 169) | func makeNRGBAImage(rect image.Rectangle, colors []color.Color) *image.N...
function fillDrawImage (line 175) | func fillDrawImage(img draw.Image, colors []color.Color) {
function readRow (line 192) | func readRow(img image.Image, y int) []uint8 {
function readColumn (line 206) | func readColumn(img image.Image, x int) []uint8 {
function compareBytes (line 220) | func compareBytes(a, b []uint8, delta int) bool {
FILE: imop/blend.go
type BlendType (line 21) | type BlendType
constant Normal (line 24) | Normal BlendType = iota
constant Darken (line 25) | Darken
constant Lighten (line 26) | Lighten
constant Multiply (line 27) | Multiply
constant Screen (line 28) | Screen
constant Overlay (line 29) | Overlay
constant SoftLight (line 30) | SoftLight
constant HardLight (line 31) | HardLight
constant ColorDodge (line 32) | ColorDodge
constant ColorBurn (line 33) | ColorBurn
constant Difference (line 34) | Difference
constant Exclusion (line 35) | Exclusion
constant Hue (line 38) | Hue
constant Saturation (line 39) | Saturation
constant ColorMode (line 40) | ColorMode
constant Luminosity (line 41) | Luminosity
type Blend (line 45) | type Blend struct
method Set (line 68) | func (bl *Blend) Set(blendType BlendType) error {
method Get (line 78) | func (bl *Blend) Get() BlendType {
method Lum (line 83) | func (bl *Blend) Lum(rgb Color) float64 {
method SetLum (line 88) | func (bl *Blend) SetLum(rgb Color, l float64) Color {
method clip (line 98) | func (bl *Blend) clip(rgb Color) Color {
method Sat (line 120) | func (bl *Blend) Sat(rgb Color) float64 {
method SetSat (line 133) | func (bl *Blend) SetSat(rgb Color, s float64) Color {
method AlphaCompose (line 164) | func (bl *Blend) AlphaCompose(
type Color (line 51) | type Color struct
function NewBlend (line 56) | func NewBlend() *Blend {
type channel (line 128) | type channel struct
FILE: imop/blend_test.go
function TestBlend_Basic (line 12) | func TestBlend_Basic(t *testing.T) {
function TestBlend_Modes (line 42) | func TestBlend_Modes(t *testing.T) {
function TestBlend_NonSeparableModes (line 151) | func TestBlend_NonSeparableModes(t *testing.T) {
FILE: imop/comp.go
type CompType (line 17) | type CompType
constant Clear (line 20) | Clear CompType = iota
constant Copy (line 21) | Copy
constant Dst (line 22) | Dst
constant SrcOver (line 23) | SrcOver
constant DstOver (line 24) | DstOver
constant SrcIn (line 25) | SrcIn
constant DstIn (line 26) | DstIn
constant SrcOut (line 27) | SrcOut
constant DstOut (line 28) | DstOut
constant SrcAtop (line 29) | SrcAtop
constant DstAtop (line 30) | DstAtop
constant Xor (line 31) | Xor
type Bitmap (line 36) | type Bitmap struct
type Composite (line 41) | type Composite struct
method Set (line 75) | func (op *Composite) Set(compType CompType) error {
method Get (line 85) | func (op *Composite) Get() CompType {
method Draw (line 92) | func (op *Composite) Draw(bitmap *Bitmap, src, dst *image.NRGBA, blend...
function NewBitmap (line 47) | func NewBitmap(rect image.Rectangle) *Bitmap {
function InitOp (line 54) | func InitOp() *Composite {
FILE: imop/comp_test.go
function TestComp_Basic (line 12) | func TestComp_Basic(t *testing.T) {
function TestComp_Ops (line 25) | func TestComp_Ops(t *testing.T) {
FILE: preview.go
method showPreview (line 8) | func (p *Processor) showPreview(
FILE: processor.go
type worker (line 30) | type worker struct
type shrinkFn (line 38) | type shrinkFn
type enlargeFn (line 41) | type enlargeFn
type Processor (line 44) | type Processor struct
method Resize (line 77) | func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
method calculateFitness (line 405) | func (p *Processor) calculateFitness(img *image.NRGBA, c *Carver) *ima...
method Process (line 448) | func (p *Processor) Process(r io.Reader, w io.Writer) error {
method shrink (line 504) | func (p *Processor) shrink(img *image.NRGBA) (*image.NRGBA, error) {
method enlarge (line 540) | func (p *Processor) enlarge(img *image.NRGBA) (*image.NRGBA, error) {
FILE: processor_test.go
function TestResize_ShrinkImageWidth (line 10) | func TestResize_ShrinkImageWidth(t *testing.T) {
function TestResize_ShrinkImageHeight (line 33) | func TestResize_ShrinkImageHeight(t *testing.T) {
function TestResize_EnlargeImageWidth (line 58) | func TestResize_EnlargeImageWidth(t *testing.T) {
function TestResize_EnlargeImageHeight (line 81) | func TestResize_EnlargeImageHeight(t *testing.T) {
FILE: sobel.go
type kernel (line 8) | type kernel
method SobelDetector (line 26) | func (c *Carver) SobelDetector(img *image.NRGBA, threshold float64) *ima...
method getImageData (line 91) | func (c *Carver) getImageData(img *image.NRGBA) []uint8 {
FILE: stackblur.go
type blurStack (line 13) | type blurStack struct
function Stackblur (line 57) | func Stackblur(dst, src image.Image, radius uint32) error {
function process (line 76) | func process(dst *image.NRGBA, src image.Image, radius uint32) {
function blurImage (line 128) | func blurImage(src *image.NRGBA, radius uint32) {
FILE: utils/download.go
function DownloadImage (line 15) | func DownloadImage(url string) (*os.File, error) {
function IsValidUrl (line 52) | func IsValidUrl(uri string) bool {
function DetectContentType (line 67) | func DetectContentType(fname string) (any, error) {
FILE: utils/download_test.go
function TestUtils_ShouldDownloadImage (line 9) | func TestUtils_ShouldDownloadImage(t *testing.T) {
function TestUtils_ShouldBeValidUrl (line 20) | func TestUtils_ShouldBeValidUrl(t *testing.T) {
function TestUtils_ShouldDetectValidFileType (line 27) | func TestUtils_ShouldDetectValidFileType(t *testing.T) {
FILE: utils/format.go
type MessageType (line 12) | type MessageType
constant DefaultMessage (line 16) | DefaultMessage MessageType = iota
constant SuccessMessage (line 17) | SuccessMessage
constant ErrorMessage (line 18) | ErrorMessage
constant StatusMessage (line 19) | StatusMessage
constant DefaultColor (line 24) | DefaultColor = "\x1b[0m"
constant StatusColor (line 25) | StatusColor = "\x1b[36m"
constant SuccessColor (line 26) | SuccessColor = "\x1b[32m"
constant ErrorColor (line 27) | ErrorColor = "\x1b[31m"
function DecorateText (line 31) | func DecorateText(s string, msgType MessageType) string {
function FormatTime (line 48) | func FormatTime(d time.Duration) string {
function HexToRGBA (line 71) | func HexToRGBA(x string) color.NRGBA {
function RGB (line 99) | func RGB(r, g, b float32) color.NRGBA {
function RGBA (line 104) | func RGBA(r, g, b, a float32) color.NRGBA {
function HSLA (line 109) | func HSLA(h, s, l, a float32) color.NRGBA { return RGBA(hsla(h, s, l, a)) }
function HSL (line 112) | func HSL(h, s, l float32) color.NRGBA { return HSLA(h, s, l, 1) }
function hue (line 114) | func hue(v1, v2, h float32) float32 {
function hsla (line 132) | func hsla(h, s, l, a float32) (r, g, b, ra float32) {
function sat8 (line 155) | func sat8(v float32) uint8 {
function mod32 (line 165) | func mod32(x, y float32) float32 { return float32(math.Mod(float64(x), f...
FILE: utils/spinner.go
type Spinner (line 15) | type Spinner struct
method Start (line 39) | func (s *Spinner) Start() {
method Stop (line 68) | func (s *Spinner) Stop() {
method RestoreCursor (line 81) | func (s *Spinner) RestoreCursor() {
method clear (line 89) | func (s *Spinner) clear() {
function NewSpinner (line 27) | func NewSpinner(msg string, d time.Duration) *Spinner {
FILE: utils/utils.go
function Min (line 10) | func Min[T constraints.Ordered](values ...T) T {
function Max (line 22) | func Max[T constraints.Ordered](values ...T) T {
function Abs (line 34) | func Abs[T constraints.Signed | constraints.Float](x T) T {
function Contains (line 42) | func Contains[T comparable](slice []T, value T) bool {
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (176K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 15,
"preview": "github: esimov\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 1036,
"preview": "---\nname: Bug report\nabout: Create a report to help me improve the library\nlabels: ''\n\n---\n\n### Describe the bug\n<!--A c"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 599,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\nlabels: ''\n\n---\n\n### Is your feature request related t"
},
{
"path": ".github/workflows/build.yml",
"chars": 1383,
"preview": "name: build\n\non:\n push:\n branches: [master]\n pull_request:\n branches: [master]\n\njobs:\n build:\n name: Build\n "
},
{
"path": ".gitignore",
"chars": 113,
"preview": "*.jpg\n*.png\n*.jpeg\ncoverage.out\ntest-report.json\n/packages\n!/testdata/*.png\n!/testdata/*.jpg\n!/examples/**/*.png\n"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2018 Endre Simo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "Makefile",
"chars": 243,
"preview": "all: \n\t@./build.sh\nclean:\n\t@rm -f caire\ninstall: all\n\t@cp caire /usr/local/bin\nuninstall: \n\t@rm -f /usr/local/bin/caire\n"
},
{
"path": "README.md",
"chars": 12211,
"preview": "<h1 align=\"center\"><img alt=\"Caire Logo\" src=\"https://user-images.githubusercontent.com/883386/51555990-a1762600-1e81-11"
},
{
"path": "build.sh",
"chars": 1451,
"preview": "#!/bin/bash\nset -e\n\nVERSION=\"1.5.0\"\nPROTECTED_MODE=\"no\"\n\nexport GO15VENDOREXPERIMENT=1\n\ncd $(dirname \"${BASH_SOURCE[0]}\""
},
{
"path": "carver.go",
"chars": 10978,
"preview": "package caire\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"math\"\n\n\t\"github.com/esimov/caire/utils\"\n\tpigo \"gi"
},
{
"path": "carver_benchmark_test.go",
"chars": 697,
"preview": "package caire\n\nimport (\n\t\"image\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc Benchmark_Carver(b *testing.B) {\n\tsampleImg :"
},
{
"path": "carver_test.go",
"chars": 11732,
"preview": "package caire\n\nimport (\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/esimov/cai"
},
{
"path": "cmd/caire/main.go",
"chars": 2930,
"preview": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"gioui.org/app\"\n\t\"github.com/esimov/caire\"\n\t\"github.com/"
},
{
"path": "doc.go",
"chars": 734,
"preview": "/*\nPackage caire is a content aware image resize library, which can rescale the source image seamlessly\nboth vertically "
},
{
"path": "draw.go",
"chars": 2792,
"preview": "package caire\n\nimport (\n\t\"image/color\"\n\t\"math\"\n\n\t\"gioui.org/f32\"\n\t\"gioui.org/op/clip\"\n\t\"gioui.org/op/paint\"\n\t\"github.com"
},
{
"path": "exec.go",
"chars": 9646,
"preview": "package caire\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"image\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sync\"\n\t\""
},
{
"path": "go.mod",
"chars": 765,
"preview": "module github.com/esimov/caire\n\ngo 1.22\n\nrequire (\n\tgioui.org v0.8.0\n\tgithub.com/disintegration/imaging v1.6.2\n\tgithub.c"
},
{
"path": "go.sum",
"chars": 4684,
"preview": "eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=\neliasnaur.com/font"
},
{
"path": "gui.go",
"chars": 14152,
"preview": "package caire\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"math\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"gioui.org/app\"\n\t\"gi"
},
{
"path": "image.go",
"chars": 6426,
"preview": "package caire\r\n\r\nimport (\r\n\t\"errors\"\r\n\t\"fmt\"\r\n\t\"image\"\r\n\t\"image/color\"\r\n\t\"image/jpeg\"\r\n\t\"image/png\"\r\n\t\"io\"\r\n\t\"os\"\r\n\t\"pat"
},
{
"path": "image_test.go",
"chars": 5368,
"preview": "package caire\n\nimport (\n\t\"image\"\n\t\"image/color\"\n\t\"image/color/palette\"\n\t\"image/draw\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n"
},
{
"path": "imop/blend.go",
"chars": 4517,
"preview": "// Package imop implements the Porter-Duff composition operations\r\n// used for mixing a graphic element with its backdro"
},
{
"path": "imop/blend_test.go",
"chars": 6258,
"preview": "package imop\r\n\r\nimport (\r\n\t\"image\"\r\n\t\"image/color\"\r\n\t\"image/draw\"\r\n\t\"testing\"\r\n\r\n\t\"github.com/stretchr/testify/assert\"\r\n"
},
{
"path": "imop/comp.go",
"chars": 11287,
"preview": "// Package imop implements the Porter-Duff composition operations\n// used for mixing a graphic element with its backdrop"
},
{
"path": "imop/comp_test.go",
"chars": 4983,
"preview": "package imop\n\nimport (\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc T"
},
{
"path": "preview.go",
"chars": 641,
"preview": "package caire\n\nimport (\n\t\"os\"\n)\n\n// showPreview spawns a new Gio GUI window and updates its content with the resized ima"
},
{
"path": "processor.go",
"chars": 14619,
"preview": "package caire\n\nimport (\n\t_ \"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"image\"\n\t\"image/draw\"\n\t\"io\"\n\t\"math\"\n\n\t\"github.com/disintegration/im"
},
{
"path": "processor_test.go",
"chars": 2583,
"preview": "package caire\n\nimport (\n\t\"image\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestResize_ShrinkImageWidth(t"
},
{
"path": "snapcraft.yaml",
"chars": 976,
"preview": "name: caire\nversion: '1.4.4'\nsummary: Content aware image resize library\ndescription: |\n Content aware image resize lib"
},
{
"path": "sobel.go",
"chars": 2278,
"preview": "package caire\n\nimport (\n\t\"image\"\n\t\"math\"\n)\n\ntype kernel [][]int32\n\nvar (\n\tkernelX = kernel{\n\t\t{-1, 0, 1},\n\t\t{-2, 0, 2},\n"
},
{
"path": "stackblur.go",
"chars": 10411,
"preview": "// Go implementation of the StackBlur algorithm\n// http://incubator.quasimondo.com/processing/fast_blur_deluxe.php\n\npack"
},
{
"path": "utils/download.go",
"chars": 2261,
"preview": "package utils\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n)\n\n// DownloadImage downloa"
},
{
"path": "utils/download_test.go",
"chars": 946,
"preview": "package utils\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestUtils_ShouldDownloadImage(t *testing.T) {\n\tf,"
},
{
"path": "utils/format.go",
"chars": 3588,
"preview": "package utils\n\nimport (\n\t\"fmt\"\n\t\"image/color\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n)\n\n// MessageType is a custom type used as a pl"
},
{
"path": "utils/spinner.go",
"chars": 2194,
"preview": "package utils\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"unicode/utf8\"\n)\n\n// Spinner initializ"
},
{
"path": "utils/utils.go",
"chars": 792,
"preview": "package utils\n\nimport (\n\t\"slices\"\n\n\t\"golang.org/x/exp/constraints\"\n)\n\n// Min returns the slowest value of the provided p"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the esimov/caire GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (153.7 KB), approximately 52.7k tokens, and a symbol index with 197 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.