Skip to content

Commit c19d331

Browse files
Improve buy/sell detection in sub-optimal conditions
1 parent 82902a8 commit c19d331

File tree

8 files changed

+117
-30
lines changed

8 files changed

+117
-30
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
# SC Trade Companion
2+
## 1.0.1
3+
### Bugs
4+
- Improve buy/sell detection in sub-optimal conditions
5+
6+
### Other
7+
- Add an INFO level logs when listings are sent to the website
8+
29
## 1.0.0
310
### Features
411
- Spellcheck and auto-correct location and commodity names

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
group = 'tools.sctrade'
8-
version = '1.0.0'
8+
version = '1.0.1'
99

1010
java {
1111
sourceCompatibility = '17'

src/main/java/tools/sctrade/companion/domain/commodity/CommodityListingFactory.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public Collection<CommodityListing> build(BufferedImage screenCapture, String lo
5151
return buildCommodityListings(location, transactionType, rawListings, batchId);
5252
} catch (Exception e) {
5353
logger.error("Error while reading listings", e);
54-
54+
// TODO
5555
return Collections.emptyList();
5656
} finally {
5757
listingsOcr.remove();

src/main/java/tools/sctrade/companion/domain/commodity/TransactionTypeExtractor.java

+85-25
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.awt.Rectangle;
44
import java.awt.image.BufferedImage;
5+
import java.util.ArrayList;
56
import java.util.List;
67
import org.slf4j.Logger;
78
import org.slf4j.LoggerFactory;
@@ -10,11 +11,14 @@
1011
import tools.sctrade.companion.domain.ocr.LocatedFragment;
1112
import tools.sctrade.companion.domain.ocr.OcrResult;
1213
import tools.sctrade.companion.domain.ocr.OcrUtil;
14+
import tools.sctrade.companion.exceptions.UnreadableTransactionTypeException;
1315
import tools.sctrade.companion.utils.ImageUtil;
1416

1517
public class TransactionTypeExtractor {
1618
private final Logger logger = LoggerFactory.getLogger(TransactionTypeExtractor.class);
1719
private static final String SHOP_INVENTORY = "shop inventory";
20+
private static final String BUY = "buy";
21+
private static final Object SELL = "sell";
1822

1923
private ImageWriter imageWriter;
2024

@@ -23,12 +27,16 @@ public TransactionTypeExtractor(ImageWriter imageWriter) {
2327
}
2428

2529
public TransactionType extract(BufferedImage screenCapture, OcrResult result) {
26-
screenCapture = cropAroundButtons(screenCapture, result);
30+
screenCapture = ImageUtil.crop(screenCapture, new Rectangle((screenCapture.getWidth() / 2), 0,
31+
(screenCapture.getWidth() - (screenCapture.getWidth() / 2)), screenCapture.getHeight()));
32+
screenCapture = ImageUtil.makeGreyscaleCopy(screenCapture);
33+
var buttonAreaBoundingBox = findButtonAreaBoundingBox(screenCapture, result);
34+
screenCapture = ImageUtil.crop(screenCapture, buttonAreaBoundingBox);
2735
var boundingBoxes = getButtonBoundingBoxes(screenCapture);
2836

2937
boundingBoxes
3038
.sort((n, m) -> Double.compare(m.getWidth() * m.getHeight(), n.getWidth() * n.getHeight()));
31-
var boundingBoxesOrderedByX = boundingBoxes.subList(0, 2);
39+
var boundingBoxesOrderedByX = new ArrayList<>(boundingBoxes.subList(0, 2));
3240
boundingBoxesOrderedByX.sort((n, m) -> Double.compare(n.getMinX(), m.getMinX()));
3341

3442
Rectangle buyRectangle = boundingBoxesOrderedByX.get(0);
@@ -39,29 +47,33 @@ public TransactionType extract(BufferedImage screenCapture, OcrResult result) {
3947
var sellImage = ImageUtil.crop(screenCapture, sellRectangle);
4048
imageWriter.write(sellImage, ImageType.SELL_BUTTON);
4149

42-
double buyButtonAreaOverSellButtonArea = (buyRectangle.getWidth() * buyRectangle.getHeight())
43-
/ (sellRectangle.getWidth() * sellRectangle.getHeight());
44-
45-
if (Math.abs(1 - buyButtonAreaOverSellButtonArea) >= 0.2) {
46-
logger.warn("Detected buttons are not the same size: one button may have not been captured");
47-
return (buyButtonAreaOverSellButtonArea > 1) ? TransactionType.SELLS : TransactionType.BUYS;
48-
} else {
50+
if (buttonsAreAligned(buyRectangle, sellRectangle)) {
4951
return extractByLuminance(buyImage, sellImage);
52+
} else {
53+
logger.warn("Detected buttons are not aligned: one button may have not been captured");
54+
var buttonBoundingBox = getNormalizedButtonBoundingBox(buttonAreaBoundingBox, boundingBoxes);
55+
56+
return extractByStringPosition(result, buttonBoundingBox);
5057
}
5158
}
5259

53-
private BufferedImage cropAroundButtons(BufferedImage screenCapture, OcrResult result) {
60+
private Rectangle findButtonAreaBoundingBox(BufferedImage screenCapture, OcrResult result) {
5461
Rectangle shopInventoryRectangle = getShopInventoryRectangle(result);
55-
screenCapture = ImageUtil.crop(screenCapture, new Rectangle((screenCapture.getWidth() / 2), 0,
56-
(screenCapture.getWidth() - (screenCapture.getWidth() / 2)), screenCapture.getHeight()));
57-
Rectangle buttonsAreaRectangle = new Rectangle((int) shopInventoryRectangle.getMinX(),
62+
Rectangle buttonAreaBoundingBox = new Rectangle((int) shopInventoryRectangle.getMinX(),
5863
(int) (shopInventoryRectangle.getMinY() - shopInventoryRectangle.getHeight()),
5964
(int) (shopInventoryRectangle.getWidth() * 3),
6065
(int) (shopInventoryRectangle.getHeight() * 5));
61-
screenCapture = ImageUtil.crop(screenCapture, buttonsAreaRectangle);
62-
screenCapture = ImageUtil.makeGreyscaleCopy(screenCapture);
6366

64-
return screenCapture;
67+
return buttonAreaBoundingBox;
68+
}
69+
70+
private Rectangle getShopInventoryRectangle(OcrResult result) {
71+
var fragments = result.getColumns().parallelStream()
72+
.flatMap(n -> n.getFragments().parallelStream()).toList();
73+
LocatedFragment shopInventoryFragment =
74+
OcrUtil.findFragmentClosestTo(fragments, SHOP_INVENTORY);
75+
76+
return shopInventoryFragment.getBoundingBox();
6577
}
6678

6779
private List<Rectangle> getButtonBoundingBoxes(BufferedImage screenCapture) {
@@ -73,6 +85,63 @@ private List<Rectangle> getButtonBoundingBoxes(BufferedImage screenCapture) {
7385
return boundingBoxes;
7486
}
7587

88+
private boolean buttonsAreAligned(Rectangle buyRectangle, Rectangle sellRectangle) {
89+
var yOverlap1 = sellRectangle.getMaxY() - buyRectangle.getMinY();
90+
var yOverlap2 = buyRectangle.getMaxY() - sellRectangle.getMinY();
91+
var yOverlap = Math.min(yOverlap1, yOverlap2);
92+
var yHeight = Math.min(buyRectangle.getHeight(), sellRectangle.getHeight());
93+
94+
return (yOverlap / yHeight) > 0.66;
95+
}
96+
97+
private Rectangle getNormalizedButtonBoundingBox(Rectangle buttonAreaBoundingBox,
98+
List<Rectangle> boundingBoxes) {
99+
var buttonBoundingBox = boundingBoxes.iterator().next();
100+
buttonBoundingBox.setLocation(buttonAreaBoundingBox.x + buttonBoundingBox.x,
101+
buttonAreaBoundingBox.y + buttonBoundingBox.y);
102+
103+
return buttonBoundingBox;
104+
}
105+
106+
private TransactionType extractByStringPosition(OcrResult result, Rectangle buttonBoundingBox) {
107+
var buttonFragments =
108+
result.getColumns().parallelStream().flatMap(n -> n.getFragments().stream())
109+
.filter(n -> n.getBoundingBox().intersectsLine(Double.MIN_VALUE,
110+
buttonBoundingBox.getCenterY(), Double.MAX_VALUE, buttonBoundingBox.getCenterY()))
111+
.toList();
112+
113+
var buyFragment =
114+
buttonFragments.parallelStream().filter(n -> n.getText().equals(BUY)).findFirst();
115+
116+
if (buyFragment.isPresent()) {
117+
if (buttonBoundingBox.contains(buyFragment.get().getBoundingBox())) {
118+
return TransactionType.SELLS;
119+
}
120+
121+
if (buyFragment.get().getBoundingBox().getMaxX() < buttonBoundingBox.getMinX()) {
122+
// buttonBoundingBox is the SELL button
123+
return TransactionType.BUYS;
124+
}
125+
}
126+
127+
var sellFragment =
128+
buttonFragments.parallelStream().filter(n -> n.getText().equals(SELL)).findFirst();
129+
130+
if (sellFragment.isPresent()) {
131+
if (buttonBoundingBox.contains(sellFragment.get().getBoundingBox())) {
132+
return TransactionType.BUYS;
133+
}
134+
135+
if (buttonBoundingBox.getMaxX() < sellFragment.get().getBoundingBox().getMinX()) {
136+
// buttonBoundingBox is the BUY button
137+
return TransactionType.SELLS;
138+
}
139+
}
140+
141+
logger.warn("Could not detect transaction type");
142+
throw new UnreadableTransactionTypeException();
143+
}
144+
76145
private TransactionType extractByLuminance(BufferedImage buyImage, BufferedImage sellImage) {
77146
var buyRectangleColor = ImageUtil.calculateDominantColor(buyImage);
78147
var buyRectangleLuminance = buyRectangleColor.getRed();
@@ -83,13 +152,4 @@ private TransactionType extractByLuminance(BufferedImage buyImage, BufferedImage
83152
return (buyRectangleLuminance > sellRectangleLuminance) ? TransactionType.SELLS
84153
: TransactionType.BUYS;
85154
}
86-
87-
private Rectangle getShopInventoryRectangle(OcrResult result) {
88-
var fragments = result.getColumns().parallelStream()
89-
.flatMap(n -> n.getFragments().parallelStream()).toList();
90-
LocatedFragment shopInventoryFragment =
91-
OcrUtil.findFragmentClosestTo(fragments, SHOP_INVENTORY);
92-
93-
return shopInventoryFragment.getBoundingBox();
94-
}
95155
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package tools.sctrade.companion.exceptions;
2+
3+
import tools.sctrade.companion.utils.LocalizationUtil;
4+
5+
public class UnreadableTransactionTypeException extends RuntimeException {
6+
private static final long serialVersionUID = 82552490134110748L;
7+
8+
public UnreadableTransactionTypeException() {
9+
super(LocalizationUtil.get("errorUnreadableTransactionType"));
10+
}
11+
}

src/main/java/tools/sctrade/companion/output/commodity/ScTradeToolsClient.java

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import tools.sctrade.companion.domain.user.SettingRepository;
2222
import tools.sctrade.companion.exceptions.PublicationException;
2323
import tools.sctrade.companion.utils.AsynchronousProcessor;
24+
import tools.sctrade.companion.utils.LocalizationUtil;
2425

2526
public class ScTradeToolsClient extends AsynchronousProcessor<CommoditySubmission>
2627
implements CommodityRepository, LocationRepository {
@@ -86,6 +87,9 @@ public void process(CommoditySubmission submission) {
8687
.toBodilessEntity();
8788
response.block();
8889
logger.info("Sent {} commodity listings to SC Trade Tools", submission.getListings().size());
90+
notificationService.info(String.format(Locale.ROOT,
91+
LocalizationUtil.get("infoCommodityListingsScTradeToolsOutput"),
92+
submission.getListings().size()));
8993
} catch (WebClientResponseException e) {
9094
throw new PublicationException(e.getResponseBodyAsString());
9195
}

src/main/java/tools/sctrade/companion/utils/ImageUtil.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.opencv.core.Mat;
2626
import org.opencv.core.MatOfByte;
2727
import org.opencv.core.MatOfPoint;
28+
import org.opencv.core.Size;
2829
import org.opencv.imgcodecs.Imgcodecs;
2930
import org.opencv.imgproc.Imgproc;
3031
import org.slf4j.Logger;
@@ -179,9 +180,11 @@ public static BufferedImage applyOtsuBinarization(BufferedImage image) {
179180

180181
try {
181182
Mat original = toMat(image);
183+
Mat filtered = new Mat(original.rows(), original.cols(), original.type());
182184
Mat processed = new Mat(original.rows(), original.cols(), original.type());
183185

184-
Imgproc.threshold(original, processed, Imgproc.THRESH_BINARY, 255, Imgproc.THRESH_OTSU);
186+
Imgproc.GaussianBlur(original, filtered, new Size(5, 5), 0);
187+
Imgproc.threshold(filtered, processed, Imgproc.THRESH_BINARY, 255, Imgproc.THRESH_OTSU);
185188

186189
return toBufferedImage(processed);
187190
} catch (IOException e) {

src/main/resources/bundles/localization.properties

+4-2
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ tableColumnType = Type
1717
tableColumnMessage = Message
1818
infoProcessingScreenshot = Processing screenshot...
1919
infoCommodityListingsCsvOutput = Wrote %d commodity listings to '%s'
20+
infoCommodityListingsScTradeToolsOutput = Sent %d commodity listings to SC Trade Tools
2021
infoCommodityListingsRead = Read commodity listings
2122
warnNoLocation = Select the current shop/city name in the "Your inventories" dropdown
2223
errorNotEnoughColumns = [Code 1] Could not make out %d or more column(s) of text in:%n%s
2324
errorRectangleOutOfBounds = [Code 2] Rectangle '%s' is not within '%s'
2425
errorLocationNotFound = [Code 3] Could not extract location from: %s
2526
errorNoCloseString = [Code 4] Could not find string resembling '%s'
26-
errorNoListings = [Code 5] No listings were found in the image
27-
errorNoLocation = [Code 6] The shop/city name was never selected in the "Your inventories" dropdown
27+
errorNoListings = [Code 5] Listings could not be read
28+
errorNoLocation = [Code 6] The shop/city name was never selected in the "Your inventories" dropdown
29+
errorUnreadableTransactionType [Code 7] Could not find the buy & sell buttons

0 commit comments

Comments
 (0)