Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ Cullergrader is named for being a tool that culls and grades* photos. Please not
4. [Config](#config)
1. [Default Config](#default-config)
2. [Config Settings Explained](#config-settings-explained)
5. [Contributing](#contributing)
6. [License](#license)
5. [Performance Tuning](#performance-tuning)
6. [Contributing](#contributing)
7. [License](#license)

## Features
- 100% free and open-source!
- Experimental RAW image format support - works with most camera RAW files that contain an embedded JPEG preview
- Configurable options for calibrating your perceptual hash
- Hash similarity
- Timestamp difference
Expand All @@ -48,7 +50,7 @@ Cullergrader is named for being a tool that culls and grades* photos. Please not
- Cache options
- Grouping settings
- Dark theme

![images/group_viewer.png](images/group_viewer.png)


Expand Down Expand Up @@ -106,6 +108,9 @@ Best takes can be exported to a folder using `File > Export Best Takes` or with
![images/export_to.png](images/export_to.png)

## Config

Configuration is **optional**. Cullergrader includes sensible defaults for all settings. To customize, create a `config.json` file in the same directory as the executable.

### Default Config
```json
{
Expand All @@ -119,7 +124,8 @@ Best takes can be exported to a folder using `File > Export Best Takes` or with
"HASHED_WIDTH": 8,
"HASHED_HEIGHT": 8,
"TIME_THRESHOLD_SECONDS": 15,
"SIMILARITY_THRESHOLD_PERCENT": 45
"SIMILARITY_THRESHOLD_PERCENT": 45,
"IMAGE_PREVIEW_CACHE_SIZE_MB": 2048
}
```

Expand All @@ -137,8 +143,36 @@ Best takes can be exported to a folder using `File > Export Best Takes` or with
| `HASHED_HEIGHT` | The height that images are computed at before hashing, higher values mean more accurate similarity checks at the cost of performance | `int` |
| `TIME_THRESHOLD_SECONDS` | The default amount of seconds between photos (from the timestamp) before they're counted as a new group. Editable in-app, but will not change the default stored here | `float` |
| `SIMILARITY_THRESHOLD_PERCENT` | The default similarity between two photo hashes before they're counted as a new group. Higher values means more lenience in image similarity (larger groups, less in number). Editable in-app, but will not change the default stored here | `float` |
| `IMAGE_PREVIEW_CACHE_SIZE_MB` | Maximum memory (in megabytes) to use for caching image previews. Default 2048 MB (2 GB). Increase for very large photo shoots (see Performance Tuning section) | `int` |

Note: More config options are technically functional, such as `PLACEHOLDER_THUMBNAIL_PATH`, `KEYBIND_SET_BESTTAKE`, or `GRIDMEDIA_LABEL_TEXT_COLOR`, but are not documented here and aren't editable by default due to their configurability not significantly impacting program function. Users are free to explore the source code and add these into `config.json` themselves, and they should work as intended.

## Performance Tuning

For large photo shoots, you can increase the image preview cache size in `config.json`:

```json
{
"IMAGE_PREVIEW_CACHE_SIZE_MB": 2048
}
```

**Default:** 2048 MB (2 GB)

The preview cache stores scaled thumbnails (240×160) for all image files in memory to avoid re-reading from disk. The cache fills until reaching the configured limit, then stops caching new entries (no eviction).

**Configuration examples:**
- 512 MB = Smaller cache for limited memory systems
- 1024 MB = Moderate cache for typical photo shoots
- 2048 MB = Default (recommended for most users)
- 4096 MB = Very large photo shoots (4000+ files)

The cache applies to all image formats (JPEG, PNG, RAW, etc.) and significantly improves performance when:
- Changing grouping thresholds
- Reloading groups
- Scrolling through large photo sets

Note: More config options are technically functional, such as `PLACEHOLDER_THUMBNAIL_PATH`, `KEYBIND_SET_BESTTAKE`, or `GRIDMEDIA_LABEL_TEXT_COLOR`, but are not documented here and aren't editable by default due to their configurability not significantly impacting program function. Users are free to explore the source code and add these into `config.json` themselves, and they should work as intended.
The cache clears automatically when loading a new directory.

## Contributing
Contributions to Cullergrader are **greatly appreciated**, as a tool made from one photographer to another, the best way Cullergrader can improve is through continued feedback and contributions.
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.18.0</version>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/penguinpush/cullergrader/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.penguinpush.cullergrader.logic.*;
import com.penguinpush.cullergrader.ui.GroupGridFrame;
import com.penguinpush.cullergrader.config.AppConstants;
import com.penguinpush.cullergrader.media.PhotoUtils;
import javax.swing.SwingUtilities;
import com.formdev.flatlaf.FlatIntelliJLaf;
import com.formdev.flatlaf.FlatDarculaLaf;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class AppConstants {
public static final int HASHED_HEIGHT = config.HASHED_HEIGHT;
public static final float TIME_THRESHOLD_SECONDS = config.TIME_THRESHOLD_SECONDS;
public static final float SIMILARITY_THRESHOLD_PERCENT = config.SIMILARITY_THRESHOLD_PERCENT;
public static final int IMAGE_PREVIEW_CACHE_SIZE_MB = config.IMAGE_PREVIEW_CACHE_SIZE_MB;

public static final int MAX_PRIORITY = config.MAX_PRIORITY;
public static final int IMAGE_PRIORITY = config.IMAGE_PRIORITY;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class DefaultAppConstants {
public int HASHED_HEIGHT = 8;
public float TIME_THRESHOLD_SECONDS = 15;
public float SIMILARITY_THRESHOLD_PERCENT = 45;
public int IMAGE_PREVIEW_CACHE_SIZE_MB = 2048; // Default 2048 MB (2 GB)

public int MAX_PRIORITY = 0;
public int IMAGE_PRIORITY = 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,21 @@ public List<Photo> photoListFromFolder(File folder) {
}

public List<PhotoGroup> generateGroups(List<Photo> photoList, float timestampThreshold, float similarityThreshold) {
// Filter out photos with null hashes (RAW files without previews, corrupted files, etc.)
List<Photo> validPhotos = new ArrayList<>();
for (Photo photo : photoList) {
if (photo.getHash() != null) {
validPhotos.add(photo);
} else {
logMessage("Skipping photo with null hash: " + photo.getFile().getName());
}
}

List<PhotoGroup> groups = new ArrayList<>();
PhotoGroup currentGroup = new PhotoGroup();

for (int i = 0; i < photoList.size(); i++) {
Photo current = photoList.get(i);
for (int i = 0; i < validPhotos.size(); i++) {
Photo current = validPhotos.get(i);

if (currentGroup.getSize() == 0) {
current.setIndex(0);
Expand Down Expand Up @@ -69,6 +79,8 @@ public List<PhotoGroup> generateGroups(List<Photo> photoList, float timestampThr
logMessage("added " + current.getFile().getName() + " " + deltaTimeSeconds + " " + hammingDistancePercent + " to group " + groups.size());
}

logMessage("Filtered " + (photoList.size() - validPhotos.size()) + " photos with null hashes");

// add the last group too
if (currentGroup.getSize() > 0) {
currentGroup.setIndex(groups.size());
Expand Down
Loading