Skip to content

Commit

Permalink
Add intelligent black bar detection
Browse files Browse the repository at this point in the history
  • Loading branch information
mass8326 committed Jul 26, 2024
1 parent 2059c30 commit 09ba580
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 82 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

Simple program that crops multiple images into squares!

- Easy to use - simply drag and drop your files onto the executable
- Easy to use - drag and drop your files onto the executable to crop each image in a single go
- Intelligent - drag and drop entire folders and only images with black bars will be cropped
- Nondestructive - cropped images get saved as new files in the same directory
- Lossless - outputs png files with no quality degradation

Made for my friends using [Photoboio's camera asset](https://photoboio.gumroad.com/l/rgiyyl) for VRChat.
Made for my friends who've been using [Photoboio Instant Camera](https://photoboio.gumroad.com/l/rgiyyl) for [VRChat](https://hello.vrchat.com/).

## Download

Expand Down
48 changes: 48 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cmd

import (
"os"
"path/filepath"
"sync"

"github.com/mass8326/imgchop/lib/imgchop"
"github.com/mass8326/imgchop/lib/logger"
"github.com/spf13/cobra"
)

var version = "[N/A]"

func Execute() {
var flags RootFlags

cmd := &cobra.Command{
Use: filepath.Base(os.Args[0]) + " [flags] [paths...]",
Short: "Chop your images into squares",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
cmd.Help()
logger.Exit(1)
}

warning := false
var wg sync.WaitGroup
for _, file := range args {
wg.Add(1)
go imgchop.Process(&wg, &warning, file, *flags.intelligent)
}
wg.Wait()

if warning {
logger.Exit(0)
}
},
Version: version,
}

flags = initFlags(cmd)

err := cmd.Execute()
if err != nil {
logger.Exit(1)
}
}
17 changes: 17 additions & 0 deletions cmd/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cmd

import "github.com/spf13/cobra"

type RootFlags struct {
intelligent *bool
}

func initFlags(cmd *cobra.Command) RootFlags {
cmd.PersistentFlags().Bool("help", false, "print help message")
cmd.PersistentFlags().Bool("version", false, "print version")
cmd.SetVersionTemplate("{{.Version}}\n")

return RootFlags{
intelligent: cmd.PersistentFlags().BoolP("intelligent", "i", false, "enable intelligent filter"),
}
}
51 changes: 0 additions & 51 deletions cmd/root.go

This file was deleted.

117 changes: 91 additions & 26 deletions lib/imgchop/imgchop.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package imgchop

