Skip to content

Commit

Permalink
Added sides picking and folder processing (#2)
Browse files Browse the repository at this point in the history
* Added sides feature

New sides flag allows user to get only the sides they need. This flag is optional

* Added folder processing mode

Folder processing mode allows user to process a folder with all images inside.
Also had to add a lib uilive to update terminal output dinamically

* Added checks for both paths filled and changed style of path picking
  • Loading branch information
thegalkin committed Jul 22, 2024
1 parent 3985765 commit 5325836
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 48 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ Usage:

Flags:
-h, --help help for panorama
-i, --in string in image file path (required)
-i, --in string input image file path (required if --indir is not specified)
-d, --indir string input directory path (required if --in is not specified)
-l, --len int edge length of a cube face (default 1024)
-o, --out string out file dir path (default ".")
-s, --sides array list of sides splited by "," (optional)
```
``` sh
# example
./panorama --in ./sample_image.jpg --out ./dist --len 512
./panorama --in ./sample_image.jpg --out ./dist --len 512 --sides left,right,top,buttom,front,back
```
### Installation
Expand Down
193 changes: 173 additions & 20 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,191 @@ package cmd

import (
"fmt"
"image"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/briandowns/spinner"
"github.com/gosuri/uilive"
"github.com/spf13/cobra"

"github.com/blackironj/panorama/conv"
)

const defaultEdgeLen = 1024
const (
defaultEdgeLen = 1024
maxConcurrentFiles = 10 // Adjust this number based on your system's file descriptor limit
)

var (
inFilePath string
outFileDir string
inDirPath string
edgeLen int
sides []string

validSides = []string{"front", "back", "left", "right", "top", "bottom"}
semaphore = make(chan struct{}, maxConcurrentFiles)
progress = struct {
sync.Mutex
totalFiles int
processedFiles int
startTime time.Time
errors []string
}{}
rootCmd = &cobra.Command{
Use: "panorama",
Short: "convert equirectangular panorama img to Cubemap img",
Run: func(cmd *cobra.Command, args []string) {
if inFilePath == "" {
er("Need a image for converting")
if inFilePath == "" && inDirPath == "" {
er("Need an input image file path or input directory")
}

fmt.Println("Read a image...")
inImage, ext, err := conv.ReadImage(inFilePath)
if err != nil {
er(err)
if len(inFilePath) > 0 && len(inDirPath) > 0 {
er("Need only one path, not both")
}

s := spinner.New(spinner.CharSets[33], 100*time.Millisecond)
s.FinalMSG = "Complete converting!\n"
s.Prefix = "Converting..."

s.Start()
canvases := conv.ConvertEquirectangularToCubeMap(edgeLen, inImage)
s.Stop()
progress.startTime = time.Now()

fmt.Println("Write images...")
if err := conv.WriteImage(canvases, outFileDir, ext); err != nil {
er(err)
if inFilePath != "" {
progress.totalFiles = 1
processSingleImage(inFilePath, outFileDir)
} else {
processDirectory(inDirPath, outFileDir)
}
},
}
)

func init() {
rootCmd.Flags().StringVarP(&inFilePath, "in", "i", "", "in image file path (required)")
rootCmd.Flags().StringVarP(&outFileDir, "out", "o", ".", "out file dir path")
rootCmd.Flags().StringVarP(&inFilePath, "in", "i", "", "input image file path (required if --indir is not specified)")
rootCmd.Flags().StringVarP(&inDirPath, "indir", "d", "", "input directory path (required if --in is not specified)")
rootCmd.Flags().StringVarP(&outFileDir, "out", "o", ".", "output file directory path")
rootCmd.Flags().IntVarP(&edgeLen, "len", "l", defaultEdgeLen, "edge length of a cube face")
rootCmd.Flags().StringSliceVarP(&sides, "sides", "s", []string{}, "array of sides [front,back,left,right,top,bottom] (default: all sides)")
}

func processSingleImage(inPath, outDir string) {
semaphore <- struct{}{} // Acquire a semaphore
defer func() { <-semaphore }() // Release the semaphore when done

inImage, ext, err := conv.ReadImage(inPath)
if err != nil {
progress.Lock()
progress.errors = append(progress.errors, fmt.Sprintf("Error reading image %s: %v", inPath, err))
progress.Unlock()
return
}

if len(sides) == 0 {
sides = validSides
} else {
for _, side := range sides {
if !isValidSide(side) {
er(fmt.Sprintf("Invalid side specified: %s. Valid sides are %v", side, validSides))
}
}
}

var s *spinner.Spinner
if inFilePath != "" {
s = spinner.New(spinner.CharSets[33], 100*time.Millisecond)
s.FinalMSG = "Complete converting!\n"
s.Prefix = "Converting..."
s.Start()
}

canvases, err := safeConvertEquirectangularToCubeMap(edgeLen, inImage, sides)

if inFilePath != "" {
s.Stop()
}

if err != nil {
progress.Lock()
progress.errors = append(progress.errors, fmt.Sprintf("Error converting image %s: %v", inPath, err))
progress.Unlock()
return
}

outSubDir := filepath.Join(outDir, strings.TrimSuffix(filepath.Base(inPath), filepath.Ext(inPath)))
if err := conv.WriteImage(canvases, outSubDir, ext, sides); err != nil {
progress.Lock()
progress.errors = append(progress.errors, fmt.Sprintf("Error writing images for %s: %v", inPath, err))
progress.Unlock()
return
}

progress.Lock()
progress.processedFiles++
progress.Unlock()
}

func processDirectory(inDir, outDir string) {
files, err := os.ReadDir(inDir)
if err != nil {
er(err)
}

progress.totalFiles = len(files)

writer := uilive.New()
writer.Start()
defer writer.Stop()

var wg sync.WaitGroup
for _, file := range files {
if !file.IsDir() && isImageFile(file) {
wg.Add(1)
go func(file fs.DirEntry) {
defer wg.Done()
inPath := filepath.Join(inDir, file.Name())
processSingleImage(inPath, outDir)
updateProgress(writer)
}(file)
}
}

go func() {
for {
time.Sleep(1 * time.Second)
updateProgress(writer)
progress.Lock()
remaining := progress.totalFiles - progress.processedFiles
if remaining <= 0 {
progress.Unlock()
break
}
progress.Unlock()
}
}()

wg.Wait()
updateProgress(writer)
if len(progress.errors) > 0 {
fmt.Println("\nErrors:")
for _, err := range progress.errors {
fmt.Println(err)
}
}
fmt.Println("\nProcessing complete.")
}

func updateProgress(writer *uilive.Writer) {
progress.Lock()
defer progress.Unlock()
remaining := progress.totalFiles - progress.processedFiles
elapsed := time.Since(progress.startTime).Seconds()
eta := float64(remaining) / (float64(progress.processedFiles) / elapsed)
fmt.Fprintf(writer, "Progress: %d/%d files processed. ETA: %.2f seconds. IT/S: %.2f\n", progress.processedFiles, progress.totalFiles, eta, float64(progress.processedFiles)/elapsed)
}

func isImageFile(file fs.DirEntry) bool {
ext := strings.ToLower(filepath.Ext(file.Name()))
return ext == ".jpg" || ext == ".jpeg" || ext == ".png"
}

func er(msg interface{}) {
Expand All @@ -64,3 +199,21 @@ func Execute() {
er(err)
}
}

func isValidSide(side string) bool {
for _, s := range validSides {
if s == side {
return true
}
}
return false
}

func safeConvertEquirectangularToCubeMap(edgeLen int, imgIn image.Image, sides []string) ([]*image.RGBA, error) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered in safeConvertEquirectangularToCubeMap: %v\n", r)
}
}()
return conv.ConvertEquirectangularToCubeMap(edgeLen, imgIn, sides), nil
}
36 changes: 21 additions & 15 deletions conv/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Pi_2 = math.Pi / 2.0
type Number interface {
uint32 | float64
}

type Vec3[T Number] struct {
X, Y, Z T
}
Expand Down Expand Up @@ -82,41 +83,46 @@ func interpolateXYZtoColor(xyz Vec3[float64], imgIn image.Image, sw, sh int) Vec
}
}

func ConvertEquirectangularToCubeMap(rValue int, imgIn image.Image) []*image.RGBA {
func ConvertEquirectangularToCubeMap(rValue int, imgIn image.Image, sides []string) []*image.RGBA {
sw := imgIn.Bounds().Max.X
sh := imgIn.Bounds().Max.Y
sidesCount := len(sides)
var sidesInt []int

for i := 0; i < sidesCount; i++ {
sidesInt = append(sidesInt, revesedFaceMap[sides[i]])
}
var wg sync.WaitGroup

canvases := make([]*image.RGBA, 6)
for i := 0; i < 6; i++ {
wg.Add(1)
canvases := make([]*image.RGBA, sidesCount)
for i := 0; i < sidesCount; i++ {
canvases[i] = image.NewRGBA(image.Rect(0, 0, rValue, rValue))
start := i * rValue
end := start + rValue
}

go func() {
for i := 0; i < sidesCount; i++ {
wg.Add(1)
side := sidesInt[i]
canvas := canvases[i]

go func(side int, canvas *image.RGBA) {
defer wg.Done()
convert(start, end, rValue, sw, sh, imgIn, canvases)
}()
convert(rValue, side, sw, sh, imgIn, canvas)
}(side, canvas)
}
wg.Wait()

return canvases
}

func convert(start, end, edge, sw, sh int, imgIn image.Image, imgOut []*image.RGBA) {
func convert(edge, face, sw, sh int, imgIn image.Image, imgOut *image.RGBA) {
inLen := 2.0 / float64(edge)

for k := start; k < end; k++ {
face := k / edge
i := k % edge

for i := 0; i < edge; i++ {
for j := 0; j < edge; j++ {
xyz := outImgToXYZ(i, j, face, edge, inLen)
clr := interpolateXYZtoColor(xyz, imgIn, sw, sh)

imgOut[face].Set(i, j, color.RGBA{uint8(clr.X), uint8(clr.Y), uint8(clr.Z), 255})
imgOut.Set(i, j, color.RGBA{uint8(clr.X), uint8(clr.Y), uint8(clr.Z), 255})
}
}
}
Expand Down
Loading

0 comments on commit 5325836

Please sign in to comment.