import (
"fmt"
"image"
"image/png"
"os"
Expand All @@ -9,27 +10,66 @@ import (
"sync"

"github.com/mass8326/imgchop/lib/logger"
"github.com/mass8326/imgchop/lib/util"
)

func Crop(wg *sync.WaitGroup, warning *bool, file string) {
func Process(wg *sync.WaitGroup, warning *bool, name string, intelligent bool) {
defer wg.Done()

warn := func(msg string) {
logger.Logger.Printf("%s (%s)\n", msg, file)
logger.Logger.Printf("%s (%s)\n", msg, name)
*warning = true
}

img, err := readImage(file)
fd, err := os.Open(name)
if err != nil {
warn("Unable to read image!")
warn("Unable to open path!")
return
}
defer fd.Close()

stat, err := fd.Stat()
if err != nil {
warn("Unable to stat path!")
return
}

if stat.IsDir() {
entries, err := fd.ReadDir(0)
if err != nil {
warn("Unable to read directory!")
return
}
for _, entry := range entries {
wg.Add(1)
go Process(wg, warning, filepath.Join(name, entry.Name()), true)
}
} else {
wg.Add(1)
Crop(wg, warning, fd, intelligent)
}
}

func Crop(wg *sync.WaitGroup, warning *bool, fd *os.File, intelligent bool) {
defer wg.Done()

fname := fd.Name()
warn := func(msg string) {
logger.Logger.Printf("%s (%s)\n", msg, fname)
*warning = true
}

input, _, err := image.Decode(fd)
if err != nil {
warn("Unable to decode image!")
return
}

type Croppable interface {
Bounds() image.Rectangle
SubImage(r image.Rectangle) image.Image
}
croppable, ok := img.(Croppable)
croppable, ok := input.(Croppable)
if !ok {
warn("Image does not support cropping!")
return
Expand All @@ -42,43 +82,68 @@ func Crop(wg *sync.WaitGroup, warning *bool, file string) {
warn("Image is already a square!")
return
case width&1 == 1:
warn("Image width is an odd number and the image cannot be squared!")
warn("Image width is an odd number and the image cannot be cropped into a square!")
return
case height&1 == 1:
warn("Image height is an odd number and the image cannot be squared!")
warn("Image height is an odd number and the image cannot be cropped into a square!")
return
case width > height:
}

if intelligent {
var checks [2]image.Rectangle
if width > height {
offset := (width - height) / 2
checks = [2]image.Rectangle{image.Rect(0, 0, offset, height), image.Rect(width-offset, 0, width, height)}
} else {
offset := (height - width) / 2
checks = [2]image.Rectangle{image.Rect(0, 0, width, offset), image.Rect(0, height-offset, width, height)}
}

for _, check := range checks {
bounds := check.Bounds()
maximums := uint64(0)
minimums := uint64(0)
for x := bounds.Min.X; x < bounds.Max.X; x++ {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
r, g, b := util.ColorToNRGB(input.At(x, y))
maximum := max(r, g, b)
minimum := min(r, g, b)
lum := float32(maximum-minimum) * 100 / 255 / 2
if lum > 20 {
warn(fmt.Sprintf("Image did not pass intelligent filter (%f%% pixel luminosity at [%d, %d])", lum, x, y))
return
}

maximums += uint64(maximum)
minimums += uint64(minimum)
}
}
avg := float32(maximums-minimums) / 2 / float32(bounds.Dx()) / float32(bounds.Dy())
lum := avg * 100 / 255
if lum > 1 {
warn(fmt.Sprintf("Image did not pass intelligent filter (%f%% average luminosity)", lum))
return
}
}
}

if width > height {
offset := (width - height) / 2
target = image.Rect(offset, 0, width-offset, height)
default:
} else {
offset := (height - width) / 2
target = image.Rect(0, offset, width, height-offset)
}
result := croppable.SubImage(target)

dir := filepath.Dir(file)
basename := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file))
dir := filepath.Dir(fname)
basename := strings.TrimSuffix(filepath.Base(fname), filepath.Ext(fname))
err = writeImage(result, filepath.Join(dir, basename+"-imgchop.png"))
if err != nil {
warn("Could not save output image!")
}
}

func readImage(name string) (image.Image, error) {
fd, err := os.Open(name)
if err != nil {
return nil, err
}
defer fd.Close()

img, _, err := image.Decode(fd)
if err != nil {
return nil, err
}

return img, nil
}

func writeImage(img image.Image, name string) error {
fd, err := os.Create(name)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions lib/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import (
"log"
"os"

"github.com/inconshreveable/mousetrap"
"github.com/mass8326/imgchop/lib/util"
"github.com/spf13/cobra"
)

var Logger = log.New(os.Stderr, "", 0)

func Exit(code int) {
if mousetrap.StartedByExplorer() {
if util.StartedByExplorer {
Logger.Println("\nPress 'Enter' to exit...")
bufio.NewReader(os.Stdin).ReadBytes('\n')
}
Expand Down
12 changes: 12 additions & 0 deletions lib/util/color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package util

import "image/color"

func ColorToNRGB(clr color.Color) (r uint8, g uint8, b uint8) {
r16, g16, b16, a16 := clr.RGBA()
factor := float32(a16) / float32(65535)
nr8 := uint32(float32(r16)/factor) >> 8
ng8 := uint32(float32(g16)/factor) >> 8
nb8 := uint32(float32(b16)/factor) >> 8
return uint8(nr8), uint8(ng8), uint8(nb8)
}
5 changes: 5 additions & 0 deletions lib/util/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package util

import "github.com/inconshreveable/mousetrap"

var StartedByExplorer = mousetrap.StartedByExplorer()

0 comments on commit 09ba580

Please sign in to comment